[关闭]
@Bruce1Tone 2020-12-21T13:56:59.000000Z 字数 15334 阅读 834

数据结构服用手册

计算机


绪论

数据结构

基本定义

数据结构包括:

  1. 逻辑结构:抽象思考上的
  2. 存储结构:实际存储
  3. 数据的运算

其中,存储结构逻辑结构在计算机的映射,所以存储结构不能独立于逻辑结构存在。

抽象数据类型ADT:三元组:{数据对象,数据关系,基本操作集}

逻辑结构

常见的有四种:

集合
线性

算法

问题规模为n

一般规律:

语言越高级,执行同样算法的效率越低

时间复杂度

常见时间复杂度排序:

表示为


其中,相同问题规模,上式越小的一定越优

空间复杂度

一般空间复杂度与算法无关,因此只算除了输入程序之外所需要的额外空间

算法原地工作:所需辅助空间为常量,即

线性表

基本知识

定义

线性表定义:

  1. 元素数据类型相同(存储空间大小一样)
  2. 有限序列(有先后)

操作函数

部分:

操作函数 操作 解释
LocateElem(L , e) 按值查找 在表L中查找e,并返回索引
GetElem(L , i) 按位查找 在表L中查找索引为i的元素,并返回其值
ListInsert(&L , i , e) 插入 位置i处插入值为e的元素
ListDelete(&L , i , &e) 删除 删除索引为i的元素,并将他放在e中
Empty(L) 判空 如果为空表,返回True

顺序表

定义

顺序表定义:

连续地址的存储单元依次存储线性表中数据

特点:逻辑顺序与物理顺序相同,逻辑相邻=物理相邻

存取方式:随机存取(给了坐标就可以随意访问)

补充:
 顺序存取:访问第N时,必须要访问前N-1个元素
 随机存取:可直接按坐标访问

其中,顺序表在物理层常用数组来存储

操作

操作 描述 算法 平均移动次数 时间复杂度
插入 在位置 i 插入 i 之后的元素全部后移一位
删除 在位置 i 删除 i 之后的元素全部前移一位
按值查找 查找第一个值为e的元素 顺序遍历对比

链表

基本信息

常见的有:单链表、双链表、循环链表、静态链表

头节点

链表的第1个结点,其中存储的信息一般为空,或者存储长度等信息。

头指针和尾指针

头指针:指向链表的第一个结点(有表头的就指向表头)
尾指针:指向链表的最后一个结点

image_1enq5qjiu1biu4891tumj8h1rt9.png-84.7kB

单链表

单链表的基本结构

1.本结点数据data
2.后继节点指针*next

最后一个指针指向NULL

两种建立方法:头插法、尾插法

头插法:最简单,但是是逆序
尾插法:需要尾指针
image_1enq66mtkuev18epm5epd1f0om.png-153.1kB

删除操作:
image_1enq6i6u41cbg3j712oi1u0v110213.png-227kB

双链表

双链表结构:

  1. 前驱结点指针:*prior
  2. 该节点数据:data
  3. 后继节点指针:*next

循环链表

循环链表定义:

与普通的单链表、双链表相似,只不过首尾相接

静态链表

静态链表:

借助数组来定义:
用一块连续的内存存储,进行相对编号
指针存储的信息是相对位置int类型,即数组下标

需要提前分配内存

本章小结

各种链表的对比
image_1enq84ee4lo419cb10gn18urrf41g.png-213.5kB

image_1enq8qggfnj82ih1bm711in1uf41t.png-362.7kB

栈和队列

栈的两种存储结构

定义:

栈是限制了存取方式的线性表,其逻辑结构本质还是线性表

  1. #例如以下的顺序栈(数组实现):
  2. struct Stack{
  3. int data[size]; #存放栈的内容
  4. int top; #存放栈顶指针(下标)
  5. }

如果是链栈,则为*top指针

顺序栈

顺序存储的栈
一般是数组实现

共享栈

即两个栈共用一个数组,一个以数组头为栈底,一个以数组尾为栈底
top1+1=top2时,即两个栈顶相邻,即判断为栈满

链栈

链式存储的栈
一般是单链表实现

注意!表头才是栈顶,表尾是栈底!

操作

  1. bool StackEmpty(&s) #判断栈是否为空,top==-1时,栈空
  2. bool Push(&s,x) #元素x进栈s中,成功入栈返回True
  3. bool Pop(&s,&x) #弹出s的栈顶元素,并放在x中,成功弹出返回True
  4. bool GetTop(&s,&x) #获取栈顶元素,并放在x中

注意,push时可能会出现栈满,应该先检查是否满并操作。
同理,pop时可能会空栈报错,应该先检查是否为空
初始化时,top=-1

队列

队列定义

Queue简称,只能在队头出队,队尾入队
遵循规则FIFO,即First In First Out,先进先出
image_1enql50uv19dpdim2kc1noqgk02a.png-64.2kB

结构定义

  1. struct Queue{
  2. ElemType data[size]; #队列元素
  3. int front,rear; #front队头,rear队尾
  4. }

初始状态为front=rear=0
入队:rear+1
出队:front+1

