#CS225
定义: 树是一种 非线性 (non-linear) 数据结构,由 节点 (node) 和 边 (edge) 组成。
关键特性:
术语:
定义: 二叉树 (Binary Tree) 是一种特殊的树,其中每个节点最多有两个子节点,分别称为 左子节点 (left child) 和 右子节点 (right child)。
性质:
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
定义:
递归定义:

2. Full Tree
每个节点要么没有子节点,要么有两个子节点
3. Perfect Tree
所有内部节点都有两个子节点,并且所有叶子节点都在同一层
高度为 h 的完美二叉树的节点数量: $P_{h} = 2^{h+1}-1$
递归定义:
4. Complete Tree
除了最后一层,其他每一层都是满的,并且最后一层的节点都尽可能地靠左排列。
[!warning] 注意
- 满二叉树不一定是完全二叉树
- 完全二叉树不一定是满二叉树
5. NULL 的数量
NULL 的数量为当前节点数量 +1(可以利用数学归纳法证明)
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;
}
}
}
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;
}
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;
}
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;
}
}
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 << " "; // 访问根节点
}
}
按照树的层次,从上到下,从左到右逐层访问节点。
实现方法:通常使用队列(Queue)来实现。
算法步骤:
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);
}
}
}
[!tip] Traversal 与 Search
Traversal: 遍历每一个节点一次
Search: 访问每个节点直到我们找到一个或多个满足要求的节点

| 特性 | BFS | DFS |
|---|---|---|
| 数据结构 | 队列 (Queue) | 栈 (Stack) |
| 访问顺序 | 按层级 | 按深度 |
| 空间复杂度 | 可能更高 (需存储整层顶点) | 通常较低 |
| 适用场景 | 寻找最短路径、关系网络分析 | 拓扑排序、连通性检测 |
| 实现难度 | 稍微复杂 | 相对简单 |
r:
TL 中所有节点的键都 小于 节点 r 的键。TR 中所有节点的键都 大于 节点 r 的键。TL 和右子树 TR 本身也必须是二叉搜索树。h 大约是 $h \approx O(\log n)$,其中 $n$ 是节点数。h 变为 $O(n)$。节点结构 (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 <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: 指向树根节点的私有成员指针。insert(key, value))。TreeNode*& 参数来实现对树结构的修改。key 与当前节点 subroot->key:
key < subroot->key: 递归在左子树 subroot->left 中查找。key > subroot->key: 递归在右子树 subroot->right 中查找。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 的。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(...) 之所以能修改树结构,是因为 subroot 是 TreeNode*& (指针的引用)。key 的节点,并保持 BST 属性。这是 BST 操作中最复杂的一个。```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 操作 (find, insert, delete) 的时间复杂度都依赖于树的高度 (Height) h。复杂度为 $O(h)$。
n 为节点数,h 为树的高度。1, 2, 3, 4, 5, 6, 7 会导致 $O(n)$ 的高度。4, 2, 6, 1, 3, 5, 7 会得到更平衡的树,高度接近 $O(\log n)$。n 个不同的键,存在 $n!$ 种可能的插入顺序。| Operation | BST Average Case | BST Worst Case | Sorted Array | Sorted List |
|---|---|---|---|---|
find |
$O(\log n)$ | $O(n)$ | $O(\log n)$ | $O(n)$ |
insert |
$O(\log n)$ | $O(n)$ | $O(n)$ | $O(n)$ |
delete |
$O(\log n)$ | $O(n)$ | $O(n)$ | $O(n)$ |
traverse |
$O(n)$ | $O(n)$ | $O(n)$ | $O(n)$ |
insert/delete 如果给定位置 (例如,你知道要插入/删除的节点的指针/迭代器),操作本身是 O(1) (假设是双向链表)。Arbitrary insert/delete),查找时间是 O(n),所以整体是 O(n)。insert/delete 是 O(n) 因为需要移动元素来保持有序。find 可以用二分查找 O(logn)。高度平衡的定义
1. 旋转的目的
2. 旋转的种类
3. 旋转的步骤 (以右旋为例)
我们先定义两个基本的旋转操作:左旋和右旋。
当节点 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)。操作步骤:
y 的右子树 (T3) 成为 z 的新的左子树。z 成为 y 的新的右子节点。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 的右边部分,而原来夹在 y 和 z 之间的 T3 就挂在了 z 的左边空位上。
当节点 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)。操作步骤:
y 的左子树 (T2) 成为 z 的新的右子树。z 成为 y 的新的左子节点。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 的左边部分,原来夹在 z 和 y 之间的 T2 挂在了 z 的右边空位上。
当插入路径形成 “zig-zag” 形状时,需要进行两次旋转。
当节点 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
操作步骤:
z 的左子节点 y 进行一次左旋 (Left Rotation)。这会将 LR 情况转化为 LL 情况。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 右旋) 使其平衡。
当节点 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
操作步骤:
z 的右子节点 y 进行一次右旋 (Right Rotation)。这会将 RL 情况转化为 RR 情况。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 左旋) 使其平衡。
z。z。y (即 z->left),再右旋 z。y (即 z->right),再左旋 z。z 和导致不平衡的路径 (x, y)。left, right 指针。z, y, 可能还有 x) 的高度信息,否则后续的平衡判断会出错。Dictionary(字典)是一种关联式容器,存储键值对。
put(key, value) - 插入/更新键值对get(key) - 根据键检索值remove(key) - 删除键值对containsKey(key) - 检查键是否存在size() - 返回字典中的键值对数量isEmpty() - 检查字典是否为空keys() - 返回所有键的集合