为了方便复习 下面内容摘自:数据结构期末总结_夏日 の 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,然后将还没遍历完的那个直接插到最后就行,所以是相加

8.顺序表和单链表的比较

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比较也就达到了不匹配时直接根据前后缀移动的目的

2️⃣ nextval数组:两种情况

若是不匹配就看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]

2.行优先和列优先

其实就是行优先就是从上到下先一行一行的存,列优先就是从左到右一列一列的存

无论是哪个其元素如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)

习题

1.求子串数目

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<j) 的位置k的关系为( B ) 。

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<j) ,i要小于j,但存的是下三角元素,假如a13=5,确定a13的位置就是确定5的位置,而a13=a31也就是根据i,j (i=1,j=3) 确定a31的位置,B中代入即3*1+1=4,而a31位置正是4(前面是1+2)

树和二叉树

知识点

1.满二叉树(最完美最满的状态) 完全二叉树(编号是连续的即最右面缺而且是最后一层缺)

完全二叉树度为1的结点个数为0或1

当前结点编号为i,它的左孩子编号为2i,右孩子为2i+1(从1开始时)

2.二叉树常用性质

n0 = n2+1 即叶子节点个数为度为2的结点个数加1
有 n 个结点的完全二叉树的深度为⎣log2 n⎦+1 (没记住可以一个一个试)
深度为k的二叉树最多有2k-1个结点(满二叉树)
3.二叉树遍历

先序遍历NLR:根节点->左子树->右子树。
中序遍历LNR:左子树->根节点->右子树。必须要有中序遍历才能画出相应二叉树
后续遍历LRN:左子树->右子树->根节点。
助记:先后中遍历指的是根结点在先还是中还是右,且时间复杂度均为O(n)
层次遍历:一层一层从上到下,从左到右
4.二叉树线索化目的是加快查找结点的前驱或后继的速度。实质上就是遍历一次二叉树,检查当前结点左,右指针域是否为空,若为空,将它们改为指向前驱结点或后继结点

5.哈夫曼树即带权路径最短树,也称最优树。

树的带权路径长度=树根到各叶子结点的路径(树根到该结点要走几步)乘对应权值;通常记作 WPL=∑wi×li

6.哈夫曼编码是最优前缀编码(任一个编码都不是其他编码的前缀,便于通信减少数据传输)

哈夫曼树没有度为1的结点,且不一定是完全二叉树

7.树的存储结构有三种:双亲表示法、孩子表示法、孩子兄弟表示法,其中孩子兄弟表示法是最常用的表示法,任意一棵树都能通过孩子兄弟表示法转换为二叉树进行存储。

8.含有n个节点的二叉树共有(2n)!/(n!*(n+1)!) (常考3个节点共5种)

9.二叉树的高度是最大层次数(根节点为第一层)

10.树和二叉树均可以为空(注意树可为空是在严蔚敏教材中可空,有的地方规定不能为空)

11.树的先序对应二叉树的先序,树的后序对应二叉树的中序(这里的二叉树一般指经孩子兄弟法转换的树)

12.哈弗曼树属于二叉树有左右子树之分

习题

1.答:n0= n2+1 n1=0或n1=1 n0+n1+n2=1001

**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

6.

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年发明了一种压缩编码方法,并得到广泛应用。为了纪念他的成就,人们把他在编码中用到的特殊的二叉树叫做赫夫曼树,他的编码方法叫做赫夫曼编码。

下面一段程序用来给学生考试成绩划分等级:

if (a < 60)b = "不及格";
else if (a < 70)b = "及格";
else if (a < 80)b = "中等";
else if (a < 90)b = "良好";
elseb = "优秀";

这段程序的判断过程如图:

图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的序列便为该结点对应字符的编码,这就是赫夫曼编码

一步一步写平衡二叉树(AVL树)

原文地址: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树节点信息
template<class T>
class TreeNode
{public:TreeNode():lson(NULL),rson(NULL),freq(1),hgt(0){}T data;//值int hgt;//以此节点为根的树的高度unsigned int freq;//频率TreeNode* lson;//指向左儿子的地址TreeNode* rson;//指向右儿子的地址
};

