数据结构清单
一、绪论
定义:
- 数据: 是对客观事物的符号表示,可以被输入到计算机中并被处理的符号总称
- 数据元素: 是数据的基本单位,含有许多数据项
- 数据项: 是具有独立意义的最小的数据单位,不可再分
- 数据对象: 是性质相同的数据元素的集合,是数据的子集
- 数据结构: 相互之间存在某种关系的数据元素的集合
-
逻辑结构: 数据元素之间的逻辑关系,是数据结构在用户面前呈现的方式 ▪ 集合: 数据元素之间除了同属于“一个集合”的关系外,别无其它关系 ▪ 线性结构: 数据元素之间存在一对一的关系,除了头节点和尾节点每个节点都有一个前驱和一个后继【线性表、有序表、栈、队列】 ▪ 树型结构: 数据元素之间存在一对多的关系,每个节点至多有一个前驱节点,多个后继节点【树、二叉树、森林】 ▪ 图形结构:数据元素之间存在多对多的关系,每个节点有多个前驱和后继节点【连通图、强连通图、欧拉图、稠密图、稀疏图】
-
存储结构: 数据元素及其相互之间的关系(数据结构)在算机存储器中的存储方式 ▪ 顺序存储结构: 把逻辑上相邻的节点在物理上也进行连续存储,多用数组实现【顺序表】 优点: 可以随机存取、存储利用率高 缺点: 每次的插入、删除需要移动大量的节点 ▪ 链式存储结构: 不要求逻辑上相邻的节点在物理上也相邻,多用指针实现【链表】 优点: 插入、删除便捷,只需要修改指针域 缺点: 存储空间利用率低,不能进行随机存取 ▪ 索引存储结构: 通常是在存储节点信息的同时,还建立索引表,其中索引项的形式为(关键字、地址) 优点: 查找速度提高,与线性结构结合后可以进行随机存取、插入删除上只需要移动索引表中对应节点的存储地址【线索二叉树】 缺点: 空间利用率更低 ▪ 哈希存储结构: 只存储节点的数据,不存储节点之间的逻辑结构,通过待查节点的关键字来计算出该节点的存储位地址 优点: 查询速度更快 缺点: 通常情况下只适合要求对数据进行快速查找和插入的场合
-
数据的运算: 施加在数据上的操作 附: 一种逻辑结构可以映射多种存储机构
· 数据类型:是一组性质相同的值的集合和定义在此集合上的一组操作的总称,是某种程序设计语言中已经实现了的数据结
- 非结构的原子类型:不可再分解的,如C语言中的基本类型(整型、实型、字符型、枚举类型)、指针类型、空类型
- 结构类型:它的值是由若干成分按某种结构组成,成分可以是结构型,也可以是非结构型 附:在某种意义上,数据结构可以看成是“一组具有相同结构的值”,结构类型可以看成是由一种数据结构和定义在骑上的一组操作组成 数据结构 = 数据元素 + 关系 数据类型 = 数据结构 + 操作
- 抽象数据类型(ADT): 是从问题的数学模型中抽象出来的逻辑数据结构和逻辑数据结构上的运算,不考虑计算机的具体存储结构和运算的具体实现算法【意义在于数据类型的数学抽象特性】
- 非结构的原子类型:不可再分解的,如C语言中的基本类型(整型、实型、字符型、枚举类型)、指针类型、空类型
例 ADT Complex {
数据对象:D={e1, e2 | e1, e2均为实数}
数据关系:R={<e1, e2> | e1是复数的实数部分,e2是复数的虚数部分}
基本运算:AssignComplex(&z, v1, v2): 构造复数z,其中的虚数和实数分别为e1、e2
DestroyComplex(&z): 销毁复数z
GerReal(z, &real): 用real变量得到实数部分的值
GetImag(z, &imag): 用imag变量得到虚数部分
Add(z1, z2, &sum): 用sum变量得到复数z1、z2的和
} ADT Complex
- 算法: 是对特定问题的一系列求解步骤的描述【有穷性、可行性、确定性、有输入、有输出】 算法的设计目标: 正确性、可读性、可使用性、健壮性、高效率和低存储需求
时间复杂度: O(1)<O(㏒₂n)<O(n)<O(n㏒₂n)<O(n²)<O(n³)<O(2^n)<O(n!)
二、线性表
定义:
是具有相同特性数据元素的一个有限序列
ADT List {
数据对象:D={a(i) | 1<=i<=n, n>=0, a(i)为ElemType类型}
数据关系:R={<a(i), a(i+1)> | a(i), a(i+1)∈D, i=1,……,n-1}
基本运算:略
}
线性表的顺序表存储结构:
typedef struct {
ElemType data[MaxSizr];
int length;
} SQlist;
线性表的单链式存储结构:
typedef struct LNode {
ElemType data;
struct LNode *next;
} LinkList;
线性表的双链式存储结构:
typedef struct DNode {
ElemType data;
struct DNode *prior;
struct DNode *next;
} DLinkList;
- 在其存储结构上的操作(初始化、增、删、改、查找、销毁),静态链表、循环链表【真题里面如果有那么就再复习】,带头节点和不带头节点链表的操作、区别【重点】
- 在链表操作中删除、插入节点需要找到该节点的前驱和后继(怎么个(不)带头节点和尾节点与各种链表搭配最适合要求)
- 在单链表中设置头节点的作用在于简化插入、删除节点的算法
- 循环链表和双链表中,判断表尾节点*p的条件是 p->next==L (Lw为头节点指针)
三、栈和队列
栈定义:
是一种特殊的线性表,也称后进先出表
ADT Stack {
数据对象:D={a(i) | 1<=i<=n, n>=0, a(i)为ElemType类型}
数据关系:R={<a(i), a(i+1)> | a(i), a(i+1)∈D, i=1,……,n-1}
基本运算:略
}
栈的顺序存储结构:
typedef struct {
ElemType data[MaxSize];
int top;
} SqStack;
栈的链式存储结构:
typedef struct linknode {
ElemType data;
linknode *next;
} LiStack;
在其存储结构上的操作(初始化、增、删、改、查找、销毁)、每次进出站,栈顶指针怎么变化的?算术表达式如何转变为后缀表达式?前缀表达式呢?
规律: 按一定顺序进栈,把每个元素从前到后从1开始编号,出栈顺序一定是比该元素的序列号小的元素的序列号构成一个递减序列
1 2 3 4 1 4 3 2 正确 1 4 2 3 错误
进栈 a b c d 出栈 a d c b 出栈 a d b c
卡特兰数:
有n个元素,它们的编号为1~n,顺序地进入一个栈,则可能出栈的顺序有种
附:等价以下问题
- n个不相同元素进栈的出栈序列个数;
- 由n个不同元素构成不同形态的二叉树个数;
- n个不同元素的先序序列构成不用形态的二叉树个数
栈空条件 top == -1 栈满条件 top == MaxSize-1 栈顶指针始终指向当前元素
口算后缀表达式:
将中缀表达式“5+2×(1+6)-8/2”转换成后缀表达式,按照运算顺序给表达式加上括号“((5+(2×(1+6)))-(8/2))”,把右括号按照左括号前面的运算符进行替换,再去掉左括号得到后缀表达式“5216+×+82/-”
递归皇后问题求解?迷宫问题?汉诺塔?斐波那契数列?非递归会不会?
队定义:
是一种特殊的线性表,也称先进先出表
ADT Stack {
数据对象:D={a(i) | 1<=i<=n, n>=0, a(i)为ElemType类型}
数据关系:R={<a(i), a(i+1)> | a(i), a(i+1)∈D, i=1,……,n-1}
基本运算:略
}
队列的顺序表存储结构:
typedef struct {
ElemType data[MaxSizr];
int front, rear;
} SqQueue;
环形队列的存储结构:
typedef struct {
ElemType data[MaxSize];
int front, count;
} QuType;
队列的链式存储结构:
typedef strcut qnode { typedef struct {
ElemType data; QNode * font;
struct qnode *next; QNode * rear;
} QNode; // 链队数据节点的定义 } LiQueue // 链队定义
在其存储结构上的操作(初始化、增、删、改、查找、销毁),报数求解问题,迷宫问题
顺序存储结构:
-
非循环队列:
队空条件: rear == front
队满条件: rear == MaxSize-1
初始条件: front == rear == -1
队首、队尾指针均指向当前队首、队尾元素
元素个数: rear-front
-
循环队列:
队空条件: rear == front
队满条件: (rear+1)%MaxSize == front
初始条件: front == rear == 0
队首、队尾指针均指向当前队首、队尾元素
元素个数: (rear-front+MaxSize)%MaxSize
-
溢出
上溢出:队满时仍然进队
下溢出:队空时仍然出队
假溢出:队中有空位置,但rear == MaxSize-1判断队满了
链式存储结构判断条件与链表一样
四、树和二叉树
树定义:
是一种非线性结构,由n(n>=0)个节点组成的有限集合,有且仅有一个根节点
ADT List {
数据对象:D={a(i) | 1<=i<=n, n>=0, a(i)为ElemType类型}
数据关系:R={<a(i), a(j)> | a(i), a(j)∈D, 1<=i<=n, 1<=j<=n, 其中每个元素只有一个前驱节点(除根节点),有零个或多个后继节点}基本运算:
略;
}
树的表示方法: 树形表示法、文氏图、凹入表示法、括号表示法
性质:
-
树种的节点数 = 所有节点度数之和 + 1
证明: 在树中,每个节点都有且仅有一个前驱节点(分支)(除根节点),所以所有节点的度数之和(分支数),再加上没有前驱的根节点,为树的全部节点
-
m的树中,第i层的节点数至多为m^(i-1)个节点(i>=1) 证明: 每个节点满度的情况下,第i层的节点数是第i+1层的m倍
-
高度为h的m次树至多有(m^h-1)/(m-1)个节点(满m次树的高度最小) 证明: 当这棵树为满m次树(每个节点的孩子节点数为m,叶子节点全部在最下层)
-
具有n个节点的m次树的最小高度为⌈log(m)[n*(m-1)+1]⌉ 证明: 设该树的高度为h,若该树的前h-1层都是满得,最后一层可满可不满,则该树具有最小高度有不等式 [m^(h-1)-1]/(m-1) < n <= [m^h-1]/(m-1) h = ⌈log(m)[n×(m-1)+1]⌉
-
叶子节点: n(0) = n(2) + 2×n(3) + …… + (m-1)×n(m-1) + 1
树的存储结构:
双亲存储结构:是一种顺序存储结构,在每个节点中附设一个伪指针指示其双亲节点的位置,根节点的伪指针置为-1
typedef struct {
ElemType data;
int pos;
} STree[MaxSize];
该种结构充分利用了每个节点只有唯一的双亲节点的性质,查找某个节点的双亲是很容易的,但查找孩子节点则需要遍历整个结构
孩子链存储结构:
按照树的最大度来设定孩子节点指针域的个数,解决了每个节点的孩子节点指针域个数增加使算法变得复杂。
typedef struct tnode {
ElemType data;
struct tnode *child[M];
} CTree;
该存储结构在面队二叉树、四叉树等情况下更适合,但对于其它树就显得很浪费存储空间
孩子兄弟链存储结构:
每个节点设计三个域:值域、该节点的第一个孩子节点指针域、该节点的下一个兄弟节点指针域
typedef struct cbnode {
ElemType data;
struct cbnode child;
struct cbnode brother;
} CBTree;
该存储结构是把树转换为二叉树的存储结构,所以最大的优点就是可以方便地实现树和二叉树之间的转换,但查找双亲节点很麻烦,需要从根节点开始查找
二叉树的概念:
定义:二叉树是另一种树形结构,其中不存在度大于2的节点,并且二叉树的子树有左右之分,次序不能颠倒(与度为2的树的区别)
!!!阐述满二叉树、完全二叉树、二叉排序树、哈夫曼树、平衡二叉树、B+树、B-树!!!
性质:
- 非空二叉树的叶子节点数 = 度为2的节点 + 1 【 n(0) = n(2) + 1 】
- 非空二叉树上第k层最多有2^(k-1)个节点(k >= 1)
- 高度为h的二叉树最多有2^h - 1个节点(h >= 1)
- 对 完全二叉树 进行层次遍历编号1,2……,n,则有以下关系: a. 当i > 1时,节点i的双亲节点编号为⌊i/2⌋,即当i是偶数时,其双亲节点编号为i/2,且该节点是左孩子节点;当i是奇数时,其双亲节点是(i-1)/2,且该节点是右孩子节点 b. 当2i <= n时,节点i的左孩子编号为2i,否则无左孩子节点 c. 当2i+1 <= n时,节点i的右孩子编号为2i+1,否则无右孩子节点 d. 节点i所在的层次(深度)为⌊log(2)(n+1)⌋或⌊log(2)(n)+1⌋
- n个不同的节点可以构造出C(n)(2n)/(n+1)[卡特兰数]不同的二叉树
二叉树的存储结构:
顺序存储结构:完全二叉树、满二叉树比较合适,树中节点的序号可以唯一地反映节点之间的逻辑关系,既节省了空间,又能利用数组下标确定节点在树中的位置和逻辑关系
链式存储结构:在含有n个节点的二叉链表中,含有n+1个空指针(为此产生了线索二叉树【物理结构】)
typedef struct bitnode {
ElemType data;
struct bitnode *lchild;
struct bitnode *rchild;
} BiTree;
二叉树的遍历:
先序遍历(中、左、右),中序遍历(左、中、右),后序遍历(左、右、中) Ps.这三种遍历的递归和非递归算法要会,”先+中/后+中/层次+中“可以唯一确定一棵二叉树
-
线索二叉树:
目的:加快查找节点的前驱和后继的速度(传统的链式存储只能体现一种父子关系,在遍历中体现类似线性表的前驱和后继关系却不能)
本质:对一个非线性结构进行线性化操作,线索二叉树是一种物理存储结构(链式)
typedef struct threadnode {
ElemType data;
struct threadnode *lchild;
struct threadnode *rchild;
int ltag, rtag;
} ThreadNode;
ltag = 0 lchild域指示节点的左孩子 rtag = 0 rchild域指示节点的右孩子
= 1 lchild域指示节点的前驱 = 1 rchild域指示节点的后继
问题:
在线序线索二叉树中查找一个节点的先序遍历的前驱节点无法找到
在后序线索二叉树中查找一个节点的后序遍历的后继节点无法找到 因为对于该节点的双亲是无法得知的,二叉链表中没有存放双亲的指针
Ps. 怎么构造的思想要会,能够说得清楚
给定一棵树可以唯一的确定一棵二叉树,二叉树转换为树或森林是唯一的 树的先序遍历 = 森林的先序遍历 = 二叉树的先序遍历,树的后序遍历 = 森林的中序遍历 = 二叉树的中序遍历
- 哈夫曼树: 定义:从树的根节点到任意节点的路径长度(经过的边数)与该节点上权值的乘积,记为该节点带权路径长度(WPL),所有叶节点的带权路径长度之和称为该树的带权路径长度,当带权路径长度最小的“二叉树”称为哈夫曼树,也叫最优二叉树 前缀码:没有一个编码是另一个编码的前缀(通常默认0是左子树,1是右子树,反过来也可以) 哈夫曼树每层都是两个节点(根节点除外),哈夫曼编码中有多少个0和1,我们就认为它的路径数是该和
例:根据使用频率为5个字符设计的哈夫曼编码不可能是 00,100,101,110,111 原因: 路径数为3的有4个节点,必然是由2个路径数为2的节点产生,序列中还有一个路径数为2的节点,总共路径数为2的节点有3个,不符合哈夫曼树定义
【Ps.以下都为树表的查找,在进行插入删除的操作时,不需要移动表中的记录,减少额外时间开销】
-
二叉排序树(BST): 定义:也称二叉查找树,特点是左子树节点值 < 根节点值 < 右子树节点值,递归实现每个子树都是BST BST作为一种动态集合,其特点是树的结构通常不是一次生成的,而是在查找的过程中,当树不存在关键字时再进行插入,当插入的序列是有序时,BST的高度最大 查找成功的平均查找长度计算、查找失败的平均查找长度计算(主要取决于树的高度O(log(2)(n)) ~ O(n))
-
平衡二叉树(AVL): 目的:为避免二叉树高度增长过快,提高二叉排序树的性能(AVL也是二叉排序树,要与完全二叉树区分开) 特点:任意节点的左右子树高度差为-1、0、1,每次调整AVL都是在离插入节点最近的那个超出范围平衡因子进行调整(LL、RR、LR、RL)
Ps.高度为n的平衡二叉树最少节点:N0=0,N1=1,N2=2,Nn=Nn-1 + Nn-2 + 1 最多节点:2^n - 1
-
B-树: 定义:又称多路平衡二叉树,是一种组织和维护外运文件系统非常有效的数据结构 特点:树中所有节点的孩子节点最大值称为B-树的阶,通常用m表示,一颗m阶B-树或者是一棵空树,或者是满足以下要求的m次树:
- 所有叶子节点都在同一层,并且不带信息
- 树中每个节点至多有m棵子树(即至多含有m-1个关键字)
- 若根节点不是终端节点,则根节点至少有两棵子树
- 除根节点外,其它非叶子节点至少有⌈m/2⌉棵子树(即至少含有⌈m/2⌉-1个关键字)
- 除根节点外,其它非叶子节点的关键字个数为⌈m/2⌉-1 <= n <= m-1
- 操作:增、删、改、建(类似于挤牙膏,少了照常添,多了就往上挤,首先要确定每个节点的关键字上限)
-
B+树: 定义:同上 特点:
-
每个分支节点至多有m棵子树
-
根节点或者没有子树,或者至少有两棵子树
-
除根节点外,其它分支节点至少有⌈m/2⌉棵子树
-
有n棵子树的节点有n个关键字
-
所有叶子节点包含全部关键字及指向相应记录的指针,而且叶子节点按关键字大小
顺序链接:所有分支节点仅包含它的各个子节点中最大关键字及其指向子节点的指针
-
五、图
定义:
- 图:图G由V(顶点的有限非空集合)和E(顶点边的有限集合)两个集合组成,记成G=(V,E)
- 无向图:在图G中,集合E是无序的
- 有向图:在图G中,集合E是有序的
- 完全图:图的每两个顶点之间都存在一条边 【完全无向图有n(n-1)/2条边,完全有向图存在n(n-1)条边】
- 端点和邻接点:一条边(i,j)<i,j>,其中i、j互为邻接点,也为这条边的两个端点
- 顶点的度(入度、出度):在无向图中,顶点的边数称为该点的度,有向图又分出度和入度,出度+入度=度 【在一个具有e条边的图中,度之和=2*e】
- 子图:由顶点和边的集合分别是子集,组成的图
- 路径和路径长度:在一个图中,从顶点i到顶点j的一条路径是一个顶点序列,且一条路径上经过的边的数目是路径长度 【当一条路径上除了开始点和结束点可以相同外,其它顶点均不同,则称为简单路径】
- 树图:任意两个顶点之间只有一条路径的无向连通图 【n个顶点的树图恰好有(n-1)条边】
- 回路或环:一条路径开始点和结束点是同一个点 【简单路径构成的环叫简单回路或简单环】
- 连通、连通图、连通分量:在无向图G中,顶点i到顶点j有路径,则称i到j是连通的;若图中任意两个顶点都连通,则称G为连通图,否则是非连通图;无向图G的极大连通子图称为G的连通分量 【任何连通图的连通分量只有自己本身,非连通图有多个连通分量】
- 强连通图、强连通分量:在有向图G中,任意两个顶点i和j都连通,则图G是强连通图,否则是非强连通图;有向图G的极大连通子图称为G的强连通分量 【任何强连通图的强连通分量只有自己本身,非强连通图有多个强连通分量】
- 稠密图、稀疏图:当一个图接近完全图,称为稠密图;当一个图含有较少的边时,称为稀疏图(e << n(n-1))
性质:
- 设图中度为i的顶点个数为n(i),对于无向连通图则有n(0) = 0
- 若一个无向连通图中有n个顶点和e条边,所有顶点的度均小于m,则有:
- n = n(m) +……+ n(2) + n(1) + n(0)
- 所有顶点度之和 = 2 * e
- 所有顶点度之和 = m * n(m) +……+ 2 * n(2) + 1 * n(1)
图的存储:
邻接矩阵
常用于无向稠密图,利用矩阵的压缩存储节省大量空间,存储的是边的信息【怎么看出一个顶点的度、度和入度,边数和顶点数的关系,查找哪些方便哪些不方便?】
#define MaxVertexNum 100 // 顶点数目最大值
typedef char VertexType; // 顶点的数据类型
typedef int EdgeType; // 带权图中边上权值的数据类型
typedef struct {
VertexType Vex[MaxVertexNum]; // 顶点表
EdgeType Edge[MaxVertexNum][MaxVertexNum]; // 邻接矩阵,边表
int vexnum, arcnum; // 图的当前顶点数和弧数
} MGraph;
特点:
-
无向图的邻接矩阵是一个对称矩阵(并且唯一),所以在存储中通常存储上(下)三角形矩阵
-
对于无向图的邻接矩阵,第i行(或第i列)非零元素(或非无穷元素)的个数正好是第i个顶点的度
-
对于有向图的邻接矩阵,第i行(或第i列)非零元素(或非无穷元素)的个数正好是第i个顶点的出度(或入度)
-
用邻接矩阵存储图,很容易确定任意两个顶点之间是否有边,但要确定有多少条边就得遍历整个矩阵
-
稠密图适合使用邻接矩阵存储
邻接表
常用于有向图,利用顺序表+链表节省大量空间,存储的是节点的信息【怎么看出一个顶点的度、度和入度,边数和顶点数的关系,查找哪些方便哪些不方便?】
#define MaxVertexNum 100 // 顶点数目最大值
typedef struct ArcNode { // 边表节点
int adjvex; // 该边所指的顶点
struct ArcNode *next; // 指向下一条边
} ArcNode;
typedef struct VNode { // 顶点表节点
VertexType data;
ArcNode *first; // 顶点指向的第一条边
} VNode, AdjList[MaxVertexNum];
typedef struct {
AdjList adjlist; // 邻接表
int n, e; // 图的顶点数和边数
} AGraph;
特点:
-
若为无向图,则所需的存储空间为O(|V| + 2|E|)【每条边都出现了两次】;若为有向图,则所需的存储空间为O(|V| + |E|)
-
邻接表适合存储稀疏矩阵
-
用邻接表存储图,很容易确定任意一个顶点的所有邻边,但要确定两点之间是否有边,则需要跳转至另一节点去寻找相应的节点
-
在有向图中,邻接表中的一行表示该顶点的出度,求入度则需要遍历邻接表
-
邻接表表示不唯一,因为在建表过程中,各边节点的链接次序是任意的,取决于建表的算法及边的输入次序
-
十字链表
有向图的另一种链式存储结构
-
多重邻接矩阵 无向图的另一种链式存储结构
-
图的遍历:
-
广度优先遍历(BFS)
类似于二叉树的层次遍历算法(使用队列)
-
深度优先遍历(DFS)
类似于二叉树的先序遍历算法(使用栈)
-
最小生成树
无回路的无向图称为树图(含n个顶点、n条边),在带权图中,权值最小的树图称为最小生成树,树图不唯一(理由显而易见)
-
普里姆算法(Prim) 一种构造性算法,每次从当前顶点选择权值最小的边进行连接,再在下一个顶点按同样的方法选取连接顶点,其中不允许出现回路,当一个顶点不存在可连接的其它顶点时,选择退回到上一个顶点进行选择,直到加入了n个顶点 适合于稠密图,因为时间复杂度O(n^2)与边数无关
-
克鲁斯卡尔算法(Kruskal)
一种按权值的递增次序选择合适的边来构造最小生成树,从具有最小权值的边开始构造,如果形成回路则选择第二小的边,直到加入了n-1条边
适合于稀疏图,因为时间复杂度O(e*log(2)(e))与顶点树无关
-
-
最短路径
-
迪克斯特拉算法(Dijkstra)
也称单源最短路径算法(即从任意一个顶点到其余顶点的最短路径,不能存在负权值边) 过程:
-
选取一个顶点i,把顶点i到其余顶点的权值放入数组dist[]中,不能到达的设置为无穷;
-
选取距离顶点i权值最小的顶点j;
-
计算从j到其余顶点的权值;
-
将j到其余顶点的权值与i到其余顶点的权值相比较,如果权值更小则更新dist[];
-
重复上面的步骤,直到所有顶点都已被加入
时间复杂度是O(n^2)
-
-
弗洛伊德算法(Floyd)
也称多源最短路径算法(即每对顶点的最短路径)
-
AOV网和拓扑排序 无回路的有向图,称为有向无环图,其中的顶点称为一个拓扑序列(即要到达该顶点必须先经过该顶点的所有前驱顶点),这样的有向图称为AOV网 在一个有向图中寻找一个拓扑序列称的过程为拓扑排序: 1. 从有向图中选择一个没有前驱(即入度为0)的顶点并且输出它; 2. 从网中删除该顶点和从该顶点出发的所有边; 3. 重复以上步骤,直到剩余网中不再有入度为0的顶点(即剩下的是环或者没有顶点) 拓扑排序产生的拓扑序列不唯一,时间复杂度为O(n + e)
-
AOE网与关键路径
-
六、广义表
定义:
-
广义表是n(n>=0)个元素的一个有限序列,GL=(a1, a2, a3, ..., an)
-
通常表现形式:
样例 解释 A = (#) A是一个空表,‘#’不表示任何广义表元素,表长度为0,表深度为1,无表头表尾 B = (e) B是一个只含有单元素e的表,表长度为1,表深度为1 C = (a, (b, c, d)) C是含有一个原子、一个子表的表,表长度为2,表深度为2 D = (A, B, C) = ((#), (e), (a, (b, c, d))) D是含有三个子表的表,表长度为3,表深度为3 E = ((a, (a, b), ((a, b), c))) E是含有一个子表的表,表长度为1,表深度为4 -
广义表深度:是指广义表所包含括号的重数
-
表头:广义表的第一个元素a1称为表头,例如B的表头是e,D的表头是(#)
-
表尾:广义表的表头以外的其它元素构成的子表称为表尾,例如B的表尾是(#), D的表尾是((e), (a, (b, c, d))),表尾始终是一个广义表 附:取表头操作 head[GL] = a1,取表尾操作 tail[GL] = (a2, a3, ..., an)
广义表的存储结构:
广义表一般采用链式存储结构,主要有孩子兄弟存储结构【可以将广义表看成一棵树,然后按照二叉树来存储】
typedef struct GLNode {
int tag; // 0:原子节点;1:表/子表节点
struct GLNode *link; // 指向后继的元素或兄弟的指针
union {
ElemType data; // 原子的值
struct GLNode *sublist; // 指向第一个元素的指针
} val;
} GLNode;
附:广义表、数组、线性表之间的关系:
- 数组是具有相同性质的有限个数据元素的序列,每个元素有唯一的下标限定,多维数组可以看做是线性表中的线性表
- 广义表是有限个数据元素的序列,每个元素既可以是原子,也可以是子表,元素之间也是线性关系
- 线性表是具有相同性质的有限个数据元素的集合,元素之间具有线性关系,数组和广义表是线性表的推广
七、查找
对查找表的操作通常有以下:
- 查询某个特定的数据元素是否存在查找表中
- 检索满足条件的某个特定的数据元素的各种属性
- 在查找表中插入一个数据元素
- 从查找表中删除某个特定的数据元素
静态查找表
-
顺序查找
又称线性查找,主要用于在线性表中关键字的“有序”和“无序”查找
无序的查找表中查找成功的平均查找长度:(n+1)/2 查找失败的平均查找长度:(n+1)
有序的查找表中查找成功的平均查找长度:(n+1)/2 查找失败的平均查找长度:n/(n+1)
-
折半查找 又称二分查找,只适用于有序的顺序表,链式存储结构不可以使用
查找成功的平均查找长度:[log(2)(n+1)]
查找失败的平均查找长度:(n+1)
Ps.这个平均查找长度要会看着图算出来
-
分块查找
又称索引顺序查找,即块内无序,块间有序,每块中的最大关键字小于第二块中的所有关键字,索引表由每块最大的关键字和该块的起始下表构成 将长度为n的查找表均匀分为b块,每块有s个记录,在等概率的情况下 当块内和索引表中采用顺序查找:查找成功的平均查找长度:(s^2 + 2*S + n) / (2 * s) 若s = sqrt(n),则最小值为sqrt(n)+1 当对索引表采用折半查找:查找成功的平均查找长度:⌈log(2)(b+1)⌉ + (s + 1) / 2
动态查找表
-
二叉排序树查找
B树及B+树(参考上文中“二叉树的遍历”)
-
散列表(既是动态也是静态查找表)
也称哈希函数,主要通过哈希函数构造出存储地址:
-
直接定址法:H(key) = a
-
除留余数法:H(key) = key%p【通常p取最大的素数(不大于哈希表表长)】
-
数字分析法:选取数码分布较为均匀的若干位作为哈希地址
-
平方取中法:取关键字的平方中间几位作为哈希地址
-
折叠法:将关键字分割成位数相同的几部分,然后取这几部分的叠加和作为哈希地址
-
随机数法:H(key) = random(key)【在关键字长度不等的情况下使用更好】
处理地址冲突的办法:
-
开放地址法:以发生冲突的哈希地址为自变量,通过某种哈希冲突函数得到新的空闲哈希地址
- 线性探测法:H(key) = (H(key)+i)%m 0<=i<=m-1
- 平方探测法:H(key) = (H(key)+d)%m d=d+i^2 0<=i<=m-1
- 双散列法:H(key) = (H(key)+i*H(key))%m
-
拉链法:把所有同义词用单链表链接起来,哈希表每个单元存放的是相应同义词单链表的表头指针
装填因子:n/m(n是待填元素总数,m是跟在%后面的那个数) 哈希表的平均查找长度依赖于装填因子大小(表中记录数/表长度),不直接依赖于表中记录数或表长度,装填因子越大发生冲突可能性越大 _Ps.每种方法表怎么填写?匹配查找成功的平均次数?查找失败的平均次数?_
-
八、内排序
排序的稳定性:在待排序表中,存在多个关键字相同的元素,经过排序后这些具有相同关键字的元素之间的相对次序保持不变,则称这种排序是稳定的,否则是不稳定的
L[]表示一个表,L()表示一个元
插入排序:
思想是每次将一个待排序的记录按其关键字大小插入到前面“已排序的子序列”中,直到所有记录插入完成
-
直接插入排序
- 查找出L(i)在L[1……i-1]中插入位置k
- 将L[k……i-1]中的所有元素全部后移一个位置
- 将L(i)复制到L(k)中
空间复杂度:仅使用了常数个辅助单元,为O(1)
时间复杂度:O(n^2)
稳定性:由于每次插入元素时总是从后向前先比较再插入,所以元素相对位置不会变化,属于稳定排序
适用性:适用于顺序存储和链式存储
特点:初始元素逆序时,执行效率最差,越接近正序,执行效率越高
-
折半插入排序
-
先折半查找出元素待插入的位置
-
统一地移动其它元素
-
将元素插入
空间复杂度:仅使用了常数个辅助单元,为O(1)
时间复杂度:O(n^2)
稳定性:属于稳定排序
适用性:适用于顺序存储和链式存储
特点:仅在直接插入排序的基础上减少了关键字的比较次数,元素的移动次数不变
-
-
希尔排序(缩小增量排序)
-
取一个小于n的步长d(1),把表中的全部记录分成d(1)组
-
所有距离为d(1)的倍数的记录放到同一组,在各组之间进行直接插入排序
-
选取第二个步长d(2) < d(1),重复上述两步
-
一直到d取值为1,再进行直接插入排序
空间复杂度:仅使用了常数个辅助单元,为O(1)
时间复杂度:O(n^1.3) 稳定性:相同关键字的记录被划分到不同的子表中,相对次序会发生改变,属于不稳定排序 适用性:只适用于顺序存储 特点:待排序列正序时效率最高,逆序最差
-
交换排序:
-
冒泡排序
- 从第一个元素开始,从前往后两两比较相邻元素的值
- 若L[i-1] > L[i],则交换两者的值
- 从第二个元素开始,重复上述步骤
- 一直比较到最后两个元素,排序结束
空间复杂度:仅使用了常数个付诸单元,为O(1)
时间复杂度:O(n^2)
稳定性:属于稳定排序
适用性:适用于顺序存储和链式存储
特点:待排序列越接近正序效率越高
-
快速排序
对冒泡排序基于分治思想的改进,是所有内部排序里面平均性能最优的算法
- 在待排序表中选取一个基准pivot,将待排序表分成L[1……k-1]和L[k+1……n]两部分
- 将元素不断和pivot进行比较,使得L[1……k-1]内的元素都小于L[k+1……n]的元素,pivot最终存放在L(k)
- 一趟快速排序结束,在两部分内分别选取新的基准pivot,进行下一趟排序
- 重复上述过程,直到每部分内只有一个元素或者空为止
空间复杂度:使用了栈,最坏情况下为O(n),平均情况下为O(log(2)(n)) 时间复杂度:与划分算法有关,最坏为O(n^2),最好为O(n*log(2)(n)) 稳定性:在划分算法中,当右区间内存在两个相同的关键字,一趟快速排序后会改变它们的相对顺序,属于不稳定排序 适用性:只适用于顺序存储 特点:待排序咧越是随机分布,效率越高,越是有序,效率越低
选择排序:
-
简单选择排序
- 将数组分成两部分L[0……i-1]有序,L[i……n-1]无序,且有序区的所有关键字均小于无序区的关键字
- 在无序区L[i……n-1]选取最小的关键字L(i)
- 将L(i)添加到L[0……i-1]中,使得L[0……i]成为有序
- 重复上述过程,直到无序区没有关键字
空间复杂度:仅使用了常数个辅助单元,为O(1) 时间复杂度:O(n^2) 稳定性:无序区元素的相对位置会因为大小而在有序区发生改变,属于不稳定排序 适用性:适用于顺序存储和链表 特点:排序效率与待排数据无关,因为每次是比较选取最小关键字,比较次数不变
-
堆排序
每个节点的关键字不小于其孩子节点称为“大根堆”,反过来是“小根堆”
- 将初始待排序关键字序列(L1,L2….Ln)构建成大顶堆,此堆为初始的无序区;
- 将堆顶元素L[1]与最后一个元素L[n]交换,此时得到新的无序区(L1,L2,……Ln-1)和新的有序区(Ln),且满足L[1,2…n-1]<=L[n];
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(L1,L2,……Ln-1)调整为新堆(堆化、筛选),然后再次将L[1]与无序区最后一个元素交换,得到新的无序区(L1,L2….Ln-2)和新的有序区(Ln-1,Ln)。不断重复此过程直到有序区的元素个数为n-1(原堆剩最后一个元素),则整个排序过程完成。
空间复杂度:就地排序,用于堆化(又称筛选)的辅助空间,为O(1) 时间复杂度:O(nlog(2)n) 适用性:适合顺序存储、链式存储(顺序存储的完全二叉树) 特点:排序效率与待排数据无关,每次必定会选出一个最小(大)的关键字出来
归并排序:
将序列看成是n个长度为1的有序表,每一轮将相邻的有序表进行合并,合并过程中进行排序
空间复杂度:使用到递归,为O(n)
时间复杂度:O(nlog(2)n)
稳定性:稳定的排序
适用性:适合顺序存储
特点:排序效率与待排数据无关,每轮产生的有序区不一定是全局有序
基数排序:
不比较关键字的大小,而是根据关键字中各位的值进行排序(个位、十位、百位……)
空间复杂度:使用到递归,为O(n+r)
时间复杂度:O(d(n+r))
稳定性:稳定的排序
适用性:
特点:每轮产生的有序区不一定是全局有序
Ps.冒泡排序、堆排序、简单选择排序产生全局有序区
希尔排序、直接插入排序、快速排序、归并排序不产生全局有序区; 快速排序每一趟可以归位一个元素; 基数排序产生可能是全局有序,也可能全局无序
排序算法比较图:
