数据结构系列3——树与二叉树
树与二叉树
树
概念
- 结点的深度是从根结点开始自顶向下逐层累加的,结点的高度是从根结点开始自底向上逐层累加的
- 结点的度是该结点孩子个数,树的度是结点的最大度数
- 路径长度是路径上所经过的边的个数,路径是有序的(从双亲指向孩子)
性质
- 结点数 = 度数和 + 1 (根结点没有父结点)
- 度为m的树中第i层上至多有\(m^{i - 1}\)个结点(\(i \ge 1\))
- 高度为h的m叉树至多有\(\frac{m^{h} - 1}{m - 1}\)个结点
\[等比数列求和:\frac{a_1(1 - q^n)}{1 - q} \\
因此结点数最多为: \frac{m^0(1 - m^h)}{1- m} = \frac{m^h - 1}{m - 1}
\]
- 具有n个结点的m叉树的最小高度为\(\lceil \log_m(n(m - 1) + 1) \rceil\)
\[\frac{m^{h - 1} - 1}{m - 1} < n \le \frac{m^h - 1}{m - 1} \\
m^{h - 1} < n(m - 1) + 1 \le m^h \\
h - 1 < log_m(n(m - 1) + 1) \le h \\
(h \ge log_m(n(m - 1) + 1)) \\
h_{min} = \lceil log_m(n(m - 1) + 1) \rceil
\]
- 结点数与度之间的关系
- 总结点数 = $ n_0 + n_1 + n_2 + \cdots + n_m $
- 总分支数 = $ 1n_1 + 2n_2 + \cdots + mn_m $
- 总结点数 = 总分支数 + 1
二叉树
概念
- 二叉树为有序树,左右子树有区别
- 二叉树可以为空,度为2的树至少有三个结点(其中至少有一个结点有两个孩子)
- 满二叉树,高度为h,且含有$ 2^h - 1$个结点的二叉树
- 对于编号为i的结点,若有双亲,为$ \lfloor i / 2 \rfloor \(,若有左孩子,为\) 2i \(,若有右孩子,为\) 2i + 1 $
- 完全二叉树,高度为h,有n个结点的二叉树,每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应
- 若$ i \le \lfloor n / 2 \rfloor $,则结点i为分支结点,否则为叶子结点
- 若某结点i只有左孩子没有右孩子,则编号大于i的结点均为叶子结点
- 若n为奇数,则每个分支结点都有左孩子和右孩子,若n为偶数,则编号最大的分支结点只有左孩子(编号为$ n / 2 $)
性质
- 非空二叉树上的叶子结点数等于度为2的结点数加1,即$ n_0 = n_2 + 1$(用结点数 = 度数和 + 1证明)
- 非空二叉树上第k层至多有$ 2^{k - 1} (k \ge 1)$个结点
- 高度为h的二叉树至多有$ 2^h - 1 $个结点(等比数列求和)
- 完全二叉树,从上到下,从左到右编号为$ 1, 2, \ldots , n $
- $ i > 1 \(,双亲结点\) \lfloor i / 2 \rfloor $
- 左孩子$ 2i \(, 右孩子\) 2i + 1 $
- 结点i所在的层次(深度)为$ \lfloor log_2i \rfloor + 1 $
- 具有n个结点的完全二叉树高度为 $ \lceil log_2(n + 1) \rceil $ 或 $ \lfloor log_2n \rfloor + 1 $
\[设高度为h,则有2^{h - 1} - 1 < n \le 2^h - 1 或 h^{h - 1} \le n < 2^h \\ 得2^{h - 1} < n + 1 \le h,h为正整数,则h = \lceil log_2(n + 1) \rceil \\ 或得 h - 1 \le log_2n < h,则h = \lfloor log_2n \rfloor + 1\\ (由此可以计算结点i的深度) \]- 完全二叉树的构成,结点数为奇数,只含有度为0和2的结点,结点数为偶数,含有一个度为1的结点和若干度为0和2的结点
- 含有n个结点的二叉链表中,含有n + 1个空链域
有2n个指针,只有n — 1条边,因此有n + 1个空链域
表示
- 顺序存储,用连续的地址存放,用0占位

