diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c38d8c9ca..774d2773f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,4 +4,4 @@ - [ ] 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. -- [ ] The code is not relied on a particular environment or IDE and can be runned on a common system (Win, MacOS, Ubuntu). +- [ ] The code does not rely on a particular environment or IDE and can be executed on a standard system (Win, macOS, Ubuntu). diff --git a/codes/cpp/chapter_sorting/merge_sort.cpp b/codes/cpp/chapter_sorting/merge_sort.cpp index 465d0def2..7c9a41c8e 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 8bc6a939f..387ffd142 100644 --- a/codes/cpp/chapter_stack_and_queue/array_queue.cpp +++ b/codes/cpp/chapter_stack_and_queue/array_queue.cpp @@ -59,19 +59,11 @@ public: /* 访问队首元素 */ int peek() { - // 删除头结点 if (empty()) throw out_of_range("队列为空"); 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(); @@ -104,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 fe5dc3110..517a18a4d 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/go/chapter_sorting/merge_sort/merge_sort.go b/codes/go/chapter_sorting/merge_sort/merge_sort.go index e1ae9d6a1..830e2dfa4 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 acaa60b16..498a6fd81 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 620ce2db1..198c90714 100644 --- a/codes/java/chapter_stack_and_queue/array_queue.java +++ b/codes/java/chapter_stack_and_queue/array_queue.java @@ -58,19 +58,11 @@ class ArrayQueue { /* 访问队首元素 */ public int peek() { - // 删除头结点 if (isEmpty()) throw new EmptyStackException(); 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 44d0d70ce..12ac52831 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_sorting/merge_sort.js b/codes/javascript/chapter_sorting/merge_sort.js index d57e41a8e..31860ce1d 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 new file mode 100644 index 000000000..abe0766d8 --- /dev/null +++ b/codes/javascript/chapter_stack_and_queue/array_queue.js @@ -0,0 +1,110 @@ +/** + * File: array_queue.js + * Created Time: 2022-12-13 + * Author: S-N-O-R-L-A-X (snorlax.xu@outlook.com) + */ + + +/* 基于环形数组实现的队列 */ +class ArrayQueue { + #queue; // 用于存储队列元素的数组 + #front = 0; // 头指针,指向队首 + #rear = 0; // 尾指针,指向队尾 + 1 + + constructor(capacity) { + this.#queue = new Array(capacity); + } + + /* 获取队列的容量 */ + get capacity() { + return this.#queue.length; + } + + /* 获取队列的长度 */ + get size() { + // 由于将数组看作为环形,可能 rear < front ,因此需要取余数 + return (this.capacity + this.#rear - this.#front) % this.capacity; + } + + /* 判断队列是否为空 */ + empty() { + return this.#rear - this.#front == 0; + } + + /* 入队 */ + offer(num) { + if (this.size == this.capacity) + throw new Error("队列已满"); + // 尾结点后添加 num + this.#queue[this.#rear] = num; + // 尾指针向后移动一位,越过尾部后返回到数组头部 + this.#rear = (this.#rear + 1) % this.capacity; + } + + /* 出队 */ + poll() { + const num = this.peek(); + // 队头指针向后移动一位,若越过尾部则返回到数组头部 + this.#front = (this.#front + 1) % this.capacity; + return num; + } + + /* 访问队首元素 */ + peek() { + if (this.empty()) + throw new Error("队列为空"); + return this.#queue[this.#front]; + } + + /* 返回 Array */ + toArray() { + const siz = this.size; + const cap = this.capacity; + // 仅转换有效长度范围内的列表元素 + const arr = new Array(siz); + for (let i = 0, j = this.#front; i < siz; i++, j++) { + arr[i] = this.#queue[j % cap]; + } + return arr; + } +} + + +/* Driver Code */ +/* 初始化队列 */ +const capacity = 10; +const queue = new ArrayQueue(capacity); + +/* 元素入队 */ +queue.offer(1); +queue.offer(3); +queue.offer(2); +queue.offer(5); +queue.offer(4); +console.log("队列 queue = "); +console.log(queue.toArray()); + +/* 访问队首元素 */ +const peek = queue.peek(); +console.log("队首元素 peek = " + peek); + +/* 元素出队 */ +const poll = queue.poll(); +console.log("出队元素 poll = " + poll + ",出队后 queue = "); +console.log(queue.toArray()); + +/* 获取队列的长度 */ +const size = queue.size; +console.log("队列长度 size = " + size); + +/* 判断队列是否为空 */ +const empty = queue.empty(); +console.log("队列是否为空 = " + empty); + +/* 测试环形数组 */ +for (let i = 0; i < 10; i++) { + queue.offer(i); + queue.poll(); + console.log("第 " + i + " 轮入队 + 出队后 queue = "); + console.log(queue.toArray()); +} diff --git a/codes/javascript/chapter_stack_and_queue/array_stack.js b/codes/javascript/chapter_stack_and_queue/array_stack.js index 271210bd1..37fa23b1f 100644 --- a/codes/javascript/chapter_stack_and_queue/array_stack.js +++ b/codes/javascript/chapter_stack_and_queue/array_stack.js @@ -11,6 +11,7 @@ class ArrayStack { constructor() { this.stack = []; } + /* 获取栈的长度 */ get size() { return this.stack.length; @@ -28,22 +29,18 @@ class ArrayStack { /* 出栈 */ pop() { - if (this.empty()) throw "栈为空"; + if (this.empty()) + throw new Error("栈为空"); return this.stack.pop(); } /* 访问栈顶元素 */ top() { - if (this.empty()) throw "栈为空"; + if (this.empty()) + throw new Error("栈为空"); return this.stack[this.stack.length - 1]; } - /* 访问索引 index 处元素 */ - get(index) { - if (index >= this.size) throw "索引越界"; - return this.stack[index]; - } - /* 返回 Array */ toArray() { return this.stack; @@ -69,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_queue.js b/codes/javascript/chapter_stack_and_queue/linkedlist_queue.js new file mode 100644 index 000000000..62c2a3834 --- /dev/null +++ b/codes/javascript/chapter_stack_and_queue/linkedlist_queue.js @@ -0,0 +1,102 @@ +/** + * File: linkedlist_queue.js + * Created Time: 2022-12-20 + * Author: S-N-O-R-L-A-X (snorlax.xu@outlook.com) + */ + +const ListNode = require("../include/ListNode"); + +/* 基于链表实现的队列 */ +class LinkedListQueue { + #front; // 头结点 #front + #rear; // 尾结点 #rear + #queSize = 0; + + constructor() { + this.#front = null; + this.#rear = null; + } + + /* 获取队列的长度 */ + get size() { + return this.#queSize; + } + + /* 判断队列是否为空 */ + isEmpty() { + return this.size === 0; + } + + /* 入队 */ + offer(num) { + // 尾结点后添加 num + const node = new ListNode(num); + // 如果队列为空,则令头、尾结点都指向该结点 + if (!this.#front) { + this.#front = node; + this.#rear = node; + // 如果队列不为空,则将该结点添加到尾结点后 + } else { + this.#rear.next = node; + this.#rear = node; + } + this.#queSize++; + } + + /* 出队 */ + poll() { + const num = this.peek(); + // 删除头结点 + this.#front = this.#front.next; + this.#queSize--; + return num; + } + + /* 访问队首元素 */ + peek() { + if (this.size === 0) + throw new Error("队列为空"); + return this.#front.val; + } + + /* 将链表转化为 Array 并返回 */ + toArray() { + let node = this.#front; + const res = new Array(this.size); + for (let i = 0; i < res.length; i++) { + res[i] = node.val; + node = node.next; + } + return res; + } +} + + + +/* Driver Code */ +/* 初始化队列 */ +const queue = new LinkedListQueue(); + +/* 元素入队 */ +queue.offer(1); +queue.offer(3); +queue.offer(2); +queue.offer(5); +queue.offer(4); +console.log("队列 queue = " + queue.toArray()); + +/* 访问队首元素 */ +const peek = queue.peek(); +console.log("队首元素 peek = " + peek); + +/* 元素出队 */ +const poll = queue.poll(); +console.log("出队元素 poll = " + poll + ",出队后 queue = " + queue.toArray()); + +/* 获取队列的长度 */ +const size = queue.size; +console.log("队列长度 size = " + size); + +/* 判断队列是否为空 */ +const isEmpty = queue.isEmpty(); +console.log("队列是否为空 = " + isEmpty); diff --git a/codes/javascript/chapter_stack_and_queue/queue.js b/codes/javascript/chapter_stack_and_queue/queue.js index 5d26727f1..a443021cb 100644 --- a/codes/javascript/chapter_stack_and_queue/queue.js +++ b/codes/javascript/chapter_stack_and_queue/queue.js @@ -4,9 +4,10 @@ * Author: S-N-O-R-L-A-X (snorlax.xu@outlook.com) */ + +/* Driver Code */ /* 初始化队列 */ -// JavaScript 没有内置的队列,可以把 Array 当作队列来使用 -// 注意:由于是数组,所以 shift() 的时间复杂度是 O(n) +// JavaScript 没有内置的队列,可以把 Array 当作队列来使用 const queue = []; /* 元素入队 */ @@ -20,7 +21,7 @@ queue.push(4); const peek = queue[0]; /* 元素出队 */ -// O(n) +// 底层是数组,因此 shift() 方法的时间复杂度为 O(n) const poll = queue.shift(); /* 获取队列的长度 */ diff --git a/codes/javascript/chapter_stack_and_queue/stack.js b/codes/javascript/chapter_stack_and_queue/stack.js index 1d4944946..1be7980e3 100644 --- a/codes/javascript/chapter_stack_and_queue/stack.js +++ b/codes/javascript/chapter_stack_and_queue/stack.js @@ -4,6 +4,8 @@ * Author: S-N-O-R-L-A-X (snorlax.xu@outlook.com) */ + +/* Driver Code */ /* 初始化栈 */ // Javascript 没有内置的栈类,可以把 Array 当作栈来使用 const stack = []; diff --git a/codes/python/chapter_sorting/merge_sort.py b/codes/python/chapter_sorting/merge_sort.py index 09aa09bbb..533fb9de7 100644 --- a/codes/python/chapter_sorting/merge_sort.py +++ b/codes/python/chapter_sorting/merge_sort.py @@ -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_stack_and_queue/array_queue.py b/codes/python/chapter_stack_and_queue/array_queue.py index b848794be..7408f2ba2 100644 --- a/codes/python/chapter_stack_and_queue/array_queue.py +++ b/codes/python/chapter_stack_and_queue/array_queue.py @@ -42,7 +42,6 @@ class ArrayQueue: """ 出队 """ def poll(self): - # 删除头结点 num = self.peek() # 队头指针向后移动一位,若越过尾部则返回到数组头部 self.__front = (self.__front + 1) % self.capacity() @@ -50,19 +49,11 @@ class ArrayQueue: """ 访问队首元素 """ def peek(self): - # 删除头结点 if self.is_empty(): print("队列为空") 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() @@ -90,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 32ba9dcda..889a372dc 100644 --- a/codes/python/chapter_stack_and_queue/array_stack.py +++ b/codes/python/chapter_stack_and_queue/array_stack.py @@ -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/typescript/chapter_sorting/merge_sort.ts b/codes/typescript/chapter_sorting/merge_sort.ts index b84c78282..44fb4b8d0 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 new file mode 100644 index 000000000..1f67c648d --- /dev/null +++ b/codes/typescript/chapter_stack_and_queue/array_queue.ts @@ -0,0 +1,111 @@ +/** + * File: array_queue.ts + * Created Time: 2022-12-11 + * Author: S-N-O-R-L-A-X (snorlax.xu@outlook.com) + */ + + +/* 基于环形数组实现的队列 */ +class ArrayQueue { + private queue: number[]; // 用于存储队列元素的数组 + private front: number = 0; // 头指针,指向队首 + private rear: number = 0; // 尾指针,指向队尾 + 1 + private CAPACITY: number = 1e5; + + constructor(capacity?: number) { + this.queue = new Array(capacity ?? this.CAPACITY); + } + + /* 获取队列的容量 */ + get capacity(): number { + return this.queue.length; + } + + /* 获取队列的长度 */ + get size(): number { + // 由于将数组看作为环形,可能 rear < front ,因此需要取余数 + return (this.capacity + this.rear - this.front) % this.capacity; + } + + /* 判断队列是否为空 */ + empty(): boolean { + return this.rear - this.front == 0; + } + + /* 入队 */ + offer(num: number): void { + if (this.size == this.capacity) + throw new Error("队列已满"); + // 尾结点后添加 num + this.queue[this.rear] = num; + // 尾指针向后移动一位,越过尾部后返回到数组头部 + this.rear = (this.rear + 1) % this.capacity; + } + + /* 出队 */ + poll(): number { + const num = this.peek(); + // 队头指针向后移动一位,若越过尾部则返回到数组头部 + this.front = (this.front + 1) % this.capacity; + return num; + } + + /* 访问队首元素 */ + peek(): number { + if (this.empty()) + throw new Error("队列为空"); + return this.queue[this.front]; + } + + /* 返回 Array */ + toArray(): number[] { + const siz = this.size; + const cap = this.capacity; + // 仅转换有效长度范围内的列表元素 + const arr = new Array(siz); + for (let i = 0, j = this.front; i < siz; i++, j++) { + arr[i] = this.queue[j % cap]; + } + return arr; + } +} + +/* 初始化队列 */ +const capacity = 10; +const queue = new ArrayQueue(capacity); + +/* 元素入队 */ +queue.offer(1); +queue.offer(3); +queue.offer(2); +queue.offer(5); +queue.offer(4); +console.log("队列 queue = "); +console.log(queue.toArray()); + +/* 访问队首元素 */ +const peek = queue.peek(); +console.log("队首元素 peek = " + peek); + +/* 元素出队 */ +const poll = queue.poll(); +console.log("出队元素 poll = " + poll + ",出队后 queue = "); +console.log(queue.toArray()); + +/* 获取队列的长度 */ +const size = queue.size; +console.log("队列长度 size = " + size); + +/* 判断队列是否为空 */ +const empty = queue.empty(); +console.log("队列是否为空 = " + empty); + +/* 测试环形数组 */ +for (let i = 0; i < 10; i++) { + queue.offer(i); + queue.poll(); + console.log("第 " + i + " 轮入队 + 出队后 queue = "); + console.log(queue.toArray()); +} + +export { }; \ No newline at end of file diff --git a/codes/typescript/chapter_stack_and_queue/array_stack.ts b/codes/typescript/chapter_stack_and_queue/array_stack.ts index 93a380f45..2b9f8468d 100644 --- a/codes/typescript/chapter_stack_and_queue/array_stack.ts +++ b/codes/typescript/chapter_stack_and_queue/array_stack.ts @@ -29,22 +29,18 @@ class ArrayStack { /* 出栈 */ pop(): number | undefined { - if (this.empty()) throw new Error('栈为空'); + if (this.empty()) + throw new Error('栈为空'); return this.stack.pop(); } /* 访问栈顶元素 */ top(): number | undefined { - if (this.empty()) throw new Error('栈为空'); + if (this.empty()) + 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]; - } - /* 返回 Array */ toArray() { return this.stack; @@ -70,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_queue.ts b/codes/typescript/chapter_stack_and_queue/linkedlist_queue.ts new file mode 100644 index 000000000..6dabdbc0c --- /dev/null +++ b/codes/typescript/chapter_stack_and_queue/linkedlist_queue.ts @@ -0,0 +1,102 @@ +/** + * File: linkedlist_queue.ts + * Created Time: 2022-12-19 + * Author: S-N-O-R-L-A-X (snorlax.xu@outlook.com) + */ + +import ListNode from "../module/ListNode" + +/* 基于链表实现的队列 */ +class LinkedListQueue { + private front: ListNode | null; // 头结点 front + private rear: ListNode | null; // 尾结点 rear + private queSize: number = 0; + + constructor() { + this.front = null; + this.rear = null; + } + + /* 获取队列的长度 */ + get size(): number { + return this.queSize; + } + + /* 判断队列是否为空 */ + isEmpty(): boolean { + return this.size === 0; + } + + /* 入队 */ + offer(num: number): void { + // 尾结点后添加 num + const node = new ListNode(num); + // 如果队列为空,则令头、尾结点都指向该结点 + if (!this.front) { + this.front = node; + this.rear = node; + // 如果队列不为空,则将该结点添加到尾结点后 + } else { + this.rear!.next = node; + this.rear = node; + } + this.queSize++; + } + + /* 出队 */ + poll(): number { + const num = this.peek(); + if (!this.front) + throw new Error("队列为空") + // 删除头结点 + this.front = this.front.next; + this.queSize--; + return num; + } + + /* 访问队首元素 */ + peek(): number { + if (this.size === 0) + throw new Error("队列为空"); + return this.front!.val; + } + + /* 将链表转化为 Array 并返回 */ + toArray(): number[] { + let node = this.front; + const res = new Array(this.size); + for (let i = 0; i < res.length; i++) { + res[i] = node!.val; + node = node!.next; + } + return res; + } +} + + +/* 初始化队列 */ +const queue = new LinkedListQueue(); + +/* 元素入队 */ +queue.offer(1); +queue.offer(3); +queue.offer(2); +queue.offer(5); +queue.offer(4); +console.log("队列 queue = " + queue.toArray()); + +/* 访问队首元素 */ +const peek = queue.peek(); +console.log("队首元素 peek = " + peek); + +/* 元素出队 */ +const poll = queue.poll(); +console.log("出队元素 poll = " + poll + ",出队后 queue = " + queue.toArray()); + +/* 获取队列的长度 */ +const size = queue.size; +console.log("队列长度 size = " + size); + +/* 判断队列是否为空 */ +const isEmpty = queue.isEmpty(); +console.log("队列是否为空 = " + isEmpty); diff --git a/codes/typescript/chapter_stack_and_queue/queue.ts b/codes/typescript/chapter_stack_and_queue/queue.ts index 0701a9af5..0b7444f64 100644 --- a/codes/typescript/chapter_stack_and_queue/queue.ts +++ b/codes/typescript/chapter_stack_and_queue/queue.ts @@ -6,7 +6,6 @@ /* 初始化队列 */ // TypeScript 没有内置的队列,可以把 Array 当作队列来使用 -// 注意:由于是数组,所以 shift() 的时间复杂度是 O(n) const queue: number[] = []; /* 元素入队 */ @@ -20,7 +19,7 @@ queue.push(4); const peek = queue[0]; /* 元素出队 */ -// O(n) +// 底层是数组,因此 shift() 方法的时间复杂度为 O(n) const poll = queue.shift(); /* 获取队列的长度 */ diff --git a/docs/chapter_array_and_linkedlist/array.md b/docs/chapter_array_and_linkedlist/array.md index 70e30129a..2ad57f0f3 100644 --- a/docs/chapter_array_and_linkedlist/array.md +++ b/docs/chapter_array_and_linkedlist/array.md @@ -298,7 +298,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` -**数组中插入或删除元素效率低下。** 假设我们想要在数组中间某位置插入一个元素,由于数组元素在内存中是 “紧挨着的” ,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。删除元素也是类似,需要把此索引之后的元素都向前移动一位。总体看有以下缺点: +**数组中插入或删除元素效率低下。** 假设我们想要在数组中间某位置插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。删除元素也是类似,需要把此索引之后的元素都向前移动一位。总体看有以下缺点: - **时间复杂度高:** 数组的插入和删除的平均时间复杂度均为 $O(N)$ ,其中 $N$ 为数组长度。 - **丢失元素:** 由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会被丢失。 @@ -661,6 +661,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 6ad1d5658..412d09e79 100644 --- a/docs/chapter_array_and_linkedlist/linked_list.md +++ b/docs/chapter_array_and_linkedlist/linked_list.md @@ -613,7 +613,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) {} // 构造函数 }; ``` @@ -644,8 +644,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; // 指向前驱结点的指针(引用) } } ``` @@ -660,8 +660,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; // 指向前驱结点的指针(引用) } } ``` diff --git a/docs/chapter_array_and_linkedlist/list.md b/docs/chapter_array_and_linkedlist/list.md index 3c7a692e4..2098a82c1 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]; ``` diff --git a/docs/chapter_computational_complexity/performance_evaluation.md b/docs/chapter_computational_complexity/performance_evaluation.md index dc1806e7d..c664a2083 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 83db26082..d640c501b 100644 --- a/docs/chapter_computational_complexity/space_complexity.md +++ b/docs/chapter_computational_complexity/space_complexity.md @@ -154,9 +154,9 @@ comments: true ## 推算方法 -空间复杂度的推算方法和时间复杂度总体类似,只是从统计 “计算操作数量” 变为统计 “使用空间大小” 。与时间复杂度不同的是,**我们一般只关注「最差空间复杂度」**。这是因为内存空间是一个硬性要求,我们必须保证在所有输入数据下都有足够的内存空间预留。 +空间复杂度的推算方法和时间复杂度总体类似,只是从统计“计算操作数量”变为统计“使用空间大小”。与时间复杂度不同的是,**我们一般只关注「最差空间复杂度」**。这是因为内存空间是一个硬性要求,我们必须保证在所有输入数据下都有足够的内存空间预留。 -**最差空间复杂度中的 “最差” 有两层含义**,分别为输入数据的最差分布、算法运行中的最差时间点。 +**最差空间复杂度中的“最差”有两层含义**,分别为输入数据的最差分布、算法运行中的最差时间点。 - **以最差输入数据为准。** 当 $n < 10$ 时,空间复杂度为 $O(1)$ ;但是当 $n > 10$ 时,初始化的数组 `nums` 使用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$ ; - **以算法运行过程中的峰值内存为准。** 程序在执行最后一行之前,使用 $O(1)$ 空间;当初始化数组 `nums` 时,程序使用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$ ; diff --git a/docs/chapter_computational_complexity/summary.md b/docs/chapter_computational_complexity/summary.md index 7e76d2c15..0a1ee80be 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 bc8be8913..a482c7ae1 100644 --- a/docs/chapter_computational_complexity/time_complexity.md +++ b/docs/chapter_computational_complexity/time_complexity.md @@ -106,7 +106,7 @@ $$ 「时间复杂度分析」采取了不同的做法,其统计的不是算法运行时间,而是 **算法运行时间随着数据量变大时的增长趋势** 。 -“时间增长趋势” 这个概念比较抽象,我们借助一个例子来理解。设输入数据大小为 $n$ ,给定三个算法 `A` , `B` , `C` 。 +“时间增长趋势”这个概念比较抽象,我们借助一个例子来理解。设输入数据大小为 $n$ ,给定三个算法 `A` , `B` , `C` 。 - 算法 `A` 只有 $1$ 个打印操作,算法运行时间不随着 $n$ 增大而增长。我们称此算法的时间复杂度为「常数阶」。 - 算法 `B` 中的打印操作需要循环 $n$ 次,算法运行时间随着 $n$ 增大成线性增长。此算法的时间复杂度被称为「线性阶」。 @@ -223,7 +223,7 @@ $$ **时间复杂度可以有效评估算法效率。** 算法 `B` 运行时间的增长是线性的,在 $n > 1$ 时慢于算法 `A` ,在 $n > 1000000$ 时慢于算法 `C` 。实质上,只要输入数据大小 $n$ 足够大,复杂度为「常数阶」的算法一定优于「线性阶」的算法,这也正是时间增长趋势的含义。 -**时间复杂度分析将统计「计算操作的运行时间」简化为统计「计算操作的数量」。** 这是因为,无论是运行平台、还是计算操作类型,都与算法运行时间的增长趋势无关。因此,我们可以简单地将所有计算操作的执行时间统一看作是相同的 “单位时间” 。 +**时间复杂度分析将统计「计算操作的运行时间」简化为统计「计算操作的数量」。** 这是因为,无论是运行平台、还是计算操作类型,都与算法运行时间的增长趋势无关。因此,我们可以简单地将所有计算操作的执行时间统一看作是相同的“单位时间”。 **时间复杂度也存在一定的局限性。** 比如,虽然算法 `A` 和 `C` 的时间复杂度相同,但是实际的运行时间有非常大的差别。再比如,虽然算法 `B` 比 `C` 的时间复杂度要更高,但在输入数据大小 $n$ 比较小时,算法 `B` 是要明显优于算法 `C` 的。即使存在这些问题,计算复杂度仍然是评判算法效率的最有效、最常用方法。 @@ -464,7 +464,7 @@ $$ **时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将处于主导作用,其它项的影响都可以被忽略。 -以下表格给出了一些例子,其中有一些夸张的值,是想要向大家强调 **系数无法撼动阶数** 这一结论。在 $n$ 趋于无穷大时,这些常数都是 “浮云” 。 +以下表格给出了一些例子,其中有一些夸张的值,是想要向大家强调 **系数无法撼动阶数** 这一结论。在 $n$ 趋于无穷大时,这些常数都是“浮云”。
@@ -954,7 +954,7 @@ $$ !!! note - 生物学科中的 “细胞分裂” 即是指数阶增长:初始状态为 $1$ 个细胞,分裂一轮后为 $2$ 个,分裂两轮后为 $4$ 个,……,分裂 $n$ 轮后有 $2^n$ 个细胞。 + 生物学科中的“细胞分裂”即是指数阶增长:初始状态为 $1$ 个细胞,分裂一轮后为 $2$ 个,分裂两轮后为 $4$ 个,……,分裂 $n$ 轮后有 $2^n$ 个细胞。 指数阶增长得非常快,在实际应用中一般是不能被接受的。若一个问题使用「暴力枚举」求解的时间复杂度是 $O(2^n)$ ,那么一般都需要使用「动态规划」或「贪心算法」等算法来求解。 @@ -1124,9 +1124,9 @@ $$ ### 对数阶 $O(\log n)$ -对数阶与指数阶正好相反,后者反映 “每轮增加到两倍的情况” ,而前者反映 “每轮缩减到一半的情况” 。对数阶仅次于常数阶,时间增长的很慢,是理想的时间复杂度。 +对数阶与指数阶正好相反,后者反映“每轮增加到两倍的情况”,而前者反映“每轮缩减到一半的情况”。对数阶仅次于常数阶,时间增长的很慢,是理想的时间复杂度。 -对数阶常出现于「二分查找」和「分治算法」中,体现 “一分为多” 、“化繁为简” 的算法思想。 +对数阶常出现于「二分查找」和「分治算法」中,体现“一分为多”、“化繁为简”的算法思想。 设输入数据大小为 $n$ ,由于每轮缩减到一半,因此循环次数是 $\log_2 n$ ,即 $2^n$ 的反函数。 @@ -1657,9 +1657,9 @@ $$ !!! 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 7c8e22f0b..b41266bb8 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 198df88c3..930f37622 100644 --- a/docs/chapter_data_structure/data_and_memory.md +++ b/docs/chapter_data_structure/data_and_memory.md @@ -42,7 +42,7 @@ comments: true **「基本数据类型」与「数据结构」之间的联系与区别** -我们知道,数据结构是在计算机中 **组织与存储数据的方式** ,它的主语是 “结构” ,而不是 “数据” 。比如,我们想要表示 “一排数字” ,自然应该使用「数组」这个数据结构。数组的存储方式使之可以表示数字的相邻关系、先后关系等一系列我们需要的信息,但至于其中存储的是整数 int ,还是小数 float ,或是字符 char ,**则与所谓的数据的结构无关了**。 +我们知道,数据结构是在计算机中 **组织与存储数据的方式** ,它的主语是“结构”,而不是“数据”。比如,我们想要表示“一排数字”,自然应该使用「数组」这个数据结构。数组的存储方式使之可以表示数字的相邻关系、先后关系等一系列我们需要的信息,但至于其中存储的是整数 int ,还是小数 float ,或是字符 char ,**则与所谓的数据的结构无关了**。 === "Java" diff --git a/docs/chapter_hashing/hash_collision.md b/docs/chapter_hashing/hash_collision.md index 793296209..05dc20881 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 896675ec6..b3dc7521d 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) @@ -623,4 +623,4 @@ $$ - 尽量少地发生哈希冲突; - 时间复杂度 $O(1)$ ,计算尽可能高效; -- 空间使用率高,即 “键值对占用空间 / 哈希表总占用空间” 尽可能大; +- 空间使用率高,即“键值对占用空间 / 哈希表总占用空间”尽可能大; diff --git a/docs/chapter_introduction/algorithms_are_everywhere.md b/docs/chapter_introduction/algorithms_are_everywhere.md index 24f474132..b80a525b0 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_preface/about_the_book.md b/docs/chapter_preface/about_the_book.md index fdb27b87b..db70a8acd 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 8bc8b2936..c710e6de5 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/suggestions.md b/docs/chapter_preface/suggestions.md index 20cdac70a..c0377ee34 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 0a407ff91..dd9192e09 100644 --- a/docs/chapter_searching/binary_search.md +++ b/docs/chapter_searching/binary_search.md @@ -24,9 +24,9 @@ $$ 1. **双闭区间 $[0, n-1]$** ,即两个边界都包含自身;此方法下,区间 $[0, 0]$ 仍包含一个元素; 2. **左闭右开 $[0, n)$** ,即左边界包含自身、右边界不包含自身;此方法下,区间 $[0, 0)$ 为空; -### “双闭区间” 实现 +### “双闭区间”实现 -首先,我们先采用 “双闭区间” 的表示,在数组 `nums` 中查找目标元素 `target` 的对应索引。 +首先,我们先采用“双闭区间”的表示,在数组 `nums` 中查找目标元素 `target` 的对应索引。 === "Step 1" @@ -56,7 +56,7 @@ $$ ![binary_search_step7](binary_search.assets/binary_search_step7.png) -二分查找 “双闭区间” 表示下的代码如下所示。 +二分查找“双闭区间”表示下的代码如下所示。 === "Java" @@ -167,9 +167,9 @@ $$ ``` -### “左闭右开” 实现 +### “左闭右开”实现 -当然,我们也可以使用 “左闭右开” 的表示方法,写出相同功能的二分查找代码。 +当然,我们也可以使用“左闭右开”的表示方法,写出相同功能的二分查找代码。 === "Java" @@ -294,7 +294,7 @@ $$
-观察发现,在 “双闭区间” 表示中,由于对左右两边界的定义是相同的,因此缩小区间的 $i$ , $j$ 处理方法也是对称的,这样更不容易出错。综上所述,**建议你采用 “双闭区间” 的写法。** +观察发现,在“双闭区间”表示中,由于对左右两边界的定义是相同的,因此缩小区间的 $i$ , $j$ 处理方法也是对称的,这样更不容易出错。综上所述,**建议你采用“双闭区间”的写法。** ### 大数越界处理 diff --git a/docs/chapter_searching/hashing_search.md b/docs/chapter_searching/hashing_search.md index e1fcb38eb..b5537212e 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$ 值”映射查找,体现着“以空间换时间”的算法思想。 ## 算法实现 diff --git a/docs/chapter_sorting/bubble_sort.md b/docs/chapter_sorting/bubble_sort.md index 2dddba6bc..23f7eceed 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 "为什么叫“冒泡”" 在水中,越大的泡泡浮力越大,所以最大的泡泡会最先浮到水面。 diff --git a/docs/chapter_sorting/insertion_sort.md b/docs/chapter_sorting/insertion_sort.md index 1ec47f898..b1477213f 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` 之间的所有元素向右移动一位(本质上是一次数组插入操作)。 diff --git a/docs/chapter_sorting/merge_sort.md b/docs/chapter_sorting/merge_sort.md index b7486bae8..55f6050cb 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++]; } @@ -370,7 +370,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 4668e3500..8107d1825 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」是一种基于“分治思想”的排序算法,速度很快、应用很广。 快速排序的核心操作为「哨兵划分」,其目标为:选取数组某个元素为 **基准数** ,将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。「哨兵划分」的实现流程为: @@ -339,7 +339,7 @@ comments: true ## 快排为什么快? -从命名能够看出,快速排序在效率方面一定 “有两把刷子” 。快速排序的平均时间复杂度虽然与「归并排序」和「堆排序」一致,但实际 **效率更高** ,这是因为: +从命名能够看出,快速排序在效率方面一定“有两把刷子”。快速排序的平均时间复杂度虽然与「归并排序」和「堆排序」一致,但实际 **效率更高** ,这是因为: - **出现最差情况的概率很低:** 虽然快速排序的最差时间复杂度为 $O(n^2)$ ,不如归并排序,但绝大部分情况下,快速排序可以达到 $O(n \log n)$ 的复杂度。 - **缓存使用效率高:** 哨兵划分操作时,将整个子数组加载入缓存中,访问元素效率很高。而诸如「堆排序」需要跳跃式访问元素,因此不具有此特性。 @@ -351,7 +351,7 @@ comments: true 为了尽量避免这种情况发生,我们可以优化一下基准数的选取策略。首先,在哨兵划分中,我们可以 **随机选取一个元素作为基准数** 。但如果运气很差,每次都选择到比较差的基准数,那么效率依然不好。 -进一步地,我们可以在数组中选取 3 个候选元素(一般为数组的首、尾、中点元素),**并将三个候选元素的中位数作为基准数**,这样基准数 “既不大也不小” 的概率就大大提升了。当然,如果数组很长的话,我们也可以选取更多候选元素,来进一步提升算法的稳健性。采取该方法后,时间复杂度劣化至 $O(n^2)$ 的概率极低。 +进一步地,我们可以在数组中选取 3 个候选元素(一般为数组的首、尾、中点元素),**并将三个候选元素的中位数作为基准数**,这样基准数“既不大也不小”的概率就大大提升了。当然,如果数组很长的话,我们也可以选取更多候选元素,来进一步提升算法的稳健性。采取该方法后,时间复杂度劣化至 $O(n^2)$ 的概率极低。 === "Java" diff --git a/docs/chapter_stack_and_queue/queue.md b/docs/chapter_stack_and_queue/queue.md index 229c6c94a..0bcc90983 100644 --- a/docs/chapter_stack_and_queue/queue.md +++ b/docs/chapter_stack_and_queue/queue.md @@ -144,8 +144,7 @@ comments: true ```js title="queue.js" /* 初始化队列 */ - // JavaScript 没有内置的队列,可以把 Array 当作队列来使用 - // 注意:由于是数组,所以 shift() 的时间复杂度是 O(n) + // JavaScript 没有内置的队列,可以把 Array 当作队列来使用 const queue = []; /* 元素入队 */ @@ -159,7 +158,7 @@ comments: true const peek = queue[0]; /* 元素出队 */ - // O(n) + // 底层是数组,因此 shift() 方法的时间复杂度为 O(n) const poll = queue.shift(); /* 获取队列的长度 */ @@ -174,7 +173,6 @@ comments: true ```typescript title="queue.ts" /* 初始化队列 */ // TypeScript 没有内置的队列,可以把 Array 当作队列来使用 - // 注意:由于是数组,所以 shift() 的时间复杂度是 O(n) const queue: number[] = []; /* 元素入队 */ @@ -188,7 +186,7 @@ comments: true const peek = queue[0]; /* 元素出队 */ - // O(n) + // 底层是数组,因此 shift() 方法的时间复杂度为 O(n) const poll = queue.shift(); /* 获取队列的长度 */ @@ -382,19 +380,16 @@ comments: true // 使用内置包 list 来实现队列 data *list.List } - // NewLinkedListQueue 初始化链表 func NewLinkedListQueue() *LinkedListQueue { return &LinkedListQueue{ data: list.New(), } } - // Offer 入队 func (s *LinkedListQueue) Offer(value any) { s.data.PushBack(value) } - // Poll 出队 func (s *LinkedListQueue) Poll() any { if s.IsEmpty() { @@ -404,7 +399,6 @@ comments: true s.data.Remove(e) return e.Value } - // Peek 访问队首元素 func (s *LinkedListQueue) Peek() any { if s.IsEmpty() { @@ -413,12 +407,10 @@ comments: true e := s.data.Front() return e.Value } - // Size 获取队列的长度 func (s *LinkedListQueue) Size() int { return s.data.Len() } - // IsEmpty 判断队列是否为空 func (s *LinkedListQueue) IsEmpty() bool { return s.data.Len() == 0 @@ -428,13 +420,107 @@ comments: true === "JavaScript" ```js title="linkedlist_queue.js" - + /* 基于链表实现的队列 */ + class LinkedListQueue { + #front; // 头结点 #front + #rear; // 尾结点 #rear + #queSize = 0; + constructor() { + this.#front = null; + this.#rear = null; + } + /* 获取队列的长度 */ + get size() { + return this.#queSize; + } + /* 判断队列是否为空 */ + isEmpty() { + return this.size === 0; + } + /* 入队 */ + offer(num) { + // 尾结点后添加 num + const node = new ListNode(num); + // 如果队列为空,则令头、尾结点都指向该结点 + if (!this.#front) { + this.#front = node; + this.#rear = node; + // 如果队列不为空,则将该结点添加到尾结点后 + } else { + this.#rear.next = node; + this.#rear = node; + } + this.#queSize++; + } + /* 出队 */ + poll() { + const num = this.peek(); + // 删除头结点 + this.#front = this.#front.next; + this.#queSize--; + return num; + } + /* 访问队首元素 */ + peek() { + if (this.size === 0) + throw new Error("队列为空"); + return this.#front.val; + } + } ``` === "TypeScript" ```typescript title="linkedlist_queue.ts" - + /* 基于链表实现的队列 */ + class LinkedListQueue { + private front: ListNode | null; // 头结点 front + private rear: ListNode | null; // 尾结点 rear + private queSize: number = 0; + constructor() { + this.front = null; + this.rear = null; + } + /* 获取队列的长度 */ + get size(): number { + return this.queSize; + } + /* 判断队列是否为空 */ + isEmpty(): boolean { + return this.size === 0; + } + /* 入队 */ + offer(num: number): void { + // 尾结点后添加 num + const node = new ListNode(num); + // 如果队列为空,则令头、尾结点都指向该结点 + if (!this.front) { + this.front = node; + this.rear = node; + // 如果队列不为空,则将该结点添加到尾结点后 + } else { + this.rear!.next = node; + this.rear = node; + } + this.queSize++; + } + /* 出队 */ + poll(): number { + const num = this.peek(); + if (!this.front) + throw new Error("队列为空") + // 删除头结点 + this.front = this.front.next; + this.queSize--; + return num; + } + /* 访问队首元素 */ + peek(): number { + if (this.size === 0) + throw new Error("队列为空"); + return this.front!.val; + } + } ``` === "C" @@ -453,7 +539,7 @@ comments: true 数组的删除首元素的时间复杂度为 $O(n)$ ,因此不适合直接用来实现队列。然而,我们可以借助两个指针 `front` , `rear` 来分别记录队首和队尾的索引位置,在入队 / 出队时分别将 `front` / `rear` 向后移动一位即可,这样每次仅需操作一个元素,时间复杂度降至 $O(1)$ 。 -还有一个问题,在入队与出队的过程中,两个指针都在向后移动,而到达尾部后则无法继续移动了。为了解决此问题,我们可以采取一个取巧方案,即将数组看作是 “环形” 的。具体做法是规定指针越过数组尾部后,再次回到头部接续遍历,这样相当于使数组 “首尾相连” 了。 +还有一个问题,在入队与出队的过程中,两个指针都在向后移动,而到达尾部后则无法继续移动了。为了解决此问题,我们可以采取一个取巧方案,即将数组看作是“环形”的。具体做法是规定指针越过数组尾部后,再次回到头部接续遍历,这样相当于使数组“首尾相连”了。 为了适应环形数组的设定,获取长度 `size()` 、入队 `offer()` 、出队 `poll()` 方法都需要做相应的取余操作处理,使得当尾指针绕回数组头部时,仍然可以正确处理操作。 @@ -506,17 +592,10 @@ comments: true } /* 访问队首元素 */ public int peek() { - // 删除头结点 if (isEmpty()) throw new EmptyStackException(); return nums[front]; } - /* 访问指定索引元素 */ - int get(int index) { - if (index >= size()) - throw new IndexOutOfBoundsException(); - return nums[(front + index) % capacity()]; - } } ``` @@ -570,17 +649,10 @@ comments: true } /* 访问队首元素 */ int peek() { - // 删除头结点 if (empty()) throw out_of_range("队列为空"); return nums[front]; } - /* 访问指定位置元素 */ - int get(int index) { - if (index >= size()) - throw out_of_range("索引越界"); - return nums[(front + index) % capacity()] - } }; ``` @@ -619,7 +691,6 @@ comments: true """ 出队 """ def poll(self): - # 删除头结点 num = self.peek() # 队头指针向后移动一位,若越过尾部则返回到数组头部 self.__front = (self.__front + 1) % self.capacity() @@ -627,19 +698,11 @@ comments: true """ 访问队首元素 """ def peek(self): - # 删除头结点 if self.is_empty(): print("队列为空") 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() @@ -660,7 +723,6 @@ comments: true front int // 头指针,指向队首 rear int // 尾指针,指向队尾 + 1 } - // NewArrayQueue 基于环形数组实现的队列 func NewArrayQueue(capacity int) *ArrayQueue { return &ArrayQueue{ @@ -670,18 +732,15 @@ comments: true rear: 0, } } - // Size 获取队列的长度 func (q *ArrayQueue) Size() int { size := (q.capacity + q.rear - q.front) % q.capacity return size } - // IsEmpty 判断队列是否为空 func (q *ArrayQueue) IsEmpty() bool { return q.rear-q.front == 0 } - // Offer 入队 func (q *ArrayQueue) Offer(v int) { // 当 rear == capacity 表示队列已满 @@ -693,7 +752,6 @@ comments: true // 尾指针向后移动一位,越过尾部后返回到数组头部 q.rear = (q.rear + 1) % q.capacity } - // Poll 出队 func (q *ArrayQueue) Poll() any { if q.IsEmpty() { @@ -704,7 +762,6 @@ comments: true q.front = (q.front + 1) % q.capacity return v } - // Peek 访问队首元素 func (q *ArrayQueue) Peek() any { if q.IsEmpty() { @@ -718,13 +775,100 @@ comments: true === "JavaScript" ```js title="array_queue.js" - + /* 基于环形数组实现的队列 */ + class ArrayQueue { + #queue; // 用于存储队列元素的数组 + #front = 0; // 头指针,指向队首 + #rear = 0; // 尾指针,指向队尾 + 1 + constructor(capacity) { + this.#queue = new Array(capacity); + } + /* 获取队列的容量 */ + get capacity() { + return this.#queue.length; + } + /* 获取队列的长度 */ + get size() { + // 由于将数组看作为环形,可能 rear < front ,因此需要取余数 + return (this.capacity + this.#rear - this.#front) % this.capacity; + } + /* 判断队列是否为空 */ + empty() { + return this.#rear - this.#front == 0; + } + /* 入队 */ + offer(num) { + if (this.size == this.capacity) + throw new Error("队列已满"); + // 尾结点后添加 num + this.#queue[this.#rear] = num; + // 尾指针向后移动一位,越过尾部后返回到数组头部 + this.#rear = (this.#rear + 1) % this.capacity; + } + /* 出队 */ + poll() { + const num = this.peek(); + // 队头指针向后移动一位,若越过尾部则返回到数组头部 + this.#front = (this.#front + 1) % this.capacity; + return num; + } + /* 访问队首元素 */ + peek() { + if (this.empty()) + throw new Error("队列为空"); + return this.#queue[this.#front]; + } + } ``` === "TypeScript" ```typescript title="array_queue.ts" - + /* 基于环形数组实现的队列 */ + class ArrayQueue { + private queue: number[]; // 用于存储队列元素的数组 + private front: number = 0; // 头指针,指向队首 + private rear: number = 0; // 尾指针,指向队尾 + 1 + private CAPACITY: number = 1e5; + constructor(capacity?: number) { + this.queue = new Array(capacity ?? this.CAPACITY); + } + /* 获取队列的容量 */ + get capacity(): number { + return this.queue.length; + } + /* 获取队列的长度 */ + get size(): number { + // 由于将数组看作为环形,可能 rear < front ,因此需要取余数 + return (this.capacity + this.rear - this.front) % this.capacity; + } + /* 判断队列是否为空 */ + empty(): boolean { + return this.rear - this.front == 0; + } + /* 入队 */ + offer(num: number): void { + if (this.size == this.capacity) + throw new Error("队列已满"); + // 尾结点后添加 num + this.queue[this.rear] = num; + // 尾指针向后移动一位,越过尾部后返回到数组头部 + this.rear = (this.rear + 1) % this.capacity; + } + /* 出队 */ + poll(): number { + const num = this.peek(); + // 队头指针向后移动一位,若越过尾部则返回到数组头部 + this.front = (this.front + 1) % this.capacity; + return num; + } + /* 访问队首元素 */ + peek(): number { + if (this.empty()) + throw new Error("队列为空"); + return this.queue[this.front]; + } + } ``` === "C" diff --git a/docs/chapter_stack_and_queue/stack.md b/docs/chapter_stack_and_queue/stack.md index b78d79320..e53e42218 100644 --- a/docs/chapter_stack_and_queue/stack.md +++ b/docs/chapter_stack_and_queue/stack.md @@ -210,7 +210,7 @@ comments: true 为了更加清晰地了解栈的运行机制,接下来我们来自己动手实现一个栈类。 -栈规定元素是先入后出的,因此我们只能在栈顶添加或删除元素。然而,数组或链表都可以在任意位置添加删除元素,因此 **栈可被看作是一种受约束的数组或链表**。换言之,我们可以 “屏蔽” 数组或链表的部分无关操作,使之对外的表现逻辑符合栈的规定即可。 +栈规定元素是先入后出的,因此我们只能在栈顶添加或删除元素。然而,数组或链表都可以在任意位置添加删除元素,因此 **栈可被看作是一种受约束的数组或链表**。换言之,我们可以“屏蔽”数组或链表的部分无关操作,使之对外的表现逻辑符合栈的规定即可。 ### 基于链表的实现 @@ -350,19 +350,16 @@ comments: true // 使用内置包 list 来实现栈 data *list.List } - // NewLinkedListStack 初始化链表 func NewLinkedListStack() *LinkedListStack { return &LinkedListStack{ data: list.New(), } } - // Push 入栈 func (s *LinkedListStack) Push(value int) { s.data.PushBack(value) } - // Pop 出栈 func (s *LinkedListStack) Pop() any { if s.IsEmpty() { @@ -372,7 +369,6 @@ comments: true s.data.Remove(e) return e.Value } - // Peek 访问栈顶元素 func (s *LinkedListStack) Peek() any { if s.IsEmpty() { @@ -381,12 +377,10 @@ comments: true e := s.data.Back() return e.Value } - // Size 获取栈的长度 func (s *LinkedListStack) Size() int { return s.data.Len() } - // IsEmpty 判断栈是否为空 func (s *LinkedListStack) IsEmpty() bool { return s.data.Len() == 0 @@ -457,12 +451,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); - } } ``` @@ -499,12 +487,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]; - } }; ``` @@ -537,11 +519,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" @@ -613,19 +590,16 @@ comments: true } /* 出栈 */ pop() { - if (this.empty()) throw "栈为空"; + if (this.empty()) + throw new Error("栈为空"); return this.stack.pop(); } /* 访问栈顶元素 */ top() { - if (this.empty()) throw "栈为空"; + if (this.empty()) + throw new Error("栈为空"); return this.stack[this.stack.length - 1]; } - /* 访问索引 index 处元素 */ - get(index) { - if (index >= this.size) throw "索引越界"; - return this.stack[index]; - } }; ``` @@ -652,19 +626,16 @@ comments: true } /* 出栈 */ pop(): number | undefined { - if (empty()) throw new Error('栈为空'); + if (this.empty()) + throw new Error('栈为空'); return this.stack.pop(); } /* 访问栈顶元素 */ top(): number | undefined { - if (empty()) throw new Error('栈为空'); + if (this.empty()) + throw new Error('栈为空'); return this.stack[this.stack.length - 1]; } - /* 访问索引 index 处元素 */ - get(index: number): number | undefined { - if (index >= size()) throw new Error('索引越界'); - return this.stack[index]; - } }; ``` diff --git a/docs/chapter_tree/avl_tree.md b/docs/chapter_tree/avl_tree.md index bfa2391de..35397f835 100644 --- a/docs/chapter_tree/avl_tree.md +++ b/docs/chapter_tree/avl_tree.md @@ -226,7 +226,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 ![right_rotate_with_grandchild](avl_tree.assets/right_rotate_with_grandchild.png) -“向右旋转” 是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。 +“向右旋转”是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。 === "Java" @@ -290,7 +290,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 ### Case 2 - 左旋 -类似地,如果将取上述失衡二叉树的 “镜像” ,那么则需要「左旋」操作。观察发现,**「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的**。 +类似地,如果将取上述失衡二叉树的“镜像”,那么则需要「左旋」操作。观察发现,**「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的**。 ![left_rotate_with_grandchild](avl_tree.assets/left_rotate_with_grandchild.png) diff --git a/docs/chapter_tree/binary_search_tree.md b/docs/chapter_tree/binary_search_tree.md index 9ff364329..ff04e0622 100644 --- a/docs/chapter_tree/binary_search_tree.md +++ b/docs/chapter_tree/binary_search_tree.md @@ -164,7 +164,7 @@ comments: true ### 插入结点 -给定一个待插入元素 `num` ,为了保持二叉搜索树 “左子树 < 根结点 < 右子树” 的性质,插入操作分为两步: +给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根结点 < 右子树”的性质,插入操作分为两步: 1. **查找插入位置:** 与查找操作类似,我们从根结点出发,根据当前结点值和 `num` 的大小关系循环向下搜索,直到越过叶结点(遍历到 $\text{null}$ )时跳出循环; 2. **在该位置插入结点:** 初始化结点 `num` ,将该结点放到 $\text{null}$ 的位置 ; @@ -344,7 +344,7 @@ comments: true ### 删除结点 -与插入结点一样,我们需要在删除操作后维持二叉搜索树的 “左子树 < 根结点 < 右子树” 的性质。首先,我们需要在二叉树中执行查找操作,获取待删除结点。接下来,根据待删除结点的子结点数量,删除操作需要分为三种情况: +与插入结点一样,我们需要在删除操作后维持二叉搜索树的“左子树 < 根结点 < 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除结点。接下来,根据待删除结点的子结点数量,删除操作需要分为三种情况: **待删除结点的子结点数量 $= 0$ 。** 表明待删除结点是叶结点,直接删除即可。 @@ -668,7 +668,7 @@ comments: true - **删除元素:** 与无序数组中的情况相同,使用 $O(n)$ 时间; - **获取最小 / 最大元素:** 数组头部和尾部元素即是最小和最大元素,使用 $O(1)$ 时间; -观察发现,无序数组和有序数组中的各类操作的时间复杂度是 “偏科” 的,即有的快有的慢;**而二叉搜索树的各项操作的时间复杂度都是对数阶,在数据量 $n$ 很大时有巨大优势**。 +观察发现,无序数组和有序数组中的各项操作的时间复杂度是“偏科”的,即有的快有的慢;**而二叉搜索树的各项操作的时间复杂度都是对数阶,在数据量 $n$ 很大时有巨大优势**。
@@ -683,7 +683,7 @@ comments: true ## 二叉搜索树的退化 -理想情况下,我们希望二叉搜索树的是 “左右平衡” 的(详见「平衡二叉树」章节),此时可以在 $\log n$ 轮循环内查找任意结点。 +理想情况下,我们希望二叉搜索树的是“左右平衡”的(详见「平衡二叉树」章节),此时可以在 $\log n$ 轮循环内查找任意结点。 如果我们动态地在二叉搜索树中插入与删除结点,**则可能导致二叉树退化为链表**,此时各种操作的时间复杂度也退化之 $O(n)$ 。 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 243a0753f..f5e3b7b9d 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" @@ -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,13 +91,13 @@ comments: true === "C" ```c title="" - + ``` === "C#" ```csharp title="" - + ``` 结点的两个指针分别指向「左子结点 Left Child Node」和「右子结点 Right Child Node」,并且称该结点为两个子结点的「父结点 Parent Node」。给定二叉树某结点,将左子结点以下的树称为该结点的「左子树 Left Subtree」,右子树同理。 @@ -129,27 +129,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 +169,7 @@ comments: true === "Python" ```python title="binary_tree.py" - + ``` === "Go" @@ -247,13 +226,13 @@ comments: true === "C" ```c title="binary_tree.c" - + ``` === "C#" ```csharp title="binary_tree.cs" - + ``` **插入与删除结点。** 与链表类似,插入与删除结点都可以通过修改指针实现。 @@ -288,7 +267,7 @@ comments: true === "Python" ```python title="binary_tree.py" - + ``` === "Go" @@ -330,374 +309,71 @@ comments: true === "C" ```c title="binary_tree.c" - + ``` === "C#" ```csharp title="binary_tree.cs" - + ``` !!! 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$ |
- -=== "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" - - ``` - -=== "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" - - ``` - -!!! note - - 使用循环一样可以实现前、中、后序遍历,但代码相对繁琐,有兴趣的同学可以自行实现。 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 000000000..129d297c6 --- /dev/null +++ b/docs/chapter_tree/binary_tree_traversal.md @@ -0,0 +1,362 @@ +--- +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" + + ``` + +=== "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" + + ``` + +## 前序、中序、后序遍历 + +相对地,前、中、后序遍历皆属于「深度优先遍历 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" + + ``` + +=== "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" + + ``` + +!!! note + + 使用循环一样可以实现前、中、后序遍历,但代码相对繁琐,有兴趣的同学可以自行实现。 diff --git a/docs/chapter_tree/binary_tree_types.md b/docs/chapter_tree/binary_tree_types.md deleted file mode 100644 index b5879b4ec..000000000 --- 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/mkdocs.yml b/mkdocs.yml index 47406412b..4334382f4 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