#CS225

Tree

Concept & Teminology

  • 定义: 树是一种 非线性 (non-linear) 数据结构,由 节点 (node) 和 边 (edge) 组成。

  • 关键特性:

    • Acyclic Graph (无环图): 树中不存在环路。
    • Connected Graph (连通图): 任意两个节点之间都存在路径。
  • 术语:

    • 节点 (Node/Vertex): 树中的每个元素。
    • 边 (Edge): 连接两个节点的线。
    • 根节点 (Root): 树的顶端节点,没有父节点。
    • 父节点 (Parent): 有子节点的节点。
    • 子节点 (Child): 被父节点指向的节点。
    • 兄弟节点 (Sibling): 拥有相同父节点的节点。
    • 路径 (Path): 从一个节点到另一个节点经过的边的序列。
    • 叶子节点 (Leaf): 没有子节点的节点。
    • 子树 (Subtree): 树中的任意节点及其所有后代。
    • 祖先 (Ancestor): 从根节点到某个节点路径上的所有节点。
    • 后代 (Descendant): 某个节点子树中的所有节点。
    • 高度 (Height): 从根节点到最远叶子节点的 最长路径 (longest path) 上的 边的数量 (Count Edges)。

二叉树及基本性质

  • 定义: 二叉树 (Binary Tree) 是一种特殊的树,其中每个节点最多有两个子节点,分别称为 左子节点 (left child) 和 右子节点 (right child)。

  • 性质:

    • 满二叉树 (Full Binary Tree): 每个节点要么没有子节点,要么有两个子节点。
    • 完美二叉树 (Perfect Binary Tree): 所有内部节点都有两个子节点,并且所有叶子节点都在同一层。 高度为 h 的完美二叉树的节点数量: 2h+11
    • 完全二叉树 (Complete Binary Tree): 除了最后一层,其他每一层都是满的,并且最后一层的节点都尽可能地靠左排列。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <class T>
class BinaryTree {
private:
    struct TreeNode {
        T data;
        TreeNode *left;
        TreeNode *right;
    };

    TreeNode *root;

public:
    BinaryTree() : root(nullptr) {} // 构造函数
    ~BinaryTree() { clear(); }      // 析构函数,释放内存

    // ... (其他方法,例如 insert, remove, traverse 等) ...
};

1. Height

定义:

  • 从根节点到最远叶子节点的最长路径 (longest path) 上的 边的数量 (Count Edges)
    • 空树的高度是 -1。
    • 只有一个节点的树的高度是 0。

递归定义:
f7058bf799e924202be9d9d457e2b86.png

2. Full Tree
每个节点要么没有子节点,要么有两个子节点

  • F= 空树为满二叉树
  • F=r,TL,TR 其中 TL,TR 要么都为空,要么都不为空

3. Perfect Tree
所有内部节点都有两个子节点,并且所有叶子节点都在同一层
高度为 h 的完美二叉树的节点数量: Ph=2h+11

递归定义:

  • P=
  • P=r,TL,TR 其中 TL,TR 高度均为 h1

4. Complete Tree
除了最后一层,其他每一层都是满的,并且最后一层的节点都尽可能地靠左排列。

  • 递归定义:对于高度为 h 的完全二叉树 Ch​:
    1. C1= (空树是高度为 -1 的完全二叉树)
    2. Ch=r,TL,TR, 并且满足以下条件之一:
      • TLCh1TRPh2
      • TLPh1 , TRCh1
      • P: 完美二叉树 (perfect binary tree)
      • C: 完全二叉树 (complete binary tree)
      • 下标表示树的高度

[!warning] 注意

  • 满二叉树不一定是完全二叉树
  • 完全二叉树不一定是满二叉树

5. NULL 的数量

NULL 的数量为当前节点数量 +1(可以利用数学归纳法证明)

Abstract Data Type(ABT)