操作

  1. QueueEmpty(Q) #判断队Q是否为空,如果是返回True
  2. EnQueue(&Q,x) #入队,将x元素入队到Q的对尾
  3. DeQueue(&Q,&x) #离队/出队,将Q队头元素弹出并存放在x中
  4. GetHead(Q,&x) #读取队头元素,并将之赋给x

顺序队列

用数组表示

循环队列

数组表示,使用%来重复利用一个数组空间

注意:
 对于循环队列,有三种区分队满队空的方式:

  1. 最普遍:rear指向下一个空位
    a. 队空:front=rear
    b. 队满:(rear+1)%n=front
    c. 队里有m个元素:rear=front+m,其中rear指向的是一个空的位置
    d. 队的容量:n-1
    image_1ensqgs75i0t2fi11eidb61ul43u.png-369.3kB
  2. 增加变量size存储队列元素个数:
    a. 队空:size=0
    b. 队满:size=n
    c. 队容量:n
  3. 增加变量tag来区分:
    a. tag=0时,如果因为删除触发front=rear,则队空
    b. tag=1时,如果因为插入出发front=rear,则队满
    c. 队容量:n

链队列

好处:

不存在队列溢出,动态分配内存

基本组成:

有队头指针front和队尾指针rear

  1. 对于无头结点的链队:
    a. 初始化状态:front指向NULLrear指向NULL
    b. 判空:front==rear==NULL
  2. 对于有头结点的链队:
    a. 初始化状态:front指向头结点rear也指向头结点
    b. 判空:front==rear

image_1enspttql7411srd1d8qtki44f3h.png-304.6kB

压缩矩阵存储

存储方式:

  1. 按行优先
  2. 按列优先

数组存储的压缩矩阵:

  1. 对称矩阵A[n*n]:
    a. 需要空间:上三角的个数,即1+2+...+n
    image_1enstuijoe1ccbhd021sjl10814b.png-215.5kB
  2. 三角矩阵A[n*n]:
    a. 上三角需要空间:上三角+常数位置1,即1+2+...+n+1
    b. 下三角:与上三角相同
    image_1enstvjr61b1o1l6qg8d1f151ub4o.png-231.4kB
  3. 三对角矩阵A[n*n]:
    a. 需要空间:3n-2
    image_1ensu0g4p1joe3691bal1f091dvq55.png-180.6kB

稀疏矩阵:

有两种存储方法:

  1. 三元组:(row,volumn,value)
  2. 十字链表法
    image_1ensupp8b1ke9116a1ingfgm1qa95i.png-205.4kB

栈和队列的应用总结

  1. 栈的应用:
    a. 递归调用需要栈
    b. 函数调用,函数的局部变量使用栈存储
    c. 括号匹配
    d. 中缀表达式转后缀表达式
    e. 迷宫求解
    f. 进制转换
  2. 队列的应用:
    a. 层次遍历/广度优先搜索
    b. 页面替换算法
    c. 缓冲区

串的定义

串:

string:是由0个或多个字符组成的有限序列
一般记为:s = 'xxx'
 n=0时,s为空串

空格串:

由1个或者多个空格组成的串,不等于空串

串的三种存储

定长顺序存储

地址连续,每个串变量的长度固定

  1. struct String{
  2. char ch[size]; #size为最大长度,超过部分直接被截断
  3. int length; #length为实际长度,小于等于size
  4. }

堆分配存储

地址连续,动态分配内存,使用malloc()free()动态分配

  1. struct String{
  2. char *ch; #指向串的基地址的指针*ch
  3. int length;
  4. }

块链存储

由链表存储,每个结点定长
当一个结点占不满时,用#占位
image_1enumhv4kru71k445f611hims15v.png-497.6kB

串的基本操作

代码 操作 说明
StrAssign(&T , chars) 赋值 chars赋值给T
StrCopy(&T , S) 复制 S复制给T
StrCompare(S ,T) 比较 如果S>T,则返回值>0;
如果S=T,则返回值=0;
如果S<T,则返回值<0
SubString(&Sub , S , pos , len) 求子串 求串S的从pos位置开始,长度为len的子串
并存放在Sub
Concat(&T , S1 , S2) 合并串 T=S1+S2
Index(S , T) 定位子串 S中找有无等于T的子串?
如果有,则返回第一次出现的位置
如果没有,则返回0
ClearString(&S) 清空 清空S空串
DestroyString(&S) 销毁 销毁S,并释放内存

串的模式匹配

模式匹配:

即子串的定位操作
 其中子串又叫模式串

简单匹配

对于长度为n主串,和长度为m模式串

暴力求解,一一匹配

  1. 每失配一次
    a. 主串指针:退回到该趟匹配起点 + 1
    b. 模式串指针:退回到首位
  2. 时间复杂度:
    O(nm)

KMP算法

对于长度为n主串,和长度为m模式串

  1. 每失配一次,即模式串第j个字符失配
    a. 主串指针:不移动
    b. 模式串指针:回退到next[j]位置继续匹配
  2. 时间复杂度:
    O(m+n)

注意!

  1. next需要整体+1:当模式串序号从1开始时,需要整体+1
  2. next不需要整体+1:当模式串序号从0开始时,不需要整体+1

重点在于next值的求法:

以下方法都是计算的next数组+1后的值
image_1enupvi2g17ju1sqn1ira19jtdgq80.png-144.6kB

