数据结构学习笔记(第五章:树和二叉树)

5.1 树

树的定义

树是一种递归的数据结构。树既是一种逻辑结构,也是一种分层结构。具有以下的两个特点:
1)树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱
2)树中所有结点可以用零个或多个后去。
所以,树适合表示具有层次结构的数据。

树的基本术语

下面是一些树的基本术语:
1)度:树中一个结点的孩子个数称为该结点的度,树中结点的最大度数称为树的度。
2)叶子结点(终端结点):度为0(没有子女结点)的结点称为叶子结点。
3)分支结点:度大于0的结点。
4)无序树和有序树:树中结点的各子树从左到右是有次序的,不能互换,称该树为有序树。否则称为无序树。
5)结点的深度:从根结点开始自顶向下逐层累加。
6)结点的高度:从叶子结点开始自底向上逐层累加。
7)树的高度(或深度):是树中结点的最大层数
8)路径:树中两个结点之间的路径是由这两个结点之间所经过的结点序列所构成的
9)路径长度:路径上所经过的边个数。其中树的路径长度是从树根到每个结点的路径长度之和。

树的性质

1)一棵有 n 个结点的树的所有结点的度数之和为 n-1 。
2)度为 m 的树中第 i i i 层上至多有 m i − 1 m^{i-1} mi1 个结点。
3)高度为 h 的 m 叉树至多有 ( m h − 1 ) ( m − 1 ) (m^h-1)(m-1) (mh1)(m1) 个结点。
4)具有 n 个结点的 m 叉树的最小高度为 ┌ log ⁡ m [ n ( m − 1 ) + 1 ] ┐ \ulcorner \log_{m}[n(m-1)+1] \urcorner logm[n(m1)+1]

1、符号 ┌ x ┐ \ulcorner x \urcorner x表示不小于x的最小正整数
2、符号 └ x ┘ \llcorner x \lrcorner x表示不大于x的最大正整数

总结:常用语求解树节点和度的之间的关系公式:
1、 总 结 点 数 = n 0 + n 1 + n 2 + . . . + n m 总结点数=n_0+n_1+n_2+...+n_m =n0+n1+n2+...+nm
2、 总 分 支 数 = 0 ∗ n 0 + 1 ∗ n 1 + 2 ∗ n 2 + . . . + m ∗ n m 总分支数=0*n_0+1*n_1+2*n_2+...+m*n_m =0n0+1n1+2n2+...+mnm
3、 总 结 点 数 = 总 分 支 数 + 1 总结点数=总分支数+1 =+1

一棵高度为 h h h 的满 m m m 叉树,若约定编号从根节点起,自上而下,自左而右。
1)第 i i i 层的结点个数为 m i − 1 m^{i-1} mi1
2)编号为 i i i 的结点的第 k k k 个孩子结点(若存在)是 ( i − 1 ) + m + k + 1 (i-1)+m+k+1 (i1)+m+k+1
3)编号为 i i i 的结点的双亲结点(若存在)的编号是 └ ( i − 2 ) m ┘ + 1 \llcorner\frac{(i-2)}{m}\lrcorner+1 m(i2)+1
4) 编号为 i i i 的结点的有右兄弟的条件是 ( i − 1 ) % m ≠ 0 (i-1)\%m\ne0 (i1)%m=0

5.2 二叉树

二叉树的定义

二叉树是另一种树形结构,其特点是每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点),并且二叉树的子树有左右之分,其次序不可颠倒。

二叉树与度为2的有序树的区别:
1)度为2的树至少有3个结点,而二叉树可以为空
2)度为2的有序树的孩子的左右次序是相对于另一个孩子而言的,若某个结点只有一个孩子,则这个孩子不需要区别左右次序。而二叉树无论其孩子数是否为2,准需确定其左右次序。

特殊的二叉树

1、满二叉树:一棵高度为 h h h 且含有 2 h − 1 2^h-1 2h1 个结点的二叉树称为满二叉树,即树中的每层都含有最多的结点。

  • 约定编号从根节点起,自上而下,自左而右。
  • 对于编号为 i i i 的结点,若有双亲结点,则其双亲为 └ i / 2 ┘ \llcorner i/2 \lrcorner i/2,即当 i i i
    为偶数时,其双亲的编号为 i / 2 i/2 i/2 ,他是双亲的左孩子;当 i i i 为奇数时,其双亲的编号为 ( i − 1 ) / 2 (i-1)/2 (i1)/2 ,它是双亲的右孩子。
  • 若有左孩子,则左孩子为 2 i 2i 2i
  • 若有右孩子,则右孩子为 2 i + 1 2i+1 2i+1

2、完全二叉树:一棵高度为 h h h 且含有 n n n 个结点的二叉树,当且仅当其每个结点都与高度为 h h h 的满二叉树中编号 1 − n 1-n 1n 的结点一一对应时,称为完全二叉树在这里插入图片描述

  • 完全二叉树的特点:
  • 1)若有度为 1 的结点,则只能有一个,且该结点只有左孩子没右孩子。
  • 2)若 n 为奇数,则每个分支结点都有左孩子和右孩子;
  • 3)若 n 为偶数,则编号最大的分支结点(编号为 n/2 )只有左孩子和右孩子;
  • 4)若 i < = └ n / 2 ┘ i <= \llcorner n/2 \lrcorner i<=n/2 ,则结点 i 为分支结点,否则是叶子结点。
  • 5)具有n个结点的完全二叉树的高度为 └ log ⁡ 2 n ┘ + 1 \llcorner \log_2{n} \lrcorner+1 log2n+1 ┌ log ⁡ 2 ( n + 1 ) ┐ \ulcorner \log_2{(n+1)}\urcorner log2(n+1)
  • 6)结点 i i i 所在层次(深度)为 └ log ⁡ 2 i ┘ + 1 \llcorner \log_2{i} \lrcorner +1 log2i+1
  • 7)对完全二叉树按进行编号,从根节点起,自上而下,自左而右,编号的规律和满二叉树一致

3、二叉排序树:左子树上的所有结点的关键字均小于根节点的关键字;右子树上的所有结点的关键字均大于根结点;左子树和右子树仍各是一棵二叉排序树。

4、平衡二叉树:树上的任一结点的左子树和右子树的深度之差不超过1。

二叉树的性质

1)非空二叉树的叶子结点数等于度为2的结点数+1,即 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
2)非空二叉树上第 k k k 层上至多有 2 k − 1 2^{k-1} 2k1 个结点( k ⩾ 1 k\geqslant1 k1
3)高度为 h h h 的二叉树至多有 2 h − 1 2^h-1 2h1 个结点( h ⩾ 1 h\geqslant1 h1

二叉树的实现

(一)顺序存储结构
用一组地址连续的存储单元依次自上而下、自左向右存储,其实也就是用数组,存储完全二叉树上的结点元素,顺序存储结构无法直接存储其他类型的二叉树,当想要储存其他类型的二叉树时,只有将此二叉树转变为完全二叉树才可实现。
如:不完全二叉树
如果我们想要存储上面的不完全二叉树,我们需要约定以“0”表示不存在改结点,将二叉树填充成完全二叉树

完全二叉树
则存储后得到的顺序表为:

12345000065

note:
1)编号规律和完全二叉树的性质保持一致。
2)这种存储结构建议从数组下标1开始存储。若从0开始,会无法根据性质来计算出孩子结点在数组中的位置

值得注意的是,顺序存储结构存储二叉树有可能导致空间极大的浪费,最坏的情况是,一个深度为 k k k且只有 k k k个结点的单支树却需要长度为 2 k − 1 2^k-1 2k1的一维数组。


#include <iostream>
using namespace std;

#define error -1	//因为C语言中没有true和false关键字,虽然C++里有但是这里还是额外定义一下
#define FALSE 0
#define TRUE 1
typedef int elemtype;	//定义数据元素类型,这样做的好处是想要修改类型时只需修改这句话就可以了
typedef int status;	//因为C语言编译器中一般不存在bool,所以这里的定义相当于C++的bool类型,代表函数返回的状态

#define init_tree_size 100	//二叉树申请到的空间大小
#define tree_increment 10
typedef struct {
	elemtype *data;
	int treesize;
}sqbitree;

status initbitree_sq(sqbitree &bt){
	bt.data=(elemtype*)malloc(init_tree_size*sizeof(elemtype));
	if(!bt.data)	return error;//如果申请失败,返回error信号
	bt.treesize=init_tree_size;
	printf("请你自上而下、自左向右输入二叉树的结点(-1为空结点,输入0结束):\n");
	for(int i=1;;i++){
		if(!bt.data[i]){//如果二叉树的空间已满,则需要增加二叉树分配的空间
			elemtype *newbaser=(elemtype *)realloc(bt.data,(bt.treesize+tree_increment)*sizeof(elemtype));
			if(!newbaser)return error;	//申请存储空间失败,返回错误信号
			bt.data=newbaser;//将搬家的新地址登记回来
			bt.treesize+=tree_increment;	//修改之前登记的家的信息
		}
		int temp;
		cin>>temp;
		if(temp==0)break;
		bt.data[i]=temp;
	}
	return TRUE;
}