Insert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
template <class T>
void BinaryTree<T>::insert(T value) {
    TreeNode *newNode = new TreeNode{value, nullptr, nullptr};

    if (!root) {
        root = newNode;
        return;
    }

    TreeNode *current = root;
    while (current) {
        if (value < current->data) {
            if (!current->left) {
                current->left = newNode;
                return;
            }
            current = current->left;
        } else {
            if (!current->right) {
                current->right = newNode;
                return;
            }
            current = current->right;
        }
    }
}

Remove

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
template <typename T>
void Tree<T>::remove(TreeNode<T>* node) {
    if (node == nullptr) return;
    
    // 获取父节点引用
    TreeNode<T>* parent = node->parent;
    
    // 处理节点是根节点的情况
    if (parent == nullptr) {
        // 根节点删除操作
        delete_subtree(root);
        root = nullptr;
        return;
    }
    
    // 处理一般情况
    // 1. 更新父节点的子节点链表
    if (parent->firstChild == node) {
        // 如果是第一个子节点
        parent->firstChild = node->nextSibling;
    } else {
        // 如果不是第一个子节点,需要遍历找到前一个兄弟节点
        TreeNode<T>* sibling = parent->firstChild;
        while (sibling != nullptr && sibling->nextSibling != node) {
            sibling = sibling->nextSibling;
        }
        
        if (sibling != nullptr) {
            sibling->nextSibling = node->nextSibling;
        }
    }
    
    // 2. 递归删除子树
    delete_subtree(node);
}
// 删除子树的辅助函数
template <typename T>
void Tree<T>::delete_subtree(TreeNode<T>* node) {
    if (node == nullptr) return;
    
    // 递归删除所有子节点
    TreeNode<T>* child = node->firstChild;
    while (child != nullptr) {
        TreeNode<T>* next = child->nextSibling;
        delete_subtree(child);
        child = next;
    }
    
    // 删除当前节点
    delete node;
}



Copy

1
2
3
4
5
6
7
8
9
template<class T>
TreeNode * BinaryTree<T>::copy_(TreeNode* root){
	if (root != nullptr){
		TreeNode* t = new TreeNode(root->data);
		t->left = copy_(root->left);
		t->right = copy_(root->right);
	}
	return t;
}

Clear

1
2
3
4
5
6
7
8
template<class T>
void BinaryTree<T>::clear_(TreeNode* root){
	if(root != nullptr){
		clear_(root->left);
		clear_(root->right);
		delete root;
		}
}

Traversal

Depth-First 深度优先遍历

1. 前序遍历(Pre-order Traversal)
先访问根节点,然后递归地前序遍历左子树,最后递归地前序遍历右子树。

2. 中序遍历(In-order Traversal)
先递归地中序遍历左子树,然后访问根节点,最后递归地中序遍历右子树。

3. 后续遍历(Post-order Traversal)
先递归地后序遍历左子树,然后递归地后序遍历右子树,最后访问根节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
template <class T>
void BinaryTree<T>::preOrder(TreeNode *root) {
    if (root != NULL) {
        std::cout << root->data << " "; // 访问根节点
        preOrder(root->left);           // 递归遍历左子树
        preOrder(root->right);          // 递归遍历右子树
    }
}

template <class T>
void BinaryTree<T>::inOrder(TreeNode *root) {
    if (root != NULL) {
        inOrder(root->left);            // 递归遍历左子树
        std::cout << root->data << " "; // 访问根节点
        inOrder(root->right);           // 递归遍历右子树
    }
}

template <class T>
void BinaryTree<T>::postOrder(TreeNode *root) {
    if (root != NULL) {
        postOrder(root->left);           // 递归遍历左子树
        postOrder(root->right);          // 递归遍历右子树
        std::cout << root->data << " "; // 访问根节点
    }
}

