文章目录
1.思维导图
1.1树与二叉树
1.2查找
2.重要概念的笔记
2.1树
2.1.1树的定义
树是 n 个结点的有限集,它或为空树;或为非空树,对于非空树 T:
- 有且仅有一个称之为根的结点;
- 除根结点以外的其余结点可分为 m 个互不相交的有限集 T1,T2,…,Tm,其中每一个集合本身又是一棵树,并且称为根的子树。
2.1.2树的基本术语
(1) 结点:树中的一个独立单元。包含一个是数据元素及若干指向其子树的分支。
(2) 结点的度:结点拥有的子树数称为结点的度。
(3) 树的度:树的度是树内各结点度的最大值。
(4) 叶子:度为 0 的结点称为叶子或终端结点。
(5) 非终端结点:度不为 0 的结点称为非终端结点或分支结点。
(6) 双亲和孩子:结点的子树的根称为该结点的孩子,相应的,该结点称为孩子的双亲。
(7) 兄弟:同一个双亲的孩子之间互称兄弟。
(8) 祖先:从根到该结点所经分支上的所有结点。
(9) 子孙:以某结点为根的子树中的任一结点都称为该结点的子孙。
(10) 层次:结点的层次从根开始定义起,根为第一层,根的孩子为第二层。
(11) 堂兄弟:双亲在同一层的结点互为堂兄弟。
(12) 树的深度:树中结点的最大层次称为树的深度或高度。
(13) 有序树和无序树:如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。
(14) 森林:是 m 棵互不相交的树的集合。
2.1.3树的性质
- 树中的结点数等于所有结点的度数加1
- 度为m的树中第i层上至多有m^(i-1)个结点
- 高度为h的m叉树至多有(m^h)/(m-1)个结点
- 具有n个结点的m叉树的最小高度为向上取整 logm(n(m-1)+1)
2.1.4树的遍历
- 先序遍历
void PreOrderTraverse(tree *T)
{
if(T==NULL)return;
printf("%c ",T->data);
PreOrderTraverse(T->Lchild);
PreOrderTraverse(T->Rchild);
}
- 中序遍历
void InOrderTraverse(tree *T)
{
if(T==NULL)return;
InOrderTraverse(T->Lchild);
printf("%c ",T->data);
InOrderTraverse(T->Rchild);
}
- 后序遍历
void PostOrderTraverse(tree *T)
{
if(T==NULL)return;
PostOrderTraverse(T->Lchild);
PostOrderTraverse(T->Rchild);
printf("%c ",T->data);
}
2.2二叉树
2.2.1二叉树的定义
二叉树是一种每个结点至多只有两个子树(即二叉树的每个结点的度不大于2),并且二叉树的子树有左右之分,其次序不能任意颠倒。
2.2.2二叉树的性质
- 在二叉树的第i层,至多有2^(i-1)个结点
- 深度为k的二叉树至多有:(2^k)-1个结点,其实这个结果就是一个等比数列的求和得到的。
- 对任意一颗二叉树,如果其叶子结点数量为:n0,度为2的结点数为:n2,则:n0=n2+1
- 具有n个结点的完全二叉树的深度为:[log2n]+1,其中[log2n]+1是向下取整。
- 有N个结点的完全二叉树各结点如果用顺序方式存储,则结点之间有如下关系: 若i为结点编号则:
如果i>1,则其父结点的编号为[i/2],[i/2]是往下取整的;
如果2i<=N,则其左儿子(即左子树的根结点)的编号为2i;若2i>N,则无左儿子;
如果2i+1<=N,则其右儿子的结点编号为2i+1;若2i+1>N,则无右儿子。
2.2.3二叉树的存储结构
一、顺序存储结构
二叉树的顺序存储结构是指用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为i的结点元素存储在某个数组下标为i-1的分量中,然后通过一些方法确定结点在逻辑上的父子和兄弟关系。
顺序存储示意图如下:
优缺点
顺序存储结构存放二叉树,对于完全二叉树而言非常合适
当对于具有很多单分支结点的二叉树而言不合适,特别是退化的二叉树,空间浪费很大
用顺序存储结构存放二叉树,很容易就可以找到一个结点的双亲和孩子
二、链式存储结构
链式结构是指用一个链表来存储一棵二叉树,二叉树中的每个结点用链表的一个链结点来存储。
在二叉树中,结点结构通常包括若干数据域和若干指针域。二叉链表至少包含3个域:数据域data、左指针域lchild和右指针域rchild
常用的二叉链表存储结构如下图所示:
优缺点
除了指针外,二叉链比较节省存储空间。占用的存储空间与树形没有关系,只与树中结点个数有关。
在二叉链中,找一个结点的孩子很容易,但找其双亲不方便。
二叉树的链式存储结构描述如下:
typedef struct BiTNode{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
}BiTNode, *BiTree;
2.2.4二叉树的基本算法及其实现
创建二叉树
void CreateBTree(BTNode * &b, char * str)
{
BTNode * St[MaxSize], *p; //以st数组作为顺序栈
int top = -1, k,j=0;
char ch;
b = null;
ch = str[j];
while(ch!='\0')
{
switch(ch)
{
case '(': top++; St[top]=p; k=1; break;
case ')': top--; break;
case ',': k=2; break;
default: p = new BTNode;
p->data = ch;
p->lchild=p->rchild=null;
if(b=null)
{
b=p;
}
else
{
switch(k)
{
case 1: St[top]->lchild = p;
case 2: St[top]->rchild = p;
}
}
}
j++; //继续扫描
ch=str[j];
}
}
销毁二叉树
void DestroyBTree(BTNode * & p)
{
if(p!=null)
{
DestoryBTree(p->lchild);
DestroyBTree(p->rchild);
free(p)
}
}
查找结点
BTNode * FindNode(BTNode * b, char x)
{
BTNode *p;
if(b=null)
return null;
else if(b->data == x)
{
return b
}
else
{
p = FindNode(b->lchild,x);
if(p != null)
return p;
else
return FindNode(b->rchild, x);
}
}
查找孩子结点
BTNode * LchildNode(BTNode * p)
{
return p->lchild;
}
BTNode * RchildNode(BTNode * p)
{
return p->rchild;
}
求高度
int BTHeight(BTNode * b)
{
int lchild, rchild;
if(b==null) return 0;
else
{
lchild = BTHeight(b->lchild);
rchild = BTHeight(b->rchild);
return ( lchild > rchild ) ? ( lchild + 1) : ( rchild + 1 );
}
}
输出二叉树
void DispBTree(BTNode * b)
{
if(b != null)
{
cout << b->data;
if(b->lchild != null || b->rchild != null)
{
cout<<"(";
DispBTree(b->lchild);
if(b->rchild != null) cout<<",";
DispBTree(b->rchild);
cout<<")";
}
}
}
2.2.5线索二叉树
线索二叉树:按照某种遍历方式对二叉树进行遍历,可以把二叉树中所有结点排序为一个线性序列。在该序列中,除第一个结点(某种遍历方式访问的第一个结点)外每个结点有且仅有一个直接前驱结点;除最后一个结点(某种遍历方式访问的最后一个结点)外每一个结点有且仅有一个直接后继结点。这些指向直接前驱结点和指向直接后续结点的指针被称为线索(Thread),加了线索的二叉树称为线索二叉树。
如图所示:
代码实现
- 二叉树的存储结构
/* 二叉树的二叉线索存储结构定义*/
typedef struct BitNode
{
char data; //结点数据
struct BitNode *lchild, *rchild; //左右孩子指针
PointerTag Ltag; //左右标志
PointerTag rtal;
}BitNode, *BiTree;
- 中序遍历线索化
BiTree pre; //全局变量,始终指向刚刚访问过的结点
//中序遍历进行中序线索化
void InThreading(BiTree p)
{
if(p)
{
InThreading(p->lchild); //递归左子树线索化
//===
if(!p->lchild) //没有左孩子
{
p->ltag = Thread; //前驱线索
p->lchild = pre; //左孩子指针指向前驱
}
if(!pre->rchild) //没有右孩子
{
pre->rtag = Thread; //后继线索
pre->rchild = p; //前驱右孩子指针指向后继(当前结点p)
}
pre = p;
//===
InThreading(p->rchild); //递归右子树线索化
}
}
2.2.5哈夫曼树
哈夫曼树的定义
哈夫曼树指的是一种满二叉树,该类型二叉树具有一项特性,即树的带权路径长最小,所以也称之为最优二叉树。
几个与哈夫曼树有关的概念:
路径: 树中一个结点到另一个结点之间的分支构成这两个结点之间的路径。
路径长度:路径上的分枝数目称作路径长度。
树的路径长度:从树根到每一个结点的路径长度之和。
结点的带权路径长度:在一棵树中,如果其结点上附带有一个权值,通常把该结点的路径长度与该结点上的权值
树的带权路径长度:如果树中每个叶子上都带有一个权值,则把树中所有叶子的带权路径长度之和称为树的带权路径长度。
示例
哈夫曼树的构造
根据哈弗曼树的定义,一棵二叉树要使其WPL值最小,必须使权值越大的叶子结点越靠近根结点,而权值越小的叶子结点越远离根结点。
代码实现
- 哈夫曼树的存储结构
#define MAXVALUE 32767
typedef struct{ //哈夫曼树结构体
int weight; //输入权值
int parent,lchild,rchild; //双亲节点,左孩子,右孩子
}HNodeType;
typedef struct{ //哈夫曼编码结构体
int bit[8]; //存放当前结点的哈夫曼编码
int start; //bit[start]-bit[8[存放哈夫曼编码
}HCodeType;
HNodeType HuffNode[8]; //定义全局变量数组HuffNode存放哈夫曼树
HCodeType HuffCode[8]; //定义全局变量数组HuffCode存放哈夫曼编码
int n; //定义全局变量n表示叶子结点个数
- 构造哈夫曼树
void CreateHuffTree(void){ //构造哈夫曼树
int i,j,a,b,x1,x2;
scanf("%d",&n); //输入叶子节点个数
for(i=1;i<2*n;i++) //HuffNode 初始化
{
HuffNode[i].weight=0;
HuffNode[i].parent=-1;
HuffNode[i].lchild=-1;
HuffNode[i].rchild=-1;
}
printf("输入%d个节点的权值\n",n);
for(i=1;i<=n;i++)
scanf("%d",& HuffNode[i].weight);//输入N个叶子节点的权值
for(i=1;i<n;i++){ //构造哈夫曼树
a=MAXVALUE;
b=MAXVALUE;
x1=0;
x2=0;
for(j=1;j<n+i;j++){ //选取最小和次小两个权值
if(HuffNode[j].parent==-1&&HuffNode[j].weight<a){
b=a;
x2=x1;
a=HuffNode[j].weight;
x1=j;
}
else
if(HuffNode[j].parent==-1&&HuffNode[j].weight<b){
b=HuffNode[j].weight;
x2=j;
}
}
HuffNode[x1].parent=n+i;
HuffNode[x2].parent=n+i;
HuffNode[n+i].weight=HuffNode[x1].weight+HuffNode[x2].weight;
HuffNode[n+i].lchild=x1;
HuffNode[n+i].rchild=x2;
}
}
- 输出哈夫曼树
void PrintHuffTree() { //输出哈夫曼树
int i;
printf("\n哈夫曼树各项数据如下表所示:\n");
printf(" 结点i weight parent lchid rchild\n");
for(i=1;i<2*n;i++)
printf("\t%d\t%d\t%d\t%d\t%d\n",i,HuffNode[i].weight,HuffNode[i].parent,
HuffNode[i].lchild,HuffNode[i].rchild);
printf("\n");
}
2.3查找
2.3.1查找的基本概念
- 查找表: 查找表是由同一类型的数据元素(或记录)构成的集合
- 查找: 根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素
内查找:整个查找过程都在内存中进行
外查找:查找的过程需要访问外存- 关键字: 用来标识一个数据元素(或记录)的某个数据项的值
- 查找表的分类:
静态查找表: 仅作"查询"(检索)操作的查找表
动态查找表: 作"插入"和"删除"操作的查找表
平均查找长度:关键字的平均比较次数
n 为总记录的个数;
pi 查找到第i个记录的概率(通常pi就是1/n);
ci 找到第i个记录需要比较的次数;
2.3.2线性表的查找
线性表的查找有三种:
- 顺序查找
- 折半查找(二分查找)
- 分块查找
一、顺序查找
- 线性表的存储结构:可以是顺序存储【数组-下标】,也可以是链式存储
- 顺序查找的平均查找长度计算:ASL=∑PiCi (i=1,2,3,…,n), Pi为1/n, Ci为比较次数。
- 查找成功的情况:最终的结果是1/n * (1+2+3…+n) 为 (n+1)/2。 查找成功的情况下,查找次数几乎为表长的一半。
- 优点 :对数据的顺序没有要求,逻辑简单,存储结构灵活,可以是顺序表存储,也可以是链表存储。
- 缺点 : ASL太长,时间效率不高。
代码实现
int Search_Seq(RecType ST[], int n,Keytype key) //若成功返回其位置信息,否则返回0
{
int i=0;
while(i<n&&ST[i].key!=k)
i++;
if(i>=n)
return 0;
else
return i+1;
}
二、折半查找(二分查找)
二分查找,每一次将查询的记录缩小一半。折半查找对数据的序列有要求,必须是有序表
平均查找长度计算:
对于二分查找来说,实际上有序表可以转换为一个特定的二叉树 称之为 【判定树】,这颗二叉树有一些特点,每一层上面的节点,在查找成功的情况下,查找的次数为所在的层数(高度)。平均查找长度ASL(成功时): ASL \approx log_{2}(n+1)-1 (n>50)
优点 :查找效率比顺序查找高
缺点 : 必须是有序表,而且从存储结构来说只适用于顺序存储,不适用于链式存储。
代码实现
int Search_Bin(SSTable ST, Keytype key)
{
int left = 1;
int right = ST.length;
while (left <= right)
{
int mid = (left + right) / 2;
if (ST.R[mid].key == key) //找到待查元素
return mid;
else if (key < ST.R[mid].key) //缩小查找区间
right = mid - 1; //继续在前半区间进行查找
else
left = mid + 1; //继续在后半区间进行查找
}
return 0; //顺序表中不存在待查元素
}
三、分块查找
分块查找的概念是,将数据表分为多块, 每一块内内部的数据不要求排序。 但是每一块内的最大元素一定是小于下一块内的任意一个值。
分块查找的平均查询长度为:
ASL = Lb + Lw
Lb为索引表的ASL,Lw为分块内的ASL。优点 :查找效率适中,比顺序查找块,并且在块内的插入和删除无序移动大量的元素。
缺点 : 需要一个额外的索引表来存储各个块的信息。
适用情况: 如果线性表既要快速查找又经常动态变化,即可采用分块查找
三种查找方法比较
顺序查找 | 折半查找 | 分块查找 | |
---|---|---|---|
ASL | 最大 | 最小 | 中间 |
表结构 | 有序表、无序表 | 有序表 | 分块有序 |
存储结构 | 顺序表 | 顺序表 | 顺序表、线性链表 |
2.3.3数表的查找
一、二叉排序树(二叉查找树)
若它的左子树非空,则左子树上所有记录的值均小于根记录的值;
若它的右子树非空,则右子树上所有记录的值均大于根记录的值;
左、右子树又各是一棵二叉查找树。
二叉查找树的一个重要的性质是:中序遍历该树得到的序列是一个递增有序的序列。
假如有一个序列{62,88,58,47,35,73,51,99,37,93},那么构造出来的二叉查找树如下图所示:
二叉排序树的存储结构
//二叉排序树的存储结构
typedef struct
{
KeyType key; //关键字项
InfoType otherinfo; //其他数据项
}ElemType; //每个结点的数据域的类型
typedef struct BSTNode
{ //结点结构
ElemType data; //数据域
struct BSTNode* lchild, * rchild; //左右孩子的指针
}BSTNode,*BSTree;
二叉排序树的搜索
BSTree SearchBST(BSTree T, KeyType key)
{
//在指针T所指的二叉排序树中递归地查找某关键字等于key的数据元素
//若查找成功,则返回指向该数据元素结点的指针,否则返回空指针
if (!T || key == T->data.key)
return T; //查找结束
else if (key < T->data.key)
return SearchBST(T->lchild,key);//在左子树中继续查找
else
return SearchBST(T->rchild,key);//在右子树中继续查找
}
二叉排序树的插入
void InsertBST(BSTree& T, ElemType e)
{//当二叉排序树T中不存在关键字等于e.key的数据元素时,则插入该元素
if (!T) //T为空树
{ //找到插入位置,递归结束
BSTree S;
S = new BSTNode; //生成新结点*S
S->data = e; //新结点数据域置为e
S->lchild = S->rchild = NULL;//新结点*S作为叶子结点
T = S; //把新结点*S链接到已找到的插入位置
}
else if (e.key < T->data.key)
InsertBST(T->lchild, e); //将*S插入左子树
else if (e.key > T->data.key)
InsertBST(T->rchild, e); //将*S插入右子树
}
二叉排序树的删除
BinTree Delete( BinTree BST, ElementType X )
{
BinTree temp;
if(!BST)
printf("Not Found\n");
else
{
if(X < BST->Data)
BST->Left = Delete(BST->Left,X);
else if(X > BST->Data)
BST->Right = Delete(BST->Right,X);
else
{
if(BST->Left && BST->Right)
{
BinTree temp = FindMin(BST->Right);
BST->Data = temp->Data;
BST->Right = Delete(BST->Right,BST->Data);
}
else
{
temp = BST;
if(BST->Left)
BST = BST->Left;
else
BST = BST->Right;
free(temp);
}
}
}
return BST;
}
Position FindMin( BinTree BST )
{
if(BST)
{
while(BST->Left != NULL)
BST = BST->Left;
}
return BST;
}
2.3.4平衡二叉树
平衡二叉树的定义
- 平衡二叉树又称AVL树 一棵AVL树或者是空树,或者是具有下列性质的二叉排序树:
(1)它的左子树和右子树都是AVL树,且左子树和右子树的深度之差的绝对值不超过1
(2)左子树和右子树也是AVL树
每个结点附加一个数字,给出该结点的平衡因子BF:该结点的左子树深度和右子树深度之差。
AVL树任一结点平衡因子只能取-1,0,1
平衡二叉树的调整
如果在一棵AVL树中插入一个新结点,就有可能造成失衡,此时必须重新调整树的结构,使之恢复平衡
调整方法:找到离插入点最近且平衡因子绝对值超过1的祖先结点,以该结点为根的子树称为最小不平衡子树,可将重新平衡的范围局限于这棵子树。
平衡调整的四种类型
2.3.5 B树与B+树
B树
B+树
区别
2.3.5哈希表的查找
概念
哈希表查找又叫散列表查找,通过查找关键字不需要比较就可以获得需要记录的存储位置,它是通过在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。即:存储位置=f(关键字),其中f为哈希函数。
采用哈希表查找的时间复杂度为O(1)。
哈希函数的构造方法
(1)直接定址法
h(k) = k + c
这种哈希函数优点是比较简单、均匀,也不会产生冲突,但是需要事先知道关键字的分布情况,适合查找表比较小且连续的情况。在实际中并不常用。
(2)除留余数法
h(k) = k mod p(p<=m)
这是最为常用的构造哈希函数的方法。
(3)数字分析法
可以使用关键字的一部分来计算哈希存储的位置,比如手机号码的后几位(或者反转、左移右移等变换)。
哈希冲突的解决方法
(1)开放定址法
该方法是一旦发生冲突,就去寻找下一个空的哈希地址,只要哈希表足够大,空的哈希地址总能找到,并将其记录存入。
fi(key)=(f(key)+di) mod m (di=1,2,3……m-1)
(2)平方探测法
增加平方项,主要是为了不让关键字都集中在某一块区域,避免不同的关键字争夺一个地址的情况。
fi(key)=(f(key)+di) mod m (di=1^2, -1^2, 2^2, -22……q2, -q^2, q<=m/2)
(3)链地址法
将所有同关键字的记录存储在一个单链表中,称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针。
链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0…m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于
1,但一般均取α≤1。
3.疑难问题及解决方案
题目如下:
伪代码:
void InitExpTree(BTree &T,string str)//建表达式的二叉树
建数字栈s,建符号栈op
定义计量数i为0
'#'入栈op
while str[i] do //遍历字符串
if !In(str[i]) then //数字作叶子结点
新建结构体指针T
T的data为str[i],初始化T的左右子树
T入栈s
i加一
else //字符为运算符时
switch Precede(op.top(),str[i]) then //符号优先级比较
若为'<',则str[i]入栈op且i加一
若为'=',op栈顶出栈且i加一
若为'>',新建结构体指针T
T的data为op栈顶且出栈
赋予T左右子树分别为s栈顶元素并让其出栈
T入栈s
end switch
end if
end while
while op栈顶不为'#' do //符号栈有余下运算符时的处理
新建结构体指针T
T的data为op栈顶,T的右子树为s栈顶并让其出栈
如果s不为空,则T的左子树为s栈顶并让其出栈
T入栈s
op栈顶出栈
end while
T为s栈顶
double EvaluateExTree(BTree T) //计算表达式树
定义数a和b
当左右子树为空时返回 T->data-'0'
a=EvaluateExTree(T->lchild) //递归
b=EvaluateExTree(T->rchild) //递归
以a,b为运算数,T->data为运算符进行四则运算,并处理除数为0的情况
代码实现:
void InitExpTree(BTree &T,string str)
{
stack<BTree> s;
stack<char> op;
op.push('#');
int i=0;
while(str[i])
{
if(!In(str[i]))
{
T=new BTNode;
T->data= str[i++];
T->lchild=T->rchild=NULL;
s.push(T);
}
else
{
switch(Precede(op.top(),str[i]))
{
case '<':
op.push(str[i]);
i++;
break;
case '=':
op.pop();
i++;
break;
case '>':
T=new BTNode;
T->data =op.top();
T->rchild=s.top();
s.pop();
T->lchild= s.top();
s.pop();
s.push(T);
op.pop();
break;
}
}
}
while(op.top()!='#')
{
T =new BTNode;
T->data=op.top();
T->rchild=s.top();
s.pop();
if(!s.empty())
{
T->lchild=s.top();
s.pop();
}
s.push(T);
op.pop();
}
T=s.top();
}
double EvaluateExTree(BTree T)
{
double sum=0,a,b;
if(!T->lchild&&!T->rchild)
{
return T->data-'0';
}
a=EvaluateExTree(T->lchild);
b=EvaluateExTree(T->rchild);
switch(T->data)
{
case '+':
return a+b;
break;
case '-':
return a-b;
break;
case '*':
return a*b;
break;
case '/':
if(b==0)
{
cout << "divide 0 error!" << endl;
exit(0);
}
return a/b;
break;
}
}
疑难问题及解决方案
Q1:树在删除或添加子树时或通过栈操作字符串时容易出现空指针造成段错误
A1:在符号的先级的比较中,大于号和小于号的情况弄错造成出栈和入栈时操作错误;同时在递归运用上,树的分支传参和递归口没有设置好,也导致了段错误。
Q2:在树的数据进行出栈入栈操作后,可能会出现符号栈中有剩余运算符没有进行运算
A2:这就需要另设循环将运算符中的数据通过符号栈中的符号进行运算,当符号栈顶为‘#’时结束循环即所有运算符已经运算。