int main(){
	sqbitree bt;
	initbitree_sq(bt);
	return 0;
}

(二)链式存储结构

由于顺序存储结构的空间利用率较低,因此二叉树一般都采用链式存储结构,用链表结点来存储二叉树中的每个结点。

在这里插入图片描述

#include <iostream>
using namespace std;
#include <algorithm>
#include <stack>    //这里因为不想代码过于臃肿,所以不应用自己定义的栈了,就用STL库函数里的栈就行了,下面的同理
#include <set>
#include <queue>

#define error -1	//因为C语言中没有true和false关键字,虽然C++里有但是这里还是额外定义一下
#define FALSE 0
#define TRUE 1
typedef char elemtype;	//定义数据元素类型,这样做的好处是想要修改类型时只需修改这句话就可以了
typedef int status;	//因为C语言编译器中一般不存在bool,所以这里的定义相当于C++的bool类型,代表函数返回的状态


struct bitnode{
	elemtype data;
	bitnode *lchild,*rchild;
};
typedef bitnode *bitree;
status createbitree(bitree &t){
	//构造二叉链表表示的二叉树
	//按先序次序输入二叉树中的结点的值(一个字符),输入*表示空树
	elemtype ch;
	cin>>ch;
	if(ch=='$')t=NULL;//这里为了简单使用输入$表示空树
	//如果要输入空格代表空结点,cin和scanf会出错,应该用gets函数,而且还需要用字符串作为数据元素类型才能存储空格符,或者可以采用文件输入的方式来实现,在这里就不一一更改了
	else{
		t=(bitree)malloc(sizeof(bitnode));
		if(!t)return error;//申请空间失败返回错误信号
		t->data=ch;	//因为使用的先序次序输入构成二叉树,所以可以参考先序遍历的调用方法
		createbitree(t->lchild);
		createbitree(t->rchild);
	}
	return TRUE;
}

int main(){
	bitree t;
	createbitree(t);	//测试样例二叉树:- + a $ $ * b $ $ - c $ $ d $ $ / e $ $ f $ $ 
	return 0;
}

5.3 二叉树的遍历

先序遍历

二叉树的先序遍历的操作过程如下:

若二叉树为空,则什么也不做,否则
1)访问根节点;
2)先序遍历左子树;
3)先序遍历右子树

例如:下面的二叉树先序遍历是:1 2 4 5 6 7 3
不完全二叉树

//这里是用递归的方法做先序遍历函数
status preorder_traverse(bitree t){//先序遍历
	if(t){//如果树结点不为空
		cout<<t->data;//访问根结点
		preorder_traverse(t->lchild);//先序遍历左子树
		preorder_traverse(t->rchild);//先序遍历右子树
	}
	return TRUE;
}

//这里是用栈的方法做先序遍历函数
status preorder_t2(bitree t){//先序遍历
    
    if(!t)    return error;//如果是空树则返回错误信号
    bitree p;
    stack<bitree> s;
    p=t;
    while(p||!s.empty()){
        if(p){
            //一路向左
            cout<<p->data;
            s.push(p);
            p=p->lchild;
        }
        else{//出栈并转向出栈结点的右子树
            p=s.top();
            s.pop();
            p=p->rchild;
        }
    }
    cout<<endl;
    return TRUE;
}

中序遍历

二叉树的中序遍历的操作过程如下:

若二叉树为空,则什么也不做,否则
1)中序遍历左子树;
2)访问根节点;
3)中序遍历右子树

例如:下面的二叉树中序遍历是:4 2 6 5 7 1 3
不完全二叉树

//这里是用递归的方法做中序遍历函数
status inorder_traverse(bitree t){//中序遍历
	if(t){//如果树结点不为空
		inorder_traverse(t->lchild);//中序遍历左子树
		cout<<t->data;//访问根结点
		inorder_traverse(t->rchild);//中序遍历右子树
	}
	return TRUE;
}

//这里是用栈的方法做中序遍历函数

status inorder_t2(bitree t){//中序遍历
    if(!t)    return error;//如果是空树则返回错误信号
    set<bitree> st;//对已经遍历过得结点组成一个集合,避免重复
    bitree p;
    stack<bitree> s;
    p=t;
    while(p||!s.empty()){
        if(p){
            s.push(p);
            p=p->lchild;//左孩子不空就一直往左走
        }
        else{
            //出栈并转向出栈结点的右子树
            p=s.top();
            cout<<p->data;
            s.pop();
            p=p->rchild;//
        }
    }
    cout<<endl;
    return TRUE;
}

后序遍历

二叉树的后序遍历的操作过程如下:

若二叉树为空,则什么也不做,否则
1)后序遍历左子树;
2)后序遍历右子树
2)访问根节点;