Level-order Traversal 层序遍历
  • 按照树的层次,从上到下,从左到右逐层访问节点。

  • 实现方法:通常使用队列(Queue)来实现。

  • 算法步骤

    1. 将根节点放入队列。
    2. 当队列不为空时,执行以下操作: a. 取出队列的头节点并访问。 b. 如果该节点有左子节点,将左子节点放入队列。 c. 如果该节点有右子节点,将右子节点放入队列。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <class T>
void BinaryTree<T>::levelOrder(TreeNode *root) {
    std::queue<TreeNode *> q;
    q.enqueue(root);
    while (!q.isEmpty()) {
        TreeNode *node = q.dequeue();
        if (node != NULL) {
            std::cout << node->data << " ";
            if (node->left)
                q.enqueue(node->left);
            if (node->right)
                q.enqueue(node->right);
        }
    }
}

Breadth First V.S. Depth First

[!tip] Traversal 与 Search
Traversal: 遍历每一个节点一次
Search: 访问每个节点直到我们找到一个或多个满足要求的节点

49f0bf09f4436bd327c21c035b2a368.png

特性 BFS DFS
数据结构 队列 (Queue) 栈 (Stack)
访问顺序 按层级 按深度
空间复杂度 可能更高 (需存储整层顶点) 通常较低
适用场景 寻找最短路径、关系网络分析 拓扑排序、连通性检测
实现难度 稍微复杂 相对简单

Binary Search Tree

Definition

  • 基本定义: BST 是一种特殊的二叉树 (Binary Tree),它或者为空,或者满足以下递归属性:
    • 每个节点包含一个键 (Key) 和关联的值 (Value)。
    • 对于树中的任意节点 r
      • 其左子树 (Left Subtree) TL所有节点的键都 小于 节点 r 的键。
      • 其右子树 (Right Subtree) TR所有节点的键都 大于 节点 r 的键。
      • 其左子树 TL 和右子树 TR 本身也必须是二叉搜索树。
  • 核心目的: 利用这种有序属性,实现高效的查找、插入和删除操作。

性能分析

  • 最佳情况 (Best Case):
    • 当树是平衡 (Balanced) 的时候 (例如,形状接近完美二叉树 Perfect Binary Tree 或完全二叉树 Complete Binary Tree),树的高度 h 大约是 hO(logn),其中 n 是节点数。
    • 查找、插入、删除操作的时间复杂度为 O(logn)
  • 最坏情况 (Worst Case):
    • 当插入的元素是有序的 (或接近有序) 时,BST 会退化成一个链表 (Linked List) 结构。
    • 树的高度 h 变为 O(n)
    • 查找、插入、删除操作的时间复杂度变为 O(n)
  • 结论: BST 的效率高度依赖于其结构是否平衡,即依赖于元素的插入顺序。这也引出了后续更高级的自平衡 BST (如 AVL 树, Red-Black Tree) 的需求。

具体实现

  • 节点结构 (TreeNode): 通常使用一个结构体或类来表示节点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    template <class K, class V>
    struct TreeNode {
        K key;
        V value;
        TreeNode* left;
        TreeNode* right;
    
        // Constructor with initializer list
        TreeNode(const K& newKey, const V& newValue)
            : key(newKey), value(newValue), left(nullptr), right(nullptr) {}
    };
    
    • key, value: 存储键和值。
    • left, right: 指向左右子节点的指针 (Pointer)。使用 nullptr 表示没有子节点。
    • 模板 (Template) (template <class K, class V>): 使 BST 可以存储任意类型的键和值。
  • BST 类骨架:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    template <class K, class V>
    class BST {
    public:
        // Constructor, Destructor, etc.
        BST();
        ~BST(); // Important for memory management!
    
        // Public interface functions
        void insert(const K& key, const V& value);
        V find(const K& key) const;
        void remove(const K& key);
        // ... other functions like traverse ...
    
    private:
        TreeNode<K, V>* root; // Pointer to the root node
    
        // Private recursive helper functions
        // Note the use of TreeNode*& for modification
        TreeNode<K, V>*& find(TreeNode<K, V>*& subroot, const K& key); // Non-const version for internal use
        const TreeNode<K, V>* find(const TreeNode<K, V>* subroot, const K& key) const; // Const version for public find
        void insert(TreeNode<K, V>*& subroot, const K& key, const V& value);
        void remove(TreeNode<K, V>*& subroot, const K& key);
        // ... other helpers like finding successor/predecessor, clear/destroy ...
    
        // Helper for finding inorder successor (example for remove)
        TreeNode<K, V>*& inorderSuccessor(TreeNode<K, V>*& node);
    };
    
    • root: 指向树根节点的私有成员指针。
    • 公共接口 (Public Interface): 提供给用户的简洁函数 (如 insert(key, value))。
    • 私有辅助函数 (Private Helpers): 通常是递归函数,处理实际的树操作。它们经常使用 TreeNode*& 参数来实现对树结构的修改。