- 完全二叉树的顺序存储,下标从1开始,详见堆
- 链式存储结构
struct node {
int val;
node *left, *right;
};
基本操作
- 先序遍历
void preOrder(node* root) {
if (root == nullptr) return;
visit(root);
preOrder(root->left);
preOrder(root->right);
}
- 中序便利
void inOrder(node* root) {
if (root == nullptr) return;
inOrder(root->left);
visit(root);
inOrder(root->right);
}
- 后序遍历
void postOrder(node* root) {
if (root == nullptr) return;
postOrder(root->left);
postOrder(root->right);
visit(root);
}
- 先序遍历(非递归算法)教程
void preOrder(node* root) {
stack<node*> stk;
while (root != nullptr || !stk.empty()) {
while (root != nullptr) {
visit(root);
stk.push(root);
root = root->left;
}
root = stk.top();
stk.pop();
root = root->right;
}
}
- 中序便利(非递归算法)教程
void inOrder(node* root) {
stack<node*> stk;
while (root != nullptr || !stk.empty()) {
while (root != nullptr) {
stk.push(root);
root = root->left;
}
root = stk.top();
stk.pop();
visit(root);
root = root->right;
}
}
- 后序遍历(非递归算法)教程
void postOrder(node* root) {
stack<node*> stk;
node* pre; // 用来记录上一个出栈的元素
while (root != nullptr || !stk.empty()) {
while (root != nullptr) {
stk.push(root);
root = root->left;
}
root = stk.top();
stk.pop();
// 栈顶元素出栈的前提,右子树为空或者刚刚被访问
if (root->right == nullptr || root->right == pre) {
visit(root);
pre = root;
root = nullptr; // 置空,下一轮继续取栈顶元素
} else {
stk.push(root); // 未满足出栈条件,不能出栈
root = root->right; // 下一轮模拟递归访问右子树
}
}
}
- 层次遍历
void levelOrder(node* root) {
queue<node*> q;
q.push(root);
while (!q.empty()) {
node* cur = q.front();
q.pop();
visit(cur);
if (root->left != nullptr) q.push(root->left);
if (root=>right != nullptr) q.push(root->right);
}
}
- 层次遍历(控制到层)
vector<vector<int>> levelOrder(node* root) {
vector<vector<int>> a;
queue<node*> q;
q.push(root);
while (!q.empty()) {
vector<int> temp;
int cnt = q.size();
while (cnt--) {
node* cur = q.front();
temp.push_back(cur->val); // visit(cur)
q.pop();
if (cur->left != nullptr) q.push(cur->left);
if (cur->right != nullptr) q.push(cur->right);
}
a.push_back(temp);
}
return a;
}
线索二叉树
表示
struct node {
int val;
node *left, *right;
int ltag, rtag; // 0表示左右孩子,1表示前驱后继
};
基本操作
- 构造中序线索二叉树
void inThread(threadNode* &root, threadNode* &pre) {
if (root == nullptr) return;
inThread(root->left, pre); // 此时的pre是原来的,不是root,理解中序遍历左孩子的结点的前驱
if (root->left == nullptr) {
root->left = pre;
root->ltag = 1;
}
if (pre != nullptr && pre->right == nullptr) {
pre->right = root;
pre->rtag = 1;
}
pre = root;
inThread(root->right, pre);
}
void createInThread(threadNode* root) {
threadNode* pre = nullptr;
if (root != nullptr) {
inThread(root, pre);
pre->right = nullptr;
pre->rtag = 1;
}
}
- 遍历中序线索二叉树
threadNode* firstNode(threadNode* p) {
while (p->ltag == 0) p = p->left;
return p;
}
threadNode* nextNode(threadNode* p) {
if (p->rtag == 0) return firstNode(p->right);
else return p->right;
}
void inOrder(threadNode* root) {
threadNode* cur = firstNode(root);
while (cur) {
visit(cur);
cur = nextNode(cur);
}
}
- 构造先序线索二叉树
void preThread(threadNode* &root, threadNode* &pre) {
if (root == nullptr) return;
if (root->left == nullptr) {
root->left = pre;
root->ltag = 1;
}
if (pre != nullptr && pre->right == nullptr) {
pre->right = root;
pre->rtag = 1;
}
pre = root;
// 在遍历前前后线索就已经修改,在遍历前保证是左右孩子
if (root->ltag == 0) preThread(root->left, pre);
if (root->rtag == 0) preThread(root->right, pre);
}
void createPreThread(threadNode* root) {
threadNode* pre = nullptr;
if (root != nullptr) {
preThread(root, pre);
pre->right = nullptr;
pre->rtag = 1;
}
}
- 遍历先序线索二叉树
threadNode* nextNode(threadNode* p) {
if (p->ltag == 0) return p->left;
if (p->rtag == 0) return p->right;
else return p->right;
}
void preOrder(threadNode* root) {
threadNode* cur = root;
while (cur) {
visit(cur);
cur = nextNode(cur);
}
}
树和森林
表示
- 双亲表示法,用一组连续的空间来存储每个节点,每个节点除了存储数据外,另设一指针(数组下标)指向其双亲结点位置,根节点下标为0,其指针指向-1
二叉树表的顺序存储中数组下标还表示了结点的位置关系,二叉树也是树,因此二叉树都可以用树的存储结构来存储,但树却不可以用二叉树的存储结构来存储