第二步:平衡二叉树(AVL)类的声明

声明中的旋转函数将在后边的步骤中详解。代码如下:

//AVL树类的属性和方法声明
template<class T>
class AVLTree
{private:TreeNode<T>* root;//根节点void insertpri(TreeNode<T>* &node, T x);//插入TreeNode<T>* findpri(TreeNode<T>* node, T x);//查找void insubtree(TreeNode<T>* node);//中序遍历void Deletepri(TreeNode<T>* &node, T x);//删除int height(TreeNode<T>* node);//求树的高度void SingRotateLeft(TreeNode<T>* &k2);//左左情况下的旋转void SingRotateRight(TreeNode<T>* &k2);//右右情况下的旋转void DoubleRotateLR(TreeNode<T>* &k3);//左右情况下的旋转void DoubleRotateRL(TreeNode<T>* &k3);//右左情况下的旋转int Max(int cmpa,int cmpb);//求最大值public:AVLTree():root(NULL){}void insert(T x);//插入接口TreeNode<T>* find(T x);//查找接口void Delete(T x);//删除接口void traversal();//遍历接口};

第三步:两个辅助方法

旋转算法需要借助于两个功能的辅助,一个是求树的高度,一个是求两个高度的最大值。这里规定,一棵空树的高度为-1,只有一个根节点的树的高度为0,以后每多一层高度加1。为了解决指针NULL这种情况,写了一个求高度的函数,这个函数还是很有必要的。

代码如下:

//计算以节点为根的树的高度
template<class T>
int AVLTree<T>::height(TreeNode<T>* node)
{if(node!=NULL)return node->hgt;return -1;
}
//求最大值
template<class T>
int AVLTree<T>::Max(int cmpa,int cmpb)
{return cmpa>cmpb?cmpa:cmpb;
}

第四步:旋转

对于一个平衡的节点,由于任意节点最多有两个儿子,因此高度不平衡时,此节点的两颗子树的高度差为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高度长高了。因此,由于这颗子树高度没有变化,所以通往根节点的路径就不需要继续旋转了。

代码如下:

//左左情况下的旋转
template<class T>
void AVLTree<T>:: SingRotateLeft (TreeNode<T>* &k2)
{TreeNode<T>* k1;k1=k2->lson;k2->lson=k1->rson;k1->rson=k2;k2->hgt=Max(height(k2->lson),height(k2->rson))+1;k1->hgt=Max(height(k1->lson),k2->hgt)+1;
}
//右右情况下的旋转
template<class T>
void AVLTree<T>::SingRotateRight(TreeNode<T>* &k2)
{TreeNode<T>* k1;k1=k2->rson;k2->rson=k1->lson;k1->lson=k2;k2->hgt=Max(height(k2->lson),height(k2->rson))+1;k1->hgt=Max(height(k1->rson),k2->hgt)+1;
}

(我觉得SingRotateLeft和SingRotateRight函数应该在结尾处添加:k2 = k1;)

第六步:双旋转

对于左右和右左这两种情况,单旋转不能使它达到一个平衡状态,要经过两次旋转。双旋转是针对于这两种情况的解决方案,同样的,这样两种情况也是对称的,只要解决了左右这种情况,右左就很好办了。图4是左右情况的解决方案,节点k3不满足平衡特性,因为它的左子树k1比右子树Z(此处为作者笔误,应该为:右子树D)深2层,而且k1子树中,更深的一层的是k1的右子树k2子树,所以属于左右情况。

为使树恢复平衡,我们需要进行两步,第一步,把k1作为根,进行一次右右旋转,旋转之后就变成了左左情况,所以第二步再进行一次左左旋转,最后得到了一棵以k2为根的平衡二叉树树。

代码如下:

//左右情况的旋转
template<class T>
void AVLTree<T>::DoubleRotateLR(TreeNode<T>* &k3)
{SingRotateRight(k3->lson);SingRotateLeft(k3);
}
//右左情况的旋转
template<class T>
void AVLTree<T>::DoubleRotateRL(TreeNode<T>* &k3)
{SingRotateLeft(k3->rson);SingRotateRight(k3);
}

第七步:插入

插入的方法和二叉查找树基本一样,区别是,插入完成后需要从插入的节点开始维护一个到根节点的路径,每经过一个节点都要维持树的平衡。维持树的平衡要根据高度差的特点选择不同的旋转算法。

代码如下:

//插入
template<class T>
void AVLTree<T>::insertpri(TreeNode<T>* &node,T x)
{if(node==NULL)//如果节点为空,就在此节点处加入x信息{node=new TreeNode<T>();node->data=x;return;}if(node->data>x)//如果x小于节点的值,就继续在节点的左子树中插入x{insertpri(node->lson,x);if(2==height(node->lson)-height(node->rson))if(x<node->lson->data)SingRotateLeft(node);elseDoubleRotateLR(node);}else if(node->data<x)//如果x大于节点的值,就继续在节点的右子树中插入x{insertpri(node->rson,x);//如果高度之差为2的话就失去了平衡,需要旋转if(2==height(node->rson)-height(node->lson)) if(x>node->rson->data)SingRotateRight(node);elseDoubleRotateRL(node);}else ++(node->freq);//如果相等,就把频率加1node->hgt=Max(height(node->lson),height(node->rson));
}
//插入接口
template<class T>
void AVLTree<T>::insert(T x)
{insertpri(root,x);
}

(我觉得insertpri函数结尾处:node->hgt = Max(…),此处应该在Max(…)后加1:node->hgt = Max(…) + 1)

第八步:查找

和二叉查找树相比,查找方法没有变法,不过根据存储的特性,AVL树能维持在一个O(logN)的稳定的时间,而二叉查找树则相当不稳定。

代码如下:

//查找
template<class T>
TreeNode<T>* AVLTree<T>::findpri(TreeNode<T>* node,T x)
{if(node==NULL)//如果节点为空说明没找到,返回NULL{return NULL;}if(node->data>x)//如果x小于节点的值,就继续在节点的左子树中查找x{return findpri(node->lson,x);}else if(node->data<x)//如果x大于节点的值,就继续在节点的左子树中查找x{return findpri(node->rson,x);}else return node;//如果相等,就找到了此节点
}
//查找接口
template<class T>
TreeNode<T>* AVLTree<T>::find(T x)
{return findpri(root,x);
}

第九步:删除

删除的方法也和二叉查找树的一致,区别是,删除完成后,需要从删除节点的父亲开始向上维护树的平衡一直到根节点。

代码如下:

//删除
template<class T>
void AVLTree<T>::Deletepri(TreeNode<T>* &node,T x)
{if(node==NULL) return ;//没有找到值是x的节点if(x < node->data){//如果x小于节点的值,就继续在节点的左子树中删除xDeletepri(node->lson,x);if(2==height(node->rson)-height(node->lson))if(node->rson->lson!=NULL&&
(height(node->rson->lson)>height(node->rson->rson)) )DoubleRotateRL(node);elseSingRotateRight(node);}else if(x > node->data){Deletepri(node->rson,x);//如果x大于节点的值,就继续在节点的右子树中删除xif(2==height(node->lson)-height(node->rson))if(node->lson->rson!=NULL&&
(height(node->lson->rson)>height(node->lson->lson) ))DoubleRotateLR(node);elseSingRotateLeft(node);}else//如果相等,此节点就是要删除的节点{if(node->lson&&node->rson)//此节点有两个儿子{TreeNode<T>* temp=node->rson;//temp指向节点的右儿子while(temp->lson!=NULL) temp=temp->lson;//找到右子树中值最小的节点//把右子树中最小节点的值赋值给本节点node->data=temp->data;node->freq=temp->freq;Deletepri(node->rson,temp->data);//删除右子树中最小值的节点if(2==height(node->lson)-height(node->rson)){if(node->lson->rson!=NULL&&(height(node->lson->rson)>height(node->lson->lson) ))DoubleRotateLR(node);elseSingRotateLeft(node);}}else//此节点有1个或0个儿子{TreeNode<T>* temp=node;if(node->lson==NULL)//有右儿子或者没有儿子node=node->rson;else if(node->rson==NULL)//有左儿子node=node->lson;delete(temp);temp=NULL;}}if(node==NULL) return;node->hgt=Max(height(node->lson),height(node->rson))+1;return;
}
//删除接口
template<class T>
void AVLTree<T>::Delete(T x)
{Deletepri(root,x);
}