例如:下面的二叉树中序遍历是:4 6 7 5 2 3 1
不完全二叉树

//这里是用递归的方法做后序遍历函数
status postorder_traverse(bitree t){//后序遍历
	if(t){//如果树结点不为空
		postorder_traverse(t->lchild);//后序遍历左子树
		postorder_traverse(t->rchild);//后序遍历右子树
		cout<<t->data;//访问根结点
	}
	return TRUE;
}

//这里是用栈的方法做后序遍历函数
status postorder_t2(bitree t){//后序遍历
    if(!t)    return error;//如果是空树则返回错误信号
    bitree p,tag;
    stack<bitree> s;
    p=t;
    tag=NULL;//用来标记最近访问过的结点,避免重复访问
    while(p||!s.empty()){
        if(p){
            //走到最左边
            s.push(p);
            p=p->lchild;
        }
        else{
            p=s.top();
            if(p->rchild&&p->rchild!=tag){
                //如果根节点有右子树,先访问右子树
                p=p->rchild;
            }
            else{
                s.pop();
                cout<<p->data;
                tag=p;//记录最近访问的结点
                p=NULL;
            }
        }
    }
    cout<<endl;
    return TRUE;
}

后序遍历是最后访问的根节点,所以当要查找某个结点x的祖先,采用非递归后序遍历,第一次访问到x结点时,栈内的所有元素均为该结点的祖先

层次遍历

层次遍历是按照第1,2,3,4的层次顺序,对二叉树的各个结点进行访问

例如:下面的二叉树中序遍历是:1 2 3 4 5 6 7
不完全二叉树
层次遍历是需要队列进行辅助
1)先将根节点入队
2)出队,并访问出队结点
3)若队头结点有左子树,将左子树的根节点入队;
4)若队头结点有左子树,将左子树的根节点入队;
5)若队列还存在结点,则回到第二步

void levelorder(bitree t){//层次遍历
    queue<bitree> q;
    bitree p;
    q.push(t);
    while(!q.empty()){
        p=q.front();
        q.pop();
        cout<<p->data;
        if(p->lchild!=NULL){
            q.push(p->lchild);
        }
        if(p->rchild!=NULL){
            q.push(p->rchild);
        }
        
    }
    cout<<endl;
}

以上的遍历算法的时间复杂度和空间复杂度都是O(n)

由遍历序列构造二叉树

一、由二叉树的先序序列和中序序列可以确定唯一的一棵二叉树

步骤如下:
1.先序序列第一个结点一定是二叉树的根结点;
2.中序序列根结点必将中序序列分割成两个子序列;
3.前一项子序列是根结点的左子树的中序序列,后一项子序列是根结点的右子树的中序序列;
4.根据这两个子序列,在先序序列中排出左子树的先序序列,右子树的先序序列;
5.对左子树和右子树,分别递归的回到第1步,直到序列全部结束。

二、由二叉树的后序序列和中序序列可以确定唯一的一棵二叉树

与一类似,具体做法步骤如下:
1.后序序列最后一个结点一定是二叉树的根结点;
2.中序序列根结点必将中序序列分割成两个子序列;
3.前一项子序列是根结点的左子树的中序序列,后一项子序列是根结点的右子树的中序序列;
4.根据这两个子序列,在后序序列中排出左子树的后序序列,右子树的后序序列;
5.对左子树和右子树,分别递归的回到第1步,直到序列全部结束

三、由二叉树的层序序列和中序序列可以确定唯一的一棵二叉树

方法与上述方法类似,在这就不多加说明了

例题:求先序序列(ABCDEFGHI)和中序序列(BCAEDGHFI)所确定的二叉树
在这里插入图片描述
解析:
1)
A为根节点
1 左子树的中序序列为BC,先序序列为BC
2 右子树的中序序列为EDGHFI,先序序列为DEFGHI

2)A的左子树根节点为B,右子树的根节点为D

1 B为根节点
1.1 左子树的中序序列为空,故先序序也为空,分支结束
1.2 右子树的中序序列为C,先序序列为C,分支结束

2 D为根节点
2.1 左子树的中序序列为E,先序序列为E,分支结束
2.2 右子树的中序序列为GHFI,先序序列为FGHI

3)D的右子树根节点为F

3 F为根节点
3.1 左子树的中序序列为GH,先序序列为GH
3.2 右子树的中序序列为I,先序序列为I,分支结束

4)F的左子树的根节点G
4 G为根节点
4.1 左子树的中序序列为空,先序序列为空,分支结束
4.2 右子树的中序序列为H,先序序列为H,分支结束