BST 操作与 C++ 实现思路

  • 查找 (Find):
    • 逻辑: 从根节点开始,比较目标键 key 与当前节点 subroot->key
      1. 相等: 找到。
      2. key < subroot->key: 递归在左子树 subroot->left 中查找。
      3. key > subroot->key: 递归在右子树 subroot->right 中查找。
      4. subroot == nullptr: 未找到。
    • C++ (Const Helper):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      
      template <class K, class V>
      const TreeNode<K, V>* BST<K, V>::find(const TreeNode<K, V>* subroot, const K& key) const {
          if (subroot == nullptr) {
              return nullptr; // Base case: Not found
          }
          if (key == subroot->key) {
              return subroot; // Base case: Found
          } else if (key < subroot->key) {
              return find(subroot->left, key); // Recurse left
          } else {
              return find(subroot->right, key); // Recurse right
          }
      }
      // Public interface would call this helper starting from root
      // and likely return V (the value) or throw an exception if not found.
      
    • 注意: 用于 insert/remove 的内部 find 变体可能需要返回 TreeNode*& 并且是非 const 的。
  • 插入 (Insert):
    • 逻辑: 类似查找,递归向下直到找到一个 nullptr 的链接 (即合适的叶子位置),然后将新节点连接到该位置。
    • C++ (Helper using TreeNode*&):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      
      template <class K, class V>
      void BST<K, V>::insert(TreeNode<K, V>*& subroot, const K& key, const V& value) {
          if (subroot == nullptr) {
              // Found the insertion spot! Modify the pointer (passed by reference)
              subroot = new TreeNode<K, V>(key, value);
          } else if (key < subroot->key) {
              insert(subroot->left, key, value); // Recurse left
          } else if (key > subroot->key) {
              insert(subroot->right, key, value); // Recurse right
          }
          // else key == subroot->key: Handle duplicate keys (e.g., ignore, update value, throw error)
      }
      // Public interface calls: insert(root, key, value);
      
    • 关键: subroot = new TreeNode(...) 之所以能修改树结构,是因为 subrootTreeNode*& (指针的引用)。
  • 删除 (Remove):
    • 中序前驱 (In-order Predecessor, IOP):
      * 定义: 在中序遍历序列中,紧接在当前节点之前的那个节点。
      * 查找方法: 从当前节点的左子节点开始,一直向走到底。
      * 关键属性: IOP 节点本身最多只有一个左子节点 (绝对没有右子节点)。这保证了递归删除 IOP 时只会遇到 Case 1 或 Case 2。
    • 中序后继 (In-order Successor, IOS): (另一种可选策略)
      • 定义: 在中序遍历序列中,紧接在当前节点之后的那个节点。
      • 查找方法: 从当前节点的右子节点开始,一直向走到底。
      • 关键属性: IOS 节点本身最多只有一个右子节点 (绝对没有左子节点)。
    • 实现选择: 可以选择使用 IOP 或 IOS。下面的代码示例使用了 IOP。
    • 目标: 从 BST 中删除一个键为 key 的节点,并保持 BST 属性。这是 BST 操作中最复杂的一个。
    • 核心挑战: 删除节点后,如何重新连接子树以维持 BST 结构。
    • 类型:1. 无叶子节点 2. 有一个叶子节点 3. 有两个叶子节点
    • C++ 接口与辅助函数:

    ```cpp // Public interface template <class K, class V> void BST<K, V>::remove(const K& key) { remove(root, key); // Call private helper }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    
    // Private recursive helper function (using reference to pointer)
    template <class K, class V>
    void BST<K, V>::remove(TreeNode<K, V>*& subroot, const K& key) {
        if (subroot == nullptr) {
            return; // Key not found, nothing to remove
        }
    
        // Step 1: Find the node to remove
        if (key < subroot->key) {
            remove(subroot->left, key); // Recurse left
        } else if (key > subroot->key) {
            remove(subroot->right, key); // Recurse right
        } else {
            // Step 2: Node found (key == subroot->key). Handle the three cases.
    
            // Case 1: Leaf Node (0 children)
            // "leaf element, no child, very easy"
            if (subroot->left == nullptr && subroot->right == nullptr) {
                delete subroot;
                subroot = nullptr; // The parent's pointer (via reference) is now null
                
            // Case 2: One Child
            // "Remove in a linked list", "one child"
            // Only right child exists
            else if (subroot->left == nullptr) {
                TreeNode<K, V>* temp = subroot; // Store node to delete
                subroot = subroot->right;      // Bypass: parent points to the right child
                delete temp;                   // Delete the original node
            }
            // Only left child exists
            else if (subroot->right == nullptr) {
                TreeNode<K, V>* temp = subroot; // Store node to delete
                subroot = subroot->left;       // Bypass: parent points to the left child
                delete temp;                   // Delete the original node
            }
    
            // Case 3: Two Children
            // "How to arrange the children??" -> Use In-order Predecessor (IOP)
            else {
                // 3a. Find the In-order Predecessor (IOP)
                // IOP: "far right node in the left subtree"
                // Find the pointer *to* the IOP node itself
                TreeNode<K, V>* iopNode = subroot->left; // Start in the left subtree
                while (iopNode->right != nullptr) {
                    iopNode = iopNode->right; // Go as far right as possible
                }
                // Note: Slides 9-11 correctly state IOP has 0 or 1 child (never a right child)
    
                // 3b. Copy IOP's data to the node we intended to remove (Conceptual "Swap")
                // We replace the data in `subroot` with the data from `iopNode`.
                // This preserves the BST structure locally around `subroot`.
                subroot->key = iopNode->key;
                subroot->value = iopNode->value;
    
                // 3c. Recursively remove the original IOP node - Slides 16-18
                // Now, the problem reduces to removing the IOP node (which contains the copied data)
                // from the *left subtree*. Since the IOP has 0 or 1 child,
                // this recursive call will hit Case 1 or Case 2.
                remove(subroot->left, iopNode->key); // Remove the IOP from the left subtree
            }
        }
    }
    ```
    

