声明:本博客仅为本人学习途中做的笔记 采自青岛大学王卓老师的视频教学 主要内容为算法思路,具体代码实现还需修改后才能运行,望各位看官多多包涵,您的点赞与评论是对我最大的肯定!
数 据 的 逻 辑 结 构 { 线 性 结 构 { 线 性 表 栈 ( 特 殊 线 性 表 ) 队 列 ( 特 殊 线 性 表 ) 字 符 串 、 数 组 、 广 义 表 非 线 性 结 构 { 树 形 结 构 图 形 结 构 数据的逻辑结构 \begin{cases} 线性结构\begin{cases} 线性表 \\ 栈(特殊线性表) \\ 队列(特殊线性表) \\ 字符串、数组、广义表 \end{cases} 非线性结构\begin{cases} 树形结构 \\ 图形结构 \end{cases} \end{cases} 数据的逻辑结构⎩⎪⎪⎪⎨⎪⎪⎪⎧线性结构⎩⎪⎪⎪⎨⎪⎪⎪⎧线性表栈(特殊线性表)队列(特殊线性表)字符串、数组、广义表非线性结构{树形结构图形结构
树(Tree)是n(n>=0)个结点的有限集。
若n=0,称为空树
若n>0,则它满足如下两个条件:
(1) 有且仅有一个特定的称为根的结点
(2)其余结点可分为m(m>=0)个互不相交的有限集T1, T2, T3, …,Tm,其中每一个集合本身又是一棵树,并称为根的子树(SubTree)
根结点:非空树中无前驱结点的结点
结点的度:结点拥有的子树数
树的度:树内各节点的度的最大值
度$\neq$0:分支结点 非终端结点 根结点以外的分支结点称为内部结点
度 = 0:叶子 终端结点
结点的子树的根称为该结点的孩子,该结点称为孩子的双亲
结点的祖先:从根到该结点所经分支上的所有结点。
结点的子孙:以某结点为根的子树中的任一结点。
树的深度:树中结点的最大层次
森林:是m(m>=0)颗互不相交的树的集合
二叉树结构最简单,规律性最强
普通树(多叉树)若不转化为二叉树,则运算很难实现
二叉树是n(0)个结点的有限集,它或者是空集(n = 0),或者由一个根结点及两棵互不相交的分别称作这个根的左子树和右子树的二叉树组成。
特点:
1、每个结点最多有俩孩子(二叉树中不存在度大于2的结点)。
2、子树有左右之分,其次序不能颠倒。
3、二叉树可以是空集合,根可以有空的左子树或空的右子树。
注:
二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要区分,说明它是左子树,还是右子树。
树当结点只有一个孩子时,就无须区分它是左还是右的次序。因止二者是不同的。这是二叉树与树的最主要的差别。
性质1:在二叉树的第i层上至多有2 i − 1 ^{i-1} i−1个结点
性质2:深度为k的二叉树至多有2 k ^k k-1个结点(k>=1)
性质3:对任何一颗二叉树T,如果其叶子数为n0,度为2的结点数为n2,则n0=n2+1
两种特殊形式的二叉树
满二叉树
一颗深度为k且有2 k ^k k-1个结点的二叉树称为满二叉树
特点:1.每一层上的结点数都是最大结点数(即每层都满)
2.叶子结点全部在最底层
对满二叉树结点位置进行编号
编号规则:从根结点开始,自上而下,自左而右
每一结点位置都有元素
完全二叉树
深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应时,称之为完全二叉树
性质4:具有n个结点的完全二叉树的深度为[log 2 _2 2n]+1
性质5:如果对一颗有n个结点的完全二叉树(深度为[log 2 _2 2n]+1)的结点按层序编号(从第n层到第[log 2 _2 2n]+1层,每层从左到右),则对任一结点i (1<=i<=n),有:
1. 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2]
2. 如果2i>n,则结点i为叶子结点,无左孩子;否则其左孩子是结点2i
3. 如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1
按满二叉树的结点层次编号,依次存放二叉树中的数据元素
//二叉树顺序存储表示
#define MAXTSIZE 100
Typedef TElemType SqBiTree[MAXSTIZE]//定义类型数组
SqBiTree bt;//以数组名字定义变量
一个结点有一个前驱,两个后继(左孩子,右孩子)
typedef struct BiNode{
TElemType data;
struct BiNode *lchild,*rchild;//左右孩子指针
}BiNode,*BiTree
遍历定义–顺着某一条搜索路径巡防二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次
访问的含义很广,可以是对结点做各种处理,如:输出结点的信息、修改结点的数据值等,但要求这种访问不破坏原来的数据结构
遍历目的–得到树中所有结点的一个线性排列
遍历用途–它是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心
依次遍历二叉树中的三个组成部分,便是遍历了整个二叉树
L:遍历左子树 D:访问根节点 R:遍历右子树
若规定先左后右,则只有三种情况:
DLR–先(根)序遍历
LDR–中(根)序遍历
LRD–后(根)序遍历
若二叉树中各结点的值均不相同,则二叉树结点的先序序列、 中序序列和后序序列都是唯一的。
由二叉树的先序序列和中序序列,或有二叉树的后序序列和中序序列可以确定唯一一颗二叉树
例题:
Status PreOrderTraverse(BiTree *T){
if(T!==NULL){//二叉树非空
printf("%c\t",T->data);//访问根节点
PreOrderTraverse(T->lchild);//递归遍历左子树
PreOrderTraverse(T->rchild);//递归遍历右子树
}
}
Status InOrderTraverse(BiTree *T){
if(T!==NULL){//二叉树非空
PreOrderTraverse(T->lchild);//递归遍历左子树
printf("%c\t",T->data);//访问根节点
PreOrderTraverse(T->rchild);//递归遍历右子树
}
}
Status PostOrderTraverse(BiTree *T){
if(T!==NULL){//二叉树非空
PostOrderTraverse(T->lchild);//递归遍历左子树
PostOrderTraverse(T->rchild);//递归遍历右子树
printf("%c\t",T->data);//访问根节点
}
}
Status InOrderTraverse(Bitree T){
BiTree p; InitStack(S); p=T;
while(p||StackEmpty(S)){
if(p){Push(S,p); p=p->lchild;}
else{Pop(S,q); printf("%c",q->data;)
p=q->rchild; }
}//while
return OK;
}
算法设计思路: 使用队列
- 将根结点进队;
- 队不空时循环:从队列中列出一个结点*p,访问它;
- 若它有左孩子结点,将左孩子结点进队;
- 若它有右孩子结点,将右孩子结点进队;
使用队列类型定义如下:
typedef struct{
BTNode data[MAXSIZE];//存放队中元素
int front,rear; //队头和队尾指针
}SqQueue; //顺序循环队列类型
void LevelOrder(BTNode *b){
BTNode *p; SqQueue *qu;
InitQueue(qu); //初始化队列
enQueue(qu,b); //根结点指针进入队列
while(!QueueEmpty(qu)){ //队不为空,则循环
deQueue(qu,p); //出队结点p
printf("%c",p->data); //访问结点p
if(p->lchild!=NULL) enQueue(qu,p->lchild);
//有左孩子时将其进队
if(p->rchild!=NULL) enQueue(qu,p->rchild);
//有右孩子时将其进队
}
}
按先序遍历序列建立二叉树的二叉链表
例:已知先序序列为:ABCDEGF
(1) 从键盘输入二叉树的结点信息,建立二叉树的存储结构;
(2) 在建立二叉树的过程中按照二叉树先序方式建立;
Status CreateBiTree(BiTree &T){
scanf(&ch);//cin>>ch
if(ch=="#") T==NULL;//给叶子建立子结点
else{
if(!(T=(BiTNode*)malloc(sizeof(BiTNode))))
exit(OVERFLOW);//T = new BiTNode;
T->data = ch;//生成根结点
CreateBiTree(T->lchild);//构造左子树
CreateBiTree(T->rchild);//构造右子树
}
return OK;
}//CreateBiTree
如果是空树,递归结束;
否则,申请新结点空间,复制根结点
递归复制左子树
递归复制右子树
int Copy(BiTree T, BiTree &NewT){
if(T==NULL){ //如果是空树返回0
NewT=NULL; return 0;
}else{
NewT=new BiTNode;
NewT->data=T->data;
Copy(T->lChild, NewT->lchild);
Copy(T->rChild, NewT->rchild);
}
}
如果是空树,则深度为0;
否则,递归计算左子树的深度记为m,递归计算右子树的深度记为n,二叉树的深度则为m与n的较大者加1。
如果是空树,则结点个数为0;
否则,结点个数为左子树的结点个数+右子树的结点个数再+1.
提出的问题:
如何寻找特定遍历序列中二叉树结点的前驱和后继???
解决的方法:
1、通过遍历寻找——费时间
2、再增设前驱、后继指针域——增加了存储负担。
3、利用二叉链表中的空指针域。
利用二叉链表中的空指针域:
如果某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱;如果某结点的右孩子为空,则将空的右孩子指针域改为指向其后继
——这种改变指向的指针称为“线索”
加上了线索的二叉树称为线索二叉树(Threaded Binary Tree)
对二叉树按某种遍历次序使其变为线索二叉树的过程叫线索化
为区分lrchid和rchild指针到底是指向孩子的指针,还是指向前驱或后继的指针,对二叉链表中每个结点增设两个标志域 Itag 和rtag,并约定:
ltag = 0 Ichild 指向该结点的左孩子
ltag = 1 lchild 指向该结点的前驱
rtag = 0 rchild 指向该结点的右孩子
rtag = 1 rchild 指向该结点的后继
这样,结点的结构为 :
lchild | ltag | data | rtag | rchild |
---|
typedef struct BiThrNode{
int data;
int ltag, rtag;
struct BiThrNode *lchild,rchild;
}BiThrNode,*BiThrTree ;
实现:定义结构体数组存放树的结点,每个结点含两个域:
数据域:存放节点本身信息
双亲域:指示本结点的双亲结点在数组中的位置
c语言的类型描述:
typedef struct PTnode{
TElemType data;
int parent;//双亲位置域
}PTNode;
树结构:
#define MAX_TREE_SIZE 100
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int r,n;//根结点的位置和结点个数
}PTree
c语言类型描述:
孩子结点结构:child | next
typedef struct CTNode{
int child;
struct CTNode *next;
}*ChildPtr;
双亲结点结构: data | firstchild
typedef struct{
TElemType data;
ChildPtr firstchild;//孩子链表头指针
}CTBox;
树结构:
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n,r;//结点数和根结点的位置
}CTree;
实现:用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibiling;
}CSNode,*CSTree;
将树转化为二叉树进行处理,利用二叉树的算法来实现对树的操作
由于树与二叉树都可以用二叉链表做存储结构,则以二叉链表做媒介可以导出树与二叉树的一个对应关系
左孩子右兄弟–>左孩子右孩子
将树转换成二叉树:
1、加线:在兄弟之间加一连线
2、抹线:对每个结点,除了其左孩子外,除去其与其余孩子之间的关系
3、旋转:以树的根结点为轴心,将整树顺时针转45°
树变二叉树:兄弟相连留长子
将二叉树转换成树:
1、加线:若p结点是双亲结点的左孩子,则将p的右孩子,右孩子的右孩子…沿分支找到的所有右孩子,都与p的双亲用线连起来
2、抹线:抹掉原二叉树中双亲与右孩子之间的连线
3、调整:将结点按层次排列,形成树结构
1.将每棵树分别转换成二叉树
2.将每棵树的根结点用线相连
3.以第一棵树根根结点为二叉树的根,再以根节点为轴心,顺时针旋转,构成二叉树型结构
①抹线:将二叉树中根结点与其右孩子连线,及沿右分支搜索到的所
有右孩子间连线全部抹掉,使之变成孤立的二叉树
②还原:将孤立的二叉树还原成树
最优二叉树
结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树
权(weight):
将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
结点的带权路径长度:
从根结点到该结点之间的路径长度与该结点的权的乘积。
树的带权路径长度:
树中所有叶子结点的带权路径长度之和。
哈夫曼树:带权路径长度最短的树
注:“带权路径长度最短”是在“度相同”的树中比较而得的结果,因此有最优二叉树、最优三叉树之称等等。
满二叉树不一定是哈夫曼树
哈夫曼树中权越大的叶子离跟越近
具有相同带权结点的哈夫曼树不唯一
贪心算法:构造哈夫曼树时首先选择权值小的叶子结点
哈夫曼算法(构造哈夫曼树的方法)
(1)根据n个给定的权值{w1,w2,., wn}构成n棵二叉树的森林F={ T1,T2, …,Tn},其中Ti只有一个带权为wi的根结点。
(2)在F中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
(3)在F中删除这两棵树,同时将新得到的二叉树加入森林中。
(4)重复(2)和(3),直到森林中只有一棵树为止,这棵树即为哈夫曼树。
总结:
1、在哈夫曼算法中,初始时有n棵二叉树,要经过n-1次合并最终形成哈夫曼树。
2、经过n-1次合并产生n-1个新结点,且这n-1个新结点都是具有两个孩子的分支结点。
可见:哈夫曼树中共有n+n-1 = 2n-1个结点,且其所有的分支结点的度均不为1。
采用顺序存储结构–一维结构数组
结点类型定义
typedef struct{
int weight;
int parent,lch,rch;
}HTNode,*HuffmanTree;
1.初始化HT[1…2n-1]: lch=rch=parent=O;
2.输入初始q个叶子结点:置HT[1……n]的weight值;
void CreatHuffmanTree (HuffmanTree HT, int n){//构造哈夫曼树——哈夫曼算法
if(n<=1)return;
m=2*n-1;//数组共2n-1个元素
HT=new HTNode[m+1];//0号单元未用,HT[m]表示根结点
for(i=1;j<=m;++i){
//将2n-1个元素的lch、rch、parent置为0
HT[i].Ich=O; HT[i].rch=O; HT[i].parent=O;
}
for(i=1;j<=n;++i) cin> >HT[i].weight;
//输入前n个元素的weight值
//初始化结束,下面开始建立哈夫曼树
3.进行以下n-1次合并,依次产生n-1个结点HT[i],i=n+1…2n-1 :
- 在HT[1.i-1]中选两个未被选过(从parent == 0 的结点中选)的weight最小的两个结点HT[s1]和HT[s2], s1、s2为两个最小结点下标;
- 修改HT[s1]和HT[s2]的parent值:
HT[s1].parent=i; HT[s2].parent=i;- 修改新产生的HT[i]:
HT[i].weight=HT[s1].weight+ HT[s2].weight ;
HT[i]. lch=s1; HT[i]. rch=s2;
for( i=n+1;i<=m; i++){//合并产生n-1个结点——构造Huffman树
Select(HT, i-1, s1, s2);
//在HT[k](1≤k≤i-1)中选择两个其双亲域为0,
//且权值最小的结点,并返回它们在HT中的序号s1和s2
HT[s1].parent=i;
HT[s2].parent=i;//表示从F中删除s1,s2
HT[i].Ich=s1;
HT[i].rch=s2;//s1,s2分别作为i的左右孩子
HT[i].weight=HT[s1].weight+HT[s2].weight;
//i的权值为左右孩子权值之和
}
}