const int MAXN = 100;
struct node {
int val;
int parent;
};
struct PTree[
node Tree[MAXN];
int n;// 结点数
]
- 孩子表示法,将每个节点的孩子用单链表连接起来形成一个线性结构
- 孩子兄弟表示法,包含三部分,指向结点第一个孩子的指针,指向结点下一个兄弟结点的指针

struct node {
int val;
node *firstChild, *nextsibling;
};
树、森林与二叉树的转换
树和二叉树
- 左指针指向它的第一个孩子,右指针指向它在树中的相邻的右兄弟(参考孩子兄弟表示法),左孩子右兄弟
- 由于根节点没有兄弟,因此根节点没有右子树

森林和二叉树
- 类似树与二叉树,先将每棵树转换为二叉树,然后将二叉树连在根节点的右子树上
树和森林的遍历
| 树 | 森林 | 二叉树 |
|---|---|---|
| 先根遍历 | 先序遍历 | 先序遍历 |
| 后根遍历 | 中序遍历 | 中序遍历 |
- 树的遍历
- 先根遍历,先访问根节点,再依次遍历根节点的没棵子树,遍历序列与这棵树对应的先序序列相同
- 后根遍历,先依次遍历根节点的每棵子树,再访问根节点,遍历序列与这棵树相应二叉树的中序序列相同
- 森林的遍历,依次遍历各树,遍历逻辑与树的遍历相同
并查集
表示
- 用连续地址存放,指向父结点
const int N = 100;
int father[N];
基本操作
- 初始化
for (int i = 1; i <= N; i++) {
father[i] = i;
}
- 查找
void findFather(int x) {
int a = x;
while (x != father[x]) x = father[x];
while (a != father[a]) {
int z = a;
a = father[a];
father[z] = x;
}
return x;
}
- 合并
void Union(int a, int b) {
int faA = findFather(a);
int faB = findFather(b);
if (faA != faB) father[faA] = faB;
}
二叉查找树(BST)
概念
- 要么是一棵空树
- 要么二叉查找树由根节点、左子树、右子树组成,其中左子树和右子树都是二叉查找树(左右子树可能为空),且左子树上所有节点的数据域均小于或等于根结点的数据域,右子树上所有结点的数据域均大于根节点的数据域
性质
- 最好的平均查找长度(AVL)为$ O(log_2n) \(,最坏平均查找长度为\) O(n) $
- 与二分查找相比,二分查找平均执行时间为$ O(log_2n) \(,但是其删除和插入结点的代价为\) O(n) $,因此当有序表是静态时,使用顺序表,采用二分查找实现查找操作,当有序表时动态时,使用二叉搜索树
- 平均查找长度(ASL)的计算