第十步:中序遍历

代码如下:

//中序遍历函数
template<class T>
void AVLTree<T>::insubtree(TreeNode<T>* node)
{if(node==NULL) return;insubtree(node->lson);//先遍历左子树cout<<node->data<<" ";//输出根节点insubtree(node->rson);//再遍历右子树
}
//中序遍历接口
template<class T>
void AVLTree<T>::traversal()
{insubtree(root);
}

第十一步:关于效率

此数据结构插入、查找和删除的时间复杂度均为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.有向图的极大强连通子图,称为强连通分量

习题

1.

答:若要让顶点最少,就是顶点之间的边尽可能的多,最好每两个点都有边,又说是非连通,那么可以一个连通图加一个点。8个顶点有(8*7)/2=28条边加一个点就是非连通,所以是9个点

2.一个有n个结点的图,最少有(1 )个连通分量,最多有(n )个连通分量

答:最少就是整体是连通图时,最多就是每个顶点都是孤立的点,那么每个点都是连通分量,注意不可能有0个连通分量,只要有点(哪怕一个)就得是连通分量

3.N个顶点的无向连通图用邻接矩阵表示时,该矩阵 至少有 2(n-1) 个非零元素。

答:邻接矩阵非零元素的个数即图的边数之和的2倍(因为无向一条边会被存两次),图最少有n-1条边,那么矩阵最少有2(n-1)个非零元素

