树的定义是递归的,树本身也是一种递归的数据结构。其作为一种逻辑结构,同时也是一种分层结构。树适合表示具有层次结构的数据。
度:一个结点的的孩子个数
树的度:树中结点的最大度数
数中的分支是有向的,即从双亲指向孩子,所以数中的路径只能是从上往下的。同一个双亲的孩子间不存在路径。
二叉树是一种特殊的树形结构,特点是每个结点至多只有两棵子树,但其度可以小于2;并且二叉树的子树有左右之分,即使树中结点只有一棵子树,也要区分其是左子树还是右子树。
高度为h,且含有 2 h − 1 2^{h-1} 2h−1个结点的二叉树称为满二叉树,即树中每层都有最多的结点。
根结点从1开始编号,若结点编号为 i,其双亲为 ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋ 其左孩子为2i,右孩子为2i+1。
性质:
左子树上所有结点的关键字都小于根结点,右子树上的所有结点关键字都大于根结点。
树上任一结点的左右子树深度之差不超过1
用一组地址连续的存储单元依次自上而下、自左至右储存完全二叉树的结点元素。
对于一般的二叉树,必须添加一些空结点。
在含有n个结点的二叉链表中,含有n+1个空链域
采用一组连续空间来储存每个结点,同时在每个结点中增设一个伪指针,指示其双亲结点在数组中的位置。根结点的下标为0,其伪指针域为-1。
该存储结构可以很快得到每个结点的双亲位置,但求结点孩子时需要遍历整个结构。
为每个结点创建一个链表,将该结点的孩子都用单链表接起来。再将所有结点顺序存储在一个数组中,数组中每个元素不但储存结点,还设置一个指针域,指向该结点的孩子链表。n个结点就有n个孩子链表(叶子结点的孩子链表为空表)。
这种方式寻找子女的操作非常直接,而寻找双亲的操作需要遍历所有孩子链表。
以二叉链表作为树的存储结构。
二叉树的左指针指向其第一个孩子,右指针指向其下一个兄弟。沿着右指针可以找到所有兄弟结点。
最大优点是可以方便实现树到二叉树的转换,易于找到结点的孩子。缺点是查找双亲结点比较麻烦,可以添加一个parent域指向父结点来解决。
void preOrder(BiTree T){
if(T != NULL){
visit(T);
preOrder(T->lchild);
preOrder(T->rchild);
}
}
void inOrder(BiTree T){
if(T != NULL){
inOrder(T->lchild);
visit(T);
inOrder(T->rchild);
}
}
无论哪种遍历,访问左右子树的顺序都是固定的,只是访问根结点的顺序不同。
每个结点都只访问一次,时间复杂度均为O(n)。
递归工作栈的栈深恰为树的高度。在最坏情况下,n个结点的树高为n,空间复杂度为O(n)。
关键是用栈记录当前结点的祖先
void inOrder(BiTree T){
initStack(S);
BiTree p = T; // 遍历指针
while( !isEmpty(S) || p ){
if(p){
push(S, p);
p = p-> lchild; // 一路向左
}else{ // 无法向左下继续前进,访问子树根结点,进入根结点右子树
pop(S, p);
visit(p);
p = p->rchild;
}
}
}
先序遍历和中序遍历的基本思想类似,只需把访问结点操作放在入栈操作前
void inOrder(BiTree T){
initStack(S);
BiTree p = T;
while( !isEmpty(S) || p ){
if(p){
visit(p);
push(S, p);
p = p-> lchild;
}else{
pop(S, p);
p = p->rchild;
}
}
}
void postOrder(BTree T){
initStack(S);
BTree p = T;
BTree r = NULL; // 记录访问的上一个结点
while(p || isEmpty(S)){
if(p){ //一直走到树的最左边
push(S, p);
p = p->lchild;
}else{
getTop(S, p);
if(p->rchild && p->rchild != r){ // 若未访问过右子树,进入
p = p-> rchild;
}else{ // 右子树已访问过,访问根结点
pop(S, p);
visit(p);
r = p; // 记录最近访问的结点
p = NULL; // 遍历完该子树,置空
}
}
}
}
从栈底结点再加上p结点,刚好构成从根结点到p结点的一条路径。
void leverOrder(BiTree T){
initQueue(Q);
BiTree p;
enQueue(Q, T);
while( !isEmpty(Q) ){
deQueue(Q, p);
visit(p);
if(p->lchild != NULL)
enQueue(Q, p->lchild);
if(p->rchild != NULL)
enQueue(Q, p->rchild);
}
}
由二叉树的先序序列和中序序列可以唯一确定一个二叉树
在先序遍历序列中,第一个结点一定是二叉树的根结点;而在中序遍历中,根结点必然将中序序列分割成两个子序列。
由二叉树的后序序列和中序序列可以唯一确定一个二叉树
后序序列的最后一个结点一定是二叉树的根结点。
由二叉树的层次遍历和中序遍历可以唯一确定一个二叉树
增加两个标志域表示指针域是指向左(右)孩子还是指向前驱(后继)。
以这种结点结构构成的二叉链表作为二叉树的存储结构,其中指示结点前驱及后继信息的指针称作线索。加上线索的二叉树称为线索二叉树。
引入线索二叉树能够加快查找结点前驱和后继的速度,像遍历单链表那样方便地遍历二叉树。
线索化的实质就是遍历一次二叉树。
使用指针pre指向刚刚访问过的结点,p指向正在访问的结点,即pre指向p的前驱。
在遍历的过程中,检查p的左指针是否为空,若为空就将其指向pre;同样的检查pre的右指针。
中序遍历线索化代码如下
void creatInThread(ThreadTree T){
ThreadTree pre = NULL;
if(T != NULL){
inThread(T, pre);
pre->rchild = NULL; // 处理遍历的最后一个结点
pre->rtag = 1;
}
}
void inThread(ThreadTree &p, ThreadTree &pre){
if( p!= NULL ){
inThread(p->lchild, pre);
// visit
if( p->lhild == NULL ){
p->lhild = pre;
p->ltag = 1;
}
if( pre != NULL && pre->rchild == NULL ){
pre->rchild = p;
pre->rtag = 1;
}
pre = p;
inThread(p->rchild, pre);
}
}
为了方便,可以在二叉树的线索链表上添加一个头结点,令其lchild域指向二叉树的根结点,其rchild域指向中序遍历的最后一个结点,再把中序遍历的第一个结点的lchild域指向头结点。这样就为二叉树建立了一个双向线索链表。
建立先序线索二叉树和后序线索二叉树的代码类似,只需变动线索化改造的代码段以及调用左右子树递归函数的位置。
先序线索化与后序线索化最多有1个空指针域;中序线索化最多有2个空指针域。
中序线索二叉树的结点隐含了线索二叉树的前驱后继信息,在对其进行遍历时,只要先找到序列中的第一个结点,然后依次找结点的后继即可。
不含头结点的中序线索二叉树遍历算法如下
void inOrder(ThreadTree T){
ThreadTree p = firstNode(T); // 获取遍历的起始结点
while( p != NULL ){
visit(p);
p = nextNode(p); // 获得下一个遍历结点
}
}
ThreadTree firstNode(ThreadTree p){
while( p->ltag == 0 ){
p = p->lchild;
}
return p;
}
ThreadTree nextNode(ThreadTree p){
if( p->rtag == 0)
return firstNode(p->rchild); // 返回右子树的最左结点,即下一个要遍历的结点
else
return p->rchild;
}
对于先序线索二叉树,如果有左孩子,则左孩子就是其直接后继;如果无左孩子但是有右孩子,则右孩子就是其直接后继;如果是叶结点,其右链域指向了结点的后继。
对于后继线索二叉树,其寻找后继需要知道结点双亲,需采用带标志域的三叉链表作为存储结构。
森林是m棵互不相交的树的集合。只需把树的根结点删除就成了森林;反之,只要给m棵独立的树加上一个结点,并把这m棵树作为该结点的子树,则森林就成了树。
二叉树和树都可以用二叉链表作为存储结构,给定一棵树,可以找到唯一一棵二叉树与之对应。
对于一棵树,每个结点左指针指向它的第一个孩子,右指针指向它在树中的相邻右兄弟。
这种规则下,根结点只有左孩子。
先将森林中的每一棵树转换为二叉树,由于任何一棵树对应的二叉树右子树必空,只需把所有二叉树的根结点用其右指针连接起来即可,即将所有树的根结点视为兄弟结点。
若二叉树非空,则二叉树根的右子树棵视为其余树形成的二叉树,将其与根断开,以此类推,把所有子树释放。再将每棵二叉树依次转换成树,就得到了原森林。
二叉树转换成树或森林也是唯一的。
森林的先序遍历和中序遍历即为对应二叉树的先序和中序遍历。
对于二叉排序树(二叉查找树),若左子树非空,则左子树的所有结点值均小于根结点的值,且也为一棵二叉排序树;若右子树非空,则右子树的所有结点值均大于根结点的值,且也为一棵二叉排序树。
二叉排序树可以是空树。
对二叉排序树进行中序遍历,可以得到一个有序序列。
按照如下规则递归进行:
插入的结点一定是一个新添加的叶结点,且是查找失败时路径上访问的最后一个结点的孩子。
若插入序列是有序的,则会形成一个倾斜的单支树,导致二叉树的性能显著变坏。
分为三种情况进行:
从根结点开始,将给定值与根结点关键字比较:
二叉排序树的查找效率,主要取决于树的高度。若二叉树左右子树高度之差不超过1(平衡二叉树),则平均查找长度为 O ( log 2 n ) O(\log_2n) O(log2n),若二叉排序树每个结点都只有一个结点,平均查找长度为 O ( n ) O(n) O(n)。
从查找过程看,二叉排序树与二分查找十分相似,其平均时间性能差不多;但二分查找的判定树唯一,二叉排序树则不唯一。
从结构的维护角度看,二叉排序树无序移动结点,只需修改指针即可完成插入删除操作,平均执行时间是 O ( log 2 n ) O(\log_2n) O(log2n);二分查找的对象是有序顺序表,若插入删除结点,所花时间是 O ( n ) O(n) O(n)。
若有序表是静态查找表,宜采用顺序表作为存储结构,采用二分查找进行查找操作。
若有序表是动态查找表,宜采用二叉排序树作为其逻辑结构。
为避免树的高度增长过快,降低二叉排序树的性能,规定插入和删除二叉树的结点时,保证任意结点的左右子树高度差不超过1。
以 n h n_h nh表示深度为h的平衡树中含有的最少结点数,有递推公式 n h = n h − 1 + n h − 2 + 1 n_h=n_{h-1}+n_{h-2}+1 nh=nh−1+nh−2+1,且 n 0 = 0 n_0=0 n0=0, n 1 = 1 n_1=1 n1=1。
含有n个结点的平衡二叉树最大深度为 O ( log 2 n ) O(\log_2n) O(log2n),平均查找长度也为 O ( log 2 n ) O(\log_2n) O(log2n)。
平衡因子:结点左右子树的高度差,取值范围为-1、0、1。
保持二叉树平衡的基本思路:每当插入或删除一个结点,检查该结点到根结点路径上的每个结点的平衡因子,调整不平衡的最小子树的结构,在保持二叉排序树特性的前提下,使之重新平衡。
对于一个新结点,先按照普通二叉排序树的规则进行插入操作,再找到其最小不平衡树,分情况进行调整:
为树中结点赋予一个数值,成为该结点的权。从根结点到任意结点的路径长度 l l l(经过的边数)与该节点上权值 w w w的乘积称为该结点的带权路径长度。树中所有叶结点的带权路径长度之和称为树的带权路径长度。即 W P L = ∑ i = 1 n w i l i WPL=\sum_{i=1}^nw_il_i WPL=∑i=1nwili。
在含有n个带权叶结点的二叉树中,WPL最小的二叉树称为哈夫曼树,也称最优二叉树。
给定n个权值分别为 w 1 , w 2 . . . w n w_1,w_2...w_n w1,w2...wn的结点,构造算法如下:
从构造过程可以看出哈夫曼树具有如下特点:
若允许不同字符用不等长的二进制位表示,称这种编码为可变长度编码。
若任何一个编码都不是其余编码的前缀,则称这种编码为前缀编码。
利用哈夫曼树可以设计出总长度最短的二进制前缀编码。