基本操作
- 查找
void search(node* root, int x) {
if (root == nullptr) {
cout << "failed" << endl;
return;
}
if (x == root->val) cout << root->val << endl;
else if (x < root->val) search(root->left, x);
else search(root->right, x);
}
- 插入
// 注意root是引用,要修改root的值
void insert(root* &root, int x) {
if (root == nullptr) {
root = new node(x);
return;
}
if (x == root->val) return;
else if (x < root->val) insert(root->left, x);
else insert(root->right, x);
}
- 建立
node* createTree(vector<int> a) {
node* root = nullptr;
for (auto v : a) insert(root, v);
return root;
}
- 删除
- 基本思想,找左子树最大的或者右子树最小的覆盖根节点,然后递归删除左子树或右子树中选出的结点
node* findMax(node* root) {
while (root->right != nullptr) root = root->right;
return root;
}
node* findMin(node* root) {
while (root->left != nullptr) root = root->left;
return root;
}
void deleteNode(node* &root, int x) {
if (root == nullptr) return; // 当前root为空,不存在权值为x的结点,直接返回
if (root->val == x) { // 找到要删除的结点,进入删除处理
if (root->left == nullptr && root->right == nullptr) {
delete(root);
root = nullptr; // case1: 为叶子结点,直接删除
}
else if (root->left != nullptr) { // case2: 存在左孩子,找前驱pre,让前驱覆盖root,左子树中递归删除pre
node* pre = findMin(root->left);
root->val = pre->val;
deleteNode(root->left, pre->val);
} else { // case3: 不存在左子树,又不为叶子结点,那么存在右子树,找后继next,让后继覆盖root,右子树中递归删除next
node* next = findMax(root->right);
root->val = next->val;
deleteNode(root->right, next->val);
}
} else if (x < root->val) deleteNode(root->left, x); // 左子树中查找root
else deleteNode(root->right, x); // 右子树中查找root
}
平衡二叉树(AVL树)
概念
- 左子树与右子树的高度之差的绝对值不超过1,其中左子树与右子树的高度只差为该节点的平衡因子
性质
- 平均查找长度为$ O(log_2n) $
- 平衡二叉树结点数的递推公式,$ n_h $为构造此高度的平衡二叉树所需要的最少结点数,关于该公式,递归着理解,假设根节点在n层,为了结点数最少,左子树为n - 1,右子树为n - 2,再递归地推左右子树,得到该递推公式,即根节点加左子树加右子树
\[n_0 = 0, n_1 = 1, n_2 = 2, n_h = 1 + n_{h - 1} + n_{h - 2}
\]
表示
struct node {
int val, height; // 结点的初始高度为1
node *left, *right;
node() {} // 重载,不然不能初始化空结点
node(int _val) : val(_val), height(1), left(nullptr), right(nullptr) {}
};
基本操作
- 高度
int getHeight(node* root) {
return root == nullptr ? 0 : root->height;
}
- 平衡因子
int getBalanceFactor(node* root) {
return getHeight(root->left) - getHeight(root->right);
}
- 更新树高
void updateHeight(node* root) {
root->height = max(getHeight(root->left), getHeight(root->right)) + 1;
}
- 查找(同BST)
void search(node* root, int x) {
if (root == nullptr) {
cout << "failed" << endl;
return;
}
if (x == root->val) cout << root->val << endl;
else if (x < root->val) search(root->left, x);
else search(root->right, x);
}
- 左旋

void L(node* &root) {
node* temp = root->right;
root->right = temp->left;
temp->left = root;
updateHeight(root);
updateHeight(temp);
root = temp;
}
- 右旋

void R(node* &root) {
node* temp = root->left;
root->left = temp->right;
temp->right = root;
updateHeight(root);
updateHeight(temp);
root = temp;
}
- 插入,在插入中可能导致失衡的情况分为下面四种(注意:在旋转的过程中可能出现新的不平衡的树,只要最后的树平衡即可,在递归返回时,先遇到靠近叶子结点的结点,因此调整时优先调整底部的失衡节点)