4.深度优先和广度优先遍历结果均不唯一

5.最小生成树问题

若是Kruskal算法即加边,第一次选取最小的一条边即(v1,v4)第二次最小的边是8即图中所示三个边

若是Prim算法即加点法,从V4开始,v4可到达的点中到达v1最小,然后v1和v4所能到达的其他点中(v1,v3)和(v4,v3)最小,所以答案为(v2,v3)

6.下图共有3种拓扑排序P

7.判断一个图是否有回路除了用拓扑排序还可以用深度优先遍历(若遍历转一圈回到自身即存在回路)

8.有向图可拓扑排序的判别条件是____不存在环____(拓扑排序的定义就是对有向无环图定义的)

9.邻接表示例 ,注意存的是顶点的数组下标,即使有权值也是存下标

10最小生成树计算过程(加边不构成回)

11.最短路径问题

12.AOE网问题

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.线性表的查找(静态查找表)

顺序查找 (就是最简单的按顺序一个一个比较)
算法简单对表结构无要求
折半查找(二分查找) (要求是顺序存储有序表)
data[mid] == k 找到元素,返回下标mid
data[mid] > k high=mid-1 (k比中间值小,要向中间值左边继续找)
data[mid] < k low=mid+1 (k比中间值大,要向中间值右边继续找)
助记:就是找到中间值比较待查元素和中间值,再换个中间值再比较
优点:比较次数少查找效率高,但不适于经常变动
分块查找 块之间有序(左块最大小于右块最小),块内任意,另建索引表放每块最大关键字
适用于既要快速查找又经常动态变化
2.折半查找的判定树:把中间位置的值作为树根,左边和右边的记录作为根的左子树和右子树

判定树的中序遍历(左根右)得到的是有序的序列(判定树左子树比根节点小,右子树比根节点大)

3.加入监视哨(存待查元素) 免去每一步查找都要判断是否查找完的情况,只要读到监视哨就说明没查到

4.树表的查找(动态(可插入删除)查找表)

