树
一、引言:
1. 二分查找
/*二分查找 引出树*/
int BinarySearch(ElementType A[] , int N , ElementType K){
int left , right , mid ,Notfound ;
Notfound = -1 ;
left = 0 ;
right = N - 1 ;
while( left <= right ){
mid = ( left + right ) / 2;
if( K > A[mid] ) left = mid + 1 ;//不能直接取mid 否则会陷入死循环
else if( K > A[mid] ) right = mid - 1 ;
else return mid ;
}
return Notfound ;
}
$T(N) = log_2N $
2. 普通树的儿子兄弟表示法
普通树旋转45°即二叉树
typedef struct TNode *TList;
struct TNode{
ElementType elem ;
TList FisrtChild ; //从左边数第一个孩子节点
TList RightBro ; // 邻接的兄弟节点
};
二、二叉树的性质和储存方法
1.几种特殊的二叉树
斜二叉树 : 链表
完美/满 二叉树 :除最后一层 ,其他节点都有两个子节点
第i层节点数 : $ 2^{i-1}$
节点总数 = $ 2^N - 1$ (N为高度)
这也是二叉树节点数的上界
完全 二叉树 : 允许最后一层部分缺失
2.重要性质
$ N_i 表示度为i的节点数,i = {0 ,1 , 2}$
公式:$ N_0 = N_2 + 1$
证明: $ N_0 + N_1 + N_2 - 1= 0 * N_0 + 1 * N_1 + 2 * N_2 $
(左边是每个节点向上看的边数 , 右边是向下看的边数)
PS.若给出树的
N
总
N_总
N总和
N
1
N_1
N1,树有可能不存在
3.两种存储方法
数组
完全二叉树 其实就是通常意义的堆
普通二叉树 补全
链表
类似于儿子兄弟表示法
三、二叉树的遍历
0.引言
- 其实二叉树的遍历,是为了得到一个包含所有节点的序列,因此其根本是将二维结构线性化,如何对二叉树划线便决定了不同的遍历方法。
- 在划线过程中,我们需要注意的是,我们访问一个节点的唯一途径是通过其父节点的的左右指针,因此就会存在一个问题:一旦父节点丢失 , 那么他的左右孩子就再也无法访问到了。
- 因此我们的应对是 , 如果某个节点的左右孩子没被访问到 , 我们先将它储存起来,到下一次该访问它的孩子时,再取出来。
- 用以储存的数据结构可以是栈或者队列 , 注意:保存节点的方式便决定了划出来的线是怎样的,根据栈和队列各自的特性,对应的线其实就是回溯与否。
- 前中后序对应栈 , 层序对应队列 ,我们可以很容易地从划线也就是遍历的路径得出
1.递归
以中序遍历的代码为例
void InorderTraversal( BinTree BT )
{
if( BT ) {
InorderTraversal( BT->Left );
/* 此处假设对BT结点的访问就是打印数据 */
printf("%d ", BT->Data); /* 假设数据为整型 */
InorderTraversal( BT->Right );
}
}
Tips :
三种遍历访问节点的路径完全一样,都是对于每棵树递归地使用左边-内部-右边
在访问过程中,每个节点会被遇到三次
先、中、后分别对应第1、2、3次遇到访问时print
2.非递归(栈)
中序遍历(特殊的DFS)
void InorderTraversal( Bintree BT ){
Bintree T = BT ;
Stack S = CreateStack(Maxsize) ;
while( T || S){
while( T ){
Push( S , T ) ;//第一次遇到
T = T->Left ;
}
if( S ){
T = Pop( S ) ;//第二次遇到
Printf( T ) ;//对于中序 第二次遇到时print
T = T->Right ;//这时候根和左孩子已经print完毕 WhichMeans 前两次访问结束
//对右孩子进行中序遍历 Means 会对根和左孩子第三次访问
}
}
}
在这样的代码中,第一次访问和第二次访问是显式的,第三次访问藏在对右孩子的中序遍历中
先序遍历
因此我们只需要将print
操作放在Push
操作也就是第一次访问之后
后序遍历
一种思路 : 因为先序遍历是根左右 , 我们可以交换T->Left
and T->Right
的位置
得到根右左 , 再将其倒置即可得到后序遍历
具体做法是 每次Pop(S)
操作时,将其压入新的栈 ,最后将新栈依次Pop
出来
3.层序遍历(特殊的BFS)
void LevelTraversal( BinTree BT){
if( !BT ) return ;
BinTree T = BT;
Queue Q = CreateQueue( Maxsize );
InQueue( Q , T );
while( Q ){
T = DeQueue( Q );
Print( T );
if( T->Left ) InQueue( Q , T->Left );
if( T-Right ) InQueue( Q , T->Right );
}
}
PS.我们如果保持算法风格不变,单纯将队列改为栈,即InQueue()DeQueue()
改为Push()Pop()
,那么同样可以做到根左右和根右左,区别仍然是Print()
的位置
4.应用
求树的高度
公式 :
H
=
m
a
x
(
H
L
,
H
R
)
+
1
H = max(H_L , H_R) + 1
H=max(HL,HR)+1
思路: 采用后序遍历的递归形式
代码 :
int GetTreeHeight( Bitree BT ){
int HL , HR , MaxH ;
if(BT){
HL = GetTreeHeight( BT->Left ) ;
HR = GetTreeHeight( BT->Right ) ;
MaxH = ( HL > HR ) ? HL : HR ;
return MaxH+1 ;
}
else return 0;
}
由先、中序遍历推导二叉树
先序遍历第一个为根 , 对应在中序遍历中找出根 ,那么根的左边为左子树 ,对应的在先序中找出左子树
递归进行下去
同构判断
四、 二叉查找树
0.概念
for each root
Root > Left && Root < Right
2.因此while 和 递归 是BST常用操作
1.Find()
思路 : 和二分查找一样 ,只不过用判断左右子树来代替Left和Right ;
递归和循环都可以 , 不过由于这个地方用到的是“尾递归”,效率不高 , 因此用循环
代码 :
Positon InterFind( ElementType X , BinTree BST){
while( BST ){
if( X > BST->Data ) BST = BST->Right ;
else if( X < BST->Data ) BST = BST->Left ;
else return BST;
}
return NULL;
}
2.Insert()
思路 : 利用Find()操作 ,只不过循环的判定条件是 左/右节点是否空 ;
同时 Create()
操作即是对每个元素执行插入操作
代码 :
/*
递归
*/
BinTree Insert( BinTree BST, ElementType X )
{
if( !BST ){ /* 若原树为空,生成并返回一个结点的二叉搜索树 */
BST = (BinTree)malloc(sizeof(struct TNode));
BST->Data = X;
BST->Left = BST->Right = NULL;
}//真正的插入在这儿执行
else { /* 开始找要插入元素的位置 */
if( X < BST->Data )
BST->Left = Insert( BST->Left, X ); /*递归插入左子树*/
else if( X > BST->Data )
BST->Right = Insert( BST->Right, X ); /*递归插入右子树*/
/* else X已经存在,什么都不做 */
}
return BST;
}
/*
while
*/
void Insert( ElementType X , Bintree BT ){
Bintree T = BT ;
while( 1 ){
if( !T ){
T->elem = X ;
T-Left = NULL ;
T->Right = NULL ;
break ;
}
if( X > T->elem ) T = T->Right ;
else if( X < T->elem ) T = T->Left ;
else break ;
}
}
3. Delete()
思路 :
代码 :
五、平衡二叉树
0. 引言
目的 :
构建二叉搜索树时 尽可能减少AFL/因此对于任何平衡操作 ,首先确保查找性不被改变
定义 :
对于任一节点 $|BF(T)| <= 1 其中BF(T) = H_L -H_R $
性质 :
对于N个节点的平衡二叉树 , 它的最大高度是多少 ?/对于H高度,最少要多少个节点?
$ N_H = N_{H-1} + N_{H-2} + 1 $
由斐波拉契数列可推导出
N
=
O
(
l
o
g
2
N
)
N = O(log_2 N)
N=O(log2N)
1. 实现
思路
LL、RR、LR、RL 判断依据是寻找“破坏节点”和“发现节点”的关系
代码
typedef struct AVLNode *Position;
typedef Position AVLTree; /* AVL树类型 */
struct AVLNode{
ElementType Data; /* 结点数据 */
AVLTree Left; /* 指向左子树 */
AVLTree Right; /* 指向右子树 */
int Height; /* 树高 */
};
int Max ( int a, int b )
{
return a > b ? a : b;
}
AVLTree SingleLeftRotation ( AVLTree A )
{ /* 注意:A必须有一个左子结点B */
/* 将A与B做左单旋,更新A与B的高度,返回新的根结点B */
AVLTree B = A->Left;
A->Left = B->Right;
B->Right = A;
A->Height = Max( GetHeight(A->Left), GetHeight(A->Right) ) + 1;
B->Height = Max( GetHeight(B->Left), A->Height ) + 1;
return B;
}
AVLTree DoubleLeftRightRotation ( AVLTree A )
{ /* 注意:A必须有一个左子结点B,且B必须有一个右子结点C */
/* 将A、B与C做两次单旋,返回新的根结点C */
/* 将B与C做右单旋,C被返回 */
A->Left = SingleRightRotation(A->Left);
/* 将A与C做左单旋,C被返回 */
return SingleLeftRotation(A);
}
/*************************************/
/* 对称的右单旋与右-左双旋请自己实现 */
/*************************************/
AVLTree Insert( AVLTree T, ElementType X )
{ /* 将X插入AVL树T中,并且返回调整后的AVL树 */
if ( !T ) { /* 若插入空树,则新建包含一个结点的树 */
T = (AVLTree)malloc(sizeof(struct AVLNode));
T->Data = X;
T->Height = 0;
T->Left = T->Right = NULL;
} /* if (插入空树) 结束 */
else if ( X < T->Data ) {
/* 插入T的左子树 */
T->Left = Insert( T->Left, X);
/* 如果需要左旋 */
if ( GetHeight(T->Left)-GetHeight(T->Right) == 2 )
if ( X < T->Left->Data )
T = SingleLeftRotation(T); /* 左单旋 */
else
T = DoubleLeftRightRotation(T); /* 左-右双旋 */
} /* else if (插入左子树) 结束 */
else if ( X > T->Data ) {
/* 插入T的右子树 */
T->Right = Insert( T->Right, X );
/* 如果需要右旋 */
if ( GetHeight(T->Left)-GetHeight(T->Right) == -2 )
if ( X > T->Right->Data )
T = SingleRightRotation(T); /* 右单旋 */
else
T = DoubleRightLeftRotation(T); /* 右-左双旋 */
} /* else if (插入右子树) 结束 */
/* else X == T->Data,无须插入 */
/* 别忘了更新树高 */
T->Height = Max( GetHeight(T->Left), GetHeight(T->Right) ) + 1;
return T;
}
六、堆(优先队列)
七、HuffmanTree and Huffman coding
0.引言
1. 目的 :
解决等长编码效率不高的问题
2. 方法 :
根据数据出现概率 , 使用不等长编码
1.HuffmanTree的定义
$ WPL(带权路径长度) = Σ {W_kL_k} $
k为叶子节点的个数 , W为权重(概率) ,L为该叶子节点到根节点路径长(经过的边)
HuffmanTree 即为 WPL 最小的 二叉树
2. 实现
思路
利用最小堆 每次将两个权重最小的树合并 更新权重 循环进行
代码
3. 利用HuffmanTree来编码
思路
将未编码数据按频率构建HuffmanTree,左子树为0,右子树为1,依次编码
这样有两点好处 : 1.Cost最小(也就是WPL)2.无二义性