BST 复杂度分析

  • 基本原则: 所有主要的 BST 操作 (find, insert, delete) 的时间复杂度都依赖于树的高度 (Height) h。复杂度为 O(h)

  • 树的高度与节点数的关系:
    • n 为节点数,h 为树的高度。
    • 最佳情况 (Best Case - Balanced Tree):
      • 树的结构尽可能“茂密”,接近完美二叉树。
      • 节点数 n 最多为 2h+11
      • 反解得到高度 h下界 (Lower bound): hlog2n,即 h=Ω(logn)
    • 最坏情况 (Worst Case - Degenerate Tree):
      • 树退化成链表。
      • 节点数 n 最少可以只构成高度 h=n1 的树 (只有一个节点时 h=0)。
      • 高度 h上界 (Upper bound): hn1,即 h=O(n)
  • 插入顺序的影响:
    • BST 的最终高度严重依赖于键的插入顺序。
      • 例如,按顺序插入 1, 2, 3, 4, 5, 6, 7 会导致 O(n) 的高度。
      • 插入 4, 2, 6, 1, 3, 5, 7 会得到更平衡的树,高度接近 O(logn)
    • 对于 n 个不同的键,存在 n! 种可能的插入顺序。
  • 平均情况 (Average Case - Random Insertions):
    • 如果我们假设所有 n! 种插入顺序是等概率的 (即随机插入),那么 BST 的平均高度 (期望高度) 被证明是 O(logn)
    • 技术比喻: 虽然可能运气很差构造出一条长链,但“平均来说”,随机插入更倾向于产生相对平衡的树。
  • 运行时间总结 (Slides 30-31):