二叉排序树(判定树就是二叉排序树,左比根小右比根大)
时间复杂度最好为O(log2n),最差退化成O(n)的顺序查找(如都只有1个分支)
平衡二叉树(AVL) 左右子树高度差绝对值不超过1
平衡因子:左子树的高度减去右子树的高度只能为0、-1、+1
由于后人发现树越矮查找效率越高因此发明了AVL,时间复杂度为O(log2n)
B-树 适合外存文件系统索引
B+树 适合做文件系统的索引
5.二叉排序树的删除:缺右补左,缺左补右,不缺左(左子树)中(中序)后(最后一个)

6.平衡调整:当插入一个结点破坏了平衡性就要调整

LL型调整
LR型调整
RR型调整
LR型调整
LL、LR等是对不平衡状态的描述

若是LL和RR型就把画圈的中间的那个掂起来(想想有重量,另外俩即自己落下去了)

若是LR和RL型就把画圈的最下面那个掂起来(另外俩也落到它两边)

若新插入结点在最小不平衡根节点的左(L)孩子的左(L)子树上即为LL型调整

若新插入结点在最小不平衡根节点的右®孩子的右左(L)子树上即为RL型调整

7.B-树(B树) m阶B-树,阶数其实就是树的度数 适合外存文件系统索引

根结点最少有两个分支 (叶子节点除外)
非终端结点最少有(m/2)上限个分支(根节点除外)
有n个分支的结点有n-1个关键字递增从左到右排列
叶子结点(失败结点)在同一层可用空指针表示,是查找失败到达的位置
8.B-树的查找 (类似于二叉树的查找,但是可以有三个或多个方向)

如查找关键字42。首先在根结点查找,因为42>30,则沿着根结点中指针p[1](下标从0开始)往右下走;因为39<42<45,则沿着子树根结点中指针p[1]往下走,在下层结点中查找关键字42成功,查找结束。

9.B+树是B-树的变型树,更适合做文件系统的索引。

叶子结点包含所有关键字从左到右递增且顺序连接
可从根节点随机查找也可从叶子结点顺序查找 (严格来讲,不算是树了)
10.散列表:根据给定的关键字计算关键字在表中的地址

负载(装载因子):表中结点/表的空间,所以表越满越容易发生冲突

冲突:不相等的关键字计算出了相同的地址

同义词:发生冲突的两个关键字

11.散列表的构造方法

数字分析法 取关键字的若干位或其组合做哈希地址
适用于事先知道关键字集合且关键字位数比散列地址位数多
平方取中法 关键字平方后取中间若干位
适用于不了解关键字或难从关键字找到取值较分散的几位
折叠法 分割关键字后将这几部分叠加(舍去进位)
适用于散列地址位数少,关键字位数多
除留取余法 取模运算(最常用)
12.处理冲突的方法

开放地址法
线性探测法 看下一个元素是否为空,当成一个循环表来看 (可能二次聚集)
二次探测法 原基础加12、-12、22、-22、32… (可避免二次聚集)
伪随机探测法 原基础加个伪随机数 (可避免二次聚集)
链地址法 相同地址的记录放到同一个单链表中 (可避免二次聚集)

习题

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.用顺序表和单链表表示的有序表均可使用折半查找方法来提高查找速度。 (错)

答:单链表无法使用折半查找必须是顺序存储,因为要取中间值

3.二叉排序树序列判定

答:二叉排序树序列可理解为一个元素与二叉排序树比较的记录构成的序列。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) 进行折半查找,
①画出描述折半查找过程的判定树;
②若查找元素90,需依次与哪些元素比较?
③假定每个元素的查找概率相等,求查找成功时的平均查找长度。

答: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,分子就是每个元素查找的次数之和
而查找失败时可能计算得到的地址有11种,即分母为11,而关键字为空的查一次就知道失败了(要是有也不会为空),若不为空要往后找直到找到第一个空元素(说明确实没有这个元素不然该放到这个空着的位置了)
2️⃣ 链地址就是要是地址被占了放后面挂着就行

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
其他都是O(1)
4.关键字较少 ,选取简单的:

直接插入排序 (最简单,性能最佳)
冒泡排序
关键字较多,就用先进的:

关键字较乱,不要求稳定性:快速排序
关键字基本有序,就用堆排序或归并排序
不要求稳定性:堆排序
要求稳定性:归并排序
关键字多但都较小:基数排序

习题

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] 16 30 28 10 16* 20 6 18 第2个即12与前面的排序
[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] 最后一个与前面的排序 (查找插入位置是依次比)

②折半插入排序

排序过程同①,只不过查找插入位置用的是折半查询

③希尔排序 (增量选取5,3,1)

原序列为

{12,2, 16, 30, 28, 10, 16*, 20, 6, 18}

10 2 16 6 18 12 16* 20 30 28 (增量选取 5)第1个和第6个排序,第2和第7…

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 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 12 6 16 [16* 18 20 28 30]
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

⑥简单选择排序 (第n趟选择最小和第n个位置元素交换)

原序列为{12,2,16, 30, 28, 10, 16*, 20, 6, 18}
[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

⑦堆排序

原序列为{12,2, 16, 30, 28, 10, 16*, 20, 6, 18}
18 12 16 20 28 10 16* 2 6 [30] 得到最大值30,继续调整交换

6 20 16 18 12 10 16* 2 [28 30] 得到两个最大值,继续调整交换

… 由于此题没有答案,下面类似

建堆(按编号即层次遍历)然后调整堆(从最后面的非叶子结点向前选择最大的放到根,可能不止调整一趟)。

然后交换根和最后一个编号(注意不是最小),再重新调整交换重复操作

⑧二路归并排序 (每两个归并成一组有序序列,再每两组归并成一组有序序列)

原序列为{12,2, 16, 30, 28, 10, 16*, 20, 6, 18}
[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 ]

2.树形选择(锦标赛)排序

原序列为{49,38, 65, 97, 76, 13, 27, 49*}
对树8个选4个最小,4个选倆,2选1,选中13为最小输出,置最下面13为无穷大,重复操作

3.基数排序

原序列为{278,109,063,930,589,184,505,269,008,083}
准备10个桶,第一趟收集按个位放到对应桶中

即结果为:930 063 083 184 505 278 008 109 589 269 (个位已经有序)

第二趟收集按十位放到对应桶中

即结果为:505 008 109 930 063 269 278 083 184 589

我们可以看到最低2位已经有序了,只需再来一趟收集即可,就不写了

4.根据结果写排序方法

5.对 n 个不同的排序码进行冒泡排序, 在元素无序的情况下比较的次数最多为( )

答:比较次数最多时,第一趟比较 n-1 次,第二趟比较 n-2 次, 最后一趟比较 1
次,即 (n-1)+(n-2)+…+1= n(n-1)/2

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的元素排序所以不确定。

各类型存储结构

顺序表

#define MAXSIZE 100  //顺序表可能达到的最大长度
typedef struct
{ElemType *elem;    //存储空间的基地址(例如用L.elem[0]取元素)int length;  //当前长度
}SqList;

L.elem[i]取值 //L.length-1=>i>=0,如元素为1,2,3,L.length=3,i=0,1,2

单链表

typedef struct LNode
{ElemType data; //结点的数据域struct LNode *next; //结点的指针域,指向下一结点
}LNode,*LinkList;   //LinkList为指向结构体LNode的指针类型(相当于LNode *)

若带头结点,空表条件为L->next==NULL(L为头指针指向头结点永不为空)

若不带头结点,空表条件为L==NULL

双向链表

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;

栈空:S.top==S.base //首尾指针相同

栈满:S.top-S.base==S.stacksize //尾-首等于最大容量即为满

链栈

//定义类似,类似操作受限的单链表
typedef struct StackNode
{ElemType data; //数据域struct StackNode *next;    //指向下一结点
}StackNode,*LinkStack;

链栈一定是没有头结点,所以栈空的条件为:S==NULL

循环队列

#define MAXSIZE 100 //队列可能达到的最大长度
typedef struct
{QElemType *base;   //存储空间的基地址int front;    //头指针(只是有指针的作用,例如用Q.base[Q.front]取元素)int rear;    //尾指针
}SqQueue;

队空:Q.front==Q.rear //首尾指针相同

//尾指针指向的为最后一个元素的下一个地址(永远为空),所以+1

队满:(Q.rear+1)%MAXSIZE==Q.front

链队

//只看第一个定义和单链表类似,不同的是第二个设了队头和队尾指针
typedef struct QNode
{QElemType data;    //数据域struct QNode *next;    //指向下一结点
}QNode,*QueuePtr;
typedef struct
{QueuePtr front;    //队头指针(相等于QNode *front)QueuePtr rear;   //队尾指针
}LinkQueue;

队空:Q.front=Q.rear

由于串、数组、广义表的存储结构不是重点在这里就不再列出其存储结构

小结

栈和队列除了链栈都有头尾指针

顺序二叉树(不常用)

#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个因素、线性探测法解决冲突、哈希表的求解与建立)

哈希表的基本概念:

哈希表(hash table)又称三列表,其基本思路是,设要存储的元素个数为n,设置一个长度为m(m>=n)的连续内存单元,以每个元素的关键字ki(0<=i<=n-1)为自变量,通过一个称为哈希函数(hash function)的函数h(ki)吧ki映射为内存单元的地址(或下标)h(ki),并把该元素存储在这个内存单元中,h(ki)也成为哈希地址(hash address)。把如此构造的线性表存储结构称为哈希表。
三个因素:
①哈希函数。
②处理冲突的方法。
③哈希表的装填因子。

哈希函数的好坏首先影响出现冲突的频繁程度。
假定哈希函数是“均匀的”,即不同的哈希函数对同一组随机的关键字,产生冲突的可能性相同。
对同一组关键字,设定相同的哈希函数,则不同的处理冲突的方法得到的哈希表不同,它的平均查找长度也不同。
若处理冲突的方法相同,其平均查找长度依赖于哈希表的装填因子。

线性探测法

线性探测法是从发生冲突的地址(设为d)开始,依次探测d的下一个地址(当到达下标为m-1的哈希表表尾时,下一个探测的地址是表首地址0),直到找到一个空闲单元为止(当m≥n时一定能找到一个空闲单元)。线性探测法的数学递推描述公式为:
d0=h(k)
di=(di-1+1) mod m (1≤i≤m-1)
用线性探测解决冲突的例题:

除留余数法:

除留余数法是用关键字k除以某个不大于哈希表长度m的数p所得的余数作为希地址的方法。除留余数法的哈希函数h(k)为:
h(k)=k mod p (mod为求余运算,p≤m) ,p最好是质数(素数)。
采用除留余数法解决冲突例题:

采用除留余数法哈希函数建立如下关键字集合的哈希表:

{16,74,60,43,54,90,46,31,29,88,77}。

除留余数法的哈希函数为:
   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]如下表所示。