| 树形 | 判断方法 | 调整 |
|---|---|---|
| LL | BF(root) == 2 && BF(root->left) == 1 |
R(root) |
| LR | BF(root) == 2 && BF(root->left) == -1 |
L(root->left);R(root) |
| RR | BF(root) == -2 && BF(root->right) == -1 |
L(root) |
| RL | BF(root) == -2 && BF(root-right) == 1 |
R(root->right);L(root) |
void insert(node* &root, int x) {
if (root == nullptr) {
root = new node(x);
return;
}
// 递归出栈是自底向上,因此遇到的最接近的失衡结点(叶子处)的第一个结点就开始调整了,只要把最靠近失衡节点的结点进行调整,路径上的所有结点就会平衡(实际上继续出栈时仍检查了其它结点)
if (x < root->val) {
insert(root->left, x);
updateHeight(root);
if (getBalanceFactor(root) == 2) {
if (getBalanceFactor(root->left) == 1) { // LL
R(root);
} else if (getBalanceFactor(root->left) == -1) { // LR
L(root->left);
R(root);
}
}
} else {
insert(root->right, x);
updateHeight(root);
if (getBalanceFactor(root) == -2) {
if (getBalanceFactor(root->right) == -1) { // RR
L(root);
} else if (getBalanceFactor(root->right) == 1) { // RL
R(root->right);
L(root);
}
}
}
}
- 建树
node* createTree(vector<int> a) {
node* root = nullptr;
for (auto v : a) insert(root, v);
return root;
}
堆
概念
- 堆是一棵完全二叉树,树中每个结点的值都不小于(或大于)其左右孩子的值(大顶堆和小顶堆)
- 每个结点的值都是以它为根节点的子树的最小值,堆一般用于实现优先队列
表示
const int MAXN = 100;
// 下标从1开始,堆大小为n
int heap[MAXN], n = 10;
基本操作
以大顶堆为例
- 向下调整(时间复杂度:$ O(log_2n) $,与左右孩子比较)
void downAdjust(int low, int high) {
int i = low, j = i * 2;
while (j <= high) {
if (j + 1 <= high && heap[j + 1] > heap[j]) {
j = j + 1;
}
if (heap[j] > heap[i]) {
swap(heap[j], heap[i]);
i = j;
j = i * 2;
} else break;
}
}
- 建堆,$ [1, \lfloor n / 2 \rfloor] \(都是非叶子结点,于是可以从\) \lfloor n / 2 \rfloor \(位置开始倒着枚举结点,每次遍历结点从\) [i, n] $范围内的调整,倒着枚举,可以保证每个节点都是以其为根节点的子树中的最大的结点
void createHeap() {
for (int i = n / 2; i >= 1; i--) downAdjust(i, n);
}
- 删除堆顶的最大元素
void deleteTop() {
heap[1] = heap[n--];
downAdjust(1, n);
}
- 向上调整(时间复杂度:$ O(log_2n) $,与父节点比较)
void upAdjust(int low, int high) {
int i = high, j = i / 2;
while (j >= low) {
if (heap[j] < heap[i]) {
swap(heap[j], heap[i]);
i = j;
j = i / 2;
} else break;
}
}
- 插入元素,插入最后一个节点的后面再向上调整
void insert(int x) {
heap[++n] = x;
upAdjust(1, n);
}
- 堆排序,取出堆顶元素与最后一个元素交换,然后进行一次针对堆顶元素的向下调整,如此重复,直到堆顶元素中只剩一个元素(大顶堆排升序,小顶堆排降序)
void heapSort() {
createHeap();
for (int i = n; i > 1; i--) {
swap(heap[i], heap[1]);
downAdjust(1, i - 1);
}
}
哈夫曼树
概念
- 带权路径长度(WPL):从树的根到任意结点的路径长度(经过的边数)与该结点上的权值的乘积,注意区分WPL和构建的树的根节点的值,根节点的值不等于WPL,WPL要每一层每一层的算
\[WPL = \sum_{i - 1}^{n}w_il_i
\]
- 哈夫曼树:带权路径长度最小的二叉树,也称最优二叉树,注意哈夫曼树的树形不一定只有一种结构,但哈夫曼树没有度为1的结点

- 哈夫曼编码
- 固定长度编码:每个字符都用相等长度的二进制位表示,可变长度编码,允许对不同字符用不等长的二进制位表示
- 对频率高的字符赋以短编码,对频率低的字符赋以长编码,起到压缩数据的效果
- 前缀编码,没有一个编码是另一个编码的前缀,码串可以被唯一的翻译
- 解码,构造哈夫曼树后,左子树的边为0,右子树的边为1,向下一直到叶节点,剩下的码串继续该过程
性质
- 每个初始结点最终都成了叶节点,且权值越小的结点到根节点的路径长度越大
- 构造过程中新建了n - 1个结点(两两相连,连了n - 1次),因此哈夫曼树的结点总数为2n - 1
基本操作
- 已知出现频次,要求构建哈夫曼树
// 仅模拟效果,没有构建出真正的树
int wpl(vector<int> a) {
// c++默认为大顶堆,对于哈夫曼树,要构建小顶堆
// priority_queue<int> q;
// 小顶堆
priority_queue<int, vector<int>, greater<int>> q;
for (auto v : a) q.push(v);
int x, y, ans = 0;
while (q.size() > 1) {
x = q.top();
q.pop();
y = q.top();
q.pop();
q.push(x + y);
ans += (x + y);
}
return ans;
}

浙公网安备 33010602011771号