Operation BST Average Case BST Worst Case Sorted Array Sorted List
find O(logn) O(n) O(logn) O(n)
insert O(logn) O(n) O(n) O(n)
delete O(logn) O(n) O(n) O(n)
traverse O(n) O(n) O(n) O(n)
  • 对于 Sorted List:
    • insert/delete 如果给定位置 (例如,你知道要插入/删除的节点的指针/迭代器),操作本身是 O(1) (假设是双向链表)。
    • 但如果需要先查找元素 (Arbitrary insert/delete),查找时间是 O(n),所以整体是 O(n)。
  • Sorted Array 的 insert/delete 是 O(n) 因为需要移动元素来保持有序。find 可以用二分查找 O(logn)。

Rotation

Height-balanced Tree

高度平衡的定义

  • 高度平衡(Height balance):对于每一个节点,其左子树的高度和右子树的高度之差的绝对值不超过 1。
    • 平衡因子(balance factor):b=height(TR)height(TL),其中 TR 是右子树,TL 是左子树。
    • 如果一棵树是高度平衡的,则对于每个节点,1b1
  • 举例:
    • “mountain” 形状的树通常更平衡,而 “stick” 形状的树则不平衡。
    • 这就像在跷跷板上保持平衡,两边的重量要尽可能接近。

Rotation

1. 旋转的目的

  • 保持 BST 的性质:旋转操作不能破坏 BST 的有序性。
  • 调整树的平衡:通过旋转,将 “stick” 形状的树转换为 “mountain” 形状的树,使其更加平衡。

2. 旋转的种类

  • 左旋(Left Rotation):将一个节点的右子节点提升为父节点,原父节点变为左子节点。
  • 右旋(Right Rotation):将一个节点的左子节点提升为父节点,原父节点变为右子节点。

3. 旋转的步骤 (以右旋为例)

  1. 找到不平衡的节点:从底向上找到第一个平衡因子绝对值大于 1 的节点。
  2. 确定旋转类型:根据不平衡节点的子树结构,确定需要进行哪种旋转。

核心旋转操作

我们先定义两个基本的旋转操作:左旋和右旋。

1. 右旋 (Right Rotation)

当节点 z左子树 (y) 比右子树过高,并且不平衡是由插入到 y左子树 (x) 中引起的时 (即 Left-Left Case),通常执行右旋。

目标:将 y 提升为子树的新根,z 成为 y 的右子节点。

图示

1
2
3
4
5
6
7
      z              y
     / \            / \
    y   T4  ===>   x   z      (围绕 z 进行右旋)
   / \           / \   / \
  x   T3        T1 T2  T3 T4
 / \
T1  T2
  • z: 不平衡的节点。
  • y: z 的左子节点。
  • x: y 的左子节点 (导致不平衡的插入路径上的节点)。
  • T1, T2, T3, T4: 代表可能的子树 (可能是 NULL)。

操作步骤

  1. y 的右子树 (T3) 成为 z 的新的左子树。
  2. z 成为 y 的新的右子节点。
  3. y 成为原 z 位置的新根节点。

