Merge branch 'krahets:master' into typescript

This commit is contained in:
Daniel 2022-12-22 00:21:21 +11:00 committed by GitHub
commit 96a719bba6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 1163 additions and 688 deletions

View file

@ -4,4 +4,4 @@
- [ ] I've tested the code and ensured the outputs are the same as the outputs of reference codes. - [ ] 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. - [ ] 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).

View file

@ -22,13 +22,13 @@ void merge(vector<int>& nums, int left, int mid, int right) {
int i = leftStart, j = rightStart; int i = leftStart, j = rightStart;
// 通过覆盖原数组 nums 来合并左子数组和右子数组 // 通过覆盖原数组 nums 来合并左子数组和右子数组
for (int k = left; k <= right; k++) { for (int k = left; k <= right; k++) {
// 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if (i > leftEnd) if (i > leftEnd)
nums[k] = tmp[j++]; nums[k] = tmp[j++];
// 否则,若 “右子数组已全部合并完” “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
else if (j > rightEnd || tmp[i] <= tmp[j]) else if (j > rightEnd || tmp[i] <= tmp[j])
nums[k] = tmp[i++]; nums[k] = tmp[i++];
// 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
else else
nums[k] = tmp[j++]; nums[k] = tmp[j++];
} }

View file

@ -59,19 +59,11 @@ public:
/* 访问队首元素 */ /* 访问队首元素 */
int peek() { int peek() {
// 删除头结点
if (empty()) if (empty())
throw out_of_range("队列为空"); throw out_of_range("队列为空");
return nums[front]; return nums[front];
} }
/* 访问指定索引元素 */
int get(int index) {
if (index >= size())
throw out_of_range("索引越界");
return nums[(front + index) % capacity()];
}
/* 将数组转化为 Vector 并返回 */ /* 将数组转化为 Vector 并返回 */
vector<int> toVector() { vector<int> toVector() {
int siz = size(); int siz = size();
@ -104,11 +96,7 @@ int main() {
/* 访问队首元素 */ /* 访问队首元素 */
int peek = queue->peek(); int peek = queue->peek();
cout << "队首元素 peek = " << peek << endl; cout << "队首元素 peek = " << peek << endl;
/* 访问指定索引元素 */
int num = queue->get(2);
cout << "队列第 3 个元素为 num = " << num << endl;
/* 元素出队 */ /* 元素出队 */
int poll = queue->poll(); int poll = queue->poll();
cout << "出队元素 poll = " << poll << ",出队后 queue = "; cout << "出队元素 poll = " << poll << ",出队后 queue = ";

View file

@ -41,13 +41,6 @@ public:
return stack.back(); return stack.back();
} }
/* 访问索引 index 处元素 */
int get(int index) {
if(index >= size())
throw out_of_range("索引越界");
return stack[index];
}
/* 返回 Vector */ /* 返回 Vector */
vector<int> toVector() { vector<int> toVector() {
return stack; return stack;
@ -73,10 +66,6 @@ int main() {
int top = stack->top(); int top = stack->top();
cout << "栈顶元素 top = " << top << endl; cout << "栈顶元素 top = " << top << endl;
/* 访问索引 index 处元素 */
int num = stack->get(3);
cout << "栈索引 3 处的元素为 num = " << num << endl;
/* 元素出栈 */ /* 元素出栈 */
int pop = stack->pop(); int pop = stack->pop();
cout << "出栈元素 pop = " << pop << ",出栈后 stack = "; cout << "出栈元素 pop = " << pop << ",出栈后 stack = ";

View file

@ -21,15 +21,15 @@ func merge(nums []int, left, mid, right int) {
i, j := left_start, right_start i, j := left_start, right_start
// 通过覆盖原数组 nums 来合并左子数组和右子数组 // 通过覆盖原数组 nums 来合并左子数组和右子数组
for k := left; k <= right; k++ { for k := left; k <= right; k++ {
// 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if i > left_end { if i > left_end {
nums[k] = tmp[j] nums[k] = tmp[j]
j++ j++
// 否则,若 “右子数组已全部合并完” “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
} else if j > right_end || tmp[i] <= tmp[j] { } else if j > right_end || tmp[i] <= tmp[j] {
nums[k] = tmp[i] nums[k] = tmp[i]
i++ i++
// 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
} else { } else {
nums[k] = tmp[j] nums[k] = tmp[j]
j++ j++

View file

@ -25,13 +25,13 @@ public class merge_sort {
int i = leftStart, j = rightStart; int i = leftStart, j = rightStart;
// 通过覆盖原数组 nums 来合并左子数组和右子数组 // 通过覆盖原数组 nums 来合并左子数组和右子数组
for (int k = left; k <= right; k++) { for (int k = left; k <= right; k++) {
// 左子数组已全部合并完则选取右子数组元素并且 j++ // 左子数组已全部合并完则选取右子数组元素并且 j++
if (i > leftEnd) if (i > leftEnd)
nums[k] = tmp[j++]; nums[k] = tmp[j++];
// 否则 右子数组已全部合并完 左子数组元素 < 右子数组元素则选取左子数组元素并且 i++ // 否则右子数组已全部合并完左子数组元素 < 右子数组元素则选取左子数组元素并且 i++
else if (j > rightEnd || tmp[i] <= tmp[j]) else if (j > rightEnd || tmp[i] <= tmp[j])
nums[k] = tmp[i++]; nums[k] = tmp[i++];
// 否则 左子数组元素 > 右子数组元素则选取右子数组元素并且 j++ // 否则左子数组元素 > 右子数组元素则选取右子数组元素并且 j++
else else
nums[k] = tmp[j++]; nums[k] = tmp[j++];
} }

View file

@ -58,19 +58,11 @@ class ArrayQueue {
/* 访问队首元素 */ /* 访问队首元素 */
public int peek() { public int peek() {
// 删除头结点
if (isEmpty()) if (isEmpty())
throw new EmptyStackException(); throw new EmptyStackException();
return nums[front]; return nums[front];
} }
/* 访问索引 index 处元素 */
int get(int index) {
if (index >= size())
throw new IndexOutOfBoundsException();
return nums[(front + index) % capacity()];
}
/* 返回数组 */ /* 返回数组 */
public int[] toArray() { public int[] toArray() {
int size = size(); int size = size();

View file

@ -45,13 +45,6 @@ class ArrayStack {
return stack.get(size() - 1); return stack.get(size() - 1);
} }
/* 访问索引 index 处元素 */
public int get(int index) {
if (index >= size())
throw new IndexOutOfBoundsException();
return stack.get(index);
}
/* 将 List 转化为 Array 并返回 */ /* 将 List 转化为 Array 并返回 */
public Object[] toArray() { public Object[] toArray() {
return stack.toArray(); return stack.toArray();
@ -75,10 +68,6 @@ public class array_stack {
int peek = stack.peek(); int peek = stack.peek();
System.out.println("栈顶元素 peek = " + peek); System.out.println("栈顶元素 peek = " + peek);
/* 访问索引 index 处元素 */
int num = stack.get(3);
System.out.println("栈索引 3 处的元素为 num = " + num);
/* 元素出栈 */ /* 元素出栈 */
int pop = stack.pop(); int pop = stack.pop();
System.out.println("出栈元素 pop = " + pop + ",出栈后 stack = " + Arrays.toString(stack.toArray())); System.out.println("出栈元素 pop = " + pop + ",出栈后 stack = " + Arrays.toString(stack.toArray()));

View file

@ -20,13 +20,13 @@ function merge(nums, left, mid, right) {
let i = leftStart, j = rightStart; let i = leftStart, j = rightStart;
// 通过覆盖原数组 nums 来合并左子数组和右子数组 // 通过覆盖原数组 nums 来合并左子数组和右子数组
for (let k = left; k <= right; k++) { for (let k = left; k <= right; k++) {
// 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if (i > leftEnd) { if (i > leftEnd) {
nums[k] = tmp[j++]; nums[k] = tmp[j++];
// 否则,若 “右子数组已全部合并完” “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
} else if (j > rightEnd || tmp[i] <= tmp[j]) { } else if (j > rightEnd || tmp[i] <= tmp[j]) {
nums[k] = tmp[i++]; nums[k] = tmp[i++];
// 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
} else { } else {
nums[k] = tmp[j++]; nums[k] = tmp[j++];
} }

View file

@ -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());
}

View file

@ -11,6 +11,7 @@ class ArrayStack {
constructor() { constructor() {
this.stack = []; this.stack = [];
} }
/* 获取栈的长度 */ /* 获取栈的长度 */
get size() { get size() {
return this.stack.length; return this.stack.length;
@ -28,22 +29,18 @@ class ArrayStack {
/* 出栈 */ /* 出栈 */
pop() { pop() {
if (this.empty()) throw "栈为空"; if (this.empty())
throw new Error("栈为空");
return this.stack.pop(); return this.stack.pop();
} }
/* 访问栈顶元素 */ /* 访问栈顶元素 */
top() { top() {
if (this.empty()) throw "栈为空"; if (this.empty())
throw new Error("栈为空");
return this.stack[this.stack.length - 1]; return this.stack[this.stack.length - 1];
} }
/* 访问索引 index 处元素 */
get(index) {
if (index >= this.size) throw "索引越界";
return this.stack[index];
}
/* 返回 Array */ /* 返回 Array */
toArray() { toArray() {
return this.stack; return this.stack;
@ -69,10 +66,6 @@ console.log(stack.toArray());
const top = stack.top(); const top = stack.top();
console.log("栈顶元素 top = " + top); console.log("栈顶元素 top = " + top);
/* 访问索引 index 处元素 */
const num = stack.get(3);
console.log("栈索引 3 处的元素为 num = " + num);
/* 元素出栈 */ /* 元素出栈 */
const pop = stack.pop(); const pop = stack.pop();
console.log("出栈元素 pop = " + pop + ",出栈后 stack = "); console.log("出栈元素 pop = " + pop + ",出栈后 stack = ");

View file

@ -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);

View file

@ -4,9 +4,10 @@
* Author: S-N-O-R-L-A-X (snorlax.xu@outlook.com) * Author: S-N-O-R-L-A-X (snorlax.xu@outlook.com)
*/ */
/* Driver Code */
/* 初始化队列 */ /* 初始化队列 */
// JavaScript 没有内置的队列,可以把 Array 当作队列来使用 // JavaScript 没有内置的队列,可以把 Array 当作队列来使用
// 注意:由于是数组,所以 shift() 的时间复杂度是 O(n)
const queue = []; const queue = [];
/* 元素入队 */ /* 元素入队 */
@ -20,7 +21,7 @@ queue.push(4);
const peek = queue[0]; const peek = queue[0];
/* 元素出队 */ /* 元素出队 */
// O(n) // 底层是数组,因此 shift() 方法的时间复杂度为 O(n)
const poll = queue.shift(); const poll = queue.shift();
/* 获取队列的长度 */ /* 获取队列的长度 */

View file

@ -4,6 +4,8 @@
* Author: S-N-O-R-L-A-X (snorlax.xu@outlook.com) * Author: S-N-O-R-L-A-X (snorlax.xu@outlook.com)
*/ */
/* Driver Code */
/* 初始化栈 */ /* 初始化栈 */
// Javascript 没有内置的栈类,可以把 Array 当作栈来使用 // Javascript 没有内置的栈类,可以把 Array 当作栈来使用
const stack = []; const stack = [];

View file

@ -24,15 +24,15 @@ def merge(nums, left, mid, right):
i, j = left_start, right_start i, j = left_start, right_start
# 通过覆盖原数组 nums 来合并左子数组和右子数组 # 通过覆盖原数组 nums 来合并左子数组和右子数组
for k in range(left, right + 1): for k in range(left, right + 1):
# 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ # 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if i > left_end: if i > left_end:
nums[k] = tmp[j] nums[k] = tmp[j]
j += 1 j += 1
# 否则,若 “右子数组已全部合并完” “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ # 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
elif j > right_end or tmp[i] <= tmp[j]: elif j > right_end or tmp[i] <= tmp[j]:
nums[k] = tmp[i] nums[k] = tmp[i]
i += 1 i += 1
# 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ # 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
else: else:
nums[k] = tmp[j] nums[k] = tmp[j]
j += 1 j += 1

View file

@ -42,7 +42,6 @@ class ArrayQueue:
""" 出队 """ """ 出队 """
def poll(self): def poll(self):
# 删除头结点
num = self.peek() num = self.peek()
# 队头指针向后移动一位,若越过尾部则返回到数组头部 # 队头指针向后移动一位,若越过尾部则返回到数组头部
self.__front = (self.__front + 1) % self.capacity() self.__front = (self.__front + 1) % self.capacity()
@ -50,19 +49,11 @@ class ArrayQueue:
""" 访问队首元素 """ """ 访问队首元素 """
def peek(self): def peek(self):
# 删除头结点
if self.is_empty(): if self.is_empty():
print("队列为空") print("队列为空")
return False return False
return self.__nums[self.__front] 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): def to_list(self):
res = [0] * self.size() res = [0] * self.size()
@ -90,10 +81,6 @@ if __name__ == "__main__":
peek = queue.peek() peek = queue.peek()
print("队首元素 peek =", peek) print("队首元素 peek =", peek)
""" 访问索引 index 处元素 """
num = queue.get(3)
print("队列索引 3 处的元素为 num =", num)
""" 元素出队 """ """ 元素出队 """
poll = queue.poll() poll = queue.poll()
print("出队元素 poll =", poll) print("出队元素 poll =", poll)

View file

@ -34,11 +34,6 @@ class ArrayStack:
def peek(self): def peek(self):
assert not self.is_empty(), "栈为空" assert not self.is_empty(), "栈为空"
return self.__stack[-1] return self.__stack[-1]
""" 访问索引 index 处元素 """
def get(self, index):
assert index < self.size(), "索引越界"
return self.__stack[index]
""" 返回列表用于打印 """ """ 返回列表用于打印 """
def to_list(self): def to_list(self):
@ -62,10 +57,6 @@ if __name__ == "__main__":
peek = stack.peek() peek = stack.peek()
print("栈顶元素 peek =", peek) print("栈顶元素 peek =", peek)
""" 访问索引 index 处元素 """
num = stack.get(3)
print("栈索引 3 处的元素为 num =", num)
""" 元素出栈 """ """ 元素出栈 """
pop = stack.pop() pop = stack.pop()
print("出栈元素 pop =", pop) print("出栈元素 pop =", pop)

View file

@ -20,13 +20,13 @@ function merge(nums: number[], left: number, mid: number, right: number): void {
let i = leftStart, j = rightStart; let i = leftStart, j = rightStart;
// 通过覆盖原数组 nums 来合并左子数组和右子数组 // 通过覆盖原数组 nums 来合并左子数组和右子数组
for (let k = left; k <= right; k++) { for (let k = left; k <= right; k++) {
// 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if (i > leftEnd) { if (i > leftEnd) {
nums[k] = tmp[j++]; nums[k] = tmp[j++];
// 否则,若 “右子数组已全部合并完” “左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++ // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素”,则选取左子数组元素,并且 i++
} else if (j > rightEnd || tmp[i] <= tmp[j]) { } else if (j > rightEnd || tmp[i] <= tmp[j]) {
nums[k] = tmp[i++]; nums[k] = tmp[i++];
// 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
} else { } else {
nums[k] = tmp[j++]; nums[k] = tmp[j++];
} }

View file

@ -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<number>(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 { };

View file

@ -29,22 +29,18 @@ class ArrayStack {
/* 出栈 */ /* 出栈 */
pop(): number | undefined { pop(): number | undefined {
if (this.empty()) throw new Error('栈为空'); if (this.empty())
throw new Error('栈为空');
return this.stack.pop(); return this.stack.pop();
} }
/* 访问栈顶元素 */ /* 访问栈顶元素 */
top(): number | undefined { top(): number | undefined {
if (this.empty()) throw new Error('栈为空'); if (this.empty())
throw new Error('栈为空');
return this.stack[this.stack.length - 1]; 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 */ /* 返回 Array */
toArray() { toArray() {
return this.stack; return this.stack;
@ -70,10 +66,6 @@ console.log(stack.toArray());
const top = stack.top(); const top = stack.top();
console.log("栈顶元素 top = " + top); console.log("栈顶元素 top = " + top);
/* 访问索引 index 处元素 */
const num = stack.get(3);
console.log("栈索引 3 处的元素为 num = " + num);
/* 元素出栈 */ /* 元素出栈 */
const pop = stack.pop(); const pop = stack.pop();
console.log("出栈元素 pop = " + pop + ",出栈后 stack = "); console.log("出栈元素 pop = " + pop + ",出栈后 stack = ");

View file

@ -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<number>(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);

View file

@ -6,7 +6,6 @@
/* 初始化队列 */ /* 初始化队列 */
// TypeScript 没有内置的队列,可以把 Array 当作队列来使用 // TypeScript 没有内置的队列,可以把 Array 当作队列来使用
// 注意:由于是数组,所以 shift() 的时间复杂度是 O(n)
const queue: number[] = []; const queue: number[] = [];
/* 元素入队 */ /* 元素入队 */
@ -20,7 +19,7 @@ queue.push(4);
const peek = queue[0]; const peek = queue[0];
/* 元素出队 */ /* 元素出队 */
// O(n) // 底层是数组,因此 shift() 方法的时间复杂度为 O(n)
const poll = queue.shift(); const poll = queue.shift();
/* 获取队列的长度 */ /* 获取队列的长度 */

View file

@ -298,7 +298,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
} }
``` ```
**数组中插入或删除元素效率低下。** 假设我们想要在数组中间某位置插入一个元素,由于数组元素在内存中是 “紧挨着的” ,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。删除元素也是类似,需要把此索引之后的元素都向前移动一位。总体看有以下缺点: **数组中插入或删除元素效率低下。** 假设我们想要在数组中间某位置插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。删除元素也是类似,需要把此索引之后的元素都向前移动一位。总体看有以下缺点:
- **时间复杂度高:** 数组的插入和删除的平均时间复杂度均为 $O(N)$ ,其中 $N$ 为数组长度。 - **时间复杂度高:** 数组的插入和删除的平均时间复杂度均为 $O(N)$ ,其中 $N$ 为数组长度。
- **丢失元素:** 由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会被丢失。 - **丢失元素:** 由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会被丢失。
@ -661,6 +661,6 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
**随机访问。** 如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。 **随机访问。** 如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。
**二分查找。** 例如前文查字典的例子,我们可以将字典中的所有字按照拼音顺序存储在数组中,然后使用与日常查纸质字典相同的 “翻开中间,排除一半” 的方式,来实现一个查电子字典的算法。 **二分查找。** 例如前文查字典的例子,我们可以将字典中的所有字按照拼音顺序存储在数组中,然后使用与日常查纸质字典相同的“翻开中间,排除一半”的方式,来实现一个查电子字典的算法。
**深度学习。** 神经网络中大量使用了向量、矩阵、张量之间的线性代数运算,这些数据都是以数组的形式构建的。数组是神经网络编程中最常使用的数据结构。 **深度学习。** 神经网络中大量使用了向量、矩阵、张量之间的线性代数运算,这些数据都是以数组的形式构建的。数组是神经网络编程中最常使用的数据结构。

View file

@ -613,7 +613,7 @@ comments: true
int val; // 结点值 int val; // 结点值
ListNode *next; // 指向后继结点的指针(引用) ListNode *next; // 指向后继结点的指针(引用)
ListNode *prev; // 指向前驱结点的指针(引用) 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; prev;
constructor(val, next) { constructor(val, next) {
this.val = val === undefined ? 0 : val; // 结点值 this.val = val === undefined ? 0 : val; // 结点值
this.next = next === undefined ? null : next; // 指向后继结点的引用 this.next = next === undefined ? null : next; // 指向后继结点的指针(引用
this.prev = prev === undefined ? null : prev; // 指向前驱结点的引用 this.prev = prev === undefined ? null : prev; // 指向前驱结点的指针(引用
} }
} }
``` ```
@ -660,8 +660,8 @@ comments: true
prev: ListNode | null; prev: ListNode | null;
constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) { constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {
this.val = val === undefined ? 0 : val; // 结点值 this.val = val === undefined ? 0 : val; // 结点值
this.next = next === undefined ? null : next; // 指向后继结点的引用 this.next = next === undefined ? null : next; // 指向后继结点的指针(引用
this.prev = prev === undefined ? null : prev; // 指向前驱结点的引用 this.prev = prev === undefined ? null : prev; // 指向前驱结点的指针(引用
} }
} }
``` ```

View file

@ -10,13 +10,15 @@ comments: true
## 列表常用操作 ## 列表常用操作
**初始化列表。** 我们通常使用 `Integer[]` 包装类和 `Arrays.asList()` 作为中转,来初始化一个带有初始值的列表 **初始化列表。** 我们通常会使用到“无初始值”和“有初始值”的两种初始化方法
=== "Java" === "Java"
```java title="list.java" ```java title="list.java"
/* 初始化列表 */ /* 初始化列表 */
// 注意数组的元素类型是 int[] 的包装类 Integer[] // 无初始值
List<Integer> list1 = new ArrayList<>();
// 有初始值(注意数组的元素类型需为 int[] 的包装类 Integer[]
Integer[] numbers = new Integer[] { 1, 3, 2, 5, 4 }; Integer[] numbers = new Integer[] { 1, 3, 2, 5, 4 };
List<Integer> list = new ArrayList<>(Arrays.asList(numbers)); List<Integer> list = new ArrayList<>(Arrays.asList(numbers));
``` ```
@ -25,6 +27,10 @@ comments: true
```cpp title="list.cpp" ```cpp title="list.cpp"
/* 初始化列表 */ /* 初始化列表 */
// 需注意C++ 中 vector 即是本文描述的 list
// 无初始值
vector<int> list1;
// 有初始值
vector<int> list = { 1, 3, 2, 5, 4 }; vector<int> list = { 1, 3, 2, 5, 4 };
``` ```
@ -32,6 +38,9 @@ comments: true
```python title="list.py" ```python title="list.py"
""" 初始化列表 """ """ 初始化列表 """
# 无初始值
list1 = []
# 有初始值
list = [1, 3, 2, 5, 4] list = [1, 3, 2, 5, 4]
``` ```
@ -39,6 +48,9 @@ comments: true
```go title="list_test.go" ```go title="list_test.go"
/* 初始化列表 */ /* 初始化列表 */
// 无初始值
list1 := []int
// 有初始值
list := []int{1, 3, 2, 5, 4} list := []int{1, 3, 2, 5, 4}
``` ```
@ -46,6 +58,9 @@ comments: true
```js title="list.js" ```js title="list.js"
/* 初始化列表 */ /* 初始化列表 */
// 无初始值
const list1 = [];
// 有初始值
const list = [1, 3, 2, 5, 4]; const list = [1, 3, 2, 5, 4];
``` ```
@ -53,6 +68,9 @@ comments: true
```typescript title="list.ts" ```typescript title="list.ts"
/* 初始化列表 */ /* 初始化列表 */
// 无初始值
const list1: number[] = [];
// 有初始值
const list: number[] = [1, 3, 2, 5, 4]; const list: number[] = [1, 3, 2, 5, 4];
``` ```

View file

@ -16,7 +16,7 @@ comments: true
- **时间效率** ,即算法的运行速度的快慢。 - **时间效率** ,即算法的运行速度的快慢。
- **空间效率** ,即算法占用的内存空间大小。 - **空间效率** ,即算法占用的内存空间大小。
数据结构与算法追求 “运行得快、内存占用少” ,而如何去评价算法效率则是非常重要的问题,因为只有知道如何评价算法,才能去做算法之间的对比分析,以及优化算法设计。 数据结构与算法追求“运行得快、内存占用少”,而如何去评价算法效率则是非常重要的问题,因为只有知道如何评价算法,才能去做算法之间的对比分析,以及优化算法设计。
## 效率评估方法 ## 效率评估方法
@ -38,6 +38,6 @@ comments: true
## 复杂度分析的重要性 ## 复杂度分析的重要性
复杂度分析给出一把评价算法效率的 “标尺” ,告诉我们执行某个算法需要多少时间和空间资源,也让我们可以开展不同算法之间的效率对比。 复杂度分析给出一把评价算法效率的“标尺”,告诉我们执行某个算法需要多少时间和空间资源,也让我们可以开展不同算法之间的效率对比。
计算复杂度是个数学概念,对于初学者可能比较抽象,学习难度相对较高。从这个角度出发,其并不适合作为第一章内容。但是,当我们讨论某个数据结构或者算法的特点时,难以避免需要分析它的运行速度和空间使用情况。**因此,在展开学习数据结构与算法之前,建议读者先对计算复杂度建立起初步的了解,并且能够完成简单案例的复杂度分析**。 计算复杂度是个数学概念,对于初学者可能比较抽象,学习难度相对较高。从这个角度出发,其并不适合作为第一章内容。但是,当我们讨论某个数据结构或者算法的特点时,难以避免需要分析它的运行速度和空间使用情况。**因此,在展开学习数据结构与算法之前,建议读者先对计算复杂度建立起初步的了解,并且能够完成简单案例的复杂度分析**。

View file

@ -154,9 +154,9 @@ comments: true
## 推算方法 ## 推算方法
空间复杂度的推算方法和时间复杂度总体类似,只是从统计 “计算操作数量” 变为统计 “使用空间大小” 。与时间复杂度不同的是,**我们一般只关注「最差空间复杂度」**。这是因为内存空间是一个硬性要求,我们必须保证在所有输入数据下都有足够的内存空间预留。 空间复杂度的推算方法和时间复杂度总体类似,只是从统计“计算操作数量”变为统计“使用空间大小”。与时间复杂度不同的是,**我们一般只关注「最差空间复杂度」**。这是因为内存空间是一个硬性要求,我们必须保证在所有输入数据下都有足够的内存空间预留。
**最差空间复杂度中的 “最差” 有两层含义**,分别为输入数据的最差分布、算法运行中的最差时间点。 **最差空间复杂度中的“最差”有两层含义**,分别为输入数据的最差分布、算法运行中的最差时间点。
- **以最差输入数据为准。** 当 $n < 10$ 空间复杂度为 $O(1)$ 但是当 $n > 10$ 时,初始化的数组 `nums` 使用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$ - **以最差输入数据为准。** 当 $n < 10$ 空间复杂度为 $O(1)$ 但是当 $n > 10$ 时,初始化的数组 `nums` 使用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$
- **以算法运行过程中的峰值内存为准。** 程序在执行最后一行之前,使用 $O(1)$ 空间;当初始化数组 `nums` 时,程序使用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$ - **以算法运行过程中的峰值内存为准。** 程序在执行最后一行之前,使用 $O(1)$ 空间;当初始化数组 `nums` 时,程序使用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$

View file

@ -23,6 +23,6 @@ comments: true
- 与时间复杂度的定义类似,「空间复杂度」统计算法占用空间随着数据量变大时的增长趋势。 - 与时间复杂度的定义类似,「空间复杂度」统计算法占用空间随着数据量变大时的增长趋势。
- 算法运行中相关内存空间可分为输入空间、暂存空间、输出空间。通常情况下,空间复杂度不计入输入空间。暂存空间可分为指令空间、数据空间、栈帧空间,其中栈帧空间一般在递归函数中才会影响到空间复杂度。 - 算法运行中相关内存空间可分为输入空间、暂存空间、输出空间。通常情况下,输入空间不计入空间复杂度计算。暂存空间可分为指令空间、数据空间、栈帧空间,其中栈帧空间一般在递归函数中才会影响到空间复杂度。
- 我们一般只关心「最差空间复杂度」,即统计算法在「最差输入数据」和「最差运行时间点」下的空间复杂度。 - 我们一般只关心「最差空间复杂度」,即统计算法在「最差输入数据」和「最差运行时间点」下的空间复杂度。
- 常见空间复杂度从小到大排列有 $O(1)$ , $O(\log n)$ , $O(n)$ , $O(n^2)$ , $O(2^n)$ 。 - 常见空间复杂度从小到大排列有 $O(1)$ , $O(\log n)$ , $O(n)$ , $O(n^2)$ , $O(2^n)$ 。

View file

@ -106,7 +106,7 @@ $$
「时间复杂度分析」采取了不同的做法,其统计的不是算法运行时间,而是 **算法运行时间随着数据量变大时的增长趋势** 「时间复杂度分析」采取了不同的做法,其统计的不是算法运行时间,而是 **算法运行时间随着数据量变大时的增长趋势**
“时间增长趋势” 这个概念比较抽象,我们借助一个例子来理解。设输入数据大小为 $n$ ,给定三个算法 `A` , `B` , `C` “时间增长趋势”这个概念比较抽象,我们借助一个例子来理解。设输入数据大小为 $n$ ,给定三个算法 `A` , `B` , `C`
- 算法 `A` 只有 $1$ 个打印操作,算法运行时间不随着 $n$ 增大而增长。我们称此算法的时间复杂度为「常数阶」。 - 算法 `A` 只有 $1$ 个打印操作,算法运行时间不随着 $n$ 增大而增长。我们称此算法的时间复杂度为「常数阶」。
- 算法 `B` 中的打印操作需要循环 $n$ 次,算法运行时间随着 $n$ 增大成线性增长。此算法的时间复杂度被称为「线性阶」。 - 算法 `B` 中的打印操作需要循环 $n$ 次,算法运行时间随着 $n$ 增大成线性增长。此算法的时间复杂度被称为「线性阶」。
@ -223,7 +223,7 @@ $$
**时间复杂度可以有效评估算法效率。** 算法 `B` 运行时间的增长是线性的,在 $n > 1$ 时慢于算法 `A` ,在 $n > 1000000$ 时慢于算法 `C` 。实质上,只要输入数据大小 $n$ 足够大,复杂度为「常数阶」的算法一定优于「线性阶」的算法,这也正是时间增长趋势的含义。 **时间复杂度可以有效评估算法效率。** 算法 `B` 运行时间的增长是线性的,在 $n > 1$ 时慢于算法 `A` ,在 $n > 1000000$ 时慢于算法 `C` 。实质上,只要输入数据大小 $n$ 足够大,复杂度为「常数阶」的算法一定优于「线性阶」的算法,这也正是时间增长趋势的含义。
**时间复杂度分析将统计「计算操作的运行时间」简化为统计「计算操作的数量」。** 这是因为,无论是运行平台、还是计算操作类型,都与算法运行时间的增长趋势无关。因此,我们可以简单地将所有计算操作的执行时间统一看作是相同的 “单位时间” **时间复杂度分析将统计「计算操作的运行时间」简化为统计「计算操作的数量」。** 这是因为,无论是运行平台、还是计算操作类型,都与算法运行时间的增长趋势无关。因此,我们可以简单地将所有计算操作的执行时间统一看作是相同的“单位时间”。
**时间复杂度也存在一定的局限性。** 比如,虽然算法 `A``C` 的时间复杂度相同,但是实际的运行时间有非常大的差别。再比如,虽然算法 `B``C` 的时间复杂度要更高,但在输入数据大小 $n$ 比较小时,算法 `B` 是要明显优于算法 `C` 的。即使存在这些问题,计算复杂度仍然是评判算法效率的最有效、最常用方法。 **时间复杂度也存在一定的局限性。** 比如,虽然算法 `A``C` 的时间复杂度相同,但是实际的运行时间有非常大的差别。再比如,虽然算法 `B``C` 的时间复杂度要更高,但在输入数据大小 $n$ 比较小时,算法 `B` 是要明显优于算法 `C` 的。即使存在这些问题,计算复杂度仍然是评判算法效率的最有效、最常用方法。
@ -464,7 +464,7 @@ $$
**时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将处于主导作用,其它项的影响都可以被忽略。 **时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将处于主导作用,其它项的影响都可以被忽略。
以下表格给出了一些例子,其中有一些夸张的值,是想要向大家强调 **系数无法撼动阶数** 这一结论。在 $n$ 趋于无穷大时,这些常数都是 “浮云” 以下表格给出了一些例子,其中有一些夸张的值,是想要向大家强调 **系数无法撼动阶数** 这一结论。在 $n$ 趋于无穷大时,这些常数都是“浮云”。
<div class="center-table" markdown> <div class="center-table" markdown>
@ -954,7 +954,7 @@ $$
!!! note !!! note
生物学科中的 “细胞分裂” 即是指数阶增长:初始状态为 $1$ 个细胞,分裂一轮后为 $2$ 个,分裂两轮后为 $4$ 个,……,分裂 $n$ 轮后有 $2^n$ 个细胞。 生物学科中的“细胞分裂”即是指数阶增长:初始状态为 $1$ 个细胞,分裂一轮后为 $2$ 个,分裂两轮后为 $4$ 个,……,分裂 $n$ 轮后有 $2^n$ 个细胞。
指数阶增长得非常快,在实际应用中一般是不能被接受的。若一个问题使用「暴力枚举」求解的时间复杂度是 $O(2^n)$ ,那么一般都需要使用「动态规划」或「贪心算法」等算法来求解。 指数阶增长得非常快,在实际应用中一般是不能被接受的。若一个问题使用「暴力枚举」求解的时间复杂度是 $O(2^n)$ ,那么一般都需要使用「动态规划」或「贪心算法」等算法来求解。
@ -1124,9 +1124,9 @@ $$
### 对数阶 $O(\log n)$ ### 对数阶 $O(\log n)$
对数阶与指数阶正好相反,后者反映 “每轮增加到两倍的情况” ,而前者反映 “每轮缩减到一半的情况” 。对数阶仅次于常数阶,时间增长的很慢,是理想的时间复杂度。 对数阶与指数阶正好相反,后者反映“每轮增加到两倍的情况”,而前者反映“每轮缩减到一半的情况”。对数阶仅次于常数阶,时间增长的很慢,是理想的时间复杂度。
对数阶常出现于「二分查找」和「分治算法」中,体现 “一分为多” 、“化繁为简” 的算法思想。 对数阶常出现于「二分查找」和「分治算法」中,体现“一分为多”、“化繁为简”的算法思想。
设输入数据大小为 $n$ ,由于每轮缩减到一半,因此循环次数是 $\log_2 n$ ,即 $2^n$ 的反函数。 设输入数据大小为 $n$ ,由于每轮缩减到一半,因此循环次数是 $\log_2 n$ ,即 $2^n$ 的反函数。
@ -1657,9 +1657,9 @@ $$
!!! tip !!! tip
我们在实际应用中很少使用「最佳时间复杂度」,因为往往只有很小概率下才能达到,会带来一定的误导性。反之,「最差时间复杂度」最为实用,因为它给出了一个 “效率安全值” ,让我们可以放心地使用算法。 我们在实际应用中很少使用「最佳时间复杂度」,因为往往只有很小概率下才能达到,会带来一定的误导性。反之,「最差时间复杂度」最为实用,因为它给出了一个“效率安全值”,让我们可以放心地使用算法。
从上述示例可以看出,最差或最佳时间复杂度只出现在 “特殊分布的数据” 中,这些情况的出现概率往往很小,因此并不能最真实地反映算法运行效率。**相对地,「平均时间复杂度」可以体现算法在随机输入数据下的运行效率,用 $\Theta$ 记号Theta Notation来表示**。 从上述示例可以看出,最差或最佳时间复杂度只出现在“特殊分布的数据”中,这些情况的出现概率往往很小,因此并不能最真实地反映算法运行效率。**相对地,「平均时间复杂度」可以体现算法在随机输入数据下的运行效率,用 $\Theta$ 记号Theta Notation来表示**。
对于部分算法,我们可以简单地推算出随机数据分布下的平均情况。比如上述示例,由于输入数组是被打乱的,因此元素 $1$ 出现在任意索引的概率都是相等的,那么算法的平均循环次数则是数组长度的一半 $\frac{n}{2}$ ,平均时间复杂度为 $\Theta(\frac{n}{2}) = \Theta(n)$ 。 对于部分算法,我们可以简单地推算出随机数据分布下的平均情况。比如上述示例,由于输入数组是被打乱的,因此元素 $1$ 出现在任意索引的概率都是相等的,那么算法的平均循环次数则是数组长度的一半 $\frac{n}{2}$ ,平均时间复杂度为 $\Theta(\frac{n}{2}) = \Theta(n)$ 。

View file

@ -10,7 +10,7 @@ comments: true
**「逻辑结构」反映了数据之间的逻辑关系。** 数组和链表的数据按照顺序依次排列,反映了数据间的线性关系;树从顶至底按层级排列,反映了祖先与后代之间的派生关系;图由结点和边组成,反映了复杂网络关系。 **「逻辑结构」反映了数据之间的逻辑关系。** 数组和链表的数据按照顺序依次排列,反映了数据间的线性关系;树从顶至底按层级排列,反映了祖先与后代之间的派生关系;图由结点和边组成,反映了复杂网络关系。
我们一般将逻辑结构分为「线性」和「非线性」两种。“线性” 这个概念很直观,即表明数据在逻辑关系上是排成一条线的;而如果数据之间的逻辑关系是非线形的(例如是网状或树状的),那么就是非线性数据结构。 我们一般将逻辑结构分为「线性」和「非线性」两种。“线性”这个概念很直观,即表明数据在逻辑关系上是排成一条线的;而如果数据之间的逻辑关系是非线形的(例如是网状或树状的),那么就是非线性数据结构。
- **线性数据结构:** 数组、链表、栈、队列、哈希表; - **线性数据结构:** 数组、链表、栈、队列、哈希表;
- **非线性数据结构:** 树、图、堆、哈希表; - **非线性数据结构:** 树、图、堆、哈希表;
@ -40,4 +40,4 @@ comments: true
!!! tip !!! tip
数组与链表是其他所有数据结构的 “底层积木”,建议读者一定要多花些时间了解。 数组与链表是其他所有数据结构的“底层积木”,建议读者一定要多花些时间了解。

View file

@ -42,7 +42,7 @@ comments: true
**「基本数据类型」与「数据结构」之间的联系与区别** **「基本数据类型」与「数据结构」之间的联系与区别**
我们知道,数据结构是在计算机中 **组织与存储数据的方式** ,它的主语是 “结构” ,而不是 “数据” 。比如,我们想要表示 “一排数字” ,自然应该使用「数组」这个数据结构。数组的存储方式使之可以表示数字的相邻关系、先后关系等一系列我们需要的信息,但至于其中存储的是整数 int ,还是小数 float ,或是字符 char **则与所谓的数据的结构无关了**。 我们知道,数据结构是在计算机中 **组织与存储数据的方式** ,它的主语是“结构”,而不是“数据”。比如,我们想要表示“一排数字”,自然应该使用「数组」这个数据结构。数组的存储方式使之可以表示数字的相邻关系、先后关系等一系列我们需要的信息,但至于其中存储的是整数 int ,还是小数 float ,或是字符 char **则与所谓的数据的结构无关了**。
=== "Java" === "Java"

View file

@ -40,7 +40,7 @@ comments: true
## 开放寻址 ## 开放寻址
「开放寻址」不引入额外数据结构,而是通过 “向后探测” 来解决哈希冲突。根据探测方法的不同,主要分为 **线性探测、平方探测、多次哈希** 「开放寻址」不引入额外数据结构,而是通过“向后探测”来解决哈希冲突。根据探测方法的不同,主要分为 **线性探测、平方探测、多次哈希**
### 线性探测 ### 线性探测
@ -58,7 +58,7 @@ comments: true
线性探测有以下缺陷: 线性探测有以下缺陷:
- **不能直接删除元素**。删除元素会导致桶内出现一个空位,在查找其他元素时,该空位有可能导致程序认为元素不存在(即上述第 `2.` 种情况)。因此需要借助一个标志位来标记删除元素。 - **不能直接删除元素**。删除元素会导致桶内出现一个空位,在查找其他元素时,该空位有可能导致程序认为元素不存在(即上述第 `2.` 种情况)。因此需要借助一个标志位来标记删除元素。
- **容易产生聚集**。桶内被占用的连续位置越长,这些连续位置发生哈希冲突的可能性越大,从而进一步促进这一位置的 “聚堆生长” ,最终导致增删查改操作效率的劣化。 - **容易产生聚集**。桶内被占用的连续位置越长,这些连续位置发生哈希冲突的可能性越大,从而进一步促进这一位置的“聚堆生长”,最终导致增删查改操作效率的劣化。
### 多次哈希 ### 多次哈希

View file

@ -6,7 +6,7 @@ comments: true
哈希表通过建立「键 key」和「值 value」之间的映射实现高效的元素查找。具体地输入一个 key ,在哈希表中查询并获取 value ,时间复杂度为 $O(1)$ 。 哈希表通过建立「键 key」和「值 value」之间的映射实现高效的元素查找。具体地输入一个 key ,在哈希表中查询并获取 value ,时间复杂度为 $O(1)$ 。
例如,给定一个包含 $n$ 个学生的数据库,每个学生有 "姓名 `name` ” 和 “学号 `id`两项数据,希望实现一个查询功能:**输入一个学号,返回对应的姓名**,则可以使用哈希表实现。 例如,给定一个包含 $n$ 个学生的数据库,每个学生有“姓名 `name` ”和“学号 `id`两项数据,希望实现一个查询功能:**输入一个学号,返回对应的姓名**,则可以使用哈希表实现。
![hash_map](hash_map.assets/hash_map.png) ![hash_map](hash_map.assets/hash_map.png)
@ -623,4 +623,4 @@ $$
- 尽量少地发生哈希冲突; - 尽量少地发生哈希冲突;
- 时间复杂度 $O(1)$ ,计算尽可能高效; - 时间复杂度 $O(1)$ ,计算尽可能高效;
- 空间使用率高,即 “键值对占用空间 / 哈希表总占用空间” 尽可能大; - 空间使用率高,即“键值对占用空间 / 哈希表总占用空间”尽可能大;

View file

@ -4,7 +4,7 @@ comments: true
# 算法无处不在 # 算法无处不在
听到 “算法” 这个词,我们一般会联想到数学。但实际上,大多数算法并不包含复杂的数学,而更像是在考察基本逻辑,而这些逻辑在我们日常生活中处处可见。 听到“算法”这个词,我们一般会联想到数学。但实际上,大多数算法并不包含复杂的数学,而更像是在考察基本逻辑,而这些逻辑在我们日常生活中处处可见。
在正式介绍算法之前,我想告诉你一件有趣的事:**其实,你在过去已经学会了很多算法,并且已经习惯将它们应用到日常生活中。** 接下来,我将介绍两个具体例子来佐证。 在正式介绍算法之前,我想告诉你一件有趣的事:**其实,你在过去已经学会了很多算法,并且已经习惯将它们应用到日常生活中。** 接下来,我将介绍两个具体例子来佐证。

View file

@ -4,11 +4,11 @@ comments: true
# 关于本书 # 关于本书
五年前发生的一件事,成为了我职业生涯的重要转折点。当时的我在交大读研,对互联网求职一无所知,但仍然硬着头皮申请了 Microsoft 软件工程师实习。面试官让我在白板上写出 “快速排序” 代码,我畏畏缩缩地写了一个 “冒泡排序” ,并且还写错了` (ToT) ` 。从面试官的表情上,我看到了一个大大的 "GG" 。 五年前发生的一件事,成为了我职业生涯的重要转折点。当时的我在交大读研,对互联网求职一无所知,但仍然硬着头皮申请了 Microsoft 软件工程师实习。面试官让我在白板上写出“快速排序”代码,我畏畏缩缩地写了一个“冒泡排序”,并且还写错了` (ToT) ` 。从面试官的表情上,我看到了一个大大的 "GG" 。
此次失利倒逼我开始刷算法题。我采用 “扫雷游戏” 式的学习方法,两眼一抹黑刷题,扫到不会的 “雷” 就通过查资料把它 “排掉” ,配合周期性总结,逐渐形成了数据结构与算法的知识图景。幸运地,我在秋招斩获了多家大厂的 Offer 。 此次失利倒逼我开始刷算法题。我采用“扫雷游戏”式的学习方法,两眼一抹黑刷题,扫到不会的“雷”就通过查资料把它“排掉”,配合周期性总结,逐渐形成了数据结构与算法的知识图景。幸运地,我在秋招斩获了多家大厂的 Offer 。
回想自己当初在 “扫雷式” 刷题中被炸的满头包的痛苦,思考良久,我意识到一本 “前期刷题必看” 的读物可以使算法小白少走许多弯路。写作意愿滚滚袭来,那就动笔吧: 回想自己当初在“扫雷式”刷题中被炸的满头包的痛苦,思考良久,我意识到一本“前期刷题必看”的读物可以使算法小白少走许多弯路。写作意愿滚滚袭来,那就动笔吧:
<h4 align="center"> Hello算法 </h4> <h4 align="center"> Hello算法 </h4>
@ -28,7 +28,7 @@ comments: true
- 本书篇幅不长,可以帮助你提纲挈领地回顾算法知识。 - 本书篇幅不长,可以帮助你提纲挈领地回顾算法知识。
- 书中包含许多对比性、总结性的算法内容,可以帮助你梳理算法知识体系。 - 书中包含许多对比性、总结性的算法内容,可以帮助你梳理算法知识体系。
- 源代码实现了各种经典数据结构和算法,可以作为 “刷题工具库” 来使用。 - 源代码实现了各种经典数据结构和算法,可以作为“刷题工具库”来使用。
如果您是 **算法大佬**,请受我膜拜!希望您可以抽时间提出意见建议,或者[一起参与创作](https://www.hello-algo.com/chapter_preface/contribution/),帮助各位同学获取更好的学习内容,感谢! 如果您是 **算法大佬**,请受我膜拜!希望您可以抽时间提出意见建议,或者[一起参与创作](https://www.hello-algo.com/chapter_preface/contribution/),帮助各位同学获取更好的学习内容,感谢!
@ -99,15 +99,15 @@ comments: true
**视觉化学习。** 信息时代以来,视觉化的脚步从未停止。媒体形式经历了文字短信、图文 Email 、动图、短(长)视频、交互式 Web 、3D 游戏等演变过程信息的视觉化程度越来越高、愈加符合人类感官、信息传播效率大大提升。科技界也在向视觉化迈进iPhone 就是一个典型例子,其相对于传统手机是高度视觉化的,包含精心设计的字体、主题配色、交互动画等。 **视觉化学习。** 信息时代以来,视觉化的脚步从未停止。媒体形式经历了文字短信、图文 Email 、动图、短(长)视频、交互式 Web 、3D 游戏等演变过程信息的视觉化程度越来越高、愈加符合人类感官、信息传播效率大大提升。科技界也在向视觉化迈进iPhone 就是一个典型例子,其相对于传统手机是高度视觉化的,包含精心设计的字体、主题配色、交互动画等。
近两年,短视频成为最受欢迎的信息媒介,可以在短时间内将高密度的信息 “灌” 给我们,有着极其舒适的观看体验。阅读则不然,读者与书本之间天然存在一种 “疏离感”,我们看书会累、会走神、会停下来想其他事、会划下喜欢的句子、会思考某一片段的含义,这种疏离感给了读者与书本之间对话的可能,拓宽了想象空间。 近两年,短视频成为最受欢迎的信息媒介,可以在短时间内将高密度的信息“灌”给我们,有着极其舒适的观看体验。阅读则不然,读者与书本之间天然存在一种“疏离感”,我们看书会累、会走神、会停下来想其他事、会划下喜欢的句子、会思考某一片段的含义,这种疏离感给了读者与书本之间对话的可能,拓宽了想象空间。
本书作为一本入门教材,希望可以保有书本的 “慢节奏” ,但也会避免与读者产生过多 “疏离感” ,而是努力将知识完整清晰地推送到你聪明的小脑袋瓜中。我将采用视觉化的方式(例如配图、动画),尽我可能清晰易懂地讲解复杂概念和抽象示例。 本书作为一本入门教材,希望可以保有书本的“慢节奏”,但也会避免与读者产生过多“疏离感”,而是努力将知识完整清晰地推送到你聪明的小脑袋瓜中。我将采用视觉化的方式(例如配图、动画),尽我可能清晰易懂地讲解复杂概念和抽象示例。
**内容精简化。** 大多数的经典教科书,会把每个主题都讲的很透彻。虽然透彻性正是其获得读者青睐的原因,但对于想要快速入门的初学者来说,这些教材的实用性不足。本书会避免引入非必要的概念、名词、定义等,也避免展开不必要的理论分析,毕竟这不是一本真正意义上的教材,主要任务是尽快地带领读者入门。 **内容精简化。** 大多数的经典教科书,会把每个主题都讲的很透彻。虽然透彻性正是其获得读者青睐的原因,但对于想要快速入门的初学者来说,这些教材的实用性不足。本书会避免引入非必要的概念、名词、定义等,也避免展开不必要的理论分析,毕竟这不是一本真正意义上的教材,主要任务是尽快地带领读者入门。
引入一些生活案例或趣味内容,非常适合作为知识点的引子或者解释的补充,但当融入过多额外元素时,内容会稍显冗长,也许反而使读者容易迷失、抓不住重点,这也是本书需要避免的。 引入一些生活案例或趣味内容,非常适合作为知识点的引子或者解释的补充,但当融入过多额外元素时,内容会稍显冗长,也许反而使读者容易迷失、抓不住重点,这也是本书需要避免的。
敲代码如同写字,“美” 是统一的追求。本书力求美观的代码,保证规范的变量命名、统一的空格与换行、对齐的缩进、整齐的注释等。 敲代码如同写字,“美”是统一的追求。本书力求美观的代码,保证规范的变量命名、统一的空格与换行、对齐的缩进、整齐的注释等。
## 致谢 ## 致谢
@ -115,7 +115,7 @@ comments: true
- 感谢我的女朋友泡泡担任本书的首位读者,从算法小白的视角为本书的写作提出了许多建议,使这本书更加适合算法初学者来阅读。 - 感谢我的女朋友泡泡担任本书的首位读者,从算法小白的视角为本书的写作提出了许多建议,使这本书更加适合算法初学者来阅读。
- 感谢腾宝、琦宝、飞宝为本书起了个响当当的名字,好听又有梗,直接唤起我最初敲下第一行代码 "Hello, World!" 的回忆。 - 感谢腾宝、琦宝、飞宝为本书起了个响当当的名字,好听又有梗,直接唤起我最初敲下第一行代码 "Hello, World!" 的回忆。
- 感谢我的导师李博,在小酌畅谈时您告诉我 “觉得适合、想做就去做” ,坚定了我写这本书的决心。 - 感谢我的导师李博,在小酌畅谈时您告诉我“觉得适合、想做就去做”,坚定了我写这本书的决心。
- 感谢苏潼为本书设计了封面和 LOGO ,我有些强迫症,前后多次修改,谢谢你的耐心。 - 感谢苏潼为本书设计了封面和 LOGO ,我有些强迫症,前后多次修改,谢谢你的耐心。
- 感谢 @squidfunk ,包括 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) 顶级开源项目以及给出的写作排版建议。 - 感谢 @squidfunk ,包括 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) 顶级开源项目以及给出的写作排版建议。

View file

@ -14,10 +14,10 @@ comments: true
每个页面的右上角都有一个「编辑」按钮,你可以按照以下步骤修改文章: 每个页面的右上角都有一个「编辑」按钮,你可以按照以下步骤修改文章:
1. 点击编辑按钮,如果遇到提示 “需要 Fork 此仓库” ,请通过; 1. 点击编辑按钮,如果遇到提示“需要 Fork 此仓库”,请通过;
2. 修改 Markdown 源文件内容; 2. 修改 Markdown 源文件内容;
3. 在页面底部填写更改说明,然后单击 “Propose file change” 按钮; 3. 在页面底部填写更改说明然后单击“Propose file change”按钮
4. 页面跳转后,点击 “Create pull request” 按钮发起拉取请求即可,我会第一时间查看处理并及时更新内容。 4. 页面跳转后点击“Create pull request”按钮发起拉取请求即可我会第一时间查看处理并及时更新内容。
![edit_markdown](contribution.assets/edit_markdown.png) ![edit_markdown](contribution.assets/edit_markdown.png)
@ -37,7 +37,7 @@ comments: true
2. 进入 Fork 仓库网页,使用 `git clone` 克隆该仓库至本地; 2. 进入 Fork 仓库网页,使用 `git clone` 克隆该仓库至本地;
3. 在本地进行内容创作(建议通过运行测试来验证代码正确性); 3. 在本地进行内容创作(建议通过运行测试来验证代码正确性);
4. 将本地更改 Commit ,并 Push 至远程仓库; 4. 将本地更改 Commit ,并 Push 至远程仓库;
5. 刷新仓库网页,点击 “Create pull request” 按钮发起拉取请求Pull Request即可 5. 刷新仓库网页点击“Create pull request”按钮发起拉取请求Pull Request即可
非常欢迎您和我一同来创作本书! 非常欢迎您和我一同来创作本书!

View file

@ -26,7 +26,7 @@ comments: true
git clone https://github.com/krahets/hello-algo.git git clone https://github.com/krahets/hello-algo.git
``` ```
当然,你也可以点击 “Download ZIP” 直接下载代码压缩包,解压即可。 当然你也可以点击“Download ZIP”直接下载代码压缩包解压即可。
![download_code](suggestions.assets/download_code.png) ![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) ![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 道题量,熟悉大多数的算法问题。刚开始刷题时,“遗忘”是最大的困扰点,但这是很正常的,请不要担心。学习中有一种概念叫“周期性回顾”,同一道题隔段时间做一次,当做了三遍以上,往往就能牢记于心了。
**第三阶段,搭建知识体系。** 在学习方面,可以阅读算法专栏文章、解题框架、算法教材,不断地丰富知识体系。在刷题方面,可以开始采用进阶刷题方案,例如按专题分类、一题多解、一解多题等,刷题方案在社区中可以找到一些讲解,在此不做赘述。 **第三阶段,搭建知识体系。** 在学习方面,可以阅读算法专栏文章、解题框架、算法教材,不断地丰富知识体系。在刷题方面,可以开始采用进阶刷题方案,例如按专题分类、一题多解、一解多题等,刷题方案在社区中可以找到一些讲解,在此不做赘述。

View file

@ -24,9 +24,9 @@ $$
1. **双闭区间 $[0, n-1]$** ,即两个边界都包含自身;此方法下,区间 $[0, 0]$ 仍包含一个元素; 1. **双闭区间 $[0, n-1]$** ,即两个边界都包含自身;此方法下,区间 $[0, 0]$ 仍包含一个元素;
2. **左闭右开 $[0, n)$** ,即左边界包含自身、右边界不包含自身;此方法下,区间 $[0, 0)$ 为空; 2. **左闭右开 $[0, n)$** ,即左边界包含自身、右边界不包含自身;此方法下,区间 $[0, 0)$ 为空;
### “双闭区间” 实现 ### “双闭区间”实现
首先,我们先采用 “双闭区间” 的表示,在数组 `nums` 中查找目标元素 `target` 的对应索引。 首先,我们先采用“双闭区间”的表示,在数组 `nums` 中查找目标元素 `target` 的对应索引。
=== "Step 1" === "Step 1"
@ -56,7 +56,7 @@ $$
![binary_search_step7](binary_search.assets/binary_search_step7.png) ![binary_search_step7](binary_search.assets/binary_search_step7.png)
二分查找 “双闭区间” 表示下的代码如下所示。 二分查找“双闭区间”表示下的代码如下所示。
=== "Java" === "Java"
@ -167,9 +167,9 @@ $$
``` ```
### “左闭右开” 实现 ### “左闭右开”实现
当然,我们也可以使用 “左闭右开” 的表示方法,写出相同功能的二分查找代码。 当然,我们也可以使用“左闭右开”的表示方法,写出相同功能的二分查找代码。
=== "Java" === "Java"
@ -294,7 +294,7 @@ $$
</div> </div>
观察发现,在 “双闭区间” 表示中,由于对左右两边界的定义是相同的,因此缩小区间的 $i$ , $j$ 处理方法也是对称的,这样更不容易出错。综上所述,**建议你采用 “双闭区间” 的写法。** 观察发现,在“双闭区间”表示中,由于对左右两边界的定义是相同的,因此缩小区间的 $i$ , $j$ 处理方法也是对称的,这样更不容易出错。综上所述,**建议你采用“双闭区间”的写法。**
### 大数越界处理 ### 大数越界处理

View file

@ -8,7 +8,7 @@ comments: true
在数据量很大时,「线性查找」太慢;而「二分查找」要求数据必须是有序的,并且只能在数组中应用。那么是否有方法可以同时避免上述缺点呢?答案是肯定的,此方法被称为「哈希查找」。 在数据量很大时,「线性查找」太慢;而「二分查找」要求数据必须是有序的,并且只能在数组中应用。那么是否有方法可以同时避免上述缺点呢?答案是肯定的,此方法被称为「哈希查找」。
「哈希查找 Hash Searching」借助一个哈希表来存储需要的「键值对 Key Value Pair」我们可以在 $O(1)$ 时间下实现 “键 $\rightarrow$ 值” 映射查找,体现着 “以空间换时间” 的算法思想。 「哈希查找 Hash Searching」借助一个哈希表来存储需要的「键值对 Key Value Pair」我们可以在 $O(1)$ 时间下实现“键 $\rightarrow$ 值”映射查找,体现着“以空间换时间”的算法思想。
## 算法实现 ## 算法实现

View file

@ -6,7 +6,7 @@ comments: true
「冒泡排序 Bubble Sort」是一种最基础的排序算法非常适合作为第一个学习的排序算法。顾名思义「冒泡」是该算法的核心操作。 「冒泡排序 Bubble Sort」是一种最基础的排序算法非常适合作为第一个学习的排序算法。顾名思义「冒泡」是该算法的核心操作。
!!! question "为什么叫 “冒泡”" !!! question "为什么叫“冒泡”"
在水中,越大的泡泡浮力越大,所以最大的泡泡会最先浮到水面。 在水中,越大的泡泡浮力越大,所以最大的泡泡会最先浮到水面。

View file

@ -6,7 +6,7 @@ comments: true
「插入排序 Insertion Sort」是一种基于 **数组插入操作** 的排序算法。 「插入排序 Insertion Sort」是一种基于 **数组插入操作** 的排序算法。
「插入操作」的思想:选定数组的某个元素为基准数 `base` ,将 `base` 与其左边的元素依次对比大小,并 “插入” 到正确位置。 「插入操作」的思想:选定数组的某个元素为基准数 `base` ,将 `base` 与其左边的元素依次对比大小,并“插入”到正确位置。
然而,由于数组在内存中的存储方式是连续的,我们无法直接把 `base` 插入到目标位置,而是需要将从目标位置到 `base` 之间的所有元素向右移动一位(本质上是一次数组插入操作)。 然而,由于数组在内存中的存储方式是连续的,我们无法直接把 `base` 插入到目标位置,而是需要将从目标位置到 `base` 之间的所有元素向右移动一位(本质上是一次数组插入操作)。

View file

@ -4,7 +4,7 @@ comments: true
# 归并排序 # 归并排序
「归并排序 Merge Sort」是算法中 “分治思想” 的典型体现,其有「划分」和「合并」两个阶段: 「归并排序 Merge Sort」是算法中“分治思想”的典型体现其有「划分」和「合并」两个阶段
1. **划分阶段:** 通过递归不断 **将数组从中点位置划分开**,将长数组的排序问题转化为短数组的排序问题; 1. **划分阶段:** 通过递归不断 **将数组从中点位置划分开**,将长数组的排序问题转化为短数组的排序问题;
2. **合并阶段:** 划分到子数组长度为 1 时,开始向上合并,不断将 **左、右两个短排序数组** 合并为 **一个长排序数组**,直至合并至原数组时完成排序; 2. **合并阶段:** 划分到子数组长度为 1 时,开始向上合并,不断将 **左、右两个短排序数组** 合并为 **一个长排序数组**,直至合并至原数组时完成排序;
@ -78,13 +78,13 @@ comments: true
int i = leftStart, j = rightStart; int i = leftStart, j = rightStart;
// 通过覆盖原数组 nums 来合并左子数组和右子数组 // 通过覆盖原数组 nums 来合并左子数组和右子数组
for (int k = left; k <= right; k++) { for (int k = left; k <= right; k++) {
// 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if (i > leftEnd) if (i > leftEnd)
nums[k] = tmp[j++]; nums[k] = tmp[j++];
// 否则,若 “右子数组已全部合并完” “左子数组元素 < 右子数组元素则选取左子数组元素并且 i++ // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素则选取左子数组元素并且 i++
else if (j > rightEnd || tmp[i] <= tmp[j]) else if (j > rightEnd || tmp[i] <= tmp[j])
nums[k] = tmp[i++]; nums[k] = tmp[i++];
// 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
else else
nums[k] = tmp[j++]; nums[k] = tmp[j++];
} }
@ -122,13 +122,13 @@ comments: true
int i = leftStart, j = rightStart; int i = leftStart, j = rightStart;
// 通过覆盖原数组 nums 来合并左子数组和右子数组 // 通过覆盖原数组 nums 来合并左子数组和右子数组
for (int k = left; k <= right; k++) { for (int k = left; k <= right; k++) {
// 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if (i > leftEnd) if (i > leftEnd)
nums[k] = tmp[j++]; nums[k] = tmp[j++];
// 否则,若 “右子数组已全部合并完” “左子数组元素 < 右子数组元素则选取左子数组元素并且 i++ // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素则选取左子数组元素并且 i++
else if (j > rightEnd || tmp[i] <= tmp[j]) else if (j > rightEnd || tmp[i] <= tmp[j])
nums[k] = tmp[i++]; nums[k] = tmp[i++];
// 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
else else
nums[k] = tmp[j++]; nums[k] = tmp[j++];
} }
@ -166,15 +166,15 @@ comments: true
i, j = left_start, right_start i, j = left_start, right_start
# 通过覆盖原数组 nums 来合并左子数组和右子数组 # 通过覆盖原数组 nums 来合并左子数组和右子数组
for k in range(left, right + 1): for k in range(left, right + 1):
# 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ # 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if i > left_end: if i > left_end:
nums[k] = tmp[j] nums[k] = tmp[j]
j += 1 j += 1
# 否则,若 “右子数组已全部合并完” “左子数组元素 < 右子数组元素则选取左子数组元素并且 i++ # 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素则选取左子数组元素并且 i++
elif j > right_end or tmp[i] <= tmp[j]: elif j > right_end or tmp[i] <= tmp[j]:
nums[k] = tmp[i] nums[k] = tmp[i]
i += 1 i += 1
# 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ # 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
else: else:
nums[k] = tmp[j] nums[k] = tmp[j]
j += 1 j += 1
@ -214,15 +214,15 @@ comments: true
i, j := left_start, right_start i, j := left_start, right_start
// 通过覆盖原数组 nums 来合并左子数组和右子数组 // 通过覆盖原数组 nums 来合并左子数组和右子数组
for k := left; k <= right; k++ { for k := left; k <= right; k++ {
// 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if i > left_end { if i > left_end {
nums[k] = tmp[j] nums[k] = tmp[j]
j++ j++
// 否则,若 “右子数组已全部合并完” “左子数组元素 < 右子数组元素则选取左子数组元素并且 i++ // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素则选取左子数组元素并且 i++
} else if j > right_end || tmp[i] <= tmp[j] { } else if j > right_end || tmp[i] <= tmp[j] {
nums[k] = tmp[i] nums[k] = tmp[i]
i++ i++
// 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
} else { } else {
nums[k] = tmp[j] nums[k] = tmp[j]
j++ j++
@ -264,13 +264,13 @@ comments: true
let i = leftStart, j = rightStart; let i = leftStart, j = rightStart;
// 通过覆盖原数组 nums 来合并左子数组和右子数组 // 通过覆盖原数组 nums 来合并左子数组和右子数组
for (let k = left; k <= right; k++) { for (let k = left; k <= right; k++) {
// 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if (i > leftEnd) { if (i > leftEnd) {
nums[k] = tmp[j++]; nums[k] = tmp[j++];
// 否则,若 “右子数组已全部合并完” “左子数组元素 < 右子数组元素则选取左子数组元素并且 i++ // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素则选取左子数组元素并且 i++
} else if (j > rightEnd || tmp[i] <= tmp[j]) { } else if (j > rightEnd || tmp[i] <= tmp[j]) {
nums[k] = tmp[i++]; nums[k] = tmp[i++];
// 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
} else { } else {
nums[k] = tmp[j++]; nums[k] = tmp[j++];
} }
@ -309,13 +309,13 @@ comments: true
let i = leftStart, j = rightStart; let i = leftStart, j = rightStart;
// 通过覆盖原数组 nums 来合并左子数组和右子数组 // 通过覆盖原数组 nums 来合并左子数组和右子数组
for (let k = left; k <= right; k++) { for (let k = left; k <= right; k++) {
// 若 “左子数组已全部合并完”,则选取右子数组元素,并且 j++ // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
if (i > leftEnd) { if (i > leftEnd) {
nums[k] = tmp[j++]; nums[k] = tmp[j++];
// 否则,若 “右子数组已全部合并完” “左子数组元素 < 右子数组元素则选取左子数组元素并且 i++ // 否则,若“右子数组已全部合并完”或“左子数组元素 < 右子数组元素则选取左子数组元素并且 i++
} else if (j > rightEnd || tmp[i] <= tmp[j]) { } else if (j > rightEnd || tmp[i] <= tmp[j]) {
nums[k] = tmp[i++]; nums[k] = tmp[i++];
// 否则,若 “左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ // 否则,若“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
} else { } else {
nums[k] = tmp[j++]; nums[k] = tmp[j++];
} }
@ -370,7 +370,7 @@ comments: true
归并排序有一个很特别的优势,用于排序链表时有很好的性能表现,**空间复杂度可被优化至 $O(1)$** ,这是因为: 归并排序有一个很特别的优势,用于排序链表时有很好的性能表现,**空间复杂度可被优化至 $O(1)$** ,这是因为:
- 由于链表可仅通过改变指针来实现结点增删,因此 “将两个短有序链表合并为一个长有序链表” 无需使用额外空间,即回溯合并阶段不用像排序数组一样建立辅助数组 `tmp` - 由于链表可仅通过改变指针来实现结点增删,因此“将两个短有序链表合并为一个长有序链表”无需使用额外空间,即回溯合并阶段不用像排序数组一样建立辅助数组 `tmp`
- 通过使用「迭代」代替「递归划分」,可省去递归使用的栈帧空间; - 通过使用「迭代」代替「递归划分」,可省去递归使用的栈帧空间;
> 详情参考:[148. 排序链表](https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/) > 详情参考:[148. 排序链表](https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/)

View file

@ -4,7 +4,7 @@ comments: true
# 快速排序 # 快速排序
「快速排序 Quick Sort」是一种基于 “分治思想” 的排序算法,速度很快、应用很广。 「快速排序 Quick Sort」是一种基于“分治思想”的排序算法速度很快、应用很广。
快速排序的核心操作为「哨兵划分」,其目标为:选取数组某个元素为 **基准数** ,将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。「哨兵划分」的实现流程为: 快速排序的核心操作为「哨兵划分」,其目标为:选取数组某个元素为 **基准数** ,将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。「哨兵划分」的实现流程为:
@ -339,7 +339,7 @@ comments: true
## 快排为什么快? ## 快排为什么快?
从命名能够看出,快速排序在效率方面一定 “有两把刷子” 。快速排序的平均时间复杂度虽然与「归并排序」和「堆排序」一致,但实际 **效率更高** ,这是因为: 从命名能够看出,快速排序在效率方面一定“有两把刷子”。快速排序的平均时间复杂度虽然与「归并排序」和「堆排序」一致,但实际 **效率更高** ,这是因为:
- **出现最差情况的概率很低:** 虽然快速排序的最差时间复杂度为 $O(n^2)$ ,不如归并排序,但绝大部分情况下,快速排序可以达到 $O(n \log n)$ 的复杂度。 - **出现最差情况的概率很低:** 虽然快速排序的最差时间复杂度为 $O(n^2)$ ,不如归并排序,但绝大部分情况下,快速排序可以达到 $O(n \log n)$ 的复杂度。
- **缓存使用效率高:** 哨兵划分操作时,将整个子数组加载入缓存中,访问元素效率很高。而诸如「堆排序」需要跳跃式访问元素,因此不具有此特性。 - **缓存使用效率高:** 哨兵划分操作时,将整个子数组加载入缓存中,访问元素效率很高。而诸如「堆排序」需要跳跃式访问元素,因此不具有此特性。
@ -351,7 +351,7 @@ comments: true
为了尽量避免这种情况发生,我们可以优化一下基准数的选取策略。首先,在哨兵划分中,我们可以 **随机选取一个元素作为基准数** 。但如果运气很差,每次都选择到比较差的基准数,那么效率依然不好。 为了尽量避免这种情况发生,我们可以优化一下基准数的选取策略。首先,在哨兵划分中,我们可以 **随机选取一个元素作为基准数** 。但如果运气很差,每次都选择到比较差的基准数,那么效率依然不好。
进一步地,我们可以在数组中选取 3 个候选元素(一般为数组的首、尾、中点元素),**并将三个候选元素的中位数作为基准数**,这样基准数 “既不大也不小” 的概率就大大提升了。当然,如果数组很长的话,我们也可以选取更多候选元素,来进一步提升算法的稳健性。采取该方法后,时间复杂度劣化至 $O(n^2)$ 的概率极低。 进一步地,我们可以在数组中选取 3 个候选元素(一般为数组的首、尾、中点元素),**并将三个候选元素的中位数作为基准数**,这样基准数“既不大也不小”的概率就大大提升了。当然,如果数组很长的话,我们也可以选取更多候选元素,来进一步提升算法的稳健性。采取该方法后,时间复杂度劣化至 $O(n^2)$ 的概率极低。
=== "Java" === "Java"

View file

@ -144,8 +144,7 @@ comments: true
```js title="queue.js" ```js title="queue.js"
/* 初始化队列 */ /* 初始化队列 */
// JavaScript 没有内置的队列,可以把 Array 当作队列来使用 // JavaScript 没有内置的队列,可以把 Array 当作队列来使用
// 注意:由于是数组,所以 shift() 的时间复杂度是 O(n)
const queue = []; const queue = [];
/* 元素入队 */ /* 元素入队 */
@ -159,7 +158,7 @@ comments: true
const peek = queue[0]; const peek = queue[0];
/* 元素出队 */ /* 元素出队 */
// O(n) // 底层是数组,因此 shift() 方法的时间复杂度为 O(n)
const poll = queue.shift(); const poll = queue.shift();
/* 获取队列的长度 */ /* 获取队列的长度 */
@ -174,7 +173,6 @@ comments: true
```typescript title="queue.ts" ```typescript title="queue.ts"
/* 初始化队列 */ /* 初始化队列 */
// TypeScript 没有内置的队列,可以把 Array 当作队列来使用 // TypeScript 没有内置的队列,可以把 Array 当作队列来使用
// 注意:由于是数组,所以 shift() 的时间复杂度是 O(n)
const queue: number[] = []; const queue: number[] = [];
/* 元素入队 */ /* 元素入队 */
@ -188,7 +186,7 @@ comments: true
const peek = queue[0]; const peek = queue[0];
/* 元素出队 */ /* 元素出队 */
// O(n) // 底层是数组,因此 shift() 方法的时间复杂度为 O(n)
const poll = queue.shift(); const poll = queue.shift();
/* 获取队列的长度 */ /* 获取队列的长度 */
@ -382,19 +380,16 @@ comments: true
// 使用内置包 list 来实现队列 // 使用内置包 list 来实现队列
data *list.List data *list.List
} }
// NewLinkedListQueue 初始化链表 // NewLinkedListQueue 初始化链表
func NewLinkedListQueue() *LinkedListQueue { func NewLinkedListQueue() *LinkedListQueue {
return &LinkedListQueue{ return &LinkedListQueue{
data: list.New(), data: list.New(),
} }
} }
// Offer 入队 // Offer 入队
func (s *LinkedListQueue) Offer(value any) { func (s *LinkedListQueue) Offer(value any) {
s.data.PushBack(value) s.data.PushBack(value)
} }
// Poll 出队 // Poll 出队
func (s *LinkedListQueue) Poll() any { func (s *LinkedListQueue) Poll() any {
if s.IsEmpty() { if s.IsEmpty() {
@ -404,7 +399,6 @@ comments: true
s.data.Remove(e) s.data.Remove(e)
return e.Value return e.Value
} }
// Peek 访问队首元素 // Peek 访问队首元素
func (s *LinkedListQueue) Peek() any { func (s *LinkedListQueue) Peek() any {
if s.IsEmpty() { if s.IsEmpty() {
@ -413,12 +407,10 @@ comments: true
e := s.data.Front() e := s.data.Front()
return e.Value return e.Value
} }
// Size 获取队列的长度 // Size 获取队列的长度
func (s *LinkedListQueue) Size() int { func (s *LinkedListQueue) Size() int {
return s.data.Len() return s.data.Len()
} }
// IsEmpty 判断队列是否为空 // IsEmpty 判断队列是否为空
func (s *LinkedListQueue) IsEmpty() bool { func (s *LinkedListQueue) IsEmpty() bool {
return s.data.Len() == 0 return s.data.Len() == 0
@ -428,13 +420,107 @@ comments: true
=== "JavaScript" === "JavaScript"
```js title="linkedlist_queue.js" ```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"
```typescript title="linkedlist_queue.ts" ```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" === "C"
@ -453,7 +539,7 @@ comments: true
数组的删除首元素的时间复杂度为 $O(n)$ ,因此不适合直接用来实现队列。然而,我们可以借助两个指针 `front` , `rear` 来分别记录队首和队尾的索引位置,在入队 / 出队时分别将 `front` / `rear` 向后移动一位即可,这样每次仅需操作一个元素,时间复杂度降至 $O(1)$ 。 数组的删除首元素的时间复杂度为 $O(n)$ ,因此不适合直接用来实现队列。然而,我们可以借助两个指针 `front` , `rear` 来分别记录队首和队尾的索引位置,在入队 / 出队时分别将 `front` / `rear` 向后移动一位即可,这样每次仅需操作一个元素,时间复杂度降至 $O(1)$ 。
还有一个问题,在入队与出队的过程中,两个指针都在向后移动,而到达尾部后则无法继续移动了。为了解决此问题,我们可以采取一个取巧方案,即将数组看作是 “环形” 的。具体做法是规定指针越过数组尾部后,再次回到头部接续遍历,这样相当于使数组 “首尾相连” 了。 还有一个问题,在入队与出队的过程中,两个指针都在向后移动,而到达尾部后则无法继续移动了。为了解决此问题,我们可以采取一个取巧方案,即将数组看作是“环形”的。具体做法是规定指针越过数组尾部后,再次回到头部接续遍历,这样相当于使数组“首尾相连”了。
为了适应环形数组的设定,获取长度 `size()` 、入队 `offer()` 、出队 `poll()` 方法都需要做相应的取余操作处理,使得当尾指针绕回数组头部时,仍然可以正确处理操作。 为了适应环形数组的设定,获取长度 `size()` 、入队 `offer()` 、出队 `poll()` 方法都需要做相应的取余操作处理,使得当尾指针绕回数组头部时,仍然可以正确处理操作。
@ -506,17 +592,10 @@ comments: true
} }
/* 访问队首元素 */ /* 访问队首元素 */
public int peek() { public int peek() {
// 删除头结点
if (isEmpty()) if (isEmpty())
throw new EmptyStackException(); throw new EmptyStackException();
return nums[front]; 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() { int peek() {
// 删除头结点
if (empty()) if (empty())
throw out_of_range("队列为空"); throw out_of_range("队列为空");
return nums[front]; 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): def poll(self):
# 删除头结点
num = self.peek() num = self.peek()
# 队头指针向后移动一位,若越过尾部则返回到数组头部 # 队头指针向后移动一位,若越过尾部则返回到数组头部
self.__front = (self.__front + 1) % self.capacity() self.__front = (self.__front + 1) % self.capacity()
@ -627,19 +698,11 @@ comments: true
""" 访问队首元素 """ """ 访问队首元素 """
def peek(self): def peek(self):
# 删除头结点
if self.is_empty(): if self.is_empty():
print("队列为空") print("队列为空")
return False return False
return self.__nums[self.__front] 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): def to_list(self):
res = [0] * self.size() res = [0] * self.size()
@ -660,7 +723,6 @@ comments: true
front int // 头指针,指向队首 front int // 头指针,指向队首
rear int // 尾指针,指向队尾 + 1 rear int // 尾指针,指向队尾 + 1
} }
// NewArrayQueue 基于环形数组实现的队列 // NewArrayQueue 基于环形数组实现的队列
func NewArrayQueue(capacity int) *ArrayQueue { func NewArrayQueue(capacity int) *ArrayQueue {
return &ArrayQueue{ return &ArrayQueue{
@ -670,18 +732,15 @@ comments: true
rear: 0, rear: 0,
} }
} }
// Size 获取队列的长度 // Size 获取队列的长度
func (q *ArrayQueue) Size() int { func (q *ArrayQueue) Size() int {
size := (q.capacity + q.rear - q.front) % q.capacity size := (q.capacity + q.rear - q.front) % q.capacity
return size return size
} }
// IsEmpty 判断队列是否为空 // IsEmpty 判断队列是否为空
func (q *ArrayQueue) IsEmpty() bool { func (q *ArrayQueue) IsEmpty() bool {
return q.rear-q.front == 0 return q.rear-q.front == 0
} }
// Offer 入队 // Offer 入队
func (q *ArrayQueue) Offer(v int) { func (q *ArrayQueue) Offer(v int) {
// 当 rear == capacity 表示队列已满 // 当 rear == capacity 表示队列已满
@ -693,7 +752,6 @@ comments: true
// 尾指针向后移动一位,越过尾部后返回到数组头部 // 尾指针向后移动一位,越过尾部后返回到数组头部
q.rear = (q.rear + 1) % q.capacity q.rear = (q.rear + 1) % q.capacity
} }
// Poll 出队 // Poll 出队
func (q *ArrayQueue) Poll() any { func (q *ArrayQueue) Poll() any {
if q.IsEmpty() { if q.IsEmpty() {
@ -704,7 +762,6 @@ comments: true
q.front = (q.front + 1) % q.capacity q.front = (q.front + 1) % q.capacity
return v return v
} }
// Peek 访问队首元素 // Peek 访问队首元素
func (q *ArrayQueue) Peek() any { func (q *ArrayQueue) Peek() any {
if q.IsEmpty() { if q.IsEmpty() {
@ -718,13 +775,100 @@ comments: true
=== "JavaScript" === "JavaScript"
```js title="array_queue.js" ```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"
```typescript title="array_queue.ts" ```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<number>(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" === "C"

View file

@ -210,7 +210,7 @@ comments: true
为了更加清晰地了解栈的运行机制,接下来我们来自己动手实现一个栈类。 为了更加清晰地了解栈的运行机制,接下来我们来自己动手实现一个栈类。
栈规定元素是先入后出的,因此我们只能在栈顶添加或删除元素。然而,数组或链表都可以在任意位置添加删除元素,因此 **栈可被看作是一种受约束的数组或链表**。换言之,我们可以 “屏蔽” 数组或链表的部分无关操作,使之对外的表现逻辑符合栈的规定即可。 栈规定元素是先入后出的,因此我们只能在栈顶添加或删除元素。然而,数组或链表都可以在任意位置添加删除元素,因此 **栈可被看作是一种受约束的数组或链表**。换言之,我们可以“屏蔽”数组或链表的部分无关操作,使之对外的表现逻辑符合栈的规定即可。
### 基于链表的实现 ### 基于链表的实现
@ -350,19 +350,16 @@ comments: true
// 使用内置包 list 来实现栈 // 使用内置包 list 来实现栈
data *list.List data *list.List
} }
// NewLinkedListStack 初始化链表 // NewLinkedListStack 初始化链表
func NewLinkedListStack() *LinkedListStack { func NewLinkedListStack() *LinkedListStack {
return &LinkedListStack{ return &LinkedListStack{
data: list.New(), data: list.New(),
} }
} }
// Push 入栈 // Push 入栈
func (s *LinkedListStack) Push(value int) { func (s *LinkedListStack) Push(value int) {
s.data.PushBack(value) s.data.PushBack(value)
} }
// Pop 出栈 // Pop 出栈
func (s *LinkedListStack) Pop() any { func (s *LinkedListStack) Pop() any {
if s.IsEmpty() { if s.IsEmpty() {
@ -372,7 +369,6 @@ comments: true
s.data.Remove(e) s.data.Remove(e)
return e.Value return e.Value
} }
// Peek 访问栈顶元素 // Peek 访问栈顶元素
func (s *LinkedListStack) Peek() any { func (s *LinkedListStack) Peek() any {
if s.IsEmpty() { if s.IsEmpty() {
@ -381,12 +377,10 @@ comments: true
e := s.data.Back() e := s.data.Back()
return e.Value return e.Value
} }
// Size 获取栈的长度 // Size 获取栈的长度
func (s *LinkedListStack) Size() int { func (s *LinkedListStack) Size() int {
return s.data.Len() return s.data.Len()
} }
// IsEmpty 判断栈是否为空 // IsEmpty 判断栈是否为空
func (s *LinkedListStack) IsEmpty() bool { func (s *LinkedListStack) IsEmpty() bool {
return s.data.Len() == 0 return s.data.Len() == 0
@ -457,12 +451,6 @@ comments: true
throw new EmptyStackException(); throw new EmptyStackException();
return stack.get(size() - 1); 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("栈为空"); throw out_of_range("栈为空");
return stack.back(); 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): def peek(self):
assert not self.is_empty(), "栈为空" assert not self.is_empty(), "栈为空"
return self.__stack[-1] return self.__stack[-1]
""" 访问索引 index 处元素 """
def get(self, index):
assert index < self.size(), "索引越界"
return self.__stack[index]
``` ```
=== "Go" === "Go"
@ -613,19 +590,16 @@ comments: true
} }
/* 出栈 */ /* 出栈 */
pop() { pop() {
if (this.empty()) throw "栈为空"; if (this.empty())
throw new Error("栈为空");
return this.stack.pop(); return this.stack.pop();
} }
/* 访问栈顶元素 */ /* 访问栈顶元素 */
top() { top() {
if (this.empty()) throw "栈为空"; if (this.empty())
throw new Error("栈为空");
return this.stack[this.stack.length - 1]; 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 { pop(): number | undefined {
if (empty()) throw new Error('栈为空'); if (this.empty())
throw new Error('栈为空');
return this.stack.pop(); return this.stack.pop();
} }
/* 访问栈顶元素 */ /* 访问栈顶元素 */
top(): number | undefined { top(): number | undefined {
if (empty()) throw new Error('栈为空'); if (this.empty())
throw new Error('栈为空');
return this.stack[this.stack.length - 1]; return this.stack[this.stack.length - 1];
} }
/* 访问索引 index 处元素 */
get(index: number): number | undefined {
if (index >= size()) throw new Error('索引越界');
return this.stack[index];
}
}; };
``` ```

View file

@ -226,7 +226,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影
![right_rotate_with_grandchild](avl_tree.assets/right_rotate_with_grandchild.png) ![right_rotate_with_grandchild](avl_tree.assets/right_rotate_with_grandchild.png)
“向右旋转” 是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。 “向右旋转”是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。
=== "Java" === "Java"
@ -290,7 +290,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影
### Case 2 - 左旋 ### Case 2 - 左旋
类似地,如果将取上述失衡二叉树的 “镜像” ,那么则需要「左旋」操作。观察发现,**「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的**。 类似地,如果将取上述失衡二叉树的“镜像”,那么则需要「左旋」操作。观察发现,**「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的**。
![left_rotate_with_grandchild](avl_tree.assets/left_rotate_with_grandchild.png) ![left_rotate_with_grandchild](avl_tree.assets/left_rotate_with_grandchild.png)

View file

@ -164,7 +164,7 @@ comments: true
### 插入结点 ### 插入结点
给定一个待插入元素 `num` ,为了保持二叉搜索树 “左子树 < 根结点 < 右子树 的性质插入操作分为两步 给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根结点 < 右子树的性质插入操作分为两步
1. **查找插入位置:** 与查找操作类似,我们从根结点出发,根据当前结点值和 `num` 的大小关系循环向下搜索,直到越过叶结点(遍历到 $\text{null}$ )时跳出循环; 1. **查找插入位置:** 与查找操作类似,我们从根结点出发,根据当前结点值和 `num` 的大小关系循环向下搜索,直到越过叶结点(遍历到 $\text{null}$ )时跳出循环;
2. **在该位置插入结点:** 初始化结点 `num` ,将该结点放到 $\text{null}$ 的位置 2. **在该位置插入结点:** 初始化结点 `num` ,将该结点放到 $\text{null}$ 的位置
@ -344,7 +344,7 @@ comments: true
### 删除结点 ### 删除结点
与插入结点一样,我们需要在删除操作后维持二叉搜索树的 “左子树 < 根结点 < 右子树 的性质首先我们需要在二叉树中执行查找操作获取待删除结点接下来根据待删除结点的子结点数量删除操作需要分为三种情况 与插入结点一样,我们需要在删除操作后维持二叉搜索树的“左子树 < 根结点 < 右子树的性质首先我们需要在二叉树中执行查找操作获取待删除结点接下来根据待删除结点的子结点数量删除操作需要分为三种情况
**待删除结点的子结点数量 $= 0$ 。** 表明待删除结点是叶结点,直接删除即可。 **待删除结点的子结点数量 $= 0$ 。** 表明待删除结点是叶结点,直接删除即可。
@ -668,7 +668,7 @@ comments: true
- **删除元素:** 与无序数组中的情况相同,使用 $O(n)$ 时间; - **删除元素:** 与无序数组中的情况相同,使用 $O(n)$ 时间;
- **获取最小 / 最大元素:** 数组头部和尾部元素即是最小和最大元素,使用 $O(1)$ 时间; - **获取最小 / 最大元素:** 数组头部和尾部元素即是最小和最大元素,使用 $O(1)$ 时间;
观察发现,无序数组和有序数组中的各类操作的时间复杂度是 “偏科” 的,即有的快有的慢;**而二叉搜索树的各项操作的时间复杂度都是对数阶,在数据量 $n$ 很大时有巨大优势**。 观察发现,无序数组和有序数组中的各项操作的时间复杂度是“偏科”的,即有的快有的慢;**而二叉搜索树的各项操作的时间复杂度都是对数阶,在数据量 $n$ 很大时有巨大优势**。
<div class="center-table" markdown> <div class="center-table" markdown>
@ -683,7 +683,7 @@ comments: true
## 二叉搜索树的退化 ## 二叉搜索树的退化
理想情况下,我们希望二叉搜索树的是 “左右平衡” 的(详见「平衡二叉树」章节),此时可以在 $\log n$ 轮循环内查找任意结点。 理想情况下,我们希望二叉搜索树的是“左右平衡”的(详见「平衡二叉树」章节),此时可以在 $\log n$ 轮循环内查找任意结点。
如果我们动态地在二叉搜索树中插入与删除结点,**则可能导致二叉树退化为链表**,此时各种操作的时间复杂度也退化之 $O(n)$ 。 如果我们动态地在二叉搜索树中插入与删除结点,**则可能导致二叉树退化为链表**,此时各种操作的时间复杂度也退化之 $O(n)$ 。

View file

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View file

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View file

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View file

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View file

@ -4,7 +4,7 @@ comments: true
# 二叉树 # 二叉树
「二叉树 Binary Tree」是一种非线性数据结构代表着祖先与后代之间的派生关系体现着 “一分为二” 的分治逻辑。类似于链表,二叉树也是以结点为单位存储的,结点包含「值」和两个「指针」。 「二叉树 Binary Tree」是一种非线性数据结构代表着祖先与后代之间的派生关系体现着“一分为二”的分治逻辑。类似于链表二叉树也是以结点为单位存储的结点包含「值」和两个「指针」。
=== "Java" === "Java"
@ -79,7 +79,7 @@ comments: true
val: number; val: number;
left: TreeNode | null; left: TreeNode | null;
right: TreeNode | null; right: TreeNode | null;
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) { constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = val === undefined ? 0 : val; // 结点值 this.val = val === undefined ? 0 : val; // 结点值
this.left = left === undefined ? null : left; // 左子结点指针 this.left = left === undefined ? null : left; // 左子结点指针
@ -91,13 +91,13 @@ comments: true
=== "C" === "C"
```c title="" ```c title=""
``` ```
=== "C#" === "C#"
```csharp title="" ```csharp title=""
``` ```
结点的两个指针分别指向「左子结点 Left Child Node」和「右子结点 Right Child Node」并且称该结点为两个子结点的「父结点 Parent Node」。给定二叉树某结点将左子结点以下的树称为该结点的「左子树 Left Subtree」右子树同理。 结点的两个指针分别指向「左子结点 Left Child Node」和「右子结点 Right Child Node」并且称该结点为两个子结点的「父结点 Parent Node」。给定二叉树某结点将左子结点以下的树称为该结点的「左子树 Left Subtree」右子树同理。
@ -129,27 +129,6 @@ comments: true
值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过结点的数量”,此时高度或深度都需要 + 1 。 值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过结点的数量”,此时高度或深度都需要 + 1 。
## 二叉树最佳和最差结构
当二叉树的每层的结点都被填满时,达到「完美二叉树」;而当所有结点都偏向一边时,二叉树退化为「链表」。
![binary_tree_corner_cases](binary_tree.assets/binary_tree_corner_cases.png)
<p align="center"> Fig. 二叉树的最佳和最差结构 </p>
如下表所示,在最佳和最差结构下,二叉树的叶结点数量、结点总数、高度等达到极大或极小值。
<div class="center-table" markdown>
| | 完美二叉树 | 链表 |
| ----------------------------- | ---------- | ---------- |
| 第 $i$ 层的结点数量 | $2^{i-1}$ | $1$ |
| 树的高度为 $h$ 时的叶结点数量 | $2^h$ | $1$ |
| 树的高度为 $h$ 时的结点总数 | $2^{h+1} - 1$ | $h + 1$ |
| 树的结点总数为 $n$ 时的高度 | $\log_2 (n+1) - 1$ | $n - 1$ |
</div>
## 二叉树基本操作 ## 二叉树基本操作
**初始化二叉树。** 与链表类似,先初始化结点,再构建引用指向(即指针)。 **初始化二叉树。** 与链表类似,先初始化结点,再构建引用指向(即指针)。
@ -190,7 +169,7 @@ comments: true
=== "Python" === "Python"
```python title="binary_tree.py" ```python title="binary_tree.py"
``` ```
=== "Go" === "Go"
@ -247,13 +226,13 @@ comments: true
=== "C" === "C"
```c title="binary_tree.c" ```c title="binary_tree.c"
``` ```
=== "C#" === "C#"
```csharp title="binary_tree.cs" ```csharp title="binary_tree.cs"
``` ```
**插入与删除结点。** 与链表类似,插入与删除结点都可以通过修改指针实现。 **插入与删除结点。** 与链表类似,插入与删除结点都可以通过修改指针实现。
@ -288,7 +267,7 @@ comments: true
=== "Python" === "Python"
```python title="binary_tree.py" ```python title="binary_tree.py"
``` ```
=== "Go" === "Go"
@ -330,374 +309,71 @@ comments: true
=== "C" === "C"
```c title="binary_tree.c" ```c title="binary_tree.c"
``` ```
=== "C#" === "C#"
```csharp title="binary_tree.cs" ```csharp title="binary_tree.cs"
``` ```
!!! note !!! 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)
<p align="center"> Fig. 二叉树的层序遍历 </p> ### 完全二叉树
广度优先遍历一般借助「队列」来实现。队列的规则是 “先进先出” ,广度优先遍历的规则是 ”一层层平推“ ,两者背后的思想是一致的 「完全二叉树 Complete Binary Tree」只有最底层的结点未被填满且最底层结点尽量靠左填充
=== "Java" **完全二叉树非常适合用数组来表示**。如果按照层序遍历序列的顺序来存储,那么空结点 `null` 一定全部出现在序列的尾部,因此我们就可以不用存储这些 null 了。
```java title="binary_tree_bfs.java" ![complete_binary_tree](binary_tree.assets/complete_binary_tree.png)
/* 层序遍历 */
List<Integer> hierOrder(TreeNode root) {
// 初始化队列,加入根结点
Queue<TreeNode> queue = new LinkedList<>() {{ add(root); }};
// 初始化一个列表,用于保存遍历序列
List<Integer> 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" 「完满二叉树 Full Binary Tree」除了叶结点之外其余所有结点都有两个子结点。
/* 层序遍历 */
vector<int> hierOrder(TreeNode* root) {
// 初始化队列,加入根结点
queue<TreeNode*> queue;
queue.push(root);
// 初始化一个列表,用于保存遍历序列
vector<int> 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" ![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" - 完美二叉树是一个二叉树的“最佳状态”,可以完全发挥出二叉树“分治”的优势;
/* 层序遍历 */ - 链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至 $O(n)$
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" ![binary_tree_corner_cases](binary_tree.assets/binary_tree_corner_cases.png)
```typescript title="binary_tree_bfs.ts" <p align="center"> Fig. 二叉树的最佳和最差结构 </p>
/* 层序遍历 */
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.assets/binary_tree_dfs.png)
<p align="center"> Fig. 二叉树的前 / 中 / 后序遍历 </p>
<div class="center-table" markdown> <div class="center-table" markdown>
| 位置 | 含义 | 此处访问结点时对应 | | | 完美二叉树 | 链表 |
| ---------- | ------------------------------------ | ----------------------------- | | ----------------------------- | ---------- | ---------- |
| 橙色圆圈处 | 刚进入此结点,即将访问该结点的左子树 | 前序遍历 Pre-Order Traversal | | 第 $i$ 层的结点数量 | $2^{i-1}$ | $1$ |
| 蓝色圆圈处 | 已访问完左子树,即将访问右子树 | 中序遍历 In-Order Traversal | | 树的高度为 $h$ 时的叶结点数量 | $2^h$ | $1$ |
| 紫色圆圈处 | 已访问完左子树和右子树,即将返回 | 后序遍历 Post-Order Traversal | | 树的高度为 $h$ 时的结点总数 | $2^{h+1} - 1$ | $h + 1$ |
| 树的结点总数为 $n$ 时的高度 | $\log_2 (n+1) - 1$ | $n - 1$ |
</div> </div>
=== "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
使用循环一样可以实现前、中、后序遍历,但代码相对繁琐,有兴趣的同学可以自行实现。

View file

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View file

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

View file

@ -0,0 +1,362 @@
---
comments: true
---
# 二叉树遍历
非线性数据结构的遍历操作比线性数据结构更加复杂,往往需要使用搜索算法来实现。常见的二叉树遍历方式有层序遍历、前序遍历、中序遍历、后序遍历。
## 层序遍历
「层序遍历 Hierarchical-Order Traversal」从顶至底、一层一层地遍历二叉树并在每层中按照从左到右的顺序访问结点。
层序遍历本质上是「广度优先搜索 Breadth-First Traversal」其体现着一种“一圈一圈向外”的层进遍历方式。
![binary_tree_bfs](binary_tree_traversal.assets/binary_tree_bfs.png)
<p align="center"> Fig. 二叉树的层序遍历 </p>
广度优先遍历一般借助「队列」来实现。队列的规则是“先进先出”,广度优先遍历的规则是 ”一层层平推“ ,两者背后的思想是一致的。
=== "Java"
```java title="binary_tree_bfs.java"
/* 层序遍历 */
List<Integer> hierOrder(TreeNode root) {
// 初始化队列,加入根结点
Queue<TreeNode> queue = new LinkedList<>() {{ add(root); }};
// 初始化一个列表,用于保存遍历序列
List<Integer> 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<int> hierOrder(TreeNode* root) {
// 初始化队列,加入根结点
queue<TreeNode*> queue;
queue.push(root);
// 初始化一个列表,用于保存遍历序列
vector<int> 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)
<p align="center"> Fig. 二叉树的前 / 中 / 后序遍历 </p>
<div class="center-table" markdown>
| 位置 | 含义 | 此处访问结点时对应 |
| ---------- | ------------------------------------ | ----------------------------- |
| 橙色圆圈处 | 刚进入此结点,即将访问该结点的左子树 | 前序遍历 Pre-Order Traversal |
| 蓝色圆圈处 | 已访问完左子树,即将访问右子树 | 中序遍历 In-Order Traversal |
| 紫色圆圈处 | 已访问完左子树和右子树,即将返回 | 后序遍历 Post-Order Traversal |
</div>
=== "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
使用循环一样可以实现前、中、后序遍历,但代码相对繁琐,有兴趣的同学可以自行实现。

View file

@ -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

View file

@ -157,7 +157,7 @@ nav:
- 小结: chapter_hashing/summary.md - 小结: chapter_hashing/summary.md
- 二叉树: - 二叉树:
- 二叉树Binary Tree: chapter_tree/binary_tree.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 - 二叉搜索树: chapter_tree/binary_search_tree.md
- AVL 树 *: chapter_tree/avl_tree.md - AVL 树 *: chapter_tree/avl_tree.md
- 小结: chapter_tree/summary.md - 小结: chapter_tree/summary.md