next的手算法(考试用)

image_1enunop50ektmchu6rbpcrh86c.png-493.7kB

next的机算法(代码实现)

image_1enunqkfb3ip19693lutnpvrq6p.png-361.6kB

nextval的计算法(改进后的KMP)

image_1enup4o0p1ijh1os61pdi1os1138d7j.png-345.3kB

树与二叉树

两种二叉树

完全二叉树:只缺少最右下角的几个元素
满二叉树:每层全满

二叉树的存储

顺序存储:适合完全二叉树和满二叉树
链式存储:适合所有

二叉树的性质

非常重要:

结点的度:该节点孩子个数
度为0的结点个数与度为2的结点个数关系:

二叉树的遍历

三种遍历之后,叶子结点的先后顺序都是一样的
知道两种遍历序列,反求二叉树:中序序列是必需的
 如果只知道先序和后序,则无法确定孩子在左还是右,得到的二叉树不唯一

先序遍历

先序遍历:


递归算法的时间复杂度为,递归工作栈的栈深度=树的高度h

  1. #先序遍历递归法遍历:
  2. PreOrder(T):
  3. if T != NULL:
  4. visit(T)
  5. PreOrder(T->left)
  6. PreOrder(T->right)
  7. /
  1. PreOrder(T):
  2. InitStack(S)
  3. p = T
  4. while(p != NULL || !IsEmpsty(S)):
  5. if(p != NULL):
  6. visit(p)
  7. Push(S,p)
  8. p = p->left
  9. else:
  10. Pop(S,p)
  11. p = p->right

中序遍历

中序遍历:

  1. #中序遍历 非递归方法:(栈)
  2. InOrder(T):
  3. InitStack(S)
  4. p = T #p是遍历指针
  5. while(p!=NULL || !IsEmpty(S)):
  6. if (p != NULL):
  7. Push(S,p)
  8. p = p->left
  9. else:
  10. Pop(S,p)
  11. visit(p)
  12. p = p->right

中序遍历
非递归方法描述
重复执行以下直到栈空or遍历结束

  1. if p有左孩子:
    左孩子入栈,并一路向左
  2. 否则:
    栈顶出栈,并访问栈顶元素
    然后从栈顶元素向右走

后序遍历

后序遍历:


非递归实现比较繁琐

线索二叉树

X序线索二叉树构建规则:
image_1eohq5u4r8ke1h511kos1mke1ton9.png-369.3kB

三种线索二叉树的作用:

先序线索二叉树:查后继结点快
后续线索二叉树:查前驱方便
中序线索二叉树:查前查后都方便

树、森林

双亲表示法

顺序存储,每个节点存双亲的位置

孩子表示法

链式存储,每个节点后接上自己的孩子的位置

孩子兄弟表示法

最常用,树转二叉树的方法

二叉链表存储:
1. 孩子在左子树
2. 兄弟在右子树

森林转二叉树

  1. 第一棵树作为root
  2. 各个树的根结点是兄弟结点
  3. 森林变成T1为root的树,然后二叉树化:左孩子右兄弟

二叉排序树(二叉查找树)BST

基本规则


其左右子树也分别是两棵二叉排序树
进行中序遍历可以得到递增序列

插入

  1. if TreeEmpty:
    作为根节点
  2. if 大于p:
    向右
  3. if 小于p:
    向左

直到插入的数是叶子节点

删除

删除操作的文字描述:

delete z:

  1. if z是叶子结点:
    直接删除
  2. if z只有一个子树:
    子树直接替代z,即原来指向z的直接指向z->child
  3. if z有左右子树:
    a. 找到z的直接后继z->next,然后用后继替代z
    b. delete z->next

伪代码:

  1. delete(z):
  2. if z->left == NULL && z->right == NULL:
  3. free(z)
  4. elif z->left == NULL:
  5. z->pre->next = z->left
  6. free(z)
  7. elif z->right == NULL:
  8. z->pre->next = z->right
  9. free(z)
  10. elif z->left != NULL && z->right != NULL:
  11. z->data = z->next->data
  12. delete(z->next)

平衡二叉树

定义:

平衡二叉树:左右子树深度相差小于等于1
公式

设构建h层平衡二叉树需要的最小结点数为,则有递推式:

平衡二叉树的插入

左旋:

变成爹,我的左孩子变成你的右孩子

右旋:

变成爹,我的右孩子变成你的左孩子

LL

插入路径为A的-

image_1eohu46rmaur1sddu6j9cl1a4m.png-75.2kB

RR

插入路径为A的-

image_1eohu58v96do18cdhgm152k1hqv13.png-78.2kB

LR和RL

image_1eohu5r861n95s9l19gauev11bv1g.png-234kB

哈夫曼树

即带权路径长度WPL最小的树

不存在为1的结点

构建过程

对于n个带权结点

  1. 构建n棵树的森林F
  2. 在F中选择根节点权值最小的两棵树:
    a. 新建结点
    b. 把刚刚两棵树接到该结点上,并且该结点权值等于两孩子之和
  3. 重复2,直到F变成一棵树,得到哈夫曼树

哈夫曼编码

要求:
都得是前缀编码

前缀编码:即没有一个编码是另一个编码的前缀

注意:

哈夫曼编码一定是前缀编码
但前缀编码不一定是哈夫曼编码(哈夫曼是效率最高的前缀编码)

构建过程:

  1. 对于需要编码的n个字母,分别统计其出现的频数,作为结点权值
  2. 构建哈夫曼树
  3. 从root开始,查找到结点的路径:左记0,右记1
    例如:

b的路径是LLRLL,则编码为00100
c的路径是LR,则编码为01

图的基本概念

图G:

顶点集V:不能为空,至少一个
边集E:可以为空

有向图和无向图:

有向图:<a,b>
无向图:(a,b)

简单图:

满足以下:
1. 无重复边
2. 无自己到自己的边

多重图:

满足其一或同时满足:
1. 有重复边
2. 有自己到自己的边

完全图(简单完全图)
定义:

每两个顶点间都有通路的简单图
对于无向图:两点间一条边
对于有向图:两点间一来一回

生成子图

包含原图所有顶点的子图

连通图和连通分量

对于无向图:
1. 连通图:所有顶点都连通的图
2. 连通分量:最大连通子图(不仅包含所有点,还要包含所有边)

极大、极小连通子图

极大连通子图:保证连通,且包含所有的边
极小连通子图:保证连通的情况下边最少

强连通图

对于有向图:
1. 强连通图:所有顶点u和v都有路径<u,v><v,u>(但不一定两个点之间有弧)
2. 强连通分量:最大强连通子图(只有单边的顶点不包含在内)

稀疏图

边少的图
反之就是稠密图

生成树、生成森林

生成树:

包含连通图全部顶点的极小连通子图(即两点间有一条边就够了)

生成森林:

由多个连通分量分别的生成树组成的森林

顶点的度

无向图:

度:顶点上连的几条边

有向图:

入度:指进来的条数
出度:指出去的条数

简单路径,简单回路

简单路径:

顶点不重复的路径(除了首尾重复一次)

简单回路:

顶点不重复的回路(除了首尾重复一次)

图的存储

邻接矩阵

稠密图(边多)适合
无向图的邻接矩阵是对称矩阵,可以压缩存储。只存储三角矩阵
带权的网,无通路时存为

补充:

若图G的邻接矩阵为A,则的元素表示:

从顶点ij,长度为m的路径有

邻接表

稀疏图(边少)适合
组成:
将该顶点的所有邻接点接在该结点的后面
1. 顶点表:顶点值data+边表头指针firstarc
2. 边表:邻接点域adjvex+指针域nextarc

对于无向图:顶点后的是所有邻边
对于有向图:顶点后的只有出去的边

十字链表

有向图
image_1eolseuuq17ktd761ijm15tffn59.png-312.9kB

邻接多重表

无向图
image_1eolsgdlpqmh131217vu180usqlm.png-260.1kB

图的遍历

广度优先BFS

相当于层次遍历
时间复杂度:

邻接矩阵:
邻接表:,空间复杂度

非递归算法,需要借助队列

伪代码:

  1. InitQueue(Q)
  2. for i in range(len(G.v)):
  3. visited[i] = False #初始化所有点为未访问
  4. //先访问第一个点
  5. visited[root] = True
  6. for i in G.v:
  7. if visited[i] == False:
  8. BFS(G,i)
  9. def BFS(G,root):
  10. visit(root)
  11. visited[root] = True
  12. EnQueue(Q,root)
  13. while(!IsEmpty(Q)):
  14. Dequeue(Q,root)
  15. for i in root.neighbor:
  16. if visited[i] == False:
  17. visit(i)
  18. visited[i] = True
  19. EnQueue(Q,i)

文字描述:

  1. 从root出发,访问root,root入队Q
  2. 如果Q不为空,则进行以下循环:
    a. 出队,弹出元素到v
    b. 检查v的所有邻接点,没有访问过的邻接点全部入队

以上过程可以完成对一个连通分量的BFS,重复多次,对图G的所有连通分量BFS

BFS应用

当边不带权值时,求单源最短路径:

在BFS过程中,深度每增加一层,该层的distance就+1

深度优先DFS

相当于先序遍历
时间复杂度:

邻接矩阵:
邻接表:,空间复杂度

递归算法,需要借助递归工作栈

伪代码:

  1. for i in G.V:
  2. visited[i] = False
  3. for i in G.v:
  4. if visited[i] == False:
  5. DFS(Q,i)
  6. def DFS(Q,root):
  7. visit(root)
  8. visites[root] = True
  9. for i in root.neighbor:
  10. if visited[i] == False:
  11. DFS(G,i)

文字描述:

  1. 从root出发,先访问root
  2. 检查root的每一个邻接结点:
    a. if该点没访问过,则从该点开始DFS

即:只要有路就向前,没路了再退回上一个分叉路

总结

邻接矩阵:BFS和DFS的结果是唯一的
邻接表:结果不唯一

最小生成树

Prim算法

Prim算法描述:
1. 选定起点root,此时访问集T={root}
2. 在T和非T之间,找一条最短的边,并将该边和顶点加入到T
3. 重复以上过程n-1边,构建n-1条边,得到最小生成树

Kruskal算法

Kruskal算法描述:
1. 找到图中最小的边,加入T
2. 重复以上过程n次,得到最小生成树