伪代码/C++ 代码 (假设节点结构包含 left, right, height 成员)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
struct Node {
    int key;
    Node *left;
    Node *right;
    int height;
    // Constructor...
};

// Helper function to get height (handles NULL nodes)
int height(Node* N) {
    if (N == nullptr)
        return -1; // 或者 0, 取决于高度定义 (空树高度为-1 或 叶子节点高度为0)
                   // 这里假设空树高度为 -1, 叶子节点高度为 0
    return N->height;
}

// Helper function to update height of a node
void updateHeight(Node* N) {
    if (N != nullptr) {
        N->height = 1 + std::max(height(N->left), height(N->right));
    }
}

// 执行右旋操作
// 输入: z - 需要围绕其进行右旋的节点 (不平衡节点)
// 返回: 旋转后子树的新根节点 (y)
Node* rightRotate(Node* z) {
    Node* y = z->left;       // y 是 z 的左孩子
    Node* T3 = y->right;     // T3 是 y 的右子树

    // 执行旋转
    y->right = z;            // z 成为 y 的右孩子
    z->left = T3;            // T3 成为 z 的左孩子

    // 更新高度 (必须先更新 z, 再更新 y, 因为 y 的高度依赖于更新后的 z)
    updateHeight(z);
    updateHeight(y);

    // 返回新的根节点
    return y;
}

现实比喻:想象 z 是一个枢轴,你抓住 y 向上推,z 就顺时针旋转下来成为 y 的右边部分,而原来夹在 yz 之间的 T3 就挂在了 z 的左边空位上。

2. 左旋 (Left Rotation)

当节点 z右子树 (y) 比左子树过高,并且不平衡是由插入到 y右子树 (x) 中引起的时 (即 Right-Right Case),通常执行左旋。

目标:将 y 提升为子树的新根,z 成为 y 的左子节点。

图示

1
2
3
4
5
6
7
      z             y
     / \           / \
    T1  y   ===>  z   x       (围绕 z 进行左旋)
       / \       / \ / \
      T2  x     T1 T2 T3 T4
         / \
        T3 T4
  • z: 不平衡的节点。
  • y: z 的右子节点。
  • x: y 的右子节点 (导致不平衡的插入路径上的节点)。
  • T1, T2, T3, T4: 代表可能的子树 (可能是 NULL)。

操作步骤

  1. y 的左子树 (T2) 成为 z 的新的右子树。
  2. z 成为 y 的新的左子节点。
  3. y 成为原 z 位置的新根节点。

伪代码/C++ 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 执行左旋操作
// 输入: z - 需要围绕其进行左旋的节点 (不平衡节点)
// 返回: 旋转后子树的新根节点 (y)
Node* leftRotate(Node* z) {
    Node* y = z->right;      // y 是 z 的右孩子
    Node* T2 = y->left;      // T2 是 y 的左子树

    // 执行旋转
    y->left = z;             // z 成为 y 的左孩子
    z->right = T2;           // T2 成为 z 的右孩子

    // 更新高度 (先更新 z, 再更新 y)
    updateHeight(z);
    updateHeight(y);

    // 返回新的根节点
    return y;
}

现实比喻:与右旋相反,抓住 y 向上推,z 逆时针旋转下来成为 y 的左边部分,原来夹在 zy 之间的 T2 挂在了 z 的右边空位上。

当插入路径形成 “zig-zag” 形状时,需要进行两次旋转。

3. 左右旋 (Left-Right Rotation, LR)

当节点 z左子树 (y) 比右子树过高,但!不平衡是由插入到 y右子树 (x) 中引起的时 (即 Left-Right Case)。形状像一个 “<”。

目标:将 x 提升为子树的新根。

图示

