第五章 树与二叉树
基础概念知识
结点的度:结点的孩子个数
树的度:树中结点的最大度数
分支节点:度>0
叶子结点:度=0
结点的层次:根结点为第1层,往下以此类推
结点的深度:从根结点开自顶向下逐层累加
结点的高度:从叶结点开始自底向上逐层累加
树的高度(或深度):树中结点的最大层数
有序树:树中结点的各子树从左到右是有次序的;否则称为无序树
路径:两个结点之间所经过的节点序列构成
路径长度:所经过的边的个数(树的分支有向,从上向下)
森林:m棵互不相交的树的集合
树的性质:
-
结点数 = 所有结点的度数之和 + 1
-
度为m的树中第i层至多有 m i − 1 m^{i-1} mi−1个结点
-
高度为h的m叉树至多有 ( m h − 1 ) / ( m − 1 ) (m^h-1)/(m-1) (mh−1)/(m−1);( m 0 + m 1 + m 1 + . . . + m h = ( m h − 1 ) / m − 1 m^0+m^1+m^1+...+m^h = (m^h-1)/m-1 m0+m1+m1+...+mh=(mh−1)/m−1)
-
具有n个结点的m叉树的最小高度为 ⌈ l o g m ( n ( m − 1 ) ) + 1 ⌉ \lceil log_m(n(m-1))+1 \rceil ⌈logm(n(m−1))+1⌉
具有n个结点的m叉树的最大高度为 n − m + 1 n-m+1 n−m+1
满二叉树:高度为h,且含有 2 h − 1 2^h-1 2h−1个结点的二叉树
-
编号为 i i i 的结点的第 1 个子女结点(若存在)的编号: ( i − 1 ) ∗ m + 2 (i-1)*m+2 (i−1)∗m+2
编号为 i i i 的结点的第 k k k 个子女结点(若存在)的编号: ( i − 1 ) ∗ m + k + 1 (i-1)*m+k+1 (i−1)∗m+k+1
-
编号为 i i i 的结点的双亲结点(若存在)的编号: ⌊ ( i − 2 ) / m ⌋ + 1 \lfloor (i-2)/m \rfloor + 1 ⌊(i−2)/m⌋+1
-
编号为 i i i 的结点有右兄弟的条件: i ≤ ⌊ ( i + m − 2 ) / m ⌋ ∗ m i \leq \lfloor (i+m-2)/m \rfloor *m i≤⌊(i+m−2)/m⌋∗m;且右兄弟编号为 i + 1 i+1 i+1
结点 i i i 不是其双亲的第 m m m 个子女时才有右兄弟。
完全二叉树:高度为h,有n个结点的二叉树,当且仅当其每个结点都与高度为h的满二叉树中的编号1~n一一对应时
完全二叉树特点:
-
i ≤ ⌊ n / 2 ⌋ i\leq \lfloor n/2 \rfloor i≤⌊n/2⌋,则结点 i i i 为分支节点,否则为叶子节点
-
叶子结点只可能在层次最大的两层出现
-
若有度为1的结点,则只可能有一个,且为左孩子; n 1 = 0 或 1 n_1 = 0或1 n1=0或1
若有2k(偶数)个结点, n 1 = 1 ; n 0 = k , n 2 = k − 1 n_1 = 1;n_0 = k,n_2 = k-1 n1=1;n0=k,n2=k−1
若有2k-1(奇数)个结点, n 1 = 0 ; n 0 = k , n 2 = k − 1 n_1 = 0;n_0 = k,n_2 = k-1 n1=0;n0=k,n2=k−1
-
按层序编号,一旦出现某结点 i i i 为叶子结点,则编号大于 i i i 的结点均为叶子结点
二叉排序树:左子树<根<右子树
平衡二叉树:任一结点的左右子树深度差<=1
二叉树的性质:
-
叶子结点数 = 度为2的结点数+1; n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1 ;
{ n = n 0 + n 1 + n 2 n = n 1 + 2 n 2 + 1 ⟹ n 0 = n 2 + 1 \left\{ \begin{matrix} n = n_0 + n_1 + n_2\\ n = n_1 + 2n_2 + 1\\ \end{matrix} \right. \Longrightarrow n_0 = n_2 + 1 {n=n0+n1+n2n=n1+2n2+1⟹n0=n2+1 -
非空二叉树上第k层上至多有 2 k − 1 2^{k-1} 2k−1个结点
-
高度为h的二叉树至多有 2 h − 1 2^{h}-1 2h−1个结点
-
完全二叉树结点 i i i 所在的层次(深度)为 ⌊ l o g 2 i ⌋ + 1 \lfloor log_2i \rfloor + 1 ⌊log2i⌋+1
-
具有n个结点的完全二叉树的高度为 ⌈ l o g 2 ( n + 1 ) ⌉ \lceil log_2(n+1) \rceil ⌈log2(n+1)⌉或 ⌊ l o g 2 n ⌋ + 1 \lfloor log_2n \rfloor+1 ⌊log2n⌋+1
2 h − 1 − 1 < n < 2 h − 1 或 2 h − 1 < n < 2 h ⟹ 2 h − 1 < n + 1 < 2 h 或 h − 1 < l o g 2 ( n + 1 ) < h ⟹ h = ⌈ l o g 2 ( n + 1 ) ⌉ 或 h − 1 < l o g 2 n < h h = ⌊ l o g 2 n ⌋ + 1 2^{h-1}-1<n<2^h-1 或 2^{h-1}<n<2^{h}\\ \Longrightarrow 2^{h-1}<n+1<2^h 或 h-1<log_2(n+1)<h\\ \Longrightarrow h = \lceil log_2(n+1) \rceil 或 h-1 < log_2n<h\\ h = \lfloor log_2n \rfloor + 1 2h−1−1<n<2h−1或2h−1<n<2h⟹2h−1<n+1<2h或h−1<log2(n+1)<h⟹h=⌈log2(n+1)⌉或h−1<log2n<hh=⌊log2n⌋+1
n个结点的二叉链表中,含有n+1个空指针域
先序序列二叉树的个数: 1 n + 1 C 2 n n \dfrac{1}{n+1}C_{2n}^{n} n+11C2nn
二叉树遍历(C++)
指路leetcode,可自行练习
前序遍历 leetcode144
迭代法
//前序遍历 leetcode144
//迭代法
class Solution {
public:
vector<int> a;
vector<int> preorderTraversal(TreeNode* root) {
TreeNode* p = root;
stack<TreeNode*> s;
while(p || !s.empty()){
while(p){
a.push_back(p->val);
s.push(p);
p = p->left;
}
p = s.top();
s.pop();
p = p->right;
}
return a;
}
};
递归法
//递归法
class Solution {
public:
vector<int> a;
vector<int> preorderTraversal(TreeNode* root) {
if(root == NULL) return a;
a.push_back(root->val);
preorderTraversal(root->left);
preorderTraversal(root->right);
return a;
}
};
中序遍历 leetcode94
迭代法
//中序遍历 leetcode94
//迭代法
class Solution {
public:
vector<int> a;
vector<int> inorderTraversal(TreeNode* root) {
TreeNode*p =root;
stack<TreeNode*> s;
while(p || !s.empty()){
while(p){
s.push(p);
p = p->left;
}
p = s.top();
s.pop();
a.push_back(p->val);
p = p->right;
}
return a;
}
};
递归法
//递归法
class Solution {
public:
vector<int> a;
vector<int> inorderTraversal(TreeNode* root) {
if(root == NULL)return a;
inorderTraversal(root->left);
a.push_back(root->val);
inorderTraversal(root->right);
return a;
}
};
后序遍历 leetcode145
迭代法
//后序遍历 leetcode145
//迭代法
class Solution {
public:
vector<int> a;
vector<int> postorderTraversal(TreeNode* root) {
if (root == nullptr) {
return a;
}
stack<TreeNode*> s;
TreeNode* p = root;
TreeNode* r = nullptr;
while(p || !s.empty()){
while(p){
s.push(p);
p = p->left;
}
p = s.top();
s.pop();
if(p->right == nullptr || p->right == r){//当p的右子树为空 或 p的右子树已经被访问过
a.push_back(p->val);
r = p;
p = nullptr;
}else{
s.push(p);
p = p->right;
}
}
return a;
}
};
递归法
//递归法
class Solution {
public:
vector<int> a;
vector<int> inorderTraversal(TreeNode* root) {
if(root == NULL)return a;
inorderTraversal(root->left);
inorderTraversal(root->right);
a.push_back(root->val);
return a;
}
};
层序遍历 leetcode102
//层序遍历 leetcode102
class Solution {
public:
vector<vector<int>> a;
vector<vector<int>> levelOrder(TreeNode* root) {
if(root == NULL) return a;
queue<TreeNode*> q;
TreeNode * p = root;
q.push(p);
while(!q.empty()){
int n = q.size();
a.push_back(vector<int>());
for(int i = 0;i < n;i++){
p = q.front();
q.pop();
a.back().push_back(p->val);
if(p->left != NULL)q.push(p->left);
if(p->right != NULL)q.push(p->right);
}
}
return a;
}
};
中序和另外三种任意一种序列组合,都可以唯一确定一棵二叉树。
完全二叉树(Java)
//is complete binary tree
public static boolean isCBT(Node head) {
if(head == null) {
return true;
}
Queue<Node> queue = new LinkedList<>();
boolean leaf = false;
Node leftN = null;
Node rightN = null;
queue.offer(head);
while (!queue.isEmpty()) {
head = queue.poll();
leftN = head.left;
rightN = head.right;
if((leftN == null && rightN != null) || (leaf && (leftN != null || rightN != null))) {
return false;
}
if(leftN != null) {
queue.offer(leftN);
}
if(rightN != null) {
queue.offer(rightN);
} else {
leaf = true;//叶子节点开始标志,若为“完全二叉树”则之后的待处理的节点都应为叶子节点
}
}
return true;
}
算法解析:
1.按层遍历二叉树,从每层的左边向右边依次遍历;
2.如果当前节点有右孩子,但没有左孩子,直接返回false;
3.如果当前节点并不是左右孩子都有,那之后的节点应都为叶节点,否则返回false;
4.设置初始标志位true, 如果遍历结束没有返回false,则为完全二叉树,返回最终结果true。
二叉搜索树
左子树 < 根 < 右子树
删除操作:
- 被删除结点 z 为叶子节点,直接删除
- 结点z只有一颗左子树或右子树,让 z 的子树成为 z 父结点的子树,替代 z 的位置。
- 若结点 z 有左、右两棵子树,则令 z 的直接后继(或直接前驱)替代 z ,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
二叉平衡树
平衡调整
-
LL平衡旋转(右单旋转)----左孩子的左子树
f->lchild = p->rchild p->rchild = f gf->lchild/rchild = p
-
RR平衡旋转(左单旋转)----右孩子的右子树
f->lchild = p->rchild p->rchild = f gf->lchild/rchild = p
-
LR平衡旋转(先左后右双旋转)----左孩子的右子树
-
RL平衡旋转(先右后左双旋转)----右孩子的左子树
n h n_h nh表示深度为h的平衡树中含有最少结点数。 n 0 = 0 n_0 = 0 n0=0, n 1 = 1 n_1 = 1 n1=1, n 2 = 2 n_2 = 2 n2=2,并且有 n h = n h − 1 + n h − 2 + 1 n_h = n_{h-1}+n_{h-2}+1 nh=nh−1+nh−2+1。
最大深度为 O ( l o g 2 n ) O(log_2n) O(log2n),即平均查找长度为 O ( l o g 2 n ) O(log_2n) O(log2n)。
霍夫曼树
编码压缩方法;霍夫曼编码;霍夫曼树创建方法;
结点的带权路径长度:从树根到任意结点的路径长度(经过的边数)与该节点上权值的乘积
树的带权路径长度:树根中所有叶结点的带权路径长度之和
W
P
L
=
∑
i
=
1
n
w
i
l
i
WPL = \sum_{i=1}^{n}w_il_i
WPL=i=1∑nwili
其中,
w
i
w_i
wi是第
i
i
i 个叶结点所带的权值,
l
i
l_i
li 是该叶结点到根节点的路径长度
带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树。
哈夫曼树特点:
- 每个初始结点最终都成为叶结点,且权值越小的结点到根节点的路径长度越大
- 构建过程共新建了n-1个结点(双分支节点),因此哈夫曼树的节点总数为2n-1
- 每次构造都选择2棵树作为新结点的孩子,因此哈夫曼树中不存在度为1的结点
哈夫曼编码:数据压缩编码
固定长度编码:每个字符用相等长度的二进制位表示
可变长度编码:允许对不同字符用不等长的二进制位表示
对高频字符用短编码,对低频字符用长编码
前缀编码:没有一个编码是另一个编码的前缀
由哈夫曼树构造哈夫曼编码: