相关
-
静态查找 :集合是固定的,没有插入和删除
-
动态查找:集合是动态的,可能发生插入和删除
-
二分查找
- 将复杂度降为 log 2 N \log_2N log2N,相当于将数组变为树。
- 前提:数组是排好序的
树
定义
树:
n
(
n
≥
0
)
n (n\geq0)
n(n≥0)各节点构成的有限集合
n=0时,是空树。没有节点也是树。
- 非根节点可分为m个不相交的有限集合,每个集合又是一棵树,成为原树的子树。
- 除了根节点,每个节点仅有一个父节点
- n个节点有n-1条边
- 节点的度:子树的个数
- 树的度:所有节点中最大的度数
- 路径和路径长度:从节点 n 1 n_1 n1到 n k n_k nk的路径为一个节点序列 n 1 , n 2 , . . . n k n_1,n_2,...n_k n1,n2,...nk, n i n_i ni是 n i + 1 n_{i+1} ni+1的父节点。路径所包含的边数为路径长度
- 节点的层次,根节点在1层。
- 树的深度:所有节点中最大层次
二叉树表示
儿子兄弟法可以将所有树变为二叉树。
二叉树的五种基本形态
- 空树
- 只有根节点
- 只有左孩子
- 只有右孩子
- 有左,右孩子
几种特殊的二叉树
- 斜二叉树(skewed Binary tree)。即链表,所有节点只有一个孩子
- 完美二叉树(perfect Binary tree) 或 满二叉树(full binary tree) .每层节点数量达到最大
- 完全二叉树(complete binary tree) 对节点从上到下,从左到右进行编号,与满二叉树编号相同
二叉树的性质
- 第i层最大节点数为 2 i − 1 , i ≥ 1 2^{i-1},i\geq1 2i−1,i≥1
- 深度为k的二叉树最多有 2 i − 1 2^i-1 2i−1节点
- 对于非空二叉树, n 0 n_0 n0为叶子节点数, n 2 n_2 n2是度为二的非叶子节点个数,那么满足 n 0 = n 2 + 1 n_0 =n_2 +1 n0=n2+1
存储
完全二叉树
- 顺序存储 i = index+1
- 节点为i,左孩子为2I,右孩子为2i+1
- 节点为i,父节点为 i/2 (向下取整)
一般二叉树
- 链表存储,用数组会造成空间浪费
树的遍历
树有两大类遍历方法,深度优先和广度优先。对于二叉树来说,深度优先又分为先序,中序,后序三种遍历方式。如果从根开始遍历,最后回到根,每个节点会被遍历三次,对应前序,中序,后序。深度优先的遍历线路时固定的,只是什么时候遍历该节点是不确定的。而先中后三种是针对根节点访问顺序而言的。
对于一个
n
n
n叉树来说,深度优先遍历时,每个节点会被遍历
n
+
1
n+1
n+1次。
F
i
g
u
r
e
1
深
度
优
先
遍
历
路
径
Figure 1 深度优先遍历路径
Figure1深度优先遍历路径
递归遍历(针对深度优先遍历)
void preOrder(vector v ,int root){
if(root<v.size()){
visit(v[root]);
preOrder(vector,root*2);
preOrder(vector v ,root*2+1);
}
}
该顺序是先遍历根节点,再遍历左子树,最后右子树。从根到各个叶子是从左到右的顺序遍历。若要先遍历右侧路径,换一个位置即可.对于递归遍历,只需要调节三条语句的顺序,即可实现三种遍历。
非递归遍历
因为递归和深度遍历都使用了栈,所以可以通过递归来实现深度优先遍历。如果使用非递归实现深度优先遍历,只需要配合一个栈即可。
- 中序遍历
Tree t;
stack s;
while(t || !s.empty()){
while(t){
s.push(t);
t = t.left;
}
if(!s.empty()){
t = s.top();
s.pop();
visit(t);
t = t.right;
}
}
- 先序遍历
Tree t;
stack s;
while(t || !s.empty()){
while(t){
s.push(t);
visit(t); //在第一次遇到就进行遍历
t = t.left;
}
if(!s.empty()){
t = s.top();
s.pop();
// visit(t); 将此处遍历上移
t = t.right;
}
}
- 后序遍历
stack s;
tree t ,last = null;
while(t || ! s.empty()){
while(t){
s.push(t);
t = t.left;
}
while(!s.empty()){
if(!s.top.right ||(! last && s.top.right == last) || ! last ){
t = s.top();
s.pop();
last = t;
visit(t);
}else{
t = s.top().right;
break;
}
}
}
层次遍历,广度优先遍历
广度优先遍历需要queue。从队列中取出一个元素并访问,将其子节点都压入队列。
queue q;
Tree t;
q.push(t);
while(!q.empty()){
t = q.front();
q.pop();
visit(t);
if(t.left)
q.push(t.left);
if(t.right)
q.push(t.right);
}
应用
深度优先遍历中,通过两种遍历顺序推测一个二叉树树,必须含有中序遍历。没有中序遍历的话,无法判断左右子树。除非是真二叉树(没有度为1的节点,都是2或0)可以还原。
先序或后序遍历中找出根节点,在中序遍历中利用根节点将树分为左右两个。
Tree build(vector inorder ,vector post){
Tree root = post[post.size()-1];
int root_index = find(inorder ,root);
vector l_inoder = copy(inorder,0,root_index-1);
vector l_post = copy(post,0,root_index-1);
vector r_inorder = copy(inoder,root_index+1,inorder.size()-1);
vector r_post = copy(post,root_index,post.size()-2);
root ->left(l_inorder,l_post);
root ->right (r_inorder,r_post);
return root;
}
二叉搜索树(BST)
- 又叫二叉排序/查找树
- 对于非空的搜索树:
- 非空左子树的所有值小于根节点的值
- 右子树的所有值大于根节点的值
- 左右子树都是二叉搜索树
- 最大元素在最右端
- 最小元素在最左端
- 二叉搜索树的查找效率取决于树的高度
操作
- 插入 ,类似查找,一定插入的是叶子节点
- 删除
- 删除叶子
- 直接删除
- 删除只有一个孩子的节点
- 孩子和爷爷相连
- 删除两个孩子的节点
- 用右子树最小节点或左子树最大节点代替。需要将那条路径上所有节点进行移动,直到叶子节点。
- 删除叶子
平衡二叉树 AVL
是个查找树
平衡因子(balance factor ,BF) BF(T)=
h
L
−
h
R
h_L-h_R
hL−hR,
h
L
h_L
hL和
h
R
h_R
hR分别是左右子树的高度
AVL定义
- 空树或者任意节点的 ∣ B F ∣ |BF| ∣BF∣不超过1
AVL调整
- 插入节点在不平衡节点的左子树的左边
- 插入节点是不平衡节点的左子树的右侧
- 插入节点后,可能不需要调整,但BF可能需要更新
堆 heap
优先级队列
- 取出元素的顺序是按优先级顺序
- 可以用数组,链表,有序数组,查找树等实现。
如果用查找树实现,插入删除都比较省时间,但一直删除会导致树倾斜。
堆 定义
- 优先级队列的完全二叉树表示
特性
- 结构性,用数组表示完全二叉树
- 有序性 ,任意节点都是其子树的最大值或最小值
每条路径是有序的
操作
全部是大根堆
- 建堆
- 插入 ,时间复杂度较大,T(n) = O(nlogn)
vector build(vector v){
vector heap;
for(i=0;i<v.size();i++){
insert(heap,v[i]);
}
return heap;
}
- 调整。类似递归,叶子一定是个堆,从最后一个非叶子节点开始调整,使之成堆。最坏只需移动书中所有节点高度之和 T(n)= O(n)
vector build(vector v){
for(i=(v.size()-1)/2 ;i>0;i--){
tem = v[i];
for(p=i;p*2<v.size();p=c){
c = p*2;
if(p*2+1<v.size() && v[p*2+1]>v[p*2]){
v[p] = v[c];
}
}
v[p] = tem;
}
}
- 插入
- 将新节点插入数组末尾,保证插入后满足二叉树的结构。再调整,判断该节点是否大于其父节点,若大于则交换,直到不大于父节点或到达根节点。
- 实现中,可以再数组下标为0的元素中设置哨兵,远大于堆中其他元素;在与父节点交换值时,可以只改变子节点位置处的值,不修改父节点处的值,直到停止交换才修改
insert( vector heap ,int node){
heap.push_back(node);// 将元素插入末尾
for(i= heap.size()-1; heap[i] >heap[i/2]; i= i/2){ // 设有哨兵,保证不会越界 heap[0]>>heap[i] (i!=0)
heap[i] = heap[i/2]; // 将子节点处的值进行修改
}
heap[i] = node;
}
// T(N) = O(logN)
- 删除,返回最大值
- 要保证二叉树的结构完整,只能删除最后一个元素。故记录最大元素,作为返回值;将最后一个元素放在根节点上,与其最大的孩子进行交换,向下移动至合适的位置。
int del(vector heap){
int max = heap[1];
int item = heap[heap.size()-1];
int size = heap.size()-1;
for(parent = 1; parent *2 < size ;p = c){
c = p*2;
if(p*2+1 <size && heap[p*2+1]>heap[p*2]){
c = p*2+1;
}
if(heap[p]<heap[c]){
heap[p] = heap[c];
}else{
beak;
}
}
heap[p] = item;
return max;
}
哈夫曼树(Huffman Tree)与哈夫曼编码
哈夫曼树是根据节点不同的查找频率构建的搜索树
哈夫曼树定义
带权路径长度(WPL):设二叉树有n个叶子节点,每个叶子节点带有权值 w k w_k wk,从根节点到每个叶子节点的长度为 I k I_k Ik,则每个叶子节点的带权路径长度之和就是 W P L = ∑ k = 1 n w k l k WPL=\sum^n_{k=1}w_kl_k WPL=k=1∑nwklk
最优二叉树或哈弗曼树:WPL最小的二叉树
哈夫曼树操作
- 构建
- 将权重最小的两个进行合并,生成新的节点
- 每个子树都是哈夫曼树
Tree build(minHeap heap){
Node n1,n2,n3;
for(i=0;i<heap.szie();i++){
n1 = heap.del();
n2 = heap.del();
n3 = newNode(n1,n2);
heap.insert(n3);
}
}
- 特点
- 没有度为1的节点
- n个叶子节点的哈弗曼树共有2n-1个节点
- 左右子树交换后还是哈弗曼树
- 一组权值可能对应多个不同构的哈弗曼树
- 当出现节点值相同时,无论是叶子节点还是非叶子节点,权值相同时,取点顺序不同会导致树不同
哈夫曼编码
不等长编码,出现的频率高,编码短;频率的,编码长。
不等长编码要处理二义性。
- 前缀码 (prefix code):任何字符的编码都不是另一个字符的前缀,可进行无二意的解码
- 要实现,所有编码必须在叶子上
用二叉树进行编码
- 左孩子为0 ,右孩子为1
- 字符在叶子上。(保证无前缀码)
用字符组建一棵哈夫曼树,左0右1,得到编码
判断哈夫曼编码时
- 是否是最优的编码
- 字符都在叶子上
- 不需要判断有没有度为1 的节点。反证:如果有,则不是最优编码,某个节点只有一个孩子,那该节点不存字符,将该节点的子节点与该节点的父节点相连,没有影响其他节点,且叶子节点编码长度缩短一位,即原来不是最优编码,矛盾。
并查集-集合的表示和运算
主要用于合并集合,查找元素所属的集合
每个节点除了自身的数据外,还需要保存期父亲节点。
根的父亲节点使用-1或自身等特殊标记。
为了标记每个集合元素数量,可以在跟的父节点中记录集合元素数量的相反数,既表明了是根节点,又记录集合中的数量元素。
操作
可以对并查集进行化简,每个节点的父亲节点是数组中的值,自身的数据用数组下标表示。如果自身数据到数组下标这个转化较复杂,可以单独写一个映射。
- 初始化
for(i=0;i<n;i++)
set[i] = -1; // 父节点且集合中只有一个节点
- 查找
int find(int item ,vector set){
int a = item,t;
while(set[item] >0){
item = set[item];
}
// 路径压缩
while(a != item){
t = set[a];
set[a] = item;
a = t;
}
retrun item;
}
- 合并
- 最简单是直接合并,但会导致树不平衡,
- 按秩归并
- 将小集合并入大集合中
void union (int a ,int b,vector set){
int x,y;
x = find(a,set);
y = find(b,set);
if(x != y){
// 按秩归并
if(set[x] >set[y])
set[y]+=set[x];
set[x] = y;
}else{
set[x] +=set[y];
set[y] =x;
}
}
code技巧
- 建树时如何判断根节点。遍历所有节点,标记各个节点的子节点,没有节点指向的为根节点。
- 判断两个序列形成的二叉搜索树是否一样。先建一颗树A,节点增加一个flag标记,用于记录是否被访问过。另一个序列B从头开始遍历,B中每个节点在已建好的树A上进行搜索,如果在搜索过程中A树上遇到了未访问的节点且不是要查找的节点,则树不一样。
reference
浙江大学 数据结构mook 树 https://www.icourse163.org/learn/ZJU-93001?tid=1003013004#/learn/announce