数据结构与算法
第一章 绪论
第二章 线性表
第三章 树与二叉树
第四章 图
第五章 查找
第六章 排序
文章目录
第三章 树与二叉树
一、基本术语
树的构造性递归定义
树的逻辑结构特点:一对多的关系
结点的度:结点所具有的子树的个数
树的度:树中各结点度的最大值
叶子结点:度为0的结点
分支结点:度不为0的结点
孩子结点:树中某结点的子树的根结点称为为个结点的孩子结点,这个结点称为它孩子结点的双亲结点(父结点)
兄弟:具有同一个双亲的孩子结点
路和路长度
在树中,如果有一条路从结点x到结点y,那么x就称为y的祖先,而y称为x的子孙
结点的层数,根结点的导数为1
树的深度:树中所有结点的最大层数,也树的高度
有序数、无序树
森林:m棵互不相交的树的集合
线性结构与树弄结构的比较
二、二叉树
每个结点最多只有两棵子树,即结点的度不大于2
子树有左右之别,子树的次序不能颠倒,即使某结点只有一棵子树,也有左右之分
特殊的树:
- 左斜树:所有结点都只有左子树的二叉树称为左斜树
- 右斜树:所有结点都只有左子树的二叉树称为右斜树
- 左斜树和右斜树统称为斜树
- 在斜树中,每一层只有一个结点,斜树的结点个数与其高度相同
- 满二叉树:高为 h h h且有 2 h − 1 2^h-1 2h−1个结点的二叉树
- 完全二叉树:
- 所有的叶子结点都出现在k或k-1层
- k-1层的所有叶都在非终端结点的右边
- 除了k-1的最右非终端结点可能有一个(只能是左分支)或两个分支之外,其余非终端结点都有两个分支
- (编号时不会出现空缺)
- 二叉查找树(BST树):
- 又叫做二叉搜索树、二叉分类(排序)树
- 若它的左子树不空,则左子树所有结点的关键字的值都小于根结点关键字的值
- 若它的右子树不空,则右子树所有结点的关键字的值都大于根结点关键字的值
- 它的左、右子树本身又是一个二叉查找树
- 平衡二叉树(AVL树):
- 是一棵BST树
- 根结点的左、右子树高度之差的绝对值不超过1
- 且根结点左子树和右子树仍然是AVL树
二叉树的性质
- 性质1:二叉树的第 i i i层最多有 2 i − 1 2^{i-1} 2i−1个结点
- 性质2:高度为 k k k的二叉树最多有 2 k − 1 2^k-1 2k−1个结点,最少有 k k k个结点
- 性质3:在非空二叉树中,如果叶子结点数为 n 0 n_0 n0,度为2的结点树为 n 2 n_2 n2,则有 n 0 = n 2 + 1 n_0 = n_2 +1 n0=n2+1,而与度数为1的结点数 n 1 n_1 n1无关
- 性质4:具有 n n n个结点的完全二叉树的高度为 log 2 ( n + 1 ) \log_{2}{(n+1)} log2(n+1)或 log 2 n + 1 \log_{2}{n} + 1 log2n+1
- 性质5:完全二叉树的顺序存储结构
- 儿子:2n或2n+1
- 父亲:n/2
二叉树的遍历
遍历:根据某种策略,按照一定的次序访问二叉树中的每一个结点,使每个结点被访问一次且只被访问一次,这个过程称为二叉树的遍历
- 先序遍历:根->左->右
- 中序遍历:左->根->右
- 后序遍历:左->右->根
- 层序遍历
恢复算法:
只要有中序,加上前序或后序,就可唯一恢复一棵二叉树
但只有前序和后序不可以恢复,因为任何结点只有左子树的二叉树和任何结点只有右子树的二叉树其前序后序序列都相同,却是两棵不同的二叉树
二叉树的基本操作
Empty(BT) //建立一棵空二叉树
IsEmpty(BT)
CreateBT(V,LT,RT)
Lchild(BT)
Rchild(BT)
Data(BT)
二叉树的存储结构
顺序存储:
完全二叉树:采用一维数组,按层序顺序依次存储二叉树的每一个结点。
一般二叉树:通过虚设部分结点,使其变成相应的完全二叉树。根据性质 5 ,如已知某结点的层序编号 i 则可求得该结点的双亲结点、左孩子结点和右孩子结点,然后检测其值是否为虚设的特殊结点 。
这种方法可能会浪费大量空间(如斜树)
链式存储(动态二叉链表):
结点结构:data :数据域,存放该结点的数据信息;lchild :左指针域,存放指向左孩子的指针;rchild :右指针域,存放指向右孩子的指针。
具有
n
n
n个结点的二叉链表中,有
n
+
1
n+1
n+1个空指针
存储结构定义:
struct node{
struct node *lchild;
struct node *rchild;
datatype data;
};
typedef struct node *BTREE;
创建二叉树:
方法一:
BTREE CreateBT(datatype v,BTREE ltree, BTREE rtree){
BTREE root;
root = new node;
root->data = v;
root->lchild = ltree;
root->rchild = rtree;
return root;
}
方法二:
按先序序列输入 :ABDH##I##E##CF#J##G##其中#表示空
void CreateBT(BTREE &T)
{
cin >> ch;
if (ch == '#')
T = NULL;
else{
T = new node;
T->data = ch;
CReateBT(T->lchild);
CReateBT(T->rchild);
}
}
方法三:方法二的非递归写法
struct node *s[max];
BTREE CreateBT(){
int i,j;
datatype ch;
struct node *bt,*p;
cin >> i >> ch;
while ( i!=0 && ch!=0){
p = new node;
p->data = ch;
p->lchild = NULL;
p->rdchild = NULL;
s[i] = p;
if( i == 1)
bt = p;
else{
j = i/2;
if(i%2 == 0)
s[j]->lchild = p;
else
s[j]->rchild = p;
}
cin >> i >> ch;
}
}
遍历二叉树:
递归先序:
void PreOrder(BTREE BT){
if (BT!=NULL){
cout << BT->data;
PreOrder(BT->lchild);
PreOrder(BT->rchild);
}
}
递归中序:
void InOrder(BTREE BT){
if(BT!=NULL){
InOrder(BT->lchild);
cout << BT->data;
InOrder(BT->rchild);
}
}
递归后序:
void PostOrder(BTREE BT){
if (BT!=NULL){
PostOrder(BT->lchild);
PostOrder(BT->rchild);
cout << BT->data;
}
}
非递归前序:
void PreOrder(BTREE root){
top = -1;
while(root!=NULL || top!=-1){
while(root!=NULL){
cout << root->data;
s[++top] = root;
root = root->lchild;
}
if(top != -1){
root = s[top--];
root =root->rchild;
}
}
}
非递归中序:
void InOrder(BTREE root){
top = -1;
while(root!=NULL || top!=-1){
while(root!=NULL){
s[++top] = root;
root = root->child;
}
if(top != -1){
root = s[top--];
cout << root->data;
root =root->child;
}
}
}
非递归后序:
void PostOrder(BTREE root){
top = -1;
while(root != NULL || top !=-1){
while(root != NULL){
s[top++].ptr = root;
s[top].flag = 1;
root = root->child;
}
while(top != -1 && s[top].flag == 2){
root = s[top--].ptr;
cout << root->data;
}
if (top != -1){
s[top].flag =2;
root = s[top].ptr->child;
}
}
}
层序遍历:
void LevelOrder(BTREE root){
front = rear = 0;
if(root == NULL)
return ;
Q[++rear] = root;
while (front != rear){
q = Q[++front];
cout << q->data;
if(q->lchild != NULL)
Q[++rear] = q->lchild;
if(q->rchild != NULL)
Q[++rear] = q->rchild;
}
}
遍历算法应用
1、计算二叉树结点个数的递归算法:
int count(BTREE T){
if (T == NULL)
return 0;
else return 1 + count(t->lchild) + count(t->rchild;)
}
2、求二叉树高度的递归算法
int height(BTREE T){
if(T == NULL)
return 0;
else{
int m = height(T->lchild);
int n = height(T->rchild);
return (m>n)? (m+1) : (n+1);
}
}
3、交换二叉树所有结点子树的算法
递归:
void change(BTREE T){
if (T == NULL)
return ;
else{
node *tem = new node;
tem = T->lchild;
T->lchild = T->rchild;
T->rchild = tem;
change(T->lchild);
change(t->rchild);
}
}
非递归
void change(BTREE T){
struct node *p,*tmp;
top = -1;
if(T!=NULL){
s[++top] = T;
while(top != -1){
p = s[top--];
tmp = p->lchild;
T->lchild = T->rchild;
T->rchild = tem;
if(p->lchild != NULL)
s[++top] = p->lchild;
if(p->rchild != NULL)
s[++top] = p->rchild;
}
}
}
4、按先序打印二叉树中的叶子结点
void PreOrderPrnLeaf(BTREE T){
if(T){
if(!T->lchild && !T->rchild)
cout << T->data;
PreOrderPrnLeaf(T->lchild);
PreOrderPrnLeaf(T->rchild);
}
}
动态三叉链表
在二叉链表的基础上增加了一个指向双亲的指针域。
静态二叉链表与静态三叉链表
线索二叉树
传统的二叉链表存储方式(二叉树)只能体现父子关系,无法体现结点在遍历过程中的前驱和后继关系
若结点 p 有左孩子,则p ->lchild指向其左孩子结点,否则令其指向其(先序、中序、后序、层序)前驱
若结点 p 有右孩子,则p ->rchild指向其右孩子结点,否则令其指向其(先序、中序、后序、层序)后继
在每个结点中增加两个 标志位 ,以区分该结点的的两个链域是指向其左 右孩子还是指向 某种 遍历的前驱 后继。
struct Node{
datatype data;
struct Node *lchild,*rchild;
bool ltag,rtag;
}
typedef struct Node * THTREE;
在中序二叉树中求一个结点p的中序后继p:
THTREE InNext(THTREE p){
THTREE q;
q = p-> rchild;
if(p->rtag == true){
while(q->ltag == true)
q = q->lchild;
}
return q;
}
利用InNext算法,中序遍历线索二叉树:
void InOrderTh (THTREE head)
{
THTREE tmp ;
tmp = head
do {
tmp =InNext ( tmp )
if (tmp != head)
visit ( tmp-> data )
} while (tmp != head );
}
求中序线索二叉树中结点p的先序序列的后继结点 p:
THTREE PreNext(THTREE p){
THTREE q;
if(p->ltag == true)
q = p->lchild;
else{
q = p;
while(q->tag == false){
q = q->rchild;
}
q = q->rchild;
}
return q;
}
中序线索二叉树的插入算法:
二叉树的(中序)线索化算法 递归算法
// I am so sleepy!
// The class of Data Structure and algroithm is so boring!
二叉树的复制
具有相同结构的二叉树叫做相似二叉树
相似且对应结点包含相同信息的二叉树称为等价二叉树
三、堆(Heap)
ADT堆
如果一棵 完全二叉树 的任意一个非终端结点的元素都 不小于 其左儿子结点和右儿子结点(如果有的话)的元素,则称此完全二叉树为 最大堆大顶堆、大根堆
如果一棵 完全二叉树 的任意一个非终端结点的元素都 不大于 其左儿子结点和右儿子结点(如果有的话)的元素,则称此完全二叉树为 最小堆小顶堆、小根堆 )。
基本操作
MaxHeap(maxsize):创建一个空堆
HeapFull(heap,n):判断堆是否为满
Insert(heap,item,n):插入一个元素
HeapEmpty(heap,n):判断堆是否为空
DeleteMax(heap,n):删除最大元素
ADT的存储结构
可以采用完全二叉树的数组表示
#define Maxsize 200
typedef struct{
int key;
}ElemType;
typedef struct{
ElemType data[Maxsize];
int n;
}HEAP;
ADT基本操作的实现
1、创建空堆
void MaxHeap(HEAP heap){
heap.n = 0;
}
2、判空
bool HeapEmpty(HEAP heap){
return (!heap.n);
}
3、判满
bool HeapFull(HEAP heap){
return (heap.n == Maxsize);
}
4、插入
void Insert(HEAP &heap, ElemType elem){
int i ;
if(!HeapFull(heap)){
i = heap.n + 1;
while((i!=1)&&(elem > heap.eata[i/2])){
heap.data[i] = heap.data[i/2];
i/=2;
}
}
}
heap.data[i] = elem;
5、删除最大元素
ElemType DeleteMax(HEAP &heap){
int parent = 1;child = 2;
ElemType elem,tmp;
if(!HeapEmpty(heap)){
elem = heap.data[1];
tmp = heap.data[heap.n--];
while(child <= heap.n){
if((child < heap.n) && (heap.data[child]<heap.data[child+1]))
child++;
if(tem>=heap.data[child])
break;
heap.data[parent] = heap.data[child];
parent = child;
child*=2;
}
heap[parent] = tem;
return elem;
}
}
四、选择树(也称Tournament Tree)
选择树 就是能够记载上一次比较获得的知识的完全二叉树
种类:胜者树和败者树。
胜者树:
五、树
1、树的基本操作
Parent(n, T):求结点n的双亲
LeftMostChild(n, T):返回结点n的最左儿子
RightSibling(n, T ):返回结点n的右兄弟
Data(n, T ):回结点n的信息
CreateK k (v, T1, T2,……, Tk) , k = 1,2,……
Root(T)返回树T的根结点
用树的基本操作写先序遍历的递归算法:
void PreOrder(node n, TREE T){
node c;
visit(Data(n));
c = LeftMostChild(n,T);
while(c!=NULL){
PreOrder(c,T);
c = RightSibling(c,T)l
}
}
2、树的存储结构
1、双亲表示法
2、孩子链表表示法(邻接表表示)
把每个结点的孩子结点组成是一个单链表,则n个结点共有n个孩子链表。
再把每个单链表的表首结点指针,组织成一个顺序表便于进行查找。
最后,将存放n个表首结点指针的数组和存放 n个结点的数组结合起来,构成孩子链表的表头数组。
struct CTNode{
int child;
CTNode *next;
};
struct CTBox{
DataType data;
CTNode *firstchild;
};
struct {
CTBox nodes[MaxSize];
int n,r;
}CTree;
3、二叉链表表示法(左孩子右兄弟链表表示法)
某结点的右兄弟是唯一的
设置两个分别指向该结点的第一个孩子和右兄弟的指针
遍历 | 树 | 二叉树 |
---|---|---|
先序 | ABEFCGLDHIMJ | ABEFCGLDHIMJ |
中序 | EBFAKGLCHDMIJ | EFBKLGCHMIJDA |
后序 | EFBKLGCHMIJDA | FELKGMJIHDCBA |
struct CSNode{
DataType data;
CSNode *firstchild,*rightsib;
};
typedef struct CSNode *CSTREE;
六、森林(树)与二叉树间的转换
左儿子右兄弟
七、树的应用
哈夫曼树
路径长度:从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称为路径长度
树的路径长度:从树根到每一结点的路径长度之和
假设有n个权值{
w
1
,
w
2
,
…
,
w
n
w_1,w_2,\dots,w_n
w1,w2,…,wn},构造一棵有n个叶子结点的二叉树,每个叶子结点带权
w
k
w_k
wk,每个叶子结点的路径长度为
l
k
l_k
lk,我们将 带权路径长度 WPL 最小的二叉树称作哈夫曼树(最优二叉树)。
哈夫曼树不唯一,但 WPL唯一
只有度为 0 (叶子结点)和度为 2 (分支结点)的结点,不存在度为 1 的结点。
n个叶结点的哈夫曼树的结点总数为 2n-1个。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uqu8Slo1-1650244886958)(image/2021-10-15-14-31-29.png)]
存储结构:静态三叉链表
typedef struct{
double weight;
int lchild;
int rchild;
int parent;
}HTNODE;
typedef HTNODE HuffmanT[2n-1];
构造算法的实现:
void CreatHT(GuffmanT T){
int i,p1,p2;
Init(T);
InputW(T);
for (int i = n;i<2n-1;i++){
SelectMin(T,i-1,&p1,&p2);
T[p1].parent = T[p2].parent = i;
T[i].lchild = p1;
T[i].rchild = p2;
T[i].weight = T[p1].weight + T[p2].weight;
}
}
哈夫曼编码
设需要编码的字符集为 {d 1 ,d 2 ,…,d n },各个字符在电文中出现的次数或频率集合为 {w 1 ,w 2 ,…w n,以 d 1 ,d 2 ,…,d n 作为叶子结点,以 w 1 ,w 2 ,…w n 作为相应叶子结点的权值来构造一棵赫夫曼树。规定赫夫曼树的左分支代表 0 ,右分支代表 1则从根结点到叶子结点所经过的路径分支组成的 0 和 1 的序列便为该结点对应字符的编码,这就是赫夫曼编码 。
编码的前缀性:如果一组编码中,任意一个编码 都不是其它任何一个编码的前缀 ,则称这种编码具有前缀性,简称前缀码 。
对于给定的字符集及其每个字符出现的概率 使用频度求该字符集的最优的前缀性编码哈夫曼编码问题