为了方便复习 下面内容摘自:数据结构期末总结_夏日 の blog-CSDN博客_数据结构期末
目录
绪论
知识点
习题
线性表
知识点
习题
栈和队列
知识点
习题
串、数组和广义表
知识点
树和二叉树
知识点
习题
赫夫曼树及其应用
一步一步写平衡二叉树(AVL树)
图
知识点
习题
查找
知识点
习题
排序
知识点
习题
各类型存储结构
顺序表
单链表
双向链表
顺序栈
链栈
循环队列
链队
小结
顺序二叉树(不常用)
二叉链表(常用)
线索二叉树
孩子兄弟二叉树
邻接矩阵
邻接表
1.逻辑结构:数据之间的相互关系。(与计算机无关)
集合 结构中的数据元素除了同属于一种类型外,别无其它关系。
线性结构 数据元素之间一对一的关系
树形结构 数据元素之间一对多的关系
图状结构或网状结构 结构中的数据元素之间存在多对多的关系
也可分为线性结构(可理解成一条直线能串起来)和非线性结构
2.存储结构分为顺序存储结构和链式存储结构(散列、索引) (与计算机有关)
3.算法五个特性: 有穷性、确定性、可行性、输入、输出
4.算法设计要求:正确性、可读性、健壮性、高效性。 (好的算法)
5.typedef可以理解成给现有数据类型起个别名
例如:typedef struct{…}SqList,即给struct{…}起了个名字叫SqList
也用于类似于typedef int ElemType; 给int 起个别名叫ElemType即ElemType a;等价于int a;
这样做的好处是代码中用ElemType定义变量,如果想修改变量类型只需修改typedef ** ElemType即可,而不用一一修改。
我们注意到有时候会有typedef struct LNode{…}LNode,即struct后有个LNode,这是因为如果结构体内部有指向结构体的指针则必须在struct后面加上LNode(单链表里有next指针struct LNode *next)
6.时间复杂度:基本操作的执行次数(可以理解成就看执行了多少次)
7.研究数据结构就是研究数据的逻辑结构、存储结构及其基本操作
8.抽象数据类型的三个组成部分为数据对象、数据关系、基本操作。
9.数据:描述客观事物的符号
数据元素:是数据的基本单位(元素、结点)
数据项:组成数据元素的最小单位 (如学生信息表中的学号、姓名等)
数据对象:相同性质的数据元素的集合(如大写字母)
大小关系为:数据=数据对象 > 数据元素 > 数据项
10.数据结构:相互之间存在一种或多种特定关系的数据元素的集合
11.数据的运算包含:插入、删除、修改、查找、排序
12.算法:解决某类问题而规定的一个有限长的操作序列
13.算法的空间复杂度:算法在运行时所需存储空间的度量
1.通常要求同一逻辑结构中的所有数据元素具有相同的特性, 这意味着( B )。
A. 数据具有同一特点
B. 不仅数据元素所包含的数据项的个数要相同, 而且对应数据项的类型要一致
C. 每个数据元素都一样
D. 数据元素所包含的数据项的个数要相等
2.以下说法正确的是( D )。
A. 数据元素是数据的最小单位
B. 数据项是数据的基本单位
C. 数据结构是带有结构的各数据项的集合
D. 一些表面上很不相同的数据可以有相同的逻辑结构
答:数据元素是数据的基本单位,数据项是数据的最小单位,数据结构是带有结构的各数据元素的集合
3.算法的时间复杂度取决于( D )。
A.问题的规模 B.待处理数据的初态 C.计算机的配置 D. A 和 B
答:肯定与问题规模(难和简单的问题)有关,不过也与初态有关,比如某些排序算法,若初始已经排好序可能时间复杂度就会降低。
4.下列算法时间复杂度为
count=0;
for(k=1;k<=n;k*=2)
for(j=1;j<=n;j+=1)
count++;
答:最外层循环数值为20,21,22…所以假设执行m次即2m=n所以外层执行了log2n次
内层执行了n次,所以时间复杂度为nlog2n(可理解为log2n个n相加)
int fact(int n){
if(n<=1) return 1;
return n*fact(n-1);
}
答:第一次是n*fact(n-1),然后是n*(n-1)*fact(n-2)…一直到n(n-1)(n-2)…2*1
但是我们要看执行了多少次,也就是函数fact调用了多少次,从n到1也就是n次,所以时间复杂度为O(n)
1.线性结构:第一个无前驱,最后一个无后继,其他都有前驱和后继
2.顺序表插入一个元素平均移动n/2个元素,删除平均移(n-1)/2个
插入的那一位置需要向后移,删除的位置那一位不用移(直接覆盖)所以删除少1
3.首元结点:存储第一个有效数据元素的结点
头结点:首元结点之前指向首元结点的结点,为处理方便而设
头指针:指向第一个结点(有头结点指头结点没有指首元结点)的指针
单链表通常用头指针命名
4.随机存取:可以像数组一样根据下标直接取元素
顺序存取:只能顺藤摸瓜从前往后一个一个来
5.单链表加一个前驱指针prior就变成了双向链表
6.单链表最后一个元素的next指针指向第一个结点即为循环链表 (属于线性表!)
7.线性表和有序表合并的时间复杂度
线性表的合并时间复杂度为O(m*n)
A=(7,5,3,11),B=(2,6,3),结果为A=(7,5,3,11,2,6)
算法需要循环遍历B(O(n))且LocateElem(A)(判断是否与B重复为O(m))所以为O(m*n)
有序表的合并时间复杂度为O(m+n)
A=(3,5,8,11),B=(2,6,8),结果为A=(2,3,5,6,8,11)
算法只需同时遍历A和B,然后将还没遍历完的那个直接插到最后就行,所以是相加
9.单链表也是线性表(一对一的关系,用绳子可以穿起来)的一种
10.顺序表存储密度(数据占比/结点占比)等于1,单链表的小于1(因为要存指针)
1.线性表只能用顺序存储结构实现 (X)也可用链式如单链表
2.在双向循环链表中,在 p指针所指的结点后插入 q所指向的新结点,其修改指针的操作是( C )。
A. p->next = q; q->prior = p; p->next->prior = q; q->next = q;
B. p->next = q; p->next->prior = q; q->prior=p; q->next = p->next;
C. q->prior = p; q->next = p->next; p->next->prior = q; p->next = q;
D. q->prior = p; q->next = p->next; p->next = q; p->next->prior = q;
答:这样的题只能画图看看对不,但是我们可以看到在p的后面插入,那么p->next就不能非常早的更改否则就会出现找不到的情况,所以排除A,B。C和D画个图试下
3.在一个有127个元素的顺序表中插入一个新元素并保持原来顺序不变,平均要移动的元素个数为( B)。
A. 8 B. 63.5 C. 63 D. 7
答:插入平均移动n/2即63.5,注意不用取整
1.栈和队列是操作受限的线性表(1对1)
2.栈后进先出,只能在栈顶(表尾)插入删除
3.队列先进先出,队头删除,队尾插入(和平常排队一样排后面)
4.顺序栈栈空时:S.top=S.base 栈顶指针等于栈底指针
栈满时:S.top-S.base=S.stacksize 栈顶-栈底等于最大空间
5.链栈在栈顶操作,用链表头部作为栈顶即可,不需要头结点
栈空:S=NULL (指向第一个结点的指针为空)
6.栈的应用:括号匹配,表达式求值(中缀式求值),递归转非递归、函数调用
7.中缀表达式:符号在中间,如a+b,前缀就是+ab(前缀中缀指的是符号的位置)
8.循环队列队空:Q.front=Q.rear
队满:(Q.rear+1)%MAXSIZE==Q.front
队列元素个数:(Q.rear-Q.front+MAXSIZE)%MAXSIZE
入队:Q.rear=(Q.rear+1)%MAXSIZE
出队:Q.front=(Q.front+1)%MAXSIZE
1.若一个栈以向量V[1…n]存储,初始栈顶指针 top设为n+1, 则元素x进栈的正确操
作 是( C )。
A. top++; V[top]=x; B. V[top]=x; top++; C. top–; V[top]= x; D. V[top]=x; top–;
答:注意初始top为n+1,而存储下标为v[1]~v[n],所以就不存在ABD中的v[n+2]或者v[n+1]。应该先让top减一使得指向最后一个地址v[n],可以把它看成是倒过来的栈,然后存v[n-1],v[n-2]…
2.用链接方式存储的队列,在进行删除运算时( D )。
A. 仅修改头指针 B. 仅修改 尾指针 C. 头、尾指针都要修改 D. 头、尾指针可能都要修改
答:由于只能在队头删除,一般只需修改头指针(head=head->next)即可。但当删最后一个元素时(此时head=rear)删除后(delete p)尾指针就丢失了也得修改
3.一个递归算法必须包括( B )。
A. 递归部分 C. 迭代部分 B. 终止条件和递归部分 D. 终止条件和迭代
答:算法有穷形所以都得有终止条件,递归算法那肯定得有递归部分
4.最不适合用作队列的链表是( A )。
A.只带队首指针的非循环双链表 B.只带队首指针的循环双链表
C.只带队尾指针的循环双链表 D.只带队尾指针的循环单链表
答:就看找头尾指针好不好找,A只有头指针还非循环只能从头到尾遍历找到尾指针
5.表达式a*(b+c)-d的后缀表达式是( B )。
A. abcd*± B. abc+*d- C. abc*+d- D. -+*abcd
答:前缀后缀指的是运算符号位置,先看原运算顺序,先算(b+c)后缀表达式是bc+
原式然后算*,a*(bc+)后缀表达式是abc+*,然后是abc+*d-
6.已知循环队列存储在一维数组A[0…n-1]中,且队列非空时front和rear分别指向队头元素和队尾元素。若初始时队列为空,且要求第1个进入队列的元素存储在A[0]处,则初始时front和rear的值分别是( B )。
A.0,0 B.0,n-1 C.n-1,0 D.n-1,n-1
答:平常入队时先在rear位置赋值,再把rear+1,即rear指向的是队尾元素的下一位置,所以入队时先赋值再加一。但是此题说的是rear指向队尾。也即第一个入队后队尾指向的是第一个元素的位置也即0,所以入队前rear那就是0前面的n-1而front默认都为0
1.求next数组和nextval数组
当j=1(即第一个字符)时为特殊情况next和nextval均为0
当j=1(即第一个字符)时为特殊情况next和nextval均为0
1️⃣ next数组:其值为当前字母前方的最大前后缀+1
例如:j=3(A),前面有A,B。没有前后缀即为0,0+1=1
j=4(B),前面有ABA,有前缀和后缀A,即前后缀为1,1+1=2
j=5(A),前面有ABAB,前后缀为AB,2+1=3 //ABA和BAB不等,所以AB为最大前后缀
next[j]=k,它的意思是,当模式串的第j位与主串的第i位失配时,这时主串的位置不回退,而是将模式串退到第k位,再次与主串的第i位进行匹配。
比如主串为ABAA,不匹配时next[4]=2,将模式串中的2位置即B与主串的最后A比较也就达到了不匹配时直接根据前后缀移动的目的
若是不匹配就看next[j]数值,若当前字母和next[j]字母不等时,nextval等于上面落下来的next[j]
若是不匹配就看next[j]数值,若当前字母和next[j]字母相等时,nextval值为前面的那个nextval[]
不等就用自家的,相等直接拿过来
例如:j=2,next[2]为1表不匹配时退到下标为1的位置,1的位置是A和当前2对应的B不等用自家的所以next[2]落下来成为nextval[2]
j=3,next[3]=1表不匹配时模式串回退到下标为1的位置,1的位置是A和当前3对应的A相等,所以把前面的nextval数值拿过来即为nextval[3]
其实就是行优先就是从上到下先一行一行的存,列优先就是从左到右一列一列的存
无论是哪个其元素如a[2][3]位置不变(但顺序变了),行优先就是先存上面2行再到它,列优先就是先存左面3列再存它
3.广义表是线性表的推广,也称列表(暂时理解成python里的列表)
4.广义表元素可为原子或子表
广义表长度:即元素个数(最外层括号里的小括号算一个元素)
广义表深度:就看有多少对括号就行(注意要将里面的子表全部展开)
5.表头(Head)和表尾(Tail):当表非空时,第一个元素为表头其余均为表尾
注意表头是第一个元素所以不带最外层的那个括号,表尾带最外层的括号
例如A=((a,b),c),表头为(a,b)而表尾为(c)
6.串的子串个数为n(n+1)/2+1(1+1+2+…+n,空串也算所以加1)
7.主串长度为n,模式串长度为m,KMP算法时间复杂度为O(m+n)
习题
2.串 “ababaabab” 的 nextval 为(A)
A. 010104101 B. 010102101 C. 010100011 D0101010
3.设有数组 A[i,j], 数组的每个元素长度为 3 字节, i 的值为 1~8 , j的值为 1~10 ,
数组从内存首地址 BA 开始顺序存放, 当用以列为主存放时, 元素 A[5,8]的存储首地址为(B)
A. BA+ 141 B. BA+ 180 C. BA+222 D. BA+225
答:以列为主那就是一列一列的存,[5,8]表示这是第8列,前面有7列是存满的,所以这是第(7*8)+5=61个元素,而其地址为BA+(61-1)*3=BA+180
注意要不要减1的问题,可先试下,假如是第二个元素只需要加一倍的3即BA+3所以要减1
4.二维数组 A 的每个元素是由 10 个字符组成的串,其行下标 i=0,1, …,8,列下标j=1,2, , ,10 。若 A 按行先存储,元素 A[8,5] 的起始地址与当 A 按列先存储时的元素(B)的起始地址相同。设每个字符占一个字节。
A. A[8,5] B . A[3,10] C. A[5,8] D . A[0,9]
答:一定要注意下标是否从0开始,这里共有9行
行优先,[8,5]前面有8行(0,1,2,3,4,5,6,7共8行)所以是第8*10+5=85个元素
列优先,[3,10]前面有9列,所以是第9*9+4=85个元素 (注意行标从0开始)
计算总数记住行乘列,列乘行
5.广义表 ((a,b,c,d)) 的表头是( C ),表尾是( B )
A. a B . ( ) C. (a,b,c,d) D. (b,c,d)
答:第一个元素为表头其余均为表尾,所以表尾要带个外层的括号
6.设广义表 L=((a,b,c)) ,则 L 的长度和深度分别为( 1和2 )。
答:长度就看有多长(元素个数),深度就看有多深(括号层数)
7.以行序为主序方式,将n阶对称矩阵A的下三角形的元素(包括主对角线上所有元素)依次存放于一维数组B[1…(n(n+1))/2-1]中,则在B中确定aij (i A.i*(i-1)/2+j B.j*(j-1)/2+i C.i*(i+1)/2+j D.j*(j+1)/2+i 答:注意题目说的是确定aij (i 1.满二叉树(最完美最满的状态) 完全二叉树(编号是连续的即最右面缺而且是最后一层缺) 完全二叉树度为1的结点个数为0或1 当前结点编号为i,它的左孩子编号为2i,右孩子为2i+1(从1开始时) 2.二叉树常用性质 n0 = n2+1 即叶子节点个数为度为2的结点个数加1 先序遍历NLR:根节点->左子树->右子树。 5.哈夫曼树即带权路径最短树,也称最优树。 树的带权路径长度=树根到各叶子结点的路径(树根到该结点要走几步)乘对应权值;通常记作 WPL=∑wi×li 6.哈夫曼编码是最优前缀编码(任一个编码都不是其他编码的前缀,便于通信减少数据传输) 哈夫曼树没有度为1的结点,且不一定是完全二叉树 7.树的存储结构有三种:双亲表示法、孩子表示法、孩子兄弟表示法,其中孩子兄弟表示法是最常用的表示法,任意一棵树都能通过孩子兄弟表示法转换为二叉树进行存储。 8.含有n个节点的二叉树共有(2n)!/(n!*(n+1)!) (常考3个节点共5种) 9.二叉树的高度是最大层次数(根节点为第一层) 10.树和二叉树均可以为空(注意树可为空是在严蔚敏教材中可空,有的地方规定不能为空) 11.树的先序对应二叉树的先序,树的后序对应二叉树的中序(这里的二叉树一般指经孩子兄弟法转换的树) 12.哈弗曼树属于二叉树有左右子树之分 1. **2.**注意题目说的是存储树,而树的存储结构中,孩子兄弟表示法又称二叉链表表示法 3.在一颗度为4的树T中,若有20个度为4的结点,10个度为3的结点,1个度为2的结点,10个度为1的结点,则树T的叶结点个数是______82_。 答:任何树中,分支数(度数之和)比节点数少1 题目中,分支数为20*4+10*3+1*2+10*1=122,所以有123个节点 度为0的节点为123-20-10-1-10=82 也可用公式n0=1*n2+2*n3+3*n4+1=1+2*10+3*20+1=82 **4.**设哈夫曼树中有199 个结点,则该哈夫曼树中有_100__个叶子结点 答:哈弗曼树没有度为1的结点,n0=n2+1,n0+n2=199,所以n0=100 **5.**一棵高度为4的完全二叉树至少有______8_个结点 答:前三层是满二叉树,最后一层只有一个即1+2+4+1=8 7.一颗高度为h的完全二叉树至少有_____2h-1__个结点 答:最少的情况就是前h-1层是满的,第h层只有一个。 即2h-1-1(前h-1层)+1(第h层) 8.有n个结点,高度为n的二叉树的数目为_____2n-1__ 答:结点数和高度相同,那么每层都只有一个结点。对于除根节点以外的结点都可能是左子树或右子树,即有两种可能,n-1个2相乘即为2n-1 9.二叉树遍历 美国数学家赫夫曼(David Huffman)1952年发明了一种压缩编码方法,并得到广泛应用。为了纪念他的成就,人们把他在编码中用到的特殊的二叉树叫做赫夫曼树,他的编码方法叫做赫夫曼编码。 下面一段程序用来给学生考试成绩划分等级: 这段程序的判断过程如图: 图T36 不过这样的判断算法效率可能有问题,因为对于一般的考卷,学生成绩在5个等级上的分布规律如下表: 分数 0 ~ 59 60 ~ 69 70 ~ 79 80 ~ 89 90 ~ 100 所占比例 5% 15% 40% 30% 10% 仔细观察,中等成绩(70 ~ 79)比例最高,其次是良好(80 ~ 89),不及格所占比例最少。于是把图T36中的表示判断过程的二叉树重新调整如下图: 图T37 看起来判断效率肯定是提高了,但具体提高多少未知。下面就来看看赫夫曼先生是如何说的。 赫夫曼树的定义与原理 首先把上面两颗二叉树简化为叶子结点带权的二叉树(注:树结点之间的边相关的数叫做权(weight))。其中A表示不及格,B表示及格,C表示中等,D表示良好,E表示优秀。 图T38 从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称作路径长度。如上图中左边的二叉树,根节点到D的路径长度为4,而右边的二叉树根节点到D的路径长度为2。树的路径长度就是从根节点到每一结点的路径长度之和。上图中左边的二叉树的路径长度为1+1+2+2+3+3+4+4=20,右边的二叉树的路径长度为1+2+2+3+3+1+2+2=16。 如果考虑到带权的结点,结点的带权路径长度就是从该结点到根节点之间的路径长度与结点上权的乘积。树的带权路径长度就是树中所有叶子结点的带权路径长度之和。假设有n个权值{w1,w2,…wn},构造一棵有n个叶子结点的二叉树,每个叶子结点带权wk,每个叶子的路径长度为lk,则其中带权路径长度WPL最小的二叉树称作赫夫曼树或最优二叉树。如图T38中左边二叉树的带权路径长度为WPL=5*1 + 15*2 + 40*3 + 30*4 + 10*4 = 315,右边二叉树的WPL=5*3 + 15*3 + 40*2 + 30*2 + 10*2 = 220。这样就可以看出右边的二叉树的性能要比左边的二叉树的性能高上很多。那右边的二叉树是否是最优的赫夫曼树呢?赫夫曼树是如何构造出来的呢?看看下面的解决办法: 1 先把有权值的叶子结点按照从小到大的顺序排列:A5,E10,B15,D30,C40。 2 取头两个最小权值的结点作为一个新结点N1的两个孩子,相对较小的是左孩子。新结点的权值为两个叶子权值的和。如下图: 图T39 3 将N1替换A和E,新序列为:N115,B15,D30,C40。 4 重复步骤2,将N1与B作为新结点N2的两个孩子,N2的权值为15+15=30。如下图: 图T40 5 将N2替换N1和B,新序列为:N230,D30,C40。 6 重复步骤2。将N2和D作为新结点N3的两个孩子,N3的权值为30+30=60,如下图: 图T41 7 将N3替换N2和D,新序列为:C40,N360。 8 重复步骤2,将C与N3作为新结点T的两个孩子,T是根节点,至此完成赫夫曼树的构造。如下图: 图T42 图T42中的二叉树的WPL = 40*1 + 30*2 + 15*3 + 10*4 + 5*4 = 205。经过上面步骤构造出来的二叉树就是最优的赫夫曼树。 由此得出赫夫曼树的构造方法描述: 1 根据给定的n个权值{w1,w2,…wn}构成n棵二叉树的集合F={T1,T2,…Tn},其中每棵二叉树Ti中只有一个带权为wi的根节点,其左右子树为空。 2 在F中选取两棵根节点的权值最小的树作为左右子树构造一棵新的二叉树,且新置的二叉树的根节点的权值为其左右子树根节点的权值之和。 3 在F中删除这两棵树,同时将新得到的二叉树加入F中。 4 重复2和3步骤,直到F只含一棵树为止,这棵树就是赫夫曼树。 赫夫曼编码 赫夫曼在研究这种最优二叉树时的主要目的是解决当年远距离通信(主要是电报)的数据传输的最优化问题。比如传输一串字符“BADCADFEED”,采用二进制数据表示,如下表: 字母 A B C D E F 二进制字符 000 001 010 011 100 101 编码之后的二进制数据流为“001000011010000011101100100011”,对方接收时同样按照3位一组解码。现在假设这6个字母出现的频率不同,A 27%,B %8,C 15%,D 15%,E 30%,F 5%。下面将27、8、15、15、30、5分别作为A、B、C、D、E、F的权值构造赫夫曼树,如下图: 图T43 将图T43中赫夫曼树的权值左分支改为0,右分支改为1,如下图: 图T44 现在将这6个字母用从根节点到叶子所经过路径的0或1来编码,得到的编码表如下: 字母 A B C D E F 二进制字符 01 1001 101 00 11 1000 将“BADCADFEED”再次编码得到“1001010010101001000111100”,共25个字符,与之前编码得到的30个字符相比大约节约了17%的存储和传输成本。 在解码时,用同样的赫夫曼树,即发送方和接收方约定好同样的赫夫曼编码规则。当接收方接收到“1001010010101001000111100”时,比对图T44中的赫夫曼树,由1001正好走到字母B,如下图: 图T45 然后是01,则从根结点走到字母A,如下图: 图T46 其余的字母也可相应成功解码。 仔细观察上面的赫夫曼编码表中各个字母的编码会发现,不存在容易与1001、1000混淆的10、100等编码。这就说明若要设计长短不等的编码,则必须是任一字符的编码都不是另一个字符的编码的前缀,这种编码称作前缀编码。 下面是赫夫曼编码的定义: 一般的,设需要编码的字符集为{d1,d2,…,dn},各个字符在电文中出现的次数或频率集合为{w1,w2,…,wn},以d1,d2,…dn作为叶子结点,以w1,w2,…wn作为相应叶子结点的权值来构造一棵赫夫曼树。规定赫夫曼树的左分支代表0,右分支代表1,则从根节点到叶子节点所经过的路径分支组成的0和1的序列便为该结点对应字符的编码,这就是赫夫曼编码。 原文地址:http://www.cppblog.com/cxiaojia/archive/2012/08/20/187776.html 平衡二叉树(Balanced Binary Tree)是二叉查找树的一个进化体,也是第一个引入平衡概念的二叉树。1962年,G.M. Adelson-Velsky 和 E.M. Landis发明了这棵树,所以它又叫AVL树。平衡二叉树要求对于每一个节点来说,它的左右子树的高度之差不能超过1,如果插入或者删除一个节点使得高度之差大于1,就要进行节点之间的旋转,将二叉树重新维持在一个平衡状态。这个方案很好的解决了二叉查找树退化成链表的问题,把插入、查找、删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。 平衡二叉树实现的大部分过程和二叉查找树是一样的(学平衡二叉树之前一定要会二叉查找树),区别就在于插入和删除之后要写一个旋转算法去维持平衡,维持平衡需要借助一个节点高度的属性。我参考了机械工业出版社的《数据结构与算法分析 - C语言描述》写了一个C++版的代码。这本书的AVLTree讲的很好,不过没有很完整的去描述。我会一步一步的讲解如何写平衡二叉树,重点是平衡二叉树的核心部分,也就是旋转算法。 第一步:节点信息 相对于二叉查找树的节点来说,我们需要用一个属性表示二叉树的高度,目的是维护插入和删除过程中的旋转算法。代码如下 第二步:平衡二叉树(AVL)类的声明 声明中的旋转函数将在后边的步骤中详解。代码如下: 第三步:两个辅助方法 旋转算法需要借助于两个功能的辅助,一个是求树的高度,一个是求两个高度的最大值。这里规定,一棵空树的高度为-1,只有一个根节点的树的高度为0,以后每多一层高度加1。为了解决指针NULL这种情况,写了一个求高度的函数,这个函数还是很有必要的。 代码如下: 第四步:旋转 对于一个平衡的节点,由于任意节点最多有两个儿子,因此高度不平衡时,此节点的两颗子树的高度差为2。容易看出,这种不平衡出现在下面四种情况: (1)6节点的左子树3节点高度比右子树7节点大2,左子树3节点的左子树1节点高度大于右子树4节点,这种情况成为左左。 (2)6节点的左子树2节点高度比右子树7节点大2,左子树2节点的左子树1节点高度小于右子树4节点,这种情况成为左右。 (3)2节点的左子树1节点高度比右子树5节点小2,右子树5节点的左子树3节点高度大于右子树6节点,这种情况成为右左。 (4)2节点的左子树1节点高度比右子树4节点小2,右子树4节点的左子树3节点高度小于右子树6节点,这种情况成为右右。 从图2中可以看出,1和4两种情况是对称的,这两种情况的旋转算法是一致的,只需要经过一次旋转就可以达到目标,我们称之为单旋转。2和3两种情况也是对称的,这两种情况的旋转算法也是一致的,需要进行两次旋转,我们称之为双旋转。 第五步:单旋转 单旋转是针对于左左和右右这两种情况的解决方案,这两种情况是对称的,只要解决了左左这种情况,右右就很好办了。图3是左左情况的解决方案,节点k2不满足平衡特性,因为它的左子树k1比右子树Z深2层,而且k1子树中,更深的一层的是k1的左子树X子树,所以属于左左情况。 为使树恢复平衡,我们把k2(此处可能是作者笔误,应该为k1)变成这棵树的根节点,因为k2大于k1,把k2置于k1的右子树上,而原本在k1右子树的Y大于k1,小于k2,就把Y置于k2的左子树上,这样既满足了二叉查找树的性质,又满足了平衡二叉树的性质。 这样的操作只需要一部分指针改变,结果我们得到另外一颗二叉查找树,它是一棵AVL树,因为X向上一移动了一层,Y还停留在原来的层面上,Z向下移动了一层。整棵树的新高度和之前没有在左子树上插入的高度相同,插入操作使得X高度长高了。因此,由于这颗子树高度没有变化,所以通往根节点的路径就不需要继续旋转了。 代码如下: (我觉得SingRotateLeft和SingRotateRight函数应该在结尾处添加:k2 = k1;) 第六步:双旋转 对于左右和右左这两种情况,单旋转不能使它达到一个平衡状态,要经过两次旋转。双旋转是针对于这两种情况的解决方案,同样的,这样两种情况也是对称的,只要解决了左右这种情况,右左就很好办了。图4是左右情况的解决方案,节点k3不满足平衡特性,因为它的左子树k1比右子树Z(此处为作者笔误,应该为:右子树D)深2层,而且k1子树中,更深的一层的是k1的右子树k2子树,所以属于左右情况。 为使树恢复平衡,我们需要进行两步,第一步,把k1作为根,进行一次右右旋转,旋转之后就变成了左左情况,所以第二步再进行一次左左旋转,最后得到了一棵以k2为根的平衡二叉树树。 代码如下: 第七步:插入 插入的方法和二叉查找树基本一样,区别是,插入完成后需要从插入的节点开始维护一个到根节点的路径,每经过一个节点都要维持树的平衡。维持树的平衡要根据高度差的特点选择不同的旋转算法。 代码如下: (我觉得insertpri函数结尾处:node->hgt = Max(…),此处应该在Max(…)后加1:node->hgt = Max(…) + 1) 第八步:查找 和二叉查找树相比,查找方法没有变法,不过根据存储的特性,AVL树能维持在一个O(logN)的稳定的时间,而二叉查找树则相当不稳定。 代码如下: 第九步:删除 删除的方法也和二叉查找树的一致,区别是,删除完成后,需要从删除节点的父亲开始向上维护树的平衡一直到根节点。 代码如下: 第十步:中序遍历 代码如下: 第十一步:关于效率 此数据结构插入、查找和删除的时间复杂度均为O(logN),但是插入和删除需要额外的旋转算法需要的时间,有时旋转过多也会影响效率。 关于递归和非递归。我用的是递归的方法进行插入,查找和删除,而非递归的方法一般来说要比递归的方法快很多,但是我感觉非递归的方法写出来会比较困难,所以我还是选择了递归的方法。 还有一种效率的问题是关于高度信息的存储,由于我们需要的仅仅是高度的差,不需要知道这棵树的高度,所以只需要使用两个二进制位就可以表示这个差。这样可以避免平衡因子的重复计算,可以稍微的加快一些速度,不过代码也丧失了相对简明性和清晰度。如果采用递归写法的话,这种微加速就更显得微乎其微了。 如果有哪些不对的或者不清晰的地方请指出,我会修改并加以完善。 附:完整代码 1.完全图:任意两个顶点都有边,都可达,有向完全图的边数(n*(n-1))是无向完全图的2倍 2.子图:一个图的一部分称为其子图 3.回路或环:简单来说就是转个圈 4.简单回路:转圈的过程不能有重复的点 5.连通图:图的每两个顶点都有一个到另一个的路径,若都互相可达就是强连通(不一定是完全图) 6.生成树:含图的全部顶点但只有n-1条边而且是连通图(就是用线串起来所有顶点) 7.邻接矩阵存储图,若没权值1代表有边,0代表没边。若有权值,有边存权值,没边存无穷大 8.图中度数之和为边数之和的2倍(一条边被两个顶点共用所以是2倍) 9.完全图要求每两个顶点都有一条边(无向时),连通图只要求两个顶点之间存在路径就行(可能是多条边) 10.深度优先(DFS)即越深越好,直到不能再深了这时退到上一层继续深度优先。类似先序借助于栈(递归) 广度优先(BFS)就是越广越好类似层次遍历,而且先被访问的节点其子节点也先被访问。借助于队列(存放被访问的结点) 广度和深度若用邻接矩阵实现时间复杂度为O(n2),邻接表是O(n+e)即O(顶点+边) 层次遍历就是一层一层从左到右遍历 树的先序,中序,后序遍历用栈,层次遍历用队列。 11.最小生成树:加权连通图的最小权值生成树,常用于修一条路使得可到所有顶点且花费最小 普里姆(Prim)算法:加点不构成回(选可达的最小的点)适合稠密图 克鲁斯卡尔(Kruskal)算法:加边不构成回(选现有的最小的边)适合稀疏图 12.v(vertex)是顶点,e(edge)是边 13.求某个点到其余各点的最短路径:迪杰斯特拉(Dijkstra)算法O(n2)(必考) 求每对顶点的最短路径:弗洛伊德(Floyd)算法O(n3)(不常考) Floyd:比如求v0到其他顶点,在邻接矩阵中,v0这一行这一列这一主对角线划掉,剩下的中间经过v0看是否比原来路径短,若短则更新 14.拓扑排序:对有向无环图的顶点的一种排序 15.AOV网:在有向图中,用顶点表示活动,弧表示活动间的优先关系,则称此有向图为用顶点表示活动的网络(Activity On Vertex Network翻译即在顶点上的活动) 16.拓扑排序可以解决先决条件问题,比如学院有的课是其他课的基础,怎样排课的问题 找到入度为0的点输出,删除该点的所有出边,找到剩余点中入度为0的点输出,删除所有出边,重复操作(借用队列实现,若入度为0则入队,当前没有入度为0的点则出队,也可用栈二者结果不同) 17.AOE网:用顶点表示事件,弧表示活动(注意和AOV网相反),弧上的权值表示活动持续时间(Activity On Edge Network)。其用于研究 1.完成工程最短时间 2.哪些活动是影响工程的关键 18.关键路径:即从源点(起始点)到汇点(最终点)最长的路径,路径上的活动称为关键活动 19.事件的最早发生时间:从前往后找前驱节点到当前节点的最大时间 前面的都完成就发生就是最早 事件的最迟发生时间:从后往前,后继节点的最迟-边的权值(找其中最小的)超过最迟后面就无法完成 源点和汇点的最早(都为0)和最晚(路径最大值)相同 20.有向图的极大强连通子图,称为强连通分量 答:若要让顶点最少,就是顶点之间的边尽可能的多,最好每两个点都有边,又说是非连通,那么可以一个连通图加一个点。8个顶点有(8*7)/2=28条边加一个点就是非连通,所以是9个点 2.一个有n个结点的图,最少有(1 )个连通分量,最多有(n )个连通分量 答:最少就是整体是连通图时,最多就是每个顶点都是孤立的点,那么每个点都是连通分量,注意不可能有0个连通分量,只要有点(哪怕一个)就得是连通分量 3.N个顶点的无向连通图用邻接矩阵表示时,该矩阵 至少有 2(n-1) 个非零元素。 答:邻接矩阵非零元素的个数即图的边数之和的2倍(因为无向一条边会被存两次),图最少有n-1条边,那么矩阵最少有2(n-1)个非零元素 4.深度优先和广度优先遍历结果均不唯一 若是Kruskal算法即加边,第一次选取最小的一条边即(v1,v4)第二次最小的边是8即图中所示三个边 若是Prim算法即加点法,从V4开始,v4可到达的点中到达v1最小,然后v1和v4所能到达的其他点中(v1,v3)和(v4,v3)最小,所以答案为(v2,v3) 6.下图共有3种拓扑排序P 7.判断一个图是否有回路除了用拓扑排序还可以用深度优先遍历(若遍历转一圈回到自身即存在回路) 8.有向图可拓扑排序的判别条件是____不存在环____(拓扑排序的定义就是对有向无环图定义的) 9.邻接表示例 ,注意存的是顶点的数组下标,即使有权值也是存下标 10最小生成树计算过程(加边不构成回) 11.最短路径问题 13.由邻接矩阵写成深度优先和广度优先遍历结果 深度优先:要求越深越好。第一行1和7有边,然后由7出发,7和3有边,然后由3出发,3和4有边… 广度优先:要求越广越好。第一行1和7,1和9有边(所以7和9是1的左右孩子),然后7和9同时出发… 14.由邻接表写成深度优先和广度优先遍历结果 广度优先:0出发,0后面有1,2,3。所以遍历结果为0 1 2 3 深度优先:0出发,0后面第一个为1,由1出发,1后面第一个0访问过了,所以访问2,由2出发。2后面0和1都被访问过了,所以访问3也是 0 1 2 3 注意深度优先给出邻接表不能画图求,画图比如0后面的1 2 3是没有次序的,先访问哪个都行。但是若给出邻接表那么一定先访问1,所以邻接表求深度优先遍历是唯一的 虽然这题二者结果相同,但思想不同(越深越好和越广越好) 15.用DFS遍历一个无环有向图,并在DFS算法退栈返回时打印相应的顶点,则输出的顶点序列是____逆拓扑有序___ 答:比如有A->B->C。A先入栈,然后A可到B所以B入栈,B可到C所以C入栈,C没有可达的所以C出栈,然后是BA出栈。而拓扑排序先是A,删除A的出边,B入度为0所以是B,以此类推得到ABC 这题说的退栈返回打印顶点不是按照深度优先搜索的顺序输出,最先访问的在栈底最后才能弹出 16.假设一个有n个顶点和e条弧的有向图用邻接表表示,则删除与某个顶点V1相关的所有弧的时间复杂度是(C) A.O(n) B.O(e) C.O(n+e) D.O(n*e) 答:要找到所有指向这个顶点的边,必须得遍历邻接表所有顶点然后遍历每个顶点的边看是否和V1相连,相当于对邻接表遍历,而邻接表遍历深度优先和广度优先都是O(n+e),注意不是O(n*e) 1.线性表的查找(静态查找表) 顺序查找 (就是最简单的按顺序一个一个比较) 判定树的中序遍历(左根右)得到的是有序的序列(判定树左子树比根节点小,右子树比根节点大) 3.加入监视哨(存待查元素) 免去每一步查找都要判断是否查找完的情况,只要读到监视哨就说明没查到 4.树表的查找(动态(可插入删除)查找表) 二叉排序树(判定树就是二叉排序树,左比根小右比根大) 6.平衡调整:当插入一个结点破坏了平衡性就要调整 LL型调整 若是LL和RR型就把画圈的中间的那个掂起来(想想有重量,另外俩即自己落下去了) 若是LR和RL型就把画圈的最下面那个掂起来(另外俩也落到它两边) 若新插入结点在最小不平衡根节点的左(L)孩子的左(L)子树上即为LL型调整 若新插入结点在最小不平衡根节点的右®孩子的右左(L)子树上即为RL型调整 7.B-树(B树) m阶B-树,阶数其实就是树的度数 适合外存文件系统索引 根结点最少有两个分支 (叶子节点除外) 如查找关键字42。首先在根结点查找,因为42>30,则沿着根结点中指针p[1](下标从0开始)往右下走;因为39<42<45,则沿着子树根结点中指针p[1]往下走,在下层结点中查找关键字42成功,查找结束。 9.B+树是B-树的变型树,更适合做文件系统的索引。 叶子结点包含所有关键字从左到右递增且顺序连接 负载(装载因子):表中结点/表的空间,所以表越满越容易发生冲突 冲突:不相等的关键字计算出了相同的地址 同义词:发生冲突的两个关键字 11.散列表的构造方法 数字分析法 取关键字的若干位或其组合做哈希地址 开放地址法 1.折半查找求判定树 答:先找中间的值,(1+20)/2=10,所以1-9为10的左子树(比根小),11-20为10的右子树。 比较时先和10比较,若比10小,则比较1-9,那先和谁比较呢,1-9中的中间值为(1+9)/2=5,所以先和5比较(即5和10相连)。如果还比5小,那就要和1-4比了,同样1-4先和谁比呢,1-4的中间值(1+4)/2=2,所以先和2比较(即2和5相连比5小在左边),其他依次类推 查找为4的有1、3、6、8、11、13、16、19(依次和10,15,18,19比较所以4次) 2.用顺序表和单链表表示的有序表均可使用折半查找方法来提高查找速度。 (错) 答:单链表无法使用折半查找必须是顺序存储,因为要取中间值 答:二叉排序树序列可理解为一个元素与二叉排序树比较的记录构成的序列。A中91后面是24说明待查元素X比91小所以后面是24,而24后面是94,说明X比24大,但是24前面已经比较过91了(说明已经肯定比91小了),现在后面又来了个94显然是错的 4. 答:装填因子越大就越满越可能发生冲突。冲突少减少不必要的查找。 不能完全避免聚集(不是同义词却抢相同地址)只能减少但可避免二次聚集 5.n个元素的表做顺序查找时,若查找每个元素的概率相同,则平均查找长度为_______(n+1)/2______ 答:总查找次数为1+2+3+…+n=n(n+1)/2,则平均查找长度为N/n=(n+1)/2 6.如果要求一个线性表既能较快的查找,又能适应动态变化的要求,最好采用 (C) A.顺序查找 B.折半查找 C.分块查找 D.哈希查找 答:分块查找的优点是:在表中插入和删除数据元素时,只要找到该元素对应的块, 就可以在该块内进行插入和删除运算。由于块内是无序的,故插入和删除比较容易,无需进行大量移动。如果线性表既要快速查找又经常动态变化,则可采用分块查找。严版P198 7.对22个记录的有序表作折半查找,当查找失败时,至少需要比较 ( 4 ) 次关键字。 答:4层的满二叉树有24-1=15个结点,5层的有31。题目是22个结点,所以是前4层是满二叉树,第五层不是满的,因此最少4次,最多5次。 8.下面关于 B- 和 B+ 树的叙述中,不正确的是( C)。 A. B- 树和 B+ 树都是平衡的多叉树 B. B- 树和 B+ 树都可用于文件的索引结构 C. B- 树和 B+ 树都能有效地支持顺序检索 D. B- 树和 B+ 树都能有效地支持随机检索 答:B+树支持顺序(从最小的关键字叶子起从左到右),而B-树因为其叶子结点互相没连接只支持从根节点起随机检索 9.假定对有序表: (3, 4,5,7,24,30,42,54,63,72,87,95) 进行折半查找, 答:1️⃣ 画判定树一般先画出坐标的判定树,再根据坐标填值即可,注意取下界及low和high的变化 2️⃣ 需要与30、63、87、95比较 3️⃣ 前3层:1+2*2+4*3=17 第四层:5*4=20 ASL=(17+20)/ 12 = 3.08 即总查找次数除总个数 10.设哈希函数 H(K) =3 K mod 11 ,哈希地址空间为 0~ 10 ,对关键字序列( 32, 13 ,49, 24 , 38, 21 , 4, 12),按下述两种解决冲突的方法构造哈希表,并分别求出等概率下查找成功时和查找失败时的平均查找长度 ASLsucc 和 ASLunsucc 。 答:1️⃣ 散列地址就是若算的关键字为空就放里面,不为空就往后找 ASLsucc = ( 1+1+1+2+1+2+1+2 ) /8=11/8 ASLunsucc =( 1+2+1+8+7+6+5+4+3+2+1 ) /11=40/11 因为最多成功查8个元素,所以查找成功时分母为8,分子就是每个元素查找的次数之和 ASLsucc = ( 1*5+2*3 ) /8=11/8 第一列查一次就知道了第二列要查两次 ASLunsucc =( 1+2+1+2+3+1+3+1+3+1+1 ) /11=19/11 失败的情况:查一次若为空说明肯定不存在,若不为空继续向下查直到为空说明到底了查找失败(比如第二行需要查两次,第一次查到为4,第二次查到了空,记住不是查一次就行) 11.适宜于对动态查找表进行高效率查找的组织结构是 (C) A.有序表 B. 分块有序表 C. 三叉排序表 D. 线性 答:如果线性表既要快速查找又要经常动态变化,则可采用分块查找。而这里说的是动态查找表,分块查找属于静态查找表。动态即要进行修改。有序表和线性不适合修改操作 1.稳定性:排序前和排序后相同关键字的相对位置不发生变化就是稳定的 若关键字都不重复稳定与否无关紧要,若重复就得具体分析 稳定性:希尔快速选择堆不稳,其他都稳 时间: 除特例外插入和交换类时间都是O(n2),剩下的时间都是O(nlog2n) 可记忆为因为简单所以时间长,快速是最快的不可能是O(n2),希尔是最怪的,基数是最长的 空间: 树形(锦标赛)分叉多或赛道多而归并要一级一级选占空间最多,快速去掉n,基数去掉d 直接插入排序 (最简单,性能最佳) 关键字较乱,不要求稳定性:快速排序 1.设待排序的关键字序列为{12,2, 16, 30, 28, 10, 16*, 20, 6, 18}, 试分别写出使用以下排序方法, 每趟排序结束后关键字序列的状态 ①直接插入排序 (第n趟将第n个待排序关键字插入到前面已排序的序列) 原序列为{12,2, 16, 30, 28, 10, 16*, 20, 6, 18} [2 12] 16 30 28 10 16* 20 6 18 第2个即12与前面的排序 ②折半插入排序 排序过程同①,只不过查找插入位置用的是折半查询 ③希尔排序 (增量选取5,3,1) {12,2, 16, 30, 28, 10, 16*, 20, 6, 18} 6 2 12 10 18 16 16* 20 30 28 (增量选取 3)第1和第4,第2和第5… 2 6 10 12 16 16* 18 20 28 30 (增量选取 1) 就是直接插入排序 ④冒泡排序 (1号与2号比较然后2号与3号比较…,可确定最大的元素放在最后) 原序列为{12,2, 16, 30, 28, 10, 16*, 20, 6, 18} 2 10 12 6 16 [16* 18 20 28 30] ⑤快速排序 (选一枢轴,两边指针往中间移使得比枢轴小的移到其左边,先移右指针) ⑥简单选择排序 (第n趟选择最小和第n个位置元素交换) 原序列为{12,2,16, 30, 28, 10, 16*, 20, 6, 18} ⑦堆排序 原序列为{12,2, 16, 30, 28, 10, 16*, 20, 6, 18} 6 20 16 18 12 10 16* 2 [28 30] 得到两个最大值,继续调整交换 … 由于此题没有答案,下面类似 建堆(按编号即层次遍历)然后调整堆(从最后面的非叶子结点向前选择最大的放到根,可能不止调整一趟)。 然后交换根和最后一个编号(注意不是最小),再重新调整交换重复操作 ⑧二路归并排序 (每两个归并成一组有序序列,再每两组归并成一组有序序列) 原序列为{12,2, 16, 30, 28, 10, 16*, 20, 6, 18} 2.树形选择(锦标赛)排序 原序列为{49,38, 65, 97, 76, 13, 27, 49*} 3.基数排序 原序列为{278,109,063,930,589,184,505,269,008,083} 第二趟收集按十位放到对应桶中 即结果为:505 008 109 930 063 269 278 083 184 589 我们可以看到最低2位已经有序了,只需再来一趟收集即可,就不写了 5.对 n 个不同的排序码进行冒泡排序, 在元素无序的情况下比较的次数最多为( ) 答:比较次数最多时,第一趟比较 n-1 次,第二趟比较 n-2 次, 最后一趟比较 1 6.若一组记录的排序码为( 46, 79 , 56,38 , 40,84),则利用快速排序的方法,以第一个记录为基准得到的一次划分结果为() 答:左右设两指针,右指针先移,84比46大不移动,40比46小所以40覆盖46的位置,然后该左边的指针移动了,79比46大,所以移到空着的原40的位置。然后该右指针移了,38比46小所以38覆盖空着的原79位置,左边的56比46大移到空着的原38,然后将46放到空着的原56即可。结果为:40,38,46,56,79,84 7.数据表中有 10000 个元素,如果仅要求求出其中最大的 10 个元素,则采用 ( D ) A.冒泡排序 B .快速排序 C.简单选择排序 D.堆 答:堆用大根堆一趟选取一个最大的最快 冒泡每两个比较,有10000个肯定慢。快速是选枢轴,再左右移动也慢 简单选择每一趟都几乎快遍历一遍也肯定慢 8.下列排序算法中, ( A )不能保证每趟排序至少能将一个元素放到其最终的位置上 A.希尔排序 B .快速排序 C. 冒泡排序 D.堆 答:快速排序的每趟排序能将作为枢轴的元素放到最终位置;冒泡排序的每趟排序能将最大或最小的元素放到最终位置;堆排序的每趟排序能将最大或最小的元素放到最终位置。而希尔排序只是对间隔为n的元素排序所以不确定。 L.elem[i]取值 //L.length-1=>i>=0,如元素为1,2,3,L.length=3,i=0,1,2 若带头结点,空表条件为L->next==NULL(L为头指针指向头结点永不为空) 若不带头结点,空表条件为L==NULL 栈空:S.top==S.base //首尾指针相同 栈满:S.top-S.base==S.stacksize //尾-首等于最大容量即为满 链栈一定是没有头结点,所以栈空的条件为:S==NULL 队空:Q.front==Q.rear //首尾指针相同 //尾指针指向的为最后一个元素的下一个地址(永远为空),所以+1 队满:(Q.rear+1)%MAXSIZE==Q.front 队空:Q.front=Q.rear 由于串、数组、广义表的存储结构不是重点在这里就不再列出其存储结构 栈和队列除了链栈都有头尾指针 哈希表(hash table)又称三列表,其基本思路是,设要存储的元素个数为n,设置一个长度为m(m>=n)的连续内存单元,以每个元素的关键字ki(0<=i<=n-1)为自变量,通过一个称为哈希函数(hash function)的函数h(ki)吧ki映射为内存单元的地址(或下标)h(ki),并把该元素存储在这个内存单元中,h(ki)也成为哈希地址(hash address)。把如此构造的线性表存储结构称为哈希表。 哈希函数的好坏首先影响出现冲突的频繁程度。 除留余数法是用关键字k除以某个不大于哈希表长度m的数p所得的余数作为希地址的方法。除留余数法的哈希函数h(k)为: 采用除留余数法哈希函数建立如下关键字集合的哈希表: {16,74,60,43,54,90,46,31,29,88,77}。 树和二叉树
知识点
有 n 个结点的完全二叉树的深度为⎣log2 n⎦+1 (没记住可以一个一个试)
深度为k的二叉树最多有2k-1个结点(满二叉树)
3.二叉树遍历
中序遍历LNR:左子树->根节点->右子树。必须要有中序遍历才能画出相应二叉树
后续遍历LRN:左子树->右子树->根节点。
助记:先后中遍历指的是根结点在先还是中还是右,且时间复杂度均为O(n)
层次遍历:一层一层从上到下,从左到右
4.二叉树线索化目的是加快查找结点的前驱或后继的速度。实质上就是遍历一次二叉树,检查当前结点左,右指针域是否为空,若为空,将它们改为指向前驱结点或后继结点
习题
答:n0= n2+1 n1=0或n1=1 n0+n1+n2=1001
赫夫曼树及其应用
if (a < 60)
b = "不及格";
else if (a < 70)
b = "及格";
else if (a < 80)
b = "中等";
else if (a < 90)
b = "良好";
else
b = "优秀";
一步一步写平衡二叉树(AVL树)
//AVL树节点信息
template
//AVL树类的属性和方法声明
template
//计算以节点为根的树的高度
template
//左左情况下的旋转
template
//左右情况的旋转
template
//插入
template
//查找
template
//删除
template
//中序遍历函数
template
图
知识点
习题
5.最小生成树问题
查找
知识点
算法简单对表结构无要求
折半查找(二分查找) (要求是顺序存储有序表)
data[mid] == k 找到元素,返回下标mid
data[mid] > k high=mid-1 (k比中间值小,要向中间值左边继续找)
data[mid] < k low=mid+1 (k比中间值大,要向中间值右边继续找)
助记:就是找到中间值比较待查元素和中间值,再换个中间值再比较
优点:比较次数少查找效率高,但不适于经常变动
分块查找 块之间有序(左块最大小于右块最小),块内任意,另建索引表放每块最大关键字
适用于既要快速查找又经常动态变化
2.折半查找的判定树:把中间位置的值作为树根,左边和右边的记录作为根的左子树和右子树
时间复杂度最好为O(log2n),最差退化成O(n)的顺序查找(如都只有1个分支)
平衡二叉树(AVL) 左右子树高度差绝对值不超过1
平衡因子:左子树的高度减去右子树的高度只能为0、-1、+1
由于后人发现树越矮查找效率越高因此发明了AVL,时间复杂度为O(log2n)
B-树 适合外存文件系统索引
B+树 适合做文件系统的索引
5.二叉排序树的删除:缺右补左,缺左补右,不缺左(左子树)中(中序)后(最后一个)
LR型调整
RR型调整
LR型调整
LL、LR等是对不平衡状态的描述
非终端结点最少有(m/2)上限个分支(根节点除外)
有n个分支的结点有n-1个关键字递增从左到右排列
叶子结点(失败结点)在同一层可用空指针表示,是查找失败到达的位置
8.B-树的查找 (类似于二叉树的查找,但是可以有三个或多个方向)
可从根节点随机查找也可从叶子结点顺序查找 (严格来讲,不算是树了)
10.散列表:根据给定的关键字计算关键字在表中的地址
适用于事先知道关键字集合且关键字位数比散列地址位数多
平方取中法 关键字平方后取中间若干位
适用于不了解关键字或难从关键字找到取值较分散的几位
折叠法 分割关键字后将这几部分叠加(舍去进位)
适用于散列地址位数少,关键字位数多
除留取余法 取模运算(最常用)
12.处理冲突的方法
线性探测法 看下一个元素是否为空,当成一个循环表来看 (可能二次聚集)
二次探测法 原基础加12、-12、22、-22、32… (可避免二次聚集)
伪随机探测法 原基础加个伪随机数 (可避免二次聚集)
链地址法 相同地址的记录放到同一个单链表中 (可避免二次聚集)习题
①画出描述折半查找过程的判定树;
②若查找元素90,需依次与哪些元素比较?
③假定每个元素的查找概率相等,求查找成功时的平均查找长度。
① 线性探测法;
② 链地址法。
而查找失败时可能计算得到的地址有11种,即分母为11,而关键字为空的查一次就知道失败了(要是有也不会为空),若不为空要往后找直到找到第一个空元素(说明确实没有这个元素不然该放到这个空着的位置了)
2️⃣ 链地址就是要是地址被占了放后面挂着就行
总结:查找成功看位置,查找失败就找空
排序
知识点
其他都是O(1)
4.关键字较少 ,选取简单的:
冒泡排序
关键字较多,就用先进的:
关键字基本有序,就用堆排序或归并排序
不要求稳定性:堆排序
要求稳定性:归并排序
关键字多但都较小:基数排序习题
[2] 12 16 30 28 10 16* 20 6 18 第一趟第一个有序
[2 12 16] 30 28 10 16* 20 6 18 第3个即16与前面的排序
[2 12 16 30] 28 10 16* 20 6 18 前4个有序
[2 12 16 28 30] 10 16* 20 6 18 前5个有序
[2 10 12 16 28 30] 16* 20 6 18
[2 10 12 16 16* 28 30] 20 6 18
[2 10 12 16 16* 20 28 30] 6 18
[2 6 10 12 16 16* 20 28 30] 18
[2 6 10 12 16 16* 18 20 28 30] 最后一个与前面的排序 (查找插入位置是依次比)
原序列为
10 2 16 6 18 12 16* 20 30 28 (增量选取 5)第1个和第6个排序,第2和第7…
2 12 16 28 10 16* 20 6 18 [30] 12与2比较交换,12和16比较,16和30比较…
2 12 16 10 16* 20 6 18 [28 30] 每一趟确定一个最大的放最后
2 12 10 16 16* 6 18 [20 28 30] 第3趟确定3个最大
2 10 12 16 6 16* [18 20 28 30] 确定4个最大
2 10 6 12 [16 16* 18 20 28 30]
2 6 10 [12 16 16* 18 20 28 30]
2 6 10 12 16 16* 18 20 28 30]
原序列为{12,2,16, 30, 28, 10, 16*, 20, 6, 18}
12 [6 2 10] 12 [28 30 16* 20 16 18] 先让右指针往左移
6 [2] 6 [10] 12 [28 30 16* 20 16 18 ] 一般选第一个为枢轴
28 2 6 10 12 [18 16 16* 20 ] 28 [30 ]
18 2 6 10 12 [16* 16] 18 [20] 28 30
16* 2 6 10 12 16* [16] 18 20 28 30
[2] 12 16 30 28 10 16* 20 6 18 最小的2和第一个12交换
[2 6 ]16 30 28 10 16* 20 12 18 最小的6和第二个12交换
[2 6 10 ]30 28 16 16* 20 12 18 最小的10和第三个16交换
[2 6 10 12] 28 16 16* 20 30 18 最小的12和第四个30交换
[2 6 10 12 16] 28 16* 20 30 18
[2 6 10 12 16 16* ]28 20 30 18
[2 6 10 12 16 16* 18 ]20 30 28
[2 6 10 12 16 16* 18 20 ]28 30
[2 6 10 12 16 16* 18 20 28] 30
18 12 16 20 28 10 16* 2 6 [30] 得到最大值30,继续调整交换
[2 12] [16 30] [10 28] [16 * 20] [6 18] 每两个合为一组
[2 12 16 30] [10 16* 20 28] [6 18] 每两组即四个合为一组
[2 10 12 16 16* 20 28 30] [6 18] 每两组即八个合为一组
[2 6 10 12 16 16* 18 20 28 30 ]
对树8个选4个最小,4个选倆,2选1,选中13为最小输出,置最下面13为无穷大,重复操作
准备10个桶,第一趟收集按个位放到对应桶中即结果为:930 063 083 184 505 278 008 109 589 269 (个位已经有序)
次,即 (n-1)+(n-2)+…+1= n(n-1)/2
算法最节省时间各类型存储结构
顺序表
#define MAXSIZE 100 //顺序表可能达到的最大长度
typedef struct
{
ElemType *elem; //存储空间的基地址(例如用L.elem[0]取元素)
int length; //当前长度
}SqList;
单链表
typedef struct LNode
{
ElemType data; //结点的数据域
struct LNode *next; //结点的指针域,指向下一结点
}LNode,*LinkList; //LinkList为指向结构体LNode的指针类型(相当于LNode *)
双向链表
typedef struct DuLNode
{
ElemType data; //结点的数据域
struct DuLNode *prior; //指向直接前驱
struct DuLNode *next; //指向直接后继
}DuLNode,*DuLinkList;
顺序栈
#define MAXSIZE 100 //顺序栈存储空间的初始分配量
typedef struct
{
SElemType *base; //栈底指针
SElemType *top; //栈顶指针
int stacksize; //栈可用的最大容量
}SqStack;
链栈
//定义类似,类似操作受限的单链表
typedef struct StackNode
{
ElemType data; //数据域
struct StackNode *next; //指向下一结点
}StackNode,*LinkStack;
循环队列
#define MAXSIZE 100 //队列可能达到的最大长度
typedef struct
{
QElemType *base; //存储空间的基地址
int front; //头指针(只是有指针的作用,例如用Q.base[Q.front]取元素)
int rear; //尾指针
}SqQueue;
链队
//只看第一个定义和单链表类似,不同的是第二个设了队头和队尾指针
typedef struct QNode
{
QElemType data; //数据域
struct QNode *next; //指向下一结点
}QNode,*QueuePtr;
typedef struct
{
QueuePtr front; //队头指针(相等于QNode *front)
QueuePtr rear; //队尾指针
}LinkQueue;
小结
顺序二叉树(不常用)
#define MAXSIZE 100 //二叉树的最大结点数
typedef TElemType SqBiTree[MAXSIZE] //0号存储根结点
SqBiTree bt;
二叉链表(常用)
typedef struct BiTNode
{
TElemType data; //结点数据域
struct BiTNode *lchild,*rchild; //左右孩子指针
}BiTNode,*BiTree;
线索二叉树
typedef struct BiThrNode
{
TElemType data;
struct BiThrNode *lchild,*rchild; //左右孩子指针
int LTag,RTag; //左右标志
}BiThrNode,*BiThrTree;
孩子兄弟二叉树
tyrpedef struct CSNode //又称二叉链表表示,本质存的是树用类似存二叉树的方法存
{
ElemType data;
struct CSNode *firstchild,*nextsibling; //即左是孩子右是兄弟
}CSNode,*CSTree;
邻接矩阵
#define MaxInt 32767 //表示极大值,即∞
#define MVNum 100 //最大顶点数
typedef char VertexType; //假设顶点的数据类型为字符型
typedef int ArcType; //假设边的权值类型为整型
typedef struct{
VertexType vexs[MVNum]; //顶点表
ArcType arcs[MVNum][MVNum]; //邻接矩阵
int vexnum,arcnum; //图的顶点数和边数
}AMGraph;
邻接表
//注意存的是顶点数组下标不是存的顶点本身
typedef struct ArcNode { //边结构
int adjvex; //该边所指向的顶点位置
struct ArcNode *nextarc; //指向下一条边的指针
OtherInfo info; //和边相关的信息
} ArcNode;
#define MVNum 100
typedef struct VNode{ //顶点结构
VertexType data; //顶点信息
ArcNode * firstarc; //指向依附该顶点的第一条弧的指针
} VNode, AdjList[MVNum];
typedef struct { //图结构
AdjList vertics ; //邻接表
int vexnum, arcnum; //顶点数和弧数
int kind; //图的种类
} ALGraph;
哈希表查找(哈希表的基本概念、3个因素、线性探测法解决冲突、哈希表的求解与建立)
哈希表的基本概念:
三个因素:
①哈希函数。
②处理冲突的方法。
③哈希表的装填因子。
假定哈希函数是“均匀的”,即不同的哈希函数对同一组随机的关键字,产生冲突的可能性相同。
对同一组关键字,设定相同的哈希函数,则不同的处理冲突的方法得到的哈希表不同,它的平均查找长度也不同。
若处理冲突的方法相同,其平均查找长度依赖于哈希表的装填因子。线性探测法
线性探测法是从发生冲突的地址(设为d)开始,依次探测d的下一个地址(当到达下标为m-1的哈希表表尾时,下一个探测的地址是表首地址0),直到找到一个空闲单元为止(当m≥n时一定能找到一个空闲单元)。线性探测法的数学递推描述公式为:
d0=h(k)
di=(di-1+1) mod m (1≤i≤m-1)
用线性探测解决冲突的例题:
除留余数法:
h(k)=k mod p (mod为求余运算,p≤m) ,p最好是质数(素数)。
采用除留余数法解决冲突例题:
除留余数法的哈希函数为:
h(k)=k mod 13
对构造的哈希表采用线性探测法解决冲突。
解:h(16)=3,h(74)=9,h(60)=8,h(43)=4,
h(54)=2,h(90)=12,h(46)=7,h(31)=5,
h(29)=3 冲突
d0=3,d1=(3+1) mod 13=4 冲突
d2=(4+1) mod 13=5 仍冲突
d3=(5+1) mod 13=6
h(88)=10
h(77)=12 冲突
d0=12,d1=(12+1) mod 13=0
建立的哈希表ha[0…12]如下表所示。
散列表查找
迪杰斯特拉算法
弗洛伊德算法
拓扑排序
关键路径算法