diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 774d2773..68be3e70 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,4 @@ -> Tip: If this PR is not related to the coding or code translation, please ignore the checklist. - -### Checklist +If this PR is related to coding or code translation, please fill out the checklist. - [ ] I've tested the code and ensured the outputs are the same as the outputs of reference codes. - [ ] I've checked the codes (formatting, comments, indentation, file header, etc) carefully. diff --git a/.gitignore b/.gitignore index 70b2f49f..3ff30549 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ docs/overrides/ # python files __pycache__ + +# iml +hello-algo.iml diff --git a/README.md b/README.md index 6a6e165d..560e7395 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ - 开源免费,所有同学都可在网上获取本书; - 新手友好,适合算法初学者自主学习入门; - 动画讲解,尽可能地保证平滑的学习曲线; -- 代码导向,提供精简、可运行的算法代码; +- 代码导向,提供可一键运行的算法源代码; - 讨论学习,提问一般能在三日内得到回复; 如果感觉本书对你有所帮助,请点个 Star :star: 支持一下,谢谢! diff --git a/codes/c/chapter_sorting/bubble_sort.c b/codes/c/chapter_sorting/bubble_sort.c new file mode 100644 index 00000000..6b67bd72 --- /dev/null +++ b/codes/c/chapter_sorting/bubble_sort.c @@ -0,0 +1,72 @@ +/** + * File: bubble_sort.c + * Created Time: 2022-12-26 + * Author: Listening (https://github.com/L-Super) + */ + +#include "../include/include.h" + +/* 冒泡排序 */ +void bubble_sort(int nums[], int size) +{ + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = 0; i < size - 1; i++) + { + // 内循环:冒泡操作 + for (int j = 0; j < size - 1 - i; j++) + { + if (nums[j] > nums[j + 1]) + { + int temp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = temp; + } + } + } +} + +/* 冒泡排序(标志优化)*/ +void bubble_sort_with_flag(int nums[], int size) +{ + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = 0; i < size - 1; i++) + { + bool flag = false; + // 内循环:冒泡操作 + for (int j = 0; j < size - 1 - i; j++) + { + if (nums[j] > nums[j + 1]) + { + int temp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = temp; + flag = true; + } + } + if (!flag) + break; + } +} + + +/* Driver Code */ +int main() +{ + int nums[6] = {4, 1, 3, 1, 5, 2}; + printf("冒泡排序后:\n"); + bubble_sort(nums, 6); + for (int i = 0; i < 6; i++) + { + printf("%d ", nums[i]); + } + + printf("优化版冒泡排序后:\n"); + bubble_sort_with_flag(nums, 6); + for (int i = 0; i < 6; i++) + { + printf("%d ", nums[i]); + } + printf("\n"); + + return 0; +} \ No newline at end of file diff --git a/codes/c/include/include.h b/codes/c/include/include.h new file mode 100644 index 00000000..44843254 --- /dev/null +++ b/codes/c/include/include.h @@ -0,0 +1,2 @@ +#include +#include \ No newline at end of file diff --git a/codes/cpp/chapter_sorting/bubble_sort.cpp b/codes/cpp/chapter_sorting/bubble_sort.cpp index 07182745..87ca305d 100644 --- a/codes/cpp/chapter_sorting/bubble_sort.cpp +++ b/codes/cpp/chapter_sorting/bubble_sort.cpp @@ -14,9 +14,8 @@ void bubbleSort(vector& nums) { for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // 交换 nums[j] 与 nums[j + 1] - int tmp = nums[j]; - nums[j] = nums[j + 1]; - nums[j + 1] = tmp; + // 这里使用了 std::swap() 函数 + swap(nums[j], nums[j + 1]); } } } @@ -31,9 +30,8 @@ void bubbleSortWithFlag(vector& nums) { for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // 交换 nums[j] 与 nums[j + 1] - int tmp = nums[j]; - nums[j] = nums[j + 1]; - nums[j + 1] = tmp; + // 这里使用了 std::swap() 函数 + swap(nums[j], nums[j + 1]); flag = true; // 记录交换元素 } } diff --git a/codes/cpp/chapter_sorting/merge_sort.cpp b/codes/cpp/chapter_sorting/merge_sort.cpp index 465d0def..7c9a41c8 100644 --- a/codes/cpp/chapter_sorting/merge_sort.cpp +++ b/codes/cpp/chapter_sorting/merge_sort.cpp @@ -22,13 +22,13 @@ void merge(vector& nums, int left, int mid, int right) { int i = leftStart, j = rightStart; // 通过覆盖原数组 nums 来合并左子数组和右子数组 for (int k = left; k <= right; k++) { - // 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ if (i > leftEnd) nums[k] = tmp[j++]; - // 否则,若 “右子数组已全部合并完” 或 “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ + // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ else if (j > rightEnd || tmp[i] <= tmp[j]) nums[k] = tmp[i++]; - // 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ else nums[k] = tmp[j++]; } diff --git a/codes/cpp/chapter_stack_and_queue/array_queue.cpp b/codes/cpp/chapter_stack_and_queue/array_queue.cpp index 14df4abd..387ffd14 100644 --- a/codes/cpp/chapter_stack_and_queue/array_queue.cpp +++ b/codes/cpp/chapter_stack_and_queue/array_queue.cpp @@ -64,13 +64,6 @@ public: return nums[front]; } - /* 访问指定索引元素 */ - int get(int index) { - if (index >= size()) - throw out_of_range("索引越界"); - return nums[(front + index) % capacity()]; - } - /* 将数组转化为 Vector 并返回 */ vector toVector() { int siz = size(); @@ -103,11 +96,7 @@ int main() { /* 访问队首元素 */ int peek = queue->peek(); cout << "队首元素 peek = " << peek << endl; - - /* 访问指定索引元素 */ - int num = queue->get(2); - cout << "队列第 3 个元素为 num = " << num << endl; - + /* 元素出队 */ int poll = queue->poll(); cout << "出队元素 poll = " << poll << ",出队后 queue = "; diff --git a/codes/cpp/chapter_stack_and_queue/array_stack.cpp b/codes/cpp/chapter_stack_and_queue/array_stack.cpp index fe5dc311..517a18a4 100644 --- a/codes/cpp/chapter_stack_and_queue/array_stack.cpp +++ b/codes/cpp/chapter_stack_and_queue/array_stack.cpp @@ -41,13 +41,6 @@ public: return stack.back(); } - /* 访问索引 index 处元素 */ - int get(int index) { - if(index >= size()) - throw out_of_range("索引越界"); - return stack[index]; - } - /* 返回 Vector */ vector toVector() { return stack; @@ -73,10 +66,6 @@ int main() { int top = stack->top(); cout << "栈顶元素 top = " << top << endl; - /* 访问索引 index 处元素 */ - int num = stack->get(3); - cout << "栈索引 3 处的元素为 num = " << num << endl; - /* 元素出栈 */ int pop = stack->pop(); cout << "出栈元素 pop = " << pop << ",出栈后 stack = "; diff --git a/codes/cpp/chapter_tree/binary_tree_bfs.cpp b/codes/cpp/chapter_tree/binary_tree_bfs.cpp index eeec2a18..30c2d600 100644 --- a/codes/cpp/chapter_tree/binary_tree_bfs.cpp +++ b/codes/cpp/chapter_tree/binary_tree_bfs.cpp @@ -15,12 +15,12 @@ vector hierOrder(TreeNode* root) { vector vec; while (!queue.empty()) { TreeNode* node = queue.front(); - queue.pop(); // 队列出队 - vec.push_back(node->val); // 保存结点 + queue.pop(); // 队列出队 + vec.push_back(node->val); // 保存结点 if (node->left != nullptr) - queue.push(node->left); // 左子结点入队 + queue.push(node->left); // 左子结点入队 if (node->right != nullptr) - queue.push(node->right); // 右子结点入队 + queue.push(node->right); // 右子结点入队 } return vec; } diff --git a/codes/csharp/chapter_array_and_linkedlist/Array.cs b/codes/csharp/chapter_array_and_linkedlist/array.cs similarity index 99% rename from codes/csharp/chapter_array_and_linkedlist/Array.cs rename to codes/csharp/chapter_array_and_linkedlist/array.cs index cc6b79dd..95479574 100644 --- a/codes/csharp/chapter_array_and_linkedlist/Array.cs +++ b/codes/csharp/chapter_array_and_linkedlist/array.cs @@ -1,4 +1,4 @@ -// File: Array.cs +// File: array.cs // Created Time: 2022-12-14 // Author: mingXta (1195669834@qq.com) @@ -134,4 +134,4 @@ namespace hello_algo.chapter_array_and_linkedlist Console.WriteLine("在 nums 中查找元素 3 ,得到索引 = " + index); } } -} \ No newline at end of file +} diff --git a/codes/csharp/chapter_array_and_linkedlist/LinkedList.cs b/codes/csharp/chapter_array_and_linkedlist/linked_list.cs similarity index 90% rename from codes/csharp/chapter_array_and_linkedlist/LinkedList.cs rename to codes/csharp/chapter_array_and_linkedlist/linked_list.cs index 46b87a62..d700a5ae 100644 --- a/codes/csharp/chapter_array_and_linkedlist/LinkedList.cs +++ b/codes/csharp/chapter_array_and_linkedlist/linked_list.cs @@ -1,4 +1,4 @@ -// File: LinkedList.cs +// File: linked_list.cs // Created Time: 2022-12-16 // Author: mingXta (1195669834@qq.com) @@ -7,14 +7,14 @@ using NUnit.Framework; namespace hello_algo.chapter_array_and_linkedlist { - public class LinkedList + public class linked_list { /// /// 在链表的结点 n0 之后插入结点 P /// public static void Insert(ListNode n0, ListNode P) { - ListNode n1 = n0.next; + ListNode? n1 = n0.next; n0.next = P; P.next = n1; } @@ -28,14 +28,14 @@ namespace hello_algo.chapter_array_and_linkedlist return; // n0 -> P -> n1 ListNode P = n0.next; - ListNode n1 = P.next; + ListNode? n1 = P.next; n0.next = n1; } /// /// 访问链表中索引为 index 的结点 /// - public static ListNode Access(ListNode head, int index) + public static ListNode? Access(ListNode head, int index) { for (int i = 0; i < index; i++) { @@ -89,12 +89,12 @@ namespace hello_algo.chapter_array_and_linkedlist Console.WriteLine($"删除结点后的链表为{n0}"); // 访问结点 - ListNode node = Access(n0, 3); - Console.WriteLine($"链表中索引 3 处的结点的值 = {node.val}"); + ListNode? node = Access(n0, 3); + Console.WriteLine($"链表中索引 3 处的结点的值 = {node?.val}"); // 查找结点 int index = Find(n0, 2); Console.WriteLine($"链表中值为 2 的结点的索引 = {index}"); } } -} \ No newline at end of file +} diff --git a/codes/csharp/chapter_array_and_linkedlist/list.cs b/codes/csharp/chapter_array_and_linkedlist/list.cs new file mode 100644 index 00000000..4f702163 --- /dev/null +++ b/codes/csharp/chapter_array_and_linkedlist/list.cs @@ -0,0 +1,75 @@ +/** + * File: list.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using NUnit.Framework; + +namespace hello_algo.chapter_array_and_linkedlist +{ + public class list + { + [Test] + public void Test() + { + + /* 初始化列表 */ + // 注意数组的元素类型是 int[] 的包装类 int[] + int[] numbers = new int[] { 1, 3, 2, 5, 4 }; + List list = numbers.ToList(); + Console.WriteLine("列表 list = " + string.Join(",",list)); + + /* 访问元素 */ + int num = list[1]; + Console.WriteLine("访问索引 1 处的元素,得到 num = " + num); + + /* 更新元素 */ + list[1] = 0; + Console.WriteLine("将索引 1 处的元素更新为 0 ,得到 list = " + string.Join(",", list)); + + /* 清空列表 */ + list.Clear(); + Console.WriteLine("清空列表后 list = " + string.Join(",", list)); + + /* 尾部添加元素 */ + list.Add(1); + list.Add(3); + list.Add(2); + list.Add(5); + list.Add(4); + Console.WriteLine("添加元素后 list = " + string.Join(",", list)); + + /* 中间插入元素 */ + list.Insert(3, 6); + Console.WriteLine("在索引 3 处插入数字 6 ,得到 list = " + string.Join(",", list)); + + /* 删除元素 */ + list.RemoveAt(3); + Console.WriteLine("删除索引 3 处的元素,得到 list = " + string.Join(",", list)); + + /* 通过索引遍历列表 */ + int count = 0; + for (int i = 0; i < list.Count(); i++) + { + count++; + } + + /* 直接遍历列表元素 */ + count = 0; + foreach (int n in list) + { + count++; + } + + /* 拼接两个列表 */ + List list1 = new() { 6, 8, 7, 10, 9 }; + list.AddRange(list1); + Console.WriteLine("将列表 list1 拼接到 list 之后,得到 list = " + string.Join(",", list)); + + /* 排序列表 */ + list.Sort(); // 排序后,列表元素从小到大排列 + Console.WriteLine("排序列表后 list = " + string.Join(",", list)); + } + } +} diff --git a/codes/csharp/chapter_array_and_linkedlist/my_list.cs b/codes/csharp/chapter_array_and_linkedlist/my_list.cs new file mode 100644 index 00000000..32718062 --- /dev/null +++ b/codes/csharp/chapter_array_and_linkedlist/my_list.cs @@ -0,0 +1,164 @@ +/** + * File: my_list.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using NUnit.Framework; + +namespace hello_algo.chapter_array_and_linkedlist +{ + class MyList + { + private int[] nums; // 数组(存储列表元素) + private int capacity = 10; // 列表容量 + private int size = 0; // 列表长度(即当前元素数量) + private int extendRatio = 2; // 每次列表扩容的倍数 + + /* 构造函数 */ + public MyList() + { + nums = new int[capacity]; + } + + /* 获取列表长度(即当前元素数量)*/ + public int Size() + { + return size; + } + + /* 获取列表容量 */ + public int Capacity() + { + return capacity; + } + + /* 访问元素 */ + public int Get(int index) + { + // 索引如果越界则抛出异常,下同 + if (index >= size) + throw new IndexOutOfRangeException("索引越界"); + return nums[index]; + } + + /* 更新元素 */ + public void Set(int index, int num) + { + if (index >= size) + throw new IndexOutOfRangeException("索引越界"); + nums[index] = num; + } + + /* 尾部添加元素 */ + public void Add(int num) + { + // 元素数量超出容量时,触发扩容机制 + if (size == Capacity()) + ExtendCapacity(); + nums[size] = num; + // 更新元素数量 + size++; + } + + /* 中间插入元素 */ + public void Insert(int index, int num) + { + if (index >= size) + throw new IndexOutOfRangeException("索引越界"); + // 元素数量超出容量时,触发扩容机制 + if (size == Capacity()) + ExtendCapacity(); + // 将索引 index 以及之后的元素都向后移动一位 + for (int j = size - 1; j >= index; j--) + { + nums[j + 1] = nums[j]; + } + nums[index] = num; + // 更新元素数量 + size++; + } + + /* 删除元素 */ + public int Remove(int index) + { + if (index >= size) + throw new IndexOutOfRangeException("索引越界"); + int num = nums[index]; + // 将索引 index 之后的元素都向前移动一位 + for (int j = index; j < size - 1; j++) + { + nums[j] = nums[j + 1]; + } + // 更新元素数量 + size--; + // 返回被删除元素 + return num; + } + + /* 列表扩容 */ + public void ExtendCapacity() + { + // 新建一个长度为 size 的数组,并将原数组拷贝到新数组 + System.Array.Resize(ref nums, Capacity() * extendRatio); + // 更新列表容量 + capacity = nums.Length; + } + + /* 将列表转换为数组 */ + public int[] ToArray() + { + int size = Size(); + // 仅转换有效长度范围内的列表元素 + int[] nums = new int[size]; + for (int i = 0; i < size; i++) + { + nums[i] = Get(i); + } + return nums; + } + } + + public class my_list + { + [Test] + public void Test() + { + /* 初始化列表 */ + MyList list = new MyList(); + /* 尾部添加元素 */ + list.Add(1); + list.Add(3); + list.Add(2); + list.Add(5); + list.Add(4); + Console.WriteLine("列表 list = " + string.Join(",", list.ToArray()) + + " ,容量 = " + list.Capacity() + " ,长度 = " + list.Size()); + + /* 中间插入元素 */ + list.Insert(3, 6); + Console.WriteLine("在索引 3 处插入数字 6 ,得到 list = " + string.Join(",", list.ToArray())); + + /* 删除元素 */ + list.Remove(3); + Console.WriteLine("删除索引 3 处的元素,得到 list = " + string.Join(",", list.ToArray())); + + /* 访问元素 */ + int num = list.Get(1); + Console.WriteLine("访问索引 1 处的元素,得到 num = " + num); + + /* 更新元素 */ + list.Set(1, 0); + Console.WriteLine("将索引 1 处的元素更新为 0 ,得到 list = " + string.Join(",", list.ToArray())); + + /* 测试扩容机制 */ + for (int i = 0; i < 10; i++) + { + // 在 i = 5 时,列表长度将超出列表容量,此时触发扩容机制 + list.Add(i); + } + Console.WriteLine("扩容后的列表 list = " + string.Join(",", list.ToArray()) + + " ,容量 = " + list.Capacity() + " ,长度 = " + list.Size()); + } + } +} diff --git a/codes/csharp/chapter_computational_complexity/leetcode_two_sum.cs b/codes/csharp/chapter_computational_complexity/leetcode_two_sum.cs new file mode 100644 index 00000000..6d45f99b --- /dev/null +++ b/codes/csharp/chapter_computational_complexity/leetcode_two_sum.cs @@ -0,0 +1,69 @@ +/** + * File: leetcode_two_sum.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using NUnit.Framework; + +namespace hello_algo.chapter_computational_complexity +{ + class SolutionBruteForce + { + public int[] twoSum(int[] nums, int target) + { + int size = nums.Length; + // 两层循环,时间复杂度 O(n^2) + for (int i = 0; i < size - 1; i++) + { + for (int j = i + 1; j < size; j++) + { + if (nums[i] + nums[j] == target) + return new int[] { i, j }; + } + } + return new int[0]; + } + } + + class SolutionHashMap + { + public int[] twoSum(int[] nums, int target) + { + int size = nums.Length; + // 辅助哈希表,空间复杂度 O(n) + Dictionary dic = new(); + // 单层循环,时间复杂度 O(n) + for (int i = 0; i < size; i++) + { + if (dic.ContainsKey(target - nums[i])) + { + return new int[] { dic[target - nums[i]], i }; + } + dic.Add(nums[i], i); + } + return new int[0]; + } + } + + public class leetcode_two_sum + { + [Test] + public void Test() + { + // ======= Test Case ======= + int[] nums = { 2, 7, 11, 15 }; + int target = 9; + + // ====== Driver Code ====== + // 方法一 + SolutionBruteForce slt1 = new SolutionBruteForce(); + int[] res = slt1.twoSum(nums, target); + Console.WriteLine("方法一 res = " + string.Join(",", res)); + // 方法二 + SolutionHashMap slt2 = new SolutionHashMap(); + res = slt2.twoSum(nums, target); + Console.WriteLine("方法二 res = " + string.Join(",", res)); + } + } +} diff --git a/codes/csharp/chapter_computational_complexity/space_complexity.cs b/codes/csharp/chapter_computational_complexity/space_complexity.cs new file mode 100644 index 00000000..c534c81b --- /dev/null +++ b/codes/csharp/chapter_computational_complexity/space_complexity.cs @@ -0,0 +1,122 @@ +/** + * File: space_complexity.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using hello_algo.include; +using NUnit.Framework; + +namespace hello_algo.chapter_computational_complexity +{ + public class space_complexity + { + /* 函数 */ + static int function() + { + // do something + return 0; + } + + /* 常数阶 */ + static void constant(int n) + { + // 常量、变量、对象占用 O(1) 空间 + int a = 0; + int b = 0; + int[] nums = new int[10000]; + ListNode node = new ListNode(0); + // 循环中的变量占用 O(1) 空间 + for (int i = 0; i < n; i++) + { + int c = 0; + } + // 循环中的函数占用 O(1) 空间 + for (int i = 0; i < n; i++) + { + function(); + } + } + + /* 线性阶 */ + static void linear(int n) + { + // 长度为 n 的数组占用 O(n) 空间 + int[] nums = new int[n]; + // 长度为 n 的列表占用 O(n) 空间 + List nodes = new(); + for (int i = 0; i < n; i++) + { + nodes.Add(new ListNode(i)); + } + // 长度为 n 的哈希表占用 O(n) 空间 + Dictionary map = new(); + for (int i = 0; i < n; i++) + { + map.Add(i, i.ToString()); + } + } + + /* 线性阶(递归实现) */ + static void linearRecur(int n) + { + Console.WriteLine("递归 n = " + n); + if (n == 1) return; + linearRecur(n - 1); + } + + /* 平方阶 */ + static void quadratic(int n) + { + // 矩阵占用 O(n^2) 空间 + int[,] numMatrix = new int[n, n]; + // 二维列表占用 O(n^2) 空间 + List> numList = new(); + for (int i = 0; i < n; i++) + { + List tmp = new(); + for (int j = 0; j < n; j++) + { + tmp.Add(0); + } + numList.Add(tmp); + } + } + + /* 平方阶(递归实现) */ + static int quadraticRecur(int n) + { + if (n <= 0) return 0; + int[] nums = new int[n]; + Console.WriteLine("递归 n = " + n + " 中的 nums 长度 = " + nums.Length); + return quadraticRecur(n - 1); + } + + /* 指数阶(建立满二叉树) */ + static TreeNode? buildTree(int n) + { + if (n == 0) return null; + TreeNode root = new TreeNode(0); + root.left = buildTree(n - 1); + root.right = buildTree(n - 1); + return root; + } + + [Test] + public void Test() + { + int n = 5; + // 常数阶 + constant(n); + // 线性阶 + linear(n); + linearRecur(n); + // 平方阶 + quadratic(n); + quadraticRecur(n); + // 指数阶 + TreeNode? root = buildTree(n); + PrintUtil.PrintTree(root); + } + } +} diff --git a/codes/csharp/chapter_computational_complexity/time_complexity.cs b/codes/csharp/chapter_computational_complexity/time_complexity.cs new file mode 100644 index 00000000..ac7b405a --- /dev/null +++ b/codes/csharp/chapter_computational_complexity/time_complexity.cs @@ -0,0 +1,232 @@ +/** + * File: time_complexity.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using NUnit.Framework; + +namespace hello_algo.chapter_computational_complexity +{ + public class time_complexity + { + void algorithm(int n) + { + int a = 1; // +0(技巧 1) + a = a + n; // +0(技巧 1) + // +n(技巧 2) + for (int i = 0; i < 5 * n + 1; i++) + { + Console.WriteLine(0); + } + // +n*n(技巧 3) + for (int i = 0; i < 2 * n; i++) + { + for (int j = 0; j < n + 1; j++) + { + Console.WriteLine(0); + } + } + } + + // 算法 A 时间复杂度:常数阶 + void algorithm_A(int n) + { + Console.WriteLine(0); + } + // 算法 B 时间复杂度:线性阶 + void algorithm_B(int n) + { + for (int i = 0; i < n; i++) + { + Console.WriteLine(0); + } + } + // 算法 C 时间复杂度:常数阶 + void algorithm_C(int n) + { + for (int i = 0; i < 1000000; i++) + { + Console.WriteLine(0); + } + } + + /* 常数阶 */ + static int constant(int n) + { + int count = 0; + int size = 100000; + for (int i = 0; i < size; i++) + count++; + return count; + } + + /* 线性阶 */ + static int linear(int n) + { + int count = 0; + for (int i = 0; i < n; i++) + count++; + return count; + } + + /* 线性阶(遍历数组) */ + static int arrayTraversal(int[] nums) + { + int count = 0; + // 循环次数与数组长度成正比 + foreach (int num in nums) + { + count++; + } + return count; + } + + /* 平方阶 */ + static int quadratic(int n) + { + int count = 0; + // 循环次数与数组长度成平方关系 + for (int i = 0; i < n; i++) + { + for (int j = 0; j < n; j++) + { + count++; + } + } + return count; + } + + /* 平方阶(冒泡排序) */ + static int bubbleSort(int[] nums) + { + int count = 0; // 计数器 + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = nums.Length - 1; i > 0; i--) + { + // 内循环:冒泡操作 + for (int j = 0; j < i; j++) + { + if (nums[j] > nums[j + 1]) + { + // 交换 nums[j] 与 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交换包含 3 个单元操作 + } + } + } + return count; + } + + /* 指数阶(循环实现) */ + static int exponential(int n) + { + int count = 0, bas = 1; + // cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) + for (int i = 0; i < n; i++) + { + for (int j = 0; j < bas; j++) + { + count++; + } + bas *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + + /* 指数阶(递归实现) */ + static int expRecur(int n) + { + if (n == 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + + /* 对数阶(循环实现) */ + static int logarithmic(float n) + { + int count = 0; + while (n > 1) + { + n = n / 2; + count++; + } + return count; + } + + /* 对数阶(递归实现) */ + static int logRecur(float n) + { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } + + /* 线性对数阶 */ + static int linearLogRecur(float n) + { + if (n <= 1) return 1; + int count = linearLogRecur(n / 2) + + linearLogRecur(n / 2); + for (int i = 0; i < n; i++) + { + count++; + } + return count; + } + + /* 阶乘阶(递归实现) */ + static int factorialRecur(int n) + { + if (n == 0) return 1; + int count = 0; + // 从 1 个分裂出 n 个 + for (int i = 0; i < n; i++) + { + count += factorialRecur(n - 1); + } + return count; + } + + [Test] + public void Test() + { + // 可以修改 n 运行,体会一下各种复杂度的操作数量变化趋势 + int n = 8; + Console.WriteLine("输入数据大小 n = " + n); + + int count = constant(n); + Console.WriteLine("常数阶的计算操作数量 = " + count); + + count = linear(n); + Console.WriteLine("线性阶的计算操作数量 = " + count); + count = arrayTraversal(new int[n]); + Console.WriteLine("线性阶(遍历数组)的计算操作数量 = " + count); + + count = quadratic(n); + Console.WriteLine("平方阶的计算操作数量 = " + count); + int[] nums = new int[n]; + for (int i = 0; i < n; i++) + nums[i] = n - i; // [n,n-1,...,2,1] + count = bubbleSort(nums); + Console.WriteLine("平方阶(冒泡排序)的计算操作数量 = " + count); + + count = exponential(n); + Console.WriteLine("指数阶(循环实现)的计算操作数量 = " + count); + count = expRecur(n); + Console.WriteLine("指数阶(递归实现)的计算操作数量 = " + count); + + count = logarithmic((float)n); + Console.WriteLine("对数阶(循环实现)的计算操作数量 = " + count); + count = logRecur((float)n); + Console.WriteLine("对数阶(递归实现)的计算操作数量 = " + count); + + count = linearLogRecur((float)n); + Console.WriteLine("线性对数阶(递归实现)的计算操作数量 = " + count); + + count = factorialRecur(n); + Console.WriteLine("阶乘阶(递归实现)的计算操作数量 = " + count); + } + } +} diff --git a/codes/csharp/chapter_computational_complexity/worst_best_time_complexity.cs b/codes/csharp/chapter_computational_complexity/worst_best_time_complexity.cs new file mode 100644 index 00000000..9759cd58 --- /dev/null +++ b/codes/csharp/chapter_computational_complexity/worst_best_time_complexity.cs @@ -0,0 +1,61 @@ +/** + * File: worst_best_time_complexity.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using NUnit.Framework; + +namespace hello_algo.chapter_computational_complexity +{ + public class worst_best_time_complexity + { + /* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */ + static int[] randomNumbers(int n) + { + int[] nums = new int[n]; + // 生成数组 nums = { 1, 2, 3, ..., n } + for (int i = 0; i < n; i++) + { + nums[i] = i + 1; + } + + // 随机打乱数组元素 + for (int i = 0; i < nums.Length; i++) + { + var index = new Random().Next(i, nums.Length); + var tmp = nums[i]; + var ran = nums[index]; + nums[i] = ran; + nums[index] = tmp; + } + return nums; + } + + /* 查找数组 nums 中数字 1 所在索引 */ + static int findOne(int[] nums) + { + for (int i = 0; i < nums.Length; i++) + { + if (nums[i] == 1) + return i; + } + return -1; + } + + + /* Driver Code */ + [Test] + public void Test() + { + for (int i = 0; i < 10; i++) + { + int n = 100; + int[] nums = randomNumbers(n); + int index = findOne(nums); + Console.WriteLine("\n数组 [ 1, 2, ..., n ] 被打乱后 = " + string.Join(",", nums)); + Console.WriteLine("数字 1 的索引为 " + index); + } + } + } +} diff --git a/codes/csharp/chapter_hashing/array_hash_map.cs b/codes/csharp/chapter_hashing/array_hash_map.cs new file mode 100644 index 00000000..3320fa85 --- /dev/null +++ b/codes/csharp/chapter_hashing/array_hash_map.cs @@ -0,0 +1,164 @@ +/** + * File: array_hash_map.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using NUnit.Framework; + +namespace hello_algo.chapter_hashing +{ + + /* 键值对 int->String */ + class Entry + { + public int key; + public String val; + public Entry(int key, String val) + { + this.key = key; + this.val = val; + } + } + + /* 基于数组简易实现的哈希表 */ + class ArrayHashMap + { + private List bucket; + public ArrayHashMap() + { + // 初始化一个长度为 100 的桶(数组) + bucket = new (); + for (int i = 0; i < 100; i++) + { + bucket.Add(null); + } + } + + /* 哈希函数 */ + private int hashFunc(int key) + { + int index = key % 100; + return index; + } + + /* 查询操作 */ + public String? get(int key) + { + int index = hashFunc(key); + Entry? pair = bucket[index]; + if (pair == null) return null; + return pair.val; + } + + /* 添加操作 */ + public void put(int key, String val) + { + Entry pair = new Entry(key, val); + int index = hashFunc(key); + bucket[index]=pair; + } + + /* 删除操作 */ + public void remove(int key) + { + int index = hashFunc(key); + // 置为 null ,代表删除 + bucket[index]=null; + } + + /* 获取所有键值对 */ + public List entrySet() + { + List entrySet = new (); + foreach (Entry? pair in bucket) + { + if (pair != null) + entrySet.Add(pair); + } + return entrySet; + } + + /* 获取所有键 */ + public List keySet() + { + List keySet = new (); + foreach (Entry? pair in bucket) + { + if (pair != null) + keySet.Add(pair.key); + } + return keySet; + } + + /* 获取所有值 */ + public List valueSet() + { + List valueSet = new (); + foreach (Entry? pair in bucket) + { + if (pair != null) + valueSet.Add(pair.val); + } + return valueSet; + } + + /* 打印哈希表 */ + public void print() + { + foreach (Entry kv in entrySet()) + { + Console.WriteLine(kv.key + " -> " + kv.val); + } + } + } + + + public class array_hash_map + { + [Test] + public void Test() + { + /* 初始化哈希表 */ + ArrayHashMap map = new ArrayHashMap(); + + /* 添加操作 */ + // 在哈希表中添加键值对 (key, value) + map.put(12836, "小哈"); + map.put(15937, "小啰"); + map.put(16750, "小算"); + map.put(13276, "小法"); + map.put(10583, "小鸭"); + Console.WriteLine("\n添加完成后,哈希表为\nKey -> Value"); + map.print(); + + /* 查询操作 */ + // 向哈希表输入键 key ,得到值 value + String? name = map.get(15937); + Console.WriteLine("\n输入学号 15937 ,查询到姓名 " + name); + + /* 删除操作 */ + // 在哈希表中删除键值对 (key, value) + map.remove(10583); + Console.WriteLine("\n删除 10583 后,哈希表为\nKey -> Value"); + map.print(); + + /* 遍历哈希表 */ + Console.WriteLine("\n遍历键值对 Key->Value"); + foreach (Entry kv in map.entrySet()) + { + Console.WriteLine(kv.key + " -> " + kv.val); + } + Console.WriteLine("\n单独遍历键 Key"); + foreach (int key in map.keySet()) + { + Console.WriteLine(key); + } + Console.WriteLine("\n单独遍历值 Value"); + foreach (String val in map.valueSet()) + { + Console.WriteLine(val); + } + } + } +} \ No newline at end of file diff --git a/codes/csharp/chapter_hashing/hash_map.cs b/codes/csharp/chapter_hashing/hash_map.cs new file mode 100644 index 00000000..6afcc82c --- /dev/null +++ b/codes/csharp/chapter_hashing/hash_map.cs @@ -0,0 +1,57 @@ + +/** + * File: hash_map.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using hello_algo.include; +using NUnit.Framework; + +namespace hello_algo.chapter_hashing +{ + + public class hash_map { + [Test] + public void Test() + { + /* 初始化哈希表 */ + Dictionary map = new (); + + /* 添加操作 */ + // 在哈希表中添加键值对 (key, value) + map.Add(12836, "小哈"); + map.Add(15937, "小啰"); + map.Add(16750, "小算"); + map.Add(13276, "小法"); + map.Add(10583, "小鸭"); + Console.WriteLine("\n添加完成后,哈希表为\nKey -> Value"); + PrintUtil.printHashMap(map); + + /* 查询操作 */ + // 向哈希表输入键 key ,得到值 value + String name = map[15937]; + Console.WriteLine("\n输入学号 15937 ,查询到姓名 " + name); + + /* 删除操作 */ + // 在哈希表中删除键值对 (key, value) + map.Remove(10583); + Console.WriteLine("\n删除 10583 后,哈希表为\nKey -> Value"); + PrintUtil.printHashMap(map); + + /* 遍历哈希表 */ + Console.WriteLine("\n遍历键值对 Key->Value"); + foreach (var kv in map) { + Console.WriteLine(kv.Key + " -> " + kv.Value); + } + Console.WriteLine("\n单独遍历键 Key"); + foreach (int key in map.Keys) { + Console.WriteLine(key); + } + Console.WriteLine("\n单独遍历值 Value"); + foreach (String val in map.Values) { + Console.WriteLine(val); + } + } + } +} diff --git a/codes/csharp/chapter_searching/binary_search.cs b/codes/csharp/chapter_searching/binary_search.cs new file mode 100644 index 00000000..1bf8a5b2 --- /dev/null +++ b/codes/csharp/chapter_searching/binary_search.cs @@ -0,0 +1,68 @@ +/** + * File: binary_search.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using NUnit.Framework; + +namespace hello_algo.chapter_searching +{ + public class binary_search + { + /* 二分查找(双闭区间) */ + static int binarySearch(int[] nums, int target) + { + // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + int i = 0, j = nums.Length - 1; + // 循环,当搜索区间为空时跳出(当 i > j 时为空) + while (i <= j) + { + int m = (i + j) / 2; // 计算中点索引 m + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1; + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中 + j = m - 1; + else // 找到目标元素,返回其索引 + return m; + } + // 未找到目标元素,返回 -1 + return -1; + } + + /* 二分查找(左闭右开) */ + static int binarySearch1(int[] nums, int target) + { + // 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 + int i = 0, j = nums.Length; + // 循环,当搜索区间为空时跳出(当 i = j 时为空) + while (i < j) + { + int m = (i + j) / 2; // 计算中点索引 m + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j) 中 + i = m + 1; + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中 + j = m; + else // 找到目标元素,返回其索引 + return m; + } + // 未找到目标元素,返回 -1 + return -1; + } + + [Test] + public void Test() + { + int target = 6; + int[] nums = { 1, 3, 6, 8, 12, 15, 23, 67, 70, 92 }; + + /* 二分查找(双闭区间) */ + int index = binarySearch(nums, target); + Console.WriteLine("目标元素 6 的索引 = " + index); + + /* 二分查找(左闭右开) */ + index = binarySearch1(nums, target); + Console.WriteLine("目标元素 6 的索引 = " + index); + } + } +} diff --git a/codes/csharp/chapter_searching/hashing_search.cs b/codes/csharp/chapter_searching/hashing_search.cs new file mode 100644 index 00000000..33d5fe9d --- /dev/null +++ b/codes/csharp/chapter_searching/hashing_search.cs @@ -0,0 +1,60 @@ +/** + * File: hashing_search.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using hello_algo.include; +using NUnit.Framework; + +namespace hello_algo.chapter_searching +{ + public class hashing_search + { + /* 哈希查找(数组) */ + static int hashingSearch(Dictionary map, int target) + { + // 哈希表的 key: 目标元素,value: 索引 + // 若哈希表中无此 key ,返回 -1 + return map.GetValueOrDefault(target, -1); + } + + /* 哈希查找(链表) */ + static ListNode? hashingSearch1(Dictionary map, int target) + { + + // 哈希表的 key: 目标结点值,value: 结点对象 + // 若哈希表中无此 key ,返回 null + return map.GetValueOrDefault(target); + } + + [Test] + public void Test() + { + int target = 3; + + /* 哈希查找(数组) */ + int[] nums = { 1, 5, 3, 2, 4, 7, 5, 9, 10, 8 }; + // 初始化哈希表 + Dictionary map = new(); + for (int i = 0; i < nums.Length; i++) + { + map[nums[i]] = i; // key: 元素,value: 索引 + } + int index = hashingSearch(map, target); + Console.WriteLine("目标元素 3 的索引 = " + index); + + /* 哈希查找(链表) */ + ListNode? head = ListNode.ArrToLinkedList(nums); + // 初始化哈希表 + Dictionary map1 = new(); + while (head != null) + { + map1[head.val] = head; // key: 结点值,value: 结点 + head = head.next; + } + ListNode? node = hashingSearch1(map1, target); + Console.WriteLine("目标结点值 3 的对应结点对象为 " + node); + } + } +} diff --git a/codes/csharp/chapter_searching/linear_search.cs b/codes/csharp/chapter_searching/linear_search.cs new file mode 100644 index 00000000..412eb37d --- /dev/null +++ b/codes/csharp/chapter_searching/linear_search.cs @@ -0,0 +1,59 @@ +/** + * File: linear_search.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using hello_algo.include; +using NUnit.Framework; + +namespace hello_algo.chapter_searching +{ + public class linear_search + { + /* 线性查找(数组) */ + static int linearSearch(int[] nums, int target) + { + // 遍历数组 + for (int i = 0; i < nums.Length; i++) + { + // 找到目标元素,返回其索引 + if (nums[i] == target) + return i; + } + // 未找到目标元素,返回 -1 + return -1; + } + + /* 线性查找(链表) */ + static ListNode? linearSearch(ListNode head, int target) + { + // 遍历链表 + while (head != null) + { + // 找到目标结点,返回之 + if (head.val == target) + return head; + head = head.next; + } + // 未找到目标结点,返回 null + return null; + } + + [Test] + public void Test() + { + int target = 3; + + /* 在数组中执行线性查找 */ + int[] nums = { 1, 5, 3, 2, 4, 7, 5, 9, 10, 8 }; + int index = linearSearch(nums, target); + Console.WriteLine("目标元素 3 的索引 = " + index); + + /* 在链表中执行线性查找 */ + ListNode head = ListNode.ArrToLinkedList(nums); + ListNode? node = linearSearch(head, target); + Console.WriteLine("目标结点值 3 的对应结点对象为 " + node); + } + } +} diff --git a/codes/csharp/chapter_sorting/bubble_sort.cs b/codes/csharp/chapter_sorting/bubble_sort.cs new file mode 100644 index 00000000..b3304df0 --- /dev/null +++ b/codes/csharp/chapter_sorting/bubble_sort.cs @@ -0,0 +1,68 @@ +/** + * File: bubble_sort.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using NUnit.Framework; + +namespace hello_algo.chapter_sorting +{ + public class bubble_sort + { + /* 冒泡排序 */ + static void bubbleSort(int[] nums) + { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = nums.Length - 1; i > 0; i--) + { + // 内循环:冒泡操作 + for (int j = 0; j < i; j++) + { + if (nums[j] > nums[j + 1]) + { + // 交换 nums[j] 与 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } + + /* 冒泡排序(标志优化)*/ + static void bubbleSortWithFlag(int[] nums) + { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = nums.Length - 1; i > 0; i--) + { + bool flag = false; // 初始化标志位 + // 内循环:冒泡操作 + for (int j = 0; j < i; j++) + { + if (nums[j] > nums[j + 1]) + { + // 交换 nums[j] 与 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // 记录交换元素 + } + } + if (!flag) break; // 此轮冒泡未交换任何元素,直接跳出 + } + } + + [Test] + public void Test() + { + int[] nums = { 4, 1, 3, 1, 5, 2 }; + bubbleSort(nums); + Console.WriteLine("冒泡排序完成后 nums = " + string.Join(",",nums)); + + int[] nums1 = { 4, 1, 3, 1, 5, 2 }; + bubbleSortWithFlag(nums1); + Console.WriteLine("冒泡排序完成后 nums1 = " + string.Join(",", nums)); + } + } +} diff --git a/codes/csharp/chapter_sorting/insertion_sort.cs b/codes/csharp/chapter_sorting/insertion_sort.cs new file mode 100644 index 00000000..0f674c9f --- /dev/null +++ b/codes/csharp/chapter_sorting/insertion_sort.cs @@ -0,0 +1,38 @@ +/** + * File: insertion_sort.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using NUnit.Framework; + +namespace hello_algo.chapter_sorting +{ + public class insertion_sort + { + /* 插入排序 */ + static void insertionSort(int[] nums) + { + // 外循环:base = nums[1], nums[2], ..., nums[n-1] + for (int i = 1; i < nums.Length; i++) + { + int bas = nums[i], j = i - 1; + // 内循环:将 base 插入到左边的正确位置 + while (j >= 0 && nums[j] > bas) + { + nums[j + 1] = nums[j]; // 1. 将 nums[j] 向右移动一位 + j--; + } + nums[j + 1] = bas; // 2. 将 base 赋值到正确位置 + } + } + + [Test] + public void Test() + { + int[] nums = { 4, 1, 3, 1, 5, 2 }; + insertionSort(nums); + Console.WriteLine("插入排序完成后 nums = " + string.Join(",", nums)); + } + } +} diff --git a/codes/csharp/chapter_sorting/merge_sort.cs b/codes/csharp/chapter_sorting/merge_sort.cs new file mode 100644 index 00000000..04ac8acd --- /dev/null +++ b/codes/csharp/chapter_sorting/merge_sort.cs @@ -0,0 +1,65 @@ +/** + * File: merge_sort.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using NUnit.Framework; + +namespace hello_algo.chapter_sorting +{ + public class merge_sort + { + /** + * 合并左子数组和右子数组 + * 左子数组区间 [left, mid] + * 右子数组区间 [mid + 1, right] + */ + static void merge(int[] nums, int left, int mid, int right) + { + // 初始化辅助数组 + int[] tmp = nums[left..(right + 1)]; + // 左子数组的起始索引和结束索引 + int leftStart = left - left, leftEnd = mid - left; + // 右子数组的起始索引和结束索引 + int rightStart = mid + 1 - left, rightEnd = right - left; + // i, j 分别指向左子数组、右子数组的首元素 + int i = leftStart, j = rightStart; + // 通过覆盖原数组 nums 来合并左子数组和右子数组 + for (int k = left; k <= right; k++) + { + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ + if (i > leftEnd) + nums[k] = tmp[j++]; + // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ + else if (j > rightEnd || tmp[i] <= tmp[j]) + nums[k] = tmp[i++]; + // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + else + nums[k] = tmp[j++]; + } + } + + /* 归并排序 */ + static void mergeSort(int[] nums, int left, int right) + { + // 终止条件 + if (left >= right) return; // 当子数组长度为 1 时终止递归 + // 划分阶段 + int mid = (left + right) / 2; // 计算中点 + mergeSort(nums, left, mid); // 递归左子数组 + mergeSort(nums, mid + 1, right); // 递归右子数组 + // 合并阶段 + merge(nums, left, mid, right); + } + + [Test] + public void Test() + { + /* 归并排序 */ + int[] nums = { 7, 3, 2, 6, 0, 1, 5, 4 }; + mergeSort(nums, 0, nums.Length - 1); + Console.WriteLine("归并排序完成后 nums = " + string.Join(",", nums)); + } + } +} diff --git a/codes/csharp/chapter_sorting/quick_sort.cs b/codes/csharp/chapter_sorting/quick_sort.cs new file mode 100644 index 00000000..020d4f34 --- /dev/null +++ b/codes/csharp/chapter_sorting/quick_sort.cs @@ -0,0 +1,183 @@ +/** + * File: quick_sort.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using NUnit.Framework; + +namespace hello_algo.chapter_sorting +{ + class QuickSort + { + /* 元素交换 */ + static void swap(int[] nums, int i, int j) + { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + /* 哨兵划分 */ + static int partition(int[] nums, int left, int right) + { + // 以 nums[left] 作为基准数 + int i = left, j = right; + while (i < j) + { + while (i < j && nums[j] >= nums[left]) + j--; // 从右向左找首个小于基准数的元素 + while (i < j && nums[i] <= nums[left]) + i++; // 从左向右找首个大于基准数的元素 + swap(nums, i, j); // 交换这两个元素 + } + swap(nums, i, left); // 将基准数交换至两子数组的分界线 + return i; // 返回基准数的索引 + } + + /* 快速排序 */ + public static void quickSort(int[] nums, int left, int right) + { + // 子数组长度为 1 时终止递归 + if (left >= right) + return; + // 哨兵划分 + int pivot = partition(nums, left, right); + // 递归左子数组、右子数组 + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } + } + + /* 快速排序类(中位基准数优化) */ + class QuickSortMedian + { + /* 元素交换 */ + static void swap(int[] nums, int i, int j) + { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + /* 选取三个元素的中位数 */ + static int medianThree(int[] nums, int left, int mid, int right) + { + // 使用了异或操作来简化代码 + // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 + if ((nums[left] > nums[mid]) ^ (nums[left] > nums[right])) + return left; + else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right])) + return mid; + else + return right; + } + + /* 哨兵划分(三数取中值) */ + static int partition(int[] nums, int left, int right) + { + // 选取三个候选元素的中位数 + int med = medianThree(nums, left, (left + right) / 2, right); + // 将中位数交换至数组最左端 + swap(nums, left, med); + // 以 nums[left] 作为基准数 + int i = left, j = right; + while (i < j) + { + while (i < j && nums[j] >= nums[left]) + j--; // 从右向左找首个小于基准数的元素 + while (i < j && nums[i] <= nums[left]) + i++; // 从左向右找首个大于基准数的元素 + swap(nums, i, j); // 交换这两个元素 + } + swap(nums, i, left); // 将基准数交换至两子数组的分界线 + return i; // 返回基准数的索引 + } + + /* 快速排序 */ + public static void quickSort(int[] nums, int left, int right) + { + // 子数组长度为 1 时终止递归 + if (left >= right) + return; + // 哨兵划分 + int pivot = partition(nums, left, right); + // 递归左子数组、右子数组 + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } + } + + /* 快速排序类(尾递归优化) */ + class QuickSortTailCall + { + /* 元素交换 */ + static void swap(int[] nums, int i, int j) + { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + /* 哨兵划分 */ + static int partition(int[] nums, int left, int right) + { + // 以 nums[left] 作为基准数 + int i = left, j = right; + while (i < j) + { + while (i < j && nums[j] >= nums[left]) + j--; // 从右向左找首个小于基准数的元素 + while (i < j && nums[i] <= nums[left]) + i++; // 从左向右找首个大于基准数的元素 + swap(nums, i, j); // 交换这两个元素 + } + swap(nums, i, left); // 将基准数交换至两子数组的分界线 + return i; // 返回基准数的索引 + } + + /* 快速排序(尾递归优化) */ + public static void quickSort(int[] nums, int left, int right) + { + // 子数组长度为 1 时终止 + while (left < right) + { + // 哨兵划分操作 + int pivot = partition(nums, left, right); + // 对两个子数组中较短的那个执行快排 + if (pivot - left < right - pivot) + { + quickSort(nums, left, pivot - 1); // 递归排序左子数组 + left = pivot + 1; // 剩余待排序区间为 [pivot + 1, right] + } + else + { + quickSort(nums, pivot + 1, right); // 递归排序右子数组 + right = pivot - 1; // 剩余待排序区间为 [left, pivot - 1] + } + } + } + } + + public class quick_sort + { + [Test] + public void Test() + { + /* 快速排序 */ + int[] nums = { 2, 4, 1, 0, 3, 5 }; + QuickSort.quickSort(nums, 0, nums.Length - 1); + Console.WriteLine("快速排序完成后 nums = " + string.Join(",", nums)); + + /* 快速排序(中位基准数优化) */ + int[] nums1 = { 2, 4, 1, 0, 3, 5 }; + QuickSortMedian.quickSort(nums1, 0, nums1.Length - 1); + Console.WriteLine("快速排序(中位基准数优化)完成后 nums1 = " + string.Join(",", nums1)); + + /* 快速排序(尾递归优化) */ + int[] nums2 = { 2, 4, 1, 0, 3, 5 }; + QuickSortTailCall.quickSort(nums2, 0, nums2.Length - 1); + Console.WriteLine("快速排序(尾递归优化)完成后 nums2 = " + string.Join(",", nums2)); + } + } +} diff --git a/codes/csharp/chapter_stack_and_queue/array_queue.cs b/codes/csharp/chapter_stack_and_queue/array_queue.cs new file mode 100644 index 00000000..81085a36 --- /dev/null +++ b/codes/csharp/chapter_stack_and_queue/array_queue.cs @@ -0,0 +1,133 @@ +/** + * File: array_queue.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using NUnit.Framework; + +namespace hello_algo.chapter_stack_and_queue +{ + + /* 基于环形数组实现的队列 */ + class ArrayQueue + { + private int[] nums; // 用于存储队列元素的数组 + private int front = 0; // 头指针,指向队首 + private int rear = 0; // 尾指针,指向队尾 + 1 + + public ArrayQueue(int capacity) + { + // 初始化数组 + nums = new int[capacity]; + } + + /* 获取队列的容量 */ + public int capacity() + { + return nums.Length; + } + + /* 获取队列的长度 */ + public int size() + { + int capacity = this.capacity(); + // 由于将数组看作为环形,可能 rear < front ,因此需要取余数 + return (capacity + rear - front) % capacity; + } + + /* 判断队列是否为空 */ + public bool isEmpty() + { + return rear - front == 0; + } + + /* 入队 */ + public void offer(int num) + { + if (size() == capacity()) + { + Console.WriteLine("队列已满"); + return; + } + // 尾结点后添加 num + nums[rear] = num; + // 尾指针向后移动一位,越过尾部后返回到数组头部 + rear = (rear + 1) % capacity(); + } + + /* 出队 */ + public int poll() + { + int num = peek(); + // 队头指针向后移动一位,若越过尾部则返回到数组头部 + front = (front + 1) % capacity(); + return num; + } + + /* 访问队首元素 */ + public int peek() + { + if (isEmpty()) + throw new Exception(); + return nums[front]; + } + + /* 返回数组 */ + public int[] toArray() + { + int size = this.size(); + int capacity = this.capacity(); + // 仅转换有效长度范围内的列表元素 + int[] res = new int[size]; + for (int i = 0, j = front; i < size; i++, j++) + { + res[i] = nums[j % capacity]; + } + return res; + } + } + + public class array_queue + { + [Test] + public void Test() + { + /* 初始化队列 */ + int capacity = 10; + ArrayQueue queue = new ArrayQueue(capacity); + + /* 元素入队 */ + queue.offer(1); + queue.offer(3); + queue.offer(2); + queue.offer(5); + queue.offer(4); + Console.WriteLine("队列 queue = " + string.Join(",", queue.toArray())); + + /* 访问队首元素 */ + int peek = queue.peek(); + Console.WriteLine("队首元素 peek = " + peek); + + /* 元素出队 */ + int poll = queue.poll(); + Console.WriteLine("出队元素 poll = " + poll + ",出队后 queue = " + string.Join(",", queue.toArray())); + + /* 获取队列的长度 */ + int size = queue.size(); + Console.WriteLine("队列长度 size = " + size); + + /* 判断队列是否为空 */ + bool isEmpty = queue.isEmpty(); + Console.WriteLine("队列是否为空 = " + isEmpty); + + /* 测试环形数组 */ + for (int i = 0; i < 10; i++) + { + queue.offer(i); + queue.poll(); + Console.WriteLine("第 " + i + " 轮入队 + 出队后 queue = " + string.Join(",", queue.toArray())); + } + } + } +} \ No newline at end of file diff --git a/codes/csharp/chapter_stack_and_queue/array_stack.cs b/codes/csharp/chapter_stack_and_queue/array_stack.cs new file mode 100644 index 00000000..b7ea6df7 --- /dev/null +++ b/codes/csharp/chapter_stack_and_queue/array_stack.cs @@ -0,0 +1,98 @@ +/** + * File: array_stack.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using NUnit.Framework; + +namespace hello_algo.chapter_stack_and_queue +{ + + /* 基于数组实现的栈 */ + class ArrayStack + { + private List stack; + public ArrayStack() + { + // 初始化列表(动态数组) + stack = new(); + } + + /* 获取栈的长度 */ + public int size() + { + return stack.Count(); + } + + /* 判断栈是否为空 */ + public bool isEmpty() + { + return size() == 0; + } + + /* 入栈 */ + public void push(int num) + { + stack.Add(num); + } + + /* 出栈 */ + public int pop() + { + if (isEmpty()) + throw new Exception(); + var val = peek(); + stack.RemoveAt(size() - 1); + return val; + } + + /* 访问栈顶元素 */ + public int peek() + { + if (isEmpty()) + throw new Exception(); + return stack[size() - 1]; + } + + /* 将 List 转化为 Array 并返回 */ + public int[] toArray() + { + return stack.ToArray(); + } + } + + public class array_stack + { + [Test] + public void Test() + { + /* 初始化栈 */ + ArrayStack stack = new ArrayStack(); + + /* 元素入栈 */ + stack.push(1); + stack.push(3); + stack.push(2); + stack.push(5); + stack.push(4); + Console.WriteLine("栈 stack = " + String.Join(",", stack.toArray())); + + /* 访问栈顶元素 */ + int peek = stack.peek(); + Console.WriteLine("栈顶元素 peek = " + peek); + + /* 元素出栈 */ + int pop = stack.pop(); + Console.WriteLine("出栈元素 pop = " + pop + ",出栈后 stack = " + String.Join(",", stack.toArray())); + + /* 获取栈的长度 */ + int size = stack.size(); + Console.WriteLine("栈的长度 size = " + size); + + /* 判断是否为空 */ + bool isEmpty = stack.isEmpty(); + Console.WriteLine("栈是否为空 = " + isEmpty); + } + } +} diff --git a/codes/csharp/chapter_stack_and_queue/linkedlist_queue.cs b/codes/csharp/chapter_stack_and_queue/linkedlist_queue.cs new file mode 100644 index 00000000..70816c8f --- /dev/null +++ b/codes/csharp/chapter_stack_and_queue/linkedlist_queue.cs @@ -0,0 +1,124 @@ +/** + * File: linkedlist_queue.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using hello_algo.include; +using NUnit.Framework; + +namespace hello_algo.chapter_stack_and_queue +{ + /* 基于链表实现的队列 */ + class LinkedListQueue + { + private ListNode? front, rear; // 头结点 front ,尾结点 rear + private int queSize = 0; + + public LinkedListQueue() + { + front = null; + rear = null; + } + + /* 获取队列的长度 */ + public int size() + { + return queSize; + } + + /* 判断队列是否为空 */ + public bool isEmpty() + { + return size() == 0; + } + + /* 入队 */ + public void offer(int num) + { + // 尾结点后添加 num + ListNode node = new ListNode(num); + // 如果队列为空,则令头、尾结点都指向该结点 + if (front == null) + { + front = node; + rear = node; + // 如果队列不为空,则将该结点添加到尾结点后 + } + else if (rear != null) + { + rear.next = node; + rear = node; + } + queSize++; + } + + /* 出队 */ + public int poll() + { + int num = peek(); + // 删除头结点 + front = front?.next; + queSize--; + return num; + } + + /* 访问队首元素 */ + public int peek() + { + if (size() == 0 || front == null) + throw new Exception(); + return front.val; + } + + /* 将链表转化为 Array 并返回 */ + public int[] toArray() + { + if (front == null) + return Array.Empty(); + + ListNode node = front; + int[] res = new int[size()]; + for (int i = 0; i < res.Length; i++) + { + res[i] = node.val; + node = node.next; + } + return res; + } + } + + public class linkedlist_queue + { + [Test] + public void Test() + { + /* 初始化队列 */ + LinkedListQueue queue = new LinkedListQueue(); + + /* 元素入队 */ + queue.offer(1); + queue.offer(3); + queue.offer(2); + queue.offer(5); + queue.offer(4); + Console.WriteLine("队列 queue = " + String.Join(",", queue.toArray())); + + /* 访问队首元素 */ + int peek = queue.peek(); + Console.WriteLine("队首元素 peek = " + peek); + + /* 元素出队 */ + int poll = queue.poll(); + Console.WriteLine("出队元素 poll = " + poll + ",出队后 queue = " + String.Join(",", queue.toArray())); + + /* 获取队列的长度 */ + int size = queue.size(); + Console.WriteLine("队列长度 size = " + size); + + /* 判断队列是否为空 */ + bool isEmpty = queue.isEmpty(); + Console.WriteLine("队列是否为空 = " + isEmpty); + } + } +} \ No newline at end of file diff --git a/codes/csharp/chapter_stack_and_queue/linkedlist_stack.cs b/codes/csharp/chapter_stack_and_queue/linkedlist_stack.cs new file mode 100644 index 00000000..78a40944 --- /dev/null +++ b/codes/csharp/chapter_stack_and_queue/linkedlist_stack.cs @@ -0,0 +1,113 @@ +/** + * File: linkedlist_stack.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using hello_algo.include; +using NUnit.Framework; + +namespace hello_algo.chapter_stack_and_queue +{ + class LinkedListStack + { + private ListNode? stackPeek; // 将头结点作为栈顶 + private int stkSize = 0; // 栈的长度 + + public LinkedListStack() + { + stackPeek = null; + } + + /* 获取栈的长度 */ + public int size() + { + return stkSize; + } + + /* 判断栈是否为空 */ + public bool isEmpty() + { + return size() == 0; + } + + /* 入栈 */ + public void push(int num) + { + ListNode node = new ListNode(num); + node.next = stackPeek; + stackPeek = node; + stkSize++; + } + + /* 出栈 */ + public int pop() + { + if (stackPeek == null) + throw new Exception(); + + int num = peek(); + stackPeek = stackPeek.next; + stkSize--; + return num; + } + + /* 访问栈顶元素 */ + public int peek() + { + if (size() == 0 || stackPeek==null) + throw new Exception(); + return stackPeek.val; + } + + /* 将 List 转化为 Array 并返回 */ + public int[] toArray() + { + if (stackPeek == null) + return Array.Empty(); + + ListNode node = stackPeek; + int[] res = new int[size()]; + for (int i = res.Length - 1; i >= 0; i--) + { + res[i] = node.val; + node = node.next; + } + return res; + } + } + + public class linkedlist_stack + { + [Test] + public void Test() + { + /* 初始化栈 */ + LinkedListStack stack = new LinkedListStack(); + + /* 元素入栈 */ + stack.push(1); + stack.push(3); + stack.push(2); + stack.push(5); + stack.push(4); + Console.WriteLine("栈 stack = " + String.Join(",",stack.toArray())); + + /* 访问栈顶元素 */ + int peek = stack.peek(); + Console.WriteLine("栈顶元素 peek = " + peek); + + /* 元素出栈 */ + int pop = stack.pop(); + Console.WriteLine("出栈元素 pop = " + pop + ",出栈后 stack = " + String.Join(",",stack.toArray())); + + /* 获取栈的长度 */ + int size = stack.size(); + Console.WriteLine("栈的长度 size = " + size); + + /* 判断是否为空 */ + bool isEmpty = stack.isEmpty(); + Console.WriteLine("栈是否为空 = " + isEmpty); + } + } +} \ No newline at end of file diff --git a/codes/csharp/chapter_stack_and_queue/queue.cs b/codes/csharp/chapter_stack_and_queue/queue.cs new file mode 100644 index 00000000..a5ce9695 --- /dev/null +++ b/codes/csharp/chapter_stack_and_queue/queue.cs @@ -0,0 +1,45 @@ +/** + * File: queue.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using NUnit.Framework; + +namespace hello_algo.chapter_stack_and_queue +{ + public class queue + { + [Test] + public void Test() + { + /* 初始化队列 */ + Queue queue = new(); + + /* 元素入队 */ + queue.Enqueue(1); + queue.Enqueue(3); + queue.Enqueue(2); + queue.Enqueue(5); + queue.Enqueue(4); + Console.WriteLine("队列 queue = " + String.Join(",", queue.ToArray())); + + /* 访问队首元素 */ + int peek = queue.Peek(); + Console.WriteLine("队首元素 peek = " + peek); + + /* 元素出队 */ + int poll = queue.Dequeue(); + Console.WriteLine("出队元素 poll = " + poll + ",出队后 queue = " + String.Join(",", queue.ToArray())); + + /* 获取队列的长度 */ + int size = queue.Count(); + Console.WriteLine("队列长度 size = " + size); + + /* 判断队列是否为空 */ + bool isEmpty = queue.Count() == 0; + Console.WriteLine("队列是否为空 = " + isEmpty); + } + } + +} diff --git a/codes/csharp/chapter_stack_and_queue/stack.cs b/codes/csharp/chapter_stack_and_queue/stack.cs new file mode 100644 index 00000000..c19666b7 --- /dev/null +++ b/codes/csharp/chapter_stack_and_queue/stack.cs @@ -0,0 +1,45 @@ +/** + * File: stack.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using NUnit.Framework; + +namespace hello_algo.chapter_stack_and_queue +{ + public class stack + { + [Test] + public void Test() + { + /* 初始化栈 */ + Stack stack = new(); + + /* 元素入栈 */ + stack.Push(1); + stack.Push(3); + stack.Push(2); + stack.Push(5); + stack.Push(4); + // 请注意,stack.ToArray() 得到的是倒序序列,即索引 0 为栈顶 + Console.WriteLine("栈 stack = " + string.Join(",", stack.ToArray())); + + /* 访问栈顶元素 */ + int peek = stack.Peek(); + Console.WriteLine("栈顶元素 peek = " + peek); + + /* 元素出栈 */ + int pop = stack.Pop(); + Console.WriteLine("出栈元素 pop = " + pop + ",出栈后 stack = " + string.Join(",", stack.ToArray())); + + /* 获取栈的长度 */ + int size = stack.Count(); + Console.WriteLine("栈的长度 size = " + size); + + /* 判断是否为空 */ + bool isEmpty = stack.Count() == 0; + Console.WriteLine("栈是否为空 = " + isEmpty); + } + } +} \ No newline at end of file diff --git a/codes/csharp/chapter_tree/avl_tree.cs b/codes/csharp/chapter_tree/avl_tree.cs new file mode 100644 index 00000000..99ec17f6 --- /dev/null +++ b/codes/csharp/chapter_tree/avl_tree.cs @@ -0,0 +1,260 @@ +/** + * File: avl_tree.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using hello_algo.include; +using NUnit.Framework; + +namespace hello_algo.chapter_tree +{ + // Tree class + class AVLTree + { + public TreeNode? root; // 根节点 + + /* 获取结点高度 */ + public int height(TreeNode? node) + { + // 空结点高度为 -1 ,叶结点高度为 0 + return node == null ? -1 : node.height; + } + + /* 更新结点高度 */ + private void updateHeight(TreeNode node) + { + // 结点高度等于最高子树高度 + 1 + node.height = Math.Max(height(node.left), height(node.right)) + 1; + } + + /* 获取平衡因子 */ + public int balanceFactor(TreeNode? node) + { + // 空结点平衡因子为 0 + if (node == null) return 0; + // 结点平衡因子 = 左子树高度 - 右子树高度 + return height(node.left) - height(node.right); + } + + /* 右旋操作 */ + TreeNode? rightRotate(TreeNode? node) + { + TreeNode? child = node.left; + TreeNode? grandChild = child?.right; + // 以 child 为原点,将 node 向右旋转 + child.right = node; + node.left = grandChild; + // 更新结点高度 + updateHeight(node); + updateHeight(child); + // 返回旋转后子树的根节点 + return child; + } + + /* 左旋操作 */ + TreeNode? leftRotate(TreeNode? node) + { + TreeNode? child = node.right; + TreeNode? grandChild = child?.left; + // 以 child 为原点,将 node 向左旋转 + child.left = node; + node.right = grandChild; + // 更新结点高度 + updateHeight(node); + updateHeight(child); + // 返回旋转后子树的根节点 + return child; + } + + /* 执行旋转操作,使该子树重新恢复平衡 */ + TreeNode? rotate(TreeNode? node) + { + // 获取结点 node 的平衡因子 + int balanceFactorInt = balanceFactor(node); + // 左偏树 + if (balanceFactorInt > 1) + { + if (balanceFactor(node.left) >= 0) + { + // 右旋 + return rightRotate(node); + } + else + { + // 先左旋后右旋 + node.left = leftRotate(node?.left); + return rightRotate(node); + } + } + // 右偏树 + if (balanceFactorInt < -1) + { + if (balanceFactor(node.right) <= 0) + { + // 左旋 + return leftRotate(node); + } + else + { + // 先右旋后左旋 + node.right = rightRotate(node?.right); + return leftRotate(node); + } + } + // 平衡树,无需旋转,直接返回 + return node; + } + + /* 插入结点 */ + public TreeNode? insert(int val) + { + root = insertHelper(root, val); + return root; + } + + /* 递归插入结点(辅助函数) */ + private TreeNode? insertHelper(TreeNode? node, int val) + { + if (node == null) return new TreeNode(val); + /* 1. 查找插入位置,并插入结点 */ + if (val < node.val) + node.left = insertHelper(node.left, val); + else if (val > node.val) + node.right = insertHelper(node.right, val); + else + return node; // 重复结点不插入,直接返回 + updateHeight(node); // 更新结点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = rotate(node); + // 返回子树的根节点 + return node; + } + + /* 删除结点 */ + public TreeNode? remove(int val) + { + root = removeHelper(root, val); + return root; + } + + /* 递归删除结点(辅助函数) */ + private TreeNode? removeHelper(TreeNode? node, int val) + { + if (node == null) return null; + /* 1. 查找结点,并删除之 */ + if (val < node.val) + node.left = removeHelper(node.left, val); + else if (val > node.val) + node.right = removeHelper(node.right, val); + else + { + if (node.left == null || node.right == null) + { + TreeNode? child = node.left != null ? node.left : node.right; + // 子结点数量 = 0 ,直接删除 node 并返回 + if (child == null) + return null; + // 子结点数量 = 1 ,直接删除 node + else + node = child; + } + else + { + // 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + TreeNode? temp = minNode(node.right); + node.right = removeHelper(node.right, temp.val); + node.val = temp.val; + } + } + updateHeight(node); // 更新结点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = rotate(node); + // 返回子树的根节点 + return node; + } + + /* 获取最小结点 */ + private TreeNode? minNode(TreeNode? node) + { + if (node == null) return node; + // 循环访问左子结点,直到叶结点时为最小结点,跳出 + while (node.left != null) + { + node = node.left; + } + return node; + } + + /* 查找结点 */ + public TreeNode? search(int val) + { + TreeNode? cur = root; + // 循环查找,越过叶结点后跳出 + while (cur != null) + { + // 目标结点在 root 的右子树中 + if (cur.val < val) + cur = cur.right; + // 目标结点在 root 的左子树中 + else if (cur.val > val) + cur = cur.left; + // 找到目标结点,跳出循环 + else + break; + } + // 返回目标结点 + return cur; + } + } + + public class avl_tree + { + static void testInsert(AVLTree tree, int val) + { + tree.insert(val); + Console.WriteLine("\n插入结点 " + val + " 后,AVL 树为"); + PrintUtil.PrintTree(tree.root); + } + + static void testRemove(AVLTree tree, int val) + { + tree.remove(val); + Console.WriteLine("\n删除结点 " + val + " 后,AVL 树为"); + PrintUtil.PrintTree(tree.root); + } + + [Test] + public void Test() + { + /* 初始化空 AVL 树 */ + AVLTree avlTree = new AVLTree(); + + /* 插入结点 */ + // 请关注插入结点后,AVL 树是如何保持平衡的 + testInsert(avlTree, 1); + testInsert(avlTree, 2); + testInsert(avlTree, 3); + testInsert(avlTree, 4); + testInsert(avlTree, 5); + testInsert(avlTree, 8); + testInsert(avlTree, 7); + testInsert(avlTree, 9); + testInsert(avlTree, 10); + testInsert(avlTree, 6); + + /* 插入重复结点 */ + testInsert(avlTree, 7); + + /* 删除结点 */ + // 请关注删除结点后,AVL 树是如何保持平衡的 + testRemove(avlTree, 8); // 删除度为 0 的结点 + testRemove(avlTree, 5); // 删除度为 1 的结点 + testRemove(avlTree, 4); // 删除度为 2 的结点 + + /* 查询结点 */ + TreeNode? node = avlTree.search(7); + Console.WriteLine("\n查找到的结点对象为 " + node + ",结点值 = " + node?.val); + } + } +} diff --git a/codes/csharp/chapter_tree/binary_search_tree.cs b/codes/csharp/chapter_tree/binary_search_tree.cs new file mode 100644 index 00000000..8224317d --- /dev/null +++ b/codes/csharp/chapter_tree/binary_search_tree.cs @@ -0,0 +1,186 @@ +/** + * File: binary_search_tree.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using hello_algo.include; +using NUnit.Framework; + +namespace hello_algo.chapter_tree +{ + class BinarySearchTree + { + TreeNode? root; + + public BinarySearchTree(int[] nums) { + Array.Sort(nums); // 排序数组 + root = buildTree(nums, 0, nums.Length - 1); // 构建二叉搜索树 + } + + /* 获取二叉树根结点 */ + public TreeNode? getRoot() { + return root; + } + + /* 构建二叉搜索树 */ + public TreeNode? buildTree(int[] nums, int i, int j) { + if (i > j) return null; + // 将数组中间结点作为根结点 + int mid = (i + j) / 2; + TreeNode root = new TreeNode(nums[mid]); + // 递归建立左子树和右子树 + root.left = buildTree(nums, i, mid - 1); + root.right = buildTree(nums, mid + 1, j); + return root; + } + + /// + /// 查找结点 + /// + /// + /// + public TreeNode? search(int num) + { + TreeNode? cur = root; + // 循环查找,越过叶结点后跳出 + while (cur != null) + { + // 目标结点在 root 的右子树中 + if (cur.val < num) cur = cur.right; + // 目标结点在 root 的左子树中 + else if (cur.val > num) cur = cur.left; + // 找到目标结点,跳出循环 + else break; + } + // 返回目标结点 + return cur; + } + + /* 插入结点 */ + public TreeNode? insert(int num) + { + // 若树为空,直接提前返回 + if (root == null) return null; + TreeNode? cur = root, pre = null; + // 循环查找,越过叶结点后跳出 + while (cur != null) + { + // 找到重复结点,直接返回 + if (cur.val == num) return null; + pre = cur; + // 插入位置在 root 的右子树中 + if (cur.val < num) cur = cur.right; + // 插入位置在 root 的左子树中 + else cur = cur.left; + } + + // 插入结点 val + TreeNode node = new TreeNode(num); + if (pre != null) + { + if (pre.val < num) pre.right = node; + else pre.left = node; + } + return node; + } + + + /* 删除结点 */ + public TreeNode? remove(int num) + { + // 若树为空,直接提前返回 + if (root == null) return null; + TreeNode? cur = root, pre = null; + // 循环查找,越过叶结点后跳出 + while (cur != null) + { + // 找到待删除结点,跳出循环 + if (cur.val == num) break; + pre = cur; + // 待删除结点在 root 的右子树中 + if (cur.val < num) cur = cur.right; + // 待删除结点在 root 的左子树中 + else cur = cur.left; + } + // 若无待删除结点,则直接返回 + if (cur == null || pre == null) return null; + // 子结点数量 = 0 or 1 + if (cur.left == null || cur.right == null) + { + // 当子结点数量 = 0 / 1 时, child = null / 该子结点 + TreeNode? child = cur.left != null ? cur.left : cur.right; + // 删除结点 cur + if (pre.left == cur) + { + pre.left = child; + } + else + { + pre.right = child; + } + + } + // 子结点数量 = 2 + else + { + // 获取中序遍历中 cur 的下一个结点 + TreeNode? nex = min(cur.right); + if (nex != null) + { + int tmp = nex.val; + // 递归删除结点 nex + remove(nex.val); + // 将 nex 的值复制给 cur + cur.val = tmp; + } + } + return cur; + } + + /* 获取最小结点 */ + private TreeNode? min(TreeNode? root) + { + if (root == null) return root; + // 循环访问左子结点,直到叶结点时为最小结点,跳出 + while (root.left != null) + { + root = root.left; + } + return root; + } + } + + public class binary_search_tree + { + [Test] + public void Test() + { + /* 初始化二叉搜索树 */ + int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; + BinarySearchTree bst = new BinarySearchTree(nums); + Console.WriteLine("\n初始化的二叉树为\n"); + PrintUtil.PrintTree(bst.getRoot()); + + /* 查找结点 */ + TreeNode? node = bst.search(5); + Console.WriteLine("\n查找到的结点对象为 " + node + ",结点值 = " + node.val); + + /* 插入结点 */ + node = bst.insert(16); + Console.WriteLine("\n插入结点 16 后,二叉树为\n"); + PrintUtil.PrintTree(bst.getRoot()); + + /* 删除结点 */ + bst.remove(1); + Console.WriteLine("\n删除结点 1 后,二叉树为\n"); + PrintUtil.PrintTree(bst.getRoot()); + bst.remove(2); + Console.WriteLine("\n删除结点 2 后,二叉树为\n"); + PrintUtil.PrintTree(bst.getRoot()); + bst.remove(4); + Console.WriteLine("\n删除结点 4 后,二叉树为\n"); + PrintUtil.PrintTree(bst.getRoot()); + } + } +} diff --git a/codes/csharp/chapter_tree/binary_tree.cs b/codes/csharp/chapter_tree/binary_tree.cs new file mode 100644 index 00000000..275a69f9 --- /dev/null +++ b/codes/csharp/chapter_tree/binary_tree.cs @@ -0,0 +1,46 @@ +/** + * File: binary_tree.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using hello_algo.include; +using NUnit.Framework; + +namespace hello_algo.chapter_tree +{ + + public class binary_tree + { + [Test] + public void Test() + { + /* 初始化二叉树 */ + // 初始化结点 + TreeNode n1 = new TreeNode(1); + TreeNode n2 = new TreeNode(2); + TreeNode n3 = new TreeNode(3); + TreeNode n4 = new TreeNode(4); + TreeNode n5 = new TreeNode(5); + // 构建引用指向(即指针) + n1.left = n2; + n1.right = n3; + n2.left = n4; + n2.right = n5; + Console.WriteLine("\n初始化二叉树\n"); + PrintUtil.PrintTree(n1); + + /* 插入与删除结点 */ + TreeNode P = new TreeNode(0); + // 在 n1 -> n2 中间插入结点 P + n1.left = P; + P.left = n2; + Console.WriteLine("\n插入结点 P 后\n"); + PrintUtil.PrintTree(n1); + // 删除结点 P + n1.left = n2; + Console.WriteLine("\n删除结点 P 后\n"); + PrintUtil.PrintTree(n1); + } + } +} \ No newline at end of file diff --git a/codes/csharp/chapter_tree/binary_tree_bfs.cs b/codes/csharp/chapter_tree/binary_tree_bfs.cs new file mode 100644 index 00000000..31f70724 --- /dev/null +++ b/codes/csharp/chapter_tree/binary_tree_bfs.cs @@ -0,0 +1,53 @@ +/** + * File: binary_tree_bfs.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using hello_algo.include; +using NUnit.Framework; + +namespace hello_algo.chapter_tree +{ + public class binary_tree_bfs + { + + /// + /// 层序遍历 + /// + /// + /// + public List hierOrder(TreeNode root) + { + // 初始化队列,加入根结点 + Queue queue = new(); + queue.Enqueue(root); + // 初始化一个列表,用于保存遍历序列 + List list = new(); + while (queue.Count != 0) + { + TreeNode node = queue.Dequeue(); // 队列出队 + list.Add(node.val); // 保存结点值 + if (node.left != null) + queue.Enqueue(node.left); // 左子结点入队 + if (node.right != null) + queue.Enqueue(node.right); // 右子结点入队 + } + return list; + } + + [Test] + public void Test() + { + /* 初始化二叉树 */ + // 这里借助了一个从数组直接生成二叉树的函数 + TreeNode? root = TreeNode.ArrToTree(new int?[] { + 1, 2, 3, 4, 5, 6, 7, null, null, null, null, null, null, null, null}); + Console.WriteLine("\n初始化二叉树\n"); + PrintUtil.PrintTree(root); + + List list = hierOrder(root); + Console.WriteLine("\n层序遍历的结点打印序列 = " + string.Join(",", list.ToArray())); + } + } +} diff --git a/codes/csharp/chapter_tree/binary_tree_dfs.cs b/codes/csharp/chapter_tree/binary_tree_dfs.cs new file mode 100644 index 00000000..6e38ebe4 --- /dev/null +++ b/codes/csharp/chapter_tree/binary_tree_dfs.cs @@ -0,0 +1,78 @@ +/** + * File: binary_tree_bfs.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +using hello_algo.include; +using NUnit.Framework; + +namespace hello_algo.chapter_tree +{ + public class binary_tree_dfs + { + List list = new(); + + /// + /// 前序遍历 + /// + /// + void preOrder(TreeNode? root) + { + if (root == null) return; + // 访问优先级:根结点 -> 左子树 -> 右子树 + list.Add(root.val); + preOrder(root.left); + preOrder(root.right); + } + + /// + /// 中序遍历 + /// + /// + void inOrder(TreeNode? root) + { + if (root == null) return; + // 访问优先级:左子树 -> 根结点 -> 右子树 + inOrder(root.left); + list.Add(root.val); + inOrder(root.right); + } + + /// + /// 后序遍历 + /// + /// + void postOrder(TreeNode? root) + { + if (root == null) return; + // 访问优先级:左子树 -> 右子树 -> 根结点 + postOrder(root.left); + postOrder(root.right); + list.Add(root.val); + } + + [Test] + public void Test() + { + /* 初始化二叉树 */ + // 这里借助了一个从数组直接生成二叉树的函数 + TreeNode? root = TreeNode.ArrToTree(new int?[] { + 1, 2, 3, 4, 5, 6, 7, null, null, null, null, null, null, null, null}); + Console.WriteLine("\n初始化二叉树\n"); + PrintUtil.PrintTree(root); + + list.Clear(); + preOrder(root); + Console.WriteLine("\n前序遍历的结点打印序列 = " + string.Join(",", list.ToArray())); + + list.Clear(); + inOrder(root); + Console.WriteLine("\n中序遍历的结点打印序列 = " + string.Join(",", list.ToArray())); + + list.Clear(); + postOrder(root); + Console.WriteLine("\n后序遍历的结点打印序列 = " + string.Join(",", list.ToArray())); + } + } +} diff --git a/codes/csharp/include/ListNode.cs b/codes/csharp/include/ListNode.cs index 580a1d37..0fe2913e 100644 --- a/codes/csharp/include/ListNode.cs +++ b/codes/csharp/include/ListNode.cs @@ -10,7 +10,7 @@ namespace hello_algo.include public class ListNode { public int val; - public ListNode next; + public ListNode? next; /// /// Generate a linked list with an array @@ -26,7 +26,7 @@ namespace hello_algo.include /// /// /// - public static ListNode ArrToLinkedList(int[] arr) + public static ListNode? ArrToLinkedList(int[] arr) { ListNode dum = new ListNode(0); ListNode head = dum; @@ -44,7 +44,7 @@ namespace hello_algo.include /// /// /// - public static ListNode GetListNode(ListNode head, int val) + public static ListNode? GetListNode(ListNode? head, int val) { while (head != null && head.val != val) { diff --git a/codes/csharp/include/PrintUtil.cs b/codes/csharp/include/PrintUtil.cs new file mode 100644 index 00000000..06e85a93 --- /dev/null +++ b/codes/csharp/include/PrintUtil.cs @@ -0,0 +1,124 @@ +/** + * File: PrintUtil.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +namespace hello_algo.include +{ + public class Trunk + { + public Trunk? prev; + public String str; + + public Trunk(Trunk? prev, String str) + { + this.prev = prev; + this.str = str; + } + }; + + public class PrintUtil + { + /** + * Print a linked list + * @param head + */ + public static void PrintLinkedList(ListNode head) + { + List list = new(); + while (head != null) + { + list.Add(head.val.ToString()); + head = head.next; + } + Console.Write(String.Join(" -> ", list)); + } + + /** + * The interface of the tree printer + * This tree printer is borrowed from TECHIE DELIGHT + * https://www.techiedelight.com/c-program-print-binary-tree/ + * @param root + */ + public static void PrintTree(TreeNode? root) + { + PrintTree(root, null, false); + } + + /** + * Print a binary tree + * @param root + * @param prev + * @param isLeft + */ + public static void PrintTree(TreeNode? root, Trunk? prev, bool isLeft) + { + if (root == null) + { + return; + } + + String prev_str = " "; + Trunk trunk = new Trunk(prev, prev_str); + + PrintTree(root.right, trunk, true); + + if (prev == null) + { + trunk.str = "———"; + } + else if (isLeft) + { + trunk.str = "/———"; + prev_str = " |"; + } + else + { + trunk.str = "\\———"; + prev.str = prev_str; + } + + showTrunks(trunk); + Console.WriteLine(" " + root.val); + + if (prev != null) + { + prev.str = prev_str; + } + trunk.str = " |"; + + PrintTree(root.left, trunk, false); + } + + /** + * Helper function to print branches of the binary tree + * @param p + */ + public static void showTrunks(Trunk? p) + { + if (p == null) + { + return; + } + + showTrunks(p.prev); + Console.Write(p.str); + } + + /** + * Print a hash map + * @param + * @param + * @param map + */ + public static void printHashMap(Dictionary map) where K : notnull + { + foreach (var kv in map.Keys) + { + Console.WriteLine(kv.ToString() + " -> " + map[kv]?.ToString()); + } + } + } + +} diff --git a/codes/csharp/include/TreeNode.cs b/codes/csharp/include/TreeNode.cs new file mode 100644 index 00000000..e5cacd59 --- /dev/null +++ b/codes/csharp/include/TreeNode.cs @@ -0,0 +1,98 @@ +/** + * File: TreeNode.cs + * Created Time: 2022-12-23 + * Author: haptear (haptear@hotmail.com) + */ + +namespace hello_algo.include +{ + public class TreeNode + { + public int val; // 结点值 + public int height; // 结点高度 + public TreeNode? left; // 左子结点引用 + public TreeNode? right; // 右子结点引用 + + public TreeNode(int x) + { + val = x; + } + + /** + * Generate a binary tree with an array + * @param arr + * @return + */ + public static TreeNode? ArrToTree(int?[] arr) + { + if (arr.Length == 0 || arr[0] == null) + return null; + + TreeNode root = new TreeNode((int) arr[0]); + Queue queue = new Queue(); + queue.Enqueue(root); + int i = 1; + while (queue.Count!=0) + { + TreeNode node = queue.Dequeue(); + if (arr[i] != null) + { + node.left = new TreeNode((int) arr[i]); + queue.Enqueue(node.left); + } + i++; + if (arr[i] != null) + { + node.right = new TreeNode((int) arr[i]); + queue.Enqueue(node.right); + } + i++; + } + return root; + } + + /** + * Serialize a binary tree to a list + * @param root + * @return + */ + public static List TreeToList(TreeNode root) + { + List list = new(); + if (root == null) return list; + Queue queue = new(); + while (queue.Count != 0) + { + TreeNode? node = queue.Dequeue(); + if (node != null) + { + list.Add(node.val); + queue.Enqueue(node.left); + queue.Enqueue(node.right); + } + else + { + list.Add(null); + } + } + return list; + } + + /** + * Get a tree node with specific value in a binary tree + * @param root + * @param val + * @return + */ + public static TreeNode? GetTreeNode(TreeNode? root, int val) + { + if (root == null) + return null; + if (root.val == val) + return root; + TreeNode? left = GetTreeNode(root.left, val); + TreeNode? right = GetTreeNode(root.right, val); + return left != null ? left : right; + } + } +} diff --git a/codes/go/chapter_array_and_linkedlist/my_list.go b/codes/go/chapter_array_and_linkedlist/my_list.go index cd465d58..8f13630b 100644 --- a/codes/go/chapter_array_and_linkedlist/my_list.go +++ b/codes/go/chapter_array_and_linkedlist/my_list.go @@ -79,16 +79,19 @@ func (l *MyList) insert(num, index int) { } /* 删除元素 */ -func (l *MyList) remove(index int) { +func (l *MyList) remove(index int) int { if index >= l.numsSize { panic("索引越界") } + num := l.nums[index] // 索引 i 之后的元素都向前移动一位 for j := index; j < l.numsSize-1; j++ { l.nums[j] = l.nums[j+1] } // 更新元素数量 l.numsSize-- + // 返回被删除元素 + return num } /* 列表扩容 */ @@ -103,4 +106,4 @@ func (l *MyList) extendCapacity() { func (l *MyList) toArray() []int { // 仅转换有效长度范围内的列表元素 return l.nums[:l.numsSize] -} \ No newline at end of file +} diff --git a/codes/go/chapter_computational_complexity/space_complexity.go b/codes/go/chapter_computational_complexity/space_complexity.go index 77780ae0..eb3ebec9 100644 --- a/codes/go/chapter_computational_complexity/space_complexity.go +++ b/codes/go/chapter_computational_complexity/space_complexity.go @@ -42,7 +42,7 @@ func printTree(root *TreeNode) { printTree(root.right) } -/* 函数(或称方法)*/ +/* 函数 */ func function() int { // do something... return 0 diff --git a/codes/go/chapter_sorting/merge_sort/merge_sort.go b/codes/go/chapter_sorting/merge_sort/merge_sort.go index e1ae9d6a..830e2dfa 100644 --- a/codes/go/chapter_sorting/merge_sort/merge_sort.go +++ b/codes/go/chapter_sorting/merge_sort/merge_sort.go @@ -21,15 +21,15 @@ func merge(nums []int, left, mid, right int) { i, j := left_start, right_start // 通过覆盖原数组 nums 来合并左子数组和右子数组 for k := left; k <= right; k++ { - // 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ if i > left_end { nums[k] = tmp[j] j++ - // 否则,若 “右子数组已全部合并完” 或 “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ + // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ } else if j > right_end || tmp[i] <= tmp[j] { nums[k] = tmp[i] i++ - // 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ } else { nums[k] = tmp[j] j++ diff --git a/codes/java/chapter_sorting/merge_sort.java b/codes/java/chapter_sorting/merge_sort.java index acaa60b1..498a6fd8 100644 --- a/codes/java/chapter_sorting/merge_sort.java +++ b/codes/java/chapter_sorting/merge_sort.java @@ -25,13 +25,13 @@ public class merge_sort { int i = leftStart, j = rightStart; // 通过覆盖原数组 nums 来合并左子数组和右子数组 for (int k = left; k <= right; k++) { - // 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ if (i > leftEnd) nums[k] = tmp[j++]; - // 否则,若 “右子数组已全部合并完” 或 “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ + // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ else if (j > rightEnd || tmp[i] <= tmp[j]) nums[k] = tmp[i++]; - // 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ else nums[k] = tmp[j++]; } diff --git a/codes/java/chapter_stack_and_queue/array_queue.java b/codes/java/chapter_stack_and_queue/array_queue.java index 6ffb546c..198c9071 100644 --- a/codes/java/chapter_stack_and_queue/array_queue.java +++ b/codes/java/chapter_stack_and_queue/array_queue.java @@ -63,13 +63,6 @@ class ArrayQueue { return nums[front]; } - /* 访问索引 index 处元素 */ - int get(int index) { - if (index >= size()) - throw new IndexOutOfBoundsException(); - return nums[(front + index) % capacity()]; - } - /* 返回数组 */ public int[] toArray() { int size = size(); diff --git a/codes/java/chapter_stack_and_queue/array_stack.java b/codes/java/chapter_stack_and_queue/array_stack.java index 44d0d70c..12ac5283 100644 --- a/codes/java/chapter_stack_and_queue/array_stack.java +++ b/codes/java/chapter_stack_and_queue/array_stack.java @@ -45,13 +45,6 @@ class ArrayStack { return stack.get(size() - 1); } - /* 访问索引 index 处元素 */ - public int get(int index) { - if (index >= size()) - throw new IndexOutOfBoundsException(); - return stack.get(index); - } - /* 将 List 转化为 Array 并返回 */ public Object[] toArray() { return stack.toArray(); @@ -75,10 +68,6 @@ public class array_stack { int peek = stack.peek(); System.out.println("栈顶元素 peek = " + peek); - /* 访问索引 index 处元素 */ - int num = stack.get(3); - System.out.println("栈索引 3 处的元素为 num = " + num); - /* 元素出栈 */ int pop = stack.pop(); System.out.println("出栈元素 pop = " + pop + ",出栈后 stack = " + Arrays.toString(stack.toArray())); diff --git a/codes/javascript/chapter_hashing/array_hash_map.js b/codes/javascript/chapter_hashing/array_hash_map.js new file mode 100644 index 00000000..f5c77aa2 --- /dev/null +++ b/codes/javascript/chapter_hashing/array_hash_map.js @@ -0,0 +1,129 @@ +/* + * File: array_hash_map.js + * Created Time: 2022-12-26 + * Author: Justin (xiefahit@gmail.com) + */ + +/* 键值对 Number -> String */ +class Entry { + constructor(key, val) { + this.key = key; + this.val = val; + } +} + +/* 基于数组简易实现的哈希表 */ +class ArrayHashMap { + #bucket; + constructor() { + // 初始化一个长度为 100 的桶(数组) + this.#bucket = new Array(100).fill(null); + } + + /* 哈希函数 */ + #hashFunc(key) { + return key % 100; + } + + /* 查询操作 */ + get(key) { + let index = this.#hashFunc(key); + let entry = this.#bucket[index]; + if (entry === null) return null; + return entry.val; + } + + /* 添加操作 */ + set(key, val) { + let index = this.#hashFunc(key); + this.#bucket[index] = new Entry(key, val); + } + + /* 删除操作 */ + delete(key) { + let index = this.#hashFunc(key); + // 置为 null ,代表删除 + this.#bucket[index] = null; + } + + /* 获取所有键值对 */ + entries() { + let arr = []; + for (let i = 0; i < this.#bucket.length; i++) { + if (this.#bucket[i]) { + arr.push(this.#bucket[i]); + } + } + return arr; + } + + /* 获取所有键 */ + keys() { + let arr = []; + for (let i = 0; i < this.#bucket.length; i++) { + if (this.#bucket[i]) { + arr.push(this.#bucket[i]?.key); + } + } + return arr; + } + + /* 获取所有值 */ + values() { + let arr = []; + for (let i = 0; i < this.#bucket.length; i++) { + if (this.#bucket[i]) { + arr.push(this.#bucket[i]?.val); + } + } + return arr; + } + + /* 打印哈希表 */ + print() { + let entrySet = this.entries(); + for (const entry of entrySet) { + if (!entry) continue; + console.info(`${entry.key} -> ${entry.val}`); + } + } +} + +/* Driver Code */ +/* 初始化哈希表 */ +const map = new ArrayHashMap(); +/* 添加操作 */ +// 在哈希表中添加键值对 (key, value) +map.set(12836, '小哈'); +map.set(15937, '小啰'); +map.set(16750, '小算'); +map.set(13276, '小法'); +map.set(10583, '小鸭'); +console.info('\n添加完成后,哈希表为\nKey -> Value'); +map.print(); + +/* 查询操作 */ +// 向哈希表输入键 key ,得到值 value +let name = map.get(15937); +console.info('\n输入学号 15937 ,查询到姓名 ' + name); + +/* 删除操作 */ +// 在哈希表中删除键值对 (key, value) +map.delete(10583); +console.info('\n删除 10583 后,哈希表为\nKey -> Value'); +map.print(); + +/* 遍历哈希表 */ +console.info('\n遍历键值对 Key->Value'); +for (const entry of map.entries()) { + if (!entry) continue; + console.info(entry.key + ' -> ' + entry.val); +} +console.info('\n单独遍历键 Key'); +for (const key of map.keys()) { + console.info(key); +} +console.info('\n单独遍历值 Value'); +for (const val of map.values()) { + console.info(val); +} diff --git a/codes/javascript/chapter_hashing/hash_map.js b/codes/javascript/chapter_hashing/hash_map.js new file mode 100644 index 00000000..197fb7dd --- /dev/null +++ b/codes/javascript/chapter_hashing/hash_map.js @@ -0,0 +1,44 @@ +/* + * File: hash_map.js + * Created Time: 2022-12-26 + * Author: Justin (xiefahit@gmail.com) + */ + +/* Driver Code */ +/* 初始化哈希表 */ +const map = new Map(); + +/* 添加操作 */ +// 在哈希表中添加键值对 (key, value) +map.set(12836, '小哈'); +map.set(15937, '小啰'); +map.set(16750, '小算'); +map.set(13276, '小法'); +map.set(10583, '小鸭'); +console.info('\n添加完成后,哈希表为\nKey -> Value'); +console.info(map); + +/* 查询操作 */ +// 向哈希表输入键 key ,得到值 value +let name = map.get(15937); +console.info('\n输入学号 15937 ,查询到姓名 ' + name); + +/* 删除操作 */ +// 在哈希表中删除键值对 (key, value) +map.delete(10583); +console.info('\n删除 10583 后,哈希表为\nKey -> Value'); +console.info(map); + +/* 遍历哈希表 */ +console.info('\n遍历键值对 Key->Value'); +for (const [k, v] of map.entries()) { + console.info(k + ' -> ' + v); +} +console.info('\n单独遍历键 Key'); +for (const k of map.keys()) { + console.info(k); +} +console.info('\n单独遍历值 Value'); +for (const v of map.values()) { + console.info(v); +} diff --git a/codes/javascript/chapter_searching/binary_search.js b/codes/javascript/chapter_searching/binary_search.js new file mode 100644 index 00000000..3cca1788 --- /dev/null +++ b/codes/javascript/chapter_searching/binary_search.js @@ -0,0 +1,53 @@ +/** + * File: binary_search.js + * Created Time: 2022-12-22 + * Author: JoseHung (szhong@link.cuhk.edu.hk) + */ + +/* 二分查找(双闭区间) */ +function binarySearch(nums, target) { + // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + let i = 0, j = nums.length - 1; + // 循环,当搜索区间为空时跳出(当 i > j 时为空) + while (i <= j) { + let m = parseInt((i + j) / 2); // 计算中点索引 m ,在 JS 中需使用 parseInt 函数取整 + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1; + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中 + j = m - 1; + else + return m; // 找到目标元素,返回其索引 + } + // 未找到目标元素,返回 -1 + return -1; +} + +/* 二分查找(左闭右开) */ +function binarySearch1(nums, target) { + // 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 + let i = 0, j = nums.length; + // 循环,当搜索区间为空时跳出(当 i = j 时为空) + while (i < j) { + let m = parseInt((i + j) / 2); // 计算中点索引 m ,在 JS 中需使用 parseInt 函数取整 + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j) 中 + i = m + 1; + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中 + j = m; + else // 找到目标元素,返回其索引 + return m; + } + // 未找到目标元素,返回 -1 + return -1; +} + +/* Driver Code */ +var target = 6; +var nums = [1, 3, 6, 8, 12, 15, 23, 67, 70, 92]; + +/* 二分查找(双闭区间) */ +var index = binarySearch(nums, target); +console.log("目标元素 6 的索引 = " + index); + +/* 二分查找(左闭右开) */ +index = binarySearch1(nums, target); +console.log("目标元素 6 的索引 = " + index); diff --git a/codes/javascript/chapter_searching/linear_search.js b/codes/javascript/chapter_searching/linear_search.js new file mode 100644 index 00000000..b71f0ced --- /dev/null +++ b/codes/javascript/chapter_searching/linear_search.js @@ -0,0 +1,48 @@ +/** + * File: linear-search.js + * Created Time: 2022-12-22 + * Author: JoseHung (szhong@link.cuhk.edu.hk) + */ + +const ListNode = require("../include/ListNode"); + +/* 线性查找(数组) */ +function linearSearchArray(nums, target) { + // 遍历数组 + for (let i = 0; i < nums.length; i++) { + // 找到目标元素,返回其索引 + if (nums[i] === target) { + return i; + } + } + // 未找到目标元素,返回 -1 + return -1; +} + +/* 线性查找(链表)*/ +function linearSearchLinkedList(head, target) { + // 遍历链表 + while(head) { + // 找到目标结点,返回之 + if(head.val === target) { + return head; + } + head = head.next; + } + // 未找到目标结点,返回 null + return null; +} + +/* Driver Code */ +var target = 3; + +/* 在数组中执行线性查找 */ +var nums = [1, 5, 3, 2, 4, 7, 5, 9, 10, 8]; +var index = linearSearchArray(nums, target); +console.log("目标元素 3 的索引 = " + index); + +/* 在链表中执行线性查找 */ +var linkedList = new ListNode(); +var head = linkedList.arrToLinkedList(nums); +var node = linearSearchLinkedList(head, target); +console.log("目标结点值 3 的对应结点对象为 " + node); diff --git a/codes/javascript/chapter_sorting/merge_sort.js b/codes/javascript/chapter_sorting/merge_sort.js index d57e41a8..31860ce1 100644 --- a/codes/javascript/chapter_sorting/merge_sort.js +++ b/codes/javascript/chapter_sorting/merge_sort.js @@ -20,13 +20,13 @@ function merge(nums, left, mid, right) { let i = leftStart, j = rightStart; // 通过覆盖原数组 nums 来合并左子数组和右子数组 for (let k = left; k <= right; k++) { - // 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ if (i > leftEnd) { nums[k] = tmp[j++]; - // 否则,若 “右子数组已全部合并完” 或 “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ + // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ } else if (j > rightEnd || tmp[i] <= tmp[j]) { nums[k] = tmp[i++]; - // 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ } else { nums[k] = tmp[j++]; } diff --git a/codes/javascript/chapter_stack_and_queue/array_queue.js b/codes/javascript/chapter_stack_and_queue/array_queue.js index d57b7024..abe0766d 100644 --- a/codes/javascript/chapter_stack_and_queue/array_queue.js +++ b/codes/javascript/chapter_stack_and_queue/array_queue.js @@ -56,13 +56,6 @@ class ArrayQueue { return this.#queue[this.#front]; } - /* 访问指定索引元素 */ - get(index) { - if (index >= this.size) - throw new Error("索引越界"); - return this.#queue[(this.#front + index) % this.capacity]; - } - /* 返回 Array */ toArray() { const siz = this.size; @@ -95,10 +88,6 @@ console.log(queue.toArray()); const peek = queue.peek(); console.log("队首元素 peek = " + peek); -/* 访问指定索引元素 */ -const num = queue.get(2); -console.log("队列第 3 个元素为 num = " + num); - /* 元素出队 */ const poll = queue.poll(); console.log("出队元素 poll = " + poll + ",出队后 queue = "); diff --git a/codes/javascript/chapter_stack_and_queue/array_stack.js b/codes/javascript/chapter_stack_and_queue/array_stack.js index ba620ebd..37fa23b1 100644 --- a/codes/javascript/chapter_stack_and_queue/array_stack.js +++ b/codes/javascript/chapter_stack_and_queue/array_stack.js @@ -41,13 +41,6 @@ class ArrayStack { return this.stack[this.stack.length - 1]; } - /* 访问索引 index 处元素 */ - get(index) { - if (index >= this.size) - throw new Error("索引越界"); - return this.stack[index]; - } - /* 返回 Array */ toArray() { return this.stack; @@ -73,10 +66,6 @@ console.log(stack.toArray()); const top = stack.top(); console.log("栈顶元素 top = " + top); -/* 访问索引 index 处元素 */ -const num = stack.get(3); -console.log("栈索引 3 处的元素为 num = " + num); - /* 元素出栈 */ const pop = stack.pop(); console.log("出栈元素 pop = " + pop + ",出栈后 stack = "); diff --git a/codes/javascript/chapter_stack_and_queue/linkedlist_stack.js b/codes/javascript/chapter_stack_and_queue/linkedlist_stack.js new file mode 100644 index 00000000..c6e9f27c --- /dev/null +++ b/codes/javascript/chapter_stack_and_queue/linkedlist_stack.js @@ -0,0 +1,89 @@ +/** + * File: linkedlist_stack.js + * Created Time: 2022-12-22 + * Author: S-N-O-R-L-A-X (snorlax.xu@outlook.com) + */ + +const ListNode = require("../include/ListNode"); + +/* 基于链表实现的栈 */ +class LinkedListStack { + #stackPeek; // 将头结点作为栈顶 + #stkSize = 0; // 栈的长度 + + constructor() { + this.#stackPeek = null; + } + + /* 获取栈的长度 */ + get size() { + return this.#stkSize; + } + + /* 判断栈是否为空 */ + isEmpty() { + return this.size == 0; + } + + /* 入栈 */ + push(num) { + const node = new ListNode(num); + node.next = this.#stackPeek; + this.#stackPeek = node; + this.#stkSize++; + } + + /* 出栈 */ + pop() { + const num = this.peek(); + this.#stackPeek = this.#stackPeek.next; + this.#stkSize--; + return num; + } + + /* 访问栈顶元素 */ + peek() { + if (!this.#stackPeek) + throw new Error("栈为空!"); + return this.#stackPeek.val; + } + + /* 将链表转化为 Array 并返回 */ + toArray() { + let node = this.#stackPeek; + const res = new Array(this.size); + for (let i = res.length - 1; i >= 0; i--) { + res[i] = node.val; + node = node.next; + } + return res; + } +} + + +/* 初始化栈 */ +const stack = new LinkedListStack(); + +/* 元素入栈 */ +stack.push(1); +stack.push(3); +stack.push(2); +stack.push(5); +stack.push(4); +console.log("栈 stack = " + stack.toArray()); + +/* 访问栈顶元素 */ +const peek = stack.peek(); +console.log("栈顶元素 peek = " + peek); + +/* 元素出栈 */ +const pop = stack.pop(); +console.log("出栈元素 pop = " + pop + ",出栈后 stack = " + stack.toArray()); + +/* 获取栈的长度 */ +const size = stack.size; +console.log("栈的长度 size = " + size); + +/* 判断是否为空 */ +const isEmpty = stack.isEmpty(); +console.log("栈是否为空 = " + isEmpty); diff --git a/codes/javascript/include/ListNode.js b/codes/javascript/include/ListNode.js index 72306548..6e226e9c 100755 --- a/codes/javascript/include/ListNode.js +++ b/codes/javascript/include/ListNode.js @@ -22,7 +22,7 @@ class ListNode { */ arrToLinkedList(arr) { const dum = new ListNode(0); - const head = dum; + let head = dum; for (const val of arr) { head.next = new ListNode(val); head = head.next; diff --git a/codes/python/chapter_array_and_linkedlist/array.py b/codes/python/chapter_array_and_linkedlist/array.py index 7f34472c..8933fb48 100644 --- a/codes/python/chapter_array_and_linkedlist/array.py +++ b/codes/python/chapter_array_and_linkedlist/array.py @@ -1,8 +1,8 @@ -''' +""" File: array.py Created Time: 2022-11-25 Author: Krahets (krahets@163.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) diff --git a/codes/python/chapter_array_and_linkedlist/linked_list.py b/codes/python/chapter_array_and_linkedlist/linked_list.py index d176d8dd..dce11034 100644 --- a/codes/python/chapter_array_and_linkedlist/linked_list.py +++ b/codes/python/chapter_array_and_linkedlist/linked_list.py @@ -1,8 +1,8 @@ -''' +""" File: linked_list.py Created Time: 2022-11-25 Author: Krahets (krahets@163.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) diff --git a/codes/python/chapter_array_and_linkedlist/list.py b/codes/python/chapter_array_and_linkedlist/list.py index ac71b98b..b0e2f253 100644 --- a/codes/python/chapter_array_and_linkedlist/list.py +++ b/codes/python/chapter_array_and_linkedlist/list.py @@ -1,8 +1,8 @@ -''' +""" File: list.py Created Time: 2022-11-25 Author: Krahets (krahets@163.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) diff --git a/codes/python/chapter_array_and_linkedlist/my_list.py b/codes/python/chapter_array_and_linkedlist/my_list.py index 077fc0ce..bd76fdcd 100644 --- a/codes/python/chapter_array_and_linkedlist/my_list.py +++ b/codes/python/chapter_array_and_linkedlist/my_list.py @@ -1,8 +1,8 @@ -''' +""" File: my_list.py Created Time: 2022-11-25 Author: Krahets (krahets@163.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) @@ -55,11 +55,14 @@ class MyList: """ 删除元素 """ def remove(self, index): assert index < self.__size, "索引越界" + num = self.nums[index] # 索引 i 之后的元素都向前移动一位 for j in range(index, self.__size - 1): self.__nums[j] = self.__nums[j + 1] # 更新元素数量 self.__size -= 1 + # 返回被删除元素 + return num """ 列表扩容 """ def extend_capacity(self): diff --git a/codes/python/chapter_computational_complexity/leetcode_two_sum.py b/codes/python/chapter_computational_complexity/leetcode_two_sum.py index 182caec3..cb3bf3b0 100644 --- a/codes/python/chapter_computational_complexity/leetcode_two_sum.py +++ b/codes/python/chapter_computational_complexity/leetcode_two_sum.py @@ -1,8 +1,8 @@ -''' +""" File: leetcode_two_sum.py Created Time: 2022-11-25 Author: Krahets (krahets@163.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) diff --git a/codes/python/chapter_computational_complexity/space_complexity.py b/codes/python/chapter_computational_complexity/space_complexity.py index 69c1ac01..7efe8d94 100644 --- a/codes/python/chapter_computational_complexity/space_complexity.py +++ b/codes/python/chapter_computational_complexity/space_complexity.py @@ -1,8 +1,8 @@ -''' +""" File: space_complexity.py Created Time: 2022-11-25 Author: Krahets (krahets@163.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) diff --git a/codes/python/chapter_computational_complexity/time_complexity.py b/codes/python/chapter_computational_complexity/time_complexity.py index a3e69838..bfb5b3b1 100644 --- a/codes/python/chapter_computational_complexity/time_complexity.py +++ b/codes/python/chapter_computational_complexity/time_complexity.py @@ -1,8 +1,8 @@ -''' +""" File: time_complexity.py Created Time: 2022-11-25 Author: Krahets (krahets@163.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) diff --git a/codes/python/chapter_computational_complexity/worst_best_time_complexity.py b/codes/python/chapter_computational_complexity/worst_best_time_complexity.py index 7b052a3e..b70dce4f 100644 --- a/codes/python/chapter_computational_complexity/worst_best_time_complexity.py +++ b/codes/python/chapter_computational_complexity/worst_best_time_complexity.py @@ -1,8 +1,8 @@ -''' +""" File: worst_best_time_complexity.py Created Time: 2022-11-25 Author: Krahets (krahets@163.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) diff --git a/codes/python/chapter_searching/binary_search.py b/codes/python/chapter_searching/binary_search.py index 3d9ee0ff..275ae28e 100644 --- a/codes/python/chapter_searching/binary_search.py +++ b/codes/python/chapter_searching/binary_search.py @@ -1,8 +1,8 @@ -''' +""" File: binary_search.py Created Time: 2022-11-26 Author: timi (xisunyy@163.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) diff --git a/codes/python/chapter_searching/hashing_search.py b/codes/python/chapter_searching/hashing_search.py index 68cffe64..f49c703b 100644 --- a/codes/python/chapter_searching/hashing_search.py +++ b/codes/python/chapter_searching/hashing_search.py @@ -1,8 +1,8 @@ -''' +""" File: hashing_search.py Created Time: 2022-11-26 Author: timi (xisunyy@163.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) diff --git a/codes/python/chapter_searching/linear_search.py b/codes/python/chapter_searching/linear_search.py index 93309565..8ff9e080 100644 --- a/codes/python/chapter_searching/linear_search.py +++ b/codes/python/chapter_searching/linear_search.py @@ -1,8 +1,8 @@ -''' +""" File: linear_search.py Created Time: 2022-11-26 Author: timi (xisunyy@163.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) diff --git a/codes/python/chapter_sorting/bubble_sort.py b/codes/python/chapter_sorting/bubble_sort.py index 54260dd9..610e3186 100644 --- a/codes/python/chapter_sorting/bubble_sort.py +++ b/codes/python/chapter_sorting/bubble_sort.py @@ -1,8 +1,8 @@ -''' +""" File: bubble_sort.py Created Time: 2022-11-25 Author: timi (xisunyy@163.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) diff --git a/codes/python/chapter_sorting/insertion_sort.py b/codes/python/chapter_sorting/insertion_sort.py index db85515d..bf8492cd 100644 --- a/codes/python/chapter_sorting/insertion_sort.py +++ b/codes/python/chapter_sorting/insertion_sort.py @@ -1,8 +1,8 @@ -''' +""" File: insertion_sort.py Created Time: 2022-11-25 Author: timi (xisunyy@163.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) diff --git a/codes/python/chapter_sorting/merge_sort.py b/codes/python/chapter_sorting/merge_sort.py index 09aa09bb..f3fb538a 100644 --- a/codes/python/chapter_sorting/merge_sort.py +++ b/codes/python/chapter_sorting/merge_sort.py @@ -1,8 +1,8 @@ -''' +""" File: merge_sort.py Created Time: 2022-11-25 Author: timi (xisunyy@163.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) @@ -24,15 +24,15 @@ def merge(nums, left, mid, right): i, j = left_start, right_start # 通过覆盖原数组 nums 来合并左子数组和右子数组 for k in range(left, right + 1): - # 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ + # 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ if i > left_end: nums[k] = tmp[j] j += 1 - # 否则,若 “右子数组已全部合并完” 或 “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ + # 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ elif j > right_end or tmp[i] <= tmp[j]: nums[k] = tmp[i] i += 1 - # 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + # 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ else: nums[k] = tmp[j] j += 1 diff --git a/codes/python/chapter_sorting/quick_sort.py b/codes/python/chapter_sorting/quick_sort.py index 5ba826bf..f5ce8120 100644 --- a/codes/python/chapter_sorting/quick_sort.py +++ b/codes/python/chapter_sorting/quick_sort.py @@ -1,8 +1,8 @@ -''' +""" File: quick_sort.py Created Time: 2022-11-25 Author: timi (xisunyy@163.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) diff --git a/codes/python/chapter_stack_and_queue/array_queue.py b/codes/python/chapter_stack_and_queue/array_queue.py index 5f3c4f3b..947c0609 100644 --- a/codes/python/chapter_stack_and_queue/array_queue.py +++ b/codes/python/chapter_stack_and_queue/array_queue.py @@ -1,8 +1,8 @@ -''' +""" File: array_queue.py Created Time: 2022-12-01 Author: Peng Chen (pengchzn@gmail.com) -''' +""" import os.path as osp import sys @@ -54,13 +54,6 @@ class ArrayQueue: return False return self.__nums[self.__front] - """ 访问指定位置元素 """ - def get(self, index): - if index >= self.size(): - print("索引越界") - return False - return self.__nums[(self.__front + index) % self.capacity()] - """ 返回列表用于打印 """ def to_list(self): res = [0] * self.size() @@ -88,10 +81,6 @@ if __name__ == "__main__": peek = queue.peek() print("队首元素 peek =", peek) - """ 访问索引 index 处元素 """ - num = queue.get(3) - print("队列索引 3 处的元素为 num =", num) - """ 元素出队 """ poll = queue.poll() print("出队元素 poll =", poll) diff --git a/codes/python/chapter_stack_and_queue/array_stack.py b/codes/python/chapter_stack_and_queue/array_stack.py index 32ba9dcd..18aee16b 100644 --- a/codes/python/chapter_stack_and_queue/array_stack.py +++ b/codes/python/chapter_stack_and_queue/array_stack.py @@ -1,8 +1,8 @@ -''' +""" File: array_stack.py Created Time: 2022-11-29 Author: Peng Chen (pengchzn@gmail.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) @@ -34,11 +34,6 @@ class ArrayStack: def peek(self): assert not self.is_empty(), "栈为空" return self.__stack[-1] - - """ 访问索引 index 处元素 """ - def get(self, index): - assert index < self.size(), "索引越界" - return self.__stack[index] """ 返回列表用于打印 """ def to_list(self): @@ -62,10 +57,6 @@ if __name__ == "__main__": peek = stack.peek() print("栈顶元素 peek =", peek) - """ 访问索引 index 处元素 """ - num = stack.get(3) - print("栈索引 3 处的元素为 num =", num) - """ 元素出栈 """ pop = stack.pop() print("出栈元素 pop =", pop) diff --git a/codes/python/chapter_stack_and_queue/deque.py b/codes/python/chapter_stack_and_queue/deque.py index 7881a965..30444c6b 100644 --- a/codes/python/chapter_stack_and_queue/deque.py +++ b/codes/python/chapter_stack_and_queue/deque.py @@ -1,8 +1,8 @@ -''' +""" File: deque.py Created Time: 2022-11-29 Author: Peng Chen (pengchzn@gmail.com) -''' +""" import os.path as osp import sys diff --git a/codes/python/chapter_stack_and_queue/linkedlist_queue.py b/codes/python/chapter_stack_and_queue/linkedlist_queue.py index 0d4a28d4..2ab7196d 100644 --- a/codes/python/chapter_stack_and_queue/linkedlist_queue.py +++ b/codes/python/chapter_stack_and_queue/linkedlist_queue.py @@ -1,8 +1,8 @@ -''' +""" File: linkedlist_queue.py Created Time: 2022-12-01 Author: Peng Chen (pengchzn@gmail.com) -''' +""" import os.path as osp import sys diff --git a/codes/python/chapter_stack_and_queue/linkedlist_stack.py b/codes/python/chapter_stack_and_queue/linkedlist_stack.py index 5ee90353..21f6ca6a 100644 --- a/codes/python/chapter_stack_and_queue/linkedlist_stack.py +++ b/codes/python/chapter_stack_and_queue/linkedlist_stack.py @@ -1,8 +1,8 @@ -''' +""" File: linkedlist_stack.py Created Time: 2022-11-29 Author: Peng Chen (pengchzn@gmail.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) diff --git a/codes/python/chapter_stack_and_queue/queue.py b/codes/python/chapter_stack_and_queue/queue.py index 740d9b1e..ce220d7c 100644 --- a/codes/python/chapter_stack_and_queue/queue.py +++ b/codes/python/chapter_stack_and_queue/queue.py @@ -1,8 +1,8 @@ -''' +""" File: queue.py Created Time: 2022-11-29 Author: Peng Chen (pengchzn@gmail.com) -''' +""" import os.path as osp import sys diff --git a/codes/python/chapter_stack_and_queue/stack.py b/codes/python/chapter_stack_and_queue/stack.py index a8d2a7a5..5e95258f 100644 --- a/codes/python/chapter_stack_and_queue/stack.py +++ b/codes/python/chapter_stack_and_queue/stack.py @@ -1,8 +1,8 @@ -''' +""" File: stack.py Created Time: 2022-11-29 Author: Peng Chen (pengchzn@gmail.com) -''' +""" import sys, os.path as osp sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) diff --git a/codes/python/chapter_tree/avl_tree.py b/codes/python/chapter_tree/avl_tree.py new file mode 100644 index 00000000..d0ff48cb --- /dev/null +++ b/codes/python/chapter_tree/avl_tree.py @@ -0,0 +1,208 @@ +""" +File: avl_tree.py +Created Time: 2022-12-20 +Author: a16su (lpluls001@gmail.com) +""" + +import sys, os.path as osp +import typing + +sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) +from include import * + + +class AVLTree: + def __init__(self, root: typing.Optional[TreeNode] = None): + self.root = root + + """ 获取结点高度 """ + def height(self, node: typing.Optional[TreeNode]) -> int: + # 空结点高度为 -1 ,叶结点高度为 0 + if node is not None: + return node.height + return -1 + + """ 更新结点高度 """ + def __update_height(self, node: TreeNode): + # 结点高度等于最高子树高度 + 1 + node.height = max([self.height(node.left), self.height(node.right)]) + 1 + + """ 获取平衡因子 """ + def balance_factor(self, node: TreeNode) -> int: + # 空结点平衡因子为 0 + if node is None: + return 0 + # 结点平衡因子 = 左子树高度 - 右子树高度 + return self.height(node.left) - self.height(node.right) + + """ 右旋操作 """ + def __right_rotate(self, node: TreeNode) -> TreeNode: + child = node.left + grand_child = child.right + # 以 child 为原点,将 node 向右旋转 + child.right = node + node.left = grand_child + # 更新结点高度 + self.__update_height(node) + self.__update_height(child) + # 返回旋转后子树的根节点 + return child + + """ 左旋操作 """ + def __left_rotate(self, node: TreeNode) -> TreeNode: + child = node.right + grand_child = child.left + # 以 child 为原点,将 node 向左旋转 + child.left = node + node.right = grand_child + # 更新结点高度 + self.__update_height(node) + self.__update_height(child) + # 返回旋转后子树的根节点 + return child + + """ 执行旋转操作,使该子树重新恢复平衡 """ + def __rotate(self, node: TreeNode) -> TreeNode: + # 获取结点 node 的平衡因子 + balance_factor = self.balance_factor(node) + # 左偏树 + if balance_factor > 1: + if self.balance_factor(node.left) >= 0: + # 右旋 + return self.__right_rotate(node) + else: + # 先左旋后右旋 + node.left = self.__left_rotate(node.left) + return self.__right_rotate(node) + # 右偏树 + elif balance_factor < -1: + if self.balance_factor(node.right) <= 0: + # 左旋 + return self.__left_rotate(node) + else: + # 先右旋后左旋 + node.right = self.__right_rotate(node.right) + return self.__left_rotate(node) + # 平衡树,无需旋转,直接返回 + return node + + """ 插入结点 """ + def insert(self, val) -> TreeNode: + self.root = self.__insert_helper(self.root, val) + return self.root + + """ 递归插入结点(辅助函数)""" + def __insert_helper(self, node: typing.Optional[TreeNode], val: int) -> TreeNode: + if node is None: + return TreeNode(val) + # 1. 查找插入位置,并插入结点 + if val < node.val: + node.left = self.__insert_helper(node.left, val) + elif val > node.val: + node.right = self.__insert_helper(node.right, val) + else: + # 重复结点不插入,直接返回 + return node + # 更新结点高度 + self.__update_height(node) + # 2. 执行旋转操作,使该子树重新恢复平衡 + return self.__rotate(node) + + """ 删除结点 """ + def remove(self, val: int): + root = self.__remove_helper(self.root, val) + return root + + """ 递归删除结点(辅助函数) """ + def __remove_helper(self, node: typing.Optional[TreeNode], val: int) -> typing.Optional[TreeNode]: + if node is None: + return None + # 1. 查找结点,并删除之 + if val < node.val: + node.left = self.__remove_helper(node.left, val) + elif val > node.val: + node.right = self.__remove_helper(node.right, val) + else: + if node.left is None or node.right is None: + child = node.left or node.right + # 子结点数量 = 0 ,直接删除 node 并返回 + if child is None: + return None + # 子结点数量 = 1 ,直接删除 node + else: + node = child + else: # 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + temp = self.__min_node(node.right) + node.right = self.__remove_helper(node.right, temp.val) + node.val = temp.val + # 更新结点高度 + self.__update_height(node) + # 2. 执行旋转操作,使该子树重新恢复平衡 + return self.__rotate(node) + + """ 获取最小结点 """ + def __min_node(self, node: typing.Optional[TreeNode]) -> typing.Optional[TreeNode]: + if node is None: + return None + # 循环访问左子结点,直到叶结点时为最小结点,跳出 + while node.left is not None: + node = node.left + return node + + """ 查找结点 """ + def search(self, val: int): + cur = self.root + # 循环查找,越过叶结点后跳出 + while cur is not None: + # 目标结点在 root 的右子树中 + if cur.val < val: + cur = cur.right + # 目标结点在 root 的左子树中 + elif cur.val > val: + cur = cur.left + # 找到目标结点,跳出循环 + else: + break + # 返回目标结点 + return cur + + +""" Driver Code """ +if __name__ == "__main__": + def test_insert(tree: AVLTree, val: int): + tree.insert(val) + print("\n插入结点 {} 后,AVL 树为".format(val)) + print_tree(tree.root) + + def test_remove(tree: AVLTree, val: int): + tree.remove(val) + print("\n删除结点 {} 后,AVL 树为".format(val)) + print_tree(tree.root) + + # 初始化空 AVL 树 + avl_tree = AVLTree() + + # 插入结点 + # 请关注插入结点后,AVL 树是如何保持平衡的 + test_insert(avl_tree, 1) + test_insert(avl_tree, 2) + test_insert(avl_tree, 3) + test_insert(avl_tree, 4) + test_insert(avl_tree, 5) + test_insert(avl_tree, 8) + test_insert(avl_tree, 7) + test_insert(avl_tree, 9) + test_insert(avl_tree, 10) + test_insert(avl_tree, 6) + + # 插入重复结点 + test_insert(avl_tree, 7) + + # 删除结点 + # 请关注删除结点后,AVL 树是如何保持平衡的 + test_remove(avl_tree, 8) # 删除度为 0 的结点 + test_remove(avl_tree, 5) # 删除度为 1 的结点 + test_remove(avl_tree, 4) # 删除度为 2 的结点 + + result_node = avl_tree.search(7) + print("\n查找到的结点对象为 {},结点值 = {}".format(result_node, result_node.val)) diff --git a/codes/python/chapter_tree/binary_search_tree.py b/codes/python/chapter_tree/binary_search_tree.py index f9dd9ec9..23e34185 100644 --- a/codes/python/chapter_tree/binary_search_tree.py +++ b/codes/python/chapter_tree/binary_search_tree.py @@ -1,10 +1,167 @@ -''' +""" File: binary_search_tree.py -Created Time: 2022-11-25 -Author: Krahets (krahets@163.com) -''' +Created Time: 2022-12-20 +Author: a16su (lpluls001@gmail.com) +""" import sys, os.path as osp +import typing + sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) from include import * + +""" 二叉搜索树 """ +class BinarySearchTree: + def __init__(self, nums: typing.List[int]) -> None: + nums.sort() + self.__root = self.build_tree(nums, 0, len(nums) - 1) + + """ 构建二叉搜索树 """ + def build_tree(self, nums: typing.List[int], start_index: int, end_index: int) -> typing.Optional[TreeNode]: + if start_index > end_index: + return None + + # 将数组中间结点作为根结点 + mid = (start_index + end_index) // 2 + root = TreeNode(nums[mid]) + # 递归建立左子树和右子树 + root.left = self.build_tree(nums=nums, start_index=start_index, end_index=mid - 1) + root.right = self.build_tree(nums=nums, start_index=mid + 1, end_index=end_index) + return root + + @property + def root(self) -> typing.Optional[TreeNode]: + return self.__root + + """ 查找结点 """ + def search(self, num: int) -> typing.Optional[TreeNode]: + cur = self.root + # 循环查找,越过叶结点后跳出 + while cur is not None: + # 目标结点在 root 的右子树中 + if cur.val < num: + cur = cur.right + # 目标结点在 root 的左子树中 + elif cur.val > num: + cur = cur.left + # 找到目标结点,跳出循环 + else: + break + return cur + + """ 插入结点 """ + def insert(self, num: int) -> typing.Optional[TreeNode]: + root = self.root + # 若树为空,直接提前返回 + if root is None: + return None + + cur = root + pre = None + + # 循环查找,越过叶结点后跳出 + while cur is not None: + # 找到重复结点,直接返回 + if cur.val == num: + return None + pre = cur + + if cur.val < num: # 插入位置在 root 的右子树中 + cur = cur.right + else: # 插入位置在 root 的左子树中 + cur = cur.left + + # 插入结点 val + node = TreeNode(num) + if pre.val < num: + pre.right = node + else: + pre.left = node + return node + + """ 删除结点 """ + def remove(self, num: int) -> typing.Optional[TreeNode]: + root = self.root + # 若树为空,直接提前返回 + if root is None: + return None + + cur = root + pre = None + + # 循环查找,越过叶结点后跳出 + while cur is not None: + # 找到待删除结点,跳出循环 + if cur.val == num: + break + pre = cur + if cur.val < num: # 待删除结点在 root 的右子树中 + cur = cur.right + else: # 待删除结点在 root 的左子树中 + cur = cur.left + + # 若无待删除结点,则直接返回 + if cur is None: + return None + + # 子结点数量 = 0 or 1 + if cur.left is None or cur.right is None: + # 当子结点数量 = 0 / 1 时, child = null / 该子结点 + child = cur.left or cur.right + # 删除结点 cur + if pre.left == cur: + pre.left = child + else: + pre.right = child + # 子结点数量 = 2 + else: + # 获取中序遍历中 cur 的下一个结点 + nex = self.min(cur.right) + tmp = nex.val + # 递归删除结点 nex + self.remove(nex.val) + # 将 nex 的值复制给 cur + cur.val = tmp + return cur + + """ 获取最小结点 """ + def min(self, root: typing.Optional[TreeNode]) -> typing.Optional[TreeNode]: + if root is None: + return root + + # 循环访问左子结点,直到叶结点时为最小结点,跳出 + while root.left is not None: + root = root.left + return root + + +""" Driver Code """ +if __name__ == "__main__": + # 初始化二叉搜索树 + nums = list(range(1, 16)) + bst = BinarySearchTree(nums=nums) + print("\n初始化的二叉树为\n") + print_tree(bst.root) + + # 查找结点 + node = bst.search(5) + print("\n查找到的结点对象为: {},结点值 = {}".format(node, node.val)) + + # 插入结点 + ndoe = bst.insert(16) + print("\n插入结点 16 后,二叉树为\n") + print_tree(bst.root) + + # 删除结点 + bst.remove(1) + print("\n删除结点 1 后,二叉树为\n") + print_tree(bst.root) + + bst.remove(2) + print("\n删除结点 2 后,二叉树为\n") + print_tree(bst.root) + + bst.remove(4) + print("\n删除结点 4 后,二叉树为\n") + print_tree(bst.root) diff --git a/codes/python/chapter_tree/binary_tree.py b/codes/python/chapter_tree/binary_tree.py index 78afa868..d9902635 100644 --- a/codes/python/chapter_tree/binary_tree.py +++ b/codes/python/chapter_tree/binary_tree.py @@ -1,10 +1,40 @@ -''' +""" File: binary_tree.py -Created Time: 2022-11-25 -Author: Krahets (krahets@163.com) -''' +Created Time: 2022-12-20 +Author: a16su (lpluls001@gmail.com) +""" import sys, os.path as osp + sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) from include import * + +""" Driver Code """ +if __name__ == "__main__": + """ 初始化二叉树 """ + # 初始化节点 + n1 = TreeNode(val=1) + n2 = TreeNode(val=2) + n3 = TreeNode(val=3) + n4 = TreeNode(val=4) + n5 = TreeNode(val=5) + # 构建引用指向(即指针) + n1.left = n2 + n1.right = n3 + n2.left = n4 + n2.right = n5 + print("\n初始化二叉树\n") + print_tree(n1) + + """ 插入与删除结点 """ + P = TreeNode(0) + # 在 n1 -> n2 中间插入节点 P + n1.left = P + P.left = n2 + print("\n插入结点 P 后\n") + print_tree(n1) + # 删除结点 + n1.left = n2 + print("\n删除结点 P 后\n"); + print_tree(n1) diff --git a/codes/python/chapter_tree/binary_tree_bfs.py b/codes/python/chapter_tree/binary_tree_bfs.py index 43f19220..0320a08d 100644 --- a/codes/python/chapter_tree/binary_tree_bfs.py +++ b/codes/python/chapter_tree/binary_tree_bfs.py @@ -1,10 +1,42 @@ -''' +""" File: binary_tree_bfs.py -Created Time: 2022-11-25 -Author: Krahets (krahets@163.com) -''' +Created Time: 2022-12-20 +Author: a16su (lpluls001@gmail.com) +""" import sys, os.path as osp +import typing + sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) from include import * + +""" 层序遍历 """ +def hier_order(root: TreeNode): + # 初始化队列,加入根结点 + queue = collections.deque() + queue.append(root) + # 初始化一个列表,用于保存遍历序列 + res = [] + while queue: + node = queue.popleft() # 队列出队 + res.append(node.val) # 保存节点值 + if node.left is not None: + queue.append(node.left) # 左子结点入队 + if node.right is not None: + queue.append(node.right) # 右子结点入队 + return res + + +""" Driver Code """ +if __name__ == "__main__": + # 初始化二叉树 + # 这里借助了一个从数组直接生成二叉树的函数 + root = list_to_tree(arr=[1, 2, 3, 4, 5, 6, 7, None, None, None, None, None, None, None, None]) + print("\n初始化二叉树\n") + print_tree(root) + + # 层序遍历 + res = hier_order(root) + print("\n层序遍历的结点打印序列 = ", res) + assert res == [1, 2, 3, 4, 5, 6, 7] diff --git a/codes/python/chapter_tree/binary_tree_dfs.py b/codes/python/chapter_tree/binary_tree_dfs.py index f8415ef5..11ee8339 100644 --- a/codes/python/chapter_tree/binary_tree_dfs.py +++ b/codes/python/chapter_tree/binary_tree_dfs.py @@ -1,10 +1,68 @@ -''' +""" File: binary_tree_dfs.py -Created Time: 2022-11-25 -Author: Krahets (krahets@163.com) -''' +Created Time: 2022-12-20 +Author: a16su (lpluls001@gmail.com) +""" import sys, os.path as osp +import typing + sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) from include import * + +res = [] + +""" 前序遍历 """ +def pre_order(root: typing.Optional[TreeNode]): + if root is None: + return + # 访问优先级:根结点 -> 左子树 -> 右子树 + res.append(root.val) + pre_order(root=root.left) + pre_order(root=root.right) + +""" 中序遍历 """ +def in_order(root: typing.Optional[TreeNode]): + if root is None: + return + # 访问优先级:左子树 -> 根结点 -> 右子树 + in_order(root=root.left) + res.append(root.val) + in_order(root=root.right) + +""" 后序遍历 """ +def post_order(root: typing.Optional[TreeNode]): + if root is None: + return + # 访问优先级:左子树 -> 右子树 -> 根结点 + post_order(root=root.left) + post_order(root=root.right) + res.append(root.val) + + +""" Driver Code """ +if __name__ == "__main__": + # 初始化二叉树 + # 这里借助了一个从数组直接生成二叉树的函数 + root = list_to_tree(arr=[1, 2, 3, 4, 5, 6, 7, None, None, None, None, None, None, None, None]) + print("\n初始化二叉树\n") + print_tree(root) + + # 前序遍历 + res.clear() + pre_order(root) + print("\n前序遍历的结点打印序列 = ", res) + assert res == [1, 2, 4, 5, 3, 6, 7] + + # 中序遍历 + res.clear() + in_order(root) + print("\n中序遍历的结点打印序列 = ", res) + assert res == [4, 2, 5, 1, 6, 3, 7] + + # 后序遍历 + res.clear() + post_order(root) + print("\n后序遍历的结点打印序列 = ", res) + assert res == [4, 5, 2, 6, 7, 3, 1] diff --git a/codes/python/include/binary_tree.py b/codes/python/include/binary_tree.py index 24acb47d..670fd18c 100644 --- a/codes/python/include/binary_tree.py +++ b/codes/python/include/binary_tree.py @@ -1,8 +1,8 @@ -''' +""" File: binary_tree.py Created Time: 2021-12-11 Author: Krahets (krahets@163.com) -''' +""" import collections @@ -10,9 +10,19 @@ class TreeNode: """Definition for a binary tree node """ def __init__(self, val=0, left=None, right=None): - self.val = val - self.left = left - self.right = right + self.val = val # 结点值 + self.height = 0 # 结点高度 + self.left = left # 左子结点引用 + self.right = right # 右子结点引用 + + def __str__(self): + val = self.val + left_node_val = self.left.val if self.left else None + right_node_val = self.right.val if self.right else None + return "".format(val, left_node_val, right_node_val) + + __repr__ = __str__ + def list_to_tree(arr): """Generate a binary tree with a list diff --git a/codes/python/include/linked_list.py b/codes/python/include/linked_list.py index f6773c6f..e2eff9a2 100644 --- a/codes/python/include/linked_list.py +++ b/codes/python/include/linked_list.py @@ -1,8 +1,8 @@ -''' +""" File: linked_list.py Created Time: 2021-12-11 Author: Krahets (krahets@163.com) -''' +""" class ListNode: """Definition for a singly-linked list node diff --git a/codes/python/include/print_util.py b/codes/python/include/print_util.py index f84d548b..9f211806 100644 --- a/codes/python/include/print_util.py +++ b/codes/python/include/print_util.py @@ -1,8 +1,8 @@ -''' +""" File: print_util.py Created Time: 2021-12-11 Author: Krahets (krahets@163.com), msk397 (machangxinq@gmail.com) -''' +""" import copy import queue diff --git a/codes/swift/chapter_computational_complexity/time_complexity.swift b/codes/swift/chapter_computational_complexity/time_complexity.swift new file mode 100644 index 00000000..38bac4fa --- /dev/null +++ b/codes/swift/chapter_computational_complexity/time_complexity.swift @@ -0,0 +1,170 @@ +/* + * File: time_complexity.swift + * Created Time: 2022-12-26 + * Author: nuomi1 (nuomi1@qq.com) + */ + +// 常数阶 +func constant(n: Int) -> Int { + var count = 0 + let size = 100_000 + for _ in 0 ..< size { + count += 1 + } + return count +} + +// 线性阶 +func linear(n: Int) -> Int { + var count = 0 + for _ in 0 ..< n { + count += 1 + } + return count +} + +// 线性阶(遍历数组) +func arrayTraversal(nums: [Int]) -> Int { + var count = 0 + // 循环次数与数组长度成正比 + for _ in nums { + count += 1 + } + return count +} + +// 平方阶 +func quadratic(n: Int) -> Int { + var count = 0 + // 循环次数与数组长度成平方关系 + for _ in 0 ..< n { + for _ in 0 ..< n { + count += 1 + } + } + return count +} + +// 平方阶(冒泡排序) +func bubbleSort(nums: inout [Int]) -> Int { + var count = 0 // 计数器 + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for i in sequence(first: nums.count - 1, next: { $0 > 0 ? $0 - 1 : nil }) { + // 内循环:冒泡操作 + for j in 0 ..< i { + if nums[j] > nums[j + 1] { + // 交换 nums[j] 与 nums[j + 1] + let tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 // 元素交换包含 3 个单元操作 + } + } + } + return count +} + +// 指数阶(循环实现) +func exponential(n: Int) -> Int { + var count = 0 + var base = 1 + // cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) + for _ in 0 ..< n { + for _ in 0 ..< base { + count += 1 + } + base *= 2 + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count +} + +// 指数阶(递归实现) +func expRecur(n: Int) -> Int { + if n == 1 { + return 1 + } + return expRecur(n: n - 1) + expRecur(n: n - 1) + 1 +} + +// 对数阶(循环实现) +func logarithmic(n: Int) -> Int { + var count = 0 + var n = n + while n > 1 { + n = n / 2 + count += 1 + } + return count +} + +// 对数阶(递归实现) +func logRecur(n: Int) -> Int { + if n <= 1 { + return 0 + } + return logRecur(n: n / 2) + 1 +} + +// 线性对数阶 +func linearLogRecur(n: Double) -> Int { + if n <= 1 { + return 1 + } + var count = linearLogRecur(n: n / 2) + linearLogRecur(n: n / 2) + for _ in 0 ..< Int(n) { + count += 1 + } + return count +} + +// 阶乘阶(递归实现) +func factorialRecur(n: Int) -> Int { + if n == 0 { + return 1 + } + var count = 0 + // 从 1 个分裂出 n 个 + for _ in 0 ..< n { + count += factorialRecur(n: n - 1) + } + return count +} + +func main() { + // 可以修改 n 运行,体会一下各种复杂度的操作数量变化趋势 + let n = 8 + print("输入数据大小 n =", n) + + var count = constant(n: n) + print("常数阶的计算操作数量 =", count) + + count = linear(n: n) + print("线性阶的计算操作数量 =", count) + count = arrayTraversal(nums: Array(repeating: 0, count: n)) + print("线性阶(遍历数组)的计算操作数量 =", count) + + count = quadratic(n: n) + print("平方阶的计算操作数量 =", count) + var nums = Array(sequence(first: n, next: { $0 > 0 ? $0 - 1 : nil })) // [n,n-1,...,2,1] + count = bubbleSort(nums: &nums) + print("平方阶(冒泡排序)的计算操作数量 =", count) + + count = exponential(n: n) + print("指数阶(循环实现)的计算操作数量 =", count) + count = expRecur(n: n) + print("指数阶(递归实现)的计算操作数量 =", count) + + count = logarithmic(n: n) + print("对数阶(循环实现)的计算操作数量 =", count) + count = logRecur(n: n) + print("对数阶(递归实现)的计算操作数量 =", count) + + count = linearLogRecur(n: Double(n)) + print("线性对数阶(递归实现)的计算操作数量 =", count) + + count = factorialRecur(n: n) + print("阶乘阶(递归实现)的计算操作数量 =", count) +} + +main() diff --git a/codes/swift/chapter_computational_complexity/worst_best_time_complexity.swift b/codes/swift/chapter_computational_complexity/worst_best_time_complexity.swift new file mode 100644 index 00000000..73db6954 --- /dev/null +++ b/codes/swift/chapter_computational_complexity/worst_best_time_complexity.swift @@ -0,0 +1,37 @@ +/* + * File: worst_best_time_complexity.swift + * Created Time: 2022-12-26 + * Author: nuomi1 (nuomi1@qq.com) + */ + +// 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 +func randomNumbers(n: Int) -> [Int] { + // 生成数组 nums = { 1, 2, 3, ..., n } + var nums = Array(1 ... n) + // 随机打乱数组元素 + nums.shuffle() + return nums +} + +// 查找数组 nums 中数字 1 所在索引 +func findOne(nums: [Int]) -> Int { + for i in nums.indices { + if nums[i] == 1 { + return i + } + } + return -1 +} + +// Driver Code +func main() { + for _ in 0 ..< 10 { + let n = 100 + let nums = randomNumbers(n: n) + let index = findOne(nums: nums) + print("数组 [ 1, 2, ..., n ] 被打乱后 =", nums) + print("数字 1 的索引为", index) + } +} + +main() diff --git a/codes/typescript/chapter_hashing/array_hash_map.ts b/codes/typescript/chapter_hashing/array_hash_map.ts new file mode 100644 index 00000000..627205f7 --- /dev/null +++ b/codes/typescript/chapter_hashing/array_hash_map.ts @@ -0,0 +1,136 @@ +/* + * File: array_hash_map.ts + * Created Time: 2022-12-20 + * Author: Daniel (better.sunjian@gmail.com) + */ + +/* 键值对 Number -> String */ +class Entry { + public key: number; + public val: string; + + constructor(key: number, val: string) { + this.key = key; + this.val = val; + } +} + +/* 基于数组简易实现的哈希表 */ +class ArrayHashMap { + + private readonly bucket: (Entry | null)[]; + + constructor() { + // 初始化一个长度为 100 的桶(数组) + this.bucket = (new Array(100)).fill(null); + } + + /* 哈希函数 */ + private hashFunc(key: number): number { + return key % 100; + } + + /* 查询操作 */ + public get(key: number): string | null { + let index = this.hashFunc(key); + let entry = this.bucket[index]; + if (entry === null) return null; + return entry.val; + } + + /* 添加操作 */ + public set(key: number, val: string) { + let index = this.hashFunc(key); + this.bucket[index] = new Entry(key, val); + } + + /* 删除操作 */ + public delete(key: number) { + let index = this.hashFunc(key); + // 置为 null ,代表删除 + this.bucket[index] = null; + } + + /* 获取所有键值对 */ + public entries(): (Entry | null)[] { + let arr: (Entry | null)[] = []; + for (let i = 0; i < this.bucket.length; i++) { + if (this.bucket[i]) { + arr.push(this.bucket[i]); + } + } + return arr; + } + + /* 获取所有键 */ + public keys(): (number | undefined)[] { + let arr: (number | undefined)[] = []; + for (let i = 0; i < this.bucket.length; i++) { + if (this.bucket[i]) { + arr.push(this.bucket[i]?.key); + } + } + return arr; + } + + /* 获取所有值 */ + public values(): (string | undefined)[] { + let arr: (string | undefined)[] = []; + for (let i = 0; i < this.bucket.length; i++) { + if (this.bucket[i]) { + arr.push(this.bucket[i]?.val); + } + } + return arr; + } + + /* 打印哈希表 */ + public print() { + let entrySet = this.entries(); + for (const entry of entrySet) { + if (!entry) continue; + console.info(`${entry.key} -> ${entry.val}`); + } + } +} + +/* Driver Code */ +/* 初始化哈希表 */ +const map = new ArrayHashMap(); +/* 添加操作 */ +// 在哈希表中添加键值对 (key, value) +map.set(12836, '小哈'); +map.set(15937, '小啰'); +map.set(16750, '小算'); +map.set(13276, '小法'); +map.set(10583, '小鸭'); +console.info('\n添加完成后,哈希表为\nKey -> Value'); +map.print(); + +/* 查询操作 */ +// 向哈希表输入键 key ,得到值 value +let name = map.get(15937); +console.info('\n输入学号 15937 ,查询到姓名 ' + name); + +/* 删除操作 */ +// 在哈希表中删除键值对 (key, value) +map.delete(10583); +console.info('\n删除 10583 后,哈希表为\nKey -> Value'); +map.print(); + +/* 遍历哈希表 */ +console.info('\n遍历键值对 Key->Value'); +for (const entry of map.entries()) { + if (!entry) continue; + console.info(entry.key + ' -> ' + entry.val); +} +console.info('\n单独遍历键 Key'); +for (const key of map.keys()) { + console.info(key); +} +console.info('\n单独遍历值 Value'); +for (const val of map.values()) { + console.info(val); +} + +export {}; diff --git a/codes/typescript/chapter_hashing/hash_map.ts b/codes/typescript/chapter_hashing/hash_map.ts new file mode 100644 index 00000000..7e54cf5c --- /dev/null +++ b/codes/typescript/chapter_hashing/hash_map.ts @@ -0,0 +1,44 @@ +/* + * File: hash_map.ts + * Created Time: 2022-12-20 + * Author: Daniel (better.sunjian@gmail.com) + */ + +/* Driver Code */ +/* 初始化哈希表 */ +const map = new Map(); + +/* 添加操作 */ +// 在哈希表中添加键值对 (key, value) +map.set(12836, '小哈'); +map.set(15937, '小啰'); +map.set(16750, '小算'); +map.set(13276, '小法'); +map.set(10583, '小鸭'); +console.info('\n添加完成后,哈希表为\nKey -> Value'); +console.info(map); + +/* 查询操作 */ +// 向哈希表输入键 key ,得到值 value +let name = map.get(15937); +console.info('\n输入学号 15937 ,查询到姓名 ' + name); + +/* 删除操作 */ +// 在哈希表中删除键值对 (key, value) +map.delete(10583); +console.info('\n删除 10583 后,哈希表为\nKey -> Value'); +console.info(map); + +/* 遍历哈希表 */ +console.info('\n遍历键值对 Key->Value'); +for (const [k, v] of map.entries()) { + console.info(k + ' -> ' + v); +} +console.info('\n单独遍历键 Key'); +for (const k of map.keys()) { + console.info(k); +} +console.info('\n单独遍历值 Value'); +for (const v of map.values()) { + console.info(v); +} diff --git a/codes/typescript/chapter_searching/binary_search.ts b/codes/typescript/chapter_searching/binary_search.ts new file mode 100644 index 00000000..2a03994b --- /dev/null +++ b/codes/typescript/chapter_searching/binary_search.ts @@ -0,0 +1,54 @@ +/* +* File: binary_search.ts +* Created Time: 2022-12-27 +* Author: Daniel (better.sunjian@gmail.com) +*/ + +/* 二分查找(双闭区间) */ +const binarySearch = function (nums: number[], target: number): number { + // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + let i = 0, j = nums.length - 1; + // 循环,当搜索区间为空时跳出(当 i > j 时为空) + while (i <= j) { + const m = Math.floor(i + (j - i) / 2); // 计算中点索引 m + if (nums[m] < target) { // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1; + } else if (nums[m] > target) { // 此情况说明 target 在区间 [i, m-1] 中 + j = m - 1; + } else { // 找到目标元素,返回其索引 + return m; + } + } + return -1; // 未找到目标元素,返回 -1 +} + +/* 二分查找(左闭右开) */ +const binarySearch1 = function (nums: number[], target: number): number { + // 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 + let i = 0, j = nums.length; + // 循环,当搜索区间为空时跳出(当 i = j 时为空) + while (i < j) { + const m = Math.floor(i + (j - i) / 2); // 计算中点索引 m + if (nums[m] < target) { // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1; + } else if (nums[m] > target) { // 此情况说明 target 在区间 [i, m] 中 + j = m; + } else { // 找到目标元素,返回其索引 + return m; + } + } + return -1; // 未找到目标元素,返回 -1 +} + + +/* Driver Code */ +const target = 6; +const nums = [ 1, 3, 6, 8, 12, 15, 23, 67, 70, 92 ]; + +/* 二分查找(双闭区间) */ +let index = binarySearch(nums, target); +console.info('目标元素 6 的索引 = %d', index); + +/* 二分查找(左闭右开) */ +index = binarySearch1(nums, target); +console.info('目标元素 6 的索引 = %d', index); diff --git a/codes/typescript/chapter_sorting/merge_sort.ts b/codes/typescript/chapter_sorting/merge_sort.ts index b84c7828..44fb4b8d 100644 --- a/codes/typescript/chapter_sorting/merge_sort.ts +++ b/codes/typescript/chapter_sorting/merge_sort.ts @@ -20,13 +20,13 @@ function merge(nums: number[], left: number, mid: number, right: number): void { let i = leftStart, j = rightStart; // 通过覆盖原数组 nums 来合并左子数组和右子数组 for (let k = left; k <= right; k++) { - // 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ if (i > leftEnd) { nums[k] = tmp[j++]; - // 否则,若 “右子数组已全部合并完” 或 “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ + // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ } else if (j > rightEnd || tmp[i] <= tmp[j]) { nums[k] = tmp[i++]; - // 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ } else { nums[k] = tmp[j++]; } diff --git a/codes/typescript/chapter_stack_and_queue/array_queue.ts b/codes/typescript/chapter_stack_and_queue/array_queue.ts index 4fb72bbb..1f67c648 100644 --- a/codes/typescript/chapter_stack_and_queue/array_queue.ts +++ b/codes/typescript/chapter_stack_and_queue/array_queue.ts @@ -57,13 +57,6 @@ class ArrayQueue { return this.queue[this.front]; } - /* 访问指定索引元素 */ - get(index: number): number { - if (index >= this.size) - throw new Error("索引越界"); - return this.queue[(this.front + index) % this.capacity]; - } - /* 返回 Array */ toArray(): number[] { const siz = this.size; @@ -94,10 +87,6 @@ console.log(queue.toArray()); const peek = queue.peek(); console.log("队首元素 peek = " + peek); -/* 访问指定索引元素 */ -const num = queue.get(2); -console.log("队列第 3 个元素为 num = " + num); - /* 元素出队 */ const poll = queue.poll(); console.log("出队元素 poll = " + poll + ",出队后 queue = "); diff --git a/codes/typescript/chapter_stack_and_queue/array_stack.ts b/codes/typescript/chapter_stack_and_queue/array_stack.ts index ef6419f7..2b9f8468 100644 --- a/codes/typescript/chapter_stack_and_queue/array_stack.ts +++ b/codes/typescript/chapter_stack_and_queue/array_stack.ts @@ -41,13 +41,6 @@ class ArrayStack { return this.stack[this.stack.length - 1]; } - /* 访问索引 index 处元素 */ - get(index: number): number | undefined { - if (index >= this.size) - throw new Error('索引越界'); - return this.stack[index]; - } - /* 返回 Array */ toArray() { return this.stack; @@ -73,10 +66,6 @@ console.log(stack.toArray()); const top = stack.top(); console.log("栈顶元素 top = " + top); -/* 访问索引 index 处元素 */ -const num = stack.get(3); -console.log("栈索引 3 处的元素为 num = " + num); - /* 元素出栈 */ const pop = stack.pop(); console.log("出栈元素 pop = " + pop + ",出栈后 stack = "); diff --git a/codes/typescript/chapter_stack_and_queue/linkedlist_stack.ts b/codes/typescript/chapter_stack_and_queue/linkedlist_stack.ts new file mode 100644 index 00000000..1df4b499 --- /dev/null +++ b/codes/typescript/chapter_stack_and_queue/linkedlist_stack.ts @@ -0,0 +1,91 @@ +/** + * File: linkedlist_stack.ts + * Created Time: 2022-12-21 + * Author: S-N-O-R-L-A-X (snorlax.xu@outlook.com) + */ + +import ListNode from "../module/ListNode" + +/* 基于链表实现的栈 */ +class LinkedListStack { + private stackPeek: ListNode | null; // 将头结点作为栈顶 + private stkSize: number = 0; // 栈的长度 + + constructor() { + this.stackPeek = null; + } + + /* 获取栈的长度 */ + get size(): number { + return this.stkSize; + } + + /* 判断栈是否为空 */ + isEmpty(): boolean { + return this.size == 0; + } + + /* 入栈 */ + push(num: number): void { + const node = new ListNode(num); + node.next = this.stackPeek; + this.stackPeek = node; + this.stkSize++; + } + + /* 出栈 */ + pop(): number { + const num = this.peek(); + if (!this.stackPeek) + throw new Error("栈为空"); + this.stackPeek = this.stackPeek.next; + this.stkSize--; + return num; + } + + /* 访问栈顶元素 */ + peek(): number { + if (!this.stackPeek) + throw new Error("栈为空"); + return this.stackPeek.val; + } + + /* 将链表转化为 Array 并返回 */ + toArray(): number[] { + let node = this.stackPeek; + const res = new Array(this.size); + for (let i = res.length - 1; i >= 0; i--) { + res[i] = node!.val; + node = node!.next; + } + return res; + } +} + + +/* 初始化栈 */ +const stack = new LinkedListStack(); + +/* 元素入栈 */ +stack.push(1); +stack.push(3); +stack.push(2); +stack.push(5); +stack.push(4); +console.log("栈 stack = " + stack.toArray()); + +/* 访问栈顶元素 */ +const peek = stack.peek(); +console.log("栈顶元素 peek = " + peek); + +/* 元素出栈 */ +const pop = stack.pop(); +console.log("出栈元素 pop = " + pop + ",出栈后 stack = " + stack.toArray()); + +/* 获取栈的长度 */ +const size = stack.size; +console.log("栈的长度 size = " + size); + +/* 判断是否为空 */ +const isEmpty = stack.isEmpty(); +console.log("栈是否为空 = " + isEmpty); diff --git a/docs/chapter_array_and_linkedlist/array.md b/docs/chapter_array_and_linkedlist/array.md index bae5c277..bd98a914 100644 --- a/docs/chapter_array_and_linkedlist/array.md +++ b/docs/chapter_array_and_linkedlist/array.md @@ -75,9 +75,6 @@ comments: true /* 初始化数组 */ int[] arr = new int[5]; // { 0, 0, 0, 0, 0 } int[] nums = { 1, 3, 2, 5, 4 }; - - var arr2=new int[5]; // { 0, 0, 0, 0, 0 } - var nums2=new int[]{1,2,3,4,5}; ``` ## 数组优点 @@ -314,7 +311,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` -**数组中插入或删除元素效率低下。** 假设我们想要在数组中间某位置插入一个元素,由于数组元素在内存中是 “紧挨着的” ,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。删除元素也是类似,需要把此索引之后的元素都向前移动一位。总体看有以下缺点: +**数组中插入或删除元素效率低下。** 假设我们想要在数组中间某位置插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。删除元素也是类似,需要把此索引之后的元素都向前移动一位。总体看有以下缺点: - **时间复杂度高:** 数组的插入和删除的平均时间复杂度均为 $O(N)$ ,其中 $N$ 为数组长度。 - **丢失元素:** 由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会被丢失。 @@ -712,6 +709,6 @@ elementAddr = firtstElementAddr + elementLength * elementIndex **随机访问。** 如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。 -**二分查找。** 例如前文查字典的例子,我们可以将字典中的所有字按照拼音顺序存储在数组中,然后使用与日常查纸质字典相同的 “翻开中间,排除一半” 的方式,来实现一个查电子字典的算法。 +**二分查找。** 例如前文查字典的例子,我们可以将字典中的所有字按照拼音顺序存储在数组中,然后使用与日常查纸质字典相同的“翻开中间,排除一半”的方式,来实现一个查电子字典的算法。 **深度学习。** 神经网络中大量使用了向量、矩阵、张量之间的线性代数运算,这些数据都是以数组的形式构建的。数组是神经网络编程中最常使用的数据结构。 diff --git a/docs/chapter_array_and_linkedlist/linked_list.md b/docs/chapter_array_and_linkedlist/linked_list.md index 83c8db64..6627fb93 100644 --- a/docs/chapter_array_and_linkedlist/linked_list.md +++ b/docs/chapter_array_and_linkedlist/linked_list.md @@ -103,7 +103,7 @@ comments: true === "C#" ```csharp title="" - // 链表结点类 + /* 链表结点类 */ class ListNode { int val; // 结点值 @@ -232,13 +232,13 @@ comments: true === "C#" ```csharp title="" - // 初始化链表 1 -> 3 -> 2 -> 5 -> 4 - // 初始化各结点 - n0 = new ListNode(1); - n1 = new ListNode(3); - n2 = new ListNode(2); - n3 = new ListNode(5); - n4 = new ListNode(4); + /* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */ + // 初始化各个结点 + ListNode n0 = new ListNode(1); + ListNode n1 = new ListNode(3); + ListNode n2 = new ListNode(2); + ListNode n3 = new ListNode(5); + ListNode n4 = new ListNode(4); // 构建引用指向 n0.next = n1; n1.next = n2; @@ -673,7 +673,7 @@ comments: true int val; // 结点值 ListNode *next; // 指向后继结点的指针(引用) ListNode *prev; // 指向前驱结点的指针(引用) - ListNode(int x) : val(x), next(nullptr) {} // 构造函数 + ListNode(int x) : val(x), next(nullptr), prev(nullptr) {} // 构造函数 }; ``` @@ -718,8 +718,8 @@ comments: true prev; constructor(val, next) { this.val = val === undefined ? 0 : val; // 结点值 - this.next = next === undefined ? null : next; // 指向后继结点的引用 - this.prev = prev === undefined ? null : prev; // 指向前驱结点的引用 + this.next = next === undefined ? null : next; // 指向后继结点的指针(引用) + this.prev = prev === undefined ? null : prev; // 指向前驱结点的指针(引用) } } ``` @@ -734,8 +734,8 @@ comments: true prev: ListNode | null; constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) { this.val = val === undefined ? 0 : val; // 结点值 - this.next = next === undefined ? null : next; // 指向后继结点的引用 - this.prev = prev === undefined ? null : prev; // 指向前驱结点的引用 + this.next = next === undefined ? null : next; // 指向后继结点的指针(引用) + this.prev = prev === undefined ? null : prev; // 指向前驱结点的指针(引用) } } ``` @@ -749,7 +749,7 @@ comments: true === "C#" ```csharp title="" - // 双向链表结点类 + /* 双向链表结点类 */ class ListNode { int val; // 结点值 ListNode next; // 指向后继结点的指针(引用) diff --git a/docs/chapter_array_and_linkedlist/list.md b/docs/chapter_array_and_linkedlist/list.md index 3c7a692e..749b1948 100644 --- a/docs/chapter_array_and_linkedlist/list.md +++ b/docs/chapter_array_and_linkedlist/list.md @@ -10,13 +10,15 @@ comments: true ## 列表常用操作 -**初始化列表。** 我们通常使用 `Integer[]` 包装类和 `Arrays.asList()` 作为中转,来初始化一个带有初始值的列表。 +**初始化列表。** 我们通常会使用到“无初始值”和“有初始值”的两种初始化方法。 === "Java" ```java title="list.java" /* 初始化列表 */ - // 注意数组的元素类型是 int[] 的包装类 Integer[] + // 无初始值 + List list1 = new ArrayList<>(); + // 有初始值(注意数组的元素类型需为 int[] 的包装类 Integer[]) Integer[] numbers = new Integer[] { 1, 3, 2, 5, 4 }; List list = new ArrayList<>(Arrays.asList(numbers)); ``` @@ -25,6 +27,10 @@ comments: true ```cpp title="list.cpp" /* 初始化列表 */ + // 需注意,C++ 中 vector 即是本文描述的 list + // 无初始值 + vector list1; + // 有初始值 vector list = { 1, 3, 2, 5, 4 }; ``` @@ -32,6 +38,9 @@ comments: true ```python title="list.py" """ 初始化列表 """ + # 无初始值 + list1 = [] + # 有初始值 list = [1, 3, 2, 5, 4] ``` @@ -39,6 +48,9 @@ comments: true ```go title="list_test.go" /* 初始化列表 */ + // 无初始值 + list1 := []int + // 有初始值 list := []int{1, 3, 2, 5, 4} ``` @@ -46,6 +58,9 @@ comments: true ```js title="list.js" /* 初始化列表 */ + // 无初始值 + const list1 = []; + // 有初始值 const list = [1, 3, 2, 5, 4]; ``` @@ -53,6 +68,9 @@ comments: true ```typescript title="list.ts" /* 初始化列表 */ + // 无初始值 + const list1: number[] = []; + // 有初始值 const list: number[] = [1, 3, 2, 5, 4]; ``` @@ -65,7 +83,12 @@ comments: true === "C#" ```csharp title="list.cs" - + /* 初始化列表 */ + // 无初始值 + List list1 = new (); + // 有初始值(注意数组的元素类型需为 int[] 的包装类 Integer[]) + int[] numbers = new int[] { 1, 3, 2, 5, 4 }; + List list = numbers.ToList(); ``` **访问与更新元素。** 列表的底层数据结构是数组,因此可以在 $O(1)$ 时间内访问与更新元素,效率很高。 @@ -114,20 +137,20 @@ comments: true ```js title="list.js" /* 访问元素 */ - const num = list[1]; + const num = list[1]; // 访问索引 1 处的元素 /* 更新元素 */ - list[1] = 0; + list[1] = 0; // 将索引 1 处的元素更新为 0 ``` === "TypeScript" ```typescript title="list.ts" /* 访问元素 */ - const num: number = list[1]; + const num: number = list[1]; // 访问索引 1 处的元素 /* 更新元素 */ - list[1] = 0; + list[1] = 0; // 将索引 1 处的元素更新为 0 ``` === "C" @@ -139,7 +162,11 @@ comments: true === "C#" ```csharp title="list.cs" + /* 访问元素 */ + int num = list[1]; // 访问索引 1 处的元素 + /* 更新元素 */ + list[1] = 0; // 将索引 1 处的元素更新为 0 ``` **在列表中添加、插入、删除元素。** 相对于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为 $O(1)$ ,但是插入与删除元素的效率仍与数组一样低,时间复杂度为 $O(N)$ 。 @@ -273,7 +300,21 @@ comments: true === "C#" ```csharp title="list.cs" + /* 清空列表 */ + list.Clear(); + /* 尾部添加元素 */ + list.Add(1); + list.Add(3); + list.Add(2); + list.Add(5); + list.Add(4); + + /* 中间插入元素 */ + list.Insert(3, 6); + + /* 删除元素 */ + list.RemoveAt(3); ``` **遍历列表。** 与数组一样,列表可以使用索引遍历,也可以使用 `for-each` 直接遍历。 @@ -335,9 +376,9 @@ comments: true /* 直接遍历列表元素 */ count = 0 - for range list { - count++ - } + for range list { + count++ + } ``` === "JavaScript" @@ -381,7 +422,19 @@ comments: true === "C#" ```csharp title="list.cs" + /* 通过索引遍历列表 */ + int count = 0; + for (int i = 0; i < list.Count(); i++) + { + count++; + } + /* 直接遍历列表元素 */ + count = 0; + foreach (int n in list) + { + count++; + } ``` **拼接两个列表。** 再创建一个新列表 `list1` ,我们可以将其中一个列表拼接到另一个的尾部。 @@ -424,7 +477,7 @@ comments: true ```js title="list.js" /* 拼接两个列表 */ const list1 = [6, 8, 7, 10, 9]; - list.push(...list1); + list.push(...list1); // 将列表 list1 拼接到 list 之后 ``` === "TypeScript" @@ -432,7 +485,7 @@ comments: true ```typescript title="list.ts" /* 拼接两个列表 */ const list1: number[] = [6, 8, 7, 10, 9]; - list.push(...list1); + list.push(...list1); // 将列表 list1 拼接到 list 之后 ``` === "C" @@ -444,7 +497,9 @@ comments: true === "C#" ```csharp title="list.cs" - + /* 拼接两个列表 */ + List list1 = new() { 6, 8, 7, 10, 9 }; + list.AddRange(list1); // 将列表 list1 拼接到 list 之后 ``` **排序列表。** 排序也是常用的方法之一,完成列表排序后,我们就可以使用在数组类算法题中经常考察的「二分查找」和「双指针」算法了。 @@ -481,7 +536,7 @@ comments: true ```js title="list.js" /* 排序列表 */ - list.sort((a, b) => a - b); + list.sort((a, b) => a - b); // 排序后,列表元素从小到大排列 ``` === "TypeScript" @@ -500,7 +555,8 @@ comments: true === "C#" ```csharp title="list.cs" - + /* 排序列表 */ + list.Sort(); // 排序后,列表元素从小到大排列 ``` ## 列表简易实现 * @@ -1066,5 +1122,101 @@ comments: true === "C#" ```csharp title="my_list.cs" + class MyList + { + private int[] nums; // 数组(存储列表元素) + private int capacity = 10; // 列表容量 + private int size = 0; // 列表长度(即当前元素数量) + private int extendRatio = 2; // 每次列表扩容的倍数 + /* 构造函数 */ + public MyList() + { + nums = new int[capacity]; + } + + /* 获取列表长度(即当前元素数量)*/ + public int Size() + { + return size; + } + + /* 获取列表容量 */ + public int Capacity() + { + return capacity; + } + + /* 访问元素 */ + public int Get(int index) + { + // 索引如果越界则抛出异常,下同 + if (index >= size) + throw new IndexOutOfRangeException("索引越界"); + return nums[index]; + } + + /* 更新元素 */ + public void Set(int index, int num) + { + if (index >= size) + throw new IndexOutOfRangeException("索引越界"); + nums[index] = num; + } + + /* 尾部添加元素 */ + public void Add(int num) + { + // 元素数量超出容量时,触发扩容机制 + if (size == Capacity()) + ExtendCapacity(); + nums[size] = num; + // 更新元素数量 + size++; + } + + /* 中间插入元素 */ + public void Insert(int index, int num) + { + if (index >= size) + throw new IndexOutOfRangeException("索引越界"); + // 元素数量超出容量时,触发扩容机制 + if (size == Capacity()) + ExtendCapacity(); + // 将索引 index 以及之后的元素都向后移动一位 + for (int j = size - 1; j >= index; j--) + { + nums[j + 1] = nums[j]; + } + nums[index] = num; + // 更新元素数量 + size++; + } + + /* 删除元素 */ + public int Remove(int index) + { + if (index >= size) + throw new IndexOutOfRangeException("索引越界"); + int num = nums[index]; + // 将索引 index 之后的元素都向前移动一位 + for (int j = index; j < size - 1; j++) + { + nums[j] = nums[j + 1]; + } + // 更新元素数量 + size--; + // 返回被删除元素 + return num; + } + + /* 列表扩容 */ + public void ExtendCapacity() + { + // 新建一个长度为 size 的数组,并将原数组拷贝到新数组 + System.Array.Resize(ref nums, Capacity() * extendRatio); + // 更新列表容量 + capacity = nums.Length; + } + } ``` diff --git a/docs/chapter_computational_complexity/performance_evaluation.md b/docs/chapter_computational_complexity/performance_evaluation.md index dc1806e7..c664a208 100644 --- a/docs/chapter_computational_complexity/performance_evaluation.md +++ b/docs/chapter_computational_complexity/performance_evaluation.md @@ -16,7 +16,7 @@ comments: true - **时间效率** ,即算法的运行速度的快慢。 - **空间效率** ,即算法占用的内存空间大小。 -数据结构与算法追求 “运行得快、内存占用少” ,而如何去评价算法效率则是非常重要的问题,因为只有知道如何评价算法,才能去做算法之间的对比分析,以及优化算法设计。 +数据结构与算法追求“运行得快、内存占用少”,而如何去评价算法效率则是非常重要的问题,因为只有知道如何评价算法,才能去做算法之间的对比分析,以及优化算法设计。 ## 效率评估方法 @@ -38,6 +38,6 @@ comments: true ## 复杂度分析的重要性 -复杂度分析给出一把评价算法效率的 “标尺” ,告诉我们执行某个算法需要多少时间和空间资源,也让我们可以开展不同算法之间的效率对比。 +复杂度分析给出一把评价算法效率的“标尺”,告诉我们执行某个算法需要多少时间和空间资源,也让我们可以开展不同算法之间的效率对比。 计算复杂度是个数学概念,对于初学者可能比较抽象,学习难度相对较高。从这个角度出发,其并不适合作为第一章内容。但是,当我们讨论某个数据结构或者算法的特点时,难以避免需要分析它的运行速度和空间使用情况。**因此,在展开学习数据结构与算法之前,建议读者先对计算复杂度建立起初步的了解,并且能够完成简单案例的复杂度分析**。 diff --git a/docs/chapter_computational_complexity/space_complexity.md b/docs/chapter_computational_complexity/space_complexity.md index 83db2608..df5d98f3 100644 --- a/docs/chapter_computational_complexity/space_complexity.md +++ b/docs/chapter_computational_complexity/space_complexity.md @@ -38,7 +38,7 @@ comments: true Node(int x) { val = x; } } - /* 函数(或称方法) */ + /* 函数 */ int function() { // do something... return 0; @@ -63,7 +63,7 @@ comments: true Node(int x) : val(x), next(nullptr) {} }; - /* 函数(或称方法) */ + /* 函数 */ int func() { // do something... return 0; @@ -87,7 +87,7 @@ comments: true self.val = x # 结点值 self.next = None # 指向下一结点的指针(引用) - """ 函数(或称方法) """ + """ 函数 """ def function(): # do something... return 0 @@ -113,7 +113,7 @@ comments: true return &Node{val: val} } - /* 函数(或称方法)*/ + /* 函数 */ func function() int { // do something... return 0 @@ -149,14 +149,36 @@ comments: true === "C#" ```csharp title="" + /* 类 */ + class Node + { + int val; + Node next; + Node(int x) { val = x; } + } + /* 函数 */ + int function() + { + // do something... + return 0; + } + + int algorithm(int n) // 输入数据 + { + int a = 0; // 暂存数据(常量) + int b = 0; // 暂存数据(变量) + Node node = new Node(0); // 暂存数据(对象) + int c = function(); // 栈帧空间(调用函数) + return a + b + c; // 输出数据 + } ``` ## 推算方法 -空间复杂度的推算方法和时间复杂度总体类似,只是从统计 “计算操作数量” 变为统计 “使用空间大小” 。与时间复杂度不同的是,**我们一般只关注「最差空间复杂度」**。这是因为内存空间是一个硬性要求,我们必须保证在所有输入数据下都有足够的内存空间预留。 +空间复杂度的推算方法和时间复杂度总体类似,只是从统计“计算操作数量”变为统计“使用空间大小”。与时间复杂度不同的是,**我们一般只关注「最差空间复杂度」**。这是因为内存空间是一个硬性要求,我们必须保证在所有输入数据下都有足够的内存空间预留。 -**最差空间复杂度中的 “最差” 有两层含义**,分别为输入数据的最差分布、算法运行中的最差时间点。 +**最差空间复杂度中的“最差”有两层含义**,分别为输入数据的最差分布、算法运行中的最差时间点。 - **以最差输入数据为准。** 当 $n < 10$ 时,空间复杂度为 $O(1)$ ;但是当 $n > 10$ 时,初始化的数组 `nums` 使用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$ ; - **以算法运行过程中的峰值内存为准。** 程序在执行最后一行之前,使用 $O(1)$ 空间;当初始化数组 `nums` 时,程序使用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$ ; @@ -228,7 +250,15 @@ comments: true === "C#" ```csharp title="" - + void algorithm(int n) + { + int a = 0; // O(1) + int[] b = new int[10000]; // O(1) + if (n > 10) + { + int[] nums = new int[n]; // O(n) + } + } ``` **在递归函数中,需要注意统计栈帧空间。** 例如函数 `loop()`,在循环中调用了 $n$ 次 `function()` ,每轮中的 `function()` 都返回并释放了栈帧空间,因此空间复杂度仍为 $O(1)$ 。而递归函数 `recur()` 在运行中会同时存在 $n$ 个未返回的 `recur()` ,从而使用 $O(n)$ 的栈帧空间。 @@ -330,13 +360,31 @@ comments: true === "C" ```c title="" - + ``` === "C#" ```csharp title="" - + int function() + { + // do something + return 0; + } + /* 循环 O(1) */ + void loop(int n) + { + for (int i = 0; i < n; i++) + { + function(); + } + } + /* 递归 O(n) */ + int recur(int n) + { + if (n == 1) return 1; + return recur(n - 1); + } ``` ## 常见类型 @@ -467,7 +515,25 @@ $$ === "C#" ```csharp title="space_complexity.cs" - + /* 常数阶 */ + void constant(int n) + { + // 常量、变量、对象占用 O(1) 空间 + int a = 0; + int b = 0; + int[] nums = new int[10000]; + ListNode node = new ListNode(0); + // 循环中的变量占用 O(1) 空间 + for (int i = 0; i < n; i++) + { + int c = 0; + } + // 循环中的函数占用 O(1) 空间 + for (int i = 0; i < n; i++) + { + function(); + } + } ``` ### 线性阶 $O(n)$ @@ -540,7 +606,7 @@ $$ nodes = append(nodes, newNode(i)) } // 长度为 n 的哈希表占用 O(n) 空间 - m := make(map[int]string, n) + m := make(map[int]string, n) for i := 0; i < n; i++ { m[i] = strconv.Itoa(i) } @@ -568,7 +634,24 @@ $$ === "C#" ```csharp title="space_complexity.cs" - + /* 线性阶 */ + void linear(int n) + { + // 长度为 n 的数组占用 O(n) 空间 + int[] nums = new int[n]; + // 长度为 n 的列表占用 O(n) 空间 + List nodes = new(); + for (int i = 0; i < n; i++) + { + nodes.Add(new ListNode(i)); + } + // 长度为 n 的哈希表占用 O(n) 空间 + Dictionary map = new(); + for (int i = 0; i < n; i++) + { + map.Add(i, i.ToString()); + } + } ``` 以下递归函数会同时存在 $n$ 个未返回的 `algorithm()` 函数,使用 $O(n)$ 大小的栈帧空间。 @@ -639,7 +722,13 @@ $$ === "C#" ```csharp title="space_complexity.cs" - + /* 线性阶(递归实现) */ + void linearRecur(int n) + { + Console.WriteLine("递归 n = " + n); + if (n == 1) return; + linearRecur(n - 1); + } ``` ![space_complexity_recursive_linear](space_complexity.assets/space_complexity_recursive_linear.png) @@ -729,6 +818,23 @@ $$ === "C#" ```csharp title="space_complexity.cs" + /* 平方阶 */ + void quadratic(int n) + { + // 矩阵占用 O(n^2) 空间 + int[,] numMatrix = new int[n, n]; + // 二维列表占用 O(n^2) 空间 + List> numList = new(); + for (int i = 0; i < n; i++) + { + List tmp = new(); + for (int j = 0; j < n; j++) + { + tmp.Add(0); + } + numList.Add(tmp); + } + } ``` @@ -777,8 +883,8 @@ $$ if n <= 0 { return 0 } + // 数组 nums 长度为 n, n-1, ..., 2, 1 nums := make([]int, n) - fmt.Printf("递归 n = %d 中的 nums 长度 = %d \n", n, len(nums)) return spaceQuadraticRecur(n - 1) } ``` @@ -804,6 +910,14 @@ $$ === "C#" ```csharp title="space_complexity.cs" + /* 平方阶(递归实现) */ + int quadraticRecur(int n) + { + if (n <= 0) return 0; + // 数组 nums 长度为 n, n-1, ..., 2, 1 + int[] nums = new int[n]; + return quadraticRecur(n - 1); + } ``` @@ -889,7 +1003,15 @@ $$ === "C#" ```csharp title="space_complexity.cs" - + /* 指数阶(建立满二叉树) */ + TreeNode? buildTree(int n) + { + if (n == 0) return null; + TreeNode root = new TreeNode(0); + root.left = buildTree(n - 1); + root.right = buildTree(n - 1); + return root; + } ``` ![space_complexity_exponential](space_complexity.assets/space_complexity_exponential.png) diff --git a/docs/chapter_computational_complexity/space_time_tradeoff.md b/docs/chapter_computational_complexity/space_time_tradeoff.md index 23c2ae7a..651aff14 100644 --- a/docs/chapter_computational_complexity/space_time_tradeoff.md +++ b/docs/chapter_computational_complexity/space_time_tradeoff.md @@ -130,7 +130,23 @@ comments: true === "C#" ```csharp title="leetcode_two_sum.cs" - + class SolutionBruteForce + { + public int[] twoSum(int[] nums, int target) + { + int size = nums.Length; + // 两层循环,时间复杂度 O(n^2) + for (int i = 0; i < size - 1; i++) + { + for (int j = i + 1; j < size; j++) + { + if (nums[i] + nums[j] == target) + return new int[] { i, j }; + } + } + return new int[0]; + } + } ``` ### 方法二:辅助哈希表 @@ -258,5 +274,23 @@ comments: true === "C#" ```csharp title="leetcode_two_sum.cs" - + class SolutionHashMap + { + public int[] twoSum(int[] nums, int target) + { + int size = nums.Length; + // 辅助哈希表,空间复杂度 O(n) + Dictionary dic = new(); + // 单层循环,时间复杂度 O(n) + for (int i = 0; i < size; i++) + { + if (dic.ContainsKey(target - nums[i])) + { + return new int[] { dic[target - nums[i]], i }; + } + dic.Add(nums[i], i); + } + return new int[0]; + } + } ``` diff --git a/docs/chapter_computational_complexity/summary.md b/docs/chapter_computational_complexity/summary.md index 7e76d2c1..0a1ee80b 100644 --- a/docs/chapter_computational_complexity/summary.md +++ b/docs/chapter_computational_complexity/summary.md @@ -23,6 +23,6 @@ comments: true - 与时间复杂度的定义类似,「空间复杂度」统计算法占用空间随着数据量变大时的增长趋势。 -- 算法运行中相关内存空间可分为输入空间、暂存空间、输出空间。通常情况下,空间复杂度不计入输入空间。暂存空间可分为指令空间、数据空间、栈帧空间,其中栈帧空间一般在递归函数中才会影响到空间复杂度。 +- 算法运行中相关内存空间可分为输入空间、暂存空间、输出空间。通常情况下,输入空间不计入空间复杂度计算。暂存空间可分为指令空间、数据空间、栈帧空间,其中栈帧空间一般在递归函数中才会影响到空间复杂度。 - 我们一般只关心「最差空间复杂度」,即统计算法在「最差输入数据」和「最差运行时间点」下的空间复杂度。 - 常见空间复杂度从小到大排列有 $O(1)$ , $O(\log n)$ , $O(n)$ , $O(n^2)$ , $O(2^n)$ 。 diff --git a/docs/chapter_computational_complexity/time_complexity.md b/docs/chapter_computational_complexity/time_complexity.md index bc8be891..aafb0d89 100644 --- a/docs/chapter_computational_complexity/time_complexity.md +++ b/docs/chapter_computational_complexity/time_complexity.md @@ -97,7 +97,33 @@ $$ === "C#" ```csharp title="" + // 在某运行平台下 + void algorithm(int n) + { + int a = 2; // 1 ns + a = a + 1; // 1 ns + a = a * 2; // 10 ns + // 循环 n 次 + for (int i = 0; i < n; i++) + { // 1 ns ,每轮都要执行 i++ + Console.WriteLine(0); // 5 ns + } + } + ``` +=== "Swift" + + ```swift title="" + // 在某运行平台下 + func algorithm(_ n: Int) { + var a = 2 // 1 ns + a = a + 1 // 1 ns + a = a * 2 // 10 ns + // 循环 n 次 + for _ in 0 ..< n { // 1 ns + print(0) // 5 ns + } + } ``` 但实际上, **统计算法的运行时间既不合理也不现实。** 首先,我们不希望预估时间和运行平台绑定,毕竟算法需要跑在各式各样的平台之上。其次,我们很难获知每一种操作的运行时间,这为预估过程带来了极大的难度。 @@ -106,7 +132,7 @@ $$ 「时间复杂度分析」采取了不同的做法,其统计的不是算法运行时间,而是 **算法运行时间随着数据量变大时的增长趋势** 。 -“时间增长趋势” 这个概念比较抽象,我们借助一个例子来理解。设输入数据大小为 $n$ ,给定三个算法 `A` , `B` , `C` 。 +“时间增长趋势”这个概念比较抽象,我们借助一个例子来理解。设输入数据大小为 $n$ ,给定三个算法 `A` , `B` , `C` 。 - 算法 `A` 只有 $1$ 个打印操作,算法运行时间不随着 $n$ 增大而增长。我们称此算法的时间复杂度为「常数阶」。 - 算法 `B` 中的打印操作需要循环 $n$ 次,算法运行时间随着 $n$ 增大成线性增长。此算法的时间复杂度被称为「线性阶」。 @@ -212,7 +238,50 @@ $$ === "C#" ```csharp title="" + // 算法 A 时间复杂度:常数阶 + void algorithm_A(int n) + { + Console.WriteLine(0); + } + // 算法 B 时间复杂度:线性阶 + void algorithm_B(int n) + { + for (int i = 0; i < n; i++) + { + Console.WriteLine(0); + } + } + // 算法 C 时间复杂度:常数阶 + void algorithm_C(int n) + { + for (int i = 0; i < 1000000; i++) + { + Console.WriteLine(0); + } + } + ``` +=== "Swift" + + ```swift title="" + // 算法 A 时间复杂度:常数阶 + func algorithmA(_ n: Int) { + print(0) + } + + // 算法 B 时间复杂度:线性阶 + func algorithmB(_ n: Int) { + for _ in 0 ..< n { + print(0) + } + } + + // 算法 C 时间复杂度:常数阶 + func algorithmC(_ n: Int) { + for _ in 0 ..< 1000000 { + print(0) + } + } ``` ![time_complexity_first_example](time_complexity.assets/time_complexity_first_example.png) @@ -223,7 +292,7 @@ $$ **时间复杂度可以有效评估算法效率。** 算法 `B` 运行时间的增长是线性的,在 $n > 1$ 时慢于算法 `A` ,在 $n > 1000000$ 时慢于算法 `C` 。实质上,只要输入数据大小 $n$ 足够大,复杂度为「常数阶」的算法一定优于「线性阶」的算法,这也正是时间增长趋势的含义。 -**时间复杂度分析将统计「计算操作的运行时间」简化为统计「计算操作的数量」。** 这是因为,无论是运行平台、还是计算操作类型,都与算法运行时间的增长趋势无关。因此,我们可以简单地将所有计算操作的执行时间统一看作是相同的 “单位时间” 。 +**时间复杂度分析将统计「计算操作的运行时间」简化为统计「计算操作的数量」。** 这是因为,无论是运行平台、还是计算操作类型,都与算法运行时间的增长趋势无关。因此,我们可以简单地将所有计算操作的执行时间统一看作是相同的“单位时间”。 **时间复杂度也存在一定的局限性。** 比如,虽然算法 `A` 和 `C` 的时间复杂度相同,但是实际的运行时间有非常大的差别。再比如,虽然算法 `B` 比 `C` 的时间复杂度要更高,但在输入数据大小 $n$ 比较小时,算法 `B` 是要明显优于算法 `C` 的。即使存在这些问题,计算复杂度仍然是评判算法效率的最有效、最常用方法。 @@ -310,7 +379,29 @@ $$ === "C#" ```csharp title="" + void algorithm(int n) { + int a = 1; // +1 + a = a + 1; // +1 + a = a * 2; // +1 + // 循环 n 次 + for (int i = 0; i < n; i++) { // +1(每轮都执行 i ++) + Console.WriteLine(0); // +1 + } + } + ``` +=== "Swift" + + ```swift title="" + func algorithm(n: Int) { + var a = 1 // +1 + a = a + 1 // +1 + a = a * 2 // +1 + // 循环 n 次 + for _ in 0 ..< n { // +1 + print(0) // +1 + } + } ``` $T(n)$ 是个一次函数,说明时间增长趋势是线性的,因此易得时间复杂度是线性阶。 @@ -325,7 +416,7 @@ $T(n)$ 是个一次函数,说明时间增长趋势是线性的,因此易得 $$ T(n) \leq c \cdot f(n) $$ - 则可认为 $f(n)$ 给出了 $T(n)$ 的一个渐近上界,记为 + 则可认为 $f(n)$ 给出了 $T(n)$ 的一个渐近上界,记为 $$ T(n) = O(f(n)) $$ @@ -457,14 +548,50 @@ $$ === "C#" ```csharp title="" + void algorithm(int n) + { + int a = 1; // +0(技巧 1) + a = a + n; // +0(技巧 1) + // +n(技巧 2) + for (int i = 0; i < 5 * n + 1; i++) + { + Console.WriteLine(0); + } + // +n*n(技巧 3) + for (int i = 0; i < 2 * n; i++) + { + for (int j = 0; j < n + 1; j++) + { + Console.WriteLine(0); + } + } + } + ``` +=== "Swift" + + ```swift title="" + func algorithm(n: Int) { + var a = 1 // +0(技巧 1) + a = a + n // +0(技巧 1) + // +n(技巧 2) + for _ in 0 ..< (5 * n + 1) { + print(0) + } + // +n*n(技巧 3) + for _ in 0 ..< (2 * n) { + for _ in 0 ..< (n + 1) { + print(0) + } + } + } ``` ### 2. 判断渐近上界 **时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将处于主导作用,其它项的影响都可以被忽略。 -以下表格给出了一些例子,其中有一些夸张的值,是想要向大家强调 **系数无法撼动阶数** 这一结论。在 $n$ 趋于无穷大时,这些常数都是 “浮云” 。 +以下表格给出了一些例子,其中有一些夸张的值,是想要向大家强调 **系数无法撼动阶数** 这一结论。在 $n$ 趋于无穷大时,这些常数都是“浮云”。
@@ -576,7 +703,29 @@ $$ === "C#" ```csharp title="time_complexity.cs" + /* 常数阶 */ + int constant(int n) + { + int count = 0; + int size = 100000; + for (int i = 0; i < size; i++) + count++; + return count; + } + ``` +=== "Swift" + + ```swift title="time_complexity.swift" + // 常数阶 + func constant(n: Int) -> Int { + var count = 0 + let size = 100000 + for _ in 0 ..< size { + count += 1 + } + return count + } ``` ### 线性阶 $O(n)$ @@ -652,7 +801,27 @@ $$ === "C#" ```csharp title="time_complexity.cs" + /* 线性阶 */ + int linear(int n) + { + int count = 0; + for (int i = 0; i < n; i++) + count++; + return count; + } + ``` +=== "Swift" + + ```swift title="time_complexity.swift" + // 线性阶 + func linear(n: Int) -> Int { + var count = 0 + for _ in 0 ..< n { + count += 1 + } + return count + } ``` 「遍历数组」和「遍历链表」等操作,时间复杂度都为 $O(n)$ ,其中 $n$ 为数组或链表的长度。 @@ -736,7 +905,31 @@ $$ === "C#" ```csharp title="time_complexity.cs" + /* 线性阶(遍历数组) */ + int arrayTraversal(int[] nums) + { + int count = 0; + // 循环次数与数组长度成正比 + foreach(int num in nums) + { + count++; + } + return count; + } + ``` +=== "Swift" + + ```swift title="time_complexity.swift" + // 线性阶(遍历数组) + func arrayTraversal(nums: [Int]) -> Int { + var count = 0 + // 循环次数与数组长度成正比 + for _ in nums { + count += 1 + } + return count + } ``` ### 平方阶 $O(n^2)$ @@ -825,7 +1018,36 @@ $$ === "C#" ```csharp title="time_complexity.cs" + /* 平方阶 */ + int quadratic(int n) + { + int count = 0; + // 循环次数与数组长度成平方关系 + for (int i = 0; i < n; i++) + { + for (int j = 0; j < n; j++) + { + count++; + } + } + return count; + } + ``` +=== "Swift" + + ```swift title="time_complexity.swift" + // 平方阶 + func quadratic(n: Int) -> Int { + var count = 0 + // 循环次数与数组长度成平方关系 + for _ in 0 ..< n { + for _ in 0 ..< n { + count += 1 + } + } + return count + } ``` ![time_complexity_constant_linear_quadratic](time_complexity.assets/time_complexity_constant_linear_quadratic.png) @@ -947,14 +1169,59 @@ $$ === "C#" ```csharp title="time_complexity.cs" + /* 平方阶(冒泡排序) */ + int bubbleSort(int[] nums) + { + int count = 0; // 计数器 + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = nums.Length - 1; i > 0; i--) + { + // 内循环:冒泡操作 + for (int j = 0; j < i; j++) + { + if (nums[j] > nums[j + 1]) + { + // 交换 nums[j] 与 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交换包含 3 个单元操作 + } + } + } + return count; + } ``` +=== "Swift" + + ```swift title="time_complexity.swift" + // 平方阶(冒泡排序) + func bubbleSort(nums: inout [Int]) -> Int { + var count = 0 // 计数器 + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for i in sequence(first: nums.count - 1, next: { $0 > 0 ? $0 - 1 : nil }) { + // 内循环:冒泡操作 + for j in 0 ..< i { + if nums[j] > nums[j + 1] { + // 交换 nums[j] 与 nums[j + 1] + let tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 // 元素交换包含 3 个单元操作 + } + } + } + return count + } + ``` + ### 指数阶 $O(2^n)$ !!! note - 生物学科中的 “细胞分裂” 即是指数阶增长:初始状态为 $1$ 个细胞,分裂一轮后为 $2$ 个,分裂两轮后为 $4$ 个,……,分裂 $n$ 轮后有 $2^n$ 个细胞。 + 生物学科中的“细胞分裂”即是指数阶增长:初始状态为 $1$ 个细胞,分裂一轮后为 $2$ 个,分裂两轮后为 $4$ 个,……,分裂 $n$ 轮后有 $2^n$ 个细胞。 指数阶增长得非常快,在实际应用中一般是不能被接受的。若一个问题使用「暴力枚举」求解的时间复杂度是 $O(2^n)$ ,那么一般都需要使用「动态规划」或「贪心算法」等算法来求解。 @@ -1048,7 +1315,41 @@ $$ === "C#" ```csharp title="time_complexity.cs" + /* 指数阶(循环实现) */ + int exponential(int n) + { + int count = 0, bas = 1; + // cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) + for (int i = 0; i < n; i++) + { + for (int j = 0; j < bas; j++) + { + count++; + } + bas *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + ``` +=== "Swift" + + ```swift title="time_complexity.swift" + // 指数阶(循环实现) + func exponential(n: Int) -> Int { + var count = 0 + var base = 1 + // cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) + for _ in 0 ..< n { + for _ in 0 ..< base { + count += 1 + } + base *= 2 + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count + } ``` ![time_complexity_exponential](time_complexity.assets/time_complexity_exponential.png) @@ -1119,14 +1420,31 @@ $$ === "C#" ```csharp title="time_complexity.cs" + /* 指数阶(递归实现) */ + int expRecur(int n) + { + if (n == 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + ``` +=== "Swift" + + ```swift title="time_complexity.swift" + // 指数阶(递归实现) + func expRecur(n: Int) -> Int { + if n == 1 { + return 1 + } + return expRecur(n: n - 1) + expRecur(n: n - 1) + 1 + } ``` ### 对数阶 $O(\log n)$ -对数阶与指数阶正好相反,后者反映 “每轮增加到两倍的情况” ,而前者反映 “每轮缩减到一半的情况” 。对数阶仅次于常数阶,时间增长的很慢,是理想的时间复杂度。 +对数阶与指数阶正好相反,后者反映“每轮增加到两倍的情况”,而前者反映“每轮缩减到一半的情况”。对数阶仅次于常数阶,时间增长的很慢,是理想的时间复杂度。 -对数阶常出现于「二分查找」和「分治算法」中,体现 “一分为多” 、“化繁为简” 的算法思想。 +对数阶常出现于「二分查找」和「分治算法」中,体现“一分为多”、“化繁为简”的算法思想。 设输入数据大小为 $n$ ,由于每轮缩减到一半,因此循环次数是 $\log_2 n$ ,即 $2^n$ 的反函数。 @@ -1205,7 +1523,32 @@ $$ === "C#" ```csharp title="time_complexity.cs" + /* 对数阶(循环实现) */ + int logarithmic(float n) + { + int count = 0; + while (n > 1) + { + n = n / 2; + count++; + } + return count; + } + ``` +=== "Swift" + + ```swift title="time_complexity.swift" + // 对数阶(循环实现) + func logarithmic(n: Int) -> Int { + var count = 0 + var n = n + while n > 1 { + n = n / 2 + count += 1 + } + return count + } ``` ![time_complexity_logarithmic](time_complexity.assets/time_complexity_logarithmic.png) @@ -1276,7 +1619,24 @@ $$ === "C#" ```csharp title="time_complexity.cs" + /* 对数阶(递归实现) */ + int logRecur(float n) + { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } + ``` +=== "Swift" + + ```swift title="time_complexity.swift" + // 对数阶(递归实现) + func logRecur(n: Int) -> Int { + if n <= 1 { + return 0 + } + return logRecur(n: n / 2) + 1 + } ``` ### 线性对数阶 $O(n \log n)$ @@ -1366,7 +1726,34 @@ $$ === "C#" ```csharp title="time_complexity.cs" + /* 线性对数阶 */ + int linearLogRecur(float n) + { + if (n <= 1) return 1; + int count = linearLogRecur(n / 2) + + linearLogRecur(n / 2); + for (int i = 0; i < n; i++) + { + count++; + } + return count; + } + ``` +=== "Swift" + + ```swift title="time_complexity.swift" + // 线性对数阶 + func linearLogRecur(n: Double) -> Int { + if n <= 1 { + return 1 + } + var count = linearLogRecur(n: n / 2) + linearLogRecur(n: n / 2) + for _ in 0 ..< Int(n) { + count += 1 + } + return count + } ``` ![time_complexity_logarithmic_linear](time_complexity.assets/time_complexity_logarithmic_linear.png) @@ -1464,7 +1851,35 @@ $$ === "C#" ```csharp title="time_complexity.cs" + /* 阶乘阶(递归实现) */ + int factorialRecur(int n) + { + if (n == 0) return 1; + int count = 0; + // 从 1 个分裂出 n 个 + for (int i = 0; i < n; i++) + { + count += factorialRecur(n - 1); + } + return count; + } + ``` +=== "Swift" + + ```swift title="time_complexity.swift" + // 阶乘阶(递归实现) + func factorialRecur(n: Int) -> Int { + if n == 0 { + return 1 + } + var count = 0 + // 从 1 个分裂出 n 个 + for _ in 0 ..< n { + count += factorialRecur(n: n - 1) + } + return count + } ``` ![time_complexity_factorial](time_complexity.assets/time_complexity_factorial.png) @@ -1485,7 +1900,7 @@ $$ ```java title="worst_best_time_complexity.java" public class worst_best_time_complexity { /* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */ - static int[] randomNumbers(int n) { + int[] randomNumbers(int n) { Integer[] nums = new Integer[n]; // 生成数组 nums = { 1, 2, 3, ..., n } for (int i = 0; i < n; i++) { @@ -1502,7 +1917,7 @@ $$ } /* 查找数组 nums 中数字 1 所在索引 */ - static int findOne(int[] nums) { + int findOne(int[] nums) { for (int i = 0; i < nums.length; i++) { if (nums[i] == 1) return i; @@ -1511,7 +1926,7 @@ $$ } /* Driver Code */ - public static void main(String[] args) { + public void main(String[] args) { for (int i = 0; i < 10; i++) { int n = 100; int[] nums = randomNumbers(n); @@ -1652,14 +2067,92 @@ $$ === "C#" ```csharp title="worst_best_time_complexity.cs" + /* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */ + int[] randomNumbers(int n) + { + int[] nums = new int[n]; + // 生成数组 nums = { 1, 2, 3, ..., n } + for (int i = 0; i < n; i++) + { + nums[i] = i + 1; + } + // 随机打乱数组元素 + for (int i = 0; i < nums.Length; i++) + { + var index = new Random().Next(i, nums.Length); + var tmp = nums[i]; + var ran = nums[index]; + nums[i] = ran; + nums[index] = tmp; + } + return nums; + } + + /* 查找数组 nums 中数字 1 所在索引 */ + int findOne(int[] nums) + { + for (int i = 0; i < nums.Length; i++) + { + if (nums[i] == 1) + return i; + } + return -1; + } + + /* Driver Code */ + public void main(String[] args) + { + for (int i = 0; i < 10; i++) + { + int n = 100; + int[] nums = randomNumbers(n); + int index = findOne(nums); + Console.WriteLine("\n数组 [ 1, 2, ..., n ] 被打乱后 = " + string.Join(",", nums)); + Console.WriteLine("数字 1 的索引为 " + index); + } + } + ``` + +=== "Swift" + + ```swift title="" + // 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 + func randomNumbers(n: Int) -> [Int] { + // 生成数组 nums = { 1, 2, 3, ..., n } + var nums = Array(1 ... n) + // 随机打乱数组元素 + nums.shuffle() + return nums + } + + // 查找数组 nums 中数字 1 所在索引 + func findOne(nums: [Int]) -> Int { + for i in nums.indices { + if nums[i] == 1 { + return i + } + } + return -1 + } + + // Driver Code + func main() { + for _ in 0 ..< 10 { + let n = 100 + let nums = randomNumbers(n: n) + let index = findOne(nums: nums) + print("数组 [ 1, 2, ..., n ] 被打乱后 =", nums) + print("数字 1 的索引为", index) + } + } ``` !!! tip - 我们在实际应用中很少使用「最佳时间复杂度」,因为往往只有很小概率下才能达到,会带来一定的误导性。反之,「最差时间复杂度」最为实用,因为它给出了一个 “效率安全值” ,让我们可以放心地使用算法。 + 我们在实际应用中很少使用「最佳时间复杂度」,因为往往只有很小概率下才能达到,会带来一定的误导性。反之,「最差时间复杂度」最为实用,因为它给出了一个“效率安全值”,让我们可以放心地使用算法。 -从上述示例可以看出,最差或最佳时间复杂度只出现在 “特殊分布的数据” 中,这些情况的出现概率往往很小,因此并不能最真实地反映算法运行效率。**相对地,「平均时间复杂度」可以体现算法在随机输入数据下的运行效率,用 $\Theta$ 记号(Theta Notation)来表示**。 +从上述示例可以看出,最差或最佳时间复杂度只出现在“特殊分布的数据”中,这些情况的出现概率往往很小,因此并不能最真实地反映算法运行效率。**相对地,「平均时间复杂度」可以体现算法在随机输入数据下的运行效率,用 $\Theta$ 记号(Theta Notation)来表示**。 对于部分算法,我们可以简单地推算出随机数据分布下的平均情况。比如上述示例,由于输入数组是被打乱的,因此元素 $1$ 出现在任意索引的概率都是相等的,那么算法的平均循环次数则是数组长度的一半 $\frac{n}{2}$ ,平均时间复杂度为 $\Theta(\frac{n}{2}) = \Theta(n)$ 。 diff --git a/docs/chapter_data_structure/classification_of_data_strcuture.md b/docs/chapter_data_structure/classification_of_data_strcuture.md index 7c8e22f0..b41266bb 100644 --- a/docs/chapter_data_structure/classification_of_data_strcuture.md +++ b/docs/chapter_data_structure/classification_of_data_strcuture.md @@ -10,7 +10,7 @@ comments: true **「逻辑结构」反映了数据之间的逻辑关系。** 数组和链表的数据按照顺序依次排列,反映了数据间的线性关系;树从顶至底按层级排列,反映了祖先与后代之间的派生关系;图由结点和边组成,反映了复杂网络关系。 -我们一般将逻辑结构分为「线性」和「非线性」两种。“线性” 这个概念很直观,即表明数据在逻辑关系上是排成一条线的;而如果数据之间的逻辑关系是非线形的(例如是网状或树状的),那么就是非线性数据结构。 +我们一般将逻辑结构分为「线性」和「非线性」两种。“线性”这个概念很直观,即表明数据在逻辑关系上是排成一条线的;而如果数据之间的逻辑关系是非线形的(例如是网状或树状的),那么就是非线性数据结构。 - **线性数据结构:** 数组、链表、栈、队列、哈希表; - **非线性数据结构:** 树、图、堆、哈希表; @@ -40,4 +40,4 @@ comments: true !!! tip - 数组与链表是其他所有数据结构的 “底层积木”,建议读者一定要多花些时间了解。 + 数组与链表是其他所有数据结构的“底层积木”,建议读者一定要多花些时间了解。 diff --git a/docs/chapter_data_structure/data_and_memory.md b/docs/chapter_data_structure/data_and_memory.md index 198df88c..3e1e2167 100644 --- a/docs/chapter_data_structure/data_and_memory.md +++ b/docs/chapter_data_structure/data_and_memory.md @@ -10,10 +10,10 @@ comments: true **「基本数据类型」是 CPU 可以直接进行运算的类型,在算法中直接被使用。** -- 「整数」根据不同的长度分为 byte, short, int, long ,根据算法需求选用,即在满足取值范围的情况下尽量减小内存空间占用。 -- 「浮点数」代表小数,根据长度分为 float, double ,同样根据算法的实际需求选用。 -- 「字符」在计算机中是以字符集的形式保存的,char 的值实际上是数字,代表字符集中的编号,计算机通过字符集查表来完成编号到字符的转换。 -- 「布尔」代表逻辑中的 ”是“ 与 ”否“ ,其占用空间需要具体根据编程语言确定,通常为 1 byte 或 1 bit 。 +- 「整数」根据不同的长度分为 byte, short, int, long ,根据算法需求选用,即在满足取值范围的情况下尽量减小内存空间占用; +- 「浮点数」代表小数,根据长度分为 float, double ,同样根据算法的实际需求选用; +- 「字符」在计算机中是以字符集的形式保存的,char 的值实际上是数字,代表字符集中的编号,计算机通过字符集查表来完成编号到字符的转换。占用空间与具体编程语言有关,通常为 2 bytes 或 1 byte ; +- 「布尔」代表逻辑中的 ”是“ 与 ”否“ ,其占用空间需要具体根据编程语言确定,通常为 1 byte 或 1 bit ; !!! note "字节与比特" @@ -31,7 +31,7 @@ comments: true | | long | 8 bytes | $-2^{63}$ ~ $2^{63} - 1$ | $0$ | | 浮点数 | **float** | 4 bytes | $-3.4 \times 10^{38}$ ~ $3.4 \times 10^{38}$ | $0.0$ f | | | double | 8 bytes | $-1.7 \times 10^{308}$ ~ $1.7 \times 10^{308}$ | $0.0$ | -| 字符 | **char** | 2 bytes | $0$ ~ $2^{16} - 1$ | $0$ | +| 字符 | **char** | 2 bytes / 1 byte | $0$ ~ $2^{16} - 1$ | $0$ | | 布尔 | **boolean(bool)** | 1 byte / 1 bit | $\text{true}$ 或 $\text{false}$ | $\text{false}$ |
@@ -42,7 +42,7 @@ comments: true **「基本数据类型」与「数据结构」之间的联系与区别** -我们知道,数据结构是在计算机中 **组织与存储数据的方式** ,它的主语是 “结构” ,而不是 “数据” 。比如,我们想要表示 “一排数字” ,自然应该使用「数组」这个数据结构。数组的存储方式使之可以表示数字的相邻关系、先后关系等一系列我们需要的信息,但至于其中存储的是整数 int ,还是小数 float ,或是字符 char ,**则与所谓的数据的结构无关了**。 +我们知道,数据结构是在计算机中 **组织与存储数据的方式** ,它的主语是“结构”,而不是“数据”。比如,我们想要表示“一排数字”,自然应该使用「数组」这个数据结构。数组的存储方式使之可以表示数字的相邻关系、先后关系等一系列我们需要的信息,但至于其中存储的是整数 int ,还是小数 float ,或是字符 char ,**则与所谓的数据的结构无关了**。 === "Java" @@ -57,19 +57,28 @@ comments: true === "C++" ```cpp title="" - + /* 使用多种「基本数据类型」来初始化「数组」 */ + int numbers[5]; + float decimals[5]; + char characters[5]; + bool booleans[5]; ``` === "Python" ```python title="" - + """ Python 的 list 可以自由存储各种基本数据类型和对象 """ + list = [0, 0.0, 'a', False] ``` === "Go" ```go title="" - + // 使用多种「基本数据类型」来初始化「数组」 + var numbers = [5]int{} + var decimals = [5]float64{} + var characters = [5]byte{} + var booleans = [5]bool{} ``` === "JavaScript" @@ -87,13 +96,22 @@ comments: true === "C" ```c title="" + /* 使用多种「基本数据类型」来初始化「数组」 */ + int numbers[10]; + float decimals[10]; + char characters[10]; + bool booleans[10]; ``` === "C#" ```csharp title="" - + /* 使用多种「基本数据类型」来初始化「数组」 */ + int[] numbers = new int[5]; + float[] decimals = new float[5]; + char[] characters = new char[5]; + bool[] booleans = new bool[5]; ``` ## 计算机内存 diff --git a/docs/chapter_hashing/hash_collision.md b/docs/chapter_hashing/hash_collision.md index 79329620..05dc2088 100644 --- a/docs/chapter_hashing/hash_collision.md +++ b/docs/chapter_hashing/hash_collision.md @@ -40,7 +40,7 @@ comments: true ## 开放寻址 -「开放寻址」不引入额外数据结构,而是通过 “向后探测” 来解决哈希冲突。根据探测方法的不同,主要分为 **线性探测、平方探测、多次哈希**。 +「开放寻址」不引入额外数据结构,而是通过“向后探测”来解决哈希冲突。根据探测方法的不同,主要分为 **线性探测、平方探测、多次哈希**。 ### 线性探测 @@ -58,7 +58,7 @@ comments: true 线性探测有以下缺陷: - **不能直接删除元素**。删除元素会导致桶内出现一个空位,在查找其他元素时,该空位有可能导致程序认为元素不存在(即上述第 `2.` 种情况)。因此需要借助一个标志位来标记删除元素。 -- **容易产生聚集**。桶内被占用的连续位置越长,这些连续位置发生哈希冲突的可能性越大,从而进一步促进这一位置的 “聚堆生长” ,最终导致增删查改操作效率的劣化。 +- **容易产生聚集**。桶内被占用的连续位置越长,这些连续位置发生哈希冲突的可能性越大,从而进一步促进这一位置的“聚堆生长”,最终导致增删查改操作效率的劣化。 ### 多次哈希 diff --git a/docs/chapter_hashing/hash_map.md b/docs/chapter_hashing/hash_map.md index 3a53f5db..d8244d80 100644 --- a/docs/chapter_hashing/hash_map.md +++ b/docs/chapter_hashing/hash_map.md @@ -6,7 +6,7 @@ comments: true 哈希表通过建立「键 key」和「值 value」之间的映射,实现高效的元素查找。具体地,输入一个 key ,在哈希表中查询并获取 value ,时间复杂度为 $O(1)$ 。 -例如,给定一个包含 $n$ 个学生的数据库,每个学生有 "姓名 `name` ” 和 “学号 `id` ” 两项数据,希望实现一个查询功能:**输入一个学号,返回对应的姓名**,则可以使用哈希表实现。 +例如,给定一个包含 $n$ 个学生的数据库,每个学生有“姓名 `name` ”和“学号 `id` ”两项数据,希望实现一个查询功能:**输入一个学号,返回对应的姓名**,则可以使用哈希表实现。 ![hash_map](hash_map.assets/hash_map.png) @@ -132,13 +132,50 @@ comments: true === "JavaScript" ```js title="hash_map.js" + /* 初始化哈希表 */ + const map = new ArrayHashMap(); + /* 添加操作 */ + // 在哈希表中添加键值对 (key, value) + map.set(12836, '小哈'); + map.set(15937, '小啰'); + map.set(16750, '小算'); + map.set(13276, '小法'); + map.set(10583, '小鸭'); + /* 查询操作 */ + // 向哈希表输入键 key ,得到值 value + let name = map.get(15937); + + /* 删除操作 */ + // 在哈希表中删除键值对 (key, value) + map.delete(10583); ``` === "TypeScript" ```typescript title="hash_map.ts" + /* 初始化哈希表 */ + const map = new Map(); + /* 添加操作 */ + // 在哈希表中添加键值对 (key, value) + map.set(12836, '小哈'); + map.set(15937, '小啰'); + map.set(16750, '小算'); + map.set(13276, '小法'); + map.set(10583, '小鸭'); + console.info('\n添加完成后,哈希表为\nKey -> Value'); + console.info(map); + /* 查询操作 */ + // 向哈希表输入键 key ,得到值 value + let name = map.get(15937); + console.info('\n输入学号 15937 ,查询到姓名 ' + name); + + /* 删除操作 */ + // 在哈希表中删除键值对 (key, value) + map.delete(10583); + console.info('\n删除 10583 后,哈希表为\nKey -> Value'); + console.info(map); ``` === "C" @@ -150,7 +187,24 @@ comments: true === "C#" ```csharp title="hash_map.cs" + /* 初始化哈希表 */ + Dictionary map = new (); + /* 添加操作 */ + // 在哈希表中添加键值对 (key, value) + map.Add(12836, "小哈"); + map.Add(15937, "小啰"); + map.Add(16750, "小算"); + map.Add(13276, "小法"); + map.Add(10583, "小鸭"); + + /* 查询操作 */ + // 向哈希表输入键 key ,得到值 value + String name = map[15937]; + + /* 删除操作 */ + // 在哈希表中删除键值对 (key, value) + map.Remove(10583); ``` 遍历哈希表有三种方式,即 **遍历键值对、遍历键、遍历值**。 @@ -227,13 +281,38 @@ comments: true === "JavaScript" ```js title="hash_map.js" - + /* 遍历哈希表 */ + // 遍历键值对 key->value + for (const entry of map.entries()) { + if (!entry) continue; + console.info(entry.key + ' -> ' + entry.val); + } + // 单独遍历键 key + for (const key of map.keys()) { + console.info(key); + } + // 单独遍历值 value + for (const val of map.values()) { + console.info(val); + } ``` === "TypeScript" ```typescript title="hash_map.ts" - + /* 遍历哈希表 */ + console.info('\n遍历键值对 Key->Value'); + for (const [k, v] of map.entries()) { + console.info(k + ' -> ' + v); + } + console.info('\n单独遍历键 Key'); + for (const k of map.keys()) { + console.info(k); + } + console.info('\n单独遍历值 Value'); + for (const v of map.values()) { + console.info(v); + } ``` === "C" @@ -245,7 +324,19 @@ comments: true === "C#" ```csharp title="hash_map.cs" - + /* 遍历哈希表 */ + // 遍历键值对 Key->Value + foreach (var kv in map) { + Console.WriteLine(kv.Key + " -> " + kv.Value); + } + // 单独遍历键 key + foreach (int key in map.Keys) { + Console.WriteLine(key); + } + // 单独遍历值 value + foreach (String val in map.Values) { + Console.WriteLine(val); + } ``` ## 哈希函数 @@ -471,13 +562,133 @@ $$ === "JavaScript" ```js title="array_hash_map.js" + /* 键值对 Number -> String */ + class Entry { + constructor(key, val) { + this.key = key; + this.val = val; + } + } + /* 基于数组简易实现的哈希表 */ + class ArrayHashMap { + #bucket; + constructor() { + // 初始化一个长度为 100 的桶(数组) + this.#bucket = new Array(100).fill(null); + } + + /* 哈希函数 */ + #hashFunc(key) { + return key % 100; + } + + /* 查询操作 */ + get(key) { + let index = this.#hashFunc(key); + let entry = this.#bucket[index]; + if (entry === null) return null; + return entry.val; + } + + /* 添加操作 */ + set(key, val) { + let index = this.#hashFunc(key); + this.#bucket[index] = new Entry(key, val); + } + + /* 删除操作 */ + delete(key) { + let index = this.#hashFunc(key); + // 置为 null ,代表删除 + this.#bucket[index] = null; + } + } ``` === "TypeScript" ```typescript title="array_hash_map.ts" - + /* 键值对 Number -> String */ + class Entry { + public key: number; + public val: string; + + constructor(key: number, val: string) { + this.key = key; + this.val = val; + } + } + + /* 基于数组简易实现的哈希表 */ + class ArrayHashMap { + + private readonly bucket: (Entry | null)[]; + + constructor() { + // 初始化一个长度为 100 的桶(数组) + this.bucket = (new Array(100)).fill(null); + } + + /* 哈希函数 */ + private hashFunc(key: number): number { + return key % 100; + } + + /* 查询操作 */ + public get(key: number): string | null { + let index = this.hashFunc(key); + let entry = this.bucket[index]; + if (entry === null) return null; + return entry.val; + } + + /* 添加操作 */ + public set(key: number, val: string) { + let index = this.hashFunc(key); + this.bucket[index] = new Entry(key, val); + } + + /* 删除操作 */ + public delete(key: number) { + let index = this.hashFunc(key); + // 置为 null ,代表删除 + this.bucket[index] = null; + } + + /* 获取所有键值对 */ + public entries(): (Entry | null)[] { + let arr: (Entry | null)[] = []; + for (let i = 0; i < this.bucket.length; i++) { + if (this.bucket[i]) { + arr.push(this.bucket[i]); + } + } + return arr; + } + + /* 获取所有键 */ + public keys(): (number | undefined)[] { + let arr: (number | undefined)[] = []; + for (let i = 0; i < this.bucket.length; i++) { + if (this.bucket[i]) { + arr.push(this.bucket[i]?.key); + } + } + return arr; + } + + /* 获取所有值 */ + public values(): (string | undefined)[] { + let arr: (string | undefined)[] = []; + for (let i = 0; i < this.bucket.length; i++) { + if (this.bucket[i]) { + arr.push(this.bucket[i]?.val); + } + } + return arr; + } + } ``` === "C" @@ -489,7 +700,60 @@ $$ === "C#" ```csharp title="array_hash_map.cs" + /* 键值对 int->String */ + class Entry + { + public int key; + public String val; + public Entry(int key, String val) + { + this.key = key; + this.val = val; + } + } + /* 基于数组简易实现的哈希表 */ + class ArrayHashMap + { + private List bucket; + public ArrayHashMap() + { + // 初始化一个长度为 100 的桶(数组) + bucket = new (); + for (int i = 0; i < 100; i++) + { + bucket.Add(null); + } + } + /* 哈希函数 */ + private int hashFunc(int key) + { + int index = key % 100; + return index; + } + /* 查询操作 */ + public String? get(int key) + { + int index = hashFunc(key); + Entry? pair = bucket[index]; + if (pair == null) return null; + return pair.val; + } + /* 添加操作 */ + public void put(int key, String val) + { + Entry pair = new Entry(key, val); + int index = hashFunc(key); + bucket[index]=pair; + } + /* 删除操作 */ + public void remove(int key) + { + int index = hashFunc(key); + // 置为 null ,代表删除 + bucket[index]=null; + } + } ``` ## 哈希冲突 @@ -510,4 +774,4 @@ $$ - 尽量少地发生哈希冲突; - 时间复杂度 $O(1)$ ,计算尽可能高效; -- 空间使用率高,即 “键值对占用空间 / 哈希表总占用空间” 尽可能大; +- 空间使用率高,即“键值对占用空间 / 哈希表总占用空间”尽可能大; diff --git a/docs/chapter_introduction/algorithms_are_everywhere.md b/docs/chapter_introduction/algorithms_are_everywhere.md index 24f47413..b80a525b 100644 --- a/docs/chapter_introduction/algorithms_are_everywhere.md +++ b/docs/chapter_introduction/algorithms_are_everywhere.md @@ -4,7 +4,7 @@ comments: true # 算法无处不在 -听到 “算法” 这个词,我们一般会联想到数学。但实际上,大多数算法并不包含复杂的数学,而更像是在考察基本逻辑,而这些逻辑在我们日常生活中处处可见。 +听到“算法”这个词,我们一般会联想到数学。但实际上,大多数算法并不包含复杂的数学,而更像是在考察基本逻辑,而这些逻辑在我们日常生活中处处可见。 在正式介绍算法之前,我想告诉你一件有趣的事:**其实,你在过去已经学会了很多算法,并且已经习惯将它们应用到日常生活中。** 接下来,我将介绍两个具体例子来佐证。 diff --git a/docs/chapter_introduction/what_is_dsa.md b/docs/chapter_introduction/what_is_dsa.md index 12ba333b..67b3c37e 100644 --- a/docs/chapter_introduction/what_is_dsa.md +++ b/docs/chapter_introduction/what_is_dsa.md @@ -31,12 +31,23 @@ comments: true - 算法是发挥数据结构优势的舞台。数据结构仅存储数据信息,结合算法才可解决特定问题。 - 算法有对应最优的数据结构。给定算法,一般可基于不同的数据结构实现,而最终执行效率往往相差很大。 -如果将数据结构与算法比作「LEGO 乐高」,数据结构就是乐高「积木」,而算法就是把积木拼成目标形态的一系列「操作步骤」。 - ![relationship_between_data_structure_and_algorithm](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png)

Fig. 数据结构与算法的关系

+如果将「LEGO 乐高」类比到「数据结构与算法」,那么可以得到下表所示的对应关系。 + +
+ +| 数据结构与算法 | LEGO 乐高 | +| -------------- | ---------------------------------------- | +| 输入数据 | 未拼装的积木 | +| 数据结构 | 积木组织形式,包括形状、大小、连接方式等 | +| 算法 | 把积木拼成目标形态的一系列操作步骤 | +| 输出数据 | 积木模型 | + +
+ !!! tip "约定俗成的简称" 在实际讨论中,我们通常会将「数据结构与算法」直接简称为「算法」。例如,我们熟称的 LeetCode 算法题目,实际上同时考察了数据结构和算法两部分知识。 diff --git a/docs/chapter_preface/about_the_book.md b/docs/chapter_preface/about_the_book.md index fdb27b87..db70a8ac 100644 --- a/docs/chapter_preface/about_the_book.md +++ b/docs/chapter_preface/about_the_book.md @@ -4,11 +4,11 @@ comments: true # 关于本书 -五年前发生的一件事,成为了我职业生涯的重要转折点。当时的我在交大读研,对互联网求职一无所知,但仍然硬着头皮申请了 Microsoft 软件工程师实习。面试官让我在白板上写出 “快速排序” 代码,我畏畏缩缩地写了一个 “冒泡排序” ,并且还写错了` (ToT) ` 。从面试官的表情上,我看到了一个大大的 "GG" 。 +五年前发生的一件事,成为了我职业生涯的重要转折点。当时的我在交大读研,对互联网求职一无所知,但仍然硬着头皮申请了 Microsoft 软件工程师实习。面试官让我在白板上写出“快速排序”代码,我畏畏缩缩地写了一个“冒泡排序”,并且还写错了` (ToT) ` 。从面试官的表情上,我看到了一个大大的 "GG" 。 -此次失利倒逼我开始刷算法题。我采用 “扫雷游戏” 式的学习方法,两眼一抹黑刷题,扫到不会的 “雷” 就通过查资料把它 “排掉” ,配合周期性总结,逐渐形成了数据结构与算法的知识图景。幸运地,我在秋招斩获了多家大厂的 Offer 。 +此次失利倒逼我开始刷算法题。我采用“扫雷游戏”式的学习方法,两眼一抹黑刷题,扫到不会的“雷”就通过查资料把它“排掉”,配合周期性总结,逐渐形成了数据结构与算法的知识图景。幸运地,我在秋招斩获了多家大厂的 Offer 。 -回想自己当初在 “扫雷式” 刷题中被炸的满头包的痛苦,思考良久,我意识到一本 “前期刷题必看” 的读物可以使算法小白少走许多弯路。写作意愿滚滚袭来,那就动笔吧: +回想自己当初在“扫雷式”刷题中被炸的满头包的痛苦,思考良久,我意识到一本“前期刷题必看”的读物可以使算法小白少走许多弯路。写作意愿滚滚袭来,那就动笔吧:

Hello,算法!

@@ -28,7 +28,7 @@ comments: true - 本书篇幅不长,可以帮助你提纲挈领地回顾算法知识。 - 书中包含许多对比性、总结性的算法内容,可以帮助你梳理算法知识体系。 -- 源代码实现了各种经典数据结构和算法,可以作为 “刷题工具库” 来使用。 +- 源代码实现了各种经典数据结构和算法,可以作为“刷题工具库”来使用。 如果您是 **算法大佬**,请受我膜拜!希望您可以抽时间提出意见建议,或者[一起参与创作](https://www.hello-algo.com/chapter_preface/contribution/),帮助各位同学获取更好的学习内容,感谢! @@ -99,15 +99,15 @@ comments: true **视觉化学习。** 信息时代以来,视觉化的脚步从未停止。媒体形式经历了文字短信、图文 Email 、动图、短(长)视频、交互式 Web 、3D 游戏等演变过程,信息的视觉化程度越来越高、愈加符合人类感官、信息传播效率大大提升。科技界也在向视觉化迈进,iPhone 就是一个典型例子,其相对于传统手机是高度视觉化的,包含精心设计的字体、主题配色、交互动画等。 - 近两年,短视频成为最受欢迎的信息媒介,可以在短时间内将高密度的信息 “灌” 给我们,有着极其舒适的观看体验。阅读则不然,读者与书本之间天然存在一种 “疏离感”,我们看书会累、会走神、会停下来想其他事、会划下喜欢的句子、会思考某一片段的含义,这种疏离感给了读者与书本之间对话的可能,拓宽了想象空间。 + 近两年,短视频成为最受欢迎的信息媒介,可以在短时间内将高密度的信息“灌”给我们,有着极其舒适的观看体验。阅读则不然,读者与书本之间天然存在一种“疏离感”,我们看书会累、会走神、会停下来想其他事、会划下喜欢的句子、会思考某一片段的含义,这种疏离感给了读者与书本之间对话的可能,拓宽了想象空间。 - 本书作为一本入门教材,希望可以保有书本的 “慢节奏” ,但也会避免与读者产生过多 “疏离感” ,而是努力将知识完整清晰地推送到你聪明的小脑袋瓜中。我将采用视觉化的方式(例如配图、动画),尽我可能清晰易懂地讲解复杂概念和抽象示例。 + 本书作为一本入门教材,希望可以保有书本的“慢节奏”,但也会避免与读者产生过多“疏离感”,而是努力将知识完整清晰地推送到你聪明的小脑袋瓜中。我将采用视觉化的方式(例如配图、动画),尽我可能清晰易懂地讲解复杂概念和抽象示例。 **内容精简化。** 大多数的经典教科书,会把每个主题都讲的很透彻。虽然透彻性正是其获得读者青睐的原因,但对于想要快速入门的初学者来说,这些教材的实用性不足。本书会避免引入非必要的概念、名词、定义等,也避免展开不必要的理论分析,毕竟这不是一本真正意义上的教材,主要任务是尽快地带领读者入门。 引入一些生活案例或趣味内容,非常适合作为知识点的引子或者解释的补充,但当融入过多额外元素时,内容会稍显冗长,也许反而使读者容易迷失、抓不住重点,这也是本书需要避免的。 - 敲代码如同写字,“美” 是统一的追求。本书力求美观的代码,保证规范的变量命名、统一的空格与换行、对齐的缩进、整齐的注释等。 + 敲代码如同写字,“美”是统一的追求。本书力求美观的代码,保证规范的变量命名、统一的空格与换行、对齐的缩进、整齐的注释等。 ## 致谢 @@ -115,7 +115,7 @@ comments: true - 感谢我的女朋友泡泡担任本书的首位读者,从算法小白的视角为本书的写作提出了许多建议,使这本书更加适合算法初学者来阅读。 - 感谢腾宝、琦宝、飞宝为本书起了个响当当的名字,好听又有梗,直接唤起我最初敲下第一行代码 "Hello, World!" 的回忆。 -- 感谢我的导师李博,在小酌畅谈时您告诉我 “觉得适合、想做就去做” ,坚定了我写这本书的决心。 +- 感谢我的导师李博,在小酌畅谈时您告诉我“觉得适合、想做就去做”,坚定了我写这本书的决心。 - 感谢苏潼为本书设计了封面和 LOGO ,我有些强迫症,前后多次修改,谢谢你的耐心。 - 感谢 @squidfunk ,包括 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) 顶级开源项目以及给出的写作排版建议。 diff --git a/docs/chapter_preface/contribution.md b/docs/chapter_preface/contribution.md index 8bc8b293..c710e6de 100644 --- a/docs/chapter_preface/contribution.md +++ b/docs/chapter_preface/contribution.md @@ -14,10 +14,10 @@ comments: true 每个页面的右上角都有一个「编辑」按钮,你可以按照以下步骤修改文章: -1. 点击编辑按钮,如果遇到提示 “需要 Fork 此仓库” ,请通过; +1. 点击编辑按钮,如果遇到提示“需要 Fork 此仓库”,请通过; 2. 修改 Markdown 源文件内容; -3. 在页面底部填写更改说明,然后单击 “Propose file change” 按钮; -4. 页面跳转后,点击 “Create pull request” 按钮发起拉取请求即可,我会第一时间查看处理并及时更新内容。 +3. 在页面底部填写更改说明,然后单击“Propose file change”按钮; +4. 页面跳转后,点击“Create pull request”按钮发起拉取请求即可,我会第一时间查看处理并及时更新内容。 ![edit_markdown](contribution.assets/edit_markdown.png) @@ -37,7 +37,7 @@ comments: true 2. 进入 Fork 仓库网页,使用 `git clone` 克隆该仓库至本地; 3. 在本地进行内容创作(建议通过运行测试来验证代码正确性); 4. 将本地更改 Commit ,并 Push 至远程仓库; -5. 刷新仓库网页,点击 “Create pull request” 按钮发起拉取请求(Pull Request)即可; +5. 刷新仓库网页,点击“Create pull request”按钮发起拉取请求(Pull Request)即可; 非常欢迎您和我一同来创作本书! diff --git a/docs/chapter_preface/installation.md b/docs/chapter_preface/installation.md index 42041255..c6c1b2ee 100644 --- a/docs/chapter_preface/installation.md +++ b/docs/chapter_preface/installation.md @@ -12,9 +12,10 @@ comments: true ## Java 环境 -1. 下载并安装 [OpenJDK](https://jdk.java.net/18/) 。 +1. 下载并安装 [OpenJDK](https://jdk.java.net/18/)(版本需满足 > JDK 9)。 2. 在 VSCode 的插件市场中搜索 `java` ,安装 Java Extension Pack 。 + ## C++ 环境 1. Windows 系统需要安装 [MinGW](https://www.mingw-w64.org/downloads/) ,MacOS 自带 Clang 无需安装。 @@ -40,3 +41,8 @@ comments: true 1. 下载并安装 [.Net 6.0](https://dotnet.microsoft.com/en-us/download) ; 2. 在 VSCode 的插件市场中搜索 `c#` ,安装 c# 。 + +## Swift 环境 + +1. 下载并安装 [Swift](https://www.swift.org/download/); +2. 在 VSCode 的插件市场中搜索 `swift`,安装 [Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang)。 diff --git a/docs/chapter_preface/suggestions.md b/docs/chapter_preface/suggestions.md index 20cdac70..c0377ee3 100644 --- a/docs/chapter_preface/suggestions.md +++ b/docs/chapter_preface/suggestions.md @@ -26,7 +26,7 @@ comments: true git clone https://github.com/krahets/hello-algo.git ``` -当然,你也可以点击 “Download ZIP” 直接下载代码压缩包,解压即可。 +当然,你也可以点击“Download ZIP”直接下载代码压缩包,解压即可。 ![download_code](suggestions.assets/download_code.png) @@ -46,17 +46,17 @@ git clone https://github.com/krahets/hello-algo.git ## 提问讨论学 -阅读本书时,请不要 “惯着” 那些弄不明白的知识点。如果有任何疑惑,**可以在评论区留下你的问题**,小伙伴们和我都会给予解答(您一般 3 天内会得到回复)。 +阅读本书时,请不要“惯着”那些弄不明白的知识点。如果有任何疑惑,**可以在评论区留下你的问题**,小伙伴们和我都会给予解答(您一般 3 天内会得到回复)。 同时,也希望你可以多花时间逛逛评论区。一方面,可以看看大家遇到了什么问题,反过来查漏补缺,这往往可以引起更加深度的思考。另一方面,也希望你可以慷慨地解答小伙伴们的问题、分享自己的见解,大家一起加油与进步! ![comment](suggestions.assets/comment.gif) -## 算法学习 “三步走” +## 算法学习“三步走” **第一阶段,算法入门,也正是本书的定位。** 熟悉各种数据结构的特点、用法,学习各种算法的工作原理、用途、效率等。 -**第二阶段,刷算法题。** 可以先从热门题单开刷,推荐 [剑指 Offer](https://leetcode.cn/problem-list/xb9nqhhg/)、[LeetCode 热题 HOT 100](https://leetcode.cn/problem-list/2cktkvj/) ,先积累至少 100 道题量,熟悉大多数的算法问题。刚开始刷题时,“遗忘” 是最大的困扰点,但这是很正常的,请不要担心。学习中有一种概念叫 “周期性回顾” ,同一道题隔段时间做一次,当做了三遍以上,往往就能牢记于心了。 +**第二阶段,刷算法题。** 可以先从热门题单开刷,推荐 [剑指 Offer](https://leetcode.cn/problem-list/xb9nqhhg/)、[LeetCode 热题 HOT 100](https://leetcode.cn/problem-list/2cktkvj/) ,先积累至少 100 道题量,熟悉大多数的算法问题。刚开始刷题时,“遗忘”是最大的困扰点,但这是很正常的,请不要担心。学习中有一种概念叫“周期性回顾”,同一道题隔段时间做一次,当做了三遍以上,往往就能牢记于心了。 **第三阶段,搭建知识体系。** 在学习方面,可以阅读算法专栏文章、解题框架、算法教材,不断地丰富知识体系。在刷题方面,可以开始采用进阶刷题方案,例如按专题分类、一题多解、一解多题等,刷题方案在社区中可以找到一些讲解,在此不做赘述。 diff --git a/docs/chapter_searching/binary_search.md b/docs/chapter_searching/binary_search.md index 0a407ff9..35d6a321 100644 --- a/docs/chapter_searching/binary_search.md +++ b/docs/chapter_searching/binary_search.md @@ -24,39 +24,32 @@ $$ 1. **双闭区间 $[0, n-1]$** ,即两个边界都包含自身;此方法下,区间 $[0, 0]$ 仍包含一个元素; 2. **左闭右开 $[0, n)$** ,即左边界包含自身、右边界不包含自身;此方法下,区间 $[0, 0)$ 为空; -### “双闭区间” 实现 +### “双闭区间”实现 -首先,我们先采用 “双闭区间” 的表示,在数组 `nums` 中查找目标元素 `target` 的对应索引。 +首先,我们先采用“双闭区间”的表示,在数组 `nums` 中查找目标元素 `target` 的对应索引。 === "Step 1" - ![binary_search_step1](binary_search.assets/binary_search_step1.png) === "Step 2" - ![binary_search_step2](binary_search.assets/binary_search_step2.png) === "Step 3" - ![binary_search_step3](binary_search.assets/binary_search_step3.png) === "Step 4" - ![binary_search_step4](binary_search.assets/binary_search_step4.png) === "Step 5" - ![binary_search_step5](binary_search.assets/binary_search_step5.png) === "Step 6" - ![binary_search_step6](binary_search.assets/binary_search_step6.png) === "Step 7" - ![binary_search_step7](binary_search.assets/binary_search_step7.png) -二分查找 “双闭区间” 表示下的代码如下所示。 +二分查找“双闭区间”表示下的代码如下所示。 === "Java" @@ -123,17 +116,17 @@ $$ === "Go" ```go title="binary_search.go" - /* 二分查找(左闭右开) */ - func binarySearch1(nums []int, target int) int { - // 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 - i, j := 0, len(nums) - // 循环,当搜索区间为空时跳出(当 i = j 时为空) - for i < j { + /* 二分查找(双闭区间) */ + func binarySearch(nums []int, target int) int { + // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + i, j := 0, len(nums)-1 + // 循环,当搜索区间为空时跳出(当 i > j 时为空) + for i <= j { m := (i + j) / 2 // 计算中点索引 m - if nums[m] < target { // 此情况说明 target 在区间 [m+1, j) 中 + if nums[m] < target { // 此情况说明 target 在区间 [m+1, j] 中 i = m + 1 - } else if nums[m] > target { // 此情况说明 target 在区间 [i, m) 中 - j = m + } else if nums[m] > target { // 此情况说明 target 在区间 [i, m-1] 中 + j = m - 1 } else { // 找到目标元素,返回其索引 return m } @@ -146,13 +139,45 @@ $$ === "JavaScript" ```js title="binary_search.js" - + /* 二分查找(双闭区间) */ + function binarySearch(nums, target) { + // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + let i = 0, j = nums.length - 1; + // 循环,当搜索区间为空时跳出(当 i > j 时为空) + while (i <= j) { + let m = parseInt((i + j) / 2); // 计算中点索引 m ,在 JS 中需使用 parseInt 函数取整 + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1; + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中 + j = m - 1; + else + return m; // 找到目标元素,返回其索引 + } + // 未找到目标元素,返回 -1 + return -1; + } ``` === "TypeScript" ```typescript title="binary_search.ts" - + /* 二分查找(双闭区间) */ + const binarySearch = function (nums: number[], target: number): number { + // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + let i = 0, j = nums.length - 1; + // 循环,当搜索区间为空时跳出(当 i > j 时为空) + while (i <= j) { + const m = Math.floor(i + (j - i) / 2); // 计算中点索引 m + if (nums[m] < target) { // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1; + } else if (nums[m] > target) { // 此情况说明 target 在区间 [i, m-1] 中 + j = m - 1; + } else { // 找到目标元素,返回其索引 + return m; + } + } + return -1; // 未找到目标元素,返回 -1 + } ``` === "C" @@ -164,12 +189,30 @@ $$ === "C#" ```csharp title="binary_search.cs" - + /* 二分查找(双闭区间) */ + int binarySearch(int[] nums, int target) + { + // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + int i = 0, j = nums.Length - 1; + // 循环,当搜索区间为空时跳出(当 i > j 时为空) + while (i <= j) + { + int m = (i + j) / 2; // 计算中点索引 m + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1; + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中 + j = m - 1; + else // 找到目标元素,返回其索引 + return m; + } + // 未找到目标元素,返回 -1 + return -1; + } ``` -### “左闭右开” 实现 +### “左闭右开”实现 -当然,我们也可以使用 “左闭右开” 的表示方法,写出相同功能的二分查找代码。 +当然,我们也可以使用“左闭右开”的表示方法,写出相同功能的二分查找代码。 === "Java" @@ -181,9 +224,9 @@ $$ // 循环,当搜索区间为空时跳出(当 i = j 时为空) while (i < j) { int m = (i + j) / 2; // 计算中点索引 m - if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j) 中 + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中 i = m + 1; - else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中 + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m] 中 j = m; else // 找到目标元素,返回其索引 return m; @@ -203,9 +246,9 @@ $$ // 循环,当搜索区间为空时跳出(当 i = j 时为空) while (i < j) { int m = (i + j) / 2; // 计算中点索引 m - if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j) 中 + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中 i = m + 1; - else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中 + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m] 中 j = m; else // 找到目标元素,返回其索引 return m; @@ -225,9 +268,9 @@ $$ # 循环,当搜索区间为空时跳出(当 i = j 时为空) while i < j: m = (i + j) // 2 # 计算中点索引 m - if nums[m] < target: # 此情况说明 target 在区间 [m+1, j) 中 + if nums[m] < target: # 此情况说明 target 在区间 [m+1, j] 中 i = m + 1 - elif nums[m] > target: # 此情况说明 target 在区间 [i, m) 中 + elif nums[m] > target: # 此情况说明 target 在区间 [i, m] 中 j = m else: # 找到目标元素,返回其索引 return m @@ -244,9 +287,9 @@ $$ // 循环,当搜索区间为空时跳出(当 i = j 时为空) for i < j { m := (i + j) / 2 // 计算中点索引 m - if nums[m] < target { // 此情况说明 target 在区间 [m+1, j) 中 + if nums[m] < target { // 此情况说明 target 在区间 [m+1, j] 中 i = m + 1 - } else if nums[m] > target { // 此情况说明 target 在区间 [i, m) 中 + } else if nums[m] > target { // 此情况说明 target 在区间 [i, m] 中 j = m } else { // 找到目标元素,返回其索引 return m @@ -260,13 +303,45 @@ $$ === "JavaScript" ```js title="binary_search.js" - + /* 二分查找(左闭右开) */ + function binarySearch1(nums, target) { + // 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 + let i = 0, j = nums.length; + // 循环,当搜索区间为空时跳出(当 i = j 时为空) + while (i < j) { + let m = parseInt((i + j) / 2); // 计算中点索引 m ,在 JS 中需使用 parseInt 函数取整 + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1; + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m] 中 + j = m; + else // 找到目标元素,返回其索引 + return m; + } + // 未找到目标元素,返回 -1 + return -1; + } ``` === "TypeScript" ```typescript title="binary_search.ts" - + /* 二分查找(左闭右开) */ + const binarySearch1 = function (nums: number[], target: number): number { + // 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 + let i = 0, j = nums.length; + // 循环,当搜索区间为空时跳出(当 i = j 时为空) + while (i < j) { + const m = Math.floor(i + (j - i) / 2); // 计算中点索引 m + if (nums[m] < target) { // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1; + } else if (nums[m] > target) { // 此情况说明 target 在区间 [i, m] 中 + j = m; + } else { // 找到目标元素,返回其索引 + return m; + } + } + return -1; // 未找到目标元素,返回 -1 + } ``` === "C" @@ -278,7 +353,25 @@ $$ === "C#" ```csharp title="binary_search.cs" - + /* 二分查找(左闭右开) */ + int binarySearch1(int[] nums, int target) + { + // 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 + int i = 0, j = nums.Length; + // 循环,当搜索区间为空时跳出(当 i = j 时为空) + while (i < j) + { + int m = (i + j) / 2; // 计算中点索引 m + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1; + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m] 中 + j = m; + else // 找到目标元素,返回其索引 + return m; + } + // 未找到目标元素,返回 -1 + return -1; + } ``` ### 两种表示对比 @@ -294,7 +387,7 @@ $$ -观察发现,在 “双闭区间” 表示中,由于对左右两边界的定义是相同的,因此缩小区间的 $i$ , $j$ 处理方法也是对称的,这样更不容易出错。综上所述,**建议你采用 “双闭区间” 的写法。** +观察发现,在“双闭区间”表示中,由于对左右两边界的定义是相同的,因此缩小区间的 $i$ , $j$ 处理方法也是对称的,这样更不容易出错。综上所述,**建议你采用“双闭区间”的写法。** ### 大数越界处理 @@ -337,13 +430,19 @@ $$ === "JavaScript" ```js title="" - + // (i + j) 有可能超出 int 的取值范围 + let m = parseInt((i + j) / 2); + // 更换为此写法则不会越界 + let m = parseInt(i + (j - i) / 2); ``` === "TypeScript" ```typescript title="" - + // (i + j) 有可能超出 Number 的取值范围 + let m = Math.floor((i + j) / 2); + // 更换为此写法则不会越界 + let m = Math.floor(i + (j - i) / 2); ``` === "C" @@ -355,7 +454,10 @@ $$ === "C#" ```csharp title="" - + // (i + j) 有可能超出 int 的取值范围 + int m = (i + j) / 2; + // 更换为此写法则不会越界 + int m = i + (j - i) / 2; ``` ## 复杂度分析 diff --git a/docs/chapter_searching/hashing_search.md b/docs/chapter_searching/hashing_search.md index e1fcb38e..a52f72e2 100644 --- a/docs/chapter_searching/hashing_search.md +++ b/docs/chapter_searching/hashing_search.md @@ -8,7 +8,7 @@ comments: true 在数据量很大时,「线性查找」太慢;而「二分查找」要求数据必须是有序的,并且只能在数组中应用。那么是否有方法可以同时避免上述缺点呢?答案是肯定的,此方法被称为「哈希查找」。 -「哈希查找 Hash Searching」借助一个哈希表来存储需要的「键值对 Key Value Pair」,我们可以在 $O(1)$ 时间下实现 “键 $\rightarrow$ 值” 映射查找,体现着 “以空间换时间” 的算法思想。 +「哈希查找 Hash Searching」借助一个哈希表来存储需要的「键值对 Key Value Pair」,我们可以在 $O(1)$ 时间下实现“键 $\rightarrow$ 值”映射查找,体现着“以空间换时间”的算法思想。 ## 算法实现 @@ -86,7 +86,13 @@ comments: true === "C#" ```csharp title="hashing_search.cs" - + /* 哈希查找(数组) */ + int hashingSearch(Dictionary map, int target) + { + // 哈希表的 key: 目标元素,value: 索引 + // 若哈希表中无此 key ,返回 -1 + return map.GetValueOrDefault(target, -1); + } ``` 再比如,如果我们想要给定一个目标结点值 `target` ,获取对应的链表结点对象,那么也可以使用哈希查找实现。 @@ -163,7 +169,14 @@ comments: true === "C#" ```csharp title="hashing_search.cs" + /* 哈希查找(链表) */ + ListNode? hashingSearch1(Dictionary map, int target) + { + // 哈希表的 key: 目标结点值,value: 结点对象 + // 若哈希表中无此 key ,返回 null + return map.GetValueOrDefault(target); + } ``` ## 复杂度分析 diff --git a/docs/chapter_searching/linear_search.md b/docs/chapter_searching/linear_search.md index 8822d375..3284db06 100644 --- a/docs/chapter_searching/linear_search.md +++ b/docs/chapter_searching/linear_search.md @@ -76,6 +76,18 @@ comments: true === "JavaScript" ```js title="linear_search.js" + /* 线性查找(数组) */ + function linearSearchArray(nums, target) { + // 遍历数组 + for (let i = 0; i < nums.length; i++) { + // 找到目标元素,返回其索引 + if (nums[i] === target) { + return i; + } + } + // 未找到目标元素,返回 -1 + return -1; + } ``` @@ -94,6 +106,19 @@ comments: true === "C#" ```csharp title="linear_search.cs" + /* 线性查找(数组) */ + int linearSearch(int[] nums, int target) + { + // 遍历数组 + for (int i = 0; i < nums.Length; i++) + { + // 找到目标元素,返回其索引 + if (nums[i] == target) + return i; + } + // 未找到目标元素,返回 -1 + return -1; + } ``` @@ -167,7 +192,19 @@ comments: true === "JavaScript" ```js title="linear_search.js" - + /* 线性查找(链表)*/ + function linearSearchLinkedList(head, target) { + // 遍历链表 + while(head) { + // 找到目标结点,返回之 + if(head.val === target) { + return head; + } + head = head.next; + } + // 未找到目标结点,返回 null + return null; + } ``` === "TypeScript" @@ -185,7 +222,20 @@ comments: true === "C#" ```csharp title="linear_search.cs" - + /* 线性查找(链表) */ + ListNode? linearSearch(ListNode head, int target) + { + // 遍历链表 + while (head != null) + { + // 找到目标结点,返回之 + if (head.val == target) + return head; + head = head.next; + } + // 未找到目标结点,返回 null + return null; + } ``` ## 复杂度分析 diff --git a/docs/chapter_sorting/bubble_sort.md b/docs/chapter_sorting/bubble_sort.md index 2dddba6b..336f3c2d 100644 --- a/docs/chapter_sorting/bubble_sort.md +++ b/docs/chapter_sorting/bubble_sort.md @@ -6,7 +6,7 @@ comments: true 「冒泡排序 Bubble Sort」是一种最基础的排序算法,非常适合作为第一个学习的排序算法。顾名思义,「冒泡」是该算法的核心操作。 -!!! question "为什么叫 “冒泡”" +!!! question "为什么叫“冒泡”" 在水中,越大的泡泡浮力越大,所以最大的泡泡会最先浮到水面。 @@ -85,9 +85,8 @@ comments: true for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // 交换 nums[j] 与 nums[j + 1] - int tmp = nums[j]; - nums[j] = nums[j + 1]; - nums[j + 1] = tmp; + // 这里使用了 std::swap() 函数 + swap(nums[j], nums[j + 1]); } } } @@ -170,13 +169,48 @@ comments: true === "C" ```c title="bubble_sort.c" - + /* 冒泡排序 */ + void bubble_sort(int nums[], int size) + { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = 0; i < size - 1; i++) + { + // 内循环:冒泡操作 + for (int j = 0; j < size - 1 - i; j++) + { + if (nums[j] > nums[j + 1]) + { + int temp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = temp; + } + } + } + } ``` === "C#" ```csharp title="bubble_sort.cs" - + /* 冒泡排序 */ + void bubbleSort(int[] nums) + { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = nums.Length - 1; i > 0; i--) + { + // 内循环:冒泡操作 + for (int j = 0; j < i; j++) + { + if (nums[j] > nums[j + 1]) + { + // 交换 nums[j] 与 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } ``` ## 算法特性 @@ -232,9 +266,8 @@ comments: true for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // 交换 nums[j] 与 nums[j + 1] - int tmp = nums[j]; - nums[j] = nums[j + 1]; - nums[j + 1] = tmp; + // 这里使用了 std::swap() 函数 + swap(nums[j], nums[j + 1]); flag = true; // 记录交换元素 } } @@ -334,11 +367,52 @@ comments: true === "C" ```c title="bubble_sort.c" - + /* 冒泡排序 */ + void bubble_sort(int nums[], int size) + { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = 0; i < size - 1; i++) + { + bool flag = false; + // 内循环:冒泡操作 + for (int j = 0; j < size - 1 - i; j++) + { + if (nums[j] > nums[j + 1]) + { + int temp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = temp; + flag = true; + } + } + if(!flag) break; + } + } ``` === "C#" ```csharp title="bubble_sort.cs" - + /* 冒泡排序(标志优化)*/ + void bubbleSortWithFlag(int[] nums) + { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = nums.Length - 1; i > 0; i--) + { + bool flag = false; // 初始化标志位 + // 内循环:冒泡操作 + for (int j = 0; j < i; j++) + { + if (nums[j] > nums[j + 1]) + { + // 交换 nums[j] 与 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // 记录交换元素 + } + } + if (!flag) break; // 此轮冒泡未交换任何元素,直接跳出 + } + } ``` diff --git a/docs/chapter_sorting/insertion_sort.md b/docs/chapter_sorting/insertion_sort.md index 1ec47f89..1614b5c3 100644 --- a/docs/chapter_sorting/insertion_sort.md +++ b/docs/chapter_sorting/insertion_sort.md @@ -6,7 +6,7 @@ comments: true 「插入排序 Insertion Sort」是一种基于 **数组插入操作** 的排序算法。 -「插入操作」的思想:选定数组的某个元素为基准数 `base` ,将 `base` 与其左边的元素依次对比大小,并 “插入” 到正确位置。 +「插入操作」的思想:选定数组的某个元素为基准数 `base` ,将 `base` 与其左边的元素依次对比大小,并“插入”到正确位置。 然而,由于数组在内存中的存储方式是连续的,我们无法直接把 `base` 插入到目标位置,而是需要将从目标位置到 `base` 之间的所有元素向右移动一位(本质上是一次数组插入操作)。 @@ -141,7 +141,22 @@ comments: true === "C#" ```csharp title="insertion_sort.cs" - + /* 插入排序 */ + void insertionSort(int[] nums) + { + // 外循环:base = nums[1], nums[2], ..., nums[n-1] + for (int i = 1; i < nums.Length; i++) + { + int bas = nums[i], j = i - 1; + // 内循环:将 base 插入到左边的正确位置 + while (j >= 0 && nums[j] > bas) + { + nums[j + 1] = nums[j]; // 1. 将 nums[j] 向右移动一位 + j--; + } + nums[j + 1] = bas; // 2. 将 base 赋值到正确位置 + } + } ``` ## 算法特性 diff --git a/docs/chapter_sorting/merge_sort.md b/docs/chapter_sorting/merge_sort.md index b7486bae..9ee98427 100644 --- a/docs/chapter_sorting/merge_sort.md +++ b/docs/chapter_sorting/merge_sort.md @@ -4,7 +4,7 @@ comments: true # 归并排序 -「归并排序 Merge Sort」是算法中 “分治思想” 的典型体现,其有「划分」和「合并」两个阶段: +「归并排序 Merge Sort」是算法中“分治思想”的典型体现,其有「划分」和「合并」两个阶段: 1. **划分阶段:** 通过递归不断 **将数组从中点位置划分开**,将长数组的排序问题转化为短数组的排序问题; 2. **合并阶段:** 划分到子数组长度为 1 时,开始向上合并,不断将 **左、右两个短排序数组** 合并为 **一个长排序数组**,直至合并至原数组时完成排序; @@ -78,13 +78,13 @@ comments: true int i = leftStart, j = rightStart; // 通过覆盖原数组 nums 来合并左子数组和右子数组 for (int k = left; k <= right; k++) { - // 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ if (i > leftEnd) nums[k] = tmp[j++]; - // 否则,若 “右子数组已全部合并完” 或 “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ + // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ else if (j > rightEnd || tmp[i] <= tmp[j]) nums[k] = tmp[i++]; - // 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ else nums[k] = tmp[j++]; } @@ -122,13 +122,13 @@ comments: true int i = leftStart, j = rightStart; // 通过覆盖原数组 nums 来合并左子数组和右子数组 for (int k = left; k <= right; k++) { - // 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ if (i > leftEnd) nums[k] = tmp[j++]; - // 否则,若 “右子数组已全部合并完” 或 “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ + // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ else if (j > rightEnd || tmp[i] <= tmp[j]) nums[k] = tmp[i++]; - // 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ else nums[k] = tmp[j++]; } @@ -166,15 +166,15 @@ comments: true i, j = left_start, right_start # 通过覆盖原数组 nums 来合并左子数组和右子数组 for k in range(left, right + 1): - # 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ + # 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ if i > left_end: nums[k] = tmp[j] j += 1 - # 否则,若 “右子数组已全部合并完” 或 “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ + # 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ elif j > right_end or tmp[i] <= tmp[j]: nums[k] = tmp[i] i += 1 - # 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + # 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ else: nums[k] = tmp[j] j += 1 @@ -214,15 +214,15 @@ comments: true i, j := left_start, right_start // 通过覆盖原数组 nums 来合并左子数组和右子数组 for k := left; k <= right; k++ { - // 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ if i > left_end { nums[k] = tmp[j] j++ - // 否则,若 “右子数组已全部合并完” 或 “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ + // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ } else if j > right_end || tmp[i] <= tmp[j] { nums[k] = tmp[i] i++ - // 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ } else { nums[k] = tmp[j] j++ @@ -264,13 +264,13 @@ comments: true let i = leftStart, j = rightStart; // 通过覆盖原数组 nums 来合并左子数组和右子数组 for (let k = left; k <= right; k++) { - // 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ if (i > leftEnd) { nums[k] = tmp[j++]; - // 否则,若 “右子数组已全部合并完” 或 “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ + // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ } else if (j > rightEnd || tmp[i] <= tmp[j]) { nums[k] = tmp[i++]; - // 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ } else { nums[k] = tmp[j++]; } @@ -309,13 +309,13 @@ comments: true let i = leftStart, j = rightStart; // 通过覆盖原数组 nums 来合并左子数组和右子数组 for (let k = left; k <= right; k++) { - // 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ if (i > leftEnd) { nums[k] = tmp[j++]; - // 否则,若 “右子数组已全部合并完” 或 “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ + // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ } else if (j > rightEnd || tmp[i] <= tmp[j]) { nums[k] = tmp[i++]; - // 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ } else { nums[k] = tmp[j++]; } @@ -344,7 +344,48 @@ comments: true === "C#" ```csharp title="merge_sort.cs" + /** + * 合并左子数组和右子数组 + * 左子数组区间 [left, mid] + * 右子数组区间 [mid + 1, right] + */ + void merge(int[] nums, int left, int mid, int right) + { + // 初始化辅助数组 + int[] tmp = nums[left..(right + 1)]; + // 左子数组的起始索引和结束索引 + int leftStart = left - left, leftEnd = mid - left; + // 右子数组的起始索引和结束索引 + int rightStart = mid + 1 - left, rightEnd = right - left; + // i, j 分别指向左子数组、右子数组的首元素 + int i = leftStart, j = rightStart; + // 通过覆盖原数组 nums 来合并左子数组和右子数组 + for (int k = left; k <= right; k++) + { + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ + if (i > leftEnd) + nums[k] = tmp[j++]; + // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ + else if (j > rightEnd || tmp[i] <= tmp[j]) + nums[k] = tmp[i++]; + // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + else + nums[k] = tmp[j++]; + } + } + /* 归并排序 */ + void mergeSort(int[] nums, int left, int right) + { + // 终止条件 + if (left >= right) return; // 当子数组长度为 1 时终止递归 + // 划分阶段 + int mid = (left + right) / 2; // 计算中点 + mergeSort(nums, left, mid); // 递归左子数组 + mergeSort(nums, mid + 1, right); // 递归右子数组 + // 合并阶段 + merge(nums, left, mid, right); + } ``` 下面重点解释一下合并方法 `merge()` 的流程: @@ -370,7 +411,7 @@ comments: true 归并排序有一个很特别的优势,用于排序链表时有很好的性能表现,**空间复杂度可被优化至 $O(1)$** ,这是因为: -- 由于链表可仅通过改变指针来实现结点增删,因此 “将两个短有序链表合并为一个长有序链表” 无需使用额外空间,即回溯合并阶段不用像排序数组一样建立辅助数组 `tmp` ; +- 由于链表可仅通过改变指针来实现结点增删,因此“将两个短有序链表合并为一个长有序链表”无需使用额外空间,即回溯合并阶段不用像排序数组一样建立辅助数组 `tmp` ; - 通过使用「迭代」代替「递归划分」,可省去递归使用的栈帧空间; > 详情参考:[148. 排序链表](https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/) diff --git a/docs/chapter_sorting/quick_sort.md b/docs/chapter_sorting/quick_sort.md index 4668e350..b7b607f2 100644 --- a/docs/chapter_sorting/quick_sort.md +++ b/docs/chapter_sorting/quick_sort.md @@ -4,7 +4,7 @@ comments: true # 快速排序 -「快速排序 Quick Sort」是一种基于 “分治思想” 的排序算法,速度很快、应用很广。 +「快速排序 Quick Sort」是一种基于“分治思想”的排序算法,速度很快、应用很广。 快速排序的核心操作为「哨兵划分」,其目标为:选取数组某个元素为 **基准数** ,将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。「哨兵划分」的实现流程为: @@ -196,6 +196,30 @@ comments: true === "C#" ```csharp title="quick_sort.cs" + /* 元素交换 */ + void swap(int[] nums, int i, int j) + { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + /* 哨兵划分 */ + int partition(int[] nums, int left, int right) + { + // 以 nums[left] 作为基准数 + int i = left, j = right; + while (i < j) + { + while (i < j && nums[j] >= nums[left]) + j--; // 从右向左找首个小于基准数的元素 + while (i < j && nums[i] <= nums[left]) + i++; // 从左向右找首个大于基准数的元素 + swap(nums, i, j); // 交换这两个元素 + } + swap(nums, i, left); // 将基准数交换至两子数组的分界线 + return i; // 返回基准数的索引 + } ``` @@ -235,7 +259,7 @@ comments: true ```cpp title="quick_sort.cpp" /* 快速排序 */ - static void quickSort(vector& nums, int left, int right) { + void quickSort(vector& nums, int left, int right) { // 子数组长度为 1 时终止递归 if (left >= right) return; @@ -320,6 +344,18 @@ comments: true === "C#" ```csharp title="quick_sort.cs" + /* 快速排序 */ + void quickSort(int[] nums, int left, int right) + { + // 子数组长度为 1 时终止递归 + if (left >= right) + return; + // 哨兵划分 + int pivot = partition(nums, left, right); + // 递归左子数组、右子数组 + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } ``` @@ -339,7 +375,7 @@ comments: true ## 快排为什么快? -从命名能够看出,快速排序在效率方面一定 “有两把刷子” 。快速排序的平均时间复杂度虽然与「归并排序」和「堆排序」一致,但实际 **效率更高** ,这是因为: +从命名能够看出,快速排序在效率方面一定“有两把刷子”。快速排序的平均时间复杂度虽然与「归并排序」和「堆排序」一致,但实际 **效率更高** ,这是因为: - **出现最差情况的概率很低:** 虽然快速排序的最差时间复杂度为 $O(n^2)$ ,不如归并排序,但绝大部分情况下,快速排序可以达到 $O(n \log n)$ 的复杂度。 - **缓存使用效率高:** 哨兵划分操作时,将整个子数组加载入缓存中,访问元素效率很高。而诸如「堆排序」需要跳跃式访问元素,因此不具有此特性。 @@ -351,7 +387,7 @@ comments: true 为了尽量避免这种情况发生,我们可以优化一下基准数的选取策略。首先,在哨兵划分中,我们可以 **随机选取一个元素作为基准数** 。但如果运气很差,每次都选择到比较差的基准数,那么效率依然不好。 -进一步地,我们可以在数组中选取 3 个候选元素(一般为数组的首、尾、中点元素),**并将三个候选元素的中位数作为基准数**,这样基准数 “既不大也不小” 的概率就大大提升了。当然,如果数组很长的话,我们也可以选取更多候选元素,来进一步提升算法的稳健性。采取该方法后,时间复杂度劣化至 $O(n^2)$ 的概率极低。 +进一步地,我们可以在数组中选取 3 个候选元素(一般为数组的首、尾、中点元素),**并将三个候选元素的中位数作为基准数**,这样基准数“既不大也不小”的概率就大大提升了。当然,如果数组很长的话,我们也可以选取更多候选元素,来进一步提升算法的稳健性。采取该方法后,时间复杂度劣化至 $O(n^2)$ 的概率极低。 === "Java" @@ -513,7 +549,29 @@ comments: true === "C#" ```csharp title="quick_sort.cs" + /* 选取三个元素的中位数 */ + int medianThree(int[] nums, int left, int mid, int right) + { + // 使用了异或操作来简化代码 + // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 + if ((nums[left] > nums[mid]) ^ (nums[left] > nums[right])) + return left; + else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right])) + return mid; + else + return right; + } + /* 哨兵划分(三数取中值) */ + int partition(int[] nums, int left, int right) + { + // 选取三个候选元素的中位数 + int med = medianThree(nums, left, (left + right) / 2, right); + // 将中位数交换至数组最左端 + swap(nums, left, med); + // 以 nums[left] 作为基准数 + // 下同省略... + } ``` ## 尾递归优化 @@ -547,7 +605,7 @@ comments: true ```cpp title="quick_sort.cpp" /* 快速排序(尾递归优化) */ - static void quickSort(vector& nums, int left, int right) { + void quickSort(vector& nums, int left, int right) { // 子数组长度为 1 时终止 while (left < right) { // 哨兵划分操作 @@ -654,5 +712,25 @@ comments: true === "C#" ```csharp title="quick_sort.cs" - + /* 快速排序(尾递归优化) */ + void quickSort(int[] nums, int left, int right) + { + // 子数组长度为 1 时终止 + while (left < right) + { + // 哨兵划分操作 + int pivot = partition(nums, left, right); + // 对两个子数组中较短的那个执行快排 + if (pivot - left < right - pivot) + { + quickSort(nums, left, pivot - 1); // 递归排序左子数组 + left = pivot + 1; // 剩余待排序区间为 [pivot + 1, right] + } + else + { + quickSort(nums, pivot + 1, right); // 递归排序右子数组 + right = pivot - 1; // 剩余待排序区间为 [left, pivot - 1] + } + } + } ``` diff --git a/docs/chapter_stack_and_queue/queue.md b/docs/chapter_stack_and_queue/queue.md index e499a6af..3c5c0528 100644 --- a/docs/chapter_stack_and_queue/queue.md +++ b/docs/chapter_stack_and_queue/queue.md @@ -205,7 +205,27 @@ comments: true === "C#" ```csharp title="queue.cs" + /* 初始化队列 */ + Queue queue = new(); + /* 元素入队 */ + queue.Enqueue(1); + queue.Enqueue(3); + queue.Enqueue(2); + queue.Enqueue(5); + queue.Enqueue(4); + + /* 访问队首元素 */ + int peek = queue.Peek(); + + /* 元素出队 */ + int poll = queue.Dequeue(); + + /* 获取队列的长度 */ + int size = queue.Count(); + + /* 判断队列是否为空 */ + bool isEmpty = queue.Count() == 0; ``` ## 队列实现 @@ -532,14 +552,69 @@ comments: true === "C#" ```csharp title="linkedlist_queue.cs" - + /* 基于链表实现的队列 */ + class LinkedListQueue + { + private ListNode? front, rear; // 头结点 front ,尾结点 rear + private int queSize = 0; + public LinkedListQueue() + { + front = null; + rear = null; + } + /* 获取队列的长度 */ + public int size() + { + return queSize; + } + /* 判断队列是否为空 */ + public bool isEmpty() + { + return size() == 0; + } + /* 入队 */ + public void offer(int num) + { + // 尾结点后添加 num + ListNode node = new ListNode(num); + // 如果队列为空,则令头、尾结点都指向该结点 + if (front == null) + { + front = node; + rear = node; + // 如果队列不为空,则将该结点添加到尾结点后 + } + else if (rear != null) + { + rear.next = node; + rear = node; + } + queSize++; + } + /* 出队 */ + public int poll() + { + int num = peek(); + // 删除头结点 + front = front?.next; + queSize--; + return num; + } + /* 访问队首元素 */ + public int peek() + { + if (size() == 0 || front == null) + throw new Exception(); + return front.val; + } + } ``` ### 基于数组的实现 数组的删除首元素的时间复杂度为 $O(n)$ ,因此不适合直接用来实现队列。然而,我们可以借助两个指针 `front` , `rear` 来分别记录队首和队尾的索引位置,在入队 / 出队时分别将 `front` / `rear` 向后移动一位即可,这样每次仅需操作一个元素,时间复杂度降至 $O(1)$ 。 -还有一个问题,在入队与出队的过程中,两个指针都在向后移动,而到达尾部后则无法继续移动了。为了解决此问题,我们可以采取一个取巧方案,即将数组看作是 “环形” 的。具体做法是规定指针越过数组尾部后,再次回到头部接续遍历,这样相当于使数组 “首尾相连” 了。 +还有一个问题,在入队与出队的过程中,两个指针都在向后移动,而到达尾部后则无法继续移动了。为了解决此问题,我们可以采取一个取巧方案,即将数组看作是“环形”的。具体做法是规定指针越过数组尾部后,再次回到头部接续遍历,这样相当于使数组“首尾相连”了。 为了适应环形数组的设定,获取长度 `size()` 、入队 `offer()` 、出队 `poll()` 方法都需要做相应的取余操作处理,使得当尾指针绕回数组头部时,仍然可以正确处理操作。 @@ -596,12 +671,6 @@ comments: true throw new EmptyStackException(); return nums[front]; } - /* 访问指定索引元素 */ - int get(int index) { - if (index >= size()) - throw new IndexOutOfBoundsException(); - return nums[(front + index) % capacity()]; - } } ``` @@ -659,12 +728,6 @@ comments: true throw out_of_range("队列为空"); return nums[front]; } - /* 访问指定位置元素 */ - int get(int index) { - if (index >= size()) - throw out_of_range("索引越界"); - return nums[(front + index) % capacity()] - } }; ``` @@ -715,13 +778,6 @@ comments: true return False return self.__nums[self.__front] - """ 访问指定位置元素 """ - def get(self, index): - if index >= self.size(): - print("索引越界") - return False - return self.__nums[(self.__front + index) % self.capacity()] - """ 返回列表用于打印 """ def to_list(self): res = [0] * self.size() @@ -837,12 +893,6 @@ comments: true throw new Error("队列为空"); return this.#queue[this.#front]; } - /* 访问指定索引元素 */ - get(index) { - if (index >= this.size) - throw new Error("索引越界"); - return this.#queue[(this.#front + index) % this.capacity]; - } } ``` @@ -893,12 +943,6 @@ comments: true throw new Error("队列为空"); return this.queue[this.front]; } - /* 访问指定索引元素 */ - get(index: number): number { - if (index >= this.size) - throw new Error("索引越界"); - return this.queue[(this.front + index) % this.capacity]; - } } ``` @@ -911,7 +955,63 @@ comments: true === "C#" ```csharp title="array_queue.cs" - + /* 基于环形数组实现的队列 */ + class ArrayQueue + { + private int[] nums; // 用于存储队列元素的数组 + private int front = 0; // 头指针,指向队首 + private int rear = 0; // 尾指针,指向队尾 + 1 + public ArrayQueue(int capacity) + { + // 初始化数组 + nums = new int[capacity]; + } + /* 获取队列的容量 */ + public int capacity() + { + return nums.Length; + } + /* 获取队列的长度 */ + public int size() + { + int capacity = this.capacity(); + // 由于将数组看作为环形,可能 rear < front ,因此需要取余数 + return (capacity + rear - front) % capacity; + } + /* 判断队列是否为空 */ + public bool isEmpty() + { + return rear - front == 0; + } + /* 入队 */ + public void offer(int num) + { + if (size() == capacity()) + { + Console.WriteLine("队列已满"); + return; + } + // 尾结点后添加 num + nums[rear] = num; + // 尾指针向后移动一位,越过尾部后返回到数组头部 + rear = (rear + 1) % capacity(); + } + /* 出队 */ + public int poll() + { + int num = peek(); + // 队头指针向后移动一位,若越过尾部则返回到数组头部 + front = (front + 1) % capacity(); + return num; + } + /* 访问队首元素 */ + public int peek() + { + if (isEmpty()) + throw new Exception(); + return nums[front]; + } + } ``` ## 队列典型应用 diff --git a/docs/chapter_stack_and_queue/stack.md b/docs/chapter_stack_and_queue/stack.md index 63d7353e..b0d577bc 100644 --- a/docs/chapter_stack_and_queue/stack.md +++ b/docs/chapter_stack_and_queue/stack.md @@ -203,14 +203,34 @@ comments: true === "C#" ```csharp title="stack.cs" + /* 初始化栈 */ + Stack stack = new (); + /* 元素入栈 */ + stack.Push(1); + stack.Push(3); + stack.Push(2); + stack.Push(5); + stack.Push(4); + + /* 访问栈顶元素 */ + int peek = stack.Peek(); + + /* 元素出栈 */ + int pop = stack.Pop(); + + /* 获取栈的长度 */ + int size = stack.Count(); + + /* 判断是否为空 */ + bool isEmpty = stack.Count()==0; ``` ## 栈的实现 为了更加清晰地了解栈的运行机制,接下来我们来自己动手实现一个栈类。 -栈规定元素是先入后出的,因此我们只能在栈顶添加或删除元素。然而,数组或链表都可以在任意位置添加删除元素,因此 **栈可被看作是一种受约束的数组或链表**。换言之,我们可以 “屏蔽” 数组或链表的部分无关操作,使之对外的表现逻辑符合栈的规定即可。 +栈规定元素是先入后出的,因此我们只能在栈顶添加或删除元素。然而,数组或链表都可以在任意位置添加删除元素,因此 **栈可被看作是一种受约束的数组或链表**。换言之,我们可以“屏蔽”数组或链表的部分无关操作,使之对外的表现逻辑符合栈的规定即可。 ### 基于链表的实现 @@ -390,13 +410,125 @@ comments: true === "JavaScript" ```js title="linkedlist_stack.js" - + /* 基于链表实现的栈 */ + class LinkedListStack { + #stackPeek; // 将头结点作为栈顶 + #stkSize = 0; // 栈的长度 + + constructor() { + this.#stackPeek = null; + } + + /* 获取栈的长度 */ + get size() { + return this.#stkSize; + } + + /* 判断栈是否为空 */ + isEmpty() { + return this.size == 0; + } + + /* 入栈 */ + push(num) { + const node = new ListNode(num); + node.next = this.#stackPeek; + this.#stackPeek = node; + this.#stkSize++; + } + + /* 出栈 */ + pop() { + const num = this.peek(); + if (!this.#stackPeek) { + throw new Error("栈为空!"); + } + this.#stackPeek = this.#stackPeek.next; + this.#stkSize--; + return num; + } + + /* 访问栈顶元素 */ + peek() { + if (!this.#stackPeek) { + throw new Error("栈为空!"); + } + return this.#stackPeek.val; + } + + /* 将链表转化为 Array 并返回 */ + toArray() { + let node = this.#stackPeek; + const res = new Array(this.size); + for (let i = res.length - 1; i >= 0; i--) { + res[i] = node.val; + node = node.next; + } + return res; + } + } ``` === "TypeScript" ```typescript title="linkedlist_stack.ts" - + /* 基于链表实现的栈 */ + class LinkedListStack { + private stackPeek: ListNode | null; // 将头结点作为栈顶 + private stkSize: number = 0; // 栈的长度 + + constructor() { + this.stackPeek = null; + } + + /* 获取栈的长度 */ + get size(): number { + return this.stkSize; + } + + /* 判断栈是否为空 */ + isEmpty(): boolean { + return this.size == 0; + } + + /* 入栈 */ + push(num: number): void { + const node = new ListNode(num); + node.next = this.stackPeek; + this.stackPeek = node; + this.stkSize++; + } + + /* 出栈 */ + pop(): number { + const num = this.peek(); + if (!this.stackPeek) { + throw new Error("栈为空!"); + } + this.stackPeek = this.stackPeek.next; + this.stkSize--; + return num; + } + + /* 访问栈顶元素 */ + peek(): number { + if (!this.stackPeek) { + throw new Error("栈为空!"); + } + return this.stackPeek.val; + } + + /* 将链表转化为 Array 并返回 */ + toArray(): number[] { + let node = this.stackPeek; + const res = new Array(this.size); + for (let i = res.length - 1; i >= 0; i--) { + res[i] = node!.val; + node = node!.next; + } + return res; + } + } ``` === "C" @@ -408,7 +540,49 @@ comments: true === "C#" ```csharp title="linkedlist_stack.cs" - + /* 基于链表实现的栈 */ + class LinkedListStack + { + private ListNode stackPeek; // 将头结点作为栈顶 + private int stkSize = 0; // 栈的长度 + public LinkedListStack() + { + stackPeek = null; + } + /* 获取栈的长度 */ + public int size() + { + return stkSize; + } + /* 判断栈是否为空 */ + public bool isEmpty() + { + return size() == 0; + } + /* 入栈 */ + public void push(int num) + { + ListNode node = new ListNode(num); + node.next = stackPeek; + stackPeek = node; + stkSize++; + } + /* 出栈 */ + public int pop() + { + int num = peek(); + stackPeek = stackPeek?.next; + stkSize--; + return num; + } + /* 访问栈顶元素 */ + public int peek() + { + if (size() == 0) + throw new Exception(); + return stackPeek.val; + } + } ``` ### 基于数组的实现 @@ -451,12 +625,6 @@ comments: true throw new EmptyStackException(); return stack.get(size() - 1); } - /* 访问索引 index 处元素 */ - public int get(int index) { - if (index >= size()) - throw new EmptyStackException(); - return stack.get(index); - } } ``` @@ -493,12 +661,6 @@ comments: true throw out_of_range("栈为空"); return stack.back(); } - /* 访问索引 index 处元素 */ - int get(int index) { - if(index >= size()) - throw out_of_range("索引越界"); - return stack[index]; - } }; ``` @@ -531,11 +693,6 @@ comments: true def peek(self): assert not self.is_empty(), "栈为空" return self.__stack[-1] - - """ 访问索引 index 处元素 """ - def get(self, index): - assert index < self.size(), "索引越界" - return self.__stack[index] ``` === "Go" @@ -617,12 +774,6 @@ comments: true throw new Error("栈为空"); return this.stack[this.stack.length - 1]; } - /* 访问索引 index 处元素 */ - get(index) { - if (index >= this.size) - throw new Error("索引越界"); - return this.stack[index]; - } }; ``` @@ -659,12 +810,6 @@ comments: true throw new Error('栈为空'); return this.stack[this.stack.length - 1]; } - /* 访问索引 index 处元素 */ - get(index: number): number | undefined { - if (index >= this.size) - throw new Error('索引越界'); - return this.stack[index]; - } }; ``` @@ -677,7 +822,47 @@ comments: true === "C#" ```csharp title="array_stack.cs" - + /* 基于数组实现的栈 */ + class ArrayStack + { + private List stack; + public ArrayStack() + { + // 初始化列表(动态数组) + stack = new(); + } + /* 获取栈的长度 */ + public int size() + { + return stack.Count(); + } + /* 判断栈是否为空 */ + public bool isEmpty() + { + return size() == 0; + } + /* 入栈 */ + public void push(int num) + { + stack.Add(num); + } + /* 出栈 */ + public int pop() + { + if (isEmpty()) + throw new Exception(); + var val = peek(); + stack.RemoveAt(size() - 1); + return val; + } + /* 访问栈顶元素 */ + public int peek() + { + if (isEmpty()) + throw new Exception(); + return stack[size() - 1]; + } + } ``` !!! tip diff --git a/docs/chapter_tree/avl_tree.md b/docs/chapter_tree/avl_tree.md index bfa2391d..e9a656be 100644 --- a/docs/chapter_tree/avl_tree.md +++ b/docs/chapter_tree/avl_tree.md @@ -48,7 +48,13 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit === "Python" ```python title="avl_tree.py" - + """ AVL 树结点类 """ + class TreeNode: + def __init__(self, val=None, left=None, right=None): + self.val = val # 结点值 + self.height = 0 # 结点高度 + self.left = left # 左子结点引用 + self.right = right # 右子结点引用 ``` === "Go" @@ -78,7 +84,14 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit === "C#" ```csharp title="avl_tree.cs" - + /* AVL 树结点类 */ + class TreeNode { + public int val; // 结点值 + public int height; // 结点高度 + public TreeNode? left; // 左子结点 + public TreeNode? right; // 右子结点 + public TreeNode(int x) { val = x; } + } ``` 「结点高度」是最远叶结点到该结点的距离,即走过的「边」的数量。需要特别注意,**叶结点的高度为 0 ,空结点的高度为 -1** 。我们封装两个工具函数,分别用于获取与更新结点的高度。 @@ -108,7 +121,17 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit === "Python" ```python title="avl_tree.py" - + """ 获取结点高度 """ + def height(self, node: typing.Optional[TreeNode]) -> int: + # 空结点高度为 -1 ,叶结点高度为 0 + if node is not None: + return node.height + return -1 + + """ 更新结点高度 """ + def __update_height(self, node: TreeNode): + # 结点高度等于最高子树高度 + 1 + node.height = max([self.height(node.left), self.height(node.right)]) + 1 ``` === "Go" @@ -138,7 +161,19 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit === "C#" ```csharp title="avl_tree.cs" - + /* 获取结点高度 */ + public int height(TreeNode? node) + { + // 空结点高度为 -1 ,叶结点高度为 0 + return node == null ? -1 : node.height; + } + + /* 更新结点高度 */ + private void updateHeight(TreeNode node) + { + // 结点高度等于最高子树高度 + 1 + node.height = Math.Max(height(node.left), height(node.right)) + 1; + } ``` ### 结点平衡因子 @@ -166,7 +201,13 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit === "Python" ```python title="avl_tree.py" - + """ 获取平衡因子 """ + def balance_factor(self, node: TreeNode) -> int: + # 空结点平衡因子为 0 + if node is None: + return 0 + # 结点平衡因子 = 左子树高度 - 右子树高度 + return self.height(node.left) - self.height(node.right) ``` === "Go" @@ -196,7 +237,14 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit === "C#" ```csharp title="avl_tree.cs" - + /* 获取平衡因子 */ + public int balanceFactor(TreeNode? node) + { + // 空结点平衡因子为 0 + if (node == null) return 0; + // 结点平衡因子 = 左子树高度 - 右子树高度 + return height(node.left) - height(node.right); + } ``` !!! note @@ -226,7 +274,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 ![right_rotate_with_grandchild](avl_tree.assets/right_rotate_with_grandchild.png) -“向右旋转” 是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。 +“向右旋转”是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。 === "Java" @@ -255,7 +303,18 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "Python" ```python title="avl_tree.py" - + """ 右旋操作 """ + def __right_rotate(self, node: TreeNode) -> TreeNode: + child = node.left + grand_child = child.right + # 以 child 为原点,将 node 向右旋转 + child.right = node + node.left = grand_child + # 更新结点高度 + self.__update_height(node) + self.__update_height(child) + # 返回旋转后子树的根节点 + return child ``` === "Go" @@ -285,12 +344,26 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "C#" ```csharp title="avl_tree.cs" + /* 右旋操作 */ + TreeNode? rightRotate(TreeNode? node) + { + TreeNode? child = node.left; + TreeNode? grandChild = child?.right; + // 以 child 为原点,将 node 向右旋转 + child.right = node; + node.left = grandChild; + // 更新结点高度 + updateHeight(node); + updateHeight(child); + // 返回旋转后子树的根节点 + return child; + } ``` ### Case 2 - 左旋 -类似地,如果将取上述失衡二叉树的 “镜像” ,那么则需要「左旋」操作。观察发现,**「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的**。 +类似地,如果将取上述失衡二叉树的“镜像”,那么则需要「左旋」操作。观察发现,**「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的**。 ![left_rotate_with_grandchild](avl_tree.assets/left_rotate_with_grandchild.png) @@ -323,7 +396,18 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "Python" ```python title="avl_tree.py" - + """ 左旋操作 """ + def __left_rotate(self, node: TreeNode) -> TreeNode: + child = node.right + grand_child = child.left + # 以 child 为原点,将 node 向左旋转 + child.left = node + node.right = grand_child + # 更新结点高度 + self.__update_height(node) + self.__update_height(child) + # 返回旋转后子树的根节点 + return child ``` === "Go" @@ -353,7 +437,20 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "C#" ```csharp title="avl_tree.cs" - + /* 左旋操作 */ + TreeNode? leftRotate(TreeNode? node) + { + TreeNode? child = node.right; + TreeNode? grandChild = child?.left; + // 以 child 为原点,将 node 向左旋转 + child.left = node; + node.right = grandChild; + // 更新结点高度 + updateHeight(node); + updateHeight(child); + // 返回旋转后子树的根节点 + return child; + } ``` ### Case 3 - 先左后右 @@ -432,7 +529,30 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "Python" ```python title="avl_tree.py" - + """ 执行旋转操作,使该子树重新恢复平衡 """ + def __rotate(self, node: TreeNode) -> TreeNode: + # 获取结点 node 的平衡因子 + balance_factor = self.balance_factor(node) + # 左偏树 + if balance_factor > 1: + if self.balance_factor(node.left) >= 0: + # 右旋 + return self.__right_rotate(node) + else: + # 先左旋后右旋 + node.left = self.__left_rotate(node.left) + return self.__right_rotate(node) + # 右偏树 + elif balance_factor < -1: + if self.balance_factor(node.right) <= 0: + # 左旋 + return self.__left_rotate(node) + else: + # 先右旋后左旋 + node.right = self.__right_rotate(node.right) + return self.__left_rotate(node) + # 平衡树,无需旋转,直接返回 + return node ``` === "Go" @@ -462,7 +582,44 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "C#" ```csharp title="avl_tree.cs" - + /* 执行旋转操作,使该子树重新恢复平衡 */ + TreeNode? rotate(TreeNode? node) + { + // 获取结点 node 的平衡因子 + int balanceFactorInt = balanceFactor(node); + // 左偏树 + if (balanceFactorInt > 1) + { + if (balanceFactor(node.left) >= 0) + { + // 右旋 + return rightRotate(node); + } + else + { + // 先左旋后右旋 + node.left = leftRotate(node?.left); + return rightRotate(node); + } + } + // 右偏树 + if (balanceFactorInt < -1) + { + if (balanceFactor(node.right) <= 0) + { + // 左旋 + return leftRotate(node); + } + else + { + // 先右旋后左旋 + node.right = rightRotate(node?.right); + return leftRotate(node); + } + } + // 平衡树,无需旋转,直接返回 + return node; + } ``` ## AVL 树常用操作 @@ -507,7 +664,27 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "Python" ```python title="avl_tree.py" - + """ 插入结点 """ + def insert(self, val) -> TreeNode: + self.root = self.__insert_helper(self.root, val) + return self.root + + """ 递归插入结点(辅助函数)""" + def __insert_helper(self, node: typing.Optional[TreeNode], val: int) -> TreeNode: + if node is None: + return TreeNode(val) + # 1. 查找插入位置,并插入结点 + if val < node.val: + node.left = self.__insert_helper(node.left, val) + elif val > node.val: + node.right = self.__insert_helper(node.right, val) + else: + # 重复结点不插入,直接返回 + return node + # 更新结点高度 + self.__update_height(node) + # 2. 执行旋转操作,使该子树重新恢复平衡 + return self.__rotate(node) ``` === "Go" @@ -537,7 +714,30 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "C#" ```csharp title="avl_tree.cs" - + /* 插入结点 */ + public TreeNode? insert(int val) + { + root = insertHelper(root, val); + return root; + } + + /* 递归插入结点(辅助函数) */ + private TreeNode? insertHelper(TreeNode? node, int val) + { + if (node == null) return new TreeNode(val); + /* 1. 查找插入位置,并插入结点 */ + if (val < node.val) + node.left = insertHelper(node.left, val); + else if (val > node.val) + node.right = insertHelper(node.right, val); + else + return node; // 重复结点不插入,直接返回 + updateHeight(node); // 更新结点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = rotate(node); + // 返回子树的根节点 + return node; + } ``` ### 删除结点 @@ -604,7 +804,46 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "Python" ```python title="avl_tree.py" - + """ 删除结点 """ + def remove(self, val: int): + root = self.__remove_helper(self.root, val) + return root + + """ 递归删除结点(辅助函数) """ + def __remove_helper(self, node: typing.Optional[TreeNode], val: int) -> typing.Optional[TreeNode]: + if node is None: + return None + # 1. 查找结点,并删除之 + if val < node.val: + node.left = self.__remove_helper(node.left, val) + elif val > node.val: + node.right = self.__remove_helper(node.right, val) + else: + if node.left is None or node.right is None: + child = node.left or node.right + # 子结点数量 = 0 ,直接删除 node 并返回 + if child is None: + return None + # 子结点数量 = 1 ,直接删除 node + else: + node = child + else: # 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + temp = self.min_node(node.right) + node.right = self.__remove_helper(node.right, temp.val) + node.val = temp.val + # 更新结点高度 + self.__update_height(node) + # 2. 执行旋转操作,使该子树重新恢复平衡 + return self.__rotate(node) + + """ 获取最小结点 """ + def min_node(self, node: typing.Optional[TreeNode]) -> typing.Optional[TreeNode]: + if node is None: + return None + # 循环访问左子结点,直到叶结点时为最小结点,跳出 + while node.left is not None: + node = node.left + return node ``` === "Go" @@ -634,7 +873,60 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "C#" ```csharp title="avl_tree.cs" - + /* 删除结点 */ + public TreeNode? remove(int val) + { + root = removeHelper(root, val); + return root; + } + + /* 递归删除结点(辅助函数) */ + private TreeNode? removeHelper(TreeNode? node, int val) + { + if (node == null) return null; + /* 1. 查找结点,并删除之 */ + if (val < node.val) + node.left = removeHelper(node.left, val); + else if (val > node.val) + node.right = removeHelper(node.right, val); + else + { + if (node.left == null || node.right == null) + { + TreeNode? child = node.left != null ? node.left : node.right; + // 子结点数量 = 0 ,直接删除 node 并返回 + if (child == null) + return null; + // 子结点数量 = 1 ,直接删除 node + else + node = child; + } + else + { + // 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + TreeNode? temp = minNode(node.right); + node.right = removeHelper(node.right, temp.val); + node.val = temp.val; + } + } + updateHeight(node); // 更新结点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = rotate(node); + // 返回子树的根节点 + return node; + } + + /* 获取最小结点 */ + private TreeNode? minNode(TreeNode? node) + { + if (node == null) return node; + // 循环访问左子结点,直到叶结点时为最小结点,跳出 + while (node.left != null) + { + node = node.left; + } + return node; + } ``` ### 查找结点 diff --git a/docs/chapter_tree/binary_search_tree.md b/docs/chapter_tree/binary_search_tree.md index 9ff36432..629c051f 100644 --- a/docs/chapter_tree/binary_search_tree.md +++ b/docs/chapter_tree/binary_search_tree.md @@ -17,9 +17,9 @@ comments: true 给定目标结点值 `num` ,可以根据二叉搜索树的性质来查找。我们声明一个结点 `cur` ,从二叉树的根结点 `root` 出发,循环比较结点值 `cur.val` 和 `num` 之间的大小关系 -- 若 `cur.val < val` ,说明目标结点在 `cur` 的右子树中,因此执行 `cur = cur.right` ; -- 若 `cur.val > val` ,说明目标结点在 `cur` 的左子树中,因此执行 `cur = cur.left` ; -- 若 `cur.val = val` ,说明找到目标结点,跳出循环并返回该结点即可; +- 若 `cur.val < num` ,说明目标结点在 `cur` 的右子树中,因此执行 `cur = cur.right` ; +- 若 `cur.val > num` ,说明目标结点在 `cur` 的左子树中,因此执行 `cur = cur.left` ; +- 若 `cur.val = num` ,说明找到目标结点,跳出循环并返回该结点即可; === "Step 1" @@ -82,7 +82,21 @@ comments: true === "Python" ```python title="binary_search_tree.py" - + """ 查找结点 """ + def search(self, num: int) -> typing.Optional[TreeNode]: + cur = self.root + # 循环查找,越过叶结点后跳出 + while cur is not None: + # 目标结点在 root 的右子树中 + if cur.val < num: + cur = cur.right + # 目标结点在 root 的左子树中 + elif cur.val > num: + cur = cur.left + # 找到目标结点,跳出循环 + else: + break + return cur ``` === "Go" @@ -159,12 +173,28 @@ comments: true === "C#" ```csharp title="binary_search_tree.cs" - + /* 查找结点 */ + TreeNode? search(int num) + { + TreeNode? cur = root; + // 循环查找,越过叶结点后跳出 + while (cur != null) + { + // 目标结点在 root 的右子树中 + if (cur.val < num) cur = cur.right; + // 目标结点在 root 的左子树中 + else if (cur.val > num) cur = cur.left; + // 找到目标结点,跳出循环 + else break; + } + // 返回目标结点 + return cur; + } ``` ### 插入结点 -给定一个待插入元素 `num` ,为了保持二叉搜索树 “左子树 < 根结点 < 右子树” 的性质,插入操作分为两步: +给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根结点 < 右子树”的性质,插入操作分为两步: 1. **查找插入位置:** 与查找操作类似,我们从根结点出发,根据当前结点值和 `num` 的大小关系循环向下搜索,直到越过叶结点(遍历到 $\text{null}$ )时跳出循环; 2. **在该位置插入结点:** 初始化结点 `num` ,将该结点放到 $\text{null}$ 的位置 ; @@ -228,7 +258,35 @@ comments: true === "Python" ```python title="binary_search_tree.py" + """ 插入结点 """ + def insert(self, num: int) -> typing.Optional[TreeNode]: + root = self.root + # 若树为空,直接提前返回 + if root is None: + return None + cur = root + pre = None + + # 循环查找,越过叶结点后跳出 + while cur is not None: + # 找到重复结点,直接返回 + if cur.val == num: + return None + pre = cur + + if cur.val < num: # 插入位置在 root 的右子树中 + cur = cur.right + else: # 插入位置在 root 的左子树中 + cur = cur.left + + # 插入结点 val + node = TreeNode(num) + if pre.val < num: + pre.right = node + else: + pre.left = node + return node ``` === "Go" @@ -335,7 +393,33 @@ comments: true === "C#" ```csharp title="binary_search_tree.cs" + /* 插入结点 */ + TreeNode? insert(int num) + { + // 若树为空,直接提前返回 + if (root == null) return null; + TreeNode? cur = root, pre = null; + // 循环查找,越过叶结点后跳出 + while (cur != null) + { + // 找到重复结点,直接返回 + if (cur.val == num) return null; + pre = cur; + // 插入位置在 root 的右子树中 + if (cur.val < num) cur = cur.right; + // 插入位置在 root 的左子树中 + else cur = cur.left; + } + // 插入结点 val + TreeNode node = new TreeNode(num); + if (pre != null) + { + if (pre.val < num) pre.right = node; + else pre.left = node; + } + return node; + } ``` 为了插入结点,需要借助 **辅助结点 `prev`** 保存上一轮循环的结点,这样在遍历到 $\text{null}$ 时,我们也可以获取到其父结点,从而完成结点插入操作。 @@ -344,7 +428,7 @@ comments: true ### 删除结点 -与插入结点一样,我们需要在删除操作后维持二叉搜索树的 “左子树 < 根结点 < 右子树” 的性质。首先,我们需要在二叉树中执行查找操作,获取待删除结点。接下来,根据待删除结点的子结点数量,删除操作需要分为三种情况: +与插入结点一样,我们需要在删除操作后维持二叉搜索树的“左子树 < 根结点 < 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除结点。接下来,根据待删除结点的子结点数量,删除操作需要分为三种情况: **待删除结点的子结点数量 $= 0$ 。** 表明待删除结点是叶结点,直接删除即可。 @@ -483,7 +567,60 @@ comments: true === "Python" ```python title="binary_search_tree.py" + """ 删除结点 """ + def remove(self, num: int) -> typing.Optional[TreeNode]: + root = self.root + # 若树为空,直接提前返回 + if root is None: + return None + cur = root + pre = None + + # 循环查找,越过叶结点后跳出 + while cur is not None: + # 找到待删除结点,跳出循环 + if cur.val == num: + break + pre = cur + if cur.val < num: # 待删除结点在 root 的右子树中 + cur = cur.right + else: # 待删除结点在 root 的左子树中 + cur = cur.left + + # 若无待删除结点,则直接返回 + if cur is None: + return None + + # 子结点数量 = 0 or 1 + if cur.left is None or cur.right is None: + # 当子结点数量 = 0 / 1 时, child = null / 该子结点 + child = cur.left or cur.right + # 删除结点 cur + if pre.left == cur: + pre.left = child + else: + pre.right = child + # 子结点数量 = 2 + else: + # 获取中序遍历中 cur 的下一个结点 + nex = self.min(cur.right) + tmp = nex.val + # 递归删除结点 nex + self.remove(nex.val) + # 将 nex 的值复制给 cur + cur.val = tmp + return cur + + """ 获取最小结点 """ + def min(self, root: typing.Optional[TreeNode]) -> typing.Optional[TreeNode]: + if root is None: + return root + + # 循环访问左子结点,直到叶结点时为最小结点,跳出 + while root.left is not None: + root = root.left + return root ``` === "Go" @@ -649,7 +786,68 @@ comments: true === "C#" ```csharp title="binary_search_tree.cs" - + /* 删除结点 */ + TreeNode? remove(int num) + { + // 若树为空,直接提前返回 + if (root == null) return null; + TreeNode? cur = root, pre = null; + // 循环查找,越过叶结点后跳出 + while (cur != null) + { + // 找到待删除结点,跳出循环 + if (cur.val == num) break; + pre = cur; + // 待删除结点在 root 的右子树中 + if (cur.val < num) cur = cur.right; + // 待删除结点在 root 的左子树中 + else cur = cur.left; + } + // 若无待删除结点,则直接返回 + if (cur == null || pre == null) return null; + // 子结点数量 = 0 or 1 + if (cur.left == null || cur.right == null) + { + // 当子结点数量 = 0 / 1 时, child = null / 该子结点 + TreeNode? child = cur.left != null ? cur.left : cur.right; + // 删除结点 cur + if (pre.left == cur) + { + pre.left = child; + } + else + { + pre.right = child; + } + } + // 子结点数量 = 2 + else + { + // 获取中序遍历中 cur 的下一个结点 + TreeNode? nex = min(cur.right); + if (nex != null) + { + int tmp = nex.val; + // 递归删除结点 nex + remove(nex.val); + // 将 nex 的值复制给 cur + cur.val = tmp; + } + } + return cur; + } + + /* 获取最小结点 */ + TreeNode? min(TreeNode? root) + { + if (root == null) return root; + // 循环访问左子结点,直到叶结点时为最小结点,跳出 + while (root.left != null) + { + root = root.left; + } + return root; + } ``` ## 二叉搜索树的优势 @@ -658,17 +856,17 @@ comments: true - **查找元素:** 由于数组是无序的,因此需要遍历数组来确定,使用 $O(n)$ 时间; - **插入元素:** 只需将元素添加至数组尾部即可,使用 $O(1)$ 时间; -- **删除元素:** 先查找元素,使用 $O(\log n)$ 时间,再在数组中删除该元素,使用 $O(n)$ 时间; +- **删除元素:** 先查找元素,使用 $O(n)$ 时间,再在数组中删除该元素,使用 $O(n)$ 时间; - **获取最小 / 最大元素:** 需要遍历数组来确定,使用 $O(n)$ 时间; 为了得到先验信息,我们也可以预先将数组元素进行排序,得到一个「排序数组」,此时操作效率为: -- **查找元素:** 由于数组已排序,可以使用二分查找,使用 $O(\log n)$ 时间; -- **插入元素:** 为了保持数组是有序的,需插入到数组某位置,平均使用 $O(n)$ 时间; -- **删除元素:** 与无序数组中的情况相同,使用 $O(n)$ 时间; +- **查找元素:** 由于数组已排序,可以使用二分查找,平均使用 $O(\log n)$ 时间; +- **插入元素:** 先查找插入位置,使用 $O(\log n)$ 时间,再插入到指定位置,使用 $O(n)$ 时间; +- **删除元素:** 先查找元素,使用 $O(\log n)$ 时间,再在数组中删除该元素,使用 $O(n)$ 时间; - **获取最小 / 最大元素:** 数组头部和尾部元素即是最小和最大元素,使用 $O(1)$ 时间; -观察发现,无序数组和有序数组中的各类操作的时间复杂度是 “偏科” 的,即有的快有的慢;**而二叉搜索树的各项操作的时间复杂度都是对数阶,在数据量 $n$ 很大时有巨大优势**。 +观察发现,无序数组和有序数组中的各项操作的时间复杂度是“偏科”的,即有的快有的慢;**而二叉搜索树的各项操作的时间复杂度都是对数阶,在数据量 $n$ 很大时有巨大优势**。
@@ -683,7 +881,7 @@ comments: true ## 二叉搜索树的退化 -理想情况下,我们希望二叉搜索树的是 “左右平衡” 的(详见「平衡二叉树」章节),此时可以在 $\log n$ 轮循环内查找任意结点。 +理想情况下,我们希望二叉搜索树的是“左右平衡”的(详见「平衡二叉树」章节),此时可以在 $\log n$ 轮循环内查找任意结点。 如果我们动态地在二叉搜索树中插入与删除结点,**则可能导致二叉树退化为链表**,此时各种操作的时间复杂度也退化之 $O(n)$ 。 diff --git a/docs/chapter_tree/binary_tree.assets/array_representation_complete_binary_tree.png b/docs/chapter_tree/binary_tree.assets/array_representation_complete_binary_tree.png new file mode 100644 index 00000000..f369d5d5 Binary files /dev/null and b/docs/chapter_tree/binary_tree.assets/array_representation_complete_binary_tree.png differ diff --git a/docs/chapter_tree/binary_tree.assets/array_representation_mapping.png b/docs/chapter_tree/binary_tree.assets/array_representation_mapping.png new file mode 100644 index 00000000..5c51843b Binary files /dev/null and b/docs/chapter_tree/binary_tree.assets/array_representation_mapping.png differ diff --git a/docs/chapter_tree/binary_tree.assets/array_representation_with_empty.png b/docs/chapter_tree/binary_tree.assets/array_representation_with_empty.png new file mode 100644 index 00000000..7d1be8dd Binary files /dev/null and b/docs/chapter_tree/binary_tree.assets/array_representation_with_empty.png differ diff --git a/docs/chapter_tree/binary_tree.assets/array_representation_without_empty.png b/docs/chapter_tree/binary_tree.assets/array_representation_without_empty.png new file mode 100644 index 00000000..d0f5d9bc Binary files /dev/null and b/docs/chapter_tree/binary_tree.assets/array_representation_without_empty.png differ diff --git a/docs/chapter_tree/binary_tree_types.assets/balanced_binary_tree.png b/docs/chapter_tree/binary_tree.assets/balanced_binary_tree.png similarity index 100% rename from docs/chapter_tree/binary_tree_types.assets/balanced_binary_tree.png rename to docs/chapter_tree/binary_tree.assets/balanced_binary_tree.png diff --git a/docs/chapter_tree/binary_tree_types.assets/complete_binary_tree.png b/docs/chapter_tree/binary_tree.assets/complete_binary_tree.png similarity index 100% rename from docs/chapter_tree/binary_tree_types.assets/complete_binary_tree.png rename to docs/chapter_tree/binary_tree.assets/complete_binary_tree.png diff --git a/docs/chapter_tree/binary_tree_types.assets/full_binary_tree.png b/docs/chapter_tree/binary_tree.assets/full_binary_tree.png similarity index 100% rename from docs/chapter_tree/binary_tree_types.assets/full_binary_tree.png rename to docs/chapter_tree/binary_tree.assets/full_binary_tree.png diff --git a/docs/chapter_tree/binary_tree_types.assets/perfect_binary_tree.png b/docs/chapter_tree/binary_tree.assets/perfect_binary_tree.png similarity index 100% rename from docs/chapter_tree/binary_tree_types.assets/perfect_binary_tree.png rename to docs/chapter_tree/binary_tree.assets/perfect_binary_tree.png diff --git a/docs/chapter_tree/binary_tree.md b/docs/chapter_tree/binary_tree.md index 243a0753..0c858fd3 100644 --- a/docs/chapter_tree/binary_tree.md +++ b/docs/chapter_tree/binary_tree.md @@ -4,7 +4,7 @@ comments: true # 二叉树 -「二叉树 Binary Tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着 “一分为二” 的分治逻辑。类似于链表,二叉树也是以结点为单位存储的,结点包含「值」和两个「指针」。 +「二叉树 Binary Tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。类似于链表,二叉树也是以结点为单位存储的,结点包含「值」和两个「指针」。 === "Java" @@ -35,7 +35,7 @@ comments: true ```python title="" """ 链表结点类 """ class TreeNode: - def __init__(self, val=0, left=None, right=None): + def __init__(self, val=None, left=None, right=None): self.val = val # 结点值 self.left = left # 左子结点指针 self.right = right # 右子结点指针 @@ -44,13 +44,13 @@ comments: true === "Go" ```go title="" - """ 链表结点类 """ + /* 链表结点类 */ type TreeNode struct { Val int Left *TreeNode Right *TreeNode } - """ 结点初始化方法 """ + /* 结点初始化方法 */ func NewTreeNode(v int) *TreeNode { return &TreeNode{ Left: nil, @@ -79,7 +79,7 @@ comments: true val: number; left: TreeNode | null; right: TreeNode | null; - + constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) { this.val = val === undefined ? 0 : val; // 结点值 this.left = left === undefined ? null : left; // 左子结点指针 @@ -91,23 +91,29 @@ comments: true === "C" ```c title="" - + ``` === "C#" ```csharp title="" - + /* 链表结点类 */ + class TreeNode { + int val; // 结点值 + TreeNode? left; // 左子结点指针 + TreeNode? right; // 右子结点指针 + TreeNode(int x) { val = x; } + } ``` 结点的两个指针分别指向「左子结点 Left Child Node」和「右子结点 Right Child Node」,并且称该结点为两个子结点的「父结点 Parent Node」。给定二叉树某结点,将左子结点以下的树称为该结点的「左子树 Left Subtree」,右子树同理。 +除了叶结点外,每个结点都有子结点和子树。例如,若将上图的「结点 2」看作父结点,那么其左子结点和右子结点分别为「结点 4」和「结点 5」,左子树和右子树分别为「结点 4 以下的树」和「结点 5 以下的树」。 + ![binary_tree_definition](binary_tree.assets/binary_tree_definition.png)

Fig. 子结点与子树

-需要注意,父结点、子结点、子树是可以向下递推的。例如,如果将上图的「结点 2」看作父结点,那么其左子结点和右子结点分别为「结点 4」和「结点 5」,左子树和右子树分别为「结点 4 以下的树」和「结点 5 以下的树」。 - ## 二叉树常见术语 二叉树的术语较多,建议尽量理解并记住。后续可能遗忘,可以在需要使用时回来查看确认。 @@ -115,7 +121,7 @@ comments: true - 「根结点 Root Node」:二叉树最顶层的结点,其没有父结点; - 「叶结点 Leaf Node」:没有子结点的结点,其两个指针都指向 $\text{null}$ ; - 结点所处「层 Level」:从顶置底依次增加,根结点所处层为 1 ; -- 结点「度 Degree」:结点的子结点数量,二叉树中度的范围是 0, 1, 2 ; +- 结点「度 Degree」:结点的子结点数量。二叉树中,度的范围是 0, 1, 2 ; - 「边 Edge」:连接两个结点的边,即结点指针; - 二叉树「高度」:二叉树中根结点到最远叶结点走过边的数量; - 结点「深度 Depth」 :根结点到该结点走过边的数量; @@ -129,27 +135,6 @@ comments: true 值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过结点的数量”,此时高度或深度都需要 + 1 。 -## 二叉树最佳和最差结构 - -当二叉树的每层的结点都被填满时,达到「完美二叉树」;而当所有结点都偏向一边时,二叉树退化为「链表」。 - -![binary_tree_corner_cases](binary_tree.assets/binary_tree_corner_cases.png) - -

Fig. 二叉树的最佳和最差结构

- -如下表所示,在最佳和最差结构下,二叉树的叶结点数量、结点总数、高度等达到极大或极小值。 - -
- -| | 完美二叉树 | 链表 | -| ----------------------------- | ---------- | ---------- | -| 第 $i$ 层的结点数量 | $2^{i-1}$ | $1$ | -| 树的高度为 $h$ 时的叶结点数量 | $2^h$ | $1$ | -| 树的高度为 $h$ 时的结点总数 | $2^{h+1} - 1$ | $h + 1$ | -| 树的结点总数为 $n$ 时的高度 | $\log_2 (n+1) - 1$ | $n - 1$ | - -
- ## 二叉树基本操作 **初始化二叉树。** 与链表类似,先初始化结点,再构建引用指向(即指针)。 @@ -190,7 +175,18 @@ comments: true === "Python" ```python title="binary_tree.py" - + """ 初始化二叉树 """ + # 初始化节点 + n1 = TreeNode(val=1) + n2 = TreeNode(val=2) + n3 = TreeNode(val=3) + n4 = TreeNode(val=4) + n5 = TreeNode(val=5) + # 构建引用指向(即指针) + n1.left = n2 + n1.right = n3 + n2.left = n4 + n2.right = n5 ``` === "Go" @@ -247,13 +243,24 @@ comments: true === "C" ```c title="binary_tree.c" - + ``` === "C#" ```csharp title="binary_tree.cs" - + /* 初始化二叉树 */ + // 初始化结点 + TreeNode n1 = new TreeNode(1); + TreeNode n2 = new TreeNode(2); + TreeNode n3 = new TreeNode(3); + TreeNode n4 = new TreeNode(4); + TreeNode n5 = new TreeNode(5); + // 构建引用指向(即指针) + n1.left = n2; + n1.right = n3; + n2.left = n4; + n2.right = n5; ``` **插入与删除结点。** 与链表类似,插入与删除结点都可以通过修改指针实现。 @@ -288,7 +295,13 @@ comments: true === "Python" ```python title="binary_tree.py" - + """ 插入与删除结点 """ + p = TreeNode(0) + # 在 n1 -> n2 中间插入结点 P + n1.left = p + p.left = n2 + # 删除节点 P + n1.left = n2 ``` === "Go" @@ -330,374 +343,162 @@ comments: true === "C" ```c title="binary_tree.c" - + ``` === "C#" ```csharp title="binary_tree.cs" - + /* 插入与删除结点 */ + TreeNode P = new TreeNode(0); + // 在 n1 -> n2 中间插入结点 P + n1.left = P; + P.left = n2; + // 删除结点 P + n1.left = n2; ``` !!! note 插入结点会改变二叉树的原有逻辑结构,删除结点往往意味着删除了该结点的所有子树。因此,二叉树中的插入与删除一般都是由一套操作配合完成的,这样才能实现有意义的操作。 -## 二叉树遍历 +## 常见二叉树类型 -非线性数据结构的遍历操作比线性数据结构更加复杂,往往需要使用搜索算法来实现。常见的二叉树遍历方式有层序遍历、前序遍历、中序遍历、后序遍历。 +### 完美二叉树 -### 层序遍历 +「完美二叉树 Perfect Binary Tree」的所有层的结点都被完全填满。在完美二叉树中,所有结点的度 = 2 ;若树高度 $= h$ ,则结点总数 $= 2^{h+1} - 1$ ,呈标准的指数级关系,反映着自然界中常见的细胞分裂。 -「层序遍历 Hierarchical-Order Traversal」从顶至底、一层一层地遍历二叉树,并在每层中按照从左到右的顺序访问结点。 +!!! tip -层序遍历本质上是「广度优先搜索 Breadth-First Traversal」,其体现着一种 “一圈一圈向外” 的层进遍历方式。 + 在中文社区中,完美二叉树常被称为「满二叉树」,请注意与完满二叉树区分。 -![binary_tree_bfs](binary_tree.assets/binary_tree_bfs.png) +![perfect_binary_tree](binary_tree.assets/perfect_binary_tree.png) -

Fig. 二叉树的层序遍历

+### 完全二叉树 -广度优先遍历一般借助「队列」来实现。队列的规则是 “先进先出” ,广度优先遍历的规则是 ”一层层平推“ ,两者背后的思想是一致的。 +「完全二叉树 Complete Binary Tree」只有最底层的结点未被填满,且最底层结点尽量靠左填充。 -=== "Java" +**完全二叉树非常适合用数组来表示**。如果按照层序遍历序列的顺序来存储,那么空结点 `null` 一定全部出现在序列的尾部,因此我们就可以不用存储这些 null 了。 - ```java title="binary_tree_bfs.java" - /* 层序遍历 */ - List hierOrder(TreeNode root) { - // 初始化队列,加入根结点 - Queue queue = new LinkedList<>() {{ add(root); }}; - // 初始化一个列表,用于保存遍历序列 - List list = new ArrayList<>(); - while (!queue.isEmpty()) { - TreeNode node = queue.poll(); // 队列出队 - list.add(node.val); // 保存结点值 - if (node.left != null) - queue.offer(node.left); // 左子结点入队 - if (node.right != null) - queue.offer(node.right); // 右子结点入队 - } - return list; - } - ``` +![complete_binary_tree](binary_tree.assets/complete_binary_tree.png) -=== "C++" +### 完满二叉树 - ```cpp title="binary_tree_bfs.cpp" - /* 层序遍历 */ - vector hierOrder(TreeNode* root) { - // 初始化队列,加入根结点 - queue queue; - queue.push(root); - // 初始化一个列表,用于保存遍历序列 - vector vec; - while (!queue.empty()) { - TreeNode* node = queue.front(); - queue.pop(); // 队列出队 - vec.push_back(node->val); // 保存结点 - if (node->left != nullptr) - queue.push(node->left); // 左子结点入队 - if (node->right != nullptr) - queue.push(node->right); // 右子结点入队 - } - return vec; - } - ``` +「完满二叉树 Full Binary Tree」除了叶结点之外,其余所有结点都有两个子结点。 -=== "Python" +![full_binary_tree](binary_tree.assets/full_binary_tree.png) - ```python title="binary_tree_bfs.py" +### 平衡二叉树 - ``` +「平衡二叉树 Balanced Binary Tree」中任意结点的左子树和右子树的高度之差的绝对值 $\leq 1$ 。 -=== "Go" +![balanced_binary_tree](binary_tree.assets/balanced_binary_tree.png) - ```go title="binary_tree_bfs.go" - /* 层序遍历 */ - func levelOrder(root *TreeNode) []int { - // 初始化队列,加入根结点 - queue := list.New() - queue.PushBack(root) - // 初始化一个切片,用于保存遍历序列 - nums := make([]int, 0) - for queue.Len() > 0 { - // poll - node := queue.Remove(queue.Front()).(*TreeNode) - // 保存结点 - nums = append(nums, node.Val) - if node.Left != nil { - // 左子结点入队 - queue.PushBack(node.Left) - } - if node.Right != nil { - // 右子结点入队 - queue.PushBack(node.Right) - } - } - return nums - } - ``` +## 二叉树的退化 -=== "JavaScript" +当二叉树的每层的结点都被填满时,达到「完美二叉树」;而当所有结点都偏向一边时,二叉树退化为「链表」。 - ```js title="binary_tree_bfs.js" - /* 层序遍历 */ - function hierOrder(root) { - // 初始化队列,加入根结点 - let queue = [root]; - // 初始化一个列表,用于保存遍历序列 - let list = []; - while (queue.length) { - let node = queue.shift(); // 队列出队 - list.push(node.val); // 保存结点 - if (node.left) - queue.push(node.left); // 左子结点入队 - if (node.right) - queue.push(node.right); // 右子结点入队 - } - return list; - } - ``` +- 完美二叉树是一个二叉树的“最佳状态”,可以完全发挥出二叉树“分治”的优势; +- 链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至 $O(n)$ ; -=== "TypeScript" +![binary_tree_corner_cases](binary_tree.assets/binary_tree_corner_cases.png) - ```typescript title="binary_tree_bfs.ts" - /* 层序遍历 */ - function hierOrder(root: TreeNode | null): number[] { - // 初始化队列,加入根结点 - const queue = [root]; - // 初始化一个列表,用于保存遍历序列 - const list: number[] = []; - while (queue.length) { - let node = queue.shift() as TreeNode; // 队列出队 - list.push(node.val); // 保存结点 - if (node.left) { - queue.push(node.left); // 左子结点入队 - } - if (node.right) { - queue.push(node.right); // 右子结点入队 - } - } - return list; - } - ``` +

Fig. 二叉树的最佳和最差结构

-=== "C" - - ```c title="binary_tree_bfs.c" - - ``` - -=== "C#" - - ```csharp title="binary_tree_bfs.cs" - - ``` - -### 前序、中序、后序遍历 - -相对地,前、中、后序遍历皆属于「深度优先遍历 Depth-First Traversal」,其体现着一种 “先走到尽头,再回头继续” 的回溯遍历方式。 - -如下图所示,左侧是深度优先遍历的的示意图,右上方是对应的递归实现代码。深度优先遍历就像是绕着整个二叉树的外围 “走” 一圈,走的过程中,在每个结点都会遇到三个位置,分别对应前序遍历、中序遍历、后序遍历。 - -![binary_tree_dfs](binary_tree.assets/binary_tree_dfs.png) - -

Fig. 二叉树的前 / 中 / 后序遍历

+如下表所示,在最佳和最差结构下,二叉树的叶结点数量、结点总数、高度等达到极大或极小值。
-| 位置 | 含义 | 此处访问结点时对应 | -| ---------- | ------------------------------------ | ----------------------------- | -| 橙色圆圈处 | 刚进入此结点,即将访问该结点的左子树 | 前序遍历 Pre-Order Traversal | -| 蓝色圆圈处 | 已访问完左子树,即将访问右子树 | 中序遍历 In-Order Traversal | -| 紫色圆圈处 | 已访问完左子树和右子树,即将返回 | 后序遍历 Post-Order Traversal | +| | 完美二叉树 | 链表 | +| ----------------------------- | ---------- | ---------- | +| 第 $i$ 层的结点数量 | $2^{i-1}$ | $1$ | +| 树的高度为 $h$ 时的叶结点数量 | $2^h$ | $1$ | +| 树的高度为 $h$ 时的结点总数 | $2^{h+1} - 1$ | $h + 1$ | +| 树的结点总数为 $n$ 时的高度 | $\log_2 (n+1) - 1$ | $n - 1$ |
+## 二叉树表示方式 * + +我们一般使用二叉树的「链表表示」,即存储单位为结点 `TreeNode` ,结点之间通过指针(引用)相连接。本文前述示例代码展示了二叉树在链表表示下的各项基本操作。 + +那能否可以用「数组表示」二叉树呢?答案是肯定的。先来分析一个简单案例,给定一个「完美二叉树」,将结点按照层序遍历的顺序编号(从 0 开始),那么可以推导得出父结点索引与子结点索引之间的「映射公式」:**设结点的索引为 $i$ ,则该结点的左子结点索引为 $2i + 1$ 、右子结点索引为 $2i + 2$** 。 + +**本质上,映射公式的作用就是链表中的指针**。对于层序遍历序列中的任意结点,我们都可以使用映射公式来访问子结点。因此,可以直接使用层序遍历序列(即数组)来表示完美二叉树。 + +![array_representation_mapping](binary_tree.assets/array_representation_mapping.png) + +然而,完美二叉树只是个例,二叉树中间层往往存在许多空结点(即 `null` ),而层序遍历序列并不包含这些空结点,并且我们无法单凭序列来猜测空结点的数量和分布位置,**即理论上存在许多种二叉树都符合该层序遍历序列**。显然,这种情况无法使用数组来存储二叉树。 + +![array_representation_without_empty](binary_tree.assets/array_representation_without_empty.png) + +为了解决此问题,考虑按照完美二叉树的形式来表示所有二叉树,**即在序列中使用特殊符号来显式地表示“空位”**。如下图所示,这样处理后,序列(数组)就可以唯一表示二叉树了。 + === "Java" - ```java title="binary_tree_dfs.java" - /* 前序遍历 */ - void preOrder(TreeNode root) { - if (root == null) return; - // 访问优先级:根结点 -> 左子树 -> 右子树 - list.add(root.val); - preOrder(root.left); - preOrder(root.right); - } - - /* 中序遍历 */ - void inOrder(TreeNode root) { - if (root == null) return; - // 访问优先级:左子树 -> 根结点 -> 右子树 - inOrder(root.left); - list.add(root.val); - inOrder(root.right); - } - - /* 后序遍历 */ - void postOrder(TreeNode root) { - if (root == null) return; - // 访问优先级:左子树 -> 右子树 -> 根结点 - postOrder(root.left); - postOrder(root.right); - list.add(root.val); - } + ```java title="" + /* 二叉树的数组表示 */ + // 使用 int 的包装类 Integer ,就可以使用 null 来标记空位 + Integer[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 }; ``` === "C++" - ```cpp title="binary_tree_dfs.cpp" - /* 前序遍历 */ - void preOrder(TreeNode* root) { - if (root == nullptr) return; - // 访问优先级:根结点 -> 左子树 -> 右子树 - vec.push_back(root->val); - preOrder(root->left); - preOrder(root->right); - } - - /* 中序遍历 */ - void inOrder(TreeNode* root) { - if (root == nullptr) return; - // 访问优先级:左子树 -> 根结点 -> 右子树 - inOrder(root->left); - vec.push_back(root->val); - inOrder(root->right); - } - - /* 后序遍历 */ - void postOrder(TreeNode* root) { - if (root == nullptr) return; - // 访问优先级:左子树 -> 右子树 -> 根结点 - postOrder(root->left); - postOrder(root->right); - vec.push_back(root->val); - } + ```cpp title="" + /* 二叉树的数组表示 */ + // 为了符合数据类型为 int ,使用 int 最大值标记空位 + // 该方法的使用前提是没有结点的值 = INT_MAX + vector tree = { 1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15 }; ``` === "Python" - ```python title="binary_tree_dfs.py" - + ```python title="" + """ 二叉树的数组表示 """ + # 直接使用 None 来表示空位 + tree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15] ``` === "Go" - ```go title="binary_tree_dfs.go" - /* 前序遍历 */ - func preOrder(node *TreeNode) { - if node == nil { - return - } - // 访问优先级:根结点 -> 左子树 -> 右子树 - nums = append(nums, node.Val) - preOrder(node.Left) - preOrder(node.Right) - } - - /* 中序遍历 */ - func inOrder(node *TreeNode) { - if node == nil { - return - } - // 访问优先级:左子树 -> 根结点 -> 右子树 - inOrder(node.Left) - nums = append(nums, node.Val) - inOrder(node.Right) - } - - /* 后序遍历 */ - func postOrder(node *TreeNode) { - if node == nil { - return - } - // 访问优先级:左子树 -> 右子树 -> 根结点 - postOrder(node.Left) - postOrder(node.Right) - nums = append(nums, node.Val) - } + ```go title="" + ``` === "JavaScript" - ```js title="binary_tree_dfs.js" - /* 前序遍历 */ - function preOrder(root){ - if (root === null) return; - // 访问优先级:根结点 -> 左子树 -> 右子树 - list.push(root.val); - preOrder(root.left); - preOrder(root.right); - } - - /* 中序遍历 */ - function inOrder(root) { - if (root === null) return; - // 访问优先级:左子树 -> 根结点 -> 右子树 - inOrder(root.left); - list.push(root.val); - inOrder(root.right); - } - - /* 后序遍历 */ - function postOrder(root) { - if (root === null) return; - // 访问优先级:左子树 -> 右子树 -> 根结点 - postOrder(root.left); - postOrder(root.right); - list.push(root.val); - } + ```js title="" + /* 二叉树的数组表示 */ + // 直接使用 null 来表示空位 + let tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; ``` === "TypeScript" - ```typescript title="binary_tree_dfs.ts" - /* 前序遍历 */ - function preOrder(root: TreeNode | null): void { - if (root === null) { - return; - } - // 访问优先级:根结点 -> 左子树 -> 右子树 - list.push(root.val); - preOrder(root.left); - preOrder(root.right); - } - - /* 中序遍历 */ - function inOrder(root: TreeNode | null): void { - if (root === null) { - return; - } - // 访问优先级:左子树 -> 根结点 -> 右子树 - inOrder(root.left); - list.push(root.val); - inOrder(root.right); - } - - /* 后序遍历 */ - function postOrder(root: TreeNode | null): void { - if (root === null) { - return; - } - // 访问优先级:左子树 -> 右子树 -> 根结点 - postOrder(root.left); - postOrder(root.right); - list.push(root.val); - } + ```typescript title="" + /* 二叉树的数组表示 */ + // 直接使用 null 来表示空位 + let tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; ``` === "C" - ```c title="binary_tree_dfs.c" - + ```c title="" + ``` === "C#" - ```csharp title="binary_tree_dfs.cs" - + ```csharp title="" + /* 二叉树的数组表示 */ + // 使用 int? 可空类型 ,就可以使用 null 来标记空位 + int?[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 }; ``` -!!! note +![array_representation_with_empty](binary_tree.assets/array_representation_with_empty.png) - 使用循环一样可以实现前、中、后序遍历,但代码相对繁琐,有兴趣的同学可以自行实现。 +回顾「完全二叉树」的满足条件,其只有最底层有空结点,并且最底层的结点尽量靠左,因而所有空结点都一定出现在层序遍历序列的末尾。**因为我们先验地确定了空位的位置,所以在使用数组表示完全二叉树时,可以省略存储“空位”**。“便于使用数组表示”也是完全二叉树受欢迎的原因之一。 + +![array_representation_complete_binary_tree](binary_tree.assets/array_representation_complete_binary_tree.png) + +数组表示有两个优点: 一是不需要存储指针,节省空间;二是可以随机访问结点。然而,当二叉树中的“空位”很多时,数组中只包含很少结点的数据,空间利用率很低。 diff --git a/docs/chapter_tree/binary_tree.assets/binary_tree_bfs.png b/docs/chapter_tree/binary_tree_traversal.assets/binary_tree_bfs.png similarity index 100% rename from docs/chapter_tree/binary_tree.assets/binary_tree_bfs.png rename to docs/chapter_tree/binary_tree_traversal.assets/binary_tree_bfs.png diff --git a/docs/chapter_tree/binary_tree.assets/binary_tree_dfs.png b/docs/chapter_tree/binary_tree_traversal.assets/binary_tree_dfs.png similarity index 100% rename from docs/chapter_tree/binary_tree.assets/binary_tree_dfs.png rename to docs/chapter_tree/binary_tree_traversal.assets/binary_tree_dfs.png diff --git a/docs/chapter_tree/binary_tree_traversal.md b/docs/chapter_tree/binary_tree_traversal.md new file mode 100644 index 00000000..f4634841 --- /dev/null +++ b/docs/chapter_tree/binary_tree_traversal.md @@ -0,0 +1,448 @@ +--- +comments: true +--- + +# 二叉树遍历 + +非线性数据结构的遍历操作比线性数据结构更加复杂,往往需要使用搜索算法来实现。常见的二叉树遍历方式有层序遍历、前序遍历、中序遍历、后序遍历。 + +## 层序遍历 + +「层序遍历 Hierarchical-Order Traversal」从顶至底、一层一层地遍历二叉树,并在每层中按照从左到右的顺序访问结点。 + +层序遍历本质上是「广度优先搜索 Breadth-First Traversal」,其体现着一种“一圈一圈向外”的层进遍历方式。 + +![binary_tree_bfs](binary_tree_traversal.assets/binary_tree_bfs.png) + +

Fig. 二叉树的层序遍历

+ +广度优先遍历一般借助「队列」来实现。队列的规则是“先进先出”,广度优先遍历的规则是 ”一层层平推“ ,两者背后的思想是一致的。 + +=== "Java" + + ```java title="binary_tree_bfs.java" + /* 层序遍历 */ + List hierOrder(TreeNode root) { + // 初始化队列,加入根结点 + Queue queue = new LinkedList<>() {{ add(root); }}; + // 初始化一个列表,用于保存遍历序列 + List list = new ArrayList<>(); + while (!queue.isEmpty()) { + TreeNode node = queue.poll(); // 队列出队 + list.add(node.val); // 保存结点值 + if (node.left != null) + queue.offer(node.left); // 左子结点入队 + if (node.right != null) + queue.offer(node.right); // 右子结点入队 + } + return list; + } + ``` + +=== "C++" + + ```cpp title="binary_tree_bfs.cpp" + /* 层序遍历 */ + vector hierOrder(TreeNode* root) { + // 初始化队列,加入根结点 + queue queue; + queue.push(root); + // 初始化一个列表,用于保存遍历序列 + vector vec; + while (!queue.empty()) { + TreeNode* node = queue.front(); + queue.pop(); // 队列出队 + vec.push_back(node->val); // 保存结点 + if (node->left != nullptr) + queue.push(node->left); // 左子结点入队 + if (node->right != nullptr) + queue.push(node->right); // 右子结点入队 + } + return vec; + } + ``` + +=== "Python" + + ```python title="binary_tree_bfs.py" + """ 层序遍历 """ + def hier_order(root: TreeNode): + # 初始化队列,加入根结点 + queue = collections.deque() + queue.append(root) + # 初始化一个列表,用于保存遍历序列 + res = [] + while queue: + node = queue.popleft() # 队列出队 + res.append(node.val) # 保存节点值 + if node.left is not None: + queue.append(node.left) # 左子结点入队 + if node.right is not None: + queue.append(node.right) # 右子结点入队 + return res + ``` + +=== "Go" + + ```go title="binary_tree_bfs.go" + /* 层序遍历 */ + func levelOrder(root *TreeNode) []int { + // 初始化队列,加入根结点 + queue := list.New() + queue.PushBack(root) + // 初始化一个切片,用于保存遍历序列 + nums := make([]int, 0) + for queue.Len() > 0 { + // poll + node := queue.Remove(queue.Front()).(*TreeNode) + // 保存结点 + nums = append(nums, node.Val) + if node.Left != nil { + // 左子结点入队 + queue.PushBack(node.Left) + } + if node.Right != nil { + // 右子结点入队 + queue.PushBack(node.Right) + } + } + return nums + } + ``` + +=== "JavaScript" + + ```js title="binary_tree_bfs.js" + /* 层序遍历 */ + function hierOrder(root) { + // 初始化队列,加入根结点 + let queue = [root]; + // 初始化一个列表,用于保存遍历序列 + let list = []; + while (queue.length) { + let node = queue.shift(); // 队列出队 + list.push(node.val); // 保存结点 + if (node.left) + queue.push(node.left); // 左子结点入队 + if (node.right) + queue.push(node.right); // 右子结点入队 + } + return list; + } + ``` + +=== "TypeScript" + + ```typescript title="binary_tree_bfs.ts" + /* 层序遍历 */ + function hierOrder(root: TreeNode | null): number[] { + // 初始化队列,加入根结点 + const queue = [root]; + // 初始化一个列表,用于保存遍历序列 + const list: number[] = []; + while (queue.length) { + let node = queue.shift() as TreeNode; // 队列出队 + list.push(node.val); // 保存结点 + if (node.left) { + queue.push(node.left); // 左子结点入队 + } + if (node.right) { + queue.push(node.right); // 右子结点入队 + } + } + return list; + } + ``` + +=== "C" + + ```c title="binary_tree_bfs.c" + + ``` + +=== "C#" + + ```csharp title="binary_tree_bfs.cs" + /* 层序遍历 */ + public List hierOrder(TreeNode root) + { + // 初始化队列,加入根结点 + Queue queue = new(); + queue.Enqueue(root); + // 初始化一个列表,用于保存遍历序列 + List list = new(); + while (queue.Count != 0) + { + TreeNode node = queue.Dequeue(); // 队列出队 + list.Add(node.val); // 保存结点值 + if (node.left != null) + queue.Enqueue(node.left); // 左子结点入队 + if (node.right != null) + queue.Enqueue(node.right); // 右子结点入队 + } + return list; + } + + ``` + +## 前序、中序、后序遍历 + +相对地,前、中、后序遍历皆属于「深度优先遍历 Depth-First Traversal」,其体现着一种“先走到尽头,再回头继续”的回溯遍历方式。 + +如下图所示,左侧是深度优先遍历的的示意图,右上方是对应的递归实现代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,走的过程中,在每个结点都会遇到三个位置,分别对应前序遍历、中序遍历、后序遍历。 + +![binary_tree_dfs](binary_tree_traversal.assets/binary_tree_dfs.png) + +

Fig. 二叉树的前 / 中 / 后序遍历

+ +
+ +| 位置 | 含义 | 此处访问结点时对应 | +| ---------- | ------------------------------------ | ----------------------------- | +| 橙色圆圈处 | 刚进入此结点,即将访问该结点的左子树 | 前序遍历 Pre-Order Traversal | +| 蓝色圆圈处 | 已访问完左子树,即将访问右子树 | 中序遍历 In-Order Traversal | +| 紫色圆圈处 | 已访问完左子树和右子树,即将返回 | 后序遍历 Post-Order Traversal | + +
+ +=== "Java" + + ```java title="binary_tree_dfs.java" + /* 前序遍历 */ + void preOrder(TreeNode root) { + if (root == null) return; + // 访问优先级:根结点 -> 左子树 -> 右子树 + list.add(root.val); + preOrder(root.left); + preOrder(root.right); + } + + /* 中序遍历 */ + void inOrder(TreeNode root) { + if (root == null) return; + // 访问优先级:左子树 -> 根结点 -> 右子树 + inOrder(root.left); + list.add(root.val); + inOrder(root.right); + } + + /* 后序遍历 */ + void postOrder(TreeNode root) { + if (root == null) return; + // 访问优先级:左子树 -> 右子树 -> 根结点 + postOrder(root.left); + postOrder(root.right); + list.add(root.val); + } + ``` + +=== "C++" + + ```cpp title="binary_tree_dfs.cpp" + /* 前序遍历 */ + void preOrder(TreeNode* root) { + if (root == nullptr) return; + // 访问优先级:根结点 -> 左子树 -> 右子树 + vec.push_back(root->val); + preOrder(root->left); + preOrder(root->right); + } + + /* 中序遍历 */ + void inOrder(TreeNode* root) { + if (root == nullptr) return; + // 访问优先级:左子树 -> 根结点 -> 右子树 + inOrder(root->left); + vec.push_back(root->val); + inOrder(root->right); + } + + /* 后序遍历 */ + void postOrder(TreeNode* root) { + if (root == nullptr) return; + // 访问优先级:左子树 -> 右子树 -> 根结点 + postOrder(root->left); + postOrder(root->right); + vec.push_back(root->val); + } + ``` + +=== "Python" + + ```python title="binary_tree_dfs.py" + """ 前序遍历 """ + def pre_order(root: typing.Optional[TreeNode]): + if root is None: + return + # 访问优先级:根结点 -> 左子树 -> 右子树 + res.append(root.val) + pre_order(root=root.left) + pre_order(root=root.right) + + """ 中序遍历 """ + def in_order(root: typing.Optional[TreeNode]): + if root is None: + return + # 访问优先级:左子树 -> 根结点 -> 右子树 + in_order(root=root.left) + res.append(root.val) + in_order(root=root.right) + + """ 后序遍历 """ + def post_order(root: typing.Optional[TreeNode]): + if root is None: + return + # 访问优先级:左子树 -> 右子树 -> 根结点 + post_order(root=root.left) + post_order(root=root.right) + res.append(root.val) + ``` + +=== "Go" + + ```go title="binary_tree_dfs.go" + /* 前序遍历 */ + func preOrder(node *TreeNode) { + if node == nil { + return + } + // 访问优先级:根结点 -> 左子树 -> 右子树 + nums = append(nums, node.Val) + preOrder(node.Left) + preOrder(node.Right) + } + + /* 中序遍历 */ + func inOrder(node *TreeNode) { + if node == nil { + return + } + // 访问优先级:左子树 -> 根结点 -> 右子树 + inOrder(node.Left) + nums = append(nums, node.Val) + inOrder(node.Right) + } + + /* 后序遍历 */ + func postOrder(node *TreeNode) { + if node == nil { + return + } + // 访问优先级:左子树 -> 右子树 -> 根结点 + postOrder(node.Left) + postOrder(node.Right) + nums = append(nums, node.Val) + } + ``` + +=== "JavaScript" + + ```js title="binary_tree_dfs.js" + /* 前序遍历 */ + function preOrder(root){ + if (root === null) return; + // 访问优先级:根结点 -> 左子树 -> 右子树 + list.push(root.val); + preOrder(root.left); + preOrder(root.right); + } + + /* 中序遍历 */ + function inOrder(root) { + if (root === null) return; + // 访问优先级:左子树 -> 根结点 -> 右子树 + inOrder(root.left); + list.push(root.val); + inOrder(root.right); + } + + /* 后序遍历 */ + function postOrder(root) { + if (root === null) return; + // 访问优先级:左子树 -> 右子树 -> 根结点 + postOrder(root.left); + postOrder(root.right); + list.push(root.val); + } + ``` + +=== "TypeScript" + + ```typescript title="binary_tree_dfs.ts" + /* 前序遍历 */ + function preOrder(root: TreeNode | null): void { + if (root === null) { + return; + } + // 访问优先级:根结点 -> 左子树 -> 右子树 + list.push(root.val); + preOrder(root.left); + preOrder(root.right); + } + + /* 中序遍历 */ + function inOrder(root: TreeNode | null): void { + if (root === null) { + return; + } + // 访问优先级:左子树 -> 根结点 -> 右子树 + inOrder(root.left); + list.push(root.val); + inOrder(root.right); + } + + /* 后序遍历 */ + function postOrder(root: TreeNode | null): void { + if (root === null) { + return; + } + // 访问优先级:左子树 -> 右子树 -> 根结点 + postOrder(root.left); + postOrder(root.right); + list.push(root.val); + } + ``` + +=== "C" + + ```c title="binary_tree_dfs.c" + + ``` + +=== "C#" + + ```csharp title="binary_tree_dfs.cs" + /* 前序遍历 */ + void preOrder(TreeNode? root) + { + if (root == null) return; + // 访问优先级:根结点 -> 左子树 -> 右子树 + list.Add(root.val); + preOrder(root.left); + preOrder(root.right); + } + + /* 中序遍历 */ + void inOrder(TreeNode? root) + { + if (root == null) return; + // 访问优先级:左子树 -> 根结点 -> 右子树 + inOrder(root.left); + list.Add(root.val); + inOrder(root.right); + } + + /* 后序遍历 */ + void postOrder(TreeNode? root) + { + if (root == null) return; + // 访问优先级:左子树 -> 右子树 -> 根结点 + postOrder(root.left); + postOrder(root.right); + list.Add(root.val); + } + ``` + +!!! note + + 使用循环一样可以实现前、中、后序遍历,但代码相对繁琐,有兴趣的同学可以自行实现。 diff --git a/docs/chapter_tree/binary_tree_types.md b/docs/chapter_tree/binary_tree_types.md deleted file mode 100644 index b5879b4e..00000000 --- a/docs/chapter_tree/binary_tree_types.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -comments: true ---- - -# 常见二叉树类型 - -## 完美二叉树 - -「完美二叉树 Perfect Binary Tree」,其所有层的结点都被完全填满。 - -!!! tip - - 在中文社区中,完美二叉树常被称为「满二叉树」,请注意与完满二叉树区分。 - -![perfect_binary_tree](binary_tree_types.assets/perfect_binary_tree.png) - -完美二叉树的性质有: - -- 若树高度 $= h$ ,则结点总数 $= 2^h - 1$; -- (TODO) - -## 完全二叉树 - -「完全二叉树 Complete Binary Tree」只有最底层的结点未被填满,且最底层结点都尽量靠左填充。 - -![complete_binary_tree](binary_tree_types.assets/complete_binary_tree.png) - -完全二叉树有一个很好的性质,可以用「数组」来表示。 - -- (TODO) - -## 完满二叉树 - -「完满二叉树 Full Binary Tree」除了叶结点之外,其余所有结点都有两个子结点。 - -![full_binary_tree](binary_tree_types.assets/full_binary_tree.png) - -## 平衡二叉树 - -**「平衡二叉树 Balanced Binary Tree」** ,其任意结点的左子树和右子树的高度之差的绝对值 $\leq 1$ 。 - -![balanced_binary_tree](binary_tree_types.assets/balanced_binary_tree.png) - -- (TODO) diff --git a/docs/chapter_tree/summary.md b/docs/chapter_tree/summary.md index 5f58b759..a46a253f 100644 --- a/docs/chapter_tree/summary.md +++ b/docs/chapter_tree/summary.md @@ -3,3 +3,16 @@ comments: true --- # 小结 + +- 二叉树是一种非线性数据结构,代表着“一分为二”的分治逻辑。二叉树的结点包含「值」和两个「指针」,分别指向左子结点和右子结点。 +- 选定二叉树中某结点,将其左(右)子结点以下形成的树称为左(右)子树。 +- 二叉树的术语较多,包括根结点、叶结点、层、度、边、高度、深度等。 +- 二叉树的初始化、结点插入、结点删除操作与链表的操作方法类似。 +- 常见的二叉树类型包括完美二叉树、完全二叉树、完满二叉树、平衡二叉树。完美二叉树是理想状态,链表则是退化后的最差状态。 +- 二叉树可以使用数组表示,具体做法是将结点值和空位按照层序遍历的顺序排列,并基于父结点和子结点之间的索引映射公式实现指针。 + +- 二叉树层序遍历是一种广度优先搜索,体现着“一圈一圈向外”的层进式遍历方式,通常借助队列来实现。 +- 前序、中序、后序遍历是深度优先搜索,体现着“走到头、再回头继续”的回溯遍历方式,通常使用递归实现。 +- 二叉搜索树是一种高效的元素查找数据结构,查找、插入、删除操作的时间复杂度皆为 $O(\log n)$ 。二叉搜索树退化为链表后,各项时间复杂度劣化至 $O(n)$ ,因此如何避免退化是非常重要的课题。 +- AVL 树又称平衡二叉搜索树,其通过旋转操作,使得在不断插入与删除结点后,仍然可以保持二叉树的平衡(不退化)。 +- AVL 树的旋转操作分为右旋、左旋、先右旋后左旋、先左旋后右旋。在插入或删除结点后,AVL 树会从底置顶地执行旋转操作,使树恢复平衡。 diff --git a/docs/index.md b/docs/index.md index b0a53308..71c34360 100644 --- a/docs/index.md +++ b/docs/index.md @@ -51,12 +51,12 @@ hide: !!! quote "" -

“追风赶月莫停留,平芜尽处是春山“

+

“追风赶月莫停留,平芜尽处是春山”

一起加油!

--- -## 推荐语 +

推荐语

!!! quote @@ -64,7 +64,7 @@ hide: **—— 邓俊辉,清华大学计算机系教授** -## 致谢 +

致谢

感谢本开源书的每一位撰稿人,是他们的无私奉献让这本书变得更好,他们是: diff --git a/mkdocs.yml b/mkdocs.yml index 47406412..4334382f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -157,7 +157,7 @@ nav: - 小结: chapter_hashing/summary.md - 二叉树: - 二叉树(Binary Tree): chapter_tree/binary_tree.md - - 二叉树常见类型: chapter_tree/binary_tree_types.md + - 二叉树遍历: chapter_tree/binary_tree_traversal.md - 二叉搜索树: chapter_tree/binary_search_tree.md - AVL 树 *: chapter_tree/avl_tree.md - 小结: chapter_tree/summary.md