1.线性表
InitList(&L):初始化为空的线性表
Length(L):返回表长
LocateElem(L,e):按值查找
getElem(l,i):按位查找
ListInsert(&L,i,e):插入
ListDelete(&L,i,&e):删除
PrintList(L):输出操作
Empty(L):判断是否为空
DestroyList(&L):销毁操作
1.1 顺序表(随机存取,数组)
//定义顺序表
struct xx{
int data[max];
int length;
}
//动态分配
l.data=(int *)malloc(sizeof(ElemType) * InitSize);
1.2 链表
引入头节点:第一个节点的操作与其他节点无异;头指针不会指向空,空表与非空表无异
单链表使用表尾指针对删除表尾元素没有作用,依旧需要从表头开始遍历
循环链表:
循环单链表:最后一个节点指针指向头节点
循环双链表:头节点指向最后一个节点
//定义链表节点
struct LNode{
int data;
struct LNode *next;
}
//重命名
typedef struct LNode{
}LNode, *LinkList;
typedef struct LNode *Linklist;
前插(无法直接找到前一个节点)
1.前后指针:
2.后插,再交换两个节点的data
1.3 静态列表
data域+next
next指向下一个,-1表示结尾,初始全部设置为-2
静态链表:需要较大连续空间,插入删除不需要移动元素
1.4 顺序表和链表区别:
顺序表需要连续的内存空间,存储密度高,方便随机查找和修改,插入和删除不方便,
链表不需要连续的内存空间,每个节点都有数据域和指针域,存储密度低,方便插入和删除,不方便随机查找
2 栈
!!!上溢是指存储器满了还往里写,下溢是指存储器空还往外读
栈(stack):只允许在一端进行插入和删除的线性表,先进后出(LIFO),栈顶,栈底
InitStack(&S):初始化栈,并分配空间;
DestroyStack(&S):销毁栈,释放占用空间;
Push(&S,x):进栈,若栈未满,x成为新栈顶;
Pop(&S,&x):出栈,若栈非空,x出栈并返回;
GetTop(S,&x):读栈顶元素;
StackEmpty(S):判断栈是否为空;
n个元素进栈不同的出栈数量(卡特兰数):1/n+1 C n 2n
typedef struct{
int data[max];
int top;
}SqStack;
//初始化
s.top=-1;
//进栈
s.top=s.top+1;s[top]=x;
//出栈
x=s[top];s.top=s.top-1;
s.top==-1;//栈空
s.top==max-1;//栈满
leng=s.top+1;
2.1 共享栈
使用数组规定好栈的大小,使用连续的内存空间存储;使用top0和top1两个指针从两个方向向中间;栈满:top0+1=top1;
3.队列
队列是一种操作受限的线性表,在队尾进行插入,在队头进行删除,先进先出(FIFO)
InitQueue(&Q):初始化空队列
QueueEmpty(Q):判空
EnQueue(&Q,x):若队列未满,入队
DeQueue(&Q,&x):若队列不空,出队
GetHead(Q,&x):获取队头元素
//队列顺序存储
typedef struct{
ElemType data[max];
int front,rear;
}SqQueue;
Q.frony=Q.rear=0;//初始值
Q,front=(Q.front+1)%max;//出队
Q.rear=(Q.rear+1)%max;//入队
(rear+max-front)%max;//求队长
3.1循环队列当rear==front时判断队空还是队满:
1.牺牲一个单元
队满:(Q.rear+1) % max==Q.front;
队空:Q.front == Q.rear;
队长:(Q.rear-Q.front+max)%max;
2.数据类型新增size变量,初始size=0,入队时size+1,出队时size-1,队满size=max
队空和队满时size都Q.front==Q.rear
3.数据类型新增tag=0变量,入队tag=1,出队tag=0;当Q.front==Q.rear时,tag=0则为队空,tag=1则为队满
链式结构:同时有首位两个指针的单链表(通常带头节点)
//节点
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkNode;
//链队
typedef struct{
LinkNode *front,*rear;
}LinkQueue;
没有头节点时,Q.front==null,Q.rear==null时,队列为空
双端队列:允许两端都可以插入和删除
4.栈和队列的应用
中缀表达式–>后缀表达式(需要一个栈存符号)
扫描表达式,
1)操作数,加入后缀表达式
2)(),(加入栈,)弹出栈中元素直到遇到(
3)运算符,依次弹出栈中优先级高或等于自己的运算符,再入栈
后缀表达式求值
扫描表达式,
1)操作数,入栈
2)运算符op,弹出两个操作数Y,X,进行X OP Y运算,将结果压入栈中
队列的运用:层序遍历,缓冲区,cpu多队列
1)一个节点入队
2)一个节点出队,访问他,将他的孩子节点入队
5.数组:
多维数组有两种映射方法:按行优先和按列优先
特殊矩阵的压缩存储:指具有许多相同元素或零矩阵,有规律分布,对多个相同的值只存储一个空间,对零元素不分配空间。
常见:对称矩阵,上三角矩阵,对角矩阵
对称矩阵:存上三角+对角线,n阶对称矩阵存放在一维数组 B[n(n+1)/2]
aij在数组中的位置:数组下标从0开始
下三角部分(i>j):1+2+…+(i-1)+j-1=i(i-1)/2+j-1
上三角部分(i<j):1+2+…+(j-1)+i-1=j(j-1)/2+i-1
上三角矩阵:下三角部分为同一常量,放在A[n(n+1)/2+1]
统一元素存放在 n(n+1)/2
三对角矩阵
稀疏矩阵:非0元素非常少,可以用三元组表,数组,十字链表存储
使用三元组表存时还要保存行数,列数和非零元素个数
6.串
串长度表示:1.用length变量 2.结尾加上\0
//定长顺序存储
typedef struct{
char ch[max];
int length;
}SString;
//堆分配
typedef struct{
char *ch;
int length;
}HString;
//块链存储
StrAssign(&T,chars):赋值
StrCopy(&T,S):复制
StrEmpty(S):判断空
StrCompare(S,T):比较;S》T返回大于0
StrLength(S)
SubString(&Sub,S,pos,len):求子串,从第pos个字符开始长度为len
ConCat(&T,S1,S2):串拼接
Index(S,T):定位,若主串S中含有T,则返回T在S中第一次出现的位置,否则返回0
ClearString(&S):
DestroyString(&S):
6.1 串匹配模式:
1.简单匹配模式 O(nm) n,m分别为主串和子串长度
2.KMP O(n+m)
编号 1 2 3 4 5
S a b c a c
next 0 1 1 1 2
3.KMP优化
修改nextval数组,若next数组指向下标字母与自己字母相同,nextval的值为指向字母nextval的值
7.树
路径长度:有向的
性质:
1.节点数n=所有节点度之和+1
2.度为m第i层至多m^(i-1)
3.高为h的m叉树至多(m^h-1)/(m-1)个结点
4.度m,n个结点最大高度h=n-m+1
7.1二叉树
二叉树可以为空
满二叉树:每一层的节点都是满的
完全二叉树:最后一层的节点允许没满
二叉排序树:左子树所有结点都<根<右
平衡二叉树:任一结点左右子树高度差不超过1
正则二叉树:树中只有结点度为0或2的
二叉树的性质
1.n0=n2+1 (用度之和+1=结点数)
2.第k层最多有2^(k-1)个结点
3.高度为h的二叉树最多2^h -1个结点
4.第i个节点左孩子为2i
二叉树的存储:
1.顺序存储(需要添加空结点):适合完全二叉树和满二叉树
2.链式存储:
typedef struct{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
!!!在含有n个节点的二叉链表中,含有n+1个空链域
二叉树的遍历
先序遍历(根左右):
void PreOrder(BiTree T){
if(T!=null){
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
中序遍历(左根右):void InOrder(BiTree T)
后序遍历(左右根):void PostOrder(BiTree T)
//非递归实现中序遍历
void InOrder(BiTree T){
InitStack(S);
BiTree p=T; //p是遍历指针
while(p || IsEmpty(S)){ //栈不空或p不空时循环
if(p){
Push(S,p); //当前结点入栈
p=p->lchild;//左孩子不空,一直向左走
}else{
Pop(S,p);visit(p);//出栈
p=p->rchild;//向右子树走
}
}
}
层序遍历:借助队列实现,将根结点入队,然后出队一个结点并访问,若有孩子结点,则将孩子结点入队
void LevelOrder(BiTree T){
InitQueue(Q);
BiTree p;
EnQueue(Q,T);//将根节点入队
while(!Empty(Q)){//队列不空则循环
DeQueue(Q,p); //队头出队
visit(p);
if(p->lchild !=null)
EnQueue(Q,p->lchild);
if(p->rchild !=null)
EnQueue(Q,p->rchild);
}
}
确定一颗二叉树(先序,中序,后序,层序):中序+其他任一种
线索二叉树:结点没有对应的左右孩子时,左右指针指向前驱/后继结点
(lchild ltag data rtag rchild)
ltag=0,lchild指向结点的左孩子
ltag=1,lchild指向结点的前驱
typedef struct{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag;
}ThreadNode,*ThreadTree;
//中序线索化
void InThread(ThReadTree &p,ThreadTree &pre){
if(p!=null){
InThread(p->lchild,pre);//递归,线索化左子树
if(p->lchild==null){ //当前结点左子树为空
p->lchild=pre; //建立当前结点的前驱线索
p->ltag=1;
}
if(pre!=null && pre->rchild==null){//前驱结点非空且右子树为空
pre->rchild=p;
pre->rtag=1;
}
pre=p;
InThread(p->rchild,pre);
}
}
//中序遍历建立线索化二叉树
void CreateInThread(ThreadTree T){
ThreadTree pre=null;
if(T!=null){
InThread(T,pre);
pre->rchild=null;
pre->rtag=1;
}
}
后序线索二叉树找后继:1.根,后继为空 2.双亲的右孩子 或 左孩子但双亲没有右孩子,后继为双亲 3.左孩子且双亲有右子树,后继为右子树上后序遍历第一个结点
后序线索二叉树的遍历还须栈的支持
7.2 树
树的存储方法:
1.双亲表示法:类似于静态链表,用一组连续的内存空间存储,可以很方便找到双亲节点,但找孩子时需要遍历整个链表
typedef struct{
ElemType data;
int parent;
}PTNode; //节点定义
typedef struct{
PTNode nodes[max];
int n;//节点数
}PTree; //树
2.孩子表示法:
将每个节点的孩子节点视作一个线性表,共n个,这n个头指针又组成一个线性表,可采用线性存储,方便找到孩子,找双亲时需要遍历所有线性表
3.孩子兄弟表示法:二叉树表示法,左指针指向第一个孩子,右指针指向下一个兄弟结点
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibing;
}CSNode,*CSTree;
树–>二叉树
森林–>二叉树:先将森林中每一颗树转化为二叉树,任一颗树的右子树为空,再将每棵树的跟视为兄弟结点
二叉树–>森林
树的遍历
1.先根遍历(相当于对应二叉树的先序遍历)
2.后根遍历(相当于对应二叉树的中序遍历)
森林的遍历
1.先序遍历 一颗一颗树来
2.中序遍历 一颗一颗按后根遍历来
7.3哈夫曼编码
带权路径长度WPL:每个结点 权值*根的结点的路径长度 求和
带权路径长度WPL最小的二叉树称为哈夫曼树
构造哈夫曼树(有n个结点每个结点的权值分别为W1,W2,,,Wn):
1.从n个节点中挑选权值最小的两个结点W1和W2作为左右孩子,他们权值之和W=W1+W2,作为根节点
2.将根节点加入剩下的n-2个结点,共n-1个
3.重复以上步骤
哈夫曼树的特点:
1.权值越小,到根节点的路径长度越长
2.n个结点共选(n-1)轮,因此哈夫曼树共有2n-1个结点
3.不存在度为1的结点
固定长度编码和可变长度编码
哈夫曼编码:从根开始左分支为0,右分支为1
前缀编码:没有一个编码是另一个编码的前缀
7.4 并查集
一种简单的集合操作,主要有三种操作
存储方法:类似树的双亲表示法,用数组表示 s[i]表示i号结点的根,负数时没有根
1.Initial(S):将集合中每个元素都初始化为一个单元素的子集合
void Initial(int s[]){
for(int i=0;i<max;i++)
s[i]=-1;
}
2.Union(S,root1,root2):把集合S中的子集合root2并入子集合root1(root1和root2不相交)
void Union(int s[],int root1,int root2){
if(root1==root2) return; //要求root1和root2是不同集合
s[root2]=root1; //将root2连接到root1上
}
3.Find(S,x):查找集合S中元素x所在的子集合,时间复杂度O(d),d为树的深度
int Find(int s[],int x){
while(s[x]>=0)
x=S[x];
return x; //根小于0
}
并差集的优化:???
1.union
2.find
并查集可以实现克鲁斯卡尔算法和判断图的连通性
8.图
8.1 基本概念
图由顶点集V和边集E组成,顶点集不能为空,也就是不存在空图
有向图:<v,w> w为弧头,v为弧尾
无向图:(v,w)
简单图和多重图:不存在重复的边和不存在自身到自身的边为简单图,否则为多重图
完全图:任一两个顶点之间都存在边;无向完全图有n(n-1)/2条边,有向完全图有n(n-1)条边
子图:
连通和强连通:无向图两个顶点之间有路径连通,有向图有从a到b,b到a的路径成为强连通
连通图(无向):任意两个顶点都是连通的,注意没要求任意两个顶点都有路径,非连通图小于n-1条边
强连通图(有向):任意一对顶点都是强连通的,至少n条边形成环路
连通分量:无向图中的极大连通子图
强连通分量:有向图中的极大强连通子图
生成树和生成森林:
顶点的度,入度,出度:无向图全部顶点度之和=边数*2,无向图全部顶点入度之和=出度之和=边数
边的权,网:边有权值的带权图称为网
稠密图,稀疏图:
路径:两个顶点之间经过的顶点序列(包括本身)
路径长度:路径上经过边的数目
回路:一个图边数大于n-1,则一定有环
简单路径:不出现重复的路径
简单回路:除第一个顶点和最后一个顶点外不出现重复顶点的回路
距离:路径长度,不存在路径时为无穷
有向树:一个顶点入度为0,其余顶点入度全为1的有向图
8.2 图的存储
1.邻接矩阵(适合稠密图):
用一个一维数组存储顶点信息,用一个二维数组存储边信息,边不存在时用0或∞表示
typedef struct{
char vex[max];
int edge[max][max];
int vnum,anum;
}MGraph;
空间复杂度为O(n^2),无向图的邻接矩阵是对称矩阵,可压缩存储
对于无向图,第i行的非0元素为顶点i的出度,第j列的非0元素为顶点j的入度
邻接矩阵可直接判断两个顶点之间是否有边相连,但要判断有多少条边,需要遍历行列每个元素
!A^n[i][j]表示顶点i到j长度为n的路径数目
2.邻接表(适合稀疏图)
顺序存储建立一个顶点表,为每个顶点建立一个边表(单链表)
顶点表每个结点由data域和边表头指针组成
边表每个节点由邻接点域和指向下一条边的指针域组成
//边表结点
typedef struct{
int data;//邻接点
struct ArcNode *nextarc;
//int info; 代表权值
}ArcNode;
//顶点表
typedef struct{
int data;
struct ArcNode *firstarc;
}VNode,*AdjList[max];
//图
typedef struct{
AdjList vertices;//邻接表
int Vnum,Anum;//顶点数和边数
}ALGraph;
无向图空间复杂度为O(V+2E),有向图O(V+E)
邻接表给定一个结点很容易找出所有邻边,邻接矩阵需要扫描一整行
3.十字链表(有向图)
每条弧和每一个顶点都用一个结点来表示
弧结点:tailvex(弧尾结点) headvex(弧头结点) hlink(弧头相同下一条弧) tlink(弧尾相同下一条) info(权值)
顶点结点: data firstin(以该顶点为弧头的第一个弧结点) firstout(以该顶点为弧尾的第一个弧结点)
4.邻接多重表(无向图)
边结点:ivex(该边第一个结点i) ilink(i结点下一条边) jvex(该边第二个结点j) jlink(j结点下一条边)
顶点:data firstedge
8.3 图的基本操作
Adjacent(G,x,y):判断xy是否为临边
Neighbors(G,x):列出与x相邻的边
InsertVertex(G,x):插入节点x
DeleteVertex(G,x):删除节点x
AddEdge(G,x,y):若不存在(x,y)或<x,y>的边,则添加
RemoveEdge(G,x,y):若存在则删除边
FirstNeighbor(G,x):求X第一个邻接结点,若没有则返回-1
NextNeighbor(G,x,y):若y是x的第一个邻接结点,则返回y之外的下一个
Get_edge_value(G,x,y):获取临边xy的权值
Set_edge_value(G,x,y,v):把xy边设置权值v
8.4 图的遍历
从图中某一顶点出发,按某种算法沿着图中的边访问所有顶点且只访问一次,一般借助一个数组visit[]标记顶点是否访问过
8.4.1 广度优先 BFS
类似于二叉树的层序遍历算法,可借助辅助队列实现,从某个未被访问过的顶点w出发,依次访问它的所有邻接结点w1,w2…然后依次访问w1,w2…为被访问过的所有邻接结点(Dijkstra单源最短路径和Prim最小生成树思想类似)
空间复杂度O(v)
时间复杂度:用邻接表O(v+e) 用邻接矩阵O(v^2)
//广度优先
bool visited[max];
void BFSTraverse(Graph G){
for(int i=0,i<G.vnum,i++){
visited[i]=false;
}
InitQueue(Q);
for(int j=0;j<G.num;j++){ //从第一个结点开始遍历
if(!visited[j])
BFS(G,j); //未被访问过的
}
}
//这里可以用邻接表和邻接矩阵实现
void BFS(ALGraph G,int j){
visit(j);
visited[j]=true;
EnQueue(Q,j); //出对
//这里分别用邻接表和邻接矩阵来实现
//邻接表
while(!Empty(Q)){
DeQueue(Q,v);//队首顶点出对
for(p=G.vertices[v].firstarc;p;p=p->nextarc){ //访问该顶点连接的链表
w=p->adjvex;
if(visited[w]==false){ //未被访问过
visit(w);
visited[w]=true;
EnQueue(Q,w);
}
}
}
//邻接矩阵
while(!Empty(Q)){
DeQueue(Q,v);//队首顶点出对
for(w=0;w<G.vnum;w++){ //访问邻接矩阵该顶点对应行
if(G.edge[j][w]==1&&visited[w]==false){ //未被访问过
visit(w);
visited[w]=true;
EnQueue(Q,w);
}
}
}
}
BFS求非带全图单源最短路径
void BFS_distance(Graph G,int u){
//d[i]表示u到i的最短路径
for(int i=0;i<G.vnum;i++){
d[i]=∞;
}
visited[u]=true;d[u]=0;
EnQueue(Q,u);
while(!isEmpty(Q)){
DeQueue(Q,u);
for(w=firsNeighbor(G,u);w>=0;w=nextNeighbor(G,u)){
if(!visited[w]){
visited[w]=true;
d[w]=d[u]+1;
EnQueue(Q,w);
}
}
}
}
广度优先生成树:广度优先遍历可以得到一颗生成树,广度优先生成树,不唯一
8.4.2 深度优先 DFS
类似于树的先序遍历,从顶点v出发,访问v未被访问过的一个顶点w1,在访问w1一个未被访问过的顶点w2,以此类推,直到不能向下访问则退后最近一个顶点,若他还有为访问的邻接结点,则访问
递归算法,需要借助栈实现
空间复杂度O(v) 时间复杂度 用邻接表O(v+e) 用邻接矩阵O(v^2)
//深度优先
bool visited[max];
void DFSTraverse(Graph G){
for(int i=0;i<G.vnum;i++){
visited[i]=false;
}
for(int j=0;j<G.vnum;j++){
if(!visited[j])
DFS(G,j);
}
}
void DFS(Graph g,int j){
visit(j);
vistted[j]=true;
//邻接表
for(p=G.vertices[v].firstarc;p;p=p->nextarc){ //访问该顶点连接的链表
w=p->adjvex;
if(visited[w]==false){ //未被访问过
DFS(G,w);
}
}
//邻接矩阵
for(w=0;w<G.vnum;w++){ //访问邻接矩阵该顶点对应行
if(G.edge[j][w]==1&&visited[w]==false){ //未被访问过
DFS(G,w);
}
}
}
深度优先生成树和生成森林:连通图为生成树,否则为生成森林
判断图的连通性:若图为连通的,访问一个顶点就能遍历整个图
!!!极大连通子图为无向图的连通分量,包含所有边;极小连通子图为生成树,只包含所有顶点,要求边最少
8.5 图的应用
8.5.1 最小生成树 MST
连通图的生成树包含图的所有顶点并且尽可能少的边。
对于生成树,砍掉一条边会变成非连通图,添加一条边会形成环。
最小生成树求带权值的图。
若图中有权值相同的边,最小生成树可能不唯一,但权值之和唯一且最小;若无向连通图本身的边数比顶点数少1,则最小生成树就是他本身。
1.Prim 普里姆算法(按点)
时间复杂度O(n^2),适合边稠密
2.Kruskal算法(按边)
时间复杂度O(e log2 e) 适合边稀疏但顶点多
8.5.2 最短路径
有向图时,最短路径为各边权值之和
8.5.2.1 Dijkstra 迪杰斯特拉求单源最短路径
时间复杂度O(n^2),不适合负权值(可以求有回路的带权图,也可求任意两个顶点)
需要三个辅助数组:
final[]:标记该顶点是否已找到最短路径,初始化为-1
dist[]:记录到原点最短路径,初始为∞
path[]:记录前驱结点,初始化为-1
1.写出各顶点到初始顶点a的距离x,更新dist[]的同时将path[]改为初始顶点(dist[]没更新的path[]也不动)
2.找出final[]中标记为-1的所有顶点中dist[]对应最小的,将他的final[]更新为1,记该顶点为y,更新该顶点的path[]
3.计算所有顶点到y直接距离,若存在且比dist[]小,则更新dist[]并将path[]变为y
4.重复3
8.5.2.2 Floyd 弗洛伊德求每对顶点间最短路径??
时间复杂度O(n^3),适合负权值,但不适合有回路的负权值
一个n阶方阵,每次将一个顶点作为中间结点
一开始A^(-1)表示不可以有中转点,然后是允许在a1中转,允许在a1,a2中转…
8.5.3 有向无环图
一般用来表示表达式,不可能出现重复的操作数顶点
画表达式有向无环图步骤:
1.把各操作数不重复排成一排
2.标出各运算符操作顺序
3.按顺序加入运算符,注意“分层”
4.从底向上逐层检查同层的运算符是否可以合体
8.5.4 拓扑排序
AOV网:有向无环图中,用顶点表示活动,边无权值。
拓扑排序:每个顶点只出现一次,且若A在B前面,则不可能出现B到A的路径;每一个AOV网可以有多个拓扑排序。
拓扑排序:
1.从AOV网中选择一个没有前驱(入度为0)的顶点并输出
2.从网中删除该结点和以他为起点的所有边
3.重复以上步骤直到AOV网为空,若此时网中不存在无前驱结点且网不为空,则说明存在环
bool TopologicalSort(Graph G){
InitStack(s);
int i;
for(int i=0;i<G.vnunm;i++){
if(indegree[i]==0)
Push(S,i); //入度为0,入栈
}
int count=0;//记录加入序列顶点数
while(!IsEmpty(S)){ //栈非空,就是有入度为0的元素
Pop(S,i);
print[count++]=i;//输出
for(p=G.vertices[i];p;p=p->nextarc){
v=p->adjvex;
indegree[x]--;
if(indegree[v]==0)
Push(S,v);
}
}
if(count<G.vnum)
return false;
else
return true;
}
逆拓扑排序:
1.从AOV网中选择一个没有后继(出度为0)的顶点并输出
2.从网中删除该结点和以他为终点的所有边
3.重复以上步骤直到AOV网为空
时间复杂度:邻接表O(n+e),邻接矩阵O(n^2)
从入度为0的顶点开始,若一个顶点有多个直接后继,拓扑排序结果可能不唯一
邻接矩阵为三角矩阵的有向图一定存在拓扑序列,不为三角矩阵则可能存在可能不存在(若拓扑序列有序,则一定为三角矩阵)
拓扑序列唯一,没办法直接确定一个图
8.5.5 关键路径
带全有向图,顶点表示事件,有向边表示活动,边上的权值表示活动的开销,这种用边表示活动的网称为AOE网
AOE网的性质:
1.只有某顶点所代表的事件发生后,从该顶点出发的有向边所代表的活动才能开始
2.只有进入某顶点的各有向边所代表的活动都结束时,该顶点所代表的事件才能发生
3.AOE网仅有一个入度为0的顶点作为开始顶点(源点),只有一个出度为0的顶点作为结束顶点(汇点),表示工程的开始和结束
关键路径和关键活动:AOE网中,有些路径是可以同时进行的,从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径,关键路径上的活动称为关键活动。
求关键路径:
Vk:顶点事件
ai:边活动
Ve:事件最早发生时间,必须前面所有活动都进行完
Vl:事件最迟发生时间,必须保证不影响后继事件和整个工程的完成时间,可以在逆拓扑排序基础上计算
e(i):活动最早发生时间,与该弧为起点事件最早发生时间相同
l(i):活动最迟发生时间,该弧的终点事件最迟发生时间-边权值
d(i):l(i)-e(i),时间余量,为0的活动为关键活动,路径上的事件为关键事件
步骤:
1.从源点出发,求各顶点最早发生时间Ve()
2.从汇点出发,按逆拓扑排序求各顶点最迟发生时间Vl()
3.根据各顶点Ve()求所有弧最早开始时间e()
4.根据各顶点Vl()-弧权值,求所有弧最迟开始时间l()
5.求d()=l()-e()
加快关键活动可缩短整个工程时间,但要防止关键活动变成非关键活动
关键路径不唯一时,只缩短一条路径的关键活动不能缩短整个工程时间
8.6 图各种算法时间复杂度
9. 查找
9.1 线性查找
9.1.1 顺序查找
可以是顺序表,也可以是链表,链表只能是顺序查找
无序:
1.从前往后查找,记录查找长度,查找成功返回下标,没找到时返回失败信息
2.哨兵,把查找关键字放在第一个位置设置为哨兵,从后往前找,不用记录查找长度,当返回下标为0时说明没有找到
//哨兵
typedef struct{ //顺序表
ElemType *data;
int len;
}SSTable;
int Search_Seq(SSTable ST,Elemtype key){
ST.elem[0]=key;
for(int i=ST.len;ST.elem[i]!=key;--i);
return i;
}
有序:不必查找整个表,查到元素大于自身即可
可以画判定树,圆形为成功,方形为查找失败
查找不成功:(1+2+3+…+n+n)/(n+1)=n/2+n/(n+1)
无序时失败为:n+1
有序无序成功ASL:(n+1)/2
有序的线性查找可以是链表,折半查找只能是顺序表
时间复杂度n
9.1.2 折半查找
二分查找,仅适合顺序表
元素有序,1.将给定key与表中间元素比较,成功则返回;否则说明在前半部分或后半部分。
int Binary_Search(SSTable L,ElemType key){
int low=0,high=L.len-1,mid;
while(low<=high){
mid=(low+high)/2; //默认向下取整
if(L.elem[mid]==key)
return mid;
else if(key<L.elem[mid])//在前半部分
high=mid-1;
else //在后半部分
low=mid+1;
return -1;
}
}
折半查找判定树为平衡二叉树,向一个方向偏(圆形查找成功,方形查找失败):
ASL不会超过树的高度:log2(n+1)向上取整
图上查找成功ASL=(1 * 1+2 * 2+3* 4+4 *4)/11=3
失败ASL=(3* 4+4 * 8)/12=11/3
时间复杂度O(lon2 n)
9.1.3 分块查找
索引顺序查找,块间有序,块内无序
建立一张索引表,每个分块一个结点,结点中含有每一块最大的关键字以及第一个元素地址
//索引表
typedef struct{
ElemType max;
int low;
}Index;
//顺序表
ElemType List[100];
块间用折半查找,块内用顺序查找
最理想分块块长 根号n
9.2 树形查找
9.2.1 二叉排序树
动态树表,不是为了方便排序,而是方便查找和插入删除
左子树上所有节点 < 根 < 右子树上所有节点,所有结点都为一棵二叉排序树
对二叉排序树进行中序遍历可得到有序序列
//二叉排序树非递归查找
BSTNode BST_Search(BiTree T,int key){
while(T!=null && key!=T->data){
if(key<T->data) //在左子树
T=T->lchild;
else //在右子树
T=T->rchild;
}
return T;
}
//插入,若结点为空,则插入成功;若不为空但key等于data,则插入失败;若key<data,插入到左子树;若key>data,插入到右子树
int BST_Insert(BiTree &T,int key){
if(T==null){ //插入成功
T=(BiTree)malloc(sizeof(BSTNode));
T->data=key;
T->lchild = T->rchild =null;
return 1;
}
else if(T->data==key){ //已存在,插入失败
return 0;
}
else if(T->data<key){ //插入到左子树
return BST_Insert(T->lchild,key);
}
else{ //插入到右子树
return BST_Insert(T->rchild,key);
}
}
//构造,从一棵空树出发
void Creat_BST(BiTree &T,int str[],int len){
T=null;
int i=0;
while(i<len){
BST_Insert(T,str[i]);
i++;
}
}
//删除
1.若删除结点没有左右子树,直接删除
2.若删除结点只有左子树或只有右子树,让左(右)子树代替该结点
3.若删除结点同时拥有左右子树,让左子树上的直接前驱(右子树上的直接后继)代替该结点,其他不变
二叉排序树ASL:若为平衡二叉树,最好可以为long2 n;最差n
折半查找针对静态情况,若需要动态插入删除,可使用二叉排序树
9.2.2 平衡二叉树
AVL树,元素也是有序的,左右子树高度差不超过1,每一个结点都为一棵平衡二叉树;
平衡因子:左右子树高度差,只可能为-1,0,1;
当插入(删除)元素导致树不平衡后,需要再次调整为平衡二叉树
//插入元素后导致不平衡,找到不平衡的结点,看是在哪个子树上插入结点导致的
1.LL型(右单旋,以左孩子为基准),在左子树的左子树插入结点导致
2.RR型(左单旋,以右孩子为基准),在右子树的右子树插入结点导致
3.LR型(先左旋后右旋,以左孩子的右孩子为基准),在左子树的右子树插入结点导致
3.RL型(先右旋后左旋,以右孩子的左孩子为基准),在右子树的左子树插入结点导致
//删除元素
1.先按照二叉排序树的删除方法,删除后要找直接前驱或直接后继代替
2.按插入后不平衡的四种方法重新调整
!!3.从最小不平衡结点开始往上,一层一层看会不会有不平衡
平衡二叉树平均查找长度:log2 n
9.2.3 红黑树
保持了AVL树的平衡性,又可以方便插入删除
性质(左根右,根叶黑,不红红,黑路同)
1.每个结点都是红色或黑色的
2.根结点是黑色的
3.叶节点(null结点)是黑色的
4.不存在两个相邻的红色结点(红结点的父结点和孩子结点都是黑色的)
5.每个结点到叶子结点的路径上,黑结点数量相同
黑高:从某结点到叶结点黑结点的数量
结论1:从根结点到叶子结点最长路径不大于最短路径的两倍
结论2:有n个内部结点(不包含叶结点)的高度h<=2log2 (n+1)
插入:
1.为根,染黑
2.不为根,染红
2.1 满足红黑树性质,不用调整
2.2 不满足性质
黑叔:旋转+染色
LL(看新结点):右单旋,父换爷(这里跟AVL树旋转一样)+染色
RR:左单旋,父换爷(这里跟AVL树旋转一样)+染色
LR:左、右双旋,儿换爷(这里跟AVL树旋转一样)+染色
RL:右、左双旋,儿换爷(这里跟AVL树旋转一样)+染色
红叔:染色+变新
叔父爷染色,爷变为新结点
9.2.4 B树,B+树
B树
所有结点都有记录信息
m叉查找树:结点多少有多少个分叉就是多少叉查找树,最多有m个分叉,有m-1个关键字,每个结点关键字有序,关键字的查找可以用顺序查找也可以用折半查找
B树:m阶B树就是所有结点的平衡因子均等于0的m路平衡查找树
m阶B树:每个结点最多m棵子树,m-1个关键字;最少(m/2向上取整)棵子树,(m/2向上取整-1)个关键字;若根节点不是叶子结点,至少有2棵子树,1个关键字
非叶子结点的结构(key表示关键字,p表示指针):p0,key1,p1,key2,keyn,pn
所有叶子结点都在同一层并且不带信息
n个关键字的m阶B树,应有(n+1)个叶结点(查找失败的结点)
5阶B树如下:
B树的高:不包含外部结点,h>=logm(n+1)
1.查找
B树的查找与二叉排序树类似,只是每个结点是多个关键字的有序表;查找包括在B树上查找结点(在磁盘进行,B树通常存储在磁盘上,有几层就要查找几次磁盘,再把结点读入内存),和在结点上查找关键字(在内存中顺序或折半查找)
2.B树的高:不包含外部结点,满:h>=logm(n+1),n<=m^h-1;不满:h<=log(m/2) [(n+1)/2+1]
3.插入
把结点插入到最后一层,看是否需要分裂(若插入后结点的关键字数量大于(m-1),则需要分裂),分裂:在该结点第(n/2向上取整)个结点上升到原本父节点中,结点分裂成两个分支,在看上一层是否需要分裂,依次类推
4.删除
若删除的关键字不是在最底层,需要找到直接前驱(或直接后继)代替他,再删除关键字,若在最底层,直接删除
删除后结点关键字可能不足,需要合并:1.兄弟够借,把兄弟结点的关键字上升到上一层,把上一层一个结点拿下来,2.兄弟不够借,把兄弟,直接从父节点拿一个关键字下来,3.继续向上调整
B树插入后分裂:
B树删除后合并:
B+树
应对数据库所需,只有最后一层的结点有信息,类似于分块查找
m阶B+树,每个结点最多m个关键字,m个子树(一个关键字对应一棵子树),最少(m/2向上取整)关键字和子树,所有叶子结点在最后一层,用指针链接起来,非叶子结点的关键字代表下一层的最大关键字
9.2.5 散列查找
散列函数:Hash(key)=addr()
冲突:把两个或两个以上映射到同一地址
可以是链表
散列函数的构造:
覆盖全部关键字,尽可能均匀分布,减少冲突,简单,快速计算
1.直接定址法:线性函数,H(key)=a*key+b
简单不冲突,适合分布连续
2.除留取余法:设p为最接近表长的质数,H(key)=key%p
最简单常用
3.数字分析法
4.平方取中法
处理冲突的方法:
1.开放定址法:向其他位置递推,H=(H(key)+di)%m,di为增量序列,只能逻辑删除
线性探测法:di=1.2.3...
平方探测法,二次探测法:di=1^2,(-1)^2,2^2,(-2)^2,...,k^2,(-k)^2,k<m/2,长度m必须可表示成4k+3,不会引起聚集,无法探测到散列表上所有单元,至少一半
双散列法:两个散列函数
伪随机序列法:
2.拉链法,链接法:每个地址都是一个链表
根据散列函数查找地址,若地址为空,则查找失败;若不为空,对比关键字是否相等,不等时,按照处理冲突的方法找到下一个地址继续对比
冲突越多,查找效率越低
决定查找效率的三个因素:散列函数,处理冲突的方法,装填因子a
a=表中记录数/表长度,平均查找长度依赖于a,a越大越容易冲突
10. 排序
评判指标:时间复杂度,空间复杂度,算法稳定性
内部排序:关注时间,空间复杂度
外部排序:还需要关注读写磁盘的次数
算法的稳定性和算法的优劣无关
不同的排序方法得到的序列可能不一样
使用链表也可以进行排序
10.1 插入排序
每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成。
10.1.1 直接插入排序
每次从后面取出一个元素加入到前面的有序序列中,初始序列基本有序时,使用直接插入排序效率高
//对1-n排序,0不算
void InsertSort(ElemType A[],int n){
int i,j;
for(i=2;i<=n;i++){
if(a[i]<a[i-1]){
a[0]=a[i];
for(j=i-1;a[0]<a[j];j--){
a[j+1]=a[j];
}
a[j+1]=a[0];
}
}
}
空间复杂度:O(1)
时间复杂度:O(n^2)
稳定性:稳定
适用性:适用于顺序表和链表
10.1.2 折半插入排序
从前面的有序序列中折半查找元素应插入的位置,2.把该位置及以后的元素都往后挪,3.把待插入的元素
void InsertSort(int A[],int n){
int i,j,low,high,mid;
for(int i=2;i<=n;i++){
a[0]=a[i];
//折半查找
low=1,high=i-1;
while(low<=high){
mid=(low+high)/2;
if(a[0]<a[mid]) high=mid-1;
else low=mid+1;
}
//后移
for(j=i-1;j>=high+1;--j)
a[j+1]=a[j];
a[high+1]=a[0];
}
}
空间复杂度:O(1)
时间复杂度:O(n^2)
稳定性:稳定
适用性:仅适合顺序表
10.1.3 希尔排序
缩小增量排序,每次用直接插入排序
取增量为d1进行分组排序,再取增量为d2分组排序,,,(d1>d2>d3…)
稳定性:不稳定
适用性:仅适合顺序表
空间复杂度:O(1)
10.2 交换排序
交换,根据序列中两个关键字的比较结果对换这两个记录在序列中的位置
10.2.1 冒泡排序
从后往前(或从前往后)两两比较相邻元素的值,若为逆序,则交换他们,称为一趟冒泡排序,此时最小元素会冒到最前面;若一趟冒泡排序后没有发生交换,则元素已有序
//从后往前
void swap(int &a,int &b){
int temp=a;
a=b;
b=temp;
}
void BubbleSort(int A[],int n){
for(int i=0;i<n-1;i++){
bool flag=false;
for(int j=n-1;j>i;j--){
if(A[j]<a[j-1]){
swap(a[j-1],a[j]);
flag=true;
}
}
if(flag==true)
return;
}
}
空间复杂度:O(1)
时间复杂度:O(n^2)
稳定性:稳定
适用性:顺序表和链表都适用
10.2.2 快速排序
从一个无序表L[1,2,n]中选择一个元素pivot作为枢轴(或基准,通常选首元素),经过一趟快速排序后L[1,2,k-1,k(pivot),k+1,n];pivot落在最终位置上,左边元素都小于pivot,右边元素都大于pivot
//一次划分
int Partition(int A[],int low,int high){
int pivot=a[low]; //取第一个元素作为枢轴
while(low<high){
while(low<high && A[high]>=pivot) --high;
A[low]=A[high];
while(low<high && A[low]<=pivot) ++low;
A[low]=A[high];
}
A[low]=pivot;
return low;
}
//快速排序
void Qsort(int A[],int low,int high){
if(low<high){
int p=Partition(A,low,high);
Qsort(A,low,p-1); //对左半部分进行排序
Qsort(A,p+1,high);//对右半部分进行排序
}
}
时间复杂度:O(nlog2 n)
空间复杂度:O(log2 n)
稳定性:不稳定
适用性:仅使用顺序表
快速排序在初始元素基本有序的情况下不利发挥长处
最好情况下元素比较次数:每次都把第一个枢轴元素放到中间,对比次数为该元素与后面所有元素对比
10.3 选择排序
每次从待选择元素中选择最小的元素加入有序序列
10.3.1 简单选择排序
每次从后面选择一个最小元素放到前面,n个元素共进行n-1趟
void SelectSort(int A[],int n){
for(int i=0;i<n-1;i++){
int min=i;
for(int j=i;j<n-1;j++){
if(A[j]<A[min])
min=j;
}
if(min!=i)
swap(A[i],a[min]);
}
}
空间复杂度:O(1)
时间复杂度:O(n^2)
稳定性:不稳定
适用性:顺序表和链表
10.3.2 堆排序
L[1,2,3,n]中,大根堆:任意根>=左右孩子,即L[i]>=L[2i]且L[i]>=L[2i+1];小根堆:任意根<=左右孩子,即L[i]<=L[2i]且L[i]<=L[2i+1]
!!!大根堆获得递增序列,小根堆获得递减序列
建立大根堆:
从下往上把所有非终端结点检查一遍,看是否符合大根堆需求(顺序存储的二叉树中,非终端结点的编号i<=(n/2))
判断是否满足根>左右,如果不满足,选择左右孩子中较大的与根交换位置
若影响下一级,同样的方法向下调整(小元素不断下坠)
大根堆排序:
每一趟在待排序元素中选取关键字最大的元素加入有序子序列
将堆顶元素(最大)加入有序序列(与堆尾元素交换位置)
将待排序列调整为大根堆(小元素不断下坠)
大根堆得到的序列会是增序
插入:插入到堆尾,逐层上升
删除:被删除元素用堆尾代替,逐层下降
关键字对比:上升只需要对比一次,下降可能需要对比两次
建堆时间复杂度:O(n)
空间复杂度:O(1)
时间复杂度:O(nlog2 n)
稳定性:不稳定
适用性:仅适用顺序表
10.4 二路归并排序
每次将两个或两个以上的有序表合成一个,直到整个表有序
//将两个有序表A[low,,,mid],A[mid+1,,,high]合并成一个有序表
int *B=(int *)malloc((n+1)*sizeof(int));//构造辅助数组B
void Merge(int A[],int low,int mid,int high){
int i,j,k;
for(k=low;k<=hogh;k++) //把数组A复制到B
B[k]=A[k];
for(i=low,j=mid+1,k=i;i<=mid,j<=high;k++){
if(B[i]<=B[j])
A[k]=B[i++];
else
A[k]=B[j++];
}
while(i<=mid) A[k++]=B[i++];//第一个表为检测完
while(j<=high) A[k++]=B[j++];//第二个表为检测完
}
void MergeSort(int A[],int low,int high){
if(low<high){
int mid=(low+high)/2;//从中间划分为两个子序列
MergeSort(A,low,mid); //递归调用
MergeSort(A,mid+1,high);
Merge(A,low,mid,high); //归并
}
}
二路归并排序形态上是一棵倒立的二叉树
空间复杂度:O(n)
时间复杂度:O(nlog2 n)
稳定性:稳定
适用性:顺序表,链表
10.5 基数排序
个-十-百
不基于比较和移动进行排序,而基于关键字各位大小进行排序
假设待排序序列已r为基数,每轮排序需要使用r个初始队列,每轮收集完后将每个队列连起来;共d轮
空间复杂度:O®
时间复杂度:O(d(n+r))
稳定性:稳定
适用性:顺序表,链表
10.6 计数排序
对每个待排序元素x,统计小于x的个数,得到x的最终位置
输入数组A
输出数组B
存储计数值
10.7 内部排序总结
选择排序,归并排序比较次数与初始序列无关
n比较小,直接插入或简单选择排序
基本有序:直接插入排序或冒泡排序
10.8 外部排序
需要对外存中的大文件进行排序,由于文件中的记录很多,无法将整个文件复制到内存中进行排序,排序时需要一部分一部分读入内存,排序过程中需要多次进行内存与外存的交换。
外部归并:将外存中几个归并段读入内存,合并为一个归并段后再写回外存;合并好的归并段再次进行合并
外部排序时间=内部排序时间+读写外存时间+内部归并时间
优化思路,减少平衡归并中对外存的读写:增大归并路数、减少归并段个数
对 r 个初始归并段,做k路归并,则归并树可用 k 叉树表示,若树高为h,则归并趟数 = h-1 = ⌈logkr⌉
败者树:增大归并路数
选择-置换排序(生成初始归并段):通过增大归并段长度减少归并段个数
最佳归并树:长度不等的归并段进行多路平衡归并
10.8.1 多路平衡归并
可以减少归并趟数,但是每次从k路中选择一个元素需要k-1次对比,可以利用败者树减少对比次数
m路归并表示最多一趟可以有m路,而m路平衡归并代表每一趟都要有m路
10.8.2 败者树
一次多路进行归并,目的是从一次从m路中选出最大或者最小,减少对比次数
叶子结点代表参加比较的元素;非叶子结点代表本轮输的元素;根节点代表最终胜者
当3号归并段的元素1最小先被选出,其后从3号归并段元素6加入,先和4号对比,再和2号对比,接着和5号对比,但大于5号的元素2,所以第二个选出的是元素2
对比次数为树的高度
10.8.3 选择-置换排序
生成初始归并段,可以让每个初始归并段的长度超过内存工作区大小的限制
待排序文件为F
输出文件为FO
内存工作区为WA
FO和WA的初始状态为空,WA可容纳w个记录。
1.从FI输入w个记录到工作区WA
2.从WA中选出其中关键字取最小值的记录,输出到FO中,并记为MINIMAX
3.若FI不空,则从FI输入下一个记录到WA中
4.从WA中所有关键字比MINIMAX大的记录中选出最小关键字记录,输出到FO中,并作为新的MINIMAX记录
5.重复3.4.直至在WA中选不出新的MINIMAX记录为止,由此得到一个初始归并段,输出一个归并段的结束标志到FO中去
6.重复2.3.4.5.直至WA为空。由此得到全部初始归并段。
WA中选择MINIMAX需要利用败者树来实现
10.8.4 最佳归并树
选择置换排序会得到长度不等的初始归并段,最佳归并树用于让这些初始归并段归并排序
归并过程中磁盘I/O次数=归并树的WPL*2,结点权值为初始归并段长度
要让磁盘I/O次数最小,就要使归并树的WPL最小即构建一个k叉哈夫曼树,也就是k叉归并树
若初始归并段的数量无法构成严格的k叉归并树,需要补充几个长度为0的“虚段”
若(初始归并段数量-1)%(k-1)=0,说明刚好可以构成严格k叉树,此时不需要添加虚段
若(初始归并段数量-1)%(k-1)=u≠0,则需要补充 (k-1)-u个虚段,也就是使上面等式等于0
绿色结点为每个初始归并段的长度
多路平衡归并的目的是减少归并趟数