散列表查找

迪杰斯特拉算法

弗洛伊德算法

拓扑排序

关键路径算法

数据结构与算法期末复习总结相关推荐

  1. CAUC数据结构与算法期末复习归纳(二)

    CAUC数据结构与算法期末复习归纳(二) 二叉树 二叉树的周游 二叉树的抽象数据类型 深度优先周游二叉树或其子树 广度优先周游二叉树 二叉树的存储结构 二叉树的链式存储结构 二叉搜索树 二叉搜索树的性 ...

  2. 数据结构与算法期末复习——知识点+题库

    第一章绪论 1.1 数据结构的基本概念 (1)数据:所有能被计算机识别.存储和处理的符号的集合(包括数字.字符.声音.图像等信息 ). (2)数据元素:是数据的基本单位,具有完整确定的实际意义.在计算 ...

  3. 复制成绩表计算机专业的表结构,数据结构 数据结构与算法期末实验考试成绩表.doc...

    数据结构 数据结构与算法期末实验考试成绩表.doc (2页) 本资源提供全文预览,点击全文预览即可全文预览,如果喜欢文档就下载吧,查找使用更方便哦! 9.90 积分 数据结构与算法期末实验考试成绩表 ...

  4. 数据结构笔记(期末复习,持续更新)

    目录 一.绪论 1.数据结构基本概念 2.算法定义与特征 二.线性表 1.线性表的定义 2.顺序表的存储结构 3.链式存储结构 三.栈和队列 1.栈的基本概念 2.队列的基本概念 3.循环队列 四.字 ...

  5. 【数据结构】【期末复习】知识点总结

    --算法.线性表-- 概念明晰:随机存取.顺序存取.随机存储和顺序存储 随机存取.顺序存取.随机存储和顺序存储这四个概念是完全不一样的,切不可将之混淆 很多人包括我可能认为随机存取就是随机存储,顺序存 ...

  6. 智能优化算法期末复习

    目录 一.GA遗传算法 二.ACO蚁群算法 三.PSO粒子群算法 四.SA模拟退火算法 五.ABC人工蜂群算法 六.DE差分进化算法 七.TA阈值接收算法 八.综合 一.GA遗传算法 1.运算流程 2 ...

  7. 暨南大学算法期末复习

    可能有排版问题,可以到我的博客看看这篇文章 https://blog.ifycyu.ml/posts/8c2f6b20.html 前言 本文只针对客观题!!! 选择.填空.判断题自己看书!!!一定要看 ...

  8. 大二算法期末复习-排序-英文姓名排序

    英文姓名排序 Time Limit: 1000/1000MS (C++/Others) Memory Limit: 65536/65536KB (C++/Others) Problem Descrip ...

  9. 数据结构期末复习资料:重点总结+题库(含答案详解),助你一天复习数据结构,高分通过数据结构期末考试!不挂科!

    10小时不太可能了,但是100个小时完全OK的! 送你份独家突击复习资料,以下正文: 据说在期末考试前夕,同学们的学习能力会变强!但是数据结构,毕竟作为一门特别难的科目,如果平时没有学好,那么很可能就 ...