两种算法对比

算法 时间复杂度 适用范围 备注
Prim普里姆 边很多,顶点少
Kruskal克鲁斯卡尔 边稀疏,顶点多 存储边集

最短路径

Dijkstra算法

求单源到各个点的最短路径

注意:
边上带有负权值时,不能使用Dijkstra算法。
时间复杂度是

文字描述:

  1. dist[i]存储从v0vi的最短路径,并初始化
  2. 集合S初始选定一个起点root,即S={root}
  3. 在非S中找到一条到v0最小的边,并把其顶点v1包括进S
  4. 更新dist[i],其中
  5. 重复n-1次,使所有顶点都包含在S内,结束。其中path[i]存放了从v0到vi的最短路径的前驱

S内都是最短路径找好了的顶点

Floyd算法

求任意两点的最短路径

注意:
时间复杂度是
可以有负权,但是不能有带负权的回路
无向图有向图都适用

文字描述:

  1. 初始邻接矩阵记为
  2. 第1次更新:
    a. 两点间允许通过v1中转的话,更新
    b. 更新规则是:

    c. 运行次,更新完后,矩阵变为
  3. ...
  4. 第m次更新,更新规则为:

    更新完的矩阵变成
  5. 更新n次,得到矩阵
  6. 就存储了任意两点之间的最短路径长度

拓扑排序

DAG图

DAG:有向无环图,可以把二叉树的相同子项公用,就变成了DAG图

拓扑排序

定义:
一个有向无环图满足以下条件,称为该图的一个拓扑排序:
1. 每个顶点只出现一次
2. A在B前,则不存在B到A的路径(但也不一定存在A到B的路径)

AOV网

V顶点表示活动的DAG图,记为AOV网
边:仅表示先后顺序

每个AOV网,都有一个或多个拓扑排序序列

拓扑排序算法:

  1. 选择AOV网中,无前驱的结点v1,输出v1
  2. 在AOV网中删除v1,并删除他出发的所有边
  3. 重复1、2,直到:
    a. AOV网为空(正常情况)
    b. 不存在无前驱的结点了(说明肯定有环)

对于一般的图来说,如果邻接矩阵三角矩阵,则存在拓扑排序。反之不一定

逆拓扑排序:

在AOV网的拓扑排序过程中,选择无后继的点

AOE网 与 关键路径

AOE网:

边:表示活动,即过程耗时
顶点:表示事件,即表示 之前的活动都完成了,后面的活动可以开始了

特点:每个事件需要等他的所有前驱活动都完成了才能结束,并开始后面的活动

事件的最早发生时间Ve

从源点v1到事件vk:
Ve[k]表示了:从v1到vk所有路径里最长的一条的距离即Ve[k]最早开工时间

Ve[k]代表了,保证vk的前驱事件全部都做完了的时间
再早的话前面的活动就没做完了

计算方法:

  1. 初始化所有时间的Ve[i]=0
  2. 去掉入度为0的点(源点,即起始点):
    a. 从源点v1开始,输出v1
    b. 计算v1的所有后继Ve值,计算方法是:Ve[k]=max{Ve[k],Ve[1]+<1,k>}
  3. 重复第2步,直到所有的顶点都输出

事件的最迟发生时间Vl

Vl[k]表示了:在不推迟整个工期的情况下,vk最晚开工时间

即,Vl[k]为事件vk的各个后续事件,倒推回来必须开工的时间里最早的一个
再晚的话,后面的活动就做不完了

计算方法:

  1. 初始化所有事件的Vl[k]=Ve[k]
  2. 用栈S记录拓扑排序,栈顶为排序的最后一个元素
    a. 栈顶元素vm出栈并输出
    b. 计算vm的所有前驱事件的Vl值,计算方法是:Vl[k]=min{Vl[k],Vl[m]-<k,m>}
  3. 重复第2步直到所有元素都出栈了

活动的最早发生时间Ee

Ee[k]表示了:活动k=<k.begin,k.end>最早能开始的时间,也是k的头事件k.begin最早发生的时间Ve[k.begin]

再早的话,k.begin事件就还没发生,没有条件进行k活动

计算方法:

  1. Ee[k]=Ve[k.begin]

活动的最晚发生时间El

El[k]表示了:活动k最晚多久必须开始,也是k的尾事件k.end最晚发生时间-k需要的时间

再晚的话,k的后继活动就会没法按时展开推迟工期了

计算方法:

  1. El[k]=Vl[k.end]-len(k)

关键路径与关键活动

关键路径:整个工程开始到结束耗时最长的一条路径
关键活动:关键路径上的活动

所有El[k]=Ee[k]的活动k都是关键活动
即该活动最早开始时间和最晚开始时间一样,也就是说,时间一到必须立马开工不能拖延
一旦关键活动发生拖延,整个工程就会延期
缩短工期:必须所有的关键路径都同时缩短,工期才能缩短

最短完成时间

最短完成时间=关键路径的长度=最大路径长度的一条路径
表示整个工程最早什么时候能完成

查找

查找表/查找结构

需要满足的操作:
1. 增
2. 删
3. 查
4. 改

动态与静态查找表

静态查找表:适合顺序查找折半查找散列查找
动态查找表:适合二叉排序树散列查找

