二叉堆
基于堆的排序方式。
- 结构:它是一个完全二叉树,通常用数组来表示。
- 对于数组中下标为
i 的节点:
- 其父节点的下标是
(i-1)/2(向下取整)。
- 其左子节点的下标是
2*i + 1。
- 其右子节点的下标是
2*i + 2。
- 性质:
- 最大堆:每个节点的值都大于或等于其子节点的值。堆顶是最大值。
- 最小堆:每个节点的值都小于或等于其子节点的值。堆顶是最小值。
堆化
对于一个节点,如果它不满足堆的性质(比如它的值小于其某个子节点),就将其 “下沉” ,直到它大于其子节点,或到达叶子节点。
- 过程:
- 在当前节点、其左子节点、其右子节点中,找出值最大的节点。
- 如果最大节点不是当前节点,则交换当前节点和这个最大节点。
- 递归或迭代地对被交换下去的那个子节点位置继续执行堆化操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| void heapify(vector<int> &arr, int n, int i){ int largest_index = i; int left_index = 2*i + 1; int right_index = 2*i + 2; if(left_index < n && arr[left_index] > arr[largest_index]){ largest_index = left_index; } if(right_index < n && arr[right_index] > arr[largest_index]){ largest_index = right_index; } if(largest_index != i){ swap(arr[i], arr[largest_index]); heapify(arr, n, largest_index); } }
|
建堆
需要对半数节点堆化。
1 2 3
| for(int i = n/2-1; i >= 0; --i){ heapify(arr, n, i); }
|
二叉查找树 (BST)
一个类似二分查找的结构。核心是左<中<右。
注意:左边所有节点都小于root,右边所有节点都大于root。
构建
由一个数组建立一个二叉搜索树,首先要注意的是根节点也有数值。二叉树的定义仍遵循LeetCode的经典定义
1 2 3 4 5 6 7 8
| struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode() : val(0), left(nullptr), right(nullptr) {} TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} };
|
并定义一个数组 arr 。
选出一个数,插入树中,那么它有3种情况:成为根节点、插入左子树、插入右子树。这就得到了一个递推关系,以此实现二叉树的构建/插入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| void insert(TreeNode *root, int val){ if(!root){ root = new TreeNode(val); return; } if(val < root->val){ if(root->left){ insert(root->left, val); }else{ root->left = new TreeNode(val); } }else{ if(root->right){ insert(root->right, val); }else{ root->right = new TreeNode(val); } } }
|
或者更好的函数式解法:
1 2 3 4 5 6 7 8 9 10 11
| TreeNode* insert(TreeNode *root, int val) { if (!root) { return new TreeNode(val); } if (val < root->val) { root->left = insert(root->left, val); } else { root->right = insert(root->right, val); } return root; }
|
查找
理解3个状态即可:找到,在左边找,在右边找。
1 2 3 4 5 6
| TreeNode *query(TreeNode *root, int key){ if(!root || root->val == key){ return root; } return (root->val < key) ? query(root->right, key) : query(root->left, key); }
|
删除
删除操作需要仔细理解。对于叶子节点直接变空即可,但接下来需要考虑存在子树、存在父节点的情况。
特别考虑左右子树都存在的情况,假如此时 del 函数已经执行到将被删除的节点,那么:
- 将当前节点的值改为右子树中的最小值
- 删除右子树中的那个节点
- 左子树保持不变
特别注意删除节点需要的 delete 操作,否则节点内存仍存在。
代码实现:
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
| TreeNode* del(TreeNode *root, int val){ if(!root){ return nullptr; } if(val < root->val){ root->left = del(root->left, val); delete }else if(val > root->val){ root->right = del(root->right, val); }else{ if(!root->left){ TreeNode *tmp = root->left; delete root; return tmp; }else if(!root->right){ TreeNode *tmp = root->right; delete root; return tmp; }else{ TreeNode *succ = root->right; while(succ->left){ succ = succ->left; } root->val = succ->val; root->right = del(root->right, succ->val); } } return root; }
|
平衡二叉树 (AVL)
上述的BST存在一个极端情况:一条链,这可能由于数组读取顺序所导致。
平衡查找树确保子树尽量平均。
性质
- 左子树和右子树深度之差的绝对值不大于1
- 左子树和右子树也都是平衡二叉树
- 平衡因子(Balance Factor) :二叉树上结点的左子树的深度减去其右子树深度称为该结点的平衡因子
判定
直接翻译条件得到。
1 2 3 4 5 6 7 8 9 10 11 12 13
| int height(TreeNode *root){ if(!root){ return 0; } return max(height(root->left), height(root->right)) + 1; }
bool isBalanced(TreeNode *root){ if(!root){ return true; } return isBalanced(root->left) && isBalanced(root->right) && abs(height(root->left) - height(root->right)) <= 1; }
|
BST 平衡化
四种不平衡情况(假设节点 A 是最低不平衡节点):
- LL 型:A 的左子树的左子树插入导致
- RR 型:A 的右子树的右子树插入导致
- LR 型:A 的左子树的右子树插入导致
- RL 型:A 的右子树的左子树插入导致
判断四种情况,可使用balance factor。
LL型 :左子树的左子树插入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| TreeNode *right_rotate(TreeNode *root){ TreeNode *left_child = root->left; root->left = left_child->right; left_child->right = root; left_child->h = max(height(left_child->left), height(left_child->right)) + 1; root->h = max(height(root->left), height(root->right)) + 1;
return left_child; }
|
RR型:右子树的右子树插入
1 2 3 4 5 6 7 8 9 10 11
| TreeNode *left_rotate(TreeNode *root){ TreeNode *right_child = root->right; root->right = right_child->left; right_child->left = root; right_child->h = max(height(right_child->left), height(right_child->right)) + 1; root->h = max(height(root->left), height(root->right)) + 1;
return right_child; }
|
LR型: 左子树的右子树插入
1 2 3 4 5 6 7
| TreeNode *left_right_rotate(TreeNode *root){ root->left = left_rotate(root->left); return right_rotate(root); }
|
RL型:右子树的左子树插入
1 2 3 4 5
| TreeNode *right_left_rotate(TreeNode *root){ root->right = right_rotate(root->right); return left_rotate(root); }
|
实现插入
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
| TreeNode *insert(TreeNode *root, int key){ if(!root){ return new TreeNode(key); } if(key < root->val){ root->left = insert(root->left, key); }else if(key > root->val){ root->right = insert(root->right, key); }else{ return root; } root->h = max(height(root->left), height(root->right)) + 1; int balance_factor = height(root->left) - height(root->right);
if(balance_factor > 1 && key < root->left->val){ return right_rotate(root); } if(balance_factor < -1 && key > root->right->val){ return left_rotate(root); } if(balance_factor > 1 && key > root->left->val){ return left_right_rotate(root); } if(balance_factor < -1 && key < root->right->val){ return right_left_rotate(root); } return root; }
|
多路平衡查找树 (B-树)
主要用于文件系统中,在B-树中,每个结点的大小为一个磁盘页,结点中所包含的关键字及其孩子的数目取决于页的大小。
性质
m 阶 B-树 满足:
(1) 每个结点最多有 m 棵子树(即最多 m-1 个关键字)。
(2) 根结点如果不是叶子,则至少有 2 棵子树(即至少 1 个关键字)。
(3) 非根非叶结点至少有 ⌈m/2⌉ 棵子树,即至少 ⌈m/2⌉ - 1 个关键字。
(4) 结点的结构:
1
| (n, A₀, K₁, A₁, K₂, A₂, …, Kₙ, Aₙ)
|
其中:
K₁ < K₂ < … < Kₙ
A₀ 指向关键字都小于 K₁ 的子树
Aᵢ 指向关键字介于 Kᵢ 与 Kᵢ₊₁ 之间的子树
Aₙ 指向关键字大于 Kₙ 的子树
⌈m/2⌉ - 1 ≤ n ≤ m - 1
(5) 所有叶子结点在同一层,不含信息(B-树中叶子不含关键字,只作为查找失败的出口,实际存储时可能存放指向实际数据的指针或为空)。
称节点的值是关键字。
定义
红黑树
待补充
线段树
常用的区间查询与更新数据结构,尤其适合处理区间最值、区间和、区间覆盖、区间修改等动态问题。
结构
Huffman 树
用于数据压缩的数据结构。
编码过程
考虑一个字符串 s ,统计每个字符的词频,根据词频建立二叉树,得到 Huffman 编码。
贪心 : 使词频最高 的元素,编码最短,也就是使词频低的元素编码尽可能长。
路径 : Huffman 树产生 Huffman 编码,而编码依赖路径。定义 左节点出发,表示0;右节点出发,表示1 ,记录经过的路径,并将路径以 比特流 存储。
建树过程 : 根据以上论述可知,频次低的元素在树底,频次越高越靠近树根。
编码实现
- 利用 map 存储字符-频次对。
- 利用 pq 创建树节点 并自动排序:让频次低的在堆顶
- 每次取两个Node, 连上parent,parent的频次等于二者之和,再将parent push至pq 。
- 重复。
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
| struct TreeNode { char ch; int freq; TreeNode *left; TreeNode *right; TreeNode() : ch(0), freq(0), left(nullptr), right(nullptr){}; TreeNode(char c, int f) : ch(c), freq(f), left(nullptr), right(nullptr) {} TreeNode(char c, int f, TreeNode *left, TreeNode *right) : ch(c), freq(f), left(left), right(right) {} bool operator>(const TreeNode &other) const { return freq > other.freq; } };
string s; map<char, int> map; map<char, string> hCode; priority_queue<TreeNode*, vector<TreeNode*>, greater<TreeNode*>> pq;
for(char c : s){ freq[c]++; } for(auto it : freq){ pq.push(new TreeNode(it.first, it.second)); }
while(pq.size() > 1){ auto left = pq.top(); pq.pop(); auto right = pq.top(); pq.pop(); auto parent = new TreeNode(0, left->freq + right->freq, left, right); pq.push(parent); } auto root = pq.top(); pq.pop();
void dfs(TreeNode *root, map<char, string> &hCode, string code){ if(!root){ return; } if(!root->left && !root->right){ hCode[root->ch] = code; } dfs(root->left, hCode, code + "0"); dfs(root->right, hCode, code + "1"); }
string bits; for(auto ch: s){ bits += hCode[ch]; }
|
解码过程与实现
得到的二进制比特流是“导航”,指示一个树上的指针某一时刻要往哪里走。当走到底,就取出字符,并重新指向root。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| TreeNode *ptr = root; string res; for(auto it: bits){ if(it == '0'){ ptr = ptr->left; }else{ ptr = ptr->right; } if(ptr->ch != 0){ res += ptr->ch; ptr = root; } }
|
前缀树(字典树)
待补充