最新文章

  1. python百分号和斜杠_Python中正反斜杠(‘/’和‘\’)的意义与用法
  2. scala把序列分解成子集(group by,partition)
  3. WEB安全基础-SQL注入基础
  4. 开机即启动Activity
  5. ​java/ mysql企业动态网站设计制作作业成品
  6. 介绍几款免费APP在线制作社开发生成工具
  7. 洛谷试炼场 普及组 动态规划的背包问题
  8. 为树莓派制作系统镜像时进行瘦身,方便后续保存与批量写入
  9. 经过路由无法找到计算机,共享打印机找不到对方电脑解决方法
  10. buuctf XCTF October 2019 Twice SQL Injection 二次注入原理+题解
  11. 7. django应用及分布式路由
  12. mysql导出一个数据库的结构及遇到的问题
  13. Moir´e Photo Restoration Using Multiresolution Convolutional Neural Networks 摩尔纹领域论文阅读复现
  14. python的pyside2安装
  15. 传统图像去噪方法(一)
  16. Java Web 胡言乱语 之三
  17. svn中项目管理中ec_SVN的项目管理
  18. 【2021 CSDN年度报告】看看你今年有收获没?
  19. 进击的Android注入术
  20. MATLAB基础知识——范数求解函数norm

热门文章

  1. 第一课:路由器宽带拨号入网(小米路由器)
  2. 低功耗蓝牙BLE之连接事件、连接参数和更新方法(程序解读)
  3. Python人生重开模拟器(高级版)
  4. 微信sdk上传录音php,PHP端微信JS-SDK录音上传并转mp3和播放
  5. 「とても」「あまり」「大変」的用法区别
  6. 联想一体微型计算机N308冲上电什么原因,买给长辈的电脑 联想N308 AIO全面体验...
  7. 本周大新闻|沙特PIF再投Magic Leap,周融资超5.1亿美元
  8. 俄罗斯方块(一):简版
  9. [附源码]java毕业设计房屋租赁系统
  10. Java+集合系列3、骨骼惊奇之LinkedList