Notes:
1)先序序列和中序序列的关系,相当于先序序列是入栈的顺序,中序序列是出栈的顺序。也就是说先序序列和中序序列的合理性可以用这个特性来加以判断。
2)只知道二叉树的先序序列和后序序列,无法确定唯一的一棵二叉树。但可以确定二叉树中结点的祖先关系,当两个结点的前序序列为 XY 、后序序列为 YX ,则 X 为 Y 的祖先。
3) 前序序列和后序序列刚好相反,则不可能存在一个结点同时有左右孩子,即 n 个二叉树的高度为 n 。
4)前序序列和后序序列刚好相同,则二叉树只有一个根节点。

5.4 线索二叉树

因为我们常规的二叉树中,叶子结点和度为1的结点都存在未被利用的指针。为了不造成没有必要的空间浪费,我们这里可以利用这些空指针,指示其前驱或者后继的结点,这样可以方便我们快速的找到结点,在某种序列中的前驱和后继的信息。

线索二叉树是一种物理结构,它是二叉树在计算机内部的一种存储结构。但二叉树是一种逻辑结构。

例如:下面的二叉树中序遍历是:4 2 6 5 7 1 3
不完全二叉树
在中序序列中,对于5来说,6是前驱,7是后继
如果我们想要找到5的前驱和后继我们只能再执行一遍中序遍历函数。所以为了加快找到结点的前驱和后继信息,我们产生了线索二叉树的概念

在含 n n n 个结点的二叉树中,有 n + 1 n+1 n+1 个空指针。
因为空指针为 2 n 0 + n 1 2n_0+n_1 2n0+n1
且在二叉树中 n 0 = n 1 + 1 n_0=n_1+1 n0=n1+1

线索二叉树规定:若无左子树,令 lchild 指向其前驱结点,若无右子树,令 rchild 指向其后继结点,我们还需增加两个标志域 ltag 、rtag 标识指针是指向的孩子结点,还是前驱后继信息。

