mirror of
https://github.com/krahets/hello-algo.git
synced 2024-12-26 12:56:32 +08:00
build
This commit is contained in:
parent
43a7e5d526
commit
1a6a6b0342
12 changed files with 238 additions and 167 deletions
|
@ -405,15 +405,9 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
|||
}
|
||||
```
|
||||
|
||||
**数组中插入或删除元素效率低下**。假设我们想要在数组中间某位置插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。删除元素也是类似,需要把此索引之后的元素都向前移动一位。总体看有以下缺点:
|
||||
**数组中插入或删除元素效率低下**。如果我们想要在数组中间插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。
|
||||
|
||||
- **时间复杂度高**:数组的插入和删除的平均时间复杂度均为 $O(N)$ ,其中 $N$ 为数组长度。
|
||||
- **丢失元素**:由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会被丢失。
|
||||
- **内存浪费**:我们一般会初始化一个比较长的数组,只用前面一部分,这样在插入数据时,丢失的末尾元素都是我们不关心的,但这样做同时也会造成内存空间的浪费。
|
||||
|
||||
![array_insert_remove_element](array.assets/array_insert_remove_element.png)
|
||||
|
||||
<p align="center"> Fig. 在数组中插入与删除元素 </p>
|
||||
![array_insert_element](array.assets/array_insert_element.png)
|
||||
|
||||
=== "Java"
|
||||
|
||||
|
@ -427,14 +421,6 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
|||
// 将 num 赋给 index 处元素
|
||||
nums[index] = num;
|
||||
}
|
||||
|
||||
/* 删除索引 index 处元素 */
|
||||
void remove(int[] nums, int index) {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for (int i = index; i < nums.length - 1; i++) {
|
||||
nums[i] = nums[i + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
@ -449,14 +435,6 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
|||
// 将 num 赋给 index 处元素
|
||||
nums[index] = num;
|
||||
}
|
||||
|
||||
/* 删除索引 index 处元素 */
|
||||
void remove(int* nums, int size, int index) {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for (int i = index; i < size - 1; i++) {
|
||||
nums[i] = nums[i + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
@ -469,12 +447,6 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
|||
nums[i] = nums[i - 1]
|
||||
# 将 num 赋给 index 处元素
|
||||
nums[index] = num
|
||||
|
||||
""" 删除索引 index 处元素 """
|
||||
def remove(nums, index):
|
||||
# 把索引 index 之后的所有元素向前移动一位
|
||||
for i in range(index, len(nums) - 1):
|
||||
nums[i] = nums[i + 1]
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
@ -489,14 +461,6 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
|||
// 将 num 赋给 index 处元素
|
||||
nums[index] = num
|
||||
}
|
||||
|
||||
/* 删除索引 index 处元素 */
|
||||
func remove(nums []int, index int) {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for i := index; i < len(nums)-1; i++ {
|
||||
nums[i] = nums[i+1]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
|
@ -511,14 +475,6 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
|||
// 将 num 赋给 index 处元素
|
||||
nums[index] = num;
|
||||
}
|
||||
|
||||
/* 删除索引 index 处元素 */
|
||||
function remove(nums, index) {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for (let i = index; i < nums.length - 1; i++) {
|
||||
nums[i] = nums[i + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
@ -533,22 +489,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
|||
// 将 num 赋给 index 处元素
|
||||
nums[index] = num;
|
||||
}
|
||||
|
||||
/* 删除索引 index 处元素 */
|
||||
function remove(nums: number[], index: number): void {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for (let i = index; i < nums.length - 1; i++) {
|
||||
nums[i] = nums[i + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="array.c"
|
||||
[class]{}-[func]{insert}
|
||||
|
||||
[class]{}-[func]{removeItem}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
@ -565,16 +511,6 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
|||
// 将 num 赋给 index 处元素
|
||||
nums[index] = num;
|
||||
}
|
||||
|
||||
/* 删除索引 index 处元素 */
|
||||
void remove(int[] nums, int index)
|
||||
{
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for (int i = index; i < nums.Length - 1; i++)
|
||||
{
|
||||
nums[i] = nums[i + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
@ -589,7 +525,105 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
|||
// 将 num 赋给 index 处元素
|
||||
nums[index] = num
|
||||
}
|
||||
```
|
||||
|
||||
删除元素也是类似,如果我们想要删除索引 $i$ 处的元素,则需要把索引 $i$ 之后的元素都向前移动一位。值得注意的是,删除元素后,原先末尾的元素变得“无意义”了,我们无需特意去修改它。
|
||||
|
||||
![array_remove_element](array.assets/array_remove_element.png)
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="array.java"
|
||||
/* 删除索引 index 处元素 */
|
||||
void remove(int[] nums, int index) {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for (int i = index; i < nums.length - 1; i++) {
|
||||
nums[i] = nums[i + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="array.cpp"
|
||||
/* 删除索引 index 处元素 */
|
||||
void remove(int* nums, int size, int index) {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for (int i = index; i < size - 1; i++) {
|
||||
nums[i] = nums[i + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="array.py"
|
||||
""" 删除索引 index 处元素 """
|
||||
def remove(nums, index):
|
||||
# 把索引 index 之后的所有元素向前移动一位
|
||||
for i in range(index, len(nums) - 1):
|
||||
nums[i] = nums[i + 1]
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="array.go"
|
||||
/* 删除索引 index 处元素 */
|
||||
func remove(nums []int, index int) {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for i := index; i < len(nums)-1; i++ {
|
||||
nums[i] = nums[i+1]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
|
||||
```javascript title="array.js"
|
||||
/* 删除索引 index 处元素 */
|
||||
function remove(nums, index) {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for (let i = index; i < nums.length - 1; i++) {
|
||||
nums[i] = nums[i + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title="array.ts"
|
||||
/* 删除索引 index 处元素 */
|
||||
function remove(nums: number[], index: number): void {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for (let i = index; i < nums.length - 1; i++) {
|
||||
nums[i] = nums[i + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="array.c"
|
||||
[class]{}-[func]{removeItem}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="array.cs"
|
||||
/* 删除索引 index 处元素 */
|
||||
void remove(int[] nums, int index)
|
||||
{
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for (int i = index; i < nums.Length - 1; i++)
|
||||
{
|
||||
nums[i] = nums[i + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="array.swift"
|
||||
/* 删除索引 index 处元素 */
|
||||
func remove(nums: inout [Int], index: Int) {
|
||||
let count = nums.count
|
||||
|
@ -603,17 +637,6 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
|||
=== "Zig"
|
||||
|
||||
```zig title="array.zig"
|
||||
// 在数组的索引 index 处插入元素 num
|
||||
fn insert(nums: []i32, num: i32, index: usize) void {
|
||||
// 把索引 index 以及之后的所有元素向后移动一位
|
||||
var i = nums.len - 1;
|
||||
while (i > index) : (i -= 1) {
|
||||
nums[i] = nums[i - 1];
|
||||
}
|
||||
// 将 num 赋给 index 处元素
|
||||
nums[index] = num;
|
||||
}
|
||||
|
||||
// 删除索引 index 处元素
|
||||
fn remove(nums: []i32, index: usize) void {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
|
@ -624,6 +647,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
|||
}
|
||||
```
|
||||
|
||||
总结来看,数组的插入与删除操作有以下缺点:
|
||||
|
||||
- **时间复杂度高**:数组的插入和删除的平均时间复杂度均为 $O(N)$ ,其中 $N$ 为数组长度。
|
||||
- **丢失元素**:由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会被丢失。
|
||||
- **内存浪费**:我们一般会初始化一个比较长的数组,只用前面一部分,这样在插入数据时,丢失的末尾元素都是我们不关心的,但这样做同时也会造成内存空间的浪费。
|
||||
|
||||
## 4.1.3. 数组常用操作
|
||||
|
||||
**数组遍历**。以下介绍两种常用的遍历方法。
|
||||
|
|
|
@ -316,11 +316,9 @@ comments: true
|
|||
|
||||
## 4.2.1. 链表优点
|
||||
|
||||
**在链表中,插入与删除结点的操作效率高**。例如,如果想在链表中间的两个结点 `A` , `B` 之间插入一个新结点 `P` ,我们只需要改变两个结点指针即可,时间复杂度为 $O(1)$ ,相比数组的插入操作高效很多。在链表中删除某个结点也很方便,只需要改变一个结点指针即可。
|
||||
**在链表中,插入与删除结点的操作效率高**。比如,如果我们想在链表中间的两个结点 `A` , `B` 之间插入一个新结点 `P` ,我们只需要改变两个结点指针即可,时间复杂度为 $O(1)$ ,相比数组的插入操作高效很多。
|
||||
|
||||
![linkedlist_insert_remove_node](linked_list.assets/linkedlist_insert_remove_node.png)
|
||||
|
||||
<p align="center"> Fig. 在链表中插入与删除结点 </p>
|
||||
![linkedlist_insert_node](linked_list.assets/linkedlist_insert_node.png)
|
||||
|
||||
=== "Java"
|
||||
|
||||
|
@ -331,7 +329,109 @@ comments: true
|
|||
n0.next = P;
|
||||
P.next = n1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="linked_list.cpp"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
void insert(ListNode* n0, ListNode* P) {
|
||||
ListNode* n1 = n0->next;
|
||||
n0->next = P;
|
||||
P->next = n1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="linked_list.py"
|
||||
""" 在链表的结点 n0 之后插入结点 P """
|
||||
def insert(n0, P):
|
||||
n1 = n0.next
|
||||
n0.next = P
|
||||
P.next = n1
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="linked_list.go"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
func insertNode(n0 *ListNode, P *ListNode) {
|
||||
n1 := n0.Next
|
||||
n0.Next = P
|
||||
P.Next = n1
|
||||
}
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
|
||||
```javascript title="linked_list.js"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
function insert(n0, P) {
|
||||
const n1 = n0.next;
|
||||
n0.next = P;
|
||||
P.next = n1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title="linked_list.ts"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
function insert(n0: ListNode, P: ListNode): void {
|
||||
const n1 = n0.next;
|
||||
n0.next = P;
|
||||
P.next = n1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="linked_list.c"
|
||||
[class]{}-[func]{insertNode}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="linked_list.cs"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
void insert(ListNode n0, ListNode P)
|
||||
{
|
||||
ListNode? n1 = n0.next;
|
||||
n0.next = P;
|
||||
P.next = n1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="linked_list.swift"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
func insert(n0: ListNode, P: ListNode) {
|
||||
let n1 = n0.next
|
||||
n0.next = P
|
||||
P.next = n1
|
||||
}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="linked_list.zig"
|
||||
// 在链表的结点 n0 之后插入结点 P
|
||||
fn insert(n0: ?*inc.ListNode(i32), P: ?*inc.ListNode(i32)) void {
|
||||
var n1 = n0.?.next;
|
||||
n0.?.next = P;
|
||||
P.?.next = n1;
|
||||
}
|
||||
```
|
||||
|
||||
在链表中删除结点也很方便,只需要改变一个结点指针即可。如下图所示,虽然在完成删除后结点 `P` 仍然指向 `n2` ,但实际上 `P` 已经不属于此链表了,因为遍历此链表是无法访问到 `P` 的。
|
||||
|
||||
![linkedlist_remove_node](linked_list.assets/linkedlist_remove_node.png)
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="linked_list.java"
|
||||
/* 删除链表的结点 n0 之后的首个结点 */
|
||||
void remove(ListNode n0) {
|
||||
if (n0.next == null)
|
||||
|
@ -346,13 +446,6 @@ comments: true
|
|||
=== "C++"
|
||||
|
||||
```cpp title="linked_list.cpp"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
void insert(ListNode* n0, ListNode* P) {
|
||||
ListNode* n1 = n0->next;
|
||||
n0->next = P;
|
||||
P->next = n1;
|
||||
}
|
||||
|
||||
/* 删除链表的结点 n0 之后的首个结点 */
|
||||
void remove(ListNode* n0) {
|
||||
if (n0->next == nullptr)
|
||||
|
@ -369,12 +462,6 @@ comments: true
|
|||
=== "Python"
|
||||
|
||||
```python title="linked_list.py"
|
||||
""" 在链表的结点 n0 之后插入结点 P """
|
||||
def insert(n0, P):
|
||||
n1 = n0.next
|
||||
n0.next = P
|
||||
P.next = n1
|
||||
|
||||
""" 删除链表的结点 n0 之后的首个结点 """
|
||||
def remove(n0):
|
||||
if not n0.next:
|
||||
|
@ -388,13 +475,6 @@ comments: true
|
|||
=== "Go"
|
||||
|
||||
```go title="linked_list.go"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
func insertNode(n0 *ListNode, P *ListNode) {
|
||||
n1 := n0.Next
|
||||
n0.Next = P
|
||||
P.Next = n1
|
||||
}
|
||||
|
||||
/* 删除链表的结点 n0 之后的首个结点 */
|
||||
func removeNode(n0 *ListNode) {
|
||||
if n0.Next == nil {
|
||||
|
@ -410,13 +490,6 @@ comments: true
|
|||
=== "JavaScript"
|
||||
|
||||
```javascript title="linked_list.js"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
function insert(n0, P) {
|
||||
const n1 = n0.next;
|
||||
n0.next = P;
|
||||
P.next = n1;
|
||||
}
|
||||
|
||||
/* 删除链表的结点 n0 之后的首个结点 */
|
||||
function remove(n0) {
|
||||
if (!n0.next)
|
||||
|
@ -431,13 +504,6 @@ comments: true
|
|||
=== "TypeScript"
|
||||
|
||||
```typescript title="linked_list.ts"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
function insert(n0: ListNode, P: ListNode): void {
|
||||
const n1 = n0.next;
|
||||
n0.next = P;
|
||||
P.next = n1;
|
||||
}
|
||||
|
||||
/* 删除链表的结点 n0 之后的首个结点 */
|
||||
function remove(n0: ListNode): void {
|
||||
if (!n0.next) {
|
||||
|
@ -453,22 +519,12 @@ comments: true
|
|||
=== "C"
|
||||
|
||||
```c title="linked_list.c"
|
||||
[class]{}-[func]{insertNode}
|
||||
|
||||
[class]{}-[func]{removeNode}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="linked_list.cs"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
void insert(ListNode n0, ListNode P)
|
||||
{
|
||||
ListNode? n1 = n0.next;
|
||||
n0.next = P;
|
||||
P.next = n1;
|
||||
}
|
||||
|
||||
/* 删除链表的结点 n0 之后的首个结点 */
|
||||
void remove(ListNode n0)
|
||||
{
|
||||
|
@ -484,13 +540,6 @@ comments: true
|
|||
=== "Swift"
|
||||
|
||||
```swift title="linked_list.swift"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
func insert(n0: ListNode, P: ListNode) {
|
||||
let n1 = n0.next
|
||||
n0.next = P
|
||||
P.next = n1
|
||||
}
|
||||
|
||||
/* 删除链表的结点 n0 之后的首个结点 */
|
||||
func remove(n0: ListNode) {
|
||||
if n0.next == nil {
|
||||
|
@ -507,13 +556,6 @@ comments: true
|
|||
=== "Zig"
|
||||
|
||||
```zig title="linked_list.zig"
|
||||
// 在链表的结点 n0 之后插入结点 P
|
||||
fn insert(n0: ?*inc.ListNode(i32), P: ?*inc.ListNode(i32)) void {
|
||||
var n1 = n0.?.next;
|
||||
n0.?.next = P;
|
||||
P.?.next = n1;
|
||||
}
|
||||
|
||||
// 删除链表的结点 n0 之后的首个结点
|
||||
fn remove(n0: ?*inc.ListNode(i32)) void {
|
||||
if (n0.?.next == null) return;
|
||||
|
|
|
@ -369,7 +369,7 @@ $$
|
|||
|
||||
```
|
||||
|
||||
![time_complexity_first_example](time_complexity.assets/time_complexity_first_example.png)
|
||||
![time_complexity_simple_example](time_complexity.assets/time_complexity_simple_example.png)
|
||||
|
||||
<p align="center"> Fig. 算法 A, B, C 的时间增长趋势 </p>
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ $$
|
|||
\end{aligned}
|
||||
$$
|
||||
|
||||
![IEEE-754-float](data_and_memory.assets/IEEE-754-float.png)
|
||||
![ieee_754_float](data_and_memory.assets/ieee_754_float.png)
|
||||
|
||||
以上图为例,$\mathrm{S} = 0$ , $\mathrm{E} = 124$ ,$\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$ ,易得
|
||||
|
||||
|
|
|
@ -1427,7 +1427,7 @@ $$
|
|||
T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \cdots + 2^{(h-1)}\times1
|
||||
$$
|
||||
|
||||
![heapify_count](heap.assets/heapify_count.png)
|
||||
![heapify_operations_count](heap.assets/heapify_operations_count.png)
|
||||
|
||||
化简上式需要借助中学的数列知识,先对 $T(h)$ 乘以 $2$ ,易得
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ comments: true
|
|||
|
||||
本书主要内容分为复杂度分析、数据结构、算法三个部分。
|
||||
|
||||
![mindmap](about_the_book.assets/mindmap.png)
|
||||
![hello_algo_mindmap](about_the_book.assets/hello_algo_mindmap.png)
|
||||
|
||||
<p align="center"> Fig. 知识点思维导图 </p>
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ comments: true
|
|||
2. 同理,对剩余 $n - 1$ 个元素执行「冒泡」,可将第二大元素交换至正确位置,因而待排序元素只剩 $n - 2$ 个。
|
||||
3. 以此类推…… **循环 $n - 1$ 轮「冒泡」,即可完成整个数组的排序**。
|
||||
|
||||
![bubble_sort](bubble_sort.assets/bubble_sort.png)
|
||||
![bubble_sort_overview](bubble_sort.assets/bubble_sort_overview.png)
|
||||
|
||||
<p align="center"> Fig. 冒泡排序流程 </p>
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ comments: true
|
|||
2. 第 2 轮选取 **第 3 个元素** 为 `base` ,执行「插入操作」后,**数组前 3 个元素已完成排序**。
|
||||
3. 以此类推……最后一轮选取 **数组尾元素** 为 `base` ,执行「插入操作」后,**所有元素已完成排序**。
|
||||
|
||||
![insertion_sort](insertion_sort.assets/insertion_sort.png)
|
||||
![insertion_sort_overview](insertion_sort.assets/insertion_sort_overview.png)
|
||||
|
||||
<p align="center"> Fig. 插入排序流程 </p>
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ comments: true
|
|||
1. **划分阶段**:通过递归不断 **将数组从中点位置划分开**,将长数组的排序问题转化为短数组的排序问题;
|
||||
2. **合并阶段**:划分到子数组长度为 1 时,开始向上合并,不断将 **左、右两个短排序数组** 合并为 **一个长排序数组**,直至合并至原数组时完成排序;
|
||||
|
||||
![merge_sort_preview](merge_sort.assets/merge_sort_preview.png)
|
||||
![merge_sort_overview](merge_sort.assets/merge_sort_overview.png)
|
||||
|
||||
<p align="center"> Fig. 归并排序两阶段:划分与合并 </p>
|
||||
|
||||
|
|
|
@ -296,7 +296,7 @@ comments: true
|
|||
|
||||
观察发现,快速排序和「二分查找」的原理类似,都是以对数阶的时间复杂度来缩小处理区间。
|
||||
|
||||
![quick_sort](quick_sort.assets/quick_sort.png)
|
||||
![quick_sort_overview](quick_sort.assets/quick_sort_overview.png)
|
||||
|
||||
<p align="center"> Fig. 快速排序流程 </p>
|
||||
|
||||
|
|
|
@ -8,11 +8,11 @@ comments: true
|
|||
|
||||
如下图所示,执行两步删除结点后,该二叉搜索树就会退化为链表。
|
||||
|
||||
![degradation_from_removing_node](avl_tree.assets/degradation_from_removing_node.png)
|
||||
![avltree_degradation_from_removing_node](avl_tree.assets/avltree_degradation_from_removing_node.png)
|
||||
|
||||
再比如,在以下完美二叉树中插入两个结点后,树严重向左偏斜,查找操作的时间复杂度也随之发生劣化。
|
||||
|
||||
![degradation_from_inserting_node](avl_tree.assets/degradation_from_inserting_node.png)
|
||||
![avltree_degradation_from_inserting_node](avl_tree.assets/avltree_degradation_from_inserting_node.png)
|
||||
|
||||
G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorithm for the organization of information" 中提出了「AVL 树」。**论文中描述了一系列操作,使得在不断添加与删除结点后,AVL 树仍然不会发生退化**,进而使得各种操作的时间复杂度均能保持在 $O(\log n)$ 级别。
|
||||
|
||||
|
@ -455,20 +455,20 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
|
|||
如下图所示(结点下方为「平衡因子」),从底至顶看,二叉树中首个失衡结点是 **结点 3**。我们聚焦在以该失衡结点为根结点的子树上,将该结点记为 `node` ,将其左子结点记为 `child` ,执行「右旋」操作。完成右旋后,该子树已经恢复平衡,并且仍然为二叉搜索树。
|
||||
|
||||
=== "<1>"
|
||||
![right_rotate_step1](avl_tree.assets/right_rotate_step1.png)
|
||||
![avltree_right_rotate_step1](avl_tree.assets/avltree_right_rotate_step1.png)
|
||||
|
||||
=== "<2>"
|
||||
![right_rotate_step2](avl_tree.assets/right_rotate_step2.png)
|
||||
![avltree_right_rotate_step2](avl_tree.assets/avltree_right_rotate_step2.png)
|
||||
|
||||
=== "<3>"
|
||||
![right_rotate_step3](avl_tree.assets/right_rotate_step3.png)
|
||||
![avltree_right_rotate_step3](avl_tree.assets/avltree_right_rotate_step3.png)
|
||||
|
||||
=== "<4>"
|
||||
![right_rotate_step4](avl_tree.assets/right_rotate_step4.png)
|
||||
![avltree_right_rotate_step4](avl_tree.assets/avltree_right_rotate_step4.png)
|
||||
|
||||
进而,如果结点 `child` 本身有右子结点(记为 `grandChild` ),则需要在「右旋」中添加一步:将 `grandChild` 作为 `node` 的左子结点。
|
||||
|
||||
![right_rotate_with_grandchild](avl_tree.assets/right_rotate_with_grandchild.png)
|
||||
![avltree_right_rotate_with_grandchild](avl_tree.assets/avltree_right_rotate_with_grandchild.png)
|
||||
|
||||
“向右旋转”是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。
|
||||
|
||||
|
@ -644,11 +644,11 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
|
|||
|
||||
类似地,如果将取上述失衡二叉树的“镜像”,那么则需要「左旋」操作。
|
||||
|
||||
![left_rotate](avl_tree.assets/left_rotate.png)
|
||||
![avltree_left_rotate](avl_tree.assets/avltree_left_rotate.png)
|
||||
|
||||
同理,若结点 `child` 本身有左子结点(记为 `grandChild` ),则需要在「左旋」中添加一步:将 `grandChild` 作为 `node` 的右子结点。
|
||||
|
||||
![left_rotate_with_grandchild](avl_tree.assets/left_rotate_with_grandchild.png)
|
||||
![avltree_left_rotate_with_grandchild](avl_tree.assets/avltree_left_rotate_with_grandchild.png)
|
||||
|
||||
观察发现,**「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的**。根据对称性,我们可以很方便地从「右旋」推导出「左旋」。具体地,只需将「右旋」代码中的把所有的 `left` 替换为 `right` 、所有的 `right` 替换为 `left` ,即可得到「左旋」代码。
|
||||
|
||||
|
@ -824,19 +824,19 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
|
|||
|
||||
对于下图的失衡结点 3 ,**单一使用左旋或右旋都无法使子树恢复平衡**,此时需要「先左旋后右旋」,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。
|
||||
|
||||
![left_right_rotate](avl_tree.assets/left_right_rotate.png)
|
||||
![avltree_left_right_rotate](avl_tree.assets/avltree_left_right_rotate.png)
|
||||
|
||||
### Case 4 - 先右后左
|
||||
|
||||
同理,取以上失衡二叉树的镜像,则需要「先右旋后左旋」,即先对 `child` 执行「右旋」,然后对 `node` 执行「左旋」。
|
||||
|
||||
![right_left_rotate](avl_tree.assets/right_left_rotate.png)
|
||||
![avltree_right_left_rotate](avl_tree.assets/avltree_right_left_rotate.png)
|
||||
|
||||
### 旋转的选择
|
||||
|
||||
下图描述的四种失衡情况与上述 Cases 逐个对应,分别需采用 **右旋、左旋、先右后左、先左后右** 的旋转操作。
|
||||
|
||||
![rotation_cases](avl_tree.assets/rotation_cases.png)
|
||||
![avltree_rotation_cases](avl_tree.assets/avltree_rotation_cases.png)
|
||||
|
||||
具体地,在代码中使用 **失衡结点的平衡因子、较高一侧子结点的平衡因子** 来确定失衡结点属于上图中的哪种情况。
|
||||
|
||||
|
|
|
@ -22,16 +22,16 @@ comments: true
|
|||
- 若 `cur.val = num` ,说明找到目标结点,跳出循环并返回该结点即可;
|
||||
|
||||
=== "<1>"
|
||||
![bst_search_1](binary_search_tree.assets/bst_search_1.png)
|
||||
![bst_search_step1](binary_search_tree.assets/bst_search_step1.png)
|
||||
|
||||
=== "<2>"
|
||||
![bst_search_2](binary_search_tree.assets/bst_search_2.png)
|
||||
![bst_search_step2](binary_search_tree.assets/bst_search_step2.png)
|
||||
|
||||
=== "<3>"
|
||||
![bst_search_3](binary_search_tree.assets/bst_search_3.png)
|
||||
![bst_search_step3](binary_search_tree.assets/bst_search_step3.png)
|
||||
|
||||
=== "<4>"
|
||||
![bst_search_4](binary_search_tree.assets/bst_search_4.png)
|
||||
![bst_search_step4](binary_search_tree.assets/bst_search_step4.png)
|
||||
|
||||
二叉搜索树的查找操作和二分查找算法如出一辙,也是在每轮排除一半情况。循环次数最多为二叉树的高度,当二叉树平衡时,使用 $O(\log n)$ 时间。
|
||||
|
||||
|
@ -562,16 +562,16 @@ comments: true
|
|||
3. 使用 `nex` 替换待删除结点;
|
||||
|
||||
=== "<1>"
|
||||
![bst_remove_case3_1](binary_search_tree.assets/bst_remove_case3_1.png)
|
||||
![bst_remove_case3_step1](binary_search_tree.assets/bst_remove_case3_step1.png)
|
||||
|
||||
=== "<2>"
|
||||
![bst_remove_case3_2](binary_search_tree.assets/bst_remove_case3_2.png)
|
||||
![bst_remove_case3_step2](binary_search_tree.assets/bst_remove_case3_step2.png)
|
||||
|
||||
=== "<3>"
|
||||
![bst_remove_case3_3](binary_search_tree.assets/bst_remove_case3_3.png)
|
||||
![bst_remove_case3_step3](binary_search_tree.assets/bst_remove_case3_step3.png)
|
||||
|
||||
=== "<4>"
|
||||
![bst_remove_case3_4](binary_search_tree.assets/bst_remove_case3_4.png)
|
||||
![bst_remove_case3_step4](binary_search_tree.assets/bst_remove_case3_step4.png)
|
||||
|
||||
删除结点操作也使用 $O(\log n)$ 时间,其中查找待删除结点 $O(\log n)$ ,获取中序遍历后继结点 $O(\log n)$ 。
|
||||
|
||||
|
|
Loading…
Reference in a new issue