顺序查找

无序线性表的顺序查找

平均查找长度:


不成功平均长度:

对于线性链表只能顺序查找

有序线性表的顺序查找

查找成功的ASL一样
查找失败的结果优化了一下,如果查找的元素已经大于关键值,则返回没找到

折半查找/二分查找

必须有序,且只能顺序存储
定义指针low high mid
其中:


即向下取整
判定过程中,若keymid,则:

他的判定过程是一个平衡二叉判定树
平均查找长度为

时间复杂度为
image_1epbm942q1p9u1k3k12791grm1b6c9.png-385.2kB

分块查找/索引顺序查找

内无序,间有序
记录了块内最大元素首元素地址
是顺序查找和折半查找的结合
设有b块,每块s个元素,则
块间折半查找,块内顺序查找,平均查找长度为:


其中,当时,平均查找长度最小:

B树(不考)

散列查找/哈希查找

基本概念

散列函数:即hash(),将关键词映射到地址的函数,Hash(key)=Addr
冲突/碰撞:n个不同的key对应同一个hash值,也就是同一个地址
散列表:是一种数据结构,可以根据关键字直接访问。他构成了关键字存储地址的直接映射

散列函数构造方法

直接定址

hash(key) = key
优点:不会冲突,计算简单
缺点:浪费空间,空位多
适用于:基本连续

除留余数法(最常用)

hash(key) = key%p
关键在于选好质数p

数字分析法

选取n位数中的m位,作为散列地址
要求数码在这m位中分布较均匀
适合:已知关键字的集合,如果更换了关键字,需要重新构造hash()

平方取中法

key的平方值的中间几位直接作为散列地址

处理冲突的方法

1. 开放定址法

开放定址法:即空闲地址对同义词非同义词都开放
数学递推公式是:

注意

开放定址法中,不能随便删除元素,需要先做删除标记,进行逻辑删除
再进行

  1. 线性探测法
    • d = 0,1,2,3,4...
    • 查找成功也至少有1次探测!
    • 缺点:容易产生堆积聚集
  2. 平方探测法
    • d =
    • 其中
    • 其中m必须是可以表示为4k+3的一个质数
    • 又叫二次探测法
    • 优点:较好处理堆积的问题
    • 缺点:访问不满,但是至少能探测一半
  3. 再散列法
    • d_i = hash_2(key)
    • 散列函数为
    • i为冲突次数
    • 最多经过m-1次探测,就会遍历表中的所有位置
  4. 伪随机序列法
    • d为伪随机数
2. 链接法

把所有的同义词存储在一个线性链表
适用于:经常插入删除的表

散列查找的性能

查找效率取决于:

  • 散列函数
  • 处理冲突的方法
  • 装填因子a

排序

注意:稳定与否并不能影响算法的优劣,他只是性质

插入排序

时间复杂度都是,都是内部排序
包括:

  1. 直接插入排序
    • 前面i个数的序列有序,新元素在前面的有序序列顺序查找
    • a[0]作为哨兵,不存储信息
    • 稳定
    • 可顺序存储,可链式存储
  2. 折半插入排序
    • 与直接插入法一致,只不过在有序表中采用折半查找位置
    • a[0]作为哨兵,不存储信息
    • 稳定
    • 只能顺序存储
  3. 希尔排序(缩小增量排序)
    • 方法:
      • 间隔为d的一组数据内部进行直接插入排序
      • 缩小间隔为d-1,重复步骤1
    • a[0]不是哨兵,仅作为暂存单元
    • 不稳定
    • 只能顺序存储

交换排序

包括:

  1. 冒泡排序
    • 依此比较相邻位,若为逆序则交换,相等不交换
    • 每次排序有1个元素到最终位置
    • 时间复杂度
      • 最多比较次数:
      • 最多移动次数:
  2. 快速排序
    • 参考资料
    • :所有未归位元素处理一遍
    • 现在剩下n个无序子序列,则下一就要有n个元素归位(绝对位置)
    • 空间效率
      • 最好:,也就是递归栈的最小深度
      • 最坏:,递归栈最大深度
      • 平均
    • 时间效率
      • 最好/平均
      • 最坏:
    • 不稳定
    • 是所有内部排序算法中平均性能最优的算法
    • 适用情况:
      • 最好:枢轴元素每一次都将子序列划分成等长两个序列
      • 最坏:基本有序

选择排序

包括:

  1. 简单选择排序
    • 类似于冒泡排序,每一趟选择i后面的最小值,与i交换位置。
    • 与初始序列无关!!
    • i之前都为有序,之后都为无序
    • 时间效率
      • 比较次数:
      • 移动次数:
    • 不稳定
  2. 堆排序
    • 参考资料
    • 概念定义:一棵顺序存储的完全二叉树,并且爸爸永远比儿子大/小
      • 大根堆:爸爸永远比儿子大
      • 小根堆:爸爸永远比儿子小
    • 算法过程
      1. 建立初始堆
        将原始数据填入完全二叉树,并从顺序的末尾元素开始检查
        有不符合的子树,就交换最大/小孩子的值,并依此检查所有元素
      2. 输出根节点,作为排序的第一个元素。
        然后将最后一个元素移到根节点位置,再次检查调整堆
      3. 重复以上步骤,直到堆空
    • 空间效率
    • 时间效率
      • 总:
      • 建堆:
      • 调整堆:共调整次,每次时间复杂度为,即
    • 不稳定