在这里插入图片描述
其中
l t a g = { 0 , l c h i l d 指 向 的 是 结 点 的 左 孩 子 1 , l c h i l d 指 向 结 点 的 前 驱 ltag=\begin{cases} 0 , lchild指向的是结点的左孩子\\ 1, lchild指向结点的前驱\\ \end{cases} ltag={0lchild1lchild
r t a g = { 0 , r c h i l d 指 向 的 是 结 点 的 右 孩 子 1 , r c h i l d 指 向 结 点 的 后 继 rtag=\begin{cases} 0 , rchild指向的是结点的右孩子\\ 1, rchild指向结点的后继\\ \end{cases} rtag={0rchild1rchild

注意:线索二叉树并不能帮助每个结点通过线索可以直接找到它的前驱和后继。对于一些结点来说,它只是能加速找到前序和后继的过程而已。

typedef struct ThreadNode{
    elemtype data;
    ThreadNode *lchild,*rchild;
    int ltag,rtag;
}ThreadNode,*ThreadTree;

如何在线索二叉树中到结点的后继?

1、先序线索二叉树:

  • 如果有左孩子,则左孩子就是其的后继;
  • 如果没有左孩子,但有右孩子,则右孩子是其后继;
  • 如果为叶子结点,则可以直接看右指针,右指针指向了结点的后继

2、中序线索二叉树:

  • 若结点x,其右标志位为’1’,则其指针所指向的就是后继结点,
  • 若结点x,其右标志位为’0’,则其后继结点为遍历右子树中第一个访问到的结点(也就是右子树中最左下的结点)

3、后序线索二叉树:

  • 若结点x是二叉树的根,则其后继为空
  • 若结点x是其双亲的右孩子,或是其双亲的左孩子并且其双亲没有右子树,则其后继即为双亲
  • 若结点x是其双亲的左孩子,且其双亲有右子树,则其后继为双亲的有右子树后序遍历列出的第一个结点

从后序线索二叉树的寻找规律,我们可以得知,想要找到结点X的后继我们需要知道该结点的双亲结点
所以在求后序后继结点的问题单靠后序线索二叉树无法解决,且后序线索树的遍历仍需要栈的支持。

线索二叉树中会存在空指针吗?
会的。我们可以注意到无论任何序列,我们第一个结点都是没有前驱,而最后一个结点都是没有后继的。所以如果我们不进行任何处理,我们的线索二叉树有可能会存在空指针。
例如:一棵左子树的二叉树进行先序线索化,根结点没有前驱且左子树为空,先序遍历的最后一个元素为叶子结点,左右子树均为空且无后继结点,故线索化后,仍有两个空指针未被应用。

如果我们不想要线索二叉树有空指针的存在,我们还可以引进一个头结点来避免该情况的发生。将两个空指针导向头结点,头结点的 lchild 指针指向二叉树的根节点,而 rchild 指针指向遍历时访问的最后一个结点。

5.5 树和森林

树的存储结构

1、双亲表示法(顺序存储结构)
这种存储方式是用一组连续空间来存储每个结点,同时再每个结点中增设一个伪指针,指示其双亲结点的位置。
请添加图片描述

#define MAX_TREE_SIZE 100	//树中最多结点数
typedef struct{		//树的结点
	ElemType data;	//结点元素
	int parent;		//双亲位置指针
}PTNode;
typedef struct{
	PTNode nodes[MAX_TREE_SIZE];//双亲表示法的表格	
	int n;	//树的结点数
}PTree;

该存储结构利用了每个结点(根节点除外)只有唯一一个双亲的特点,可以很快的得到每个结点的双亲结点,但求结点的孩子的时候需要遍历整个结构。

2、孩子表示法(链式存储结构)

孩子表示法将每个结点的孩子结点都用单链表连接起来形成线性结构,比如上面的树的孩子表示法如下图所示:

在这里插入图片描述
在这里插入图片描述

3、孩子兄弟表示法(二叉树表示法)(链式存储结构)

孩子兄弟表示法使每个结点包括三部分内容:结点值、指向结点第一个孩子结点的指针,及指向结点下一个兄弟结点的指针(沿此指针路径可以找到该结点的所有兄弟结点)
如下例所示:
在这里插入图片描述
在这里插入图片描述
孩子兄弟表示法的存储结构描述如下:

typedef struct CSNode{
	ElemType data;
	struct CSNode *firstchild,*nextsibling//第一个孩子和右兄弟指针
}CSNode,*CSTree;

这种存储表示法最大的优点是可以方便的实现树转换为二叉树的操作,易于查找结点的孩子。但缺点是从当前结点查找双亲结点比较麻烦。但若每个结点都增设一个parent指针指向父结点,则查找结点的父结点也很方便。

树和森林与二叉树的转换

和上面的孩子兄弟表示法一样,给定一棵树,可以找到唯一的一棵二叉树对应。从物理结构上,它们的二叉链表是相同,只是解释不同。

树转换为二叉树的规则:每个结点左指针指向它的第一个孩子,右指针指向它在树中的相邻右兄弟。
请添加图片描述

森林转换成二叉树的规则:在将每棵树转换成二叉树的基础上,将每棵树的根视为兄弟关系,用右指针连接。
在这里插入图片描述

森林转换成二叉树时,若森林中含有 n 个非终端结点(非叶子结点),则转换成的二叉树中不含有右孩子的结点数为 n+1
原因:
n 个非叶子结点代表,他们都有孩子结点,所以肯定会存在一个孩子为处于孩子结点中的最后一位,不存在右兄弟,所以自然转换后,就有 n 个不含有右孩子的结点数
并且在森林的根结点中,最后一棵树的根结点转换后也不会有右孩子,所以最终答案是 n+1

树和森林的遍历

树的遍历有三种:

1)先根遍历
若树非空,先访问根结点,再依次遍历根结点的每棵子树,遍历子树时仍遵循先根后子树的规则。其遍历序列与这树相应的二叉树先序序列相同

2)后根遍历
若树非空,先依次遍历根结点的每棵子树,再访问根结点,遍历子树时仍遵循先子树后根的规则。其遍历序列与这树相应的二叉树中序序列相同

3)层次遍历
若树非空,按层序依次访问各结点。

例如:
请添加图片描述

上面的树的先根遍历序列为ABEFCDG,后根遍历序列为EFBCGDA

森林的遍历同理:

1)先序遍历森林

  • 访问森林中第一棵树的根节点。
  • 先序遍历第一棵树中根结点的子树森林
  • 先序遍历除去第一棵树后剩下的树构成的森林

其遍历序列与该森林相应的二叉树先序序列相同

2)中序遍历森林

  • 中序遍历第一棵树中根结点的子树森林
  • 访问第一棵树的根节点
  • 中序遍历除去第一棵树后剩下的树构成的森林

其遍历序列与该森林相应的二叉树中序序列相同

也有些书将森林的中序遍历称为后根遍历

在这里插入图片描述

上图的深林先序遍历序列为ABCDEFGHI,中序遍历序列为BCDAFEHIG

5.6 树和二叉树的应用

二叉排列树

具有以下特性的二叉树:
左子树的结点值 < 根结点值 < 右子树的结点值
称为二叉排列树
如下图的中序遍历序列为 1 2 3 4 6 8
在这里插入图片描述
二叉排序树的查找
二叉排序树在查找结点比普通二叉树有优势

