From 01d05cc1f0cd12f2ba24a092fb9ddd1a4cffd3c1 Mon Sep 17 00:00:00 2001 From: krahets Date: Sun, 9 Apr 2023 04:34:58 +0800 Subject: [PATCH] build --- chapter_array_and_linkedlist/linked_list.md | 262 ++++---- chapter_array_and_linkedlist/summary.md | 4 +- .../space_complexity.md | 14 +- .../classification_of_data_structure.md | 2 +- chapter_graph/graph.md | 4 +- chapter_graph/graph_operations.md | 6 +- chapter_graph/graph_traversal.md | 2 +- chapter_hashing/hash_collision.md | 8 +- chapter_hashing/hash_map.md | 4 +- chapter_heap/build_heap.md | 36 +- chapter_heap/heap.md | 282 ++++---- chapter_searching/hashing_search.md | 24 +- chapter_searching/linear_search.md | 34 +- chapter_sorting/bucket_sort.md | 60 +- chapter_sorting/counting_sort.md | 114 +++- chapter_sorting/merge_sort.md | 4 +- chapter_sorting/radix_sort.md | 106 ++- chapter_stack_and_queue/deque.md | 196 +++--- chapter_stack_and_queue/queue.md | 94 +-- chapter_stack_and_queue/stack.md | 24 +- chapter_stack_and_queue/summary.md | 2 +- chapter_tree/avl_tree.md | 618 +++++++++--------- chapter_tree/binary_search_tree.md | 498 +++++++------- chapter_tree/binary_tree.md | 194 +++--- chapter_tree/binary_tree_traversal.md | 142 ++-- chapter_tree/summary.md | 14 +- 26 files changed, 1501 insertions(+), 1247 deletions(-) diff --git a/chapter_array_and_linkedlist/linked_list.md b/chapter_array_and_linkedlist/linked_list.md index 0d1de0b80..3282f4669 100755 --- a/chapter_array_and_linkedlist/linked_list.md +++ b/chapter_array_and_linkedlist/linked_list.md @@ -6,9 +6,9 @@ comments: true 内存空间是所有程序的公共资源,排除已被占用的内存空间,空闲内存空间通常散落在内存各处。在上一节中,我们提到存储数组的内存空间必须是连续的,而当我们需要申请一个非常大的数组时,空闲内存中可能没有这么大的连续空间。 -与数组相比,链表更具灵活性,因为它可以存储在非连续的内存空间。「链表 Linked List」是一种线性数据结构,其每个元素都是一个结点对象,各个结点之间通过指针连接,从当前结点通过指针可以访问到下一个结点。由于指针记录了下个结点的内存地址,因此无需保证内存地址的连续性,从而可以将各个结点分散存储在内存各处。 +与数组相比,链表更具灵活性,因为它可以存储在非连续的内存空间。「链表 Linked List」是一种线性数据结构,其每个元素都是一个节点对象,各个节点之间通过指针连接,从当前节点通过指针可以访问到下一个节点。由于指针记录了下个节点的内存地址,因此无需保证内存地址的连续性,从而可以将各个节点分散存储在内存各处。 -链表「结点 Node」包含两项数据,一是结点「值 Value」,二是指向下一结点的「指针 Pointer」,或称指向下一结点的「引用 Reference」。 +链表「节点 Node」包含两项数据,一是节点「值 Value」,二是指向下一节点的「指针 Pointer」,或称指向下一节点的「引用 Reference」。 ![链表定义与存储方式](linked_list.assets/linkedlist_definition.png) @@ -17,10 +17,10 @@ comments: true === "Java" ```java title="" - /* 链表结点类 */ + /* 链表节点类 */ class ListNode { - int val; // 结点值 - ListNode next; // 指向下一结点的指针(引用) + int val; // 节点值 + ListNode next; // 指向下一节点的指针(引用) ListNode(int x) { val = x; } // 构造函数 } ``` @@ -28,10 +28,10 @@ comments: true === "C++" ```cpp title="" - /* 链表结点结构体 */ + /* 链表节点结构体 */ struct ListNode { - int val; // 结点值 - ListNode *next; // 指向下一结点的指针(引用) + int val; // 节点值 + ListNode *next; // 指向下一节点的指针(引用) ListNode(int x) : val(x), next(nullptr) {} // 构造函数 }; ``` @@ -39,20 +39,20 @@ comments: true === "Python" ```python title="" - """ 链表结点类 """ + """ 链表节点类 """ class ListNode: def __init__(self, val: int): - self.val: int = val # 结点值 - self.next: Optional[ListNode] = None # 指向下一结点的指针(引用) + self.val: int = val # 节点值 + self.next: Optional[ListNode] = None # 指向下一节点的指针(引用) ``` === "Go" ```go title="" - /* 链表结点结构体 */ + /* 链表节点结构体 */ type ListNode struct { - Val int // 结点值 - Next *ListNode // 指向下一结点的指针(引用) + Val int // 节点值 + Next *ListNode // 指向下一节点的指针(引用) } // NewListNode 构造函数,创建一个新的链表 @@ -67,13 +67,13 @@ comments: true === "JavaScript" ```javascript title="" - /* 链表结点类 */ + /* 链表节点类 */ class ListNode { val; next; constructor(val, next) { - this.val = (val === undefined ? 0 : val); // 结点值 - this.next = (next === undefined ? null : next); // 指向下一结点的引用 + this.val = (val === undefined ? 0 : val); // 节点值 + this.next = (next === undefined ? null : next); // 指向下一节点的引用 } } ``` @@ -81,13 +81,13 @@ comments: true === "TypeScript" ```typescript title="" - /* 链表结点类 */ + /* 链表节点类 */ class ListNode { val: number; next: ListNode | null; constructor(val?: number, next?: ListNode | null) { - this.val = val === undefined ? 0 : val; // 结点值 - this.next = next === undefined ? null : next; // 指向下一结点的引用 + this.val = val === undefined ? 0 : val; // 节点值 + this.next = next === undefined ? null : next; // 指向下一节点的引用 } } ``` @@ -95,16 +95,16 @@ comments: true === "C" ```c title="" - /* 链表结点结构体 */ + /* 链表节点结构体 */ struct ListNode { - int val; // 结点值 - struct ListNode *next; // 指向下一结点的指针(引用) + int val; // 节点值 + struct ListNode *next; // 指向下一节点的指针(引用) }; // typedef 作用是为一种数据类型定义一个新名字 typedef struct ListNode ListNode; - /* 构造函数,初始化一个新结点 */ + /* 构造函数,初始化一个新节点 */ ListNode *newListNode(int val) { ListNode *node, *next; node = (ListNode *) malloc(sizeof(ListNode)); @@ -117,11 +117,11 @@ comments: true === "C#" ```csharp title="" - /* 链表结点类 */ + /* 链表节点类 */ class ListNode { - int val; // 结点值 - ListNode next; // 指向下一结点的引用 + int val; // 节点值 + ListNode next; // 指向下一节点的引用 ListNode(int x) => val = x; //构造函数 } ``` @@ -129,10 +129,10 @@ comments: true === "Swift" ```swift title="" - /* 链表结点类 */ + /* 链表节点类 */ class ListNode { - var val: Int // 结点值 - var next: ListNode? // 指向下一结点的指针(引用) + var val: Int // 节点值 + var next: ListNode? // 指向下一节点的指针(引用) init(x: Int) { // 构造函数 val = x @@ -143,13 +143,13 @@ comments: true === "Zig" ```zig title="" - // 链表结点类 + // 链表节点类 pub fn ListNode(comptime T: type) type { return struct { const Self = @This(); - val: T = 0, // 结点值 - next: ?*Self = null, // 指向下一结点的指针(引用) + val: T = 0, // 节点值 + next: ?*Self = null, // 指向下一节点的指针(引用) // 构造函数 pub fn init(self: *Self, x: i32) void { @@ -160,21 +160,21 @@ comments: true } ``` -!!! question "尾结点指向什么?" +!!! question "尾节点指向什么?" - 我们将链表的最后一个结点称为「尾结点」,其指向的是“空”,在 Java, C++, Python 中分别记为 `null`, `nullptr`, `None` 。在不引起歧义的前提下,本书都使用 `null` 来表示空。 + 我们将链表的最后一个节点称为「尾节点」,其指向的是“空”,在 Java, C++, Python 中分别记为 `null`, `nullptr`, `None` 。在不引起歧义的前提下,本书都使用 `null` 来表示空。 !!! question "如何称呼链表?" - 在编程语言中,数组整体就是一个变量,例如数组 `nums` ,包含各个元素 `nums[0]` , `nums[1]` 等等。而链表是由多个结点对象组成,我们通常将头结点当作链表的代称,例如头结点 `head` 和链表 `head` 实际上是同义的。 + 在编程语言中,数组整体就是一个变量,例如数组 `nums` ,包含各个元素 `nums[0]` , `nums[1]` 等等。而链表是由多个节点对象组成,我们通常将头节点当作链表的代称,例如头节点 `head` 和链表 `head` 实际上是同义的。 -**链表初始化方法**。建立链表分为两步,第一步是初始化各个结点对象,第二步是构建引用指向关系。完成后,即可以从链表的头结点(即首个结点)出发,通过指针 `next` 依次访问所有结点。 +**链表初始化方法**。建立链表分为两步,第一步是初始化各个节点对象,第二步是构建引用指向关系。完成后,即可以从链表的头节点(即首个节点)出发,通过指针 `next` 依次访问所有节点。 === "Java" ```java title="linked_list.java" /* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */ - // 初始化各个结点 + // 初始化各个节点 ListNode n0 = new ListNode(1); ListNode n1 = new ListNode(3); ListNode n2 = new ListNode(2); @@ -191,7 +191,7 @@ comments: true ```cpp title="linked_list.cpp" /* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */ - // 初始化各个结点 + // 初始化各个节点 ListNode* n0 = new ListNode(1); ListNode* n1 = new ListNode(3); ListNode* n2 = new ListNode(2); @@ -208,7 +208,7 @@ comments: true ```python title="linked_list.py" """ 初始化链表 1 -> 3 -> 2 -> 5 -> 4 """ - # 初始化各个结点 + # 初始化各个节点 n0 = ListNode(1) n1 = ListNode(3) n2 = ListNode(2) @@ -225,7 +225,7 @@ comments: true ```go title="linked_list.go" /* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */ - // 初始化各个结点 + // 初始化各个节点 n0 := NewListNode(1) n1 := NewListNode(3) n2 := NewListNode(2) @@ -242,7 +242,7 @@ comments: true ```javascript title="linked_list.js" /* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */ - // 初始化各个结点 + // 初始化各个节点 const n0 = new ListNode(1); const n1 = new ListNode(3); const n2 = new ListNode(2); @@ -259,7 +259,7 @@ comments: true ```typescript title="linked_list.ts" /* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */ - // 初始化各个结点 + // 初始化各个节点 const n0 = new ListNode(1); const n1 = new ListNode(3); const n2 = new ListNode(2); @@ -276,7 +276,7 @@ comments: true ```c title="linked_list.c" /* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */ - // 初始化各个结点 + // 初始化各个节点 ListNode* n0 = newListNode(1); ListNode* n1 = newListNode(3); ListNode* n2 = newListNode(2); @@ -293,7 +293,7 @@ comments: true ```csharp title="linked_list.cs" /* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */ - // 初始化各个结点 + // 初始化各个节点 ListNode n0 = new ListNode(1); ListNode n1 = new ListNode(3); ListNode n2 = new ListNode(2); @@ -310,7 +310,7 @@ comments: true ```swift title="linked_list.swift" /* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */ - // 初始化各个结点 + // 初始化各个节点 let n0 = ListNode(x: 1) let n1 = ListNode(x: 3) let n2 = ListNode(x: 2) @@ -327,7 +327,7 @@ comments: true ```zig title="linked_list.zig" // 初始化链表 - // 初始化各个结点 + // 初始化各个节点 var n0 = inc.ListNode(i32){.val = 1}; var n1 = inc.ListNode(i32){.val = 3}; var n2 = inc.ListNode(i32){.val = 2}; @@ -342,16 +342,16 @@ comments: true ## 4.2.1.   链表优点 -**链表中插入与删除结点的操作效率高**。例如,如果我们想在链表中间的两个结点 `A` , `B` 之间插入一个新结点 `P` ,我们只需要改变两个结点指针即可,时间复杂度为 $O(1)$ ;相比之下,数组的插入操作效率要低得多。 +**链表中插入与删除节点的操作效率高**。例如,如果我们想在链表中间的两个节点 `A` , `B` 之间插入一个新节点 `P` ,我们只需要改变两个节点指针即可,时间复杂度为 $O(1)$ ;相比之下,数组的插入操作效率要低得多。 -![链表插入结点](linked_list.assets/linkedlist_insert_node.png) +![链表插入节点](linked_list.assets/linkedlist_insert_node.png) -

Fig. 链表插入结点

+

Fig. 链表插入节点

=== "Java" ```java title="linked_list.java" - /* 在链表的结点 n0 之后插入结点 P */ + /* 在链表的节点 n0 之后插入节点 P */ void insert(ListNode n0, ListNode P) { ListNode n1 = n0.next; P.next = n1; @@ -362,7 +362,7 @@ comments: true === "C++" ```cpp title="linked_list.cpp" - /* 在链表的结点 n0 之后插入结点 P */ + /* 在链表的节点 n0 之后插入节点 P */ void insert(ListNode* n0, ListNode* P) { ListNode* n1 = n0->next; P->next = n1; @@ -374,7 +374,7 @@ comments: true ```python title="linked_list.py" def insert(n0: ListNode, P: ListNode) -> None: - """ 在链表的结点 n0 之后插入结点 P """ + """ 在链表的节点 n0 之后插入节点 P """ n1 = n0.next P.next = n1 n0.next = P @@ -383,7 +383,7 @@ comments: true === "Go" ```go title="linked_list.go" - /* 在链表的结点 n0 之后插入结点 P */ + /* 在链表的节点 n0 之后插入节点 P */ func insertNode(n0 *ListNode, P *ListNode) { n1 := n0.Next P.Next = n1 @@ -394,7 +394,7 @@ comments: true === "JavaScript" ```javascript title="linked_list.js" - /* 在链表的结点 n0 之后插入结点 P */ + /* 在链表的节点 n0 之后插入节点 P */ function insert(n0, P) { const n1 = n0.next; P.next = n1; @@ -405,7 +405,7 @@ comments: true === "TypeScript" ```typescript title="linked_list.ts" - /* 在链表的结点 n0 之后插入结点 P */ + /* 在链表的节点 n0 之后插入节点 P */ function insert(n0: ListNode, P: ListNode): void { const n1 = n0.next; P.next = n1; @@ -422,7 +422,7 @@ comments: true === "C#" ```csharp title="linked_list.cs" - /* 在链表的结点 n0 之后插入结点 P */ + /* 在链表的节点 n0 之后插入节点 P */ void insert(ListNode n0, ListNode P) { ListNode? n1 = n0.next; @@ -434,7 +434,7 @@ comments: true === "Swift" ```swift title="linked_list.swift" - /* 在链表的结点 n0 之后插入结点 P */ + /* 在链表的节点 n0 之后插入节点 P */ func insert(n0: ListNode, P: ListNode) { let n1 = n0.next P.next = n1 @@ -445,7 +445,7 @@ comments: true === "Zig" ```zig title="linked_list.zig" - // 在链表的结点 n0 之后插入结点 P + // 在链表的节点 n0 之后插入节点 P fn insert(n0: ?*inc.ListNode(i32), P: ?*inc.ListNode(i32)) void { var n1 = n0.?.next; P.?.next = n1; @@ -453,16 +453,16 @@ comments: true } ``` -在链表中删除结点也非常方便,只需改变一个结点的指针即可。如下图所示,尽管在删除操作完成后,结点 `P` 仍然指向 `n1`,但实际上 `P` 已经不再属于此链表,因为遍历此链表时无法访问到 `P`。 +在链表中删除节点也非常方便,只需改变一个节点的指针即可。如下图所示,尽管在删除操作完成后,节点 `P` 仍然指向 `n1`,但实际上 `P` 已经不再属于此链表,因为遍历此链表时无法访问到 `P`。 -![链表删除结点](linked_list.assets/linkedlist_remove_node.png) +![链表删除节点](linked_list.assets/linkedlist_remove_node.png) -

Fig. 链表删除结点

+

Fig. 链表删除节点

=== "Java" ```java title="linked_list.java" - /* 删除链表的结点 n0 之后的首个结点 */ + /* 删除链表的节点 n0 之后的首个节点 */ void remove(ListNode n0) { if (n0.next == null) return; @@ -476,7 +476,7 @@ comments: true === "C++" ```cpp title="linked_list.cpp" - /* 删除链表的结点 n0 之后的首个结点 */ + /* 删除链表的节点 n0 之后的首个节点 */ void remove(ListNode* n0) { if (n0->next == nullptr) return; @@ -493,7 +493,7 @@ comments: true ```python title="linked_list.py" def remove(n0: ListNode) -> None: - """ 删除链表的结点 n0 之后的首个结点 """ + """ 删除链表的节点 n0 之后的首个节点 """ if not n0.next: return # n0 -> P -> n1 @@ -505,7 +505,7 @@ comments: true === "Go" ```go title="linked_list.go" - /* 删除链表的结点 n0 之后的首个结点 */ + /* 删除链表的节点 n0 之后的首个节点 */ func removeNode(n0 *ListNode) { if n0.Next == nil { return @@ -520,7 +520,7 @@ comments: true === "JavaScript" ```javascript title="linked_list.js" - /* 删除链表的结点 n0 之后的首个结点 */ + /* 删除链表的节点 n0 之后的首个节点 */ function remove(n0) { if (!n0.next) return; @@ -534,7 +534,7 @@ comments: true === "TypeScript" ```typescript title="linked_list.ts" - /* 删除链表的结点 n0 之后的首个结点 */ + /* 删除链表的节点 n0 之后的首个节点 */ function remove(n0: ListNode): void { if (!n0.next) { return; @@ -555,7 +555,7 @@ comments: true === "C#" ```csharp title="linked_list.cs" - /* 删除链表的结点 n0 之后的首个结点 */ + /* 删除链表的节点 n0 之后的首个节点 */ void remove(ListNode n0) { if (n0.next == null) @@ -570,7 +570,7 @@ comments: true === "Swift" ```swift title="linked_list.swift" - /* 删除链表的结点 n0 之后的首个结点 */ + /* 删除链表的节点 n0 之后的首个节点 */ func remove(n0: ListNode) { if n0.next == nil { return @@ -586,7 +586,7 @@ comments: true === "Zig" ```zig title="linked_list.zig" - // 删除链表的结点 n0 之后的首个结点 + // 删除链表的节点 n0 之后的首个节点 fn remove(n0: ?*inc.ListNode(i32)) void { if (n0.?.next == null) return; // n0 -> P -> n1 @@ -598,12 +598,12 @@ comments: true ## 4.2.2.   链表缺点 -**链表访问结点效率较低**。如上节所述,数组可以在 $O(1)$ 时间下访问任意元素。然而,链表无法直接访问任意结点,这是因为系统需要从头结点出发,逐个向后遍历直至找到目标结点。例如,若要访问链表索引为 `index`(即第 `index + 1` 个)的结点,则需要向后遍历 `index` 轮。 +**链表访问节点效率较低**。如上节所述,数组可以在 $O(1)$ 时间下访问任意元素。然而,链表无法直接访问任意节点,这是因为系统需要从头节点出发,逐个向后遍历直至找到目标节点。例如,若要访问链表索引为 `index`(即第 `index + 1` 个)的节点,则需要向后遍历 `index` 轮。 === "Java" ```java title="linked_list.java" - /* 访问链表中索引为 index 的结点 */ + /* 访问链表中索引为 index 的节点 */ ListNode access(ListNode head, int index) { for (int i = 0; i < index; i++) { if (head == null) @@ -617,7 +617,7 @@ comments: true === "C++" ```cpp title="linked_list.cpp" - /* 访问链表中索引为 index 的结点 */ + /* 访问链表中索引为 index 的节点 */ ListNode* access(ListNode* head, int index) { for (int i = 0; i < index; i++) { if (head == nullptr) @@ -632,7 +632,7 @@ comments: true ```python title="linked_list.py" def access(head: ListNode, index: int) -> ListNode | None: - """ 访问链表中索引为 index 的结点 """ + """ 访问链表中索引为 index 的节点 """ for _ in range(index): if not head: return None @@ -643,7 +643,7 @@ comments: true === "Go" ```go title="linked_list.go" - /* 访问链表中索引为 index 的结点 */ + /* 访问链表中索引为 index 的节点 */ func access(head *ListNode, index int) *ListNode { for i := 0; i < index; i++ { if head == nil { @@ -658,7 +658,7 @@ comments: true === "JavaScript" ```javascript title="linked_list.js" - /* 访问链表中索引为 index 的结点 */ + /* 访问链表中索引为 index 的节点 */ function access(head, index) { for (let i = 0; i < index; i++) { if (!head) { @@ -673,7 +673,7 @@ comments: true === "TypeScript" ```typescript title="linked_list.ts" - /* 访问链表中索引为 index 的结点 */ + /* 访问链表中索引为 index 的节点 */ function access(head: ListNode | null, index: number): ListNode | null { for (let i = 0; i < index; i++) { if (!head) { @@ -694,7 +694,7 @@ comments: true === "C#" ```csharp title="linked_list.cs" - /* 访问链表中索引为 index 的结点 */ + /* 访问链表中索引为 index 的节点 */ ListNode? access(ListNode head, int index) { for (int i = 0; i < index; i++) @@ -710,7 +710,7 @@ comments: true === "Swift" ```swift title="linked_list.swift" - /* 访问链表中索引为 index 的结点 */ + /* 访问链表中索引为 index 的节点 */ func access(head: ListNode, index: Int) -> ListNode? { var head: ListNode? = head for _ in 0 ..< index { @@ -726,7 +726,7 @@ comments: true === "Zig" ```zig title="linked_list.zig" - // 访问链表中索引为 index 的结点 + // 访问链表中索引为 index 的节点 fn access(node: ?*inc.ListNode(i32), index: i32) ?*inc.ListNode(i32) { var head = node; var i: i32 = 0; @@ -738,16 +738,16 @@ comments: true } ``` -**链表的内存占用较大**。链表以结点为单位,每个结点除了保存值之外,还需额外保存指针(引用)。这意味着在相同数据量的情况下,链表比数组需要占用更多的内存空间。 +**链表的内存占用较大**。链表以节点为单位,每个节点除了保存值之外,还需额外保存指针(引用)。这意味着在相同数据量的情况下,链表比数组需要占用更多的内存空间。 ## 4.2.3.   链表常用操作 -**遍历链表查找**。遍历链表,查找链表内值为 `target` 的结点,输出结点在链表中的索引。 +**遍历链表查找**。遍历链表,查找链表内值为 `target` 的节点,输出节点在链表中的索引。 === "Java" ```java title="linked_list.java" - /* 在链表中查找值为 target 的首个结点 */ + /* 在链表中查找值为 target 的首个节点 */ int find(ListNode head, int target) { int index = 0; while (head != null) { @@ -763,7 +763,7 @@ comments: true === "C++" ```cpp title="linked_list.cpp" - /* 在链表中查找值为 target 的首个结点 */ + /* 在链表中查找值为 target 的首个节点 */ int find(ListNode* head, int target) { int index = 0; while (head != nullptr) { @@ -780,7 +780,7 @@ comments: true ```python title="linked_list.py" def find(head: ListNode, target: int) -> int: - """ 在链表中查找值为 target 的首个结点 """ + """ 在链表中查找值为 target 的首个节点 """ index = 0 while head: if head.val == target: @@ -793,7 +793,7 @@ comments: true === "Go" ```go title="linked_list.go" - /* 在链表中查找值为 target 的首个结点 */ + /* 在链表中查找值为 target 的首个节点 */ func findNode(head *ListNode, target int) int { index := 0 for head != nil { @@ -810,7 +810,7 @@ comments: true === "JavaScript" ```javascript title="linked_list.js" - /* 在链表中查找值为 target 的首个结点 */ + /* 在链表中查找值为 target 的首个节点 */ function find(head, target) { let index = 0; while (head !== null) { @@ -827,7 +827,7 @@ comments: true === "TypeScript" ```typescript title="linked_list.ts" - /* 在链表中查找值为 target 的首个结点 */ + /* 在链表中查找值为 target 的首个节点 */ function find(head: ListNode | null, target: number): number { let index = 0; while (head !== null) { @@ -850,7 +850,7 @@ comments: true === "C#" ```csharp title="linked_list.cs" - /* 在链表中查找值为 target 的首个结点 */ + /* 在链表中查找值为 target 的首个节点 */ int find(ListNode head, int target) { int index = 0; @@ -868,7 +868,7 @@ comments: true === "Swift" ```swift title="linked_list.swift" - /* 在链表中查找值为 target 的首个结点 */ + /* 在链表中查找值为 target 的首个节点 */ func find(head: ListNode, target: Int) -> Int { var head: ListNode? = head var index = 0 @@ -886,7 +886,7 @@ comments: true === "Zig" ```zig title="linked_list.zig" - // 在链表中查找值为 target 的首个结点 + // 在链表中查找值为 target 的首个节点 fn find(node: ?*inc.ListNode(i32), target: i32) i32 { var head = node; var index: i32 = 0; @@ -901,20 +901,20 @@ comments: true ## 4.2.4.   常见链表类型 -**单向链表**。即上述介绍的普通链表。单向链表的结点包含值和指向下一结点的指针(引用)两项数据。我们将首个结点称为头结点,将最后一个结点成为尾结点,尾结点指向 `null` 。 +**单向链表**。即上述介绍的普通链表。单向链表的节点包含值和指向下一节点的指针(引用)两项数据。我们将首个节点称为头节点,将最后一个节点成为尾节点,尾节点指向 `null` 。 -**环形链表**。如果我们令单向链表的尾结点指向头结点(即首尾相接),则得到一个环形链表。在环形链表中,任意结点都可以视作头结点。 +**环形链表**。如果我们令单向链表的尾节点指向头节点(即首尾相接),则得到一个环形链表。在环形链表中,任意节点都可以视作头节点。 -**双向链表**。与单向链表相比,双向链表记录了两个方向的指针(引用)。双向链表的结点定义同时包含指向后继结点(下一结点)和前驱结点(上一结点)的指针。相较于单向链表,双向链表更具灵活性,可以朝两个方向遍历链表,但相应地也需要占用更多的内存空间。 +**双向链表**。与单向链表相比,双向链表记录了两个方向的指针(引用)。双向链表的节点定义同时包含指向后继节点(下一节点)和前驱节点(上一节点)的指针。相较于单向链表,双向链表更具灵活性,可以朝两个方向遍历链表,但相应地也需要占用更多的内存空间。 === "Java" ```java title="" - /* 双向链表结点类 */ + /* 双向链表节点类 */ class ListNode { - int val; // 结点值 - ListNode next; // 指向后继结点的指针(引用) - ListNode prev; // 指向前驱结点的指针(引用) + int val; // 节点值 + ListNode next; // 指向后继节点的指针(引用) + ListNode prev; // 指向前驱节点的指针(引用) ListNode(int x) { val = x; } // 构造函数 } ``` @@ -922,11 +922,11 @@ comments: true === "C++" ```cpp title="" - /* 双向链表结点结构体 */ + /* 双向链表节点结构体 */ struct ListNode { - int val; // 结点值 - ListNode *next; // 指向后继结点的指针(引用) - ListNode *prev; // 指向前驱结点的指针(引用) + int val; // 节点值 + ListNode *next; // 指向后继节点的指针(引用) + ListNode *prev; // 指向前驱节点的指针(引用) ListNode(int x) : val(x), next(nullptr), prev(nullptr) {} // 构造函数 }; ``` @@ -934,22 +934,22 @@ comments: true === "Python" ```python title="" - """ 双向链表结点类 """ + """ 双向链表节点类 """ class ListNode: def __init__(self, val: int): - self.val: int = val # 结点值 - self.next: Optional[ListNode] = None # 指向后继结点的指针(引用) - self.prev: Optional[ListNode] = None # 指向前驱结点的指针(引用) + self.val: int = val # 节点值 + self.next: Optional[ListNode] = None # 指向后继节点的指针(引用) + self.prev: Optional[ListNode] = None # 指向前驱节点的指针(引用) ``` === "Go" ```go title="" - /* 双向链表结点结构体 */ + /* 双向链表节点结构体 */ type DoublyListNode struct { - Val int // 结点值 - Next *DoublyListNode // 指向后继结点的指针(引用) - Prev *DoublyListNode // 指向前驱结点的指针(引用) + Val int // 节点值 + Next *DoublyListNode // 指向后继节点的指针(引用) + Prev *DoublyListNode // 指向前驱节点的指针(引用) } // NewDoublyListNode 初始化 @@ -965,15 +965,15 @@ comments: true === "JavaScript" ```javascript title="" - /* 双向链表结点类 */ + /* 双向链表节点类 */ class ListNode { val; next; prev; constructor(val, next) { - this.val = val === undefined ? 0 : val; // 结点值 - this.next = next === undefined ? null : next; // 指向后继结点的指针(引用) - this.prev = prev === undefined ? null : prev; // 指向前驱结点的指针(引用) + this.val = val === undefined ? 0 : val; // 节点值 + this.next = next === undefined ? null : next; // 指向后继节点的指针(引用) + this.prev = prev === undefined ? null : prev; // 指向前驱节点的指针(引用) } } ``` @@ -981,15 +981,15 @@ comments: true === "TypeScript" ```typescript title="" - /* 双向链表结点类 */ + /* 双向链表节点类 */ class ListNode { val: number; next: ListNode | null; prev: ListNode | null; constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) { - this.val = val === undefined ? 0 : val; // 结点值 - this.next = next === undefined ? null : next; // 指向后继结点的指针(引用) - this.prev = prev === undefined ? null : prev; // 指向前驱结点的指针(引用) + this.val = val === undefined ? 0 : val; // 节点值 + this.next = next === undefined ? null : next; // 指向后继节点的指针(引用) + this.prev = prev === undefined ? null : prev; // 指向前驱节点的指针(引用) } } ``` @@ -1003,11 +1003,11 @@ comments: true === "C#" ```csharp title="" - /* 双向链表结点类 */ + /* 双向链表节点类 */ class ListNode { - int val; // 结点值 - ListNode next; // 指向后继结点的指针(引用) - ListNode prev; // 指向前驱结点的指针(引用) + int val; // 节点值 + ListNode next; // 指向后继节点的指针(引用) + ListNode prev; // 指向前驱节点的指针(引用) ListNode(int x) => val = x; // 构造函数 } ``` @@ -1015,11 +1015,11 @@ comments: true === "Swift" ```swift title="" - /* 双向链表结点类 */ + /* 双向链表节点类 */ class ListNode { - var val: Int // 结点值 - var next: ListNode? // 指向后继结点的指针(引用) - var prev: ListNode? // 指向前驱结点的指针(引用) + var val: Int // 节点值 + var next: ListNode? // 指向后继节点的指针(引用) + var prev: ListNode? // 指向前驱节点的指针(引用) init(x: Int) { // 构造函数 val = x @@ -1030,14 +1030,14 @@ comments: true === "Zig" ```zig title="" - // 双向链表结点类 + // 双向链表节点类 pub fn ListNode(comptime T: type) type { return struct { const Self = @This(); - val: T = 0, // 结点值 - next: ?*Self = null, // 指向后继结点的指针(引用) - prev: ?*Self = null, // 指向前驱结点的指针(引用) + val: T = 0, // 节点值 + next: ?*Self = null, // 指向后继节点的指针(引用) + prev: ?*Self = null, // 指向前驱节点的指针(引用) // 构造函数 pub fn init(self: *Self, x: i32) void { diff --git a/chapter_array_and_linkedlist/summary.md b/chapter_array_and_linkedlist/summary.md index 3c4cf80ce..994990aa2 100644 --- a/chapter_array_and_linkedlist/summary.md +++ b/chapter_array_and_linkedlist/summary.md @@ -6,7 +6,7 @@ comments: true - 数组和链表是两种基本数据结构,分别代表数据在计算机内存中的连续空间存储和离散空间存储方式。两者的优缺点呈现出互补的特性。 - 数组支持随机访问、占用内存较少;但插入和删除元素效率低,且初始化后长度不可变。 -- 链表通过更改指针实现高效的结点插入与删除,且可以灵活调整长度;但结点访问效率低、占用内存较多。常见的链表类型包括单向链表、循环链表、双向链表。 +- 链表通过更改指针实现高效的节点插入与删除,且可以灵活调整长度;但节点访问效率低、占用内存较多。常见的链表类型包括单向链表、循环链表、双向链表。 - 动态数组,又称列表,是基于数组实现的一种数据结构。它保留了数组的优势,同时可以灵活调整长度。列表的出现极大地提高了数组的易用性,但可能导致部分内存空间浪费。 - 下表总结并对比了数组与链表的各项特性。 @@ -23,7 +23,7 @@ comments: true !!! note "缓存局部性" - 在计算机中,数据读写速度排序是“硬盘 < 内存 < CPU 缓存”。当我们访问数组元素时,计算机不仅会加载它,还会缓存其周围的其它数据,从而借助高速缓存来提升后续操作的执行速度。链表则不然,计算机只能挨个地缓存各个结点,这样的多次“搬运”降低了整体效率。 + 在计算机中,数据读写速度排序是“硬盘 < 内存 < CPU 缓存”。当我们访问数组元素时,计算机不仅会加载它,还会缓存其周围的其它数据,从而借助高速缓存来提升后续操作的执行速度。链表则不然,计算机只能挨个地缓存各个节点,这样的多次“搬运”降低了整体效率。 - 下表对比了数组与链表在各种操作上的效率。 diff --git a/chapter_computational_complexity/space_complexity.md b/chapter_computational_complexity/space_complexity.md index 72f1e4695..0e2df14af 100755 --- a/chapter_computational_complexity/space_complexity.md +++ b/chapter_computational_complexity/space_complexity.md @@ -84,8 +84,8 @@ comments: true """ 类 """ class Node: def __init__(self, x: int): - self.val: int = x # 结点值 - self.next: Optional[Node] = None # 指向下一结点的指针(引用) + self.val: int = x # 节点值 + self.next: Optional[Node] = None # 指向下一节点的指针(引用) """ 函数 """ def function() -> int: @@ -137,8 +137,8 @@ comments: true val; next; constructor(val) { - this.val = val === undefined ? 0 : val; // 结点值 - this.next = null; // 指向下一结点的引用 + this.val = val === undefined ? 0 : val; // 节点值 + this.next = null; // 指向下一节点的引用 } } @@ -165,8 +165,8 @@ comments: true val: number; next: Node | null; constructor(val?: number) { - this.val = val === undefined ? 0 : val; // 结点值 - this.next = null; // 指向下一结点的引用 + this.val = val === undefined ? 0 : val; // 节点值 + this.next = null; // 指向下一节点的引用 } } @@ -1375,7 +1375,7 @@ $$ ### 指数阶 $O(2^n)$ -指数阶常见于二叉树。高度为 $n$ 的「满二叉树」的结点数量为 $2^n - 1$ ,占用 $O(2^n)$ 空间。 +指数阶常见于二叉树。高度为 $n$ 的「满二叉树」的节点数量为 $2^n - 1$ ,占用 $O(2^n)$ 空间。 === "Java" diff --git a/chapter_data_structure/classification_of_data_structure.md b/chapter_data_structure/classification_of_data_structure.md index 68044f7f3..99b92f861 100644 --- a/chapter_data_structure/classification_of_data_structure.md +++ b/chapter_data_structure/classification_of_data_structure.md @@ -8,7 +8,7 @@ comments: true ## 3.2.1.   逻辑结构:线性与非线性 -**「逻辑结构」揭示了数据元素之间的逻辑关系**。在数组和链表中,数据按照顺序依次排列,体现了数据之间的线性关系;而在树中,数据从顶部向下按层次排列,表现出祖先与后代之间的派生关系;图则由结点和边构成,反映了复杂的网络关系。 +**「逻辑结构」揭示了数据元素之间的逻辑关系**。在数组和链表中,数据按照顺序依次排列,体现了数据之间的线性关系;而在树中,数据从顶部向下按层次排列,表现出祖先与后代之间的派生关系;图则由节点和边构成,反映了复杂的网络关系。 逻辑结构通常分为「线性」和「非线性」两类。线性结构比较直观,指数据在逻辑关系上呈线性排列;非线性结构则相反,呈非线性排列,例如网状或树状结构。 diff --git a/chapter_graph/graph.md b/chapter_graph/graph.md index f4fd6c084..4fabc408b 100644 --- a/chapter_graph/graph.md +++ b/chapter_graph/graph.md @@ -18,7 +18,7 @@ $$

Fig. 链表、树、图之间的关系

-那么,图与其他数据结构的关系是什么?如果我们把「顶点」看作结点,把「边」看作连接各个结点的指针,则可将「图」看作是一种从「链表」拓展而来的数据结构。**相较于线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,从而更为复杂**。 +那么,图与其他数据结构的关系是什么?如果我们把「顶点」看作节点,把「边」看作连接各个节点的指针,则可将「图」看作是一种从「链表」拓展而来的数据结构。**相较于线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,从而更为复杂**。 ## 9.1.1.   图常见类型 @@ -76,7 +76,7 @@ $$ ### 邻接表 -「邻接表 Adjacency List」使用 $n$ 个链表来表示图,链表结点表示顶点。第 $i$ 条链表对应顶点 $i$ ,其中存储了该顶点的所有邻接顶点(即与该顶点相连的顶点)。 +「邻接表 Adjacency List」使用 $n$ 个链表来表示图,链表节点表示顶点。第 $i$ 条链表对应顶点 $i$ ,其中存储了该顶点的所有邻接顶点(即与该顶点相连的顶点)。 ![图的邻接表表示](graph.assets/adjacency_list.png) diff --git a/chapter_graph/graph_operations.md b/chapter_graph/graph_operations.md index 2ecee1b5c..9b4506225 100644 --- a/chapter_graph/graph_operations.md +++ b/chapter_graph/graph_operations.md @@ -780,7 +780,7 @@ comments: true - **添加边**:在顶点对应链表的末尾添加边即可,使用 $O(1)$ 时间。因为是无向图,所以需要同时添加两个方向的边。 - **删除边**:在顶点对应链表中查找并删除指定边,使用 $O(m)$ 时间。在无向图中,需要同时删除两个方向的边。 -- **添加顶点**:在邻接表中添加一个链表,并将新增顶点作为链表头结点,使用 $O(1)$ 时间。 +- **添加顶点**:在邻接表中添加一个链表,并将新增顶点作为链表头节点,使用 $O(1)$ 时间。 - **删除顶点**:需遍历整个邻接表,删除包含指定顶点的所有边,使用 $O(1)$ 时间。 - **初始化**:在邻接表中创建 $n$ 个顶点和 $2m$ 条边,使用 $O(n + m)$ 时间。 @@ -799,7 +799,7 @@ comments: true === "删除顶点" ![adjacency_list_remove_vertex](graph_operations.assets/adjacency_list_remove_vertex.png) -以下是基于邻接表实现图的代码示例。细心的同学可能注意到,**我们在邻接表中使用 `Vertex` 结点类来表示顶点**,这样做的原因有: +以下是基于邻接表实现图的代码示例。细心的同学可能注意到,**我们在邻接表中使用 `Vertex` 节点类来表示顶点**,这样做的原因有: - 如果我们选择通过顶点值来区分不同顶点,那么值重复的顶点将无法被区分。 - 如果类似邻接矩阵那样,使用顶点列表索引来区分不同顶点。那么,假设我们想要删除索引为 $i$ 的顶点,则需要遍历整个邻接表,将其中 $> i$ 的索引全部减 $1$,这样操作效率较低。 @@ -889,7 +889,7 @@ comments: true // 邻接表,key: 顶点,value:该顶点的所有邻接顶点 unordered_map> adjList; - /* 在 vector 中删除指定结点 */ + /* 在 vector 中删除指定节点 */ void remove(vector &vec, Vertex *vet) { for (int i = 0; i < vec.size(); i++) { if (vec[i] == vet) { diff --git a/chapter_graph/graph_traversal.md b/chapter_graph/graph_traversal.md index 10eeff240..c19b64171 100644 --- a/chapter_graph/graph_traversal.md +++ b/chapter_graph/graph_traversal.md @@ -28,7 +28,7 @@ BFS 通常借助「队列」来实现。队列具有“先入先出”的性质 2. 在循环的每轮迭代中,弹出队首顶点并记录访问,然后将该顶点的所有邻接顶点加入到队列尾部; 3. 循环步骤 `2.` ,直到所有顶点被访问完成后结束; -为了防止重复遍历顶点,我们需要借助一个哈希表 `visited` 来记录哪些结点已被访问。 +为了防止重复遍历顶点,我们需要借助一个哈希表 `visited` 来记录哪些节点已被访问。 === "Java" diff --git a/chapter_hashing/hash_collision.md b/chapter_hashing/hash_collision.md index 8f230eb0d..0c60eb6ed 100644 --- a/chapter_hashing/hash_collision.md +++ b/chapter_hashing/hash_collision.md @@ -30,13 +30,13 @@ comments: true 链式地址下,哈希表的操作方法包括: -- **查询元素**:输入 key ,经过哈希函数得到数组索引,即可访问链表头结点,然后遍历链表并对比 key 以查找目标键值对。 -- **添加元素**:先通过哈希函数访问链表头结点,然后将结点(即键值对)添加到链表中。 -- **删除元素**:根据哈希函数的结果访问链表头部,接着遍历链表以查找目标结点,并将其删除。 +- **查询元素**:输入 key ,经过哈希函数得到数组索引,即可访问链表头节点,然后遍历链表并对比 key 以查找目标键值对。 +- **添加元素**:先通过哈希函数访问链表头节点,然后将节点(即键值对)添加到链表中。 +- **删除元素**:根据哈希函数的结果访问链表头部,接着遍历链表以查找目标节点,并将其删除。 尽管链式地址法解决了哈希冲突问题,但仍存在一些局限性,包括: -- **占用空间增大**,由于链表或二叉树包含结点指针,相比数组更加耗费内存空间; +- **占用空间增大**,由于链表或二叉树包含节点指针,相比数组更加耗费内存空间; - **查询效率降低**,因为需要线性遍历链表来查找对应元素; 为了提高操作效率,**可以将链表转换为「AVL 树」或「红黑树」**,将查询操作的时间复杂度优化至 $O(\log n)$ 。 diff --git a/chapter_hashing/hash_map.md b/chapter_hashing/hash_map.md index 845a64cbb..c4b55d9a2 100755 --- a/chapter_hashing/hash_map.md +++ b/chapter_hashing/hash_map.md @@ -18,8 +18,8 @@ comments: true 1. **无序数组**:每个元素为 `[学号, 姓名]` ; 2. **有序数组**:将 `1.` 中的数组按照学号从小到大排序; -3. **链表**:每个结点的值为 `[学号, 姓名]` ; -4. **二叉搜索树**:每个结点的值为 `[学号, 姓名]` ,根据学号大小来构建树; +3. **链表**:每个节点的值为 `[学号, 姓名]` ; +4. **二叉搜索树**:每个节点的值为 `[学号, 姓名]` ,根据学号大小来构建树; 各项操作的时间复杂度如下表所示(详解可见[二叉搜索树章节](https://www.hello-algo.com/chapter_tree/binary_search_tree/))。无论是查找元素还是增删元素,哈希表的时间复杂度都是 $O(1)$,全面胜出! diff --git a/chapter_heap/build_heap.md b/chapter_heap/build_heap.md index bfbdf762b..5a0077df8 100644 --- a/chapter_heap/build_heap.md +++ b/chapter_heap/build_heap.md @@ -16,7 +16,7 @@ comments: true ### 基于堆化操作实现 -有趣的是,存在一种更高效的建堆方法,其时间复杂度仅为 $O(n)$ 。我们先将列表所有元素原封不动添加到堆中,**然后迭代地对各个结点执行“从顶至底堆化”**。当然,**我们不需要对叶结点执行堆化操作**,因为它们没有子结点。 +有趣的是,存在一种更高效的建堆方法,其时间复杂度仅为 $O(n)$ 。我们先将列表所有元素原封不动添加到堆中,**然后迭代地对各个节点执行“从顶至底堆化”**。当然,**我们不需要对叶节点执行堆化操作**,因为它们没有子节点。 === "Java" @@ -25,7 +25,7 @@ comments: true MaxHeap(List nums) { // 将列表元素原封不动添加进堆 maxHeap = new ArrayList<>(nums); - // 堆化除叶结点以外的其他所有结点 + // 堆化除叶节点以外的其他所有节点 for (int i = parent(size() - 1); i >= 0; i--) { siftDown(i); } @@ -39,7 +39,7 @@ comments: true MaxHeap(vector nums) { // 将列表元素原封不动添加进堆 maxHeap = nums; - // 堆化除叶结点以外的其他所有结点 + // 堆化除叶节点以外的其他所有节点 for (int i = parent(size() - 1); i >= 0; i--) { siftDown(i); } @@ -53,7 +53,7 @@ comments: true """ 构造方法 """ # 将列表元素原封不动添加进堆 self.max_heap = nums - # 堆化除叶结点以外的其他所有结点 + # 堆化除叶节点以外的其他所有节点 for i in range(self.parent(self.size() - 1), -1, -1): self.sift_down(i) ``` @@ -66,7 +66,7 @@ comments: true // 将列表元素原封不动添加进堆 h := &maxHeap{data: nums} for i := len(h.data) - 1; i >= 0; i-- { - // 堆化除叶结点以外的其他所有结点 + // 堆化除叶节点以外的其他所有节点 h.siftDown(i) } return h @@ -80,7 +80,7 @@ comments: true constructor(nums) { // 将列表元素原封不动添加进堆 this.#maxHeap = nums === undefined ? [] : [...nums]; - // 堆化除叶结点以外的其他所有结点 + // 堆化除叶节点以外的其他所有节点 for (let i = this.#parent(this.size() - 1); i >= 0; i--) { this.#siftDown(i); } @@ -94,7 +94,7 @@ comments: true constructor(nums?: number[]) { // 将列表元素原封不动添加进堆 this.maxHeap = nums === undefined ? [] : [...nums]; - // 堆化除叶结点以外的其他所有结点 + // 堆化除叶节点以外的其他所有节点 for (let i = this.parent(this.size() - 1); i >= 0; i--) { this.siftDown(i); } @@ -115,7 +115,7 @@ comments: true { // 将列表元素原封不动添加进堆 maxHeap = new List(nums); - // 堆化除叶结点以外的其他所有结点 + // 堆化除叶节点以外的其他所有节点 var size = parent(this.size() - 1); for (int i = size; i >= 0; i--) { @@ -131,7 +131,7 @@ comments: true init(nums: [Int]) { // 将列表元素原封不动添加进堆 maxHeap = nums - // 堆化除叶结点以外的其他所有结点 + // 堆化除叶节点以外的其他所有节点 for i in stride(from: parent(i: size() - 1), through: 0, by: -1) { siftDown(i: i) } @@ -147,7 +147,7 @@ comments: true self.max_heap = std.ArrayList(T).init(allocator); // 将列表元素原封不动添加进堆 try self.max_heap.?.appendSlice(nums); - // 堆化除叶结点以外的其他所有结点 + // 堆化除叶节点以外的其他所有节点 var i: usize = parent(self.size() - 1) + 1; while (i > 0) : (i -= 1) { try self.siftDown(i - 1); @@ -159,18 +159,18 @@ comments: true 为什么第二种建堆方法的时间复杂度是 $O(n)$ ?我们来展开推算一下。 -- 完全二叉树中,设结点总数为 $n$ ,则叶结点数量为 $(n + 1) / 2$ ,其中 $/$ 为向下整除。因此,在排除叶结点后,需要堆化的结点数量为 $(n - 1)/2$ ,复杂度为 $O(n)$ ; -- 在从顶至底堆化的过程中,每个结点最多堆化到叶结点,因此最大迭代次数为二叉树高度 $O(\log n)$ ; +- 完全二叉树中,设节点总数为 $n$ ,则叶节点数量为 $(n + 1) / 2$ ,其中 $/$ 为向下整除。因此,在排除叶节点后,需要堆化的节点数量为 $(n - 1)/2$ ,复杂度为 $O(n)$ ; +- 在从顶至底堆化的过程中,每个节点最多堆化到叶节点,因此最大迭代次数为二叉树高度 $O(\log n)$ ; -将上述两者相乘,可得到建堆过程的时间复杂度为 $O(n \log n)$。**然而,这个估算结果并不准确,因为我们没有考虑到二叉树底层结点数量远多于顶层结点的特性**。 +将上述两者相乘,可得到建堆过程的时间复杂度为 $O(n \log n)$。**然而,这个估算结果并不准确,因为我们没有考虑到二叉树底层节点数量远多于顶层节点的特性**。 -接下来我们来进行更为详细的计算。为了减小计算难度,我们假设树是一个“完美二叉树”,该假设不会影响计算结果的正确性。设二叉树(即堆)结点数量为 $n$ ,树高度为 $h$ 。上文提到,**结点堆化最大迭代次数等于该结点到叶结点的距离,而该距离正是“结点高度”**。 +接下来我们来进行更为详细的计算。为了减小计算难度,我们假设树是一个“完美二叉树”,该假设不会影响计算结果的正确性。设二叉树(即堆)节点数量为 $n$ ,树高度为 $h$ 。上文提到,**节点堆化最大迭代次数等于该节点到叶节点的距离,而该距离正是“节点高度”**。 -![完美二叉树的各层结点数量](build_heap.assets/heapify_operations_count.png) +![完美二叉树的各层节点数量](build_heap.assets/heapify_operations_count.png) -

Fig. 完美二叉树的各层结点数量

+

Fig. 完美二叉树的各层节点数量

-因此,我们可以将各层的“结点数量 $\times$ 结点高度”求和,**从而得到所有结点的堆化迭代次数的总和**。 +因此,我们可以将各层的“节点数量 $\times$ 节点高度”求和,**从而得到所有节点的堆化迭代次数的总和**。 $$ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \cdots + 2^{(h-1)}\times1 @@ -201,4 +201,4 @@ T(h) & = 2 \frac{1 - 2^h}{1 - 2} - h \newline \end{aligned} $$ -进一步地,高度为 $h$ 的完美二叉树的结点数量为 $n = 2^{h+1} - 1$ ,易得复杂度为 $O(2^h) = O(n)$ 。以上推算表明,**输入列表并建堆的时间复杂度为 $O(n)$ ,非常高效**。 +进一步地,高度为 $h$ 的完美二叉树的节点数量为 $n = 2^{h+1} - 1$ ,易得复杂度为 $O(2^h) = O(n)$ 。以上推算表明,**输入列表并建堆的时间复杂度为 $O(n)$ ,非常高效**。 diff --git a/chapter_heap/heap.md b/chapter_heap/heap.md index 88b993af0..4c7829710 100644 --- a/chapter_heap/heap.md +++ b/chapter_heap/heap.md @@ -6,8 +6,8 @@ comments: true 「堆 Heap」是一棵限定条件下的「完全二叉树」。根据成立条件,堆主要分为两种类型: -- 「大顶堆 Max Heap」,任意结点的值 $\geq$ 其子结点的值; -- 「小顶堆 Min Heap」,任意结点的值 $\leq$ 其子结点的值; +- 「大顶堆 Max Heap」,任意节点的值 $\geq$ 其子节点的值; +- 「小顶堆 Min Heap」,任意节点的值 $\leq$ 其子节点的值; ![小顶堆与大顶堆](heap.assets/min_heap_and_max_heap.png) @@ -15,9 +15,9 @@ comments: true ## 8.1.1.   堆术语与性质 -- 由于堆是完全二叉树,因此最底层结点靠左填充,其它层结点皆被填满。 -- 二叉树中的根结点对应「堆顶」,底层最靠右结点对应「堆底」。 -- 对于大顶堆 / 小顶堆,其堆顶元素(即根结点)的值最大 / 最小。 +- 由于堆是完全二叉树,因此最底层节点靠左填充,其它层节点皆被填满。 +- 二叉树中的根节点对应「堆顶」,底层最靠右节点对应「堆底」。 +- 对于大顶堆 / 小顶堆,其堆顶元素(即根节点)的值最大 / 最小。 ## 8.1.2.   堆常用操作 @@ -314,9 +314,9 @@ comments: true 在二叉树章节我们学过,「完全二叉树」非常适合使用「数组」来表示,而堆恰好是一棵完全二叉树,**因而我们采用「数组」来存储「堆」**。 -**二叉树指针**。使用数组表示二叉树时,元素代表结点值,索引代表结点在二叉树中的位置,**而结点指针通过索引映射公式来实现**。 +**二叉树指针**。使用数组表示二叉树时,元素代表节点值,索引代表节点在二叉树中的位置,**而节点指针通过索引映射公式来实现**。 -具体地,给定索引 $i$ ,那么其左子结点索引为 $2i + 1$ 、右子结点索引为 $2i + 2$ 、父结点索引为 $(i - 1) / 2$ (向下整除)。当索引越界时,代表空结点或结点不存在。 +具体地,给定索引 $i$ ,那么其左子节点索引为 $2i + 1$ 、右子节点索引为 $2i + 2$ 、父节点索引为 $(i - 1) / 2$ (向下整除)。当索引越界时,代表空节点或节点不存在。 ![堆的表示与存储](heap.assets/representation_of_heap.png) @@ -327,17 +327,17 @@ comments: true === "Java" ```java title="my_heap.java" - /* 获取左子结点索引 */ + /* 获取左子节点索引 */ int left(int i) { return 2 * i + 1; } - /* 获取右子结点索引 */ + /* 获取右子节点索引 */ int right(int i) { return 2 * i + 2; } - /* 获取父结点索引 */ + /* 获取父节点索引 */ int parent(int i) { return (i - 1) / 2; // 向下整除 } @@ -346,17 +346,17 @@ comments: true === "C++" ```cpp title="my_heap.cpp" - /* 获取左子结点索引 */ + /* 获取左子节点索引 */ int left(int i) { return 2 * i + 1; } - /* 获取右子结点索引 */ + /* 获取右子节点索引 */ int right(int i) { return 2 * i + 2; } - /* 获取父结点索引 */ + /* 获取父节点索引 */ int parent(int i) { return (i - 1) / 2; // 向下取整 } @@ -366,32 +366,32 @@ comments: true ```python title="my_heap.py" def left(self, i: int) -> int: - """ 获取左子结点索引 """ + """ 获取左子节点索引 """ return 2 * i + 1 def right(self, i: int) -> int: - """ 获取右子结点索引 """ + """ 获取右子节点索引 """ return 2 * i + 2 def parent(self, i: int) -> int: - """ 获取父结点索引 """ + """ 获取父节点索引 """ return (i - 1) // 2 # 向下整除 ``` === "Go" ```go title="my_heap.go" - /* 获取左子结点索引 */ + /* 获取左子节点索引 */ func (h *maxHeap) left(i int) int { return 2*i + 1 } - /* 获取右子结点索引 */ + /* 获取右子节点索引 */ func (h *maxHeap) right(i int) int { return 2*i + 2 } - /* 获取父结点索引 */ + /* 获取父节点索引 */ func (h *maxHeap) parent(i int) int { // 向下整除 return (i - 1) / 2 @@ -401,17 +401,17 @@ comments: true === "JavaScript" ```javascript title="my_heap.js" - /* 获取左子结点索引 */ + /* 获取左子节点索引 */ #left(i) { return 2 * i + 1; } - /* 获取右子结点索引 */ + /* 获取右子节点索引 */ #right(i) { return 2 * i + 2; } - /* 获取父结点索引 */ + /* 获取父节点索引 */ #parent(i) { return Math.floor((i - 1) / 2); // 向下整除 } @@ -420,17 +420,17 @@ comments: true === "TypeScript" ```typescript title="my_heap.ts" - /* 获取左子结点索引 */ + /* 获取左子节点索引 */ left(i: number): number { return 2 * i + 1; } - /* 获取右子结点索引 */ + /* 获取右子节点索引 */ right(i: number): number { return 2 * i + 2; } - /* 获取父结点索引 */ + /* 获取父节点索引 */ parent(i: number): number { return Math.floor((i - 1) / 2); // 向下整除 } @@ -449,19 +449,19 @@ comments: true === "C#" ```csharp title="my_heap.cs" - /* 获取左子结点索引 */ + /* 获取左子节点索引 */ int left(int i) { return 2 * i + 1; } - /* 获取右子结点索引 */ + /* 获取右子节点索引 */ int right(int i) { return 2 * i + 2; } - /* 获取父结点索引 */ + /* 获取父节点索引 */ int parent(int i) { return (i - 1) / 2; // 向下整除 @@ -471,17 +471,17 @@ comments: true === "Swift" ```swift title="my_heap.swift" - /* 获取左子结点索引 */ + /* 获取左子节点索引 */ func left(i: Int) -> Int { 2 * i + 1 } - /* 获取右子结点索引 */ + /* 获取右子节点索引 */ func right(i: Int) -> Int { 2 * i + 2 } - /* 获取父结点索引 */ + /* 获取父节点索引 */ func parent(i: Int) -> Int { (i - 1) / 2 // 向下整除 } @@ -490,17 +490,17 @@ comments: true === "Zig" ```zig title="my_heap.zig" - // 获取左子结点索引 + // 获取左子节点索引 fn left(i: usize) usize { return 2 * i + 1; } - // 获取右子结点索引 + // 获取右子节点索引 fn right(i: usize) usize { return 2 * i + 2; } - // 获取父结点索引 + // 获取父节点索引 fn parent(i: usize) usize { // return (i - 1) / 2; // 向下整除 return @divFloor(i - 1, 2); @@ -509,7 +509,7 @@ comments: true ### 访问堆顶元素 -堆顶元素是二叉树的根结点,即列表首元素。 +堆顶元素是二叉树的根节点,即列表首元素。 === "Java" @@ -600,9 +600,9 @@ comments: true ### 元素入堆 -给定元素 `val` ,我们先将其添加到堆底。添加后,由于 `val` 可能大于堆中其它元素,此时堆的成立条件可能已经被破坏,**因此需要修复从插入结点到根结点这条路径上的各个结点**,该操作被称为「堆化 Heapify」。 +给定元素 `val` ,我们先将其添加到堆底。添加后,由于 `val` 可能大于堆中其它元素,此时堆的成立条件可能已经被破坏,**因此需要修复从插入节点到根节点这条路径上的各个节点**,该操作被称为「堆化 Heapify」。 -考虑从入堆结点开始,**从底至顶执行堆化**。具体地,比较插入结点与其父结点的值,若插入结点更大则将它们交换;并循环以上操作,从底至顶地修复堆中的各个结点;直至越过根结点时结束,或当遇到无需交换的结点时提前结束。 +考虑从入堆节点开始,**从底至顶执行堆化**。具体地,比较插入节点与其父节点的值,若插入节点更大则将它们交换;并循环以上操作,从底至顶地修复堆中的各个节点;直至越过根节点时结束,或当遇到无需交换的节点时提前结束。 === "<1>" ![元素入堆步骤](heap.assets/heap_push_step1.png) @@ -622,28 +622,28 @@ comments: true === "<6>" ![heap_push_step6](heap.assets/heap_push_step6.png) -设结点总数为 $n$ ,则树的高度为 $O(\log n)$ ,易得堆化操作的循环轮数最多为 $O(\log n)$ ,**因而元素入堆操作的时间复杂度为 $O(\log n)$** 。 +设节点总数为 $n$ ,则树的高度为 $O(\log n)$ ,易得堆化操作的循环轮数最多为 $O(\log n)$ ,**因而元素入堆操作的时间复杂度为 $O(\log n)$** 。 === "Java" ```java title="my_heap.java" /* 元素入堆 */ void push(int val) { - // 添加结点 + // 添加节点 maxHeap.add(val); // 从底至顶堆化 siftUp(size() - 1); } - /* 从结点 i 开始,从底至顶堆化 */ + /* 从节点 i 开始,从底至顶堆化 */ void siftUp(int i) { while (true) { - // 获取结点 i 的父结点 + // 获取节点 i 的父节点 int p = parent(i); - // 当“越过根结点”或“结点无需修复”时,结束堆化 + // 当“越过根节点”或“节点无需修复”时,结束堆化 if (p < 0 || maxHeap.get(i) <= maxHeap.get(p)) break; - // 交换两结点 + // 交换两节点 swap(i, p); // 循环向上堆化 i = p; @@ -656,21 +656,21 @@ comments: true ```cpp title="my_heap.cpp" /* 元素入堆 */ void push(int val) { - // 添加结点 + // 添加节点 maxHeap.push_back(val); // 从底至顶堆化 siftUp(size() - 1); } - /* 从结点 i 开始,从底至顶堆化 */ + /* 从节点 i 开始,从底至顶堆化 */ void siftUp(int i) { while (true) { - // 获取结点 i 的父结点 + // 获取节点 i 的父节点 int p = parent(i); - // 当“越过根结点”或“结点无需修复”时,结束堆化 + // 当“越过根节点”或“节点无需修复”时,结束堆化 if (p < 0 || maxHeap[i] <= maxHeap[p]) break; - // 交换两结点 + // 交换两节点 swap(maxHeap[i], maxHeap[p]); // 循环向上堆化 i = p; @@ -683,20 +683,20 @@ comments: true ```python title="my_heap.py" def push(self, val: int): """ 元素入堆 """ - # 添加结点 + # 添加节点 self.max_heap.append(val) # 从底至顶堆化 self.sift_up(self.size() - 1) def sift_up(self, i: int): - """ 从结点 i 开始,从底至顶堆化 """ + """ 从节点 i 开始,从底至顶堆化 """ while True: - # 获取结点 i 的父结点 + # 获取节点 i 的父节点 p = self.parent(i) - # 当“越过根结点”或“结点无需修复”时,结束堆化 + # 当“越过根节点”或“节点无需修复”时,结束堆化 if p < 0 or self.max_heap[i] <= self.max_heap[p]: break - # 交换两结点 + # 交换两节点 self.swap(i, p) # 循环向上堆化 i = p @@ -707,22 +707,22 @@ comments: true ```go title="my_heap.go" /* 元素入堆 */ func (h *maxHeap) push(val any) { - // 添加结点 + // 添加节点 h.data = append(h.data, val) // 从底至顶堆化 h.siftUp(len(h.data) - 1) } - /* 从结点 i 开始,从底至顶堆化 */ + /* 从节点 i 开始,从底至顶堆化 */ func (h *maxHeap) siftUp(i int) { for true { - // 获取结点 i 的父结点 + // 获取节点 i 的父节点 p := h.parent(i) - // 当“越过根结点”或“结点无需修复”时,结束堆化 + // 当“越过根节点”或“节点无需修复”时,结束堆化 if p < 0 || h.data[i].(int) <= h.data[p].(int) { break } - // 交换两结点 + // 交换两节点 h.swap(i, p) // 循环向上堆化 i = p @@ -735,20 +735,20 @@ comments: true ```javascript title="my_heap.js" /* 元素入堆 */ push(val) { - // 添加结点 + // 添加节点 this.#maxHeap.push(val); // 从底至顶堆化 this.#siftUp(this.size() - 1); } - /* 从结点 i 开始,从底至顶堆化 */ + /* 从节点 i 开始,从底至顶堆化 */ #siftUp(i) { while (true) { - // 获取结点 i 的父结点 + // 获取节点 i 的父节点 const p = this.#parent(i); - // 当“越过根结点”或“结点无需修复”时,结束堆化 + // 当“越过根节点”或“节点无需修复”时,结束堆化 if (p < 0 || this.#maxHeap[i] <= this.#maxHeap[p]) break; - // 交换两结点 + // 交换两节点 this.#swap(i, p); // 循环向上堆化 i = p; @@ -761,20 +761,20 @@ comments: true ```typescript title="my_heap.ts" /* 元素入堆 */ push(val: number): void { - // 添加结点 + // 添加节点 this.maxHeap.push(val); // 从底至顶堆化 this.siftUp(this.size() - 1); } - /* 从结点 i 开始,从底至顶堆化 */ + /* 从节点 i 开始,从底至顶堆化 */ siftUp(i: number): void { while (true) { - // 获取结点 i 的父结点 + // 获取节点 i 的父节点 const p = this.parent(i); - // 当“越过根结点”或“结点无需修复”时,结束堆化 + // 当“越过根节点”或“节点无需修复”时,结束堆化 if (p < 0 || this.maxHeap[i] <= this.maxHeap[p]) break; - // 交换两结点 + // 交换两节点 this.swap(i, p); // 循环向上堆化 i = p; @@ -796,23 +796,23 @@ comments: true /* 元素入堆 */ void push(int val) { - // 添加结点 + // 添加节点 maxHeap.Add(val); // 从底至顶堆化 siftUp(size() - 1); } - /* 从结点 i 开始,从底至顶堆化 */ + /* 从节点 i 开始,从底至顶堆化 */ void siftUp(int i) { while (true) { - // 获取结点 i 的父结点 + // 获取节点 i 的父节点 int p = parent(i); - // 若“越过根结点”或“结点无需修复”,则结束堆化 + // 若“越过根节点”或“节点无需修复”,则结束堆化 if (p < 0 || maxHeap[i] <= maxHeap[p]) break; - // 交换两结点 + // 交换两节点 swap(i, p); // 循环向上堆化 i = p; @@ -825,23 +825,23 @@ comments: true ```swift title="my_heap.swift" /* 元素入堆 */ func push(val: Int) { - // 添加结点 + // 添加节点 maxHeap.append(val) // 从底至顶堆化 siftUp(i: size() - 1) } - /* 从结点 i 开始,从底至顶堆化 */ + /* 从节点 i 开始,从底至顶堆化 */ func siftUp(i: Int) { var i = i while true { - // 获取结点 i 的父结点 + // 获取节点 i 的父节点 let p = parent(i: i) - // 当“越过根结点”或“结点无需修复”时,结束堆化 + // 当“越过根节点”或“节点无需修复”时,结束堆化 if p < 0 || maxHeap[i] <= maxHeap[p] { break } - // 交换两结点 + // 交换两节点 swap(i: i, j: p) // 循环向上堆化 i = p @@ -854,21 +854,21 @@ comments: true ```zig title="my_heap.zig" // 元素入堆 fn push(self: *Self, val: T) !void { - // 添加结点 + // 添加节点 try self.max_heap.?.append(val); // 从底至顶堆化 try self.siftUp(self.size() - 1); } - // 从结点 i 开始,从底至顶堆化 + // 从节点 i 开始,从底至顶堆化 fn siftUp(self: *Self, i_: usize) !void { var i = i_; while (true) { - // 获取结点 i 的父结点 + // 获取节点 i 的父节点 var p = parent(i); - // 当“越过根结点”或“结点无需修复”时,结束堆化 + // 当“越过根节点”或“节点无需修复”时,结束堆化 if (p < 0 or self.max_heap.?.items[i] <= self.max_heap.?.items[p]) break; - // 交换两结点 + // 交换两节点 try self.swap(i, p); // 循环向上堆化 i = p; @@ -878,13 +878,13 @@ comments: true ### 堆顶元素出堆 -堆顶元素是二叉树根结点,即列表首元素,如果我们直接将首元素从列表中删除,则二叉树中所有结点都会随之发生移位(索引发生变化),这样后续使用堆化修复就很麻烦了。为了尽量减少元素索引变动,采取以下操作步骤: +堆顶元素是二叉树根节点,即列表首元素,如果我们直接将首元素从列表中删除,则二叉树中所有节点都会随之发生移位(索引发生变化),这样后续使用堆化修复就很麻烦了。为了尽量减少元素索引变动,采取以下操作步骤: -1. 交换堆顶元素与堆底元素(即交换根结点与最右叶结点); +1. 交换堆顶元素与堆底元素(即交换根节点与最右叶节点); 2. 交换完成后,将堆底从列表中删除(注意,因为已经交换,实际上删除的是原来的堆顶元素); -3. 从根结点开始,**从顶至底执行堆化**; +3. 从根节点开始,**从顶至底执行堆化**; -顾名思义,**从顶至底堆化的操作方向与从底至顶堆化相反**,我们比较根结点的值与其两个子结点的值,将最大的子结点与根结点执行交换,并循环以上操作,直到越过叶结点时结束,或当遇到无需交换的结点时提前结束。 +顾名思义,**从顶至底堆化的操作方向与从底至顶堆化相反**,我们比较根节点的值与其两个子节点的值,将最大的子节点与根节点执行交换,并循环以上操作,直到越过叶节点时结束,或当遇到无需交换的节点时提前结束。 === "<1>" ![堆顶元素出堆步骤](heap.assets/heap_pop_step1.png) @@ -926,9 +926,9 @@ comments: true // 判空处理 if (isEmpty()) throw new EmptyStackException(); - // 交换根结点与最右叶结点(即交换首元素与尾元素) + // 交换根节点与最右叶节点(即交换首元素与尾元素) swap(0, size() - 1); - // 删除结点 + // 删除节点 int val = maxHeap.remove(size() - 1); // 从顶至底堆化 siftDown(0); @@ -936,18 +936,18 @@ comments: true return val; } - /* 从结点 i 开始,从顶至底堆化 */ + /* 从节点 i 开始,从顶至底堆化 */ void siftDown(int i) { while (true) { - // 判断结点 i, l, r 中值最大的结点,记为 ma + // 判断节点 i, l, r 中值最大的节点,记为 ma int l = left(i), r = right(i), ma = i; if (l < size() && maxHeap.get(l) > maxHeap.get(ma)) ma = l; if (r < size() && maxHeap.get(r) > maxHeap.get(ma)) ma = r; - // 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 + // 若节点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 if (ma == i) break; - // 交换两结点 + // 交换两节点 swap(i, ma); // 循环向下堆化 i = ma; @@ -964,25 +964,25 @@ comments: true if (empty()) { throw out_of_range("堆为空"); } - // 交换根结点与最右叶结点(即交换首元素与尾元素) + // 交换根节点与最右叶节点(即交换首元素与尾元素) swap(maxHeap[0], maxHeap[size() - 1]); - // 删除结点 + // 删除节点 maxHeap.pop_back(); // 从顶至底堆化 siftDown(0); } - /* 从结点 i 开始,从顶至底堆化 */ + /* 从节点 i 开始,从顶至底堆化 */ void siftDown(int i) { while (true) { - // 判断结点 i, l, r 中值最大的结点,记为 ma + // 判断节点 i, l, r 中值最大的节点,记为 ma int l = left(i), r = right(i), ma = i; - // 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 + // 若节点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l; if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r; - // 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 + // 若节点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 if (ma == i) break; swap(maxHeap[i], maxHeap[ma]); @@ -999,9 +999,9 @@ comments: true """ 元素出堆 """ # 判空处理 assert not self.is_empty() - # 交换根结点与最右叶结点(即交换首元素与尾元素) + # 交换根节点与最右叶节点(即交换首元素与尾元素) self.swap(0, self.size() - 1) - # 删除结点 + # 删除节点 val = self.max_heap.pop() # 从顶至底堆化 self.sift_down(0) @@ -1009,18 +1009,18 @@ comments: true return val def sift_down(self, i: int): - """ 从结点 i 开始,从顶至底堆化 """ + """ 从节点 i 开始,从顶至底堆化 """ while True: - # 判断结点 i, l, r 中值最大的结点,记为 ma + # 判断节点 i, l, r 中值最大的节点,记为 ma l, r, ma = self.left(i), self.right(i), i if l < self.size() and self.max_heap[l] > self.max_heap[ma]: ma = l if r < self.size() and self.max_heap[r] > self.max_heap[ma]: ma = r - # 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 + # 若节点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 if ma == i: break - # 交换两结点 + # 交换两节点 self.swap(i, ma) # 循环向下堆化 i = ma @@ -1036,9 +1036,9 @@ comments: true fmt.Println("error") return nil } - // 交换根结点与最右叶结点(即交换首元素与尾元素) + // 交换根节点与最右叶节点(即交换首元素与尾元素) h.swap(0, h.size()-1) - // 删除结点 + // 删除节点 val := h.data[len(h.data)-1] h.data = h.data[:len(h.data)-1] // 从顶至底堆化 @@ -1048,10 +1048,10 @@ comments: true return val } - /* 从结点 i 开始,从顶至底堆化 */ + /* 从节点 i 开始,从顶至底堆化 */ func (h *maxHeap) siftDown(i int) { for true { - // 判断结点 i, l, r 中值最大的结点,记为 max + // 判断节点 i, l, r 中值最大的节点,记为 max l, r, max := h.left(i), h.right(i), i if l < h.size() && h.data[l].(int) > h.data[max].(int) { max = l @@ -1059,11 +1059,11 @@ comments: true if r < h.size() && h.data[r].(int) > h.data[max].(int) { max = r } - // 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 + // 若节点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 if max == i { break } - // 交换两结点 + // 交换两节点 h.swap(i, max) // 循环向下堆化 i = max @@ -1078,9 +1078,9 @@ comments: true pop() { // 判空处理 if (this.isEmpty()) throw new Error("堆为空"); - // 交换根结点与最右叶结点(即交换首元素与尾元素) + // 交换根节点与最右叶节点(即交换首元素与尾元素) this.#swap(0, this.size() - 1); - // 删除结点 + // 删除节点 const val = this.#maxHeap.pop(); // 从顶至底堆化 this.#siftDown(0); @@ -1088,18 +1088,18 @@ comments: true return val; } - /* 从结点 i 开始,从顶至底堆化 */ + /* 从节点 i 开始,从顶至底堆化 */ #siftDown(i) { while (true) { - // 判断结点 i, l, r 中值最大的结点,记为 ma + // 判断节点 i, l, r 中值最大的节点,记为 ma const l = this.#left(i), r = this.#right(i); let ma = i; if (l < this.size() && this.#maxHeap[l] > this.#maxHeap[ma]) ma = l; if (r < this.size() && this.#maxHeap[r] > this.#maxHeap[ma]) ma = r; - // 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 + // 若节点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 if (ma == i) break; - // 交换两结点 + // 交换两节点 this.#swap(i, ma); // 循环向下堆化 i = ma; @@ -1114,9 +1114,9 @@ comments: true pop(): number { // 判空处理 if (this.isEmpty()) throw new RangeError('Heap is empty.'); - // 交换根结点与最右叶结点(即交换首元素与尾元素) + // 交换根节点与最右叶节点(即交换首元素与尾元素) this.swap(0, this.size() - 1); - // 删除结点 + // 删除节点 const val = this.maxHeap.pop(); // 从顶至底堆化 this.siftDown(0); @@ -1124,18 +1124,18 @@ comments: true return val; } - /* 从结点 i 开始,从顶至底堆化 */ + /* 从节点 i 开始,从顶至底堆化 */ siftDown(i: number): void { while (true) { - // 判断结点 i, l, r 中值最大的结点,记为 ma + // 判断节点 i, l, r 中值最大的节点,记为 ma const l = this.left(i), r = this.right(i); let ma = i; if (l < this.size() && this.maxHeap[l] > this.maxHeap[ma]) ma = l; if (r < this.size() && this.maxHeap[r] > this.maxHeap[ma]) ma = r; - // 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 + // 若节点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 if (ma == i) break; - // 交换两结点 + // 交换两节点 this.swap(i, ma); // 循环向下堆化 i = ma; @@ -1160,9 +1160,9 @@ comments: true // 判空处理 if (isEmpty()) throw new IndexOutOfRangeException(); - // 交换根结点与最右叶结点(即交换首元素与尾元素) + // 交换根节点与最右叶节点(即交换首元素与尾元素) swap(0, size() - 1); - // 删除结点 + // 删除节点 int val = maxHeap.Last(); maxHeap.RemoveAt(size() - 1); // 从顶至底堆化 @@ -1171,20 +1171,20 @@ comments: true return val; } - /* 从结点 i 开始,从顶至底堆化 */ + /* 从节点 i 开始,从顶至底堆化 */ void siftDown(int i) { while (true) { - // 判断结点 i, l, r 中值最大的结点,记为 ma + // 判断节点 i, l, r 中值最大的节点,记为 ma int l = left(i), r = right(i), ma = i; if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l; if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r; - // 若“结点 i 最大”或“越过叶结点”,则结束堆化 + // 若“节点 i 最大”或“越过叶节点”,则结束堆化 if (ma == i) break; - // 交换两结点 + // 交换两节点 swap(i, ma); // 循环向下堆化 i = ma; @@ -1201,9 +1201,9 @@ comments: true if isEmpty() { fatalError("堆为空") } - // 交换根结点与最右叶结点(即交换首元素与尾元素) + // 交换根节点与最右叶节点(即交换首元素与尾元素) swap(i: 0, j: size() - 1) - // 删除结点 + // 删除节点 let val = maxHeap.remove(at: size() - 1) // 从顶至底堆化 siftDown(i: 0) @@ -1211,11 +1211,11 @@ comments: true return val } - /* 从结点 i 开始,从顶至底堆化 */ + /* 从节点 i 开始,从顶至底堆化 */ func siftDown(i: Int) { var i = i while true { - // 判断结点 i, l, r 中值最大的结点,记为 ma + // 判断节点 i, l, r 中值最大的节点,记为 ma let l = left(i: i) let r = right(i: i) var ma = i @@ -1225,11 +1225,11 @@ comments: true if r < size(), maxHeap[r] > maxHeap[ma] { ma = r } - // 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 + // 若节点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 if ma == i { break } - // 交换两结点 + // 交换两节点 swap(i: i, j: ma) // 循环向下堆化 i = ma @@ -1244,9 +1244,9 @@ comments: true fn pop(self: *Self) !T { // 判断处理 if (self.isEmpty()) unreachable; - // 交换根结点与最右叶结点(即交换首元素与尾元素) + // 交换根节点与最右叶节点(即交换首元素与尾元素) try self.swap(0, self.size() - 1); - // 删除结点 + // 删除节点 var val = self.max_heap.?.pop(); // 从顶至底堆化 try self.siftDown(0); @@ -1254,19 +1254,19 @@ comments: true return val; } - // 从结点 i 开始,从顶至底堆化 + // 从节点 i 开始,从顶至底堆化 fn siftDown(self: *Self, i_: usize) !void { var i = i_; while (true) { - // 判断结点 i, l, r 中值最大的结点,记为 ma + // 判断节点 i, l, r 中值最大的节点,记为 ma var l = left(i); var r = right(i); var ma = i; if (l < self.size() and self.max_heap.?.items[l] > self.max_heap.?.items[ma]) ma = l; if (r < self.size() and self.max_heap.?.items[r] > self.max_heap.?.items[ma]) ma = r; - // 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 + // 若节点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 if (ma == i) break; - // 交换两结点 + // 交换两节点 try self.swap(i, ma); // 循环向下堆化 i = ma; diff --git a/chapter_searching/hashing_search.md b/chapter_searching/hashing_search.md index 974f9e934..f2ad3d30f 100755 --- a/chapter_searching/hashing_search.md +++ b/chapter_searching/hashing_search.md @@ -130,18 +130,18 @@ comments: true } ``` -再比如,如果我们想要给定一个目标结点值 `target` ,获取对应的链表结点对象,那么也可以使用哈希查找实现。 +再比如,如果我们想要给定一个目标节点值 `target` ,获取对应的链表节点对象,那么也可以使用哈希查找实现。 -![哈希查找链表结点](hashing_search.assets/hash_search_listnode.png) +![哈希查找链表节点](hashing_search.assets/hash_search_listnode.png) -

Fig. 哈希查找链表结点

+

Fig. 哈希查找链表节点

=== "Java" ```java title="hashing_search.java" /* 哈希查找(链表) */ ListNode hashingSearchLinkedList(Map map, int target) { - // 哈希表的 key: 目标结点值,value: 结点对象 + // 哈希表的 key: 目标节点值,value: 节点对象 // 若哈希表中无此 key ,返回 null return map.getOrDefault(target, null); } @@ -152,7 +152,7 @@ comments: true ```cpp title="hashing_search.cpp" /* 哈希查找(链表) */ ListNode* hashingSearchLinkedList(unordered_map map, int target) { - // 哈希表的 key: 目标结点值,value: 结点对象 + // 哈希表的 key: 目标节点值,value: 节点对象 // 若哈希表中无此 key ,返回 nullptr if (map.find(target) == map.end()) return nullptr; @@ -165,7 +165,7 @@ comments: true ```python title="hashing_search.py" def hashing_search_linkedlist(mapp: dict[int, ListNode], target: int) -> ListNode | None: """ 哈希查找(链表) """ - # 哈希表的 key: 目标元素,value: 结点对象 + # 哈希表的 key: 目标元素,value: 节点对象 # 若哈希表中无此 key ,返回 None return mapp.get(target, None) ``` @@ -175,7 +175,7 @@ comments: true ```go title="hashing_search.go" /* 哈希查找(链表) */ func hashingSearchLinkedList(m map[int]*ListNode, target int) *ListNode { - // 哈希表的 key: 目标结点值,value: 结点对象 + // 哈希表的 key: 目标节点值,value: 节点对象 // 若哈希表中无此 key ,返回 nil if node, ok := m[target]; ok { return node @@ -190,7 +190,7 @@ comments: true ```javascript title="hashing_search.js" /* 哈希查找(链表) */ function hashingSearchLinkedList(map, target) { - // 哈希表的 key: 目标结点值,value: 结点对象 + // 哈希表的 key: 目标节点值,value: 节点对象 // 若哈希表中无此 key ,返回 null return map.has(target) ? map.get(target) : null; } @@ -201,7 +201,7 @@ comments: true ```typescript title="hashing_search.ts" /* 哈希查找(链表) */ function hashingSearchLinkedList(map: Map, target: number): ListNode | null { - // 哈希表的 key: 目标结点值,value: 结点对象 + // 哈希表的 key: 目标节点值,value: 节点对象 // 若哈希表中无此 key ,返回 null return map.has(target) ? (map.get(target) as ListNode) : null; } @@ -220,7 +220,7 @@ comments: true ListNode? hashingSearchLinkedList(Dictionary map, int target) { - // 哈希表的 key: 目标结点值,value: 结点对象 + // 哈希表的 key: 目标节点值,value: 节点对象 // 若哈希表中无此 key ,返回 null return map.GetValueOrDefault(target); } @@ -231,7 +231,7 @@ comments: true ```swift title="hashing_search.swift" /* 哈希查找(链表) */ func hashingSearchLinkedList(map: [Int: ListNode], target: Int) -> ListNode? { - // 哈希表的 key: 目标结点值,value: 结点对象 + // 哈希表的 key: 目标节点值,value: 节点对象 // 若哈希表中无此 key ,返回 null return map[target] } @@ -242,7 +242,7 @@ comments: true ```zig title="hashing_search.zig" // 哈希查找(链表) fn hashingSearchLinkedList(comptime T: type, map: std.AutoHashMap(T, *inc.ListNode(T)), target: T) ?*inc.ListNode(T) { - // 哈希表的 key: 目标结点值,value: 结点对象 + // 哈希表的 key: 目标节点值,value: 节点对象 // 若哈希表中无此 key ,返回 null if (map.getKey(target) == null) return null; return map.get(target); diff --git a/chapter_searching/linear_search.md b/chapter_searching/linear_search.md index 76ae2727b..d7e2396e3 100755 --- a/chapter_searching/linear_search.md +++ b/chapter_searching/linear_search.md @@ -167,7 +167,7 @@ comments: true } ``` -再比如,我们想要在给定一个目标结点值 `target` ,返回此结点对象,也可以在链表中进行线性查找。 +再比如,我们想要在给定一个目标节点值 `target` ,返回此节点对象,也可以在链表中进行线性查找。 === "Java" @@ -176,12 +176,12 @@ comments: true ListNode linearSearchLinkedList(ListNode head, int target) { // 遍历链表 while (head != null) { - // 找到目标结点,返回之 + // 找到目标节点,返回之 if (head.val == target) return head; head = head.next; } - // 未找到目标结点,返回 null + // 未找到目标节点,返回 null return null; } ``` @@ -193,12 +193,12 @@ comments: true ListNode* linearSearchLinkedList(ListNode* head, int target) { // 遍历链表 while (head != nullptr) { - // 找到目标结点,返回之 + // 找到目标节点,返回之 if (head->val == target) return head; head = head->next; } - // 未找到目标结点,返回 nullptr + // 未找到目标节点,返回 nullptr return nullptr; } ``` @@ -210,10 +210,10 @@ comments: true """ 线性查找(链表) """ # 遍历链表 while head: - if head.val == target: # 找到目标结点,返回之 + if head.val == target: # 找到目标节点,返回之 return head head = head.next - return None # 未找到目标结点,返回 None + return None # 未找到目标节点,返回 None ``` === "Go" @@ -223,7 +223,7 @@ comments: true func linearSearchLinkedList(node *ListNode, target int) *ListNode { // 遍历链表 for node != nil { - // 找到目标结点,返回之 + // 找到目标节点,返回之 if node.Val == target { return node } @@ -241,13 +241,13 @@ comments: true function linearSearchLinkedList(head, target) { // 遍历链表 while(head) { - // 找到目标结点,返回之 + // 找到目标节点,返回之 if(head.val === target) { return head; } head = head.next; } - // 未找到目标结点,返回 null + // 未找到目标节点,返回 null return null; } ``` @@ -259,13 +259,13 @@ comments: true function linearSearchLinkedList(head: ListNode | null, target: number): ListNode | null { // 遍历链表 while (head) { - // 找到目标结点,返回之 + // 找到目标节点,返回之 if (head.val === target) { return head; } head = head.next; } - // 未找到目标结点,返回 null + // 未找到目标节点,返回 null return null; } ``` @@ -285,12 +285,12 @@ comments: true // 遍历链表 while (head != null) { - // 找到目标结点,返回之 + // 找到目标节点,返回之 if (head.val == target) return head; head = head.next; } - // 未找到目标结点,返回 null + // 未找到目标节点,返回 null return null; } ``` @@ -303,13 +303,13 @@ comments: true var head = head // 遍历链表 while head != nil { - // 找到目标结点,返回之 + // 找到目标节点,返回之 if head?.val == target { return head } head = head?.next } - // 未找到目标结点,返回 null + // 未找到目标节点,返回 null return nil } ``` @@ -322,7 +322,7 @@ comments: true var head = node; // 遍历链表 while (head != null) { - // 找到目标结点,返回之 + // 找到目标节点,返回之 if (head.?.val == target) return head; head = head.?.next; } diff --git a/chapter_sorting/bucket_sort.md b/chapter_sorting/bucket_sort.md index 0f53670e9..09cfc3953 100644 --- a/chapter_sorting/bucket_sort.md +++ b/chapter_sorting/bucket_sort.md @@ -145,13 +145,67 @@ comments: true === "JavaScript" ```javascript title="bucket_sort.js" - [class]{}-[func]{bucketSort} + /* 桶排序 */ + function bucketSort(nums) { + // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素 + const k = nums.length / 2; + const buckets = []; + for (let i = 0; i < k; i++) { + buckets.push([]); + } + // 1. 将数组元素分配到各个桶中 + for (const num of nums) { + // 输入数据范围 [0, 1),使用 num * k 映射到索引范围 [0, k-1] + const i = Math.floor(num * k); + // 将 num 添加进桶 i + buckets[i].push(num); + } + // 2. 对各个桶执行排序 + for (const bucket of buckets) { + // 使用内置排序函数,也可以替换成其它排序算法 + bucket.sort((a, b) => a - b); + } + // 3. 遍历桶合并结果 + let i = 0; + for (const bucket of buckets) { + for (const num of bucket) { + nums[i++] = num; + } + } + } ``` === "TypeScript" ```typescript title="bucket_sort.ts" - [class]{}-[func]{bucketSort} + /* 桶排序 */ + function bucketSort(nums: number[]): void { + // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素 + const k = nums.length / 2; + const buckets: number[][] = []; + for (let i = 0; i < k; i++) { + buckets.push([]); + } + // 1. 将数组元素分配到各个桶中 + for (const num of nums) { + // 输入数据范围 [0, 1),使用 num * k 映射到索引范围 [0, k-1] + const i = Math.floor(num * k); + // 将 num 添加进桶 i + buckets[i].push(num); + } + // 2. 对各个桶执行排序 + for (const bucket of buckets) { + // 使用内置排序函数,也可以替换成其它排序算法 + bucket.sort((a, b) => a - b); + } + // 3. 遍历桶合并结果 + let i = 0; + for (const bucket of buckets) { + for (const num of bucket) { + nums[i++] = num; + } + } + } ``` === "C" @@ -221,7 +275,7 @@ comments: true 桶排序的时间复杂度理论上可以达到 $O(n)$ ,**难点是需要将元素均匀分配到各个桶中**,因为现实中的数据往往都不是均匀分布的。举个例子,假设我们想要把淘宝的所有商品根据价格范围平均分配到 10 个桶中,然而商品价格不是均匀分布的,100 元以下非常多、1000 元以上非常少;如果我们将价格区间平均划为 10 份,那么各个桶内的商品数量差距会非常大。 -为了实现平均分配,我们可以先大致设置一个分界线,将数据粗略分到 3 个桶,分配完后,**再把商品较多的桶继续划分为 3 个桶,直至所有桶内元素数量大致平均为止**。此方法本质上是生成一个递归树,让叶结点的值尽量平均。当然,不一定非要划分为 3 个桶,可以根据数据特点灵活选取。 +为了实现平均分配,我们可以先大致设置一个分界线,将数据粗略分到 3 个桶,分配完后,**再把商品较多的桶继续划分为 3 个桶,直至所有桶内元素数量大致平均为止**。此方法本质上是生成一个递归树,让叶节点的值尽量平均。当然,不一定非要划分为 3 个桶,可以根据数据特点灵活选取。 ![递归划分桶](bucket_sort.assets/scatter_in_buckets_recursively.png) diff --git a/chapter_sorting/counting_sort.md b/chapter_sorting/counting_sort.md index 3fc6d6168..592f943c4 100644 --- a/chapter_sorting/counting_sort.md +++ b/chapter_sorting/counting_sort.md @@ -127,13 +127,55 @@ comments: true === "JavaScript" ```javascript title="counting_sort.js" - [class]{}-[func]{countingSortNaive} + /* 计数排序 */ + // 简单实现,无法用于排序对象 + function countingSortNaive(nums) { + // 1. 统计数组最大元素 m + let m = 0; + for (const num of nums) { + m = Math.max(m, num); + } + // 2. 统计各数字的出现次数 + // counter[num] 代表 num 的出现次数 + const counter = new Array(m + 1).fill(0); + for (const num of nums) { + counter[num]++; + } + // 3. 遍历 counter ,将各元素填入原数组 nums + let i = 0; + for (let num = 0; num < m + 1; num++) { + for (let j = 0; j < counter[num]; j++, i++) { + nums[i] = num; + } + } + } ``` === "TypeScript" ```typescript title="counting_sort.ts" - [class]{}-[func]{countingSortNaive} + /* 计数排序 */ + // 简单实现,无法用于排序对象 + function countingSortNaive(nums: number[]): void { + // 1. 统计数组最大元素 m + let m = 0; + for (const num of nums) { + m = Math.max(m, num); + } + // 2. 统计各数字的出现次数 + // counter[num] 代表 num 的出现次数 + const counter: number[] = new Array(m + 1).fill(0); + for (const num of nums) { + counter[num]++; + } + // 3. 遍历 counter ,将各元素填入原数组 nums + let i = 0; + for (let num = 0; num < m + 1; num++) { + for (let j = 0; j < counter[num]; j++, i++) { + nums[i] = num; + } + } + } ``` === "C" @@ -373,13 +415,77 @@ $$ === "JavaScript" ```javascript title="counting_sort.js" - [class]{}-[func]{countingSort} + /* 计数排序 */ + // 完整实现,可排序对象,并且是稳定排序 + function countingSort(nums) { + // 1. 统计数组最大元素 m + let m = 0; + for (const num of nums) { + m = Math.max(m, num); + } + // 2. 统计各数字的出现次数 + // counter[num] 代表 num 的出现次数 + const counter = new Array(m + 1).fill(0); + for (const num of nums) { + counter[num]++; + } + // 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引” + // 即 counter[num]-1 是 num 在 res 中最后一次出现的索引 + for (let i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. 倒序遍历 nums ,将各元素填入结果数组 res + // 初始化数组 res 用于记录结果 + const n = nums.length; + const res = new Array(n); + for (let i = n - 1; i >= 0; i--) { + const num = nums[i]; + res[counter[num] - 1] = num; // 将 num 放置到对应索引处 + counter[num]--; // 令前缀和自减 1 ,得到下次放置 num 的索引 + } + // 使用结果数组 res 覆盖原数组 nums + for (let i = 0; i < n; i++) { + nums[i] = res[i]; + } + } ``` === "TypeScript" ```typescript title="counting_sort.ts" - [class]{}-[func]{countingSort} + /* 计数排序 */ + // 完整实现,可排序对象,并且是稳定排序 + function countingSort(nums: number[]): void { + // 1. 统计数组最大元素 m + let m = 0; + for (const num of nums) { + m = Math.max(m, num); + } + // 2. 统计各数字的出现次数 + // counter[num] 代表 num 的出现次数 + const counter: number[] = new Array(m + 1).fill(0); + for (const num of nums) { + counter[num]++; + } + // 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引” + // 即 counter[num]-1 是 num 在 res 中最后一次出现的索引 + for (let i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. 倒序遍历 nums ,将各元素填入结果数组 res + // 初始化数组 res 用于记录结果 + const n = nums.length; + const res: number[] = new Array(n); + for (let i = n - 1; i >= 0; i--) { + const num = nums[i]; + res[counter[num] - 1] = num; // 将 num 放置到对应索引处 + counter[num]--; // 令前缀和自减 1 ,得到下次放置 num 的索引 + } + // 使用结果数组 res 覆盖原数组 nums + for (let i = 0; i < n; i++) { + nums[i] = res[i]; + } + } ``` === "C" diff --git a/chapter_sorting/merge_sort.md b/chapter_sorting/merge_sort.md index d0771fc5f..5205fe295 100755 --- a/chapter_sorting/merge_sort.md +++ b/chapter_sorting/merge_sort.md @@ -56,7 +56,7 @@ comments: true 观察发现,归并排序的递归顺序就是二叉树的「后序遍历」。 -- **后序遍历**:先递归左子树、再递归右子树、最后处理根结点。 +- **后序遍历**:先递归左子树、再递归右子树、最后处理根节点。 - **归并排序**:先递归左子树、再递归右子树、最后处理合并。 === "Java" @@ -507,7 +507,7 @@ comments: true 归并排序有一个很特别的优势,用于排序链表时有很好的性能表现,**空间复杂度可被优化至 $O(1)$** ,这是因为: -- 由于链表可仅通过改变指针来实现结点增删,因此“将两个短有序链表合并为一个长有序链表”无需使用额外空间,即回溯合并阶段不用像排序数组一样建立辅助数组 `tmp` ; +- 由于链表可仅通过改变指针来实现节点增删,因此“将两个短有序链表合并为一个长有序链表”无需使用额外空间,即回溯合并阶段不用像排序数组一样建立辅助数组 `tmp` ; - 通过使用「迭代」代替「递归划分」,可省去递归使用的栈帧空间; > 详情参考:[148. 排序链表](https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/) diff --git a/chapter_sorting/radix_sort.md b/chapter_sorting/radix_sort.md index e8f532f90..5f4366b7d 100644 --- a/chapter_sorting/radix_sort.md +++ b/chapter_sorting/radix_sort.md @@ -238,21 +238,115 @@ $$ === "JavaScript" ```javascript title="radix_sort.js" - [class]{}-[func]{digit} + /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */ + function digit(num, exp) { + // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算 + return Math.floor(num / exp) % 10; + } - [class]{}-[func]{countingSortDigit} + /* 计数排序(根据 nums 第 k 位排序) */ + function countingSortDigit(nums, exp) { + // 十进制的位范围为 0~9 ,因此需要长度为 10 的桶 + const counter = new Array(10).fill(0); + const n = nums.length; + // 统计 0~9 各数字的出现次数 + for (let i = 0; i < n; i++) { + const d = digit(nums[i], exp); // 获取 nums[i] 第 k 位,记为 d + counter[d]++; // 统计数字 d 的出现次数 + } + // 求前缀和,将“出现个数”转换为“数组索引” + for (let i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // 倒序遍历,根据桶内统计结果,将各元素填入 res + const res = new Array(n).fill(0); + for (let i = n - 1; i >= 0; i--) { + const d = digit(nums[i], exp); + const j = counter[d] - 1; // 获取 d 在数组中的索引 j + res[j] = nums[i]; // 将当前元素填入索引 j + counter[d]--; // 将 d 的数量减 1 + } + // 使用结果覆盖原数组 nums + for (let i = 0; i < n; i++) { + nums[i] = res[i]; + } + } - [class]{}-[func]{radixSort} + /* 基数排序 */ + function radixSort(nums) { + // 获取数组的最大元素,用于判断最大位数 + let m = Number.MIN_VALUE; + for (const num of nums) { + if (num > m) { + m = num; + } + } + // 按照从低位到高位的顺序遍历 + for (let exp = 1; exp <= m; exp *= 10) { + // 对数组元素的第 k 位执行计数排序 + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // 即 exp = 10^(k-1) + countingSortDigit(nums, exp); + } + } ``` === "TypeScript" ```typescript title="radix_sort.ts" - [class]{}-[func]{digit} + /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */ + function digit(num: number, exp: number): number { + // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算 + return Math.floor(num / exp) % 10; + } - [class]{}-[func]{countingSortDigit} + /* 计数排序(根据 nums 第 k 位排序) */ + function countingSortDigit(nums: number[], exp: number): void { + // 十进制的位范围为 0~9 ,因此需要长度为 10 的桶 + const counter = new Array(10).fill(0); + const n = nums.length; + // 统计 0~9 各数字的出现次数 + for (let i = 0; i < n; i++) { + const d = digit(nums[i], exp); // 获取 nums[i] 第 k 位,记为 d + counter[d]++; // 统计数字 d 的出现次数 + } + // 求前缀和,将“出现个数”转换为“数组索引” + for (let i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // 倒序遍历,根据桶内统计结果,将各元素填入 res + const res = new Array(n).fill(0); + for (let i = n - 1; i >= 0; i--) { + const d = digit(nums[i], exp); + const j = counter[d] - 1; // 获取 d 在数组中的索引 j + res[j] = nums[i]; // 将当前元素填入索引 j + counter[d]--; // 将 d 的数量减 1 + } + // 使用结果覆盖原数组 nums + for (let i = 0; i < n; i++) { + nums[i] = res[i]; + } + } - [class]{}-[func]{radixSort} + /* 基数排序 */ + function radixSort(nums: number[]): void { + // 获取数组的最大元素,用于判断最大位数 + let m = Number.MIN_VALUE; + for (const num of nums) { + if (num > m) { + m = num; + } + } + // 按照从低位到高位的顺序遍历 + for (let exp = 1; exp <= m; exp *= 10) { + // 对数组元素的第 k 位执行计数排序 + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // 即 exp = 10^(k-1) + countingSortDigit(nums, exp); + } + } ``` === "C" diff --git a/chapter_stack_and_queue/deque.md b/chapter_stack_and_queue/deque.md index 760bd86d7..7fe9d5cb2 100644 --- a/chapter_stack_and_queue/deque.md +++ b/chapter_stack_and_queue/deque.md @@ -295,11 +295,11 @@ comments: true ### 基于双向链表的实现 -回忆上节内容,由于可以方便地删除链表头结点(对应出队操作),以及在链表尾结点后添加新结点(对应入队操作),因此我们使用普通单向链表来实现队列。 +回忆上节内容,由于可以方便地删除链表头节点(对应出队操作),以及在链表尾节点后添加新节点(对应入队操作),因此我们使用普通单向链表来实现队列。 而双向队列的头部和尾部都可以执行入队与出队操作,换言之,双向队列的操作是“首尾对称”的,也需要实现另一个对称方向的操作。因此,双向队列需要使用「双向链表」来实现。 -我们将双向链表的头结点和尾结点分别看作双向队列的队首和队尾,并且实现在两端都能添加与删除结点。 +我们将双向链表的头节点和尾节点分别看作双向队列的队首和队尾,并且实现在两端都能添加与删除节点。 === "LinkedListDeque" ![基于链表实现双向队列的入队出队操作](deque.assets/linkedlist_deque.png) @@ -321,11 +321,11 @@ comments: true === "Java" ```java title="linkedlist_deque.java" - /* 双向链表结点 */ + /* 双向链表节点 */ class ListNode { - int val; // 结点值 - ListNode next; // 后继结点引用(指针) - ListNode prev; // 前驱结点引用(指针) + int val; // 节点值 + ListNode next; // 后继节点引用(指针) + ListNode prev; // 前驱节点引用(指针) ListNode(int val) { this.val = val; prev = next = null; @@ -334,7 +334,7 @@ comments: true /* 基于双向链表实现的双向队列 */ class LinkedListDeque { - private ListNode front, rear; // 头结点 front ,尾结点 rear + private ListNode front, rear; // 头节点 front ,尾节点 rear private int queSize = 0; // 双向队列的长度 public LinkedListDeque() { @@ -362,13 +362,13 @@ comments: true // 将 node 添加至链表头部 front.prev = node; node.next = front; - front = node; // 更新头结点 + front = node; // 更新头节点 // 队尾入队操作 } else { // 将 node 添加至链表尾部 rear.next = node; node.prev = rear; - rear = node; // 更新尾结点 + rear = node; // 更新尾节点 } queSize++; // 更新队列长度 } @@ -391,24 +391,24 @@ comments: true int val; // 队首出队操作 if (isFront) { - val = front.val; // 暂存头结点值 - // 删除头结点 + val = front.val; // 暂存头节点值 + // 删除头节点 ListNode fNext = front.next; if (fNext != null) { fNext.prev = null; front.next = null; } - front = fNext; // 更新头结点 + front = fNext; // 更新头节点 // 队尾出队操作 } else { - val = rear.val; // 暂存尾结点值 - // 删除尾结点 + val = rear.val; // 暂存尾节点值 + // 删除尾节点 ListNode rPrev = rear.prev; if (rPrev != null) { rPrev.next = null; rear.prev = null; } - rear = rPrev; // 更新尾结点 + rear = rPrev; // 更新尾节点 } queSize--; // 更新队列长度 return val; @@ -450,18 +450,18 @@ comments: true === "C++" ```cpp title="linkedlist_deque.cpp" - /* 双向链表结点 */ + /* 双向链表节点 */ struct DoublyListNode { - int val; // 结点值 - DoublyListNode *next; // 后继结点指针 - DoublyListNode *prev; // 前驱结点指针 + int val; // 节点值 + DoublyListNode *next; // 后继节点指针 + DoublyListNode *prev; // 前驱节点指针 DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) {} }; /* 基于双向链表实现的双向队列 */ class LinkedListDeque { private: - DoublyListNode *front, *rear; // 头结点 front ,尾结点 rear + DoublyListNode *front, *rear; // 头节点 front ,尾节点 rear int queSize = 0; // 双向队列的长度 public: @@ -470,7 +470,7 @@ comments: true /* 析构方法 */ ~LinkedListDeque() { - // 遍历链表删除结点,释放内存 + // 遍历链表删除节点,释放内存 DoublyListNode *pre, *cur = front; while (cur != nullptr) { pre = cur; @@ -500,13 +500,13 @@ comments: true // 将 node 添加至链表头部 front->prev = node; node->next = front; - front = node; // 更新头结点 + front = node; // 更新头节点 // 队尾入队操作 } else { // 将 node 添加至链表尾部 rear->next = node; node->prev = rear; - rear = node; // 更新尾结点 + rear = node; // 更新尾节点 } queSize++; // 更新队列长度 } @@ -529,26 +529,26 @@ comments: true int val; // 队首出队操作 if (isFront) { - val = front->val; // 暂存头结点值 - // 删除头结点 + val = front->val; // 暂存头节点值 + // 删除头节点 DoublyListNode *fNext = front->next; if (fNext != nullptr) { fNext->prev = nullptr; front->next = nullptr; delete front; } - front = fNext; // 更新头结点 + front = fNext; // 更新头节点 // 队尾出队操作 } else { - val = rear->val; // 暂存尾结点值 - // 删除尾结点 + val = rear->val; // 暂存尾节点值 + // 删除尾节点 DoublyListNode *rPrev = rear->prev; if (rPrev != nullptr) { rPrev->next = nullptr; rear->prev = nullptr; delete rear; } - rear = rPrev; // 更新尾结点 + rear = rPrev; // 更新尾节点 } queSize--; // 更新队列长度 return val; @@ -591,19 +591,19 @@ comments: true ```python title="linkedlist_deque.py" class ListNode: - """ 双向链表结点 """ + """ 双向链表节点 """ def __init__(self, val: int) -> None: """ 构造方法 """ self.val: int = val - self.next: ListNode | None = None # 后继结点引用(指针) - self.prev: ListNode | None = None # 前驱结点引用(指针) + self.next: ListNode | None = None # 后继节点引用(指针) + self.prev: ListNode | None = None # 前驱节点引用(指针) class LinkedListDeque: """ 基于双向链表实现的双向队列 """ def __init__(self) -> None: """ 构造方法 """ - self.front: ListNode | None = None # 头结点 front - self.rear: ListNode | None = None # 尾结点 rear + self.front: ListNode | None = None # 头节点 front + self.rear: ListNode | None = None # 尾节点 rear self.__size: int = 0 # 双向队列的长度 def size(self) -> int: @@ -625,13 +625,13 @@ comments: true # 将 node 添加至链表头部 self.front.prev = node node.next = self.front - self.front = node # 更新头结点 + self.front = node # 更新头节点 # 队尾入队操作 else: # 将 node 添加至链表尾部 self.rear.next = node node.prev = self.rear - self.rear = node # 更新尾结点 + self.rear = node # 更新尾节点 self.__size += 1 # 更新队列长度 def push_first(self, num: int) -> None: @@ -649,22 +649,22 @@ comments: true return None # 队首出队操作 if is_front: - val: int = self.front.val # 暂存头结点值 - # 删除头结点 + val: int = self.front.val # 暂存头节点值 + # 删除头节点 fnext: ListNode | None = self.front.next if fnext != None: fnext.prev = None self.front.next = None - self.front = fnext # 更新头结点 + self.front = fnext # 更新头节点 # 队尾出队操作 else: - val: int = self.rear.val # 暂存尾结点值 - # 删除尾结点 + val: int = self.rear.val # 暂存尾节点值 + # 删除尾节点 rprev: ListNode | None = self.rear.prev if rprev != None: rprev.next = None self.rear.prev = None - self.rear = rprev # 更新尾结点 + self.rear = rprev # 更新尾节点 self.__size -= 1 # 更新队列长度 return val @@ -777,11 +777,11 @@ comments: true === "JavaScript" ```javascript title="linkedlist_deque.js" - /* 双向链表结点 */ + /* 双向链表节点 */ class ListNode { - prev; // 前驱结点引用 (指针) - next; // 后继结点引用 (指针) - val; // 结点值 + prev; // 前驱节点引用 (指针) + next; // 后继节点引用 (指针) + val; // 节点值 constructor(val) { this.val = val; @@ -792,8 +792,8 @@ comments: true /* 基于双向链表实现的双向队列 */ class LinkedListDeque { - #front; // 头结点 front - #rear; // 尾结点 rear + #front; // 头节点 front + #rear; // 尾节点 rear #queSize; // 双向队列的长度 constructor() { @@ -813,7 +813,7 @@ comments: true // 将 node 添加至链表尾部 this.#rear.next = node; node.prev = this.#rear; - this.#rear = node; // 更新尾结点 + this.#rear = node; // 更新尾节点 } this.#queSize++; } @@ -829,7 +829,7 @@ comments: true // 将 node 添加至链表头部 this.#front.prev = node; node.next = this.#front; - this.#front = node; // 更新头结点 + this.#front = node; // 更新头节点 } this.#queSize++; } @@ -839,14 +839,14 @@ comments: true if (this.#queSize === 0) { return null; } - const value = this.#rear.val; // 存储尾结点值 - // 删除尾结点 + const value = this.#rear.val; // 存储尾节点值 + // 删除尾节点 let temp = this.#rear.prev; if (temp !== null) { temp.next = null; this.#rear.prev = null; } - this.#rear = temp; // 更新尾结点 + this.#rear = temp; // 更新尾节点 this.#queSize--; return value; } @@ -856,14 +856,14 @@ comments: true if (this.#queSize === 0) { return null; } - const value = this.#front.val; // 存储尾结点值 - // 删除头结点 + const value = this.#front.val; // 存储尾节点值 + // 删除头节点 let temp = this.#front.next; if (temp !== null) { temp.prev = null; this.#front.next = null; } - this.#front = temp; // 更新头结点 + this.#front = temp; // 更新头节点 this.#queSize--; return value; } @@ -904,11 +904,11 @@ comments: true === "TypeScript" ```typescript title="linkedlist_deque.ts" - /* 双向链表结点 */ + /* 双向链表节点 */ class ListNode { - prev: ListNode; // 前驱结点引用 (指针) - next: ListNode; // 后继结点引用 (指针) - val: number; // 结点值 + prev: ListNode; // 前驱节点引用 (指针) + next: ListNode; // 后继节点引用 (指针) + val: number; // 节点值 constructor(val: number) { this.val = val; @@ -919,8 +919,8 @@ comments: true /* 基于双向链表实现的双向队列 */ class LinkedListDeque { - private front: ListNode; // 头结点 front - private rear: ListNode; // 尾结点 rear + private front: ListNode; // 头节点 front + private rear: ListNode; // 尾节点 rear private queSize: number; // 双向队列的长度 constructor() { @@ -940,7 +940,7 @@ comments: true // 将 node 添加至链表尾部 this.rear.next = node; node.prev = this.rear; - this.rear = node; // 更新尾结点 + this.rear = node; // 更新尾节点 } this.queSize++; } @@ -956,7 +956,7 @@ comments: true // 将 node 添加至链表头部 this.front.prev = node; node.next = this.front; - this.front = node; // 更新头结点 + this.front = node; // 更新头节点 } this.queSize++; } @@ -966,14 +966,14 @@ comments: true if (this.queSize === 0) { return null; } - const value: number = this.rear.val; // 存储尾结点值 - // 删除尾结点 + const value: number = this.rear.val; // 存储尾节点值 + // 删除尾节点 let temp: ListNode = this.rear.prev; if (temp !== null) { temp.next = null; this.rear.prev = null; } - this.rear = temp; // 更新尾结点 + this.rear = temp; // 更新尾节点 this.queSize--; return value; } @@ -983,14 +983,14 @@ comments: true if (this.queSize === 0) { return null; } - const value: number = this.front.val; // 存储尾结点值 - // 删除头结点 + const value: number = this.front.val; // 存储尾节点值 + // 删除头节点 let temp: ListNode = this.front.next; if (temp !== null) { temp.prev = null; this.front.next = null; } - this.front = temp; // 更新头结点 + this.front = temp; // 更新头节点 this.queSize--; return value; } @@ -1047,11 +1047,11 @@ comments: true === "Swift" ```swift title="linkedlist_deque.swift" - /* 双向链表结点 */ + /* 双向链表节点 */ class ListNode { - var val: Int // 结点值 - var next: ListNode? // 后继结点引用(指针) - var prev: ListNode? // 前驱结点引用(指针) + var val: Int // 节点值 + var next: ListNode? // 后继节点引用(指针) + var prev: ListNode? // 前驱节点引用(指针) init(val: Int) { self.val = val @@ -1060,8 +1060,8 @@ comments: true /* 基于双向链表实现的双向队列 */ class LinkedListDeque { - private var front: ListNode? // 头结点 front - private var rear: ListNode? // 尾结点 rear + private var front: ListNode? // 头节点 front + private var rear: ListNode? // 尾节点 rear private var queSize: Int // 双向队列的长度 init() { @@ -1091,14 +1091,14 @@ comments: true // 将 node 添加至链表头部 front?.prev = node node.next = front - front = node // 更新头结点 + front = node // 更新头节点 } // 队尾入队操作 else { // 将 node 添加至链表尾部 rear?.next = node node.prev = rear - rear = node // 更新尾结点 + rear = node // 更新尾节点 } queSize += 1 // 更新队列长度 } @@ -1121,25 +1121,25 @@ comments: true let val: Int // 队首出队操作 if isFront { - val = front!.val // 暂存头结点值 - // 删除头结点 + val = front!.val // 暂存头节点值 + // 删除头节点 let fNext = front?.next if fNext != nil { fNext?.prev = nil front?.next = nil } - front = fNext // 更新头结点 + front = fNext // 更新头节点 } // 队尾出队操作 else { - val = rear!.val // 暂存尾结点值 - // 删除尾结点 + val = rear!.val // 暂存尾节点值 + // 删除尾节点 let rPrev = rear?.prev if rPrev != nil { rPrev?.next = nil rear?.prev = nil } - rear = rPrev // 更新尾结点 + rear = rPrev // 更新尾节点 } queSize -= 1 // 更新队列长度 return val @@ -1181,14 +1181,14 @@ comments: true === "Zig" ```zig title="linkedlist_deque.zig" - // 双向链表结点 + // 双向链表节点 fn ListNode(comptime T: type) type { return struct { const Self = @This(); - val: T = undefined, // 结点值 - next: ?*Self = null, // 后继结点引用(指针) - prev: ?*Self = null, // 前驱结点引用(指针) + val: T = undefined, // 节点值 + next: ?*Self = null, // 后继节点引用(指针) + prev: ?*Self = null, // 前驱节点引用(指针) // Initialize a list node with specific value pub fn init(self: *Self, x: i32) void { @@ -1204,8 +1204,8 @@ comments: true return struct { const Self = @This(); - front: ?*ListNode(T) = null, // 头结点 front - rear: ?*ListNode(T) = null, // 尾结点 rear + front: ?*ListNode(T) = null, // 头节点 front + rear: ?*ListNode(T) = null, // 尾节点 rear que_size: usize = 0, // 双向队列的长度 mem_arena: ?std.heap.ArenaAllocator = null, mem_allocator: std.mem.Allocator = undefined, // 内存分配器 @@ -1250,13 +1250,13 @@ comments: true // 将 node 添加至链表头部 self.front.?.prev = node; node.next = self.front; - self.front = node; // 更新头结点 + self.front = node; // 更新头节点 // 队尾入队操作 } else { // 将 node 添加至链表尾部 self.rear.?.next = node; node.prev = self.rear; - self.rear = node; // 更新尾结点 + self.rear = node; // 更新尾节点 } self.que_size += 1; // 更新队列长度 } @@ -1277,24 +1277,24 @@ comments: true var val: T = undefined; // 队首出队操作 if (is_front) { - val = self.front.?.val; // 暂存头结点值 - // 删除头结点 + val = self.front.?.val; // 暂存头节点值 + // 删除头节点 var fNext = self.front.?.next; if (fNext != null) { fNext.?.prev = null; self.front.?.next = null; } - self.front = fNext; // 更新头结点 + self.front = fNext; // 更新头节点 // 队尾出队操作 } else { - val = self.rear.?.val; // 暂存尾结点值 - // 删除尾结点 + val = self.rear.?.val; // 暂存尾节点值 + // 删除尾节点 var rPrev = self.rear.?.prev; if (rPrev != null) { rPrev.?.next = null; self.rear.?.prev = null; } - self.rear = rPrev; // 更新尾结点 + self.rear = rPrev; // 更新尾节点 } self.que_size -= 1; // 更新队列长度 return val; diff --git a/chapter_stack_and_queue/queue.md b/chapter_stack_and_queue/queue.md index 1e96571aa..5c1f76d48 100755 --- a/chapter_stack_and_queue/queue.md +++ b/chapter_stack_and_queue/queue.md @@ -264,7 +264,7 @@ comments: true ### 基于链表的实现 -我们将链表的「头结点」和「尾结点」分别看作是队首和队尾,并规定队尾只可添加结点,队首只可删除结点。 +我们将链表的「头节点」和「尾节点」分别看作是队首和队尾,并规定队尾只可添加节点,队首只可删除节点。 === "LinkedListQueue" ![基于链表实现队列的入队出队操作](queue.assets/linkedlist_queue.png) @@ -282,7 +282,7 @@ comments: true ```java title="linkedlist_queue.java" /* 基于链表实现的队列 */ class LinkedListQueue { - private ListNode front, rear; // 头结点 front ,尾结点 rear + private ListNode front, rear; // 头节点 front ,尾节点 rear private int queSize = 0; public LinkedListQueue() { @@ -302,13 +302,13 @@ comments: true /* 入队 */ public void push(int num) { - // 尾结点后添加 num + // 尾节点后添加 num ListNode node = new ListNode(num); - // 如果队列为空,则令头、尾结点都指向该结点 + // 如果队列为空,则令头、尾节点都指向该节点 if (front == null) { front = node; rear = node; - // 如果队列不为空,则将该结点添加到尾结点后 + // 如果队列不为空,则将该节点添加到尾节点后 } else { rear.next = node; rear = node; @@ -319,7 +319,7 @@ comments: true /* 出队 */ public int pop() { int num = peek(); - // 删除头结点 + // 删除头节点 front = front.next; queSize--; return num; @@ -351,7 +351,7 @@ comments: true /* 基于链表实现的队列 */ class LinkedListQueue { private: - ListNode *front, *rear; // 头结点 front ,尾结点 rear + ListNode *front, *rear; // 头节点 front ,尾节点 rear int queSize; public: @@ -362,7 +362,7 @@ comments: true } ~LinkedListQueue() { - // 遍历链表删除结点,释放内存 + // 遍历链表删除节点,释放内存 freeMemoryLinkedList(front); } @@ -378,14 +378,14 @@ comments: true /* 入队 */ void push(int num) { - // 尾结点后添加 num + // 尾节点后添加 num ListNode* node = new ListNode(num); - // 如果队列为空,则令头、尾结点都指向该结点 + // 如果队列为空,则令头、尾节点都指向该节点 if (front == nullptr) { front = node; rear = node; } - // 如果队列不为空,则将该结点添加到尾结点后 + // 如果队列不为空,则将该节点添加到尾节点后 else { rear->next = node; rear = node; @@ -396,7 +396,7 @@ comments: true /* 出队 */ void pop() { int num = peek(); - // 删除头结点 + // 删除头节点 ListNode *tmp = front; front = front->next; // 释放内存 @@ -431,8 +431,8 @@ comments: true """ 基于链表实现的队列 """ def __init__(self): """ 构造方法 """ - self.__front: ListNode | None = None # 头结点 front - self.__rear: ListNode | None = None # 尾结点 rear + self.__front: ListNode | None = None # 头节点 front + self.__rear: ListNode | None = None # 尾节点 rear self.__size: int = 0 def size(self) -> int: @@ -445,13 +445,13 @@ comments: true def push(self, num: int) -> None: """ 入队 """ - # 尾结点后添加 num + # 尾节点后添加 num node = ListNode(num) - # 如果队列为空,则令头、尾结点都指向该结点 + # 如果队列为空,则令头、尾节点都指向该节点 if self.__front is None: self.__front = node self.__rear = node - # 如果队列不为空,则将该结点添加到尾结点后 + # 如果队列不为空,则将该节点添加到尾节点后 else: self.__rear.next = node self.__rear = node @@ -460,7 +460,7 @@ comments: true def pop(self) -> int: """ 出队 """ num = self.peek() - # 删除头结点 + # 删除头节点 self.__front = self.__front.next self.__size -= 1 return num @@ -543,8 +543,8 @@ comments: true ```javascript title="linkedlist_queue.js" /* 基于链表实现的队列 */ class LinkedListQueue { - #front; // 头结点 #front - #rear; // 尾结点 #rear + #front; // 头节点 #front + #rear; // 尾节点 #rear #queSize = 0; constructor() { @@ -564,13 +564,13 @@ comments: true /* 入队 */ push(num) { - // 尾结点后添加 num + // 尾节点后添加 num const node = new ListNode(num); - // 如果队列为空,则令头、尾结点都指向该结点 + // 如果队列为空,则令头、尾节点都指向该节点 if (!this.#front) { this.#front = node; this.#rear = node; - // 如果队列不为空,则将该结点添加到尾结点后 + // 如果队列不为空,则将该节点添加到尾节点后 } else { this.#rear.next = node; this.#rear = node; @@ -581,7 +581,7 @@ comments: true /* 出队 */ pop() { const num = this.peek(); - // 删除头结点 + // 删除头节点 this.#front = this.#front.next; this.#queSize--; return num; @@ -612,8 +612,8 @@ comments: true ```typescript title="linkedlist_queue.ts" /* 基于链表实现的队列 */ class LinkedListQueue { - private front: ListNode | null; // 头结点 front - private rear: ListNode | null; // 尾结点 rear + private front: ListNode | null; // 头节点 front + private rear: ListNode | null; // 尾节点 rear private queSize: number = 0; constructor() { @@ -633,13 +633,13 @@ comments: true /* 入队 */ push(num: number): void { - // 尾结点后添加 num + // 尾节点后添加 num const node = new ListNode(num); - // 如果队列为空,则令头、尾结点都指向该结点 + // 如果队列为空,则令头、尾节点都指向该节点 if (!this.front) { this.front = node; this.rear = node; - // 如果队列不为空,则将该结点添加到尾结点后 + // 如果队列不为空,则将该节点添加到尾节点后 } else { this.rear!.next = node; this.rear = node; @@ -651,7 +651,7 @@ comments: true pop(): number { const num = this.peek(); if (!this.front) throw new Error('队列为空'); - // 删除头结点 + // 删除头节点 this.front = this.front.next; this.queSize--; return num; @@ -688,7 +688,7 @@ comments: true /* 基于链表实现的队列 */ class LinkedListQueue { - private ListNode? front, rear; // 头结点 front ,尾结点 rear + private ListNode? front, rear; // 头节点 front ,尾节点 rear private int queSize = 0; public LinkedListQueue() @@ -712,14 +712,14 @@ comments: true /* 入队 */ public void push(int num) { - // 尾结点后添加 num + // 尾节点后添加 num ListNode node = new ListNode(num); - // 如果队列为空,则令头、尾结点都指向该结点 + // 如果队列为空,则令头、尾节点都指向该节点 if (front == null) { front = node; rear = node; - // 如果队列不为空,则将该结点添加到尾结点后 + // 如果队列不为空,则将该节点添加到尾节点后 } else if (rear != null) { @@ -733,7 +733,7 @@ comments: true public int pop() { int num = peek(); - // 删除头结点 + // 删除头节点 front = front?.next; queSize--; return num; @@ -770,8 +770,8 @@ comments: true ```swift title="linkedlist_queue.swift" /* 基于链表实现的队列 */ class LinkedListQueue { - private var front: ListNode? // 头结点 - private var rear: ListNode? // 尾结点 + private var front: ListNode? // 头节点 + private var rear: ListNode? // 尾节点 private var _size = 0 init() {} @@ -788,14 +788,14 @@ comments: true /* 入队 */ func push(num: Int) { - // 尾结点后添加 num + // 尾节点后添加 num let node = ListNode(x: num) - // 如果队列为空,则令头、尾结点都指向该结点 + // 如果队列为空,则令头、尾节点都指向该节点 if front == nil { front = node rear = node } - // 如果队列不为空,则将该结点添加到尾结点后 + // 如果队列不为空,则将该节点添加到尾节点后 else { rear?.next = node rear = node @@ -807,7 +807,7 @@ comments: true @discardableResult func pop() -> Int { let num = peek() - // 删除头结点 + // 删除头节点 front = front?.next _size -= 1 return num @@ -842,8 +842,8 @@ comments: true return struct { const Self = @This(); - front: ?*inc.ListNode(T) = null, // 头结点 front - rear: ?*inc.ListNode(T) = null, // 尾结点 rear + front: ?*inc.ListNode(T) = null, // 头节点 front + rear: ?*inc.ListNode(T) = null, // 尾节点 rear que_size: usize = 0, // 队列的长度 mem_arena: ?std.heap.ArenaAllocator = null, mem_allocator: std.mem.Allocator = undefined, // 内存分配器 @@ -883,14 +883,14 @@ comments: true // 入队 pub fn push(self: *Self, num: T) !void { - // 尾结点后添加 num + // 尾节点后添加 num var node = try self.mem_allocator.create(inc.ListNode(T)); node.init(num); - // 如果队列为空,则令头、尾结点都指向该结点 + // 如果队列为空,则令头、尾节点都指向该节点 if (self.front == null) { self.front = node; self.rear = node; - // 如果队列不为空,则将该结点添加到尾结点后 + // 如果队列不为空,则将该节点添加到尾节点后 } else { self.rear.?.next = node; self.rear = node; @@ -901,7 +901,7 @@ comments: true // 出队 pub fn pop(self: *Self) T { var num = self.peek(); - // 删除头结点 + // 删除头节点 self.front = self.front.?.next; self.que_size -= 1; return num; diff --git a/chapter_stack_and_queue/stack.md b/chapter_stack_and_queue/stack.md index 5a05cb92d..8412b2817 100755 --- a/chapter_stack_and_queue/stack.md +++ b/chapter_stack_and_queue/stack.md @@ -264,9 +264,9 @@ comments: true ### 基于链表的实现 -使用「链表」实现栈时,将链表的头结点看作栈顶,将尾结点看作栈底。 +使用「链表」实现栈时,将链表的头节点看作栈顶,将尾节点看作栈底。 -对于入栈操作,将元素插入到链表头部即可,这种结点添加方式被称为“头插法”。而对于出栈操作,则将头结点从链表中删除即可。 +对于入栈操作,将元素插入到链表头部即可,这种节点添加方式被称为“头插法”。而对于出栈操作,则将头节点从链表中删除即可。 === "LinkedListStack" ![基于链表实现栈的入栈出栈操作](stack.assets/linkedlist_stack.png) @@ -284,7 +284,7 @@ comments: true ```java title="linkedlist_stack.java" /* 基于链表实现的栈 */ class LinkedListStack { - private ListNode stackPeek; // 将头结点作为栈顶 + private ListNode stackPeek; // 将头节点作为栈顶 private int stkSize = 0; // 栈的长度 public LinkedListStack() { @@ -343,7 +343,7 @@ comments: true /* 基于链表实现的栈 */ class LinkedListStack { private: - ListNode* stackTop; // 将头结点作为栈顶 + ListNode* stackTop; // 将头节点作为栈顶 int stkSize; // 栈的长度 public: @@ -353,7 +353,7 @@ comments: true } ~LinkedListStack() { - // 遍历链表删除结点,释放内存 + // 遍历链表删除节点,释放内存 freeMemoryLinkedList(stackTop); } @@ -515,7 +515,7 @@ comments: true ```javascript title="linkedlist_stack.js" /* 基于链表实现的栈 */ class LinkedListStack { - #stackPeek; // 将头结点作为栈顶 + #stackPeek; // 将头节点作为栈顶 #stkSize = 0; // 栈的长度 constructor() { @@ -573,7 +573,7 @@ comments: true ```typescript title="linkedlist_stack.ts" /* 基于链表实现的栈 */ class LinkedListStack { - private stackPeek: ListNode | null; // 将头结点作为栈顶 + private stackPeek: ListNode | null; // 将头节点作为栈顶 private stkSize: number = 0; // 栈的长度 constructor() { @@ -638,7 +638,7 @@ comments: true /* 基于链表实现的栈 */ class LinkedListStack { - private ListNode? stackPeek; // 将头结点作为栈顶 + private ListNode? stackPeek; // 将头节点作为栈顶 private int stkSize = 0; // 栈的长度 public LinkedListStack() @@ -710,7 +710,7 @@ comments: true ```swift title="linkedlist_stack.swift" /* 基于链表实现的栈 */ class LinkedListStack { - private var _peek: ListNode? // 将头结点作为栈顶 + private var _peek: ListNode? // 将头节点作为栈顶 private var _size = 0 // 栈的长度 init() {} @@ -771,7 +771,7 @@ comments: true return struct { const Self = @This(); - stack_top: ?*inc.ListNode(T) = null, // 将头结点作为栈顶 + stack_top: ?*inc.ListNode(T) = null, // 将头节点作为栈顶 stk_size: usize = 0, // 栈的长度 mem_arena: ?std.heap.ArenaAllocator = null, mem_allocator: std.mem.Allocator = undefined, // 内存分配器 @@ -1311,7 +1311,7 @@ comments: true 在数组(列表)实现中,入栈与出栈操作都是在预先分配好的连续内存中操作,具有很好的缓存本地性,效率很好。然而,如果入栈时超出数组容量,则会触发扩容机制,那么该次入栈操作的时间复杂度为 $O(n)$ 。 -在链表实现中,链表的扩容非常灵活,不存在上述数组扩容时变慢的问题。然而,入栈操作需要初始化结点对象并修改指针,因而效率不如数组。进一步地思考,如果入栈元素不是 `int` 而是结点对象,那么就可以省去初始化步骤,从而提升效率。 +在链表实现中,链表的扩容非常灵活,不存在上述数组扩容时变慢的问题。然而,入栈操作需要初始化节点对象并修改指针,因而效率不如数组。进一步地思考,如果入栈元素不是 `int` 而是节点对象,那么就可以省去初始化步骤,从而提升效率。 综上所述,当入栈与出栈操作的元素是基本数据类型(例如 `int` , `double` )时,则结论如下: @@ -1322,7 +1322,7 @@ comments: true 在初始化列表时,系统会给列表分配“初始容量”,该容量可能超过我们的需求。并且扩容机制一般是按照特定倍率(比如 2 倍)进行扩容,扩容后的容量也可能超出我们的需求。因此,**数组实现栈会造成一定的空间浪费**。 -当然,由于结点需要额外存储指针,因此 **链表结点比数组元素占用更大**。 +当然,由于节点需要额外存储指针,因此 **链表节点比数组元素占用更大**。 综上,我们不能简单地确定哪种实现更加省内存,需要 case-by-case 地分析。 diff --git a/chapter_stack_and_queue/summary.md b/chapter_stack_and_queue/summary.md index 3e1fe19aa..5bba4659f 100644 --- a/chapter_stack_and_queue/summary.md +++ b/chapter_stack_and_queue/summary.md @@ -6,6 +6,6 @@ comments: true - 栈是一种遵循先入后出的数据结构,可以使用数组或链表实现。 - 在时间效率方面,栈的数组实现具有更好的平均效率,但扩容时会导致单次入栈操作的时间复杂度劣化至 $O(n)$ 。相对地,栈的链表实现具有更加稳定的效率表现。 -- 在空间效率方面,栈的数组实现会造成一定空间浪费,然而链表结点比数组元素占用内存更大。 +- 在空间效率方面,栈的数组实现会造成一定空间浪费,然而链表节点比数组元素占用内存更大。 - 队列是一种遵循先入先出的数据结构,可以使用数组或链表实现。对于两种实现的时间效率与空间效率对比,与上述栈的结论相同。 - 双向队列的两端都可以添加与删除元素。 diff --git a/chapter_tree/avl_tree.md b/chapter_tree/avl_tree.md index 007e69f61..8e26e9d84 100644 --- a/chapter_tree/avl_tree.md +++ b/chapter_tree/avl_tree.md @@ -6,19 +6,19 @@ comments: true 在「二叉搜索树」章节中提到,在进行多次插入与删除操作后,二叉搜索树可能会退化为链表。此时所有操作的时间复杂度都会由 $O(\log n)$ 劣化至 $O(n)$ 。 -如下图所示,执行两步删除结点后,该二叉搜索树就会退化为链表。 +如下图所示,执行两步删除节点后,该二叉搜索树就会退化为链表。 -![AVL 树在删除结点后发生退化](avl_tree.assets/avltree_degradation_from_removing_node.png) +![AVL 树在删除节点后发生退化](avl_tree.assets/avltree_degradation_from_removing_node.png) -

Fig. AVL 树在删除结点后发生退化

+

Fig. AVL 树在删除节点后发生退化

-再比如,在以下完美二叉树中插入两个结点后,树严重向左偏斜,查找操作的时间复杂度也随之发生劣化。 +再比如,在以下完美二叉树中插入两个节点后,树严重向左偏斜,查找操作的时间复杂度也随之发生劣化。 -![AVL 树在插入结点后发生退化](avl_tree.assets/avltree_degradation_from_inserting_node.png) +![AVL 树在插入节点后发生退化](avl_tree.assets/avltree_degradation_from_inserting_node.png) -

Fig. AVL 树在插入结点后发生退化

+

Fig. AVL 树在插入节点后发生退化

-G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorithm for the organization of information" 中提出了「AVL 树」。**论文中描述了一系列操作,使得在不断添加与删除结点后,AVL 树仍然不会发生退化**,进而使得各种操作的时间复杂度均能保持在 $O(\log n)$ 级别。 +G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorithm for the organization of information" 中提出了「AVL 树」。**论文中描述了一系列操作,使得在不断添加与删除节点后,AVL 树仍然不会发生退化**,进而使得各种操作的时间复杂度均能保持在 $O(\log n)$ 级别。 换言之,在频繁增删查改的使用场景中,AVL 树可始终保持很高的数据增删查改效率,具有很好的应用价值。 @@ -26,19 +26,19 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit 「AVL 树」既是「二叉搜索树」又是「平衡二叉树」,同时满足这两种二叉树的所有性质,因此又被称为「平衡二叉搜索树」。 -### 结点高度 +### 节点高度 -在 AVL 树的操作中,需要获取结点「高度 Height」,所以给 AVL 树的结点类添加 `height` 变量。 +在 AVL 树的操作中,需要获取节点「高度 Height」,所以给 AVL 树的节点类添加 `height` 变量。 === "Java" ```java title="" - /* AVL 树结点类 */ + /* AVL 树节点类 */ class TreeNode { - public int val; // 结点值 - public int height; // 结点高度 - public TreeNode left; // 左子结点 - public TreeNode right; // 右子结点 + public int val; // 节点值 + public int height; // 节点高度 + public TreeNode left; // 左子节点 + public TreeNode right; // 右子节点 public TreeNode(int x) { val = x; } } ``` @@ -46,12 +46,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit === "C++" ```cpp title="" - /* AVL 树结点类 */ + /* AVL 树节点类 */ struct TreeNode { - int val{}; // 结点值 - int height = 0; // 结点高度 - TreeNode *left{}; // 左子结点 - TreeNode *right{}; // 右子结点 + int val{}; // 节点值 + int height = 0; // 节点高度 + TreeNode *left{}; // 左子节点 + TreeNode *right{}; // 右子节点 TreeNode() = default; explicit TreeNode(int x) : val(x){} }; @@ -60,24 +60,24 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit === "Python" ```python title="" - """ AVL 树结点类 """ + """ AVL 树节点类 """ class TreeNode: def __init__(self, val: int): - self.val: int = val # 结点值 - self.height: int = 0 # 结点高度 - self.left: Optional[TreeNode] = None # 左子结点引用 - self.right: Optional[TreeNode] = None # 右子结点引用 + self.val: int = val # 节点值 + self.height: int = 0 # 节点高度 + self.left: Optional[TreeNode] = None # 左子节点引用 + self.right: Optional[TreeNode] = None # 右子节点引用 ``` === "Go" ```go title="" - /* AVL 树结点类 */ + /* AVL 树节点类 */ type TreeNode struct { - Val int // 结点值 - Height int // 结点高度 - Left *TreeNode // 左子结点引用 - Right *TreeNode // 右子结点引用 + Val int // 节点值 + Height int // 节点高度 + Left *TreeNode // 左子节点引用 + Right *TreeNode // 右子节点引用 } ``` @@ -85,10 +85,10 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit ```javascript title="" class TreeNode { - val; // 结点值 - height; //结点高度 - left; // 左子结点指针 - right; // 右子结点指针 + val; // 节点值 + height; //节点高度 + left; // 左子节点指针 + right; // 右子节点指针 constructor(val, left, right, height) { this.val = val === undefined ? 0 : val; this.height = height === undefined ? 0 : height; @@ -102,10 +102,10 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit ```typescript title="" class TreeNode { - val: number; // 结点值 - height: number; // 结点高度 - left: TreeNode | null; // 左子结点指针 - right: TreeNode | null; // 右子结点指针 + val: number; // 节点值 + height: number; // 节点高度 + left: TreeNode | null; // 左子节点指针 + right: TreeNode | null; // 右子节点指针 constructor(val?: number, height?: number, left?: TreeNode | null, right?: TreeNode | null) { this.val = val === undefined ? 0 : val; this.height = height === undefined ? 0 : height; @@ -124,12 +124,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit === "C#" ```csharp title="" - /* AVL 树结点类 */ + /* AVL 树节点类 */ class TreeNode { - public int val; // 结点值 - public int height; // 结点高度 - public TreeNode? left; // 左子结点 - public TreeNode? right; // 右子结点 + public int val; // 节点值 + public int height; // 节点高度 + public TreeNode? left; // 左子节点 + public TreeNode? right; // 右子节点 public TreeNode(int x) { val = x; } } ``` @@ -137,12 +137,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit === "Swift" ```swift title="" - /* AVL 树结点类 */ + /* AVL 树节点类 */ class TreeNode { - var val: Int // 结点值 - var height: Int // 结点高度 - var left: TreeNode? // 左子结点 - var right: TreeNode? // 右子结点 + var val: Int // 节点值 + var height: Int // 节点高度 + var left: TreeNode? // 左子节点 + var right: TreeNode? // 右子节点 init(x: Int) { val = x @@ -157,20 +157,20 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit ``` -「结点高度」是最远叶结点到该结点的距离,即走过的「边」的数量。需要特别注意,**叶结点的高度为 0 ,空结点的高度为 -1**。我们封装两个工具函数,分别用于获取与更新结点的高度。 +「节点高度」是最远叶节点到该节点的距离,即走过的「边」的数量。需要特别注意,**叶节点的高度为 0 ,空节点的高度为 -1**。我们封装两个工具函数,分别用于获取与更新节点的高度。 === "Java" ```java title="avl_tree.java" - /* 获取结点高度 */ + /* 获取节点高度 */ int height(TreeNode node) { - // 空结点高度为 -1 ,叶结点高度为 0 + // 空节点高度为 -1 ,叶节点高度为 0 return node == null ? -1 : node.height; } - /* 更新结点高度 */ + /* 更新节点高度 */ void updateHeight(TreeNode node) { - // 结点高度等于最高子树高度 + 1 + // 节点高度等于最高子树高度 + 1 node.height = Math.max(height(node.left), height(node.right)) + 1; } ``` @@ -178,15 +178,15 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit === "C++" ```cpp title="avl_tree.cpp" - /* 获取结点高度 */ + /* 获取节点高度 */ int height(TreeNode* node) { - // 空结点高度为 -1 ,叶结点高度为 0 + // 空节点高度为 -1 ,叶节点高度为 0 return node == nullptr ? -1 : node->height; } - /* 更新结点高度 */ + /* 更新节点高度 */ void updateHeight(TreeNode* node) { - // 结点高度等于最高子树高度 + 1 + // 节点高度等于最高子树高度 + 1 node->height = max(height(node->left), height(node->right)) + 1; } ``` @@ -195,35 +195,35 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit ```python title="avl_tree.py" def height(self, node: TreeNode | None) -> int: - """ 获取结点高度 """ - # 空结点高度为 -1 ,叶结点高度为 0 + """ 获取节点高度 """ + # 空节点高度为 -1 ,叶节点高度为 0 if node is not None: return node.height return -1 def __update_height(self, node: TreeNode | None): - """ 更新结点高度 """ - # 结点高度等于最高子树高度 + 1 + """ 更新节点高度 """ + # 节点高度等于最高子树高度 + 1 node.height = max([self.height(node.left), self.height(node.right)]) + 1 ``` === "Go" ```go title="avl_tree.go" - /* 获取结点高度 */ + /* 获取节点高度 */ func (t *aVLTree) height(node *TreeNode) int { - // 空结点高度为 -1 ,叶结点高度为 0 + // 空节点高度为 -1 ,叶节点高度为 0 if node != nil { return node.Height } return -1 } - /* 更新结点高度 */ + /* 更新节点高度 */ func (t *aVLTree) updateHeight(node *TreeNode) { lh := t.height(node.Left) rh := t.height(node.Right) - // 结点高度等于最高子树高度 + 1 + // 节点高度等于最高子树高度 + 1 if lh > rh { node.Height = lh + 1 } else { @@ -235,15 +235,15 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit === "JavaScript" ```javascript title="avl_tree.js" - /* 获取结点高度 */ + /* 获取节点高度 */ height(node) { - // 空结点高度为 -1 ,叶结点高度为 0 + // 空节点高度为 -1 ,叶节点高度为 0 return node === null ? -1 : node.height; } - /* 更新结点高度 */ + /* 更新节点高度 */ #updateHeight(node) { - // 结点高度等于最高子树高度 + 1 + // 节点高度等于最高子树高度 + 1 node.height = Math.max(this.height(node.left), this.height(node.right)) + 1; } ``` @@ -251,15 +251,15 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit === "TypeScript" ```typescript title="avl_tree.ts" - /* 获取结点高度 */ + /* 获取节点高度 */ height(node: TreeNode): number { - // 空结点高度为 -1 ,叶结点高度为 0 + // 空节点高度为 -1 ,叶节点高度为 0 return node === null ? -1 : node.height; } - /* 更新结点高度 */ + /* 更新节点高度 */ updateHeight(node: TreeNode): void { - // 结点高度等于最高子树高度 + 1 + // 节点高度等于最高子树高度 + 1 node.height = Math.max(this.height(node.left), this.height(node.right)) + 1; } ``` @@ -275,17 +275,17 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit === "C#" ```csharp title="avl_tree.cs" - /* 获取结点高度 */ + /* 获取节点高度 */ int height(TreeNode? node) { - // 空结点高度为 -1 ,叶结点高度为 0 + // 空节点高度为 -1 ,叶节点高度为 0 return node == null ? -1 : node.height; } - /* 更新结点高度 */ + /* 更新节点高度 */ void updateHeight(TreeNode node) { - // 结点高度等于最高子树高度 + 1 + // 节点高度等于最高子树高度 + 1 node.height = Math.Max(height(node.left), height(node.right)) + 1; } ``` @@ -293,15 +293,15 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit === "Swift" ```swift title="avl_tree.swift" - /* 获取结点高度 */ + /* 获取节点高度 */ func height(node: TreeNode?) -> Int { - // 空结点高度为 -1 ,叶结点高度为 0 + // 空节点高度为 -1 ,叶节点高度为 0 node == nil ? -1 : node!.height } - /* 更新结点高度 */ + /* 更新节点高度 */ func updateHeight(node: TreeNode?) { - // 结点高度等于最高子树高度 + 1 + // 节点高度等于最高子树高度 + 1 node?.height = max(height(node: node?.left), height(node: node?.right)) + 1 } ``` @@ -309,32 +309,32 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit === "Zig" ```zig title="avl_tree.zig" - // 获取结点高度 + // 获取节点高度 fn height(self: *Self, node: ?*inc.TreeNode(T)) i32 { _ = self; - // 空结点高度为 -1 ,叶结点高度为 0 + // 空节点高度为 -1 ,叶节点高度为 0 return if (node == null) -1 else node.?.height; } - // 更新结点高度 + // 更新节点高度 fn updateHeight(self: *Self, node: ?*inc.TreeNode(T)) void { - // 结点高度等于最高子树高度 + 1 + // 节点高度等于最高子树高度 + 1 node.?.height = std.math.max(self.height(node.?.left), self.height(node.?.right)) + 1; } ``` -### 结点平衡因子 +### 节点平衡因子 -结点的「平衡因子 Balance Factor」是 **结点的左子树高度减去右子树高度**,并定义空结点的平衡因子为 0 。同样地,我们将获取结点平衡因子封装成函数,以便后续使用。 +节点的「平衡因子 Balance Factor」是 **节点的左子树高度减去右子树高度**,并定义空节点的平衡因子为 0 。同样地,我们将获取节点平衡因子封装成函数,以便后续使用。 === "Java" ```java title="avl_tree.java" /* 获取平衡因子 */ int balanceFactor(TreeNode node) { - // 空结点平衡因子为 0 + // 空节点平衡因子为 0 if (node == null) return 0; - // 结点平衡因子 = 左子树高度 - 右子树高度 + // 节点平衡因子 = 左子树高度 - 右子树高度 return height(node.left) - height(node.right); } ``` @@ -344,9 +344,9 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit ```cpp title="avl_tree.cpp" /* 获取平衡因子 */ int balanceFactor(TreeNode* node) { - // 空结点平衡因子为 0 + // 空节点平衡因子为 0 if (node == nullptr) return 0; - // 结点平衡因子 = 左子树高度 - 右子树高度 + // 节点平衡因子 = 左子树高度 - 右子树高度 return height(node->left) - height(node->right); } ``` @@ -356,10 +356,10 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit ```python title="avl_tree.py" def balance_factor(self, node: TreeNode | None) -> int: """ 获取平衡因子 """ - # 空结点平衡因子为 0 + # 空节点平衡因子为 0 if node is None: return 0 - # 结点平衡因子 = 左子树高度 - 右子树高度 + # 节点平衡因子 = 左子树高度 - 右子树高度 return self.height(node.left) - self.height(node.right) ``` @@ -368,11 +368,11 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit ```go title="avl_tree.go" /* 获取平衡因子 */ func (t *aVLTree) balanceFactor(node *TreeNode) int { - // 空结点平衡因子为 0 + // 空节点平衡因子为 0 if node == nil { return 0 } - // 结点平衡因子 = 左子树高度 - 右子树高度 + // 节点平衡因子 = 左子树高度 - 右子树高度 return t.height(node.Left) - t.height(node.Right) } ``` @@ -382,9 +382,9 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit ```javascript title="avl_tree.js" /* 获取平衡因子 */ balanceFactor(node) { - // 空结点平衡因子为 0 + // 空节点平衡因子为 0 if (node === null) return 0; - // 结点平衡因子 = 左子树高度 - 右子树高度 + // 节点平衡因子 = 左子树高度 - 右子树高度 return this.height(node.left) - this.height(node.right); } ``` @@ -394,9 +394,9 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit ```typescript title="avl_tree.ts" /* 获取平衡因子 */ balanceFactor(node: TreeNode): number { - // 空结点平衡因子为 0 + // 空节点平衡因子为 0 if (node === null) return 0; - // 结点平衡因子 = 左子树高度 - 右子树高度 + // 节点平衡因子 = 左子树高度 - 右子树高度 return this.height(node.left) - this.height(node.right); } ``` @@ -413,9 +413,9 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit /* 获取平衡因子 */ int balanceFactor(TreeNode? node) { - // 空结点平衡因子为 0 + // 空节点平衡因子为 0 if (node == null) return 0; - // 结点平衡因子 = 左子树高度 - 右子树高度 + // 节点平衡因子 = 左子树高度 - 右子树高度 return height(node.left) - height(node.right); } ``` @@ -425,9 +425,9 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit ```swift title="avl_tree.swift" /* 获取平衡因子 */ func balanceFactor(node: TreeNode?) -> Int { - // 空结点平衡因子为 0 + // 空节点平衡因子为 0 guard let node = node else { return 0 } - // 结点平衡因子 = 左子树高度 - 右子树高度 + // 节点平衡因子 = 左子树高度 - 右子树高度 return height(node: node.left) - height(node: node.right) } ``` @@ -437,26 +437,26 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit ```zig title="avl_tree.zig" // 获取平衡因子 fn balanceFactor(self: *Self, node: ?*inc.TreeNode(T)) i32 { - // 空结点平衡因子为 0 + // 空节点平衡因子为 0 if (node == null) return 0; - // 结点平衡因子 = 左子树高度 - 右子树高度 + // 节点平衡因子 = 左子树高度 - 右子树高度 return self.height(node.?.left) - self.height(node.?.right); } ``` !!! note - 设平衡因子为 $f$ ,则一棵 AVL 树的任意结点的平衡因子皆满足 $-1 \le f \le 1$ 。 + 设平衡因子为 $f$ ,则一棵 AVL 树的任意节点的平衡因子皆满足 $-1 \le f \le 1$ 。 ## 7.4.2.   AVL 树旋转 -AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影响二叉树中序遍历序列的前提下,使失衡结点重新恢复平衡**。换言之,旋转操作既可以使树保持为「二叉搜索树」,也可以使树重新恢复为「平衡二叉树」。 +AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影响二叉树中序遍历序列的前提下,使失衡节点重新恢复平衡**。换言之,旋转操作既可以使树保持为「二叉搜索树」,也可以使树重新恢复为「平衡二叉树」。 -我们将平衡因子的绝对值 $> 1$ 的结点称为「失衡结点」。根据结点的失衡情况,旋转操作分为 **右旋、左旋、先右旋后左旋、先左旋后右旋**,接下来我们来一起来看看它们是如何操作的。 +我们将平衡因子的绝对值 $> 1$ 的节点称为「失衡节点」。根据节点的失衡情况,旋转操作分为 **右旋、左旋、先右旋后左旋、先左旋后右旋**,接下来我们来一起来看看它们是如何操作的。 ### Case 1 - 右旋 -如下图所示(结点下方为「平衡因子」),从底至顶看,二叉树中首个失衡结点是 **结点 3**。我们聚焦在以该失衡结点为根结点的子树上,将该结点记为 `node` ,将其左子结点记为 `child` ,执行「右旋」操作。完成右旋后,该子树已经恢复平衡,并且仍然为二叉搜索树。 +如下图所示(节点下方为「平衡因子」),从底至顶看,二叉树中首个失衡节点是 **节点 3**。我们聚焦在以该失衡节点为根节点的子树上,将该节点记为 `node` ,将其左子节点记为 `child` ,执行「右旋」操作。完成右旋后,该子树已经恢复平衡,并且仍然为二叉搜索树。 === "<1>" ![右旋操作步骤](avl_tree.assets/avltree_right_rotate_step1.png) @@ -470,13 +470,13 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "<4>" ![avltree_right_rotate_step4](avl_tree.assets/avltree_right_rotate_step4.png) -进而,如果结点 `child` 本身有右子结点(记为 `grandChild` ),则需要在「右旋」中添加一步:将 `grandChild` 作为 `node` 的左子结点。 +进而,如果节点 `child` 本身有右子节点(记为 `grandChild` ),则需要在「右旋」中添加一步:将 `grandChild` 作为 `node` 的左子节点。 ![有 grandChild 的右旋操作](avl_tree.assets/avltree_right_rotate_with_grandchild.png)

Fig. 有 grandChild 的右旋操作

-“向右旋转”是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。 +“向右旋转”是一种形象化的说法,实际需要通过修改节点指针实现,代码如下所示。 === "Java" @@ -488,10 +488,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 // 以 child 为原点,将 node 向右旋转 child.right = node; node.left = grandChild; - // 更新结点高度 + // 更新节点高度 updateHeight(node); updateHeight(child); - // 返回旋转后子树的根结点 + // 返回旋转后子树的根节点 return child; } ``` @@ -506,10 +506,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 // 以 child 为原点,将 node 向右旋转 child->right = node; node->left = grandChild; - // 更新结点高度 + // 更新节点高度 updateHeight(node); updateHeight(child); - // 返回旋转后子树的根结点 + // 返回旋转后子树的根节点 return child; } ``` @@ -524,10 +524,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 # 以 child 为原点,将 node 向右旋转 child.right = node node.left = grand_child - # 更新结点高度 + # 更新节点高度 self.__update_height(node) self.__update_height(child) - # 返回旋转后子树的根结点 + # 返回旋转后子树的根节点 return child ``` @@ -541,10 +541,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 // 以 child 为原点,将 node 向右旋转 child.Right = node node.Left = grandChild - // 更新结点高度 + // 更新节点高度 t.updateHeight(node) t.updateHeight(child) - // 返回旋转后子树的根结点 + // 返回旋转后子树的根节点 return child } ``` @@ -559,10 +559,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 // 以 child 为原点,将 node 向右旋转 child.right = node; node.left = grandChild; - // 更新结点高度 + // 更新节点高度 this.#updateHeight(node); this.#updateHeight(child); - // 返回旋转后子树的根结点 + // 返回旋转后子树的根节点 return child; } ``` @@ -577,10 +577,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 // 以 child 为原点,将 node 向右旋转 child.right = node; node.left = grandChild; - // 更新结点高度 + // 更新节点高度 this.updateHeight(node); this.updateHeight(child); - // 返回旋转后子树的根结点 + // 返回旋转后子树的根节点 return child; } ``` @@ -602,10 +602,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 // 以 child 为原点,将 node 向右旋转 child.right = node; node.left = grandChild; - // 更新结点高度 + // 更新节点高度 updateHeight(node); updateHeight(child); - // 返回旋转后子树的根结点 + // 返回旋转后子树的根节点 return child; } ``` @@ -620,10 +620,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 // 以 child 为原点,将 node 向右旋转 child?.right = node node?.left = grandChild - // 更新结点高度 + // 更新节点高度 updateHeight(node: node) updateHeight(node: child) - // 返回旋转后子树的根结点 + // 返回旋转后子树的根节点 return child } ``` @@ -638,10 +638,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 // 以 child 为原点,将 node 向右旋转 child.?.right = node; node.?.left = grandChild; - // 更新结点高度 + // 更新节点高度 self.updateHeight(node); self.updateHeight(child); - // 返回旋转后子树的根结点 + // 返回旋转后子树的根节点 return child; } ``` @@ -654,7 +654,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影

Fig. 左旋操作

-同理,若结点 `child` 本身有左子结点(记为 `grandChild` ),则需要在「左旋」中添加一步:将 `grandChild` 作为 `node` 的右子结点。 +同理,若节点 `child` 本身有左子节点(记为 `grandChild` ),则需要在「左旋」中添加一步:将 `grandChild` 作为 `node` 的右子节点。 ![有 grandChild 的左旋操作](avl_tree.assets/avltree_left_rotate_with_grandchild.png) @@ -672,10 +672,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 // 以 child 为原点,将 node 向左旋转 child.left = node; node.right = grandChild; - // 更新结点高度 + // 更新节点高度 updateHeight(node); updateHeight(child); - // 返回旋转后子树的根结点 + // 返回旋转后子树的根节点 return child; } ``` @@ -690,10 +690,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 // 以 child 为原点,将 node 向左旋转 child->left = node; node->right = grandChild; - // 更新结点高度 + // 更新节点高度 updateHeight(node); updateHeight(child); - // 返回旋转后子树的根结点 + // 返回旋转后子树的根节点 return child; } ``` @@ -708,10 +708,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 # 以 child 为原点,将 node 向左旋转 child.left = node node.right = grand_child - # 更新结点高度 + # 更新节点高度 self.__update_height(node) self.__update_height(child) - # 返回旋转后子树的根结点 + # 返回旋转后子树的根节点 return child ``` @@ -725,10 +725,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 // 以 child 为原点,将 node 向左旋转 child.Left = node node.Right = grandChild - // 更新结点高度 + // 更新节点高度 t.updateHeight(node) t.updateHeight(child) - // 返回旋转后子树的根结点 + // 返回旋转后子树的根节点 return child } ``` @@ -743,10 +743,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 // 以 child 为原点,将 node 向左旋转 child.left = node; node.right = grandChild; - // 更新结点高度 + // 更新节点高度 this.#updateHeight(node); this.#updateHeight(child); - // 返回旋转后子树的根结点 + // 返回旋转后子树的根节点 return child; } ``` @@ -761,10 +761,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 // 以 child 为原点,将 node 向左旋转 child.left = node; node.right = grandChild; - // 更新结点高度 + // 更新节点高度 this.updateHeight(node); this.updateHeight(child); - // 返回旋转后子树的根结点 + // 返回旋转后子树的根节点 return child; } ``` @@ -786,10 +786,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 // 以 child 为原点,将 node 向左旋转 child.left = node; node.right = grandChild; - // 更新结点高度 + // 更新节点高度 updateHeight(node); updateHeight(child); - // 返回旋转后子树的根结点 + // 返回旋转后子树的根节点 return child; } ``` @@ -804,10 +804,10 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 // 以 child 为原点,将 node 向左旋转 child?.left = node node?.right = grandChild - // 更新结点高度 + // 更新节点高度 updateHeight(node: node) updateHeight(node: child) - // 返回旋转后子树的根结点 + // 返回旋转后子树的根节点 return child } ``` @@ -822,17 +822,17 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 // 以 child 为原点,将 node 向左旋转 child.?.left = node; node.?.right = grandChild; - // 更新结点高度 + // 更新节点高度 self.updateHeight(node); self.updateHeight(child); - // 返回旋转后子树的根结点 + // 返回旋转后子树的根节点 return child; } ``` ### Case 3 - 先左后右 -对于下图的失衡结点 3 ,**单一使用左旋或右旋都无法使子树恢复平衡**,此时需要「先左旋后右旋」,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。 +对于下图的失衡节点 3 ,**单一使用左旋或右旋都无法使子树恢复平衡**,此时需要「先左旋后右旋」,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。 ![先左旋后右旋](avl_tree.assets/avltree_left_right_rotate.png) @@ -854,11 +854,11 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影

Fig. AVL 树的四种旋转情况

-具体地,在代码中使用 **失衡结点的平衡因子、较高一侧子结点的平衡因子** 来确定失衡结点属于上图中的哪种情况。 +具体地,在代码中使用 **失衡节点的平衡因子、较高一侧子节点的平衡因子** 来确定失衡节点属于上图中的哪种情况。
-| 失衡结点的平衡因子 | 子结点的平衡因子 | 应采用的旋转方法 | +| 失衡节点的平衡因子 | 子节点的平衡因子 | 应采用的旋转方法 | | ------------------ | ---------------- | ---------------- | | $>0$ (即左偏树) | $\geq 0$ | 右旋 | | $>0$ (即左偏树) | $<0$ | 先左旋后右旋 | @@ -867,14 +867,14 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
-为方便使用,我们将旋转操作封装成一个函数。至此,**我们可以使用此函数来旋转各种失衡情况,使失衡结点重新恢复平衡**。 +为方便使用,我们将旋转操作封装成一个函数。至此,**我们可以使用此函数来旋转各种失衡情况,使失衡节点重新恢复平衡**。 === "Java" ```java title="avl_tree.java" /* 执行旋转操作,使该子树重新恢复平衡 */ TreeNode rotate(TreeNode node) { - // 获取结点 node 的平衡因子 + // 获取节点 node 的平衡因子 int balanceFactor = balanceFactor(node); // 左偏树 if (balanceFactor > 1) { @@ -908,7 +908,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 ```cpp title="avl_tree.cpp" /* 执行旋转操作,使该子树重新恢复平衡 */ TreeNode* rotate(TreeNode* node) { - // 获取结点 node 的平衡因子 + // 获取节点 node 的平衡因子 int _balanceFactor = balanceFactor(node); // 左偏树 if (_balanceFactor > 1) { @@ -942,7 +942,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 ```python title="avl_tree.py" def __rotate(self, node: TreeNode | None) -> TreeNode | None: """ 执行旋转操作,使该子树重新恢复平衡 """ - # 获取结点 node 的平衡因子 + # 获取节点 node 的平衡因子 balance_factor = self.balance_factor(node) # 左偏树 if balance_factor > 1: @@ -971,7 +971,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 ```go title="avl_tree.go" /* 执行旋转操作,使该子树重新恢复平衡 */ func (t *aVLTree) rotate(node *TreeNode) *TreeNode { - // 获取结点 node 的平衡因子 + // 获取节点 node 的平衡因子 // Go 推荐短变量,这里 bf 指代 t.balanceFactor bf := t.balanceFactor(node) // 左偏树 @@ -1006,7 +1006,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 ```javascript title="avl_tree.js" /* 执行旋转操作,使该子树重新恢复平衡 */ #rotate(node) { - // 获取结点 node 的平衡因子 + // 获取节点 node 的平衡因子 const balanceFactor = this.balanceFactor(node); // 左偏树 if (balanceFactor > 1) { @@ -1040,7 +1040,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 ```typescript title="avl_tree.ts" /* 执行旋转操作,使该子树重新恢复平衡 */ rotate(node: TreeNode): TreeNode { - // 获取结点 node 的平衡因子 + // 获取节点 node 的平衡因子 const balanceFactor = this.balanceFactor(node); // 左偏树 if (balanceFactor > 1) { @@ -1081,7 +1081,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 /* 执行旋转操作,使该子树重新恢复平衡 */ TreeNode? rotate(TreeNode? node) { - // 获取结点 node 的平衡因子 + // 获取节点 node 的平衡因子 int balanceFactorInt = balanceFactor(node); // 左偏树 if (balanceFactorInt > 1) @@ -1123,7 +1123,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 ```swift title="avl_tree.swift" /* 执行旋转操作,使该子树重新恢复平衡 */ func rotate(node: TreeNode?) -> TreeNode? { - // 获取结点 node 的平衡因子 + // 获取节点 node 的平衡因子 let balanceFactor = balanceFactor(node: node) // 左偏树 if balanceFactor > 1 { @@ -1157,7 +1157,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 ```zig title="avl_tree.zig" // 执行旋转操作,使该子树重新恢复平衡 fn rotate(self: *Self, node: ?*inc.TreeNode(T)) ?*inc.TreeNode(T) { - // 获取结点 node 的平衡因子 + // 获取节点 node 的平衡因子 var balance_factor = self.balanceFactor(node); // 左偏树 if (balance_factor > 1) { @@ -1188,33 +1188,33 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 ## 7.4.3.   AVL 树常用操作 -### 插入结点 +### 插入节点 -「AVL 树」的结点插入操作与「二叉搜索树」主体类似。不同的是,在插入结点后,从该结点到根结点的路径上会出现一系列「失衡结点」。所以,**我们需要从该结点开始,从底至顶地执行旋转操作,使所有失衡结点恢复平衡**。 +「AVL 树」的节点插入操作与「二叉搜索树」主体类似。不同的是,在插入节点后,从该节点到根节点的路径上会出现一系列「失衡节点」。所以,**我们需要从该节点开始,从底至顶地执行旋转操作,使所有失衡节点恢复平衡**。 === "Java" ```java title="avl_tree.java" - /* 插入结点 */ + /* 插入节点 */ TreeNode insert(int val) { root = insertHelper(root, val); return root; } - /* 递归插入结点(辅助方法) */ + /* 递归插入节点(辅助方法) */ TreeNode insertHelper(TreeNode node, int val) { if (node == null) return new TreeNode(val); - /* 1. 查找插入位置,并插入结点 */ + /* 1. 查找插入位置,并插入节点 */ if (val < node.val) node.left = insertHelper(node.left, val); else if (val > node.val) node.right = insertHelper(node.right, val); else - return node; // 重复结点不插入,直接返回 - updateHeight(node); // 更新结点高度 + return node; // 重复节点不插入,直接返回 + updateHeight(node); // 更新节点高度 /* 2. 执行旋转操作,使该子树重新恢复平衡 */ node = rotate(node); - // 返回子树的根结点 + // 返回子树的根节点 return node; } ``` @@ -1222,27 +1222,27 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "C++" ```cpp title="avl_tree.cpp" - /* 插入结点 */ + /* 插入节点 */ TreeNode* insert(int val) { root = insertHelper(root, val); return root; } - /* 递归插入结点(辅助方法) */ + /* 递归插入节点(辅助方法) */ TreeNode* insertHelper(TreeNode* node, int val) { if (node == nullptr) return new TreeNode(val); - /* 1. 查找插入位置,并插入结点 */ + /* 1. 查找插入位置,并插入节点 */ if (val < node->val) node->left = insertHelper(node->left, val); else if (val > node->val) node->right = insertHelper(node->right, val); else - return node; // 重复结点不插入,直接返回 - updateHeight(node); // 更新结点高度 + return node; // 重复节点不插入,直接返回 + updateHeight(node); // 更新节点高度 /* 2. 执行旋转操作,使该子树重新恢复平衡 */ node = rotate(node); - // 返回子树的根结点 + // 返回子树的根节点 return node; } ``` @@ -1251,23 +1251,23 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 ```python title="avl_tree.py" def insert(self, val) -> TreeNode: - """ 插入结点 """ + """ 插入节点 """ self.__root = self.__insert_helper(self.__root, val) return self.__root def __insert_helper(self, node: TreeNode | None, val: int) -> TreeNode: - """ 递归插入结点(辅助方法)""" + """ 递归插入节点(辅助方法)""" if node is None: return TreeNode(val) - # 1. 查找插入位置,并插入结点 + # 1. 查找插入位置,并插入节点 if val < node.val: node.left = self.__insert_helper(node.left, val) elif val > node.val: node.right = self.__insert_helper(node.right, val) else: - # 重复结点不插入,直接返回 + # 重复节点不插入,直接返回 return node - # 更新结点高度 + # 更新节点高度 self.__update_height(node) # 2. 执行旋转操作,使该子树重新恢复平衡 return self.__rotate(node) @@ -1276,31 +1276,31 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "Go" ```go title="avl_tree.go" - /* 插入结点 */ + /* 插入节点 */ func (t *aVLTree) insert(val int) *TreeNode { t.root = t.insertHelper(t.root, val) return t.root } - /* 递归插入结点(辅助方法) */ + /* 递归插入节点(辅助方法) */ func (t *aVLTree) insertHelper(node *TreeNode, val int) *TreeNode { if node == nil { return NewTreeNode(val) } - /* 1. 查找插入位置,并插入结点 */ + /* 1. 查找插入位置,并插入节点 */ if val < node.Val { node.Left = t.insertHelper(node.Left, val) } else if val > node.Val { node.Right = t.insertHelper(node.Right, val) } else { - // 重复结点不插入,直接返回 + // 重复节点不插入,直接返回 return node } - // 更新结点高度 + // 更新节点高度 t.updateHeight(node) /* 2. 执行旋转操作,使该子树重新恢复平衡 */ node = t.rotate(node) - // 返回子树的根结点 + // 返回子树的根节点 return node } ``` @@ -1308,23 +1308,23 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "JavaScript" ```javascript title="avl_tree.js" - /* 插入结点 */ + /* 插入节点 */ insert(val) { this.root = this.#insertHelper(this.root, val); return this.root; } - /* 递归插入结点(辅助方法) */ + /* 递归插入节点(辅助方法) */ #insertHelper(node, val) { if (node === null) return new TreeNode(val); - /* 1. 查找插入位置,并插入结点 */ + /* 1. 查找插入位置,并插入节点 */ if (val < node.val) node.left = this.#insertHelper(node.left, val); else if (val > node.val) node.right = this.#insertHelper(node.right, val); - else return node; // 重复结点不插入,直接返回 - this.#updateHeight(node); // 更新结点高度 + else return node; // 重复节点不插入,直接返回 + this.#updateHeight(node); // 更新节点高度 /* 2. 执行旋转操作,使该子树重新恢复平衡 */ node = this.#rotate(node); - // 返回子树的根结点 + // 返回子树的根节点 return node; } ``` @@ -1332,27 +1332,27 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "TypeScript" ```typescript title="avl_tree.ts" - /* 插入结点 */ + /* 插入节点 */ insert(val: number): TreeNode { this.root = this.insertHelper(this.root, val); return this.root; } - /* 递归插入结点(辅助方法) */ + /* 递归插入节点(辅助方法) */ insertHelper(node: TreeNode, val: number): TreeNode { if (node === null) return new TreeNode(val); - /* 1. 查找插入位置,并插入结点 */ + /* 1. 查找插入位置,并插入节点 */ if (val < node.val) { node.left = this.insertHelper(node.left, val); } else if (val > node.val) { node.right = this.insertHelper(node.right, val); } else { - return node; // 重复结点不插入,直接返回 + return node; // 重复节点不插入,直接返回 } - this.updateHeight(node); // 更新结点高度 + this.updateHeight(node); // 更新节点高度 /* 2. 执行旋转操作,使该子树重新恢复平衡 */ node = this.rotate(node); - // 返回子树的根结点 + // 返回子树的根节点 return node; } ``` @@ -1368,28 +1368,28 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "C#" ```csharp title="avl_tree.cs" - /* 插入结点 */ + /* 插入节点 */ TreeNode? insert(int val) { root = insertHelper(root, val); return root; } - /* 递归插入结点(辅助方法) */ + /* 递归插入节点(辅助方法) */ TreeNode? insertHelper(TreeNode? node, int val) { if (node == null) return new TreeNode(val); - /* 1. 查找插入位置,并插入结点 */ + /* 1. 查找插入位置,并插入节点 */ if (val < node.val) node.left = insertHelper(node.left, val); else if (val > node.val) node.right = insertHelper(node.right, val); else - return node; // 重复结点不插入,直接返回 - updateHeight(node); // 更新结点高度 + return node; // 重复节点不插入,直接返回 + updateHeight(node); // 更新节点高度 /* 2. 执行旋转操作,使该子树重新恢复平衡 */ node = rotate(node); - // 返回子树的根结点 + // 返回子树的根节点 return node; } ``` @@ -1397,31 +1397,31 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "Swift" ```swift title="avl_tree.swift" - /* 插入结点 */ + /* 插入节点 */ @discardableResult func insert(val: Int) -> TreeNode? { root = insertHelper(node: root, val: val) return root } - /* 递归插入结点(辅助方法) */ + /* 递归插入节点(辅助方法) */ func insertHelper(node: TreeNode?, val: Int) -> TreeNode? { var node = node if node == nil { return TreeNode(x: val) } - /* 1. 查找插入位置,并插入结点 */ + /* 1. 查找插入位置,并插入节点 */ if val < node!.val { node?.left = insertHelper(node: node?.left, val: val) } else if val > node!.val { node?.right = insertHelper(node: node?.right, val: val) } else { - return node // 重复结点不插入,直接返回 + return node // 重复节点不插入,直接返回 } - updateHeight(node: node) // 更新结点高度 + updateHeight(node: node) // 更新节点高度 /* 2. 执行旋转操作,使该子树重新恢复平衡 */ node = rotate(node: node) - // 返回子树的根结点 + // 返回子树的根节点 return node } ``` @@ -1429,13 +1429,13 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "Zig" ```zig title="avl_tree.zig" - // 插入结点 + // 插入节点 fn insert(self: *Self, val: T) !?*inc.TreeNode(T) { self.root = try self.insertHelper(self.root, val); return self.root; } - // 递归插入结点(辅助方法) + // 递归插入节点(辅助方法) fn insertHelper(self: *Self, node_: ?*inc.TreeNode(T), val: T) !?*inc.TreeNode(T) { var node = node_; if (node == null) { @@ -1443,39 +1443,39 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 tmp_node.init(val); return tmp_node; } - // 1. 查找插入位置,并插入结点 + // 1. 查找插入位置,并插入节点 if (val < node.?.val) { node.?.left = try self.insertHelper(node.?.left, val); } else if (val > node.?.val) { node.?.right = try self.insertHelper(node.?.right, val); } else { - return node; // 重复结点不插入,直接返回 + return node; // 重复节点不插入,直接返回 } - self.updateHeight(node); // 更新结点高度 + self.updateHeight(node); // 更新节点高度 // 2. 执行旋转操作,使该子树重新恢复平衡 node = self.rotate(node); - // 返回子树的根结点 + // 返回子树的根节点 return node; } ``` -### 删除结点 +### 删除节点 -「AVL 树」删除结点操作与「二叉搜索树」删除结点操作总体相同。类似地,**在删除结点后,也需要从底至顶地执行旋转操作,使所有失衡结点恢复平衡**。 +「AVL 树」删除节点操作与「二叉搜索树」删除节点操作总体相同。类似地,**在删除节点后,也需要从底至顶地执行旋转操作,使所有失衡节点恢复平衡**。 === "Java" ```java title="avl_tree.java" - /* 删除结点 */ + /* 删除节点 */ TreeNode remove(int val) { root = removeHelper(root, val); return root; } - /* 递归删除结点(辅助方法) */ + /* 递归删除节点(辅助方法) */ TreeNode removeHelper(TreeNode node, int val) { if (node == null) return null; - /* 1. 查找结点,并删除之 */ + /* 1. 查找节点,并删除之 */ if (val < node.val) node.left = removeHelper(node.left, val); else if (val > node.val) @@ -1483,30 +1483,30 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 else { if (node.left == null || node.right == null) { TreeNode child = node.left != null ? node.left : node.right; - // 子结点数量 = 0 ,直接删除 node 并返回 + // 子节点数量 = 0 ,直接删除 node 并返回 if (child == null) return null; - // 子结点数量 = 1 ,直接删除 node + // 子节点数量 = 1 ,直接删除 node else node = child; } else { - // 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点 TreeNode temp = getInOrderNext(node.right); node.right = removeHelper(node.right, temp.val); node.val = temp.val; } } - updateHeight(node); // 更新结点高度 + updateHeight(node); // 更新节点高度 /* 2. 执行旋转操作,使该子树重新恢复平衡 */ node = rotate(node); - // 返回子树的根结点 + // 返回子树的根节点 return node; } - /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + /* 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) */ TreeNode getInOrderNext(TreeNode node) { if (node == null) return node; - // 循环访问左子结点,直到叶结点时为最小结点,跳出 + // 循环访问左子节点,直到叶节点时为最小节点,跳出 while (node.left != null) { node = node.left; } @@ -1517,17 +1517,17 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "C++" ```cpp title="avl_tree.cpp" - /* 删除结点 */ + /* 删除节点 */ TreeNode* remove(int val) { root = removeHelper(root, val); return root; } - /* 递归删除结点(辅助方法) */ + /* 递归删除节点(辅助方法) */ TreeNode* removeHelper(TreeNode* node, int val) { if (node == nullptr) return nullptr; - /* 1. 查找结点,并删除之 */ + /* 1. 查找节点,并删除之 */ if (val < node->val) node->left = removeHelper(node->left, val); else if (val > node->val) @@ -1535,36 +1535,36 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 else { if (node->left == nullptr || node->right == nullptr) { TreeNode* child = node->left != nullptr ? node->left : node->right; - // 子结点数量 = 0 ,直接删除 node 并返回 + // 子节点数量 = 0 ,直接删除 node 并返回 if (child == nullptr) { delete node; return nullptr; } - // 子结点数量 = 1 ,直接删除 node + // 子节点数量 = 1 ,直接删除 node else { delete node; node = child; } } else { - // 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点 TreeNode* temp = getInOrderNext(node->right); int tempVal = temp->val; node->right = removeHelper(node->right, temp->val); node->val = tempVal; } } - updateHeight(node); // 更新结点高度 + updateHeight(node); // 更新节点高度 /* 2. 执行旋转操作,使该子树重新恢复平衡 */ node = rotate(node); - // 返回子树的根结点 + // 返回子树的根节点 return node; } - /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + /* 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) */ TreeNode* getInOrderNext(TreeNode* node) { if (node == nullptr) return node; - // 循环访问左子结点,直到叶结点时为最小结点,跳出 + // 循环访问左子节点,直到叶节点时为最小节点,跳出 while (node->left != nullptr) { node = node->left; } @@ -1576,15 +1576,15 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 ```python title="avl_tree.py" def remove(self, val: int) -> TreeNode | None: - """ 删除结点 """ + """ 删除节点 """ self.__root = self.__remove_helper(self.__root, val) return self.__root def __remove_helper(self, node: TreeNode | None, val: int) -> TreeNode | None: - """ 递归删除结点(辅助方法) """ + """ 递归删除节点(辅助方法) """ if node is None: return None - # 1. 查找结点,并删除之 + # 1. 查找节点,并删除之 if val < node.val: node.left = self.__remove_helper(node.left, val) elif val > node.val: @@ -1592,26 +1592,26 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 else: if node.left is None or node.right is None: child = node.left or node.right - # 子结点数量 = 0 ,直接删除 node 并返回 + # 子节点数量 = 0 ,直接删除 node 并返回 if child is None: return None - # 子结点数量 = 1 ,直接删除 node + # 子节点数量 = 1 ,直接删除 node else: node = child - else: # 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + else: # 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点 temp = self.__get_inorder_next(node.right) node.right = self.__remove_helper(node.right, temp.val) node.val = temp.val - # 更新结点高度 + # 更新节点高度 self.__update_height(node) # 2. 执行旋转操作,使该子树重新恢复平衡 return self.__rotate(node) def __get_inorder_next(self, node: TreeNode | None) -> TreeNode | None: - """ 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) """ + """ 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) """ if node is None: return None - # 循环访问左子结点,直到叶结点时为最小结点,跳出 + # 循环访问左子节点,直到叶节点时为最小节点,跳出 while node.left is not None: node = node.left return node @@ -1620,18 +1620,18 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "Go" ```go title="avl_tree.go" - /* 删除结点 */ + /* 删除节点 */ func (t *aVLTree) remove(val int) *TreeNode { root := t.removeHelper(t.root, val) return root } - /* 递归删除结点(辅助方法) */ + /* 递归删除节点(辅助方法) */ func (t *aVLTree) removeHelper(node *TreeNode, val int) *TreeNode { if node == nil { return nil } - /* 1. 查找结点,并删除之 */ + /* 1. 查找节点,并删除之 */ if val < node.Val { node.Left = t.removeHelper(node.Left, val) } else if val > node.Val { @@ -1642,34 +1642,34 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 if node.Right != nil { child = node.Right } - // 子结点数量 = 0 ,直接删除 node 并返回 + // 子节点数量 = 0 ,直接删除 node 并返回 if child == nil { return nil } else { - // 子结点数量 = 1 ,直接删除 node + // 子节点数量 = 1 ,直接删除 node node = child } } else { - // 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点 temp := t.getInOrderNext(node.Right) node.Right = t.removeHelper(node.Right, temp.Val) node.Val = temp.Val } } - // 更新结点高度 + // 更新节点高度 t.updateHeight(node) /* 2. 执行旋转操作,使该子树重新恢复平衡 */ node = t.rotate(node) - // 返回子树的根结点 + // 返回子树的根节点 return node } - /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + /* 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) */ func (t *aVLTree) getInOrderNext(node *TreeNode) *TreeNode { if node == nil { return node } - // 循环访问左子结点,直到叶结点时为最小结点,跳出 + // 循环访问左子节点,直到叶节点时为最小节点,跳出 for node.Left != nil { node = node.Left } @@ -1680,43 +1680,43 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "JavaScript" ```javascript title="avl_tree.js" - /* 删除结点 */ + /* 删除节点 */ remove(val) { this.root = this.#removeHelper(this.root, val); return this.root; } - /* 递归删除结点(辅助方法) */ + /* 递归删除节点(辅助方法) */ #removeHelper(node, val) { if (node === null) return null; - /* 1. 查找结点,并删除之 */ + /* 1. 查找节点,并删除之 */ if (val < node.val) node.left = this.#removeHelper(node.left, val); else if (val > node.val) node.right = this.#removeHelper(node.right, val); else { if (node.left === null || node.right === null) { const child = node.left !== null ? node.left : node.right; - // 子结点数量 = 0 ,直接删除 node 并返回 + // 子节点数量 = 0 ,直接删除 node 并返回 if (child === null) return null; - // 子结点数量 = 1 ,直接删除 node + // 子节点数量 = 1 ,直接删除 node else node = child; } else { - // 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点 const temp = this.#getInOrderNext(node.right); node.right = this.#removeHelper(node.right, temp.val); node.val = temp.val; } } - this.#updateHeight(node); // 更新结点高度 + this.#updateHeight(node); // 更新节点高度 /* 2. 执行旋转操作,使该子树重新恢复平衡 */ node = this.#rotate(node); - // 返回子树的根结点 + // 返回子树的根节点 return node; } - /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + /* 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) */ #getInOrderNext(node) { if (node === null) return node; - // 循环访问左子结点,直到叶结点时为最小结点,跳出 + // 循环访问左子节点,直到叶节点时为最小节点,跳出 while (node.left !== null) { node = node.left; } @@ -1727,16 +1727,16 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "TypeScript" ```typescript title="avl_tree.ts" - /* 删除结点 */ + /* 删除节点 */ remove(val: number): TreeNode { this.root = this.removeHelper(this.root, val); return this.root; } - /* 递归删除结点(辅助方法) */ + /* 递归删除节点(辅助方法) */ removeHelper(node: TreeNode, val: number): TreeNode { if (node === null) return null; - /* 1. 查找结点,并删除之 */ + /* 1. 查找节点,并删除之 */ if (val < node.val) { node.left = this.removeHelper(node.left, val); } else if (val > node.val) { @@ -1744,31 +1744,31 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 } else { if (node.left === null || node.right === null) { const child = node.left !== null ? node.left : node.right; - // 子结点数量 = 0 ,直接删除 node 并返回 + // 子节点数量 = 0 ,直接删除 node 并返回 if (child === null) { return null; } else { - // 子结点数量 = 1 ,直接删除 node + // 子节点数量 = 1 ,直接删除 node node = child; } } else { - // 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点 const temp = this.getInOrderNext(node.right); node.right = this.removeHelper(node.right, temp.val); node.val = temp.val; } } - this.updateHeight(node); // 更新结点高度 + this.updateHeight(node); // 更新节点高度 /* 2. 执行旋转操作,使该子树重新恢复平衡 */ node = this.rotate(node); - // 返回子树的根结点 + // 返回子树的根节点 return node; } - /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + /* 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) */ getInOrderNext(node: TreeNode): TreeNode { if (node === null) return node; - // 循环访问左子结点,直到叶结点时为最小结点,跳出 + // 循环访问左子节点,直到叶节点时为最小节点,跳出 while (node.left !== null) { node = node.left; } @@ -1789,18 +1789,18 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "C#" ```csharp title="avl_tree.cs" - /* 删除结点 */ + /* 删除节点 */ TreeNode? remove(int val) { root = removeHelper(root, val); return root; } - /* 递归删除结点(辅助方法) */ + /* 递归删除节点(辅助方法) */ TreeNode? removeHelper(TreeNode? node, int val) { if (node == null) return null; - /* 1. 查找结点,并删除之 */ + /* 1. 查找节点,并删除之 */ if (val < node.val) node.left = removeHelper(node.left, val); else if (val > node.val) @@ -1810,33 +1810,33 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 if (node.left == null || node.right == null) { TreeNode? child = node.left != null ? node.left : node.right; - // 子结点数量 = 0 ,直接删除 node 并返回 + // 子节点数量 = 0 ,直接删除 node 并返回 if (child == null) return null; - // 子结点数量 = 1 ,直接删除 node + // 子节点数量 = 1 ,直接删除 node else node = child; } else { - // 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点 TreeNode? temp = getInOrderNext(node.right); node.right = removeHelper(node.right, temp.val); node.val = temp.val; } } - updateHeight(node); // 更新结点高度 + updateHeight(node); // 更新节点高度 /* 2. 执行旋转操作,使该子树重新恢复平衡 */ node = rotate(node); - // 返回子树的根结点 + // 返回子树的根节点 return node; } - /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + /* 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) */ TreeNode? getInOrderNext(TreeNode? node) { if (node == null) return node; - // 循环访问左子结点,直到叶结点时为最小结点,跳出 + // 循环访问左子节点,直到叶节点时为最小节点,跳出 while (node.left != null) { node = node.left; @@ -1848,20 +1848,20 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "Swift" ```swift title="avl_tree.swift" - /* 删除结点 */ + /* 删除节点 */ @discardableResult func remove(val: Int) -> TreeNode? { root = removeHelper(node: root, val: val) return root } - /* 递归删除结点(辅助方法) */ + /* 递归删除节点(辅助方法) */ func removeHelper(node: TreeNode?, val: Int) -> TreeNode? { var node = node if node == nil { return nil } - /* 1. 查找结点,并删除之 */ + /* 1. 查找节点,并删除之 */ if val < node!.val { node?.left = removeHelper(node: node?.left, val: val) } else if val > node!.val { @@ -1869,35 +1869,35 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 } else { if node?.left == nil || node?.right == nil { let child = node?.left != nil ? node?.left : node?.right - // 子结点数量 = 0 ,直接删除 node 并返回 + // 子节点数量 = 0 ,直接删除 node 并返回 if child == nil { return nil } - // 子结点数量 = 1 ,直接删除 node + // 子节点数量 = 1 ,直接删除 node else { node = child } } else { - // 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点 let temp = getInOrderNext(node: node?.right) node?.right = removeHelper(node: node?.right, val: temp!.val) node?.val = temp!.val } } - updateHeight(node: node) // 更新结点高度 + updateHeight(node: node) // 更新节点高度 /* 2. 执行旋转操作,使该子树重新恢复平衡 */ node = rotate(node: node) - // 返回子树的根结点 + // 返回子树的根节点 return node } - /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + /* 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) */ func getInOrderNext(node: TreeNode?) -> TreeNode? { var node = node if node == nil { return node } - // 循环访问左子结点,直到叶结点时为最小结点,跳出 + // 循环访问左子节点,直到叶节点时为最小节点,跳出 while node?.left != nil { node = node?.left } @@ -1908,17 +1908,17 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 === "Zig" ```zig title="avl_tree.zig" - // 删除结点 + // 删除节点 fn remove(self: *Self, val: T) ?*inc.TreeNode(T) { self.root = self.removeHelper(self.root, val); return self.root; } - // 递归删除结点(辅助方法) + // 递归删除节点(辅助方法) fn removeHelper(self: *Self, node_: ?*inc.TreeNode(T), val: T) ?*inc.TreeNode(T) { var node = node_; if (node == null) return null; - // 1. 查找结点,并删除之 + // 1. 查找节点,并删除之 if (val < node.?.val) { node.?.left = self.removeHelper(node.?.left, val); } else if (val > node.?.val) { @@ -1926,33 +1926,33 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 } else { if (node.?.left == null or node.?.right == null) { var child = if (node.?.left != null) node.?.left else node.?.right; - // 子结点数量 = 0 ,直接删除 node 并返回 + // 子节点数量 = 0 ,直接删除 node 并返回 if (child == null) { return null; - // 子结点数量 = 1 ,直接删除 node + // 子节点数量 = 1 ,直接删除 node } else { node = child; } } else { - // 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点 var temp = self.getInOrderNext(node.?.right); node.?.right = self.removeHelper(node.?.right, temp.?.val); node.?.val = temp.?.val; } } - self.updateHeight(node); // 更新结点高度 + self.updateHeight(node); // 更新节点高度 // 2. 执行旋转操作,使该子树重新恢复平衡 node = self.rotate(node); - // 返回子树的根结点 + // 返回子树的根节点 return node; } - // 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) + // 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) fn getInOrderNext(self: *Self, node_: ?*inc.TreeNode(T)) ?*inc.TreeNode(T) { _ = self; var node = node_; if (node == null) return node; - // 循环访问左子结点,直到叶结点时为最小结点,跳出 + // 循环访问左子节点,直到叶节点时为最小节点,跳出 while (node.?.left != null) { node = node.?.left; } @@ -1960,9 +1960,9 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 } ``` -### 查找结点 +### 查找节点 -「AVL 树」的结点查找操作与「二叉搜索树」一致,在此不再赘述。 +「AVL 树」的节点查找操作与「二叉搜索树」一致,在此不再赘述。 ## 7.4.4.   AVL 树典型应用 @@ -1971,4 +1971,4 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 !!! question "为什么红黑树比 AVL 树更受欢迎?" - 红黑树的平衡条件相对宽松,因此在红黑树中插入与删除结点所需的旋转操作相对更少,结点增删操作相比 AVL 树的效率更高。 + 红黑树的平衡条件相对宽松,因此在红黑树中插入与删除节点所需的旋转操作相对更少,节点增删操作相比 AVL 树的效率更高。 diff --git a/chapter_tree/binary_search_tree.md b/chapter_tree/binary_search_tree.md index 22bb1a934..392ace1c2 100755 --- a/chapter_tree/binary_search_tree.md +++ b/chapter_tree/binary_search_tree.md @@ -6,8 +6,8 @@ comments: true 「二叉搜索树 Binary Search Tree」满足以下条件: -1. 对于根结点,左子树中所有结点的值 $<$ 根结点的值 $<$ 右子树中所有结点的值; -2. 任意结点的左子树和右子树也是二叉搜索树,即也满足条件 `1.` ; +1. 对于根节点,左子树中所有节点的值 $<$ 根节点的值 $<$ 右子树中所有节点的值; +2. 任意节点的左子树和右子树也是二叉搜索树,即也满足条件 `1.` ; ![二叉搜索树](binary_search_tree.assets/binary_search_tree.png) @@ -15,16 +15,16 @@ comments: true ## 7.3.1.   二叉搜索树的操作 -### 查找结点 +### 查找节点 -给定目标结点值 `num` ,可以根据二叉搜索树的性质来查找。我们声明一个结点 `cur` ,从二叉树的根结点 `root` 出发,循环比较结点值 `cur.val` 和 `num` 之间的大小关系 +给定目标节点值 `num` ,可以根据二叉搜索树的性质来查找。我们声明一个节点 `cur` ,从二叉树的根节点 `root` 出发,循环比较节点值 `cur.val` 和 `num` 之间的大小关系 -- 若 `cur.val < num` ,说明目标结点在 `cur` 的右子树中,因此执行 `cur = cur.right` ; -- 若 `cur.val > num` ,说明目标结点在 `cur` 的左子树中,因此执行 `cur = cur.left` ; -- 若 `cur.val = num` ,说明找到目标结点,跳出循环并返回该结点即可; +- 若 `cur.val < num` ,说明目标节点在 `cur` 的右子树中,因此执行 `cur = cur.right` ; +- 若 `cur.val > num` ,说明目标节点在 `cur` 的左子树中,因此执行 `cur = cur.left` ; +- 若 `cur.val = num` ,说明找到目标节点,跳出循环并返回该节点即可; === "<1>" - ![查找结点步骤](binary_search_tree.assets/bst_search_step1.png) + ![查找节点步骤](binary_search_tree.assets/bst_search_step1.png) === "<2>" ![bst_search_step2](binary_search_tree.assets/bst_search_step2.png) @@ -40,19 +40,19 @@ comments: true === "Java" ```java title="binary_search_tree.java" - /* 查找结点 */ + /* 查找节点 */ TreeNode search(int num) { TreeNode cur = root; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur != null) { - // 目标结点在 cur 的右子树中 + // 目标节点在 cur 的右子树中 if (cur.val < num) cur = cur.right; - // 目标结点在 cur 的左子树中 + // 目标节点在 cur 的左子树中 else if (cur.val > num) cur = cur.left; - // 找到目标结点,跳出循环 + // 找到目标节点,跳出循环 else break; } - // 返回目标结点 + // 返回目标节点 return cur; } ``` @@ -60,19 +60,19 @@ comments: true === "C++" ```cpp title="binary_search_tree.cpp" - /* 查找结点 */ + /* 查找节点 */ TreeNode* search(int num) { TreeNode* cur = root; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur != nullptr) { - // 目标结点在 cur 的右子树中 + // 目标节点在 cur 的右子树中 if (cur->val < num) cur = cur->right; - // 目标结点在 cur 的左子树中 + // 目标节点在 cur 的左子树中 else if (cur->val > num) cur = cur->left; - // 找到目标结点,跳出循环 + // 找到目标节点,跳出循环 else break; } - // 返回目标结点 + // 返回目标节点 return cur; } ``` @@ -81,17 +81,17 @@ comments: true ```python title="binary_search_tree.py" def search(self, num: int) -> TreeNode | None: - """ 查找结点 """ + """ 查找节点 """ cur: TreeNode | None = self.__root - # 循环查找,越过叶结点后跳出 + # 循环查找,越过叶节点后跳出 while cur is not None: - # 目标结点在 cur 的右子树中 + # 目标节点在 cur 的右子树中 if cur.val < num: cur = cur.right - # 目标结点在 cur 的左子树中 + # 目标节点在 cur 的左子树中 elif cur.val > num: cur = cur.left - # 找到目标结点,跳出循环 + # 找到目标节点,跳出循环 else: break return cur @@ -100,23 +100,23 @@ comments: true === "Go" ```go title="binary_search_tree.go" - /* 查找结点 */ + /* 查找节点 */ func (bst *binarySearchTree) search(num int) *TreeNode { node := bst.root - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 for node != nil { if node.Val < num { - // 目标结点在 cur 的右子树中 + // 目标节点在 cur 的右子树中 node = node.Right } else if node.Val > num { - // 目标结点在 cur 的左子树中 + // 目标节点在 cur 的左子树中 node = node.Left } else { - // 找到目标结点,跳出循环 + // 找到目标节点,跳出循环 break } } - // 返回目标结点 + // 返回目标节点 return node } ``` @@ -124,19 +124,19 @@ comments: true === "JavaScript" ```javascript title="binary_search_tree.js" - /* 查找结点 */ + /* 查找节点 */ function search(num) { let cur = root; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur !== null) { - // 目标结点在 cur 的右子树中 + // 目标节点在 cur 的右子树中 if (cur.val < num) cur = cur.right; - // 目标结点在 cur 的左子树中 + // 目标节点在 cur 的左子树中 else if (cur.val > num) cur = cur.left; - // 找到目标结点,跳出循环 + // 找到目标节点,跳出循环 else break; } - // 返回目标结点 + // 返回目标节点 return cur; } ``` @@ -144,20 +144,20 @@ comments: true === "TypeScript" ```typescript title="binary_search_tree.ts" - /* 查找结点 */ + /* 查找节点 */ function search(num: number): TreeNode | null { let cur = root; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur !== null) { if (cur.val < num) { - cur = cur.right; // 目标结点在 cur 的右子树中 + cur = cur.right; // 目标节点在 cur 的右子树中 } else if (cur.val > num) { - cur = cur.left; // 目标结点在 cur 的左子树中 + cur = cur.left; // 目标节点在 cur 的左子树中 } else { - break; // 找到目标结点,跳出循环 + break; // 找到目标节点,跳出循环 } } - // 返回目标结点 + // 返回目标节点 return cur; } ``` @@ -171,21 +171,21 @@ comments: true === "C#" ```csharp title="binary_search_tree.cs" - /* 查找结点 */ + /* 查找节点 */ TreeNode? search(int num) { TreeNode? cur = root; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur != null) { - // 目标结点在 cur 的右子树中 + // 目标节点在 cur 的右子树中 if (cur.val < num) cur = cur.right; - // 目标结点在 cur 的左子树中 + // 目标节点在 cur 的左子树中 else if (cur.val > num) cur = cur.left; - // 找到目标结点,跳出循环 + // 找到目标节点,跳出循环 else break; } - // 返回目标结点 + // 返回目标节点 return cur; } ``` @@ -193,25 +193,25 @@ comments: true === "Swift" ```swift title="binary_search_tree.swift" - /* 查找结点 */ + /* 查找节点 */ func search(num: Int) -> TreeNode? { var cur = root - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while cur != nil { - // 目标结点在 cur 的右子树中 + // 目标节点在 cur 的右子树中 if cur!.val < num { cur = cur?.right } - // 目标结点在 cur 的左子树中 + // 目标节点在 cur 的左子树中 else if cur!.val > num { cur = cur?.left } - // 找到目标结点,跳出循环 + // 找到目标节点,跳出循环 else { break } } - // 返回目标结点 + // 返回目标节点 return cur } ``` @@ -219,51 +219,51 @@ comments: true === "Zig" ```zig title="binary_search_tree.zig" - // 查找结点 + // 查找节点 fn search(self: *Self, num: T) ?*inc.TreeNode(T) { var cur = self.root; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur != null) { - // 目标结点在 cur 的右子树中 + // 目标节点在 cur 的右子树中 if (cur.?.val < num) { cur = cur.?.right; - // 目标结点在 cur 的左子树中 + // 目标节点在 cur 的左子树中 } else if (cur.?.val > num) { cur = cur.?.left; - // 找到目标结点,跳出循环 + // 找到目标节点,跳出循环 } else { break; } } - // 返回目标结点 + // 返回目标节点 return cur; } ``` -### 插入结点 +### 插入节点 -给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根结点 < 右子树”的性质,插入操作分为两步: +给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,插入操作分为两步: -1. **查找插入位置**:与查找操作类似,我们从根结点出发,根据当前结点值和 `num` 的大小关系循环向下搜索,直到越过叶结点(遍历到 $\text{null}$ )时跳出循环; -2. **在该位置插入结点**:初始化结点 `num` ,将该结点放到 $\text{null}$ 的位置 ; +1. **查找插入位置**:与查找操作类似,我们从根节点出发,根据当前节点值和 `num` 的大小关系循环向下搜索,直到越过叶节点(遍历到 $\text{null}$ )时跳出循环; +2. **在该位置插入节点**:初始化节点 `num` ,将该节点放到 $\text{null}$ 的位置 ; -二叉搜索树不允许存在重复结点,否则将会违背其定义。因此若待插入结点在树中已经存在,则不执行插入,直接返回即可。 +二叉搜索树不允许存在重复节点,否则将会违背其定义。因此若待插入节点在树中已经存在,则不执行插入,直接返回即可。 -![在二叉搜索树中插入结点](binary_search_tree.assets/bst_insert.png) +![在二叉搜索树中插入节点](binary_search_tree.assets/bst_insert.png) -

Fig. 在二叉搜索树中插入结点

+

Fig. 在二叉搜索树中插入节点

=== "Java" ```java title="binary_search_tree.java" - /* 插入结点 */ + /* 插入节点 */ TreeNode insert(int num) { // 若树为空,直接提前返回 if (root == null) return null; TreeNode cur = root, pre = null; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur != null) { - // 找到重复结点,直接返回 + // 找到重复节点,直接返回 if (cur.val == num) return null; pre = cur; // 插入位置在 cur 的右子树中 @@ -271,7 +271,7 @@ comments: true // 插入位置在 cur 的左子树中 else cur = cur.left; } - // 插入结点 val + // 插入节点 val TreeNode node = new TreeNode(num); if (pre.val < num) pre.right = node; else pre.left = node; @@ -282,14 +282,14 @@ comments: true === "C++" ```cpp title="binary_search_tree.cpp" - /* 插入结点 */ + /* 插入节点 */ TreeNode* insert(int num) { // 若树为空,直接提前返回 if (root == nullptr) return nullptr; TreeNode *cur = root, *pre = nullptr; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur != nullptr) { - // 找到重复结点,直接返回 + // 找到重复节点,直接返回 if (cur->val == num) return nullptr; pre = cur; // 插入位置在 cur 的右子树中 @@ -297,7 +297,7 @@ comments: true // 插入位置在 cur 的左子树中 else cur = cur->left; } - // 插入结点 val + // 插入节点 val TreeNode* node = new TreeNode(num); if (pre->val < num) pre->right = node; else pre->left = node; @@ -309,15 +309,15 @@ comments: true ```python title="binary_search_tree.py" def insert(self, num: int) -> TreeNode | None: - """ 插入结点 """ + """ 插入节点 """ # 若树为空,直接提前返回 if self.__root is None: return None - # 循环查找,越过叶结点后跳出 + # 循环查找,越过叶节点后跳出 cur, pre = self.__root, None while cur is not None: - # 找到重复结点,直接返回 + # 找到重复节点,直接返回 if cur.val == num: return None pre = cur @@ -328,7 +328,7 @@ comments: true else: cur = cur.left - # 插入结点 val + # 插入节点 val node = TreeNode(num) if pre.val < num: pre.right = node @@ -340,16 +340,16 @@ comments: true === "Go" ```go title="binary_search_tree.go" - /* 插入结点 */ + /* 插入节点 */ func (bst *binarySearchTree) insert(num int) *TreeNode { cur := bst.root // 若树为空,直接提前返回 if cur == nil { return nil } - // 待插入结点之前的结点位置 + // 待插入节点之前的节点位置 var pre *TreeNode = nil - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 for cur != nil { if cur.Val == num { return nil @@ -361,7 +361,7 @@ comments: true cur = cur.Left } } - // 插入结点 + // 插入节点 node := NewTreeNode(num) if pre.Val < num { pre.Right = node @@ -375,14 +375,14 @@ comments: true === "JavaScript" ```javascript title="binary_search_tree.js" - /* 插入结点 */ + /* 插入节点 */ function insert(num) { // 若树为空,直接提前返回 if (root === null) return null; let cur = root, pre = null; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur !== null) { - // 找到重复结点,直接返回 + // 找到重复节点,直接返回 if (cur.val === num) return null; pre = cur; // 插入位置在 cur 的右子树中 @@ -390,7 +390,7 @@ comments: true // 插入位置在 cur 的左子树中 else cur = cur.left; } - // 插入结点 val + // 插入节点 val let node = new TreeNode(num); if (pre.val < num) pre.right = node; else pre.left = node; @@ -401,7 +401,7 @@ comments: true === "TypeScript" ```typescript title="binary_search_tree.ts" - /* 插入结点 */ + /* 插入节点 */ function insert(num: number): TreeNode | null { // 若树为空,直接提前返回 if (root === null) { @@ -409,10 +409,10 @@ comments: true } let cur = root, pre: TreeNode | null = null; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur !== null) { if (cur.val === num) { - return null; // 找到重复结点,直接返回 + return null; // 找到重复节点,直接返回 } pre = cur; if (cur.val < num) { @@ -421,7 +421,7 @@ comments: true cur = cur.left as TreeNode; // 插入位置在 cur 的左子树中 } } - // 插入结点 val + // 插入节点 val let node = new TreeNode(num); if (pre!.val < num) { pre!.right = node; @@ -441,16 +441,16 @@ comments: true === "C#" ```csharp title="binary_search_tree.cs" - /* 插入结点 */ + /* 插入节点 */ TreeNode? insert(int num) { // 若树为空,直接提前返回 if (root == null) return null; TreeNode? cur = root, pre = null; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur != null) { - // 找到重复结点,直接返回 + // 找到重复节点,直接返回 if (cur.val == num) return null; pre = cur; // 插入位置在 cur 的右子树中 @@ -459,7 +459,7 @@ comments: true else cur = cur.left; } - // 插入结点 val + // 插入节点 val TreeNode node = new TreeNode(num); if (pre != null) { @@ -473,7 +473,7 @@ comments: true === "Swift" ```swift title="binary_search_tree.swift" - /* 插入结点 */ + /* 插入节点 */ func insert(num: Int) -> TreeNode? { // 若树为空,直接提前返回 if root == nil { @@ -481,9 +481,9 @@ comments: true } var cur = root var pre: TreeNode? - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while cur != nil { - // 找到重复结点,直接返回 + // 找到重复节点,直接返回 if cur!.val == num { return nil } @@ -497,7 +497,7 @@ comments: true cur = cur?.left } } - // 插入结点 val + // 插入节点 val let node = TreeNode(x: num) if pre!.val < num { pre?.right = node @@ -511,15 +511,15 @@ comments: true === "Zig" ```zig title="binary_search_tree.zig" - // 插入结点 + // 插入节点 fn insert(self: *Self, num: T) !?*inc.TreeNode(T) { // 若树为空,直接提前返回 if (self.root == null) return null; var cur = self.root; var pre: ?*inc.TreeNode(T) = null; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur != null) { - // 找到重复结点,直接返回 + // 找到重复节点,直接返回 if (cur.?.val == num) return null; pre = cur; // 插入位置在 cur 的右子树中 @@ -530,7 +530,7 @@ comments: true cur = cur.?.left; } } - // 插入结点 val + // 插入节点 val var node = try self.mem_allocator.create(inc.TreeNode(T)); node.init(num); if (pre.?.val < num) { @@ -542,34 +542,34 @@ comments: true } ``` -为了插入结点,需要借助 **辅助结点 `pre`** 保存上一轮循环的结点,这样在遍历到 $\text{null}$ 时,我们也可以获取到其父结点,从而完成结点插入操作。 +为了插入节点,需要借助 **辅助节点 `pre`** 保存上一轮循环的节点,这样在遍历到 $\text{null}$ 时,我们也可以获取到其父节点,从而完成节点插入操作。 -与查找结点相同,插入结点使用 $O(\log n)$ 时间。 +与查找节点相同,插入节点使用 $O(\log n)$ 时间。 -### 删除结点 +### 删除节点 -与插入结点一样,我们需要在删除操作后维持二叉搜索树的“左子树 < 根结点 < 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除结点。接下来,根据待删除结点的子结点数量,删除操作需要分为三种情况: +与插入节点一样,我们需要在删除操作后维持二叉搜索树的“左子树 < 根节点 < 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除节点。接下来,根据待删除节点的子节点数量,删除操作需要分为三种情况: -**当待删除结点的子结点数量 $= 0$ 时**,表明待删除结点是叶结点,直接删除即可。 +**当待删除节点的子节点数量 $= 0$ 时**,表明待删除节点是叶节点,直接删除即可。 -![在二叉搜索树中删除结点(度为 0)](binary_search_tree.assets/bst_remove_case1.png) +![在二叉搜索树中删除节点(度为 0)](binary_search_tree.assets/bst_remove_case1.png) -

Fig. 在二叉搜索树中删除结点(度为 0)

+

Fig. 在二叉搜索树中删除节点(度为 0)

-**当待删除结点的子结点数量 $= 1$ 时**,将待删除结点替换为其子结点即可。 +**当待删除节点的子节点数量 $= 1$ 时**,将待删除节点替换为其子节点即可。 -![在二叉搜索树中删除结点(度为 1)](binary_search_tree.assets/bst_remove_case2.png) +![在二叉搜索树中删除节点(度为 1)](binary_search_tree.assets/bst_remove_case2.png) -

Fig. 在二叉搜索树中删除结点(度为 1)

+

Fig. 在二叉搜索树中删除节点(度为 1)

-**当待删除结点的子结点数量 $= 2$ 时**,删除操作分为三步: +**当待删除节点的子节点数量 $= 2$ 时**,删除操作分为三步: -1. 找到待删除结点在 **中序遍历序列** 中的下一个结点,记为 `nex` ; -2. 在树中递归删除结点 `nex` ; -3. 使用 `nex` 替换待删除结点; +1. 找到待删除节点在 **中序遍历序列** 中的下一个节点,记为 `nex` ; +2. 在树中递归删除节点 `nex` ; +3. 使用 `nex` 替换待删除节点; === "<1>" - ![删除结点(度为 2)步骤](binary_search_tree.assets/bst_remove_case3_step1.png) + ![删除节点(度为 2)步骤](binary_search_tree.assets/bst_remove_case3_step1.png) === "<2>" ![bst_remove_case3_step2](binary_search_tree.assets/bst_remove_case3_step2.png) @@ -580,42 +580,42 @@ comments: true === "<4>" ![bst_remove_case3_step4](binary_search_tree.assets/bst_remove_case3_step4.png) -删除结点操作也使用 $O(\log n)$ 时间,其中查找待删除结点 $O(\log n)$ ,获取中序遍历后继结点 $O(\log n)$ 。 +删除节点操作也使用 $O(\log n)$ 时间,其中查找待删除节点 $O(\log n)$ ,获取中序遍历后继节点 $O(\log n)$ 。 === "Java" ```java title="binary_search_tree.java" - /* 删除结点 */ + /* 删除节点 */ TreeNode remove(int num) { // 若树为空,直接提前返回 if (root == null) return null; TreeNode cur = root, pre = null; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur != null) { - // 找到待删除结点,跳出循环 + // 找到待删除节点,跳出循环 if (cur.val == num) break; pre = cur; - // 待删除结点在 cur 的右子树中 + // 待删除节点在 cur 的右子树中 if (cur.val < num) cur = cur.right; - // 待删除结点在 cur 的左子树中 + // 待删除节点在 cur 的左子树中 else cur = cur.left; } - // 若无待删除结点,则直接返回 + // 若无待删除节点,则直接返回 if (cur == null) return null; - // 子结点数量 = 0 or 1 + // 子节点数量 = 0 or 1 if (cur.left == null || cur.right == null) { - // 当子结点数量 = 0 / 1 时, child = null / 该子结点 + // 当子节点数量 = 0 / 1 时, child = null / 该子节点 TreeNode child = cur.left != null ? cur.left : cur.right; - // 删除结点 cur + // 删除节点 cur if (pre.left == cur) pre.left = child; else pre.right = child; } - // 子结点数量 = 2 + // 子节点数量 = 2 else { - // 获取中序遍历中 cur 的下一个结点 + // 获取中序遍历中 cur 的下一个节点 TreeNode nex = getInOrderNext(cur.right); int tmp = nex.val; - // 递归删除结点 nex + // 递归删除节点 nex remove(nex.val); // 将 nex 的值复制给 cur cur.val = tmp; @@ -623,10 +623,10 @@ comments: true return cur; } - /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + /* 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) */ TreeNode getInOrderNext(TreeNode root) { if (root == null) return root; - // 循环访问左子结点,直到叶结点时为最小结点,跳出 + // 循环访问左子节点,直到叶节点时为最小节点,跳出 while (root.left != null) { root = root.left; } @@ -637,39 +637,39 @@ comments: true === "C++" ```cpp title="binary_search_tree.cpp" - /* 删除结点 */ + /* 删除节点 */ TreeNode* remove(int num) { // 若树为空,直接提前返回 if (root == nullptr) return nullptr; TreeNode *cur = root, *pre = nullptr; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur != nullptr) { - // 找到待删除结点,跳出循环 + // 找到待删除节点,跳出循环 if (cur->val == num) break; pre = cur; - // 待删除结点在 cur 的右子树中 + // 待删除节点在 cur 的右子树中 if (cur->val < num) cur = cur->right; - // 待删除结点在 cur 的左子树中 + // 待删除节点在 cur 的左子树中 else cur = cur->left; } - // 若无待删除结点,则直接返回 + // 若无待删除节点,则直接返回 if (cur == nullptr) return nullptr; - // 子结点数量 = 0 or 1 + // 子节点数量 = 0 or 1 if (cur->left == nullptr || cur->right == nullptr) { - // 当子结点数量 = 0 / 1 时, child = nullptr / 该子结点 + // 当子节点数量 = 0 / 1 时, child = nullptr / 该子节点 TreeNode* child = cur->left != nullptr ? cur->left : cur->right; - // 删除结点 cur + // 删除节点 cur if (pre->left == cur) pre->left = child; else pre->right = child; // 释放内存 delete cur; } - // 子结点数量 = 2 + // 子节点数量 = 2 else { - // 获取中序遍历中 cur 的下一个结点 + // 获取中序遍历中 cur 的下一个节点 TreeNode* nex = getInOrderNext(cur->right); int tmp = nex->val; - // 递归删除结点 nex + // 递归删除节点 nex remove(nex->val); // 将 nex 的值复制给 cur cur->val = tmp; @@ -677,10 +677,10 @@ comments: true return cur; } - /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + /* 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) */ TreeNode* getInOrderNext(TreeNode* root) { if (root == nullptr) return root; - // 循环访问左子结点,直到叶结点时为最小结点,跳出 + // 循环访问左子节点,直到叶节点时为最小节点,跳出 while (root->left != nullptr) { root = root->left; } @@ -692,51 +692,51 @@ comments: true ```python title="binary_search_tree.py" def remove(self, num: int) -> TreeNode | None: - """ 删除结点 """ + """ 删除节点 """ # 若树为空,直接提前返回 if self.__root is None: return None - # 循环查找,越过叶结点后跳出 + # 循环查找,越过叶节点后跳出 cur, pre = self.__root, None while cur is not None: - # 找到待删除结点,跳出循环 + # 找到待删除节点,跳出循环 if cur.val == num: break pre = cur - if cur.val < num: # 待删除结点在 cur 的右子树中 + if cur.val < num: # 待删除节点在 cur 的右子树中 cur = cur.right - else: # 待删除结点在 cur 的左子树中 + else: # 待删除节点在 cur 的左子树中 cur = cur.left - # 若无待删除结点,则直接返回 + # 若无待删除节点,则直接返回 if cur is None: return None - # 子结点数量 = 0 or 1 + # 子节点数量 = 0 or 1 if cur.left is None or cur.right is None: - # 当子结点数量 = 0 / 1 时, child = null / 该子结点 + # 当子节点数量 = 0 / 1 时, child = null / 该子节点 child = cur.left or cur.right - # 删除结点 cur + # 删除节点 cur if pre.left == cur: pre.left = child else: pre.right = child - # 子结点数量 = 2 + # 子节点数量 = 2 else: - # 获取中序遍历中 cur 的下一个结点 + # 获取中序遍历中 cur 的下一个节点 nex: TreeNode = self.get_inorder_next(cur.right) tmp: int = nex.val - # 递归删除结点 nex + # 递归删除节点 nex self.remove(nex.val) # 将 nex 的值复制给 cur cur.val = tmp return cur def get_inorder_next(self, root: TreeNode | None) -> TreeNode | None: - """ 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) """ + """ 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) """ if root is None: return root - # 循环访问左子结点,直到叶结点时为最小结点,跳出 + # 循环访问左子节点,直到叶节点时为最小节点,跳出 while root.left is not None: root = root.left return root @@ -745,54 +745,54 @@ comments: true === "Go" ```go title="binary_search_tree.go" - /* 删除结点 */ + /* 删除节点 */ func (bst *binarySearchTree) remove(num int) *TreeNode { cur := bst.root // 若树为空,直接提前返回 if cur == nil { return nil } - // 待删除结点之前的结点位置 + // 待删除节点之前的节点位置 var pre *TreeNode = nil - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 for cur != nil { if cur.Val == num { break } pre = cur if cur.Val < num { - // 待删除结点在右子树中 + // 待删除节点在右子树中 cur = cur.Right } else { - // 待删除结点在左子树中 + // 待删除节点在左子树中 cur = cur.Left } } - // 若无待删除结点,则直接返回 + // 若无待删除节点,则直接返回 if cur == nil { return nil } - // 子结点数为 0 或 1 + // 子节点数为 0 或 1 if cur.Left == nil || cur.Right == nil { var child *TreeNode = nil - // 取出待删除结点的子结点 + // 取出待删除节点的子节点 if cur.Left != nil { child = cur.Left } else { child = cur.Right } - // 将子结点替换为待删除结点 + // 将子节点替换为待删除节点 if pre.Left == cur { pre.Left = child } else { pre.Right = child } - // 子结点数为 2 + // 子节点数为 2 } else { - // 获取中序遍历中待删除结点 cur 的下一个结点 + // 获取中序遍历中待删除节点 cur 的下一个节点 next := bst.getInOrderNext(cur) temp := next.Val - // 递归删除结点 next + // 递归删除节点 next bst.remove(next.Val) // 将 next 的值复制给 cur cur.Val = temp @@ -800,12 +800,12 @@ comments: true return cur } - /* 获取中序遍历的下一个结点(仅适用于 root 有左子结点的情况) */ + /* 获取中序遍历的下一个节点(仅适用于 root 有左子节点的情况) */ func (bst *binarySearchTree) getInOrderNext(node *TreeNode) *TreeNode { if node == nil { return node } - // 循环访问左子结点,直到叶结点时为最小结点,跳出 + // 循环访问左子节点,直到叶节点时为最小节点,跳出 for node.Left != nil { node = node.Left } @@ -816,37 +816,37 @@ comments: true === "JavaScript" ```javascript title="binary_search_tree.js" - /* 删除结点 */ + /* 删除节点 */ function remove(num) { // 若树为空,直接提前返回 if (root === null) return null; let cur = root, pre = null; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur !== null) { - // 找到待删除结点,跳出循环 + // 找到待删除节点,跳出循环 if (cur.val === num) break; pre = cur; - // 待删除结点在 cur 的右子树中 + // 待删除节点在 cur 的右子树中 if (cur.val < num) cur = cur.right; - // 待删除结点在 cur 的左子树中 + // 待删除节点在 cur 的左子树中 else cur = cur.left; } - // 若无待删除结点,则直接返回 + // 若无待删除节点,则直接返回 if (cur === null) return null; - // 子结点数量 = 0 or 1 + // 子节点数量 = 0 or 1 if (cur.left === null || cur.right === null) { - // 当子结点数量 = 0 / 1 时, child = null / 该子结点 + // 当子节点数量 = 0 / 1 时, child = null / 该子节点 let child = cur.left !== null ? cur.left : cur.right; - // 删除结点 cur + // 删除节点 cur if (pre.left === cur) pre.left = child; else pre.right = child; } - // 子结点数量 = 2 + // 子节点数量 = 2 else { - // 获取中序遍历中 cur 的下一个结点 + // 获取中序遍历中 cur 的下一个节点 let nex = getInOrderNext(cur.right); let tmp = nex.val; - // 递归删除结点 nex + // 递归删除节点 nex remove(nex.val); // 将 nex 的值复制给 cur cur.val = tmp; @@ -854,10 +854,10 @@ comments: true return cur; } - /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + /* 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) */ function getInOrderNext(root) { if (root === null) return root; - // 循环访问左子结点,直到叶结点时为最小结点,跳出 + // 循环访问左子节点,直到叶节点时为最小节点,跳出 while (root.left !== null) { root = root.left; } @@ -868,7 +868,7 @@ comments: true === "TypeScript" ```typescript title="binary_search_tree.ts" - /* 删除结点 */ + /* 删除节点 */ function remove(num: number): TreeNode | null { // 若树为空,直接提前返回 if (root === null) { @@ -876,40 +876,40 @@ comments: true } let cur = root, pre: TreeNode | null = null; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur !== null) { - // 找到待删除结点,跳出循环 + // 找到待删除节点,跳出循环 if (cur.val === num) { break; } pre = cur; if (cur.val < num) { - cur = cur.right as TreeNode; // 待删除结点在 cur 的右子树中 + cur = cur.right as TreeNode; // 待删除节点在 cur 的右子树中 } else { - cur = cur.left as TreeNode; // 待删除结点在 cur 的左子树中 + cur = cur.left as TreeNode; // 待删除节点在 cur 的左子树中 } } - // 若无待删除结点,则直接返回 + // 若无待删除节点,则直接返回 if (cur === null) { return null; } - // 子结点数量 = 0 or 1 + // 子节点数量 = 0 or 1 if (cur.left === null || cur.right === null) { - // 当子结点数量 = 0 / 1 时, child = null / 该子结点 + // 当子节点数量 = 0 / 1 时, child = null / 该子节点 let child = cur.left !== null ? cur.left : cur.right; - // 删除结点 cur + // 删除节点 cur if (pre!.left === cur) { pre!.left = child; } else { pre!.right = child; } } - // 子结点数量 = 2 + // 子节点数量 = 2 else { - // 获取中序遍历中 cur 的下一个结点 + // 获取中序遍历中 cur 的下一个节点 let next = getInOrderNext(cur.right); let tmp = next!.val; - // 递归删除结点 nex + // 递归删除节点 nex remove(next!.val); // 将 nex 的值复制给 cur cur.val = tmp; @@ -917,12 +917,12 @@ comments: true return cur; } - /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + /* 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) */ function getInOrderNext(root: TreeNode | null): TreeNode | null { if (root === null) { return null; } - // 循环访问左子结点,直到叶结点时为最小结点,跳出 + // 循环访问左子节点,直到叶节点时为最小节点,跳出 while (root.left !== null) { root = root.left; } @@ -941,31 +941,31 @@ comments: true === "C#" ```csharp title="binary_search_tree.cs" - /* 删除结点 */ + /* 删除节点 */ TreeNode? remove(int num) { // 若树为空,直接提前返回 if (root == null) return null; TreeNode? cur = root, pre = null; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur != null) { - // 找到待删除结点,跳出循环 + // 找到待删除节点,跳出循环 if (cur.val == num) break; pre = cur; - // 待删除结点在 cur 的右子树中 + // 待删除节点在 cur 的右子树中 if (cur.val < num) cur = cur.right; - // 待删除结点在 cur 的左子树中 + // 待删除节点在 cur 的左子树中 else cur = cur.left; } - // 若无待删除结点,则直接返回 + // 若无待删除节点,则直接返回 if (cur == null || pre == null) return null; - // 子结点数量 = 0 or 1 + // 子节点数量 = 0 or 1 if (cur.left == null || cur.right == null) { - // 当子结点数量 = 0 / 1 时, child = null / 该子结点 + // 当子节点数量 = 0 / 1 时, child = null / 该子节点 TreeNode? child = cur.left != null ? cur.left : cur.right; - // 删除结点 cur + // 删除节点 cur if (pre.left == cur) { pre.left = child; @@ -975,15 +975,15 @@ comments: true pre.right = child; } } - // 子结点数量 = 2 + // 子节点数量 = 2 else { - // 获取中序遍历中 cur 的下一个结点 + // 获取中序遍历中 cur 的下一个节点 TreeNode? nex = getInOrderNext(cur.right); if (nex != null) { int tmp = nex.val; - // 递归删除结点 nex + // 递归删除节点 nex remove(nex.val); // 将 nex 的值复制给 cur cur.val = tmp; @@ -992,11 +992,11 @@ comments: true return cur; } - /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + /* 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) */ TreeNode? getInOrderNext(TreeNode? root) { if (root == null) return root; - // 循环访问左子结点,直到叶结点时为最小结点,跳出 + // 循环访问左子节点,直到叶节点时为最小节点,跳出 while (root.left != null) { root = root.left; @@ -1008,7 +1008,7 @@ comments: true === "Swift" ```swift title="binary_search_tree.swift" - /* 删除结点 */ + /* 删除节点 */ @discardableResult func remove(num: Int) -> TreeNode? { // 若树为空,直接提前返回 @@ -1017,43 +1017,43 @@ comments: true } var cur = root var pre: TreeNode? - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while cur != nil { - // 找到待删除结点,跳出循环 + // 找到待删除节点,跳出循环 if cur!.val == num { break } pre = cur - // 待删除结点在 cur 的右子树中 + // 待删除节点在 cur 的右子树中 if cur!.val < num { cur = cur?.right } - // 待删除结点在 cur 的左子树中 + // 待删除节点在 cur 的左子树中 else { cur = cur?.left } } - // 若无待删除结点,则直接返回 + // 若无待删除节点,则直接返回 if cur == nil { return nil } - // 子结点数量 = 0 or 1 + // 子节点数量 = 0 or 1 if cur?.left == nil || cur?.right == nil { - // 当子结点数量 = 0 / 1 时, child = null / 该子结点 + // 当子节点数量 = 0 / 1 时, child = null / 该子节点 let child = cur?.left != nil ? cur?.left : cur?.right - // 删除结点 cur + // 删除节点 cur if pre?.left === cur { pre?.left = child } else { pre?.right = child } } - // 子结点数量 = 2 + // 子节点数量 = 2 else { - // 获取中序遍历中 cur 的下一个结点 + // 获取中序遍历中 cur 的下一个节点 let nex = getInOrderNext(root: cur?.right) let tmp = nex!.val - // 递归删除结点 nex + // 递归删除节点 nex remove(num: nex!.val) // 将 nex 的值复制给 cur cur?.val = tmp @@ -1061,13 +1061,13 @@ comments: true return cur } - /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + /* 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) */ func getInOrderNext(root: TreeNode?) -> TreeNode? { var root = root if root == nil { return root } - // 循环访问左子结点,直到叶结点时为最小结点,跳出 + // 循环访问左子节点,直到叶节点时为最小节点,跳出 while root?.left != nil { root = root?.left } @@ -1078,43 +1078,43 @@ comments: true === "Zig" ```zig title="binary_search_tree.zig" - // 删除结点 + // 删除节点 fn remove(self: *Self, num: T) ?*inc.TreeNode(T) { // 若树为空,直接提前返回 if (self.root == null) return null; var cur = self.root; var pre: ?*inc.TreeNode(T) = null; - // 循环查找,越过叶结点后跳出 + // 循环查找,越过叶节点后跳出 while (cur != null) { - // 找到待删除结点,跳出循环 + // 找到待删除节点,跳出循环 if (cur.?.val == num) break; pre = cur; - // 待删除结点在 cur 的右子树中 + // 待删除节点在 cur 的右子树中 if (cur.?.val < num) { cur = cur.?.right; - // 待删除结点在 cur 的左子树中 + // 待删除节点在 cur 的左子树中 } else { cur = cur.?.left; } } - // 若无待删除结点,则直接返回 + // 若无待删除节点,则直接返回 if (cur == null) return null; - // 子结点数量 = 0 or 1 + // 子节点数量 = 0 or 1 if (cur.?.left == null or cur.?.right == null) { - // 当子结点数量 = 0 / 1 时, child = null / 该子结点 + // 当子节点数量 = 0 / 1 时, child = null / 该子节点 var child = if (cur.?.left != null) cur.?.left else cur.?.right; - // 删除结点 cur + // 删除节点 cur if (pre.?.left == cur) { pre.?.left = child; } else { pre.?.right = child; } - // 子结点数量 = 2 + // 子节点数量 = 2 } else { - // 获取中序遍历中 cur 的下一个结点 + // 获取中序遍历中 cur 的下一个节点 var nex = self.getInOrderNext(cur.?.right); var tmp = nex.?.val; - // 递归删除结点 nex + // 递归删除节点 nex _ = self.remove(nex.?.val); // 将 nex 的值复制给 cur cur.?.val = tmp; @@ -1122,12 +1122,12 @@ comments: true return cur; } - // 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) + // 获取中序遍历中的下一个节点(仅适用于 root 有左子节点的情况) fn getInOrderNext(self: *Self, node: ?*inc.TreeNode(T)) ?*inc.TreeNode(T) { _ = self; var node_tmp = node; if (node_tmp == null) return null; - // 循环访问左子结点,直到叶结点时为最小结点,跳出 + // 循环访问左子节点,直到叶节点时为最小节点,跳出 while (node_tmp.?.left != null) { node_tmp = node_tmp.?.left; } @@ -1137,7 +1137,7 @@ comments: true ### 排序 -我们知道,「中序遍历」遵循“左 $\rightarrow$ 根 $\rightarrow$ 右”的遍历优先级,而二叉搜索树遵循“左子结点 $<$ 根结点 $<$ 右子结点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小结点,从而得出一条重要性质:**二叉搜索树的中序遍历序列是升序的**。 +我们知道,「中序遍历」遵循“左 $\rightarrow$ 根 $\rightarrow$ 右”的遍历优先级,而二叉搜索树遵循“左子节点 $<$ 根节点 $<$ 右子节点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一条重要性质:**二叉搜索树的中序遍历序列是升序的**。 借助中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 $O(n)$ 时间,而无需额外排序,非常高效。 @@ -1176,9 +1176,9 @@ comments: true ## 7.3.3.   二叉搜索树的退化 -理想情况下,我们希望二叉搜索树的是“左右平衡”的(详见「平衡二叉树」章节),此时可以在 $\log n$ 轮循环内查找任意结点。 +理想情况下,我们希望二叉搜索树的是“左右平衡”的(详见「平衡二叉树」章节),此时可以在 $\log n$ 轮循环内查找任意节点。 -如果我们动态地在二叉搜索树中插入与删除结点,**则可能导致二叉树退化为链表**,此时各种操作的时间复杂度也退化之 $O(n)$ 。 +如果我们动态地在二叉搜索树中插入与删除节点,**则可能导致二叉树退化为链表**,此时各种操作的时间复杂度也退化之 $O(n)$ 。 !!! note diff --git a/chapter_tree/binary_tree.md b/chapter_tree/binary_tree.md index 29657f0aa..72a7738c3 100644 --- a/chapter_tree/binary_tree.md +++ b/chapter_tree/binary_tree.md @@ -4,16 +4,16 @@ comments: true # 7.1.   二叉树 -「二叉树 Binary Tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。类似于链表,二叉树也是以结点为单位存储的,结点包含「值」和两个「指针」。 +「二叉树 Binary Tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。类似于链表,二叉树也是以节点为单位存储的,节点包含「值」和两个「指针」。 === "Java" ```java title="" - /* 二叉树结点类 */ + /* 二叉树节点类 */ class TreeNode { - int val; // 结点值 - TreeNode left; // 左子结点指针 - TreeNode right; // 右子结点指针 + int val; // 节点值 + TreeNode left; // 左子节点指针 + TreeNode right; // 右子节点指针 TreeNode(int x) { val = x; } } ``` @@ -21,11 +21,11 @@ comments: true === "C++" ```cpp title="" - /* 二叉树结点结构体 */ + /* 二叉树节点结构体 */ struct TreeNode { - int val; // 结点值 - TreeNode *left; // 左子结点指针 - TreeNode *right; // 右子结点指针 + int val; // 节点值 + TreeNode *left; // 左子节点指针 + TreeNode *right; // 右子节点指针 TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; ``` @@ -33,24 +33,24 @@ comments: true === "Python" ```python title="" - """ 二叉树结点类 """ + """ 二叉树节点类 """ class TreeNode: def __init__(self, val: int): - self.val: int = val # 结点值 - self.left: Optional[TreeNode] = None # 左子结点指针 - self.right: Optional[TreeNode] = None # 右子结点指针 + self.val: int = val # 节点值 + self.left: Optional[TreeNode] = None # 左子节点指针 + self.right: Optional[TreeNode] = None # 右子节点指针 ``` === "Go" ```go title="" - /* 二叉树结点结构体 */ + /* 二叉树节点结构体 */ type TreeNode struct { Val int Left *TreeNode Right *TreeNode } - /* 结点初始化方法 */ + /* 节点初始化方法 */ func NewTreeNode(v int) *TreeNode { return &TreeNode{ Left: nil, @@ -63,27 +63,27 @@ comments: true === "JavaScript" ```javascript title="" - /* 二叉树结点类 */ + /* 二叉树节点类 */ function TreeNode(val, left, right) { - this.val = (val === undefined ? 0 : val); // 结点值 - this.left = (left === undefined ? null : left); // 左子结点指针 - this.right = (right === undefined ? null : right); // 右子结点指针 + this.val = (val === undefined ? 0 : val); // 节点值 + this.left = (left === undefined ? null : left); // 左子节点指针 + this.right = (right === undefined ? null : right); // 右子节点指针 } ``` === "TypeScript" ```typescript title="" - /* 二叉树结点类 */ + /* 二叉树节点类 */ class TreeNode { val: number; left: TreeNode | null; right: TreeNode | null; constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) { - this.val = val === undefined ? 0 : val; // 结点值 - this.left = left === undefined ? null : left; // 左子结点指针 - this.right = right === undefined ? null : right; // 右子结点指针 + this.val = val === undefined ? 0 : val; // 节点值 + this.left = left === undefined ? null : left; // 左子节点指针 + this.right = right === undefined ? null : right; // 右子节点指针 } } ``` @@ -97,11 +97,11 @@ comments: true === "C#" ```csharp title="" - /* 二叉树结点类 */ + /* 二叉树节点类 */ class TreeNode { - int val; // 结点值 - TreeNode? left; // 左子结点指针 - TreeNode? right; // 右子结点指针 + int val; // 节点值 + TreeNode? left; // 左子节点指针 + TreeNode? right; // 右子节点指针 TreeNode(int x) { val = x; } } ``` @@ -109,11 +109,11 @@ comments: true === "Swift" ```swift title="" - /* 二叉树结点类 */ + /* 二叉树节点类 */ class TreeNode { - var val: Int // 结点值 - var left: TreeNode? // 左子结点指针 - var right: TreeNode? // 右子结点指针 + var val: Int // 节点值 + var left: TreeNode? // 左子节点指针 + var right: TreeNode? // 右子节点指针 init(x: Int) { val = x @@ -127,26 +127,26 @@ comments: true ``` -结点的两个指针分别指向「左子结点」和「右子结点」,并且称该结点为两个子结点的「父结点」。给定二叉树某结点,将“左子结点及其以下结点形成的树”称为该结点的「左子树」,右子树同理。 +节点的两个指针分别指向「左子节点」和「右子节点」,并且称该节点为两个子节点的「父节点」。给定二叉树某节点,将“左子节点及其以下节点形成的树”称为该节点的「左子树」,右子树同理。 -除了叶结点外,每个结点都有子结点和子树。例如,若将下图的“结点 2”看作父结点,那么其左子结点和右子结点分别为“结点 4”和“结点 5”,左子树和右子树分别为“结点 4 及其以下结点形成的树”和“结点 5 及其以下结点形成的树”。 +除了叶节点外,每个节点都有子节点和子树。例如,若将下图的“节点 2”看作父节点,那么其左子节点和右子节点分别为“节点 4”和“节点 5”,左子树和右子树分别为“节点 4 及其以下节点形成的树”和“节点 5 及其以下节点形成的树”。 -![父结点、子结点、子树](binary_tree.assets/binary_tree_definition.png) +![父节点、子节点、子树](binary_tree.assets/binary_tree_definition.png) -

Fig. 父结点、子结点、子树

+

Fig. 父节点、子节点、子树

## 7.1.1.   二叉树常见术语 二叉树的术语较多,建议尽量理解并记住。后续可能遗忘,可以在需要使用时回来查看确认。 -- 「根结点 Root Node」:二叉树最顶层的结点,其没有父结点; -- 「叶结点 Leaf Node」:没有子结点的结点,其两个指针都指向 $\text{null}$ ; -- 结点所处「层 Level」:从顶至底依次增加,根结点所处层为 1 ; -- 结点「度 Degree」:结点的子结点数量。二叉树中,度的范围是 0, 1, 2 ; -- 「边 Edge」:连接两个结点的边,即结点指针; -- 二叉树「高度」:二叉树中根结点到最远叶结点走过边的数量; -- 结点「深度 Depth」 :根结点到该结点走过边的数量; -- 结点「高度 Height」:最远叶结点到该结点走过边的数量; +- 「根节点 Root Node」:二叉树最顶层的节点,其没有父节点; +- 「叶节点 Leaf Node」:没有子节点的节点,其两个指针都指向 $\text{null}$ ; +- 节点所处「层 Level」:从顶至底依次增加,根节点所处层为 1 ; +- 节点「度 Degree」:节点的子节点数量。二叉树中,度的范围是 0, 1, 2 ; +- 「边 Edge」:连接两个节点的边,即节点指针; +- 二叉树「高度」:二叉树中根节点到最远叶节点走过边的数量; +- 节点「深度 Depth」 :根节点到该节点走过边的数量; +- 节点「高度 Height」:最远叶节点到该节点走过边的数量; ![二叉树的常用术语](binary_tree.assets/binary_tree_terminology.png) @@ -154,16 +154,16 @@ comments: true !!! tip "高度与深度的定义" - 值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过结点的数量”,此时高度或深度都需要 + 1 。 + 值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过节点的数量”,此时高度或深度都需要 + 1 。 ## 7.1.2.   二叉树基本操作 -**初始化二叉树**。与链表类似,先初始化结点,再构建引用指向(即指针)。 +**初始化二叉树**。与链表类似,先初始化节点,再构建引用指向(即指针)。 === "Java" ```java title="binary_tree.java" - // 初始化结点 + // 初始化节点 TreeNode n1 = new TreeNode(1); TreeNode n2 = new TreeNode(2); TreeNode n3 = new TreeNode(3); @@ -180,7 +180,7 @@ comments: true ```cpp title="binary_tree.cpp" /* 初始化二叉树 */ - // 初始化结点 + // 初始化节点 TreeNode* n1 = new TreeNode(1); TreeNode* n2 = new TreeNode(2); TreeNode* n3 = new TreeNode(3); @@ -197,7 +197,7 @@ comments: true ```python title="binary_tree.py" """ 初始化二叉树 """ - # 初始化结点 + # 初始化节点 n1 = TreeNode(val=1) n2 = TreeNode(val=2) n3 = TreeNode(val=3) @@ -214,7 +214,7 @@ comments: true ```go title="binary_tree.go" /* 初始化二叉树 */ - // 初始化结点 + // 初始化节点 n1 := NewTreeNode(1) n2 := NewTreeNode(2) n3 := NewTreeNode(3) @@ -231,7 +231,7 @@ comments: true ```javascript title="binary_tree.js" /* 初始化二叉树 */ - // 初始化结点 + // 初始化节点 let n1 = new TreeNode(1), n2 = new TreeNode(2), n3 = new TreeNode(3), @@ -248,7 +248,7 @@ comments: true ```typescript title="binary_tree.ts" /* 初始化二叉树 */ - // 初始化结点 + // 初始化节点 let n1 = new TreeNode(1), n2 = new TreeNode(2), n3 = new TreeNode(3), @@ -271,7 +271,7 @@ comments: true ```csharp title="binary_tree.cs" /* 初始化二叉树 */ - // 初始化结点 + // 初始化节点 TreeNode n1 = new TreeNode(1); TreeNode n2 = new TreeNode(2); TreeNode n3 = new TreeNode(3); @@ -287,7 +287,7 @@ comments: true === "Swift" ```swift title="binary_tree.swift" - // 初始化结点 + // 初始化节点 let n1 = TreeNode(x: 1) let n2 = TreeNode(x: 2) let n3 = TreeNode(x: 3) @@ -306,80 +306,80 @@ comments: true ``` -**插入与删除结点**。与链表类似,插入与删除结点都可以通过修改指针实现。 +**插入与删除节点**。与链表类似,插入与删除节点都可以通过修改指针实现。 -![在二叉树中插入与删除结点](binary_tree.assets/binary_tree_add_remove.png) +![在二叉树中插入与删除节点](binary_tree.assets/binary_tree_add_remove.png) -

Fig. 在二叉树中插入与删除结点

+

Fig. 在二叉树中插入与删除节点

=== "Java" ```java title="binary_tree.java" TreeNode P = new TreeNode(0); - // 在 n1 -> n2 中间插入结点 P + // 在 n1 -> n2 中间插入节点 P n1.left = P; P.left = n2; - // 删除结点 P + // 删除节点 P n1.left = n2; ``` === "C++" ```cpp title="binary_tree.cpp" - /* 插入与删除结点 */ + /* 插入与删除节点 */ TreeNode* P = new TreeNode(0); - // 在 n1 -> n2 中间插入结点 P + // 在 n1 -> n2 中间插入节点 P n1->left = P; P->left = n2; - // 删除结点 P + // 删除节点 P n1->left = n2; ``` === "Python" ```python title="binary_tree.py" - """ 插入与删除结点 """ + """ 插入与删除节点 """ p = TreeNode(0) - # 在 n1 -> n2 中间插入结点 P + # 在 n1 -> n2 中间插入节点 P n1.left = p p.left = n2 - # 删除结点 P + # 删除节点 P n1.left = n2 ``` === "Go" ```go title="binary_tree.go" - /* 插入与删除结点 */ - // 在 n1 -> n2 中间插入结点 P + /* 插入与删除节点 */ + // 在 n1 -> n2 中间插入节点 P p := NewTreeNode(0) n1.Left = p p.Left = n2 - // 删除结点 P + // 删除节点 P n1.Left = n2 ``` === "JavaScript" ```javascript title="binary_tree.js" - /* 插入与删除结点 */ + /* 插入与删除节点 */ let P = new TreeNode(0); - // 在 n1 -> n2 中间插入结点 P + // 在 n1 -> n2 中间插入节点 P n1.left = P; P.left = n2; - // 删除结点 P + // 删除节点 P n1.left = n2; ``` === "TypeScript" ```typescript title="binary_tree.ts" - /* 插入与删除结点 */ + /* 插入与删除节点 */ const P = new TreeNode(0); - // 在 n1 -> n2 中间插入结点 P + // 在 n1 -> n2 中间插入节点 P n1.left = P; P.left = n2; - // 删除结点 P + // 删除节点 P n1.left = n2; ``` @@ -392,12 +392,12 @@ comments: true === "C#" ```csharp title="binary_tree.cs" - /* 插入与删除结点 */ + /* 插入与删除节点 */ TreeNode P = new TreeNode(0); - // 在 n1 -> n2 中间插入结点 P + // 在 n1 -> n2 中间插入节点 P n1.left = P; P.left = n2; - // 删除结点 P + // 删除节点 P n1.left = n2; ``` @@ -405,10 +405,10 @@ comments: true ```swift title="binary_tree.swift" let P = TreeNode(x: 0) - // 在 n1 -> n2 中间插入结点 P + // 在 n1 -> n2 中间插入节点 P n1.left = P P.left = n2 - // 删除结点 P + // 删除节点 P n1.left = n2 ``` @@ -420,13 +420,13 @@ comments: true !!! note - 插入结点会改变二叉树的原有逻辑结构,删除结点往往意味着删除了该结点的所有子树。因此,二叉树中的插入与删除一般都是由一套操作配合完成的,这样才能实现有意义的操作。 + 插入节点会改变二叉树的原有逻辑结构,删除节点往往意味着删除了该节点的所有子树。因此,二叉树中的插入与删除一般都是由一套操作配合完成的,这样才能实现有意义的操作。 ## 7.1.3.   常见二叉树类型 ### 完美二叉树 -「完美二叉树 Perfect Binary Tree」的所有层的结点都被完全填满。在完美二叉树中,叶结点的度为 $0$ ,其余所有结点的度都为 $2$ ;若树高度 $= h$ ,则结点总数 $= 2^{h+1} - 1$ ,呈标准的指数级关系,反映着自然界中常见的细胞分裂。 +「完美二叉树 Perfect Binary Tree」的所有层的节点都被完全填满。在完美二叉树中,叶节点的度为 $0$ ,其余所有节点的度都为 $2$ ;若树高度 $= h$ ,则节点总数 $= 2^{h+1} - 1$ ,呈标准的指数级关系,反映着自然界中常见的细胞分裂。 !!! tip @@ -438,9 +438,9 @@ comments: true ### 完全二叉树 -「完全二叉树 Complete Binary Tree」只有最底层的结点未被填满,且最底层结点尽量靠左填充。 +「完全二叉树 Complete Binary Tree」只有最底层的节点未被填满,且最底层节点尽量靠左填充。 -**完全二叉树非常适合用数组来表示**。如果按照层序遍历序列的顺序来存储,那么空结点 `null` 一定全部出现在序列的尾部,因此我们就可以不用存储这些 null 了。 +**完全二叉树非常适合用数组来表示**。如果按照层序遍历序列的顺序来存储,那么空节点 `null` 一定全部出现在序列的尾部,因此我们就可以不用存储这些 null 了。 ![完全二叉树](binary_tree.assets/complete_binary_tree.png) @@ -448,7 +448,7 @@ comments: true ### 完满二叉树 -「完满二叉树 Full Binary Tree」除了叶结点之外,其余所有结点都有两个子结点。 +「完满二叉树 Full Binary Tree」除了叶节点之外,其余所有节点都有两个子节点。 ![完满二叉树](binary_tree.assets/full_binary_tree.png) @@ -456,7 +456,7 @@ comments: true ### 平衡二叉树 -「平衡二叉树 Balanced Binary Tree」中任意结点的左子树和右子树的高度之差的绝对值 $\leq 1$ 。 +「平衡二叉树 Balanced Binary Tree」中任意节点的左子树和右子树的高度之差的绝对值 $\leq 1$ 。 ![平衡二叉树](binary_tree.assets/balanced_binary_tree.png) @@ -464,7 +464,7 @@ comments: true ## 7.1.4.   二叉树的退化 -当二叉树的每层的结点都被填满时,达到「完美二叉树」;而当所有结点都偏向一边时,二叉树退化为「链表」。 +当二叉树的每层的节点都被填满时,达到「完美二叉树」;而当所有节点都偏向一边时,二叉树退化为「链表」。 - 完美二叉树是一个二叉树的“最佳状态”,可以完全发挥出二叉树“分治”的优势; - 链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至 $O(n)$ ; @@ -473,32 +473,32 @@ comments: true

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

-如下表所示,在最佳和最差结构下,二叉树的叶结点数量、结点总数、高度等达到极大或极小值。 +如下表所示,在最佳和最差结构下,二叉树的叶节点数量、节点总数、高度等达到极大或极小值。
| | 完美二叉树 | 链表 | | ----------------------------- | ---------- | ---------- | -| 第 $i$ 层的结点数量 | $2^{i-1}$ | $1$ | -| 树的高度为 $h$ 时的叶结点数量 | $2^h$ | $1$ | -| 树的高度为 $h$ 时的结点总数 | $2^{h+1} - 1$ | $h + 1$ | -| 树的结点总数为 $n$ 时的高度 | $\log_2 (n+1) - 1$ | $n - 1$ | +| 第 $i$ 层的节点数量 | $2^{i-1}$ | $1$ | +| 树的高度为 $h$ 时的叶节点数量 | $2^h$ | $1$ | +| 树的高度为 $h$ 时的节点总数 | $2^{h+1} - 1$ | $h + 1$ | +| 树的节点总数为 $n$ 时的高度 | $\log_2 (n+1) - 1$ | $n - 1$ |
## 7.1.5.   二叉树表示方式 * -我们一般使用二叉树的「链表表示」,即存储单位为结点 `TreeNode` ,结点之间通过指针(引用)相连接。本文前述示例代码展示了二叉树在链表表示下的各项基本操作。 +我们一般使用二叉树的「链表表示」,即存储单位为节点 `TreeNode` ,节点之间通过指针(引用)相连接。本文前述示例代码展示了二叉树在链表表示下的各项基本操作。 -那能否可以用「数组表示」二叉树呢?答案是肯定的。先来分析一个简单案例,给定一个「完美二叉树」,将结点按照层序遍历的顺序编号(从 0 开始),那么可以推导得出父结点索引与子结点索引之间的「映射公式」:**设结点的索引为 $i$ ,则该结点的左子结点索引为 $2i + 1$ 、右子结点索引为 $2i + 2$** 。 +那能否可以用「数组表示」二叉树呢?答案是肯定的。先来分析一个简单案例,给定一个「完美二叉树」,将节点按照层序遍历的顺序编号(从 0 开始),那么可以推导得出父节点索引与子节点索引之间的「映射公式」:**设节点的索引为 $i$ ,则该节点的左子节点索引为 $2i + 1$ 、右子节点索引为 $2i + 2$** 。 -**本质上,映射公式的作用就是链表中的指针**。对于层序遍历序列中的任意结点,我们都可以使用映射公式来访问子结点。因此,可以直接使用层序遍历序列(即数组)来表示完美二叉树。 +**本质上,映射公式的作用就是链表中的指针**。对于层序遍历序列中的任意节点,我们都可以使用映射公式来访问子节点。因此,可以直接使用层序遍历序列(即数组)来表示完美二叉树。 ![完美二叉树的数组表示](binary_tree.assets/array_representation_mapping.png)

Fig. 完美二叉树的数组表示

-然而,完美二叉树只是个例,二叉树中间层往往存在许多空结点(即 `null` ),而层序遍历序列并不包含这些空结点,并且我们无法单凭序列来猜测空结点的数量和分布位置,**即理论上存在许多种二叉树都符合该层序遍历序列**。显然,这种情况无法使用数组来存储二叉树。 +然而,完美二叉树只是个例,二叉树中间层往往存在许多空节点(即 `null` ),而层序遍历序列并不包含这些空节点,并且我们无法单凭序列来猜测空节点的数量和分布位置,**即理论上存在许多种二叉树都符合该层序遍历序列**。显然,这种情况无法使用数组来存储二叉树。 ![给定数组对应多种二叉树可能性](binary_tree.assets/array_representation_without_empty.png) @@ -519,7 +519,7 @@ comments: true ```cpp title="" /* 二叉树的数组表示 */ // 为了符合数据类型为 int ,使用 int 最大值标记空位 - // 该方法的使用前提是没有结点的值 = INT_MAX + // 该方法的使用前提是没有节点的值 = INT_MAX vector tree = { 1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15 }; ``` @@ -587,10 +587,10 @@ comments: true

Fig. 任意类型二叉树的数组表示

-回顾「完全二叉树」的定义,其只有最底层有空结点,并且最底层的结点尽量靠左,因而所有空结点都一定出现在层序遍历序列的末尾。**因为我们先验地确定了空位的位置,所以在使用数组表示完全二叉树时,可以省略存储“空位”**。因此,完全二叉树非常适合使用数组来表示。 +回顾「完全二叉树」的定义,其只有最底层有空节点,并且最底层的节点尽量靠左,因而所有空节点都一定出现在层序遍历序列的末尾。**因为我们先验地确定了空位的位置,所以在使用数组表示完全二叉树时,可以省略存储“空位”**。因此,完全二叉树非常适合使用数组来表示。 ![完全二叉树的数组表示](binary_tree.assets/array_representation_complete_binary_tree.png)

Fig. 完全二叉树的数组表示

-数组表示有两个优点: 一是不需要存储指针,节省空间;二是可以随机访问结点。然而,当二叉树中的“空位”很多时,数组中只包含很少结点的数据,空间利用率很低。 +数组表示有两个优点: 一是不需要存储指针,节省空间;二是可以随机访问节点。然而,当二叉树中的“空位”很多时,数组中只包含很少节点的数据,空间利用率很低。 diff --git a/chapter_tree/binary_tree_traversal.md b/chapter_tree/binary_tree_traversal.md index a91039cf2..585955226 100755 --- a/chapter_tree/binary_tree_traversal.md +++ b/chapter_tree/binary_tree_traversal.md @@ -4,13 +4,13 @@ comments: true # 7.2.   二叉树遍历 -从物理结构角度看,树是一种基于链表的数据结构,因此遍历方式也是通过指针(即引用)逐个遍历结点。同时,树还是一种非线性数据结构,这导致遍历树比遍历链表更加复杂,需要使用搜索算法来实现。 +从物理结构角度看,树是一种基于链表的数据结构,因此遍历方式也是通过指针(即引用)逐个遍历节点。同时,树还是一种非线性数据结构,这导致遍历树比遍历链表更加复杂,需要使用搜索算法来实现。 常见的二叉树遍历方式有层序遍历、前序遍历、中序遍历、后序遍历。 ## 7.2.1.   层序遍历 -「层序遍历 Level-Order Traversal」从顶至底、一层一层地遍历二叉树,并在每层中按照从左到右的顺序访问结点。 +「层序遍历 Level-Order Traversal」从顶至底、一层一层地遍历二叉树,并在每层中按照从左到右的顺序访问节点。 层序遍历本质上是「广度优先搜索 Breadth-First Traversal」,其体现着一种“一圈一圈向外”的层进遍历方式。 @@ -27,17 +27,17 @@ comments: true ```java title="binary_tree_bfs.java" /* 层序遍历 */ List levelOrder(TreeNode root) { - // 初始化队列,加入根结点 + // 初始化队列,加入根节点 Queue queue = new LinkedList<>() {{ add(root); }}; // 初始化一个列表,用于保存遍历序列 List list = new ArrayList<>(); while (!queue.isEmpty()) { TreeNode node = queue.poll(); // 队列出队 - list.add(node.val); // 保存结点值 + list.add(node.val); // 保存节点值 if (node.left != null) - queue.offer(node.left); // 左子结点入队 + queue.offer(node.left); // 左子节点入队 if (node.right != null) - queue.offer(node.right); // 右子结点入队 + queue.offer(node.right); // 右子节点入队 } return list; } @@ -48,7 +48,7 @@ comments: true ```cpp title="binary_tree_bfs.cpp" /* 层序遍历 */ vector levelOrder(TreeNode* root) { - // 初始化队列,加入根结点 + // 初始化队列,加入根节点 queue queue; queue.push(root); // 初始化一个列表,用于保存遍历序列 @@ -56,11 +56,11 @@ comments: true while (!queue.empty()) { TreeNode* node = queue.front(); queue.pop(); // 队列出队 - vec.push_back(node->val); // 保存结点值 + vec.push_back(node->val); // 保存节点值 if (node->left != nullptr) - queue.push(node->left); // 左子结点入队 + queue.push(node->left); // 左子节点入队 if (node->right != nullptr) - queue.push(node->right); // 右子结点入队 + queue.push(node->right); // 右子节点入队 } return vec; } @@ -71,18 +71,18 @@ comments: true ```python title="binary_tree_bfs.py" def level_order(root: TreeNode | None) -> list[int]: """ 层序遍历 """ - # 初始化队列,加入根结点 + # 初始化队列,加入根节点 queue: deque[TreeNode] = deque() queue.append(root) # 初始化一个列表,用于保存遍历序列 res: list[int] = [] while queue: node: TreeNode = queue.popleft() # 队列出队 - res.append(node.val) # 保存结点值 + res.append(node.val) # 保存节点值 if node.left is not None: - queue.append(node.left) # 左子结点入队 + queue.append(node.left) # 左子节点入队 if node.right is not None: - queue.append(node.right) # 右子结点入队 + queue.append(node.right) # 右子节点入队 return res ``` @@ -91,7 +91,7 @@ comments: true ```go title="binary_tree_bfs.go" /* 层序遍历 */ func levelOrder(root *TreeNode) []int { - // 初始化队列,加入根结点 + // 初始化队列,加入根节点 queue := list.New() queue.PushBack(root) // 初始化一个切片,用于保存遍历序列 @@ -99,14 +99,14 @@ comments: true for queue.Len() > 0 { // 队列出队 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) } } @@ -119,17 +119,17 @@ comments: true ```javascript title="binary_tree_bfs.js" /* 层序遍历 */ function levelOrder(root) { - // 初始化队列,加入根结点 + // 初始化队列,加入根节点 const queue = [root]; // 初始化一个列表,用于保存遍历序列 const list = []; while (queue.length) { let node = queue.shift(); // 队列出队 - list.push(node.val); // 保存结点值 + list.push(node.val); // 保存节点值 if (node.left) - queue.push(node.left); // 左子结点入队 + queue.push(node.left); // 左子节点入队 if (node.right) - queue.push(node.right); // 右子结点入队 + queue.push(node.right); // 右子节点入队 } return list; @@ -141,18 +141,18 @@ comments: true ```typescript title="binary_tree_bfs.ts" /* 层序遍历 */ function levelOrder(root: TreeNode | null): number[] { - // 初始化队列,加入根结点 + // 初始化队列,加入根节点 const queue = [root]; // 初始化一个列表,用于保存遍历序列 const list: number[] = []; while (queue.length) { let node = queue.shift() as TreeNode; // 队列出队 - list.push(node.val); // 保存结点值 + list.push(node.val); // 保存节点值 if (node.left) { - queue.push(node.left); // 左子结点入队 + queue.push(node.left); // 左子节点入队 } if (node.right) { - queue.push(node.right); // 右子结点入队 + queue.push(node.right); // 右子节点入队 } } return list; @@ -171,7 +171,7 @@ comments: true /* 层序遍历 */ List levelOrder(TreeNode root) { - // 初始化队列,加入根结点 + // 初始化队列,加入根节点 Queue queue = new(); queue.Enqueue(root); // 初始化一个列表,用于保存遍历序列 @@ -179,11 +179,11 @@ comments: true while (queue.Count != 0) { TreeNode node = queue.Dequeue(); // 队列出队 - list.Add(node.val); // 保存结点值 + list.Add(node.val); // 保存节点值 if (node.left != null) - queue.Enqueue(node.left); // 左子结点入队 + queue.Enqueue(node.left); // 左子节点入队 if (node.right != null) - queue.Enqueue(node.right); // 右子结点入队 + queue.Enqueue(node.right); // 右子节点入队 } return list; } @@ -194,18 +194,18 @@ comments: true ```swift title="binary_tree_bfs.swift" /* 层序遍历 */ func levelOrder(root: TreeNode) -> [Int] { - // 初始化队列,加入根结点 + // 初始化队列,加入根节点 var queue: [TreeNode] = [root] // 初始化一个列表,用于保存遍历序列 var list: [Int] = [] while !queue.isEmpty { let node = queue.removeFirst() // 队列出队 - list.append(node.val) // 保存结点值 + list.append(node.val) // 保存节点值 if let left = node.left { - queue.append(left) // 左子结点入队 + queue.append(left) // 左子节点入队 } if let right = node.right { - queue.append(right) // 右子结点入队 + queue.append(right) // 右子节点入队 } } return list @@ -217,7 +217,7 @@ comments: true ```zig title="binary_tree_bfs.zig" // 层序遍历 fn levelOrder(comptime T: type, mem_allocator: std.mem.Allocator, root: *inc.TreeNode(T)) !std.ArrayList(T) { - // 初始化队列,加入根结点 + // 初始化队列,加入根节点 const L = std.TailQueue(*inc.TreeNode(T)); var queue = L{}; var root_node = try mem_allocator.create(L.Node); @@ -228,16 +228,16 @@ comments: true while (queue.len > 0) { var queue_node = queue.popFirst().?; // 队列出队 var node = queue_node.data; - try list.append(node.val); // 保存结点值 + try list.append(node.val); // 保存节点值 if (node.left != null) { var tmp_node = try mem_allocator.create(L.Node); tmp_node.data = node.left.?; - queue.append(tmp_node); // 左子结点入队 + queue.append(tmp_node); // 左子节点入队 } if (node.right != null) { var tmp_node = try mem_allocator.create(L.Node); tmp_node.data = node.right.?; - queue.append(tmp_node); // 右子结点入队 + queue.append(tmp_node); // 右子节点入队 } } return list; @@ -246,15 +246,15 @@ comments: true ### 复杂度分析 -**时间复杂度**:所有结点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为结点数量。 +**时间复杂度**:所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。 -**空间复杂度**:当为满二叉树时达到最差情况,遍历到最底层前,队列中最多同时存在 $\frac{n + 1}{2}$ 个结点,使用 $O(n)$ 空间。 +**空间复杂度**:当为满二叉树时达到最差情况,遍历到最底层前,队列中最多同时存在 $\frac{n + 1}{2}$ 个节点,使用 $O(n)$ 空间。 ## 7.2.2.   前序、中序、后序遍历 相对地,前、中、后序遍历皆属于「深度优先遍历 Depth-First Traversal」,其体现着一种“先走到尽头,再回头继续”的回溯遍历方式。 -如下图所示,左侧是深度优先遍历的的示意图,右上方是对应的递归实现代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,走的过程中,在每个结点都会遇到三个位置,分别对应前序遍历、中序遍历、后序遍历。 +如下图所示,左侧是深度优先遍历的的示意图,右上方是对应的递归实现代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,走的过程中,在每个节点都会遇到三个位置,分别对应前序遍历、中序遍历、后序遍历。 ![二叉搜索树的前、中、后序遍历](binary_tree_traversal.assets/binary_tree_dfs.png) @@ -262,9 +262,9 @@ comments: true
-| 位置 | 含义 | 此处访问结点时对应 | +| 位置 | 含义 | 此处访问节点时对应 | | ---------- | ------------------------------------ | ----------------------------- | -| 橙色圆圈处 | 刚进入此结点,即将访问该结点的左子树 | 前序遍历 Pre-Order Traversal | +| 橙色圆圈处 | 刚进入此节点,即将访问该节点的左子树 | 前序遍历 Pre-Order Traversal | | 蓝色圆圈处 | 已访问完左子树,即将访问右子树 | 中序遍历 In-Order Traversal | | 紫色圆圈处 | 已访问完左子树和右子树,即将返回 | 后序遍历 Post-Order Traversal | @@ -278,7 +278,7 @@ comments: true /* 前序遍历 */ void preOrder(TreeNode root) { if (root == null) return; - // 访问优先级:根结点 -> 左子树 -> 右子树 + // 访问优先级:根节点 -> 左子树 -> 右子树 list.add(root.val); preOrder(root.left); preOrder(root.right); @@ -287,7 +287,7 @@ comments: true /* 中序遍历 */ void inOrder(TreeNode root) { if (root == null) return; - // 访问优先级:左子树 -> 根结点 -> 右子树 + // 访问优先级:左子树 -> 根节点 -> 右子树 inOrder(root.left); list.add(root.val); inOrder(root.right); @@ -296,7 +296,7 @@ comments: true /* 后序遍历 */ void postOrder(TreeNode root) { if (root == null) return; - // 访问优先级:左子树 -> 右子树 -> 根结点 + // 访问优先级:左子树 -> 右子树 -> 根节点 postOrder(root.left); postOrder(root.right); list.add(root.val); @@ -309,7 +309,7 @@ comments: true /* 前序遍历 */ void preOrder(TreeNode* root) { if (root == nullptr) return; - // 访问优先级:根结点 -> 左子树 -> 右子树 + // 访问优先级:根节点 -> 左子树 -> 右子树 vec.push_back(root->val); preOrder(root->left); preOrder(root->right); @@ -318,7 +318,7 @@ comments: true /* 中序遍历 */ void inOrder(TreeNode* root) { if (root == nullptr) return; - // 访问优先级:左子树 -> 根结点 -> 右子树 + // 访问优先级:左子树 -> 根节点 -> 右子树 inOrder(root->left); vec.push_back(root->val); inOrder(root->right); @@ -327,7 +327,7 @@ comments: true /* 后序遍历 */ void postOrder(TreeNode* root) { if (root == nullptr) return; - // 访问优先级:左子树 -> 右子树 -> 根结点 + // 访问优先级:左子树 -> 右子树 -> 根节点 postOrder(root->left); postOrder(root->right); vec.push_back(root->val); @@ -341,7 +341,7 @@ comments: true """ 前序遍历 """ if root is None: return - # 访问优先级:根结点 -> 左子树 -> 右子树 + # 访问优先级:根节点 -> 左子树 -> 右子树 res.append(root.val) pre_order(root=root.left) pre_order(root=root.right) @@ -350,7 +350,7 @@ comments: true """ 中序遍历 """ if root is None: return - # 访问优先级:左子树 -> 根结点 -> 右子树 + # 访问优先级:左子树 -> 根节点 -> 右子树 in_order(root=root.left) res.append(root.val) in_order(root=root.right) @@ -359,7 +359,7 @@ comments: true """ 后序遍历 """ if root is None: return - # 访问优先级:左子树 -> 右子树 -> 根结点 + # 访问优先级:左子树 -> 右子树 -> 根节点 post_order(root=root.left) post_order(root=root.right) res.append(root.val) @@ -373,7 +373,7 @@ comments: true if node == nil { return } - // 访问优先级:根结点 -> 左子树 -> 右子树 + // 访问优先级:根节点 -> 左子树 -> 右子树 nums = append(nums, node.Val) preOrder(node.Left) preOrder(node.Right) @@ -384,7 +384,7 @@ comments: true if node == nil { return } - // 访问优先级:左子树 -> 根结点 -> 右子树 + // 访问优先级:左子树 -> 根节点 -> 右子树 inOrder(node.Left) nums = append(nums, node.Val) inOrder(node.Right) @@ -395,7 +395,7 @@ comments: true if node == nil { return } - // 访问优先级:左子树 -> 右子树 -> 根结点 + // 访问优先级:左子树 -> 右子树 -> 根节点 postOrder(node.Left) postOrder(node.Right) nums = append(nums, node.Val) @@ -408,7 +408,7 @@ comments: true /* 前序遍历 */ function preOrder(root) { if (root === null) return; - // 访问优先级:根结点 -> 左子树 -> 右子树 + // 访问优先级:根节点 -> 左子树 -> 右子树 list.push(root.val); preOrder(root.left); preOrder(root.right); @@ -417,7 +417,7 @@ comments: true /* 中序遍历 */ function inOrder(root) { if (root === null) return; - // 访问优先级:左子树 -> 根结点 -> 右子树 + // 访问优先级:左子树 -> 根节点 -> 右子树 inOrder(root.left); list.push(root.val); inOrder(root.right); @@ -426,7 +426,7 @@ comments: true /* 后序遍历 */ function postOrder(root) { if (root === null) return; - // 访问优先级:左子树 -> 右子树 -> 根结点 + // 访问优先级:左子树 -> 右子树 -> 根节点 postOrder(root.left); postOrder(root.right); list.push(root.val); @@ -441,7 +441,7 @@ comments: true if (root === null) { return; } - // 访问优先级:根结点 -> 左子树 -> 右子树 + // 访问优先级:根节点 -> 左子树 -> 右子树 list.push(root.val); preOrder(root.left); preOrder(root.right); @@ -452,7 +452,7 @@ comments: true if (root === null) { return; } - // 访问优先级:左子树 -> 根结点 -> 右子树 + // 访问优先级:左子树 -> 根节点 -> 右子树 inOrder(root.left); list.push(root.val); inOrder(root.right); @@ -463,7 +463,7 @@ comments: true if (root === null) { return; } - // 访问优先级:左子树 -> 右子树 -> 根结点 + // 访问优先级:左子树 -> 右子树 -> 根节点 postOrder(root.left); postOrder(root.right); list.push(root.val); @@ -487,7 +487,7 @@ comments: true void preOrder(TreeNode? root) { if (root == null) return; - // 访问优先级:根结点 -> 左子树 -> 右子树 + // 访问优先级:根节点 -> 左子树 -> 右子树 list.Add(root.val); preOrder(root.left); preOrder(root.right); @@ -497,7 +497,7 @@ comments: true void inOrder(TreeNode? root) { if (root == null) return; - // 访问优先级:左子树 -> 根结点 -> 右子树 + // 访问优先级:左子树 -> 根节点 -> 右子树 inOrder(root.left); list.Add(root.val); inOrder(root.right); @@ -507,7 +507,7 @@ comments: true void postOrder(TreeNode? root) { if (root == null) return; - // 访问优先级:左子树 -> 右子树 -> 根结点 + // 访问优先级:左子树 -> 右子树 -> 根节点 postOrder(root.left); postOrder(root.right); list.Add(root.val); @@ -522,7 +522,7 @@ comments: true guard let root = root else { return } - // 访问优先级:根结点 -> 左子树 -> 右子树 + // 访问优先级:根节点 -> 左子树 -> 右子树 list.append(root.val) preOrder(root: root.left) preOrder(root: root.right) @@ -533,7 +533,7 @@ comments: true guard let root = root else { return } - // 访问优先级:左子树 -> 根结点 -> 右子树 + // 访问优先级:左子树 -> 根节点 -> 右子树 inOrder(root: root.left) list.append(root.val) inOrder(root: root.right) @@ -544,7 +544,7 @@ comments: true guard let root = root else { return } - // 访问优先级:左子树 -> 右子树 -> 根结点 + // 访问优先级:左子树 -> 右子树 -> 根节点 postOrder(root: root.left) postOrder(root: root.right) list.append(root.val) @@ -557,7 +557,7 @@ comments: true // 前序遍历 fn preOrder(comptime T: type, root: ?*inc.TreeNode(T)) !void { if (root == null) return; - // 访问优先级:根结点 -> 左子树 -> 右子树 + // 访问优先级:根节点 -> 左子树 -> 右子树 try list.append(root.?.val); try preOrder(T, root.?.left); try preOrder(T, root.?.right); @@ -566,7 +566,7 @@ comments: true // 中序遍历 fn inOrder(comptime T: type, root: ?*inc.TreeNode(T)) !void { if (root == null) return; - // 访问优先级:左子树 -> 根结点 -> 右子树 + // 访问优先级:左子树 -> 根节点 -> 右子树 try inOrder(T, root.?.left); try list.append(root.?.val); try inOrder(T, root.?.right); @@ -575,7 +575,7 @@ comments: true // 后序遍历 fn postOrder(comptime T: type, root: ?*inc.TreeNode(T)) !void { if (root == null) return; - // 访问优先级:左子树 -> 右子树 -> 根结点 + // 访问优先级:左子树 -> 右子树 -> 根节点 try postOrder(T, root.?.left); try postOrder(T, root.?.right); try list.append(root.?.val); @@ -588,6 +588,6 @@ comments: true ### 复杂度分析 -**时间复杂度**:所有结点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为结点数量。 +**时间复杂度**:所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。 **空间复杂度**:当树退化为链表时达到最差情况,递归深度达到 $n$ ,系统使用 $O(n)$ 栈帧空间。 diff --git a/chapter_tree/summary.md b/chapter_tree/summary.md index 4101a9759..dfdcfb8d6 100644 --- a/chapter_tree/summary.md +++ b/chapter_tree/summary.md @@ -6,12 +6,12 @@ comments: true ### 二叉树 -- 二叉树是一种非线性数据结构,代表着“一分为二”的分治逻辑。二叉树的结点包含「值」和两个「指针」,分别指向左子结点和右子结点。 -- 选定二叉树中某结点,将其左(右)子结点以下形成的树称为左(右)子树。 -- 二叉树的术语较多,包括根结点、叶结点、层、度、边、高度、深度等。 -- 二叉树的初始化、结点插入、结点删除操作与链表的操作方法类似。 +- 二叉树是一种非线性数据结构,代表着“一分为二”的分治逻辑。二叉树的节点包含「值」和两个「指针」,分别指向左子节点和右子节点。 +- 选定二叉树中某节点,将其左(右)子节点以下形成的树称为左(右)子树。 +- 二叉树的术语较多,包括根节点、叶节点、层、度、边、高度、深度等。 +- 二叉树的初始化、节点插入、节点删除操作与链表的操作方法类似。 - 常见的二叉树类型包括完美二叉树、完全二叉树、完满二叉树、平衡二叉树。完美二叉树是理想状态,链表则是退化后的最差状态。 -- 二叉树可以使用数组表示,具体做法是将结点值和空位按照层序遍历的顺序排列,并基于父结点和子结点之间的索引映射公式实现指针。 +- 二叉树可以使用数组表示,具体做法是将节点值和空位按照层序遍历的顺序排列,并基于父节点和子节点之间的索引映射公式实现指针。 ### 二叉树遍历 @@ -21,5 +21,5 @@ comments: true ### 二叉搜索树 - 二叉搜索树是一种高效的元素查找数据结构,查找、插入、删除操作的时间复杂度皆为 $O(\log n)$ 。二叉搜索树退化为链表后,各项时间复杂度劣化至 $O(n)$ ,因此如何避免退化是非常重要的课题。 -- AVL 树又称平衡二叉搜索树,其通过旋转操作,使得在不断插入与删除结点后,仍然可以保持二叉树的平衡(不退化)。 -- AVL 树的旋转操作分为右旋、左旋、先右旋后左旋、先左旋后右旋。在插入或删除结点后,AVL 树会从底至顶地执行旋转操作,使树恢复平衡。 +- AVL 树又称平衡二叉搜索树,其通过旋转操作,使得在不断插入与删除节点后,仍然可以保持二叉树的平衡(不退化)。 +- AVL 树的旋转操作分为右旋、左旋、先右旋后左旋、先左旋后右旋。在插入或删除节点后,AVL 树会从底至顶地执行旋转操作,使树恢复平衡。