归并排序 和 基数排序

  1. 2路归并排序
    • 算法详解:每一趟将相邻2个子序列合并,直到只剩一个序列
    • Merge(A1,A2)合并 = A
      • 合并A1A2时,先将A1A2存在辅助空间B
      • 然后分别取A1A2的第一个元素进行对比,小的取出放进A
    • 空间效率:n个单元,
    • 时间效率
      • 总:
      • 每趟归并,一共进行趟归并
    • 稳定
  2. 基数排序
    • 一般有两种,最高为优先MSD最低位优先LSD,其中默认为最低位优先
    • 算法详解:
      • 数码是k进制的,则按照数码划分成k个子序列
      • 分配:先按最低位,将元素放到对应的子序列
      • 整理:依此输出每个子序列上的元素,同序列要分先来后到
      • 直到进行到最高位,最后一次输出得到升序序列
    • 空间效率
    • 时间效率
      • 总:,其中d为数码的位数,k为数码的进制
      • d位数,就要进行d分配+收集
        其中,分配收集
    • 稳定的,与初始序列无关

内部排序的比较

算法种类 平均时间复杂度 空间复杂度 稳定吗 适用于 最好时间复杂度 最差时间复杂度
直接插入 1 稳定 顺序、链式存储
冒泡 1 稳定 顺序、链式存储
简单选择 1 不稳定 顺序、链式存储
希尔 1 不稳定 仅顺序
快速排序 递归栈: 不稳定 顺序
堆排序 1 不稳定 顺序
2路归并 n 稳定 顺序
基数排序 k 稳定 顺序

C/C++相关

三种传递(值传递、指针、引用)

值传递:函数内都是副本,不影响本体
指针: 指向实际内存,对实际值操作
引用: 在函数内同一地址的别名,例如b = &a,则对b操作就是对a操作

&和*详解

&表示传地址引用
*表示指针变量,例如

取地址的用法:

  1. int *b = &a;
  2. # 以下语块与上句等价
  3. int *b; # int*类型的变量b,即b是个指针,指向一个int值
  4. b = &a; # 指针b存放a的地址

引用的用法

  1. a = 100;
  2. int &b = a; # int&类型的变量b,作为a的引用。对b操作就等于对a操作
  3. b += 1;
  4. cout<<a; #此处输出a的值为101

其他

中缀转后缀表达式