查找的过程如下:
1)若二叉树非空,将查找的值和根结点的关键字比较,若相等,则查找成功;
2)若不等,如果小于根结点的关键字,则在根结点的左子树上查找
3)否则在根结点的右子树上查找
4)递归到找关键字或者二叉树结束为止。

bitnode *BST_Search(bitree T,elemtype key){
    while (T!=NULL&&key!=T->data){
        if(key<T->data)T=T->lchild;//小于在左子树上查找
        else{
            T=T->rchild;//大于在右子树上查找
        }
    }
    return T;
}

二叉排序树的插入
二叉排序树的插入肯定和普通二叉树不一样,二叉排序树的内部需要服从规律。

插入结点的过程:
1)若原二叉排序树为空,则直接插入结点;
2)若关键字 k 小于根结点值,则插入到左子树;
3)若关键字 k 大于根结点值,则插入到右子树;

插入的结点一定是一个新添加的叶子结点,且是查找失败时查找路径上访问到的最后一个结点的左孩子和右孩子。

status BST_Insert(bitree &t,elemtype key){
    if(t==NULL){//原树为空直接插入即可
        t=(bitree)malloc(sizeof(bitnode));
        t->data=key;
        t->lchild=t->rchild=NULL;
        return TRUE;
    }
    else if(key==t->data){//树中存在相同关键字的结点
        return FALSE;//插入失败提示
    }
    else if(key<t->data){
        return BST_Insert(t->lchild,key);
    }
    else{
        return BST_Insert(t->rchild,key);
    }
}

二叉排序树的删除
在二叉排序树中删除一个结点,肯定是不能直接把以该结点为根的子树全删除,必须先把被删除结点从存储二叉排序树的链表上摘下,将因删除结点而断开的二叉链表重新链接起来,同时确保二叉排序树的性质不会丢失。

删除操作的过程:
1)若被删除结点是叶子结点,直接删除即可。
2)若删除结点 z 只有一棵左子树或右子树,则让 z 的子树称为 z 父结点的子树,替代 z 的位置即可
3)若删除结点 z 既有左子树又有右子树,则令 z 的直接后驱或直接前驱替代 z ,然后从二叉排序树中删除这个直接后驱或直接前驱,从而达到转换成第一和第二种情况的目的
在这里插入图片描述

我们可以注意到有时候二叉排序树的高度增长过快,导致我们查找结点的效率极速下降,如下两棵相同关键字组成的二叉排序树:
请添加图片描述
所以我们还需要引进平衡二叉排序树的概念。

普通的二叉排序树平均查找时间复杂度为 O ( n ) O(n) O(n)
平衡二叉排列树平均查找时间复杂度为 O ( l o g 2 n ) O(log_2{n}) O(log2n)

平衡二叉树

平衡二叉树的定义
规定在插入和删除二叉树二叉树结点时,要保证任意结点的左、右子树高度差的绝对值不超过1,将这样的二叉树称为平衡二叉树(AVL树),简称平衡树。
定义结点左子树与右子树的高度差为该结点的平衡因子,则平衡二叉树结点的平衡因子的值只可能是 -1,0,1
平衡二叉树的插入
平衡二叉树每当插入(或删除)一个结点时,首先要检查其插入路径上的结点是否会因为此次操作导致不平衡。
若导致不平衡,则先找到插入路径上离插入结点最近的平衡因子的绝对值大于1的结点A,再对以A为根的子树,在保持二叉排序树特性的前提下,调整各结点的位置关系。
调整规律如下:
1)LL 平衡旋转(右单旋转):由于在结点 A 的左孩子(L)的左子树(L)上插入新结点,A 的平衡因子由 1 增至 2 ,导致为根的子树失去平衡,需要一次向右旋转的操作。将 A 的左孩子 B 右上旋替代 A 称为新的根节点,将 A 结点右下旋转成为 B 的右子树的根结点,而 B 的原右子树则作为 A 结点的左子树,如下图所示:
在这里插入图片描述
2)RR 平衡旋转(左单旋转):由于在结点 A 的右孩子(R)的右子树(R)上插入新结点,A 的平衡因子从 -1 减至 -2,导致以 A 为根的子树失去平衡,需要一次向左旋转的操作。将 A 的右孩子 B 向左上旋转替代 A 成为根结点,将 A 结点向左下旋转成为 B 的左子树的根结点,而原 B 的左子树则作为 A 结点的右子树。如下图所示