1
2
3
4
5
6
7
      z               z             x
     / \             / \           / \
    y   T4  -(L@y)-> x   T4 -(R@z)-> y   z      (先对 y 左旋, 再对 z 右旋)
   / \             / \         / \ / \
  T1  x           y   T3       T1 T2 T3 T4
     / \         / \
    T2 T3       T1 T2

操作步骤

  1. 先对 z 的左子节点 y 进行一次左旋 (Left Rotation)。这会将 LR 情况转化为 LL 情况。
  2. 再对不平衡节点 z 进行一次右旋 (Right Rotation)

伪代码/C++ 代码

1
2
3
4
5
6
7
8
9
10
// 执行左右旋操作
// 输入: z - 不平衡节点
// 返回: 旋转后子树的新根节点 (x)
Node* leftRightRotate(Node* z) {
    // 1. 对 z 的左孩子 y 进行左旋
    z->left = leftRotate(z->left); // y 被 x 替换, 返回的新根 x 成为 z 的新左孩子

    // 2. 对 z 进行右旋
    return rightRotate(z);         // 返回最终的新根 x
}

现实比喻:想象 < 形状的链条,先把它掰直 (对 y 左旋,变成 / 形状,即 LL),然后再整体旋转 (对 z 右旋) 使其平衡。

4. 右左旋 (Right-Left Rotation, RL)

当节点 z右子树 (y) 比左子树过高,但!不平衡是由插入到 y左子树 (x) 中引起的时 (即 Right-Left Case)。形状像一个 “>”。

目标:将 x 提升为子树的新根。

图示

1
2
3
4
5
6
7
       z              z              x
      / \            / \            / \
     T1  y -(R@y)-> T1  x  -(L@z)-> z   y      (先对 y 右旋, 再对 z 左旋)
        / \            / \        / \ / \
       x   T4         T2  y      T1 T2 T3 T4
      / \                / \
     T2 T3              T3 T4

操作步骤

  1. 先对 z 的右子节点 y 进行一次右旋 (Right Rotation)。这会将 RL 情况转化为 RR 情况。
  2. 再对不平衡节点 z 进行一次左旋 (Left Rotation)

伪代码/C++ 代码

1
2
3
4
5
6
7
8
9
10
// 执行右左旋操作
// 输入: z - 不平衡节点
// 返回: 旋转后子树的新根节点 (x)
Node* rightLeftRotate(Node* z) {
    // 1. 对 z 的右孩子 y 进行右旋
    z->right = rightRotate(z->right); // y 被 x 替换, 返回的新根 x 成为 z 的新右孩子

    // 2. 对 z 进行左旋
    return leftRotate(z);          // 返回最终的新根 x
}

现实比喻:想象 > 形状的链条,先把它掰直 (对 y 右旋,变成 \ 形状,即 RR),然后再整体旋转 (对 z 左旋) 使其平衡。

总结

  • 旋转的目的:在保持 BST 性质的前提下,调整树的局部结构以恢复平衡。
  • 基本操作:左旋和右旋是基础,时间复杂度为 O(1)
  • 四种情况
    • LL (Left-Left): 右旋 z
    • RR (Right-Right): 左旋 z
    • LR (Left-Right): 先左旋 y (即 z->left),再右旋 z
    • RL (Right-Left): 先右旋 y (即 z->right),再左旋 z
  • 关键点
    • 正确识别不平衡节点 z 和导致不平衡的路径 (x, y)。
    • 精确地更新节点的 left, right 指针。
    • 非常重要:在每次旋转后,必须更新受影响节点 (z, y, 可能还有 x) 的高度信息,否则后续的平衡判断会出错。

Dictionary ADT

Dictionary(字典)是一种关联式容器,存储键值对。

主要操作:

  • put(key, value) - 插入/更新键值对
  • get(key) - 根据键检索值
  • remove(key) - 删除键值对
  • containsKey(key) - 检查键是否存在
  • size() - 返回字典中的键值对数量
  • isEmpty() - 检查字典是否为空
  • keys() - 返回所有键的集合