基本思路:

  1. 遇到操作数,直接输出,如:a,b,c
  2. 遇到运算符,将当前运算符的栈外优先级栈顶元素栈内优先级作比较,有下列两种情况:
    a. 栈外优先级高,则入栈,等待后续计算
    b. 栈内优先级高,则栈顶元素出栈输出,直到栈顶元素优先级<当前读取的栈外元素优先级,然后栈外元素入栈
  3. 遇到(,直接入栈。为了实现这一点,即(的栈外优先级最高,栈内优先级最低
  4. 遇到),栈顶元素依次出栈输出,直到遇到(,停止
  5. 遇到#,表示表达式结束

image_1ensm2bcr1uvt1j9j1ada1ofdlng2n.png-436.5kB

其中,ispIn Stack Priority,即栈内优先级
icpIn Coming Priority,即输入序列优先级/栈外优先级
优先级表如下所示:
image_1ensm7b8o1slj1d3i1tu0r4i1ln534.png-105.3kB
规律如下:

  1. 同等级运算符,isp=icp+1
  2. (在栈外最高,在栈内最低;)刚好相反
  3. 保证临时栈内从底到顶是单调递增
  1. #测试样例
  2. infix_expression = 'a+b-a*((c+d)/e-f)+g' #中缀表达式
  3. suffix_expression = 'ab+acd+e/f-*-g+' #后缀表达式

层次遍历(队列的应用)

算法描述:

  1. 根节点入队
  2. 如果
    a. 队列为空,则结束遍历
    b. 队列不为空,则第一个元素出队并输出
  3. 访问出队的元素,如果:
    a. 有左孩子,左孩子入队
    b. 有右孩子,右孩子入队
    c. 返回2

伪代码:

  1. EnQueue(root); #根节点入队
  2. while(!QueueEmpty): #只要队不是空的
  3. DeQueue(front); #第一个元素出队并输出
  4. if front->left != NULL:
  5. EnQueue(front->left); #如果有左孩子,则左孩子入队
  6. elif front->right != NULL;
  7. EnQueue(front->right); #如果有右孩子,则右孩子入队
  8. return; #当队列为空时,遍历结束

试题集

1.(p7.5)时间复杂度?

  1. int fact(int n){
  2. if(n<=1) return 1;
  3. return n*fact(n-1);
  4. }

2.(p9.2)时间复杂度?

  1. for(i=1;i<=n;i++){
  2. for(j=1;j<=i;j++){
  3. for(k=1;k<=j;k++)
  4. x++;
  5. }
  6. }

3.(p37.6)在一个单链表中,已知q所指结点是p所指结点的前驱结点,若在q和p之间插入结点s,则执行?

  1. A. s->next = p->next; p->next = s;
  2. B. p->next = s->next; s->next = p;
  3. C. q->next = s ; s->next = p;
  4. D. p->next = s; s->next = q;

4.(p38.12)说法正确的是?

A. 对一个设有头指针和尾指针的单链表执行删除最后一个元素的操作与链表长度无关
B. 线性表中每个元素都有一个直接前驱和直接后继
C. 为了方便插入和删除数据,可以使用双链表存放数据
D. 取线性表第i个元素的事件与i的大小有关

5.(p40.25)某线性表用带头节点的循环单链表存储,头指针为head,当head->next->next=head成立时,线性表长度可能是?

A. 0
B. 1
C. 2
D. 0或1

6.(p68.8)向一个栈顶指针为top的链栈中插入一个x结点,则执行?

  1. A. top->next = x;
  2. B. x->next = top->next; top->next = x;
  3. C. x->next = top; top = x;
  4. D. x->next = top; top = top->next;

7.(p68.7)设链表无头结点,操作在表头,则不适合作为链栈的是?

A. 有头指针,无尾指针,双向循环
B. 有尾指针,无头指针,双向循环
C. 有头指针,无尾指针,单向循环
D,有尾指针,无头指针,单项循环

8.(p70.22)一个栈的入栈序列1,2,3,...,n,出栈序列P1,P2,...,Pn,若P2=3,则P3可能的取值个数?

A. n-3
B. n-2
C. n-1
D. 无法确定

9.(p84.8)循环队列存储在A[0...n-1]中,队列非空时,front和rear分别指向队头元素和对位元素。若初始队列为空,且要求第一个进入队列的元素存储在A[0]处,则初始front和rear值是?

10.(95.11)中缀表达式为a+b-a*((c+d)/e-f)+g,在转换成后缀表达式ab+acd+e/f-*-g+的过程中,临时存放操作符的栈最大的同时存放数是多少?

11.(176.12)若森林F有15条边,25个结点,则F包含树的个数是?

12.(133.20)已知一棵有2011个结点的树,其叶结点的个数是116,该树对应的二叉树中无右孩子的结点个数是?

13.(146.5)在二叉树中有两个结点m和n,若m是n的祖先,则使用( )遍历可以找到从m到n的路径?

14.(149.34)先序序列为a,b,c,d的不同二叉树的个数是( )

15.(175.7)设F是一个森林,B是由F变换来的二叉树,若F中有n个非终端结点,则B中右指针域为空的结点有多少个?

16.(194.27)若度为m的哈夫曼树中,叶子结点个数为n,则非叶子结点的个数为?

17.(194.33)在任意一棵非空平衡二叉树T1中,删除某结点v之后形成平衡二叉树T2,再将v插入T2形成平衡二叉树T3,下列关于T1和T3的叙述中,正确的是()

  1. 若v是T1的叶结点,则T1与T3可能不相同
  2. 若v不是T1的叶结点,则T1与T3一定不相同
  3. 若v不是T1的叶结点,则T1与T3一定相同

18.(212.7)若无向图G=(V,E)中含有7个顶点,要保证图G在任何情况下都是连通的,则需要的边数最少是()

19.(212.8)以下关于图的叙述中,正确的是()

A. 强连通有向图的任何顶点到其他所有顶点都有弧
B. 图的任意顶点的入度等于出度
C. 有向完全图一定是强连通有向图
D. 有向图的边集的子集和顶点集的子集可构成原有向图的子图

20.(248.5)正确的是()

  1. 最小生成树的代价唯一
  2. 所有权值最小的边一定会出现在所有的最小生成树中
  3. 使用Prim算法从不同顶点开始得到的最小生成树一定相同
  4. 使用Prim算法和Kruskal算法得到的最小生成树总不相同

A. 仅1
B. 仅2
C. 仅1、3
D. 仅2、4

21.(250.21)求AOE网的哪些活动加快了,就可以缩短工期?

A. c和d
B. d和c
C. f和d
D. f和h
image_1eotm67l6oqf10rr10kpnin1fdt9.png-104.3kB

22.(275.2)n个元素的两个表,一个递增表,一个无序表。采用顺序查找,且递增表查到超过当前元素还不成功,就返回不成功并停止。则两种表中成功查找()

A. 递增表的ASL更小
B. 两种相同
C. 无序表ASL更小
D. 无法确定

23.(276.8)已知长度为16的顺序表,其元素按关键字有序排列,如果折半查找一个不存在的元素,则比较次数最多为?

24.(276.14)12个关键字的有序表,每个关键字概率相同,则成功的平均长度?失败的平均长度?

25.(276.15)

答案速查

  1. C,因为p和q是已知的所以单链表不会断裂
  2. C
  3. D,表头不算长度,且含有0个元素的时候也可以
  4. C,表头才是栈顶
  5. C
  6. C,除3以外的所有都可能
  7. 0,n-1,注意看rear指向队尾元素
  8. 5个
  9. 10个
  10. 1896,特殊值法
  11. 后序遍历
  12. 14个,即“入栈顺序为abcd的,出栈顺序有多少种?”
  13. n+1个
  14. 仅1
  15. 16条
  16. C
  17. A
  18. C,因为只有他包含了每条关键路径
  19. B
  20. 5
  21. 37/12,49/13
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注