在这里插入图片描述
3)LR 平衡旋转(先左后右双旋转):由于在结点 A 的左孩子(L)的右子树(R)上插入新结点,A 的平衡因子由 1 增至 2,导致以 A 为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转。先将 A 结点的左孩子 B 的右子树的根结点 C 向左上旋转提升到 B 结点的位置,然后把该 C结点右上旋转提升到 A 结点的位置。如下图所示:
请添加图片描述
4)RL平衡旋转(先右后左双旋转):由于在结点 A 的右孩子(R)的左子树(L)上插入新结点,A 的平衡因子从 -1 减至 -2 ,导致以 A 为根的子树失去平衡,需要进行两次旋转操作,先右旋转后左旋转。先将 A 结点的右孩子 B 的左子树的根结点 C 向右上旋转提升到 B 结点的位置,然后把该 C 结点向左上旋转提升到 A 结点的位置。如下图所示:
请添加图片描述

LR 和 RL 旋转时,新结点无论是插入 C 的左子树还是插入 C 右子树不影响旋转的过程,上面的示例图都是以插入 C 的左子树为例。

虽然二叉排序树和平衡二叉树查找过程相同,而且在查找过程中,比较的关键字个数都不会超过树的深度
但由于普通的二叉排列树有可能会由于序列的问题导致深度退变,最差情况达到 n 个结点 n 层深度的情况。
而假设以 N h N_h Nh 表示深度为 h h 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=Nh1+Nh2+1。(该关系和斐波那契数列极为相似)
由上面的的结论结合归纳法得出,在平衡二叉树进行搜索算法的时间复杂度为 O ( l o g 2 n ) O(log_2{n}) O(log2n)

哈夫曼树(最优二叉树)

树中结点常常被赋予一个表示某种意义的数值,称为该结点的权。从树的根结点到任意结点的路径长度(经过的边数)与该结点上权限乘积,称为该结点的带权路径长度。树中所有叶子结点的带权路径长度之和称为该树的带权路径长度,记为
W P L = ∑ i = 1 n w i l i WPL=\sum_{i=1}^n w_il_i WPL=i=1nwili
其中, w i w_i wi 是第 i i i 个叶子结点所带的权值, l i l_i li是该叶子结点到根结点的路径长度。
在含有 n 个带权叶结点的二叉树中,其中带权路径最小的二叉树称为哈夫曼树。
例如:下图3棵二叉树都有4个叶子结点 a,b,c,d,分别带权 7,5,2,4
在这里插入图片描述
其中可以酸菜 c 树的 WPL 最小。可以验证,它恰好为哈夫曼树

如何构造哈夫曼树
1)将这 n 个结点分别作为 n 棵仅含一个结点的二叉树,构成深林 F
2)构造一个新结点,从 F 中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
3)从 F 中删除刚才选出的两棵树,同时将新得到的树加入 F 中
4)重复第二和第三步,直到 F 中只剩一棵树为止

例如:权值为 7,5,2,4 的哈夫曼树构造
在这里插入图片描述
哈夫曼树有以下特点:
1)每个初始结点最终都成为叶子结点,并且权值越小的结点到根结点路径长度越大
2)哈夫曼树的结点总数为 2 n − 1 2n-1 2n1
3)每次构造都选择两棵树作为新节点的孩子,因此哈夫曼树中不存在度为 1 的结点

哈夫曼编码

假设我们要传送电文 ‘ABACCDA’ 它只有四种字符,只需两个字符的串即可区分出这四种字符,假设A,B,C,D所对应的编码为00,01,10,11,则我们要传送的电文是‘00010010101100’。这种采用相等二进制位表示的编码方式称为固定长度编码。

我们在传送电文的时候肯定希望这个总长能尽可能的短。如果我们给每个字符采用长度不等的编码表示,使电文中出现的次数多的字符采用尽可能短的编码,则传送的电文总长便可随之减少。

但我们要注意:例如给 A,B 编码我们不能取 0,00 ,因为这样编码会产生异议,例如‘0000’,我们可以认为他是 ‘ABA’ 或者 ‘BB’ 或者’ BAA’ 等。

所以我们在设计长度不同的编码,必须是任一个字符的编码都不是另一字符的编码的前缀,这种编码叫前缀编码。

我们利用哈夫曼树可以获得使电文最短的二进制前缀编码,也就是哈夫曼编码
例如:我们要传送 100 个字符的电文,其中字符 a 出现45次,b 出现13次,c 出现12次,f 出现5次,e 出现9次,d 出现15次。
如下图所示,我们可以利用哈夫曼树导出该电文应该对应的哈夫曼编码
在这里插入图片描述

构造出来的哈夫曼树并不唯一,因为 0 和 1 究竟是表示左子树还是右子树没有明确规定,而且左、右孩子结点的顺序是任意的。但各哈夫曼树的带权路径长度 WPL 相同且为最优。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值