2023 数据结构笔记
提示:本笔记为作者在大一学习所记,笔记不全,陆续更新
文章目录
一、绪论
1、什么是数据构
2、算法效率的评价
二、线性表
1、线性表的建立
struct seqlist {
int data[MAX_SIZE];
int length;
};
2、线性表的插入,删除,查找,遍历,倒置
int Locate(seqlist& L, int y)
{
int i;
for (i = 0; i < L.length; i++)
if (L.data[i] == y) {
cout << "需要查找的数据在第" << i + 1 << "位" << endl;
break;
}
if (i == L.data[L.length]) {
cout << "查找失败" << endl;
return Fail;
}
else{
cout << "查找成功" << endl;
return OK;
}
}
void inse(seqlist& L, int z, int j)
{
int i;
for (int i = L.length; i >= j; i--) {
L.data[i] = L.data[i - 1];
}
L.data[j - 1] = z;
L.length++;
for (i = 0; i < L.length; i++)
cout << L.data[i]<<" ";
cout << endl;
}
void Traverse(seqlist& L)
{
int i;
for (i = 0; i < L.length; i++)
cout << L.data[i] << " ";
cout << endl;
}
void Reverse(seqlist& L)
{
int t,i;
for (int i = 0; i < L.length / 2; i++) {
t = L.data[i];
L.data[i] = L.data[L.length - i - 1];
L.data[L.length - i - 1] = t;
}
for (i = 0; i < L.length; i++)
cout << L.data[i] << " ";
cout << endl;
}
void Delete(seqlist& L, int x) {
for (int i = x - 1; i < L.length ;i++)
{
L.data[i] = L.data[i + 1];
}
L.data[L.length] = 0;
L.length--;
Traverse(L);
}
3、链表的建立
//定义结构
typedef struct list
{
int data;
struct list* next;
}LNode;
4、链表的创建,打印,删除,插入,查找
//创建
LNode* creat()
{
LNode* H, * p, * q;
H = (LNode*)malloc(sizeof(LNode));
q = H;
p = (LNode*)malloc(sizeof(LNode));
cin >> p->data;
while (p->data)
{
q->next = p;
q = p;
p = (LNode*)malloc(sizeof(LNode));
cin >> p->data;
}
q->next = NULL;
q->next = NULL;
return H;
}
//打印操作
void print(LNode* h)
{
LNode* p;
p = h->next;
while (p)
{
cout << p->data << " ";
p = p->next;
}
}
//插入操作
LNode* inse(LNode* h, int x)
{
LNode* p, * q, * r;
int y;
p = h->next;
while (p->data != x)
p = p->next;
q = p->next;
cin >> y;
r = (LNode*)malloc(sizeof(LNode));
r->data = y;
r->next = q;
p->next = r;
return h;
}
//查找
int locate(LNode *h,int x)
{
LNode *p;
p=h;
while(p&&p->data!=x)
p=p->next;
if(p)
return 1;
return 0;
}
4,双向链表的创建
typedef struct list
{
int data;
struct list *next;
struct list *prior;
}LNode;
5,双向链表的打印,删除,插入,查找
LNode *creat()
{
LNode *H,*p,*q;
H=(LNode *)malloc(sizeof(LNode));
H->prior=NULL;
q=H;
p=(LNode *)malloc(sizeof(LNode));
cin>>p->data;
while(p->data)
{
q->next=p;
p->prior=q;
q=p;
p=(LNode *)malloc(sizeof(LNode));
cin>>p->data;
}
q->next=NULL;
return H;
}
void print(LNode *h)
{
LNode *p;
p=h->next;
while(p)
{
cout<<p->data<<" ";
p=p->next;
}
}
LNode *dele(LNode *h,int x)
{
LNode *p,*r;
p=h;
while(p->next->data!=x)
p=p->next;
if(p->next->data==x && p->next->next==NULL)
{
r=p->next;
p->next=NULL;
free(r);
}
else
{
r=p->next;
r->next->prior=p;
p->next=r->next;
free(r);}
return h;
}
LNode *inse(LNode *h,int x)
{
LNode *p,*q,*r;
int y;
p=h->next;
while(p->data!=x)
p=p->next;
q=p->next;
cin>>y;
r=(LNode *)malloc(sizeof(LNode));
r->data=y;
r->next=q;
r->prior=p;
p->next=r;
return h;
}
三、栈和队列
1、定义
1、栈的定义
typedef struct ENode
{
char data;
struct ENode *next;
} Node, *Stack;
2、栈的功能实现
void Init(Stack &s) //初始化
{
s = NULL;
}
void Destory(Stack &s) //销毁
{
Stack p;
while(s != NULL)
{
p = s->next;
delete s;
s = p;
}
}
void Clear(Stack &s) //清空
{
Destory(s);
}
int Empty(Stack s) //判空
{
return s == NULL;
}
int Length(Stack s) //长度
{
int k = 0;
while(s != NULL)
{
k ++;
s = s->next;
}
return k;
}
char GetTop(Stack s)
{
return s->data;
}
void Traverse(Stack s) //遍历
{
while(s != NULL)
{
cout<<s->data<<" ";
s = s->next;
}
}
void Push(Stack &s, char e) //出栈
{
Stack t = new Node;
t->data = e;
t->next = s;
s = t;
}
int Pop(Stack &s, char &e) //入栈
{
Stack p = s;
if(s == NULL) return 0;
e = s->data;
s = s->next;
delete p;
return 1;
}
3、队列定义
typedef struct
{
char *elem;
int front;
int rear;
} SqQueue;
4,队列功能的实现
int Init(SqQueue &q)
{
q.elem = new char[MAXSIZE];
if(q.elem == NULL)
return 0;
q.front = q.rear = 0;
return 1;
}
int Length(SqQueue q)
{
return (q.rear-q.front+MAXSIZE)%MAXSIZE;
}
int Empty(SqQueue q)
{
return q.rear == q.front;
}
int Full(SqQueue q)
{
return (q.rear + 1) % MAXSIZE == q.front;
}
char GetHead(SqQueue q)
{
return q.elem[q.front];
}
char GetTail(SqQueue q)
{
return q.elem[(q.rear-1+MAXSIZE)%MAXSIZE];
}
int EnQueue(SqQueue &q, char e)
{
if(Full(q))
return 0;
q.elem[q.rear] = e;
q.rear = (q.rear + 1) % MAXSIZE;
return 1;
}
int DeQueue(SqQueue &q, char &e)
{
if(Empty(q))
return 0;
e = q.elem[q.front];
q.front = (q.front+1) % MAXSIZE;
return 1;
}
四,串、数组、广义表
1,串
基本概念:
串(string):由零个或多个字符组成的有限序列。
串长:组成串的字符个数。
空串:零个字符的串。
主串、子串、位置
子串:串中连续多个字符组成的子序列。
主串:包含子串的串。
位置:字符(或子串)在串中的序号(从1开始)。
代码演示:
顺序存储(静态分配)
#define MAXLEN 255
typedef struct {
char ch[MAXLEN];
int length;
} SString;
顺序存储(动态分配)
typedef struct {
char *ch;
int length;
} HString;
链式存储
#define CHUNKSIZE 80
typedef struct CHUNK {
char ch[CHUNKSIZE];
struct CHUNK *next;
} LString;
模式匹配算法
串匹配(模式匹配)算法:子串的定位算法。
BF算法(Brute-Force)
(1) 分别用i、j表示主串S和模式T当前正待比较的
字符位置,i的初值为pos,j的初值为1
(2) 如果S和T均未到串尾(i<=S.length&&j<=T.length)
则循环如下操作:
① 如果S.ch[i]等于T.ch[j],则i和j均增一;
② 否则,i回退到i-j+2,j回退到1
(3) 如果j>T.length,则匹配位置为i-T.length
(或i-j+1),否则返回0
代码:
BF算法(Brute-Force)
int Index_BF(SString S, SString T, int pos) {
int i = pos, j = 1;
while(i <= S.length && j <= T.length) {
if(S.ch[i] == T.ch[j]) {
i ++; j ++;
}
else {
i = i – j + 2; j = 1;
}
}
return j > T.length ? i - T.length : 0;
}
最好时间复杂度O(n+m),最坏时间复杂度O(n*m)
2,数组
二维数组——可看做数据元素时线性表的线性表。可以是行向量形式的线性表,可以是列向量形式的线性表。
n维数组可定义为一位数组,其每个元素都是一个n-1维数组。
3,广义表
例:
(1)A=( ) 空表,长度为0
(2)B=(e) 只有一个原子e,长度为1
(3)C=(a, (b, c, d)) 有两个元素:原子a、子表(b, c, d)
(4)D=(A, B, C) 有3个子表,长度为3
(5)E=(a, E) 长度为2,是一个递归的表
LS=(a1, a2,······,an)长度 n
每个 ai 1≤i≤n 或是一个元素(原子),
或是一个子广义表。
a1是表头head, a2,······,an 是表尾。
用小写字母表示原子,大写字母表示广义表。
广义表的存储
头尾链表
表结点:tag=1、hp(表头指针)、tp(表尾指针)
原子结点:tag=0、stom(原子数据)
扩展线性表
表结点:tag=1、hp(表头指针)、tp(直接后继指针)
原子结点:tag=0、stom(原子数据)、tp(直接后继指针)
五、树和二叉数
1、树
树(Tree):n(n≥0)个结点的有限集合,或为空树(n=0),或为非空树,对于非空树:
(1)有且仅有一个结点称为“根结点”;
(2)除根结点以外的其余结点分为m(m>0)个不相交的有限子集,每个子集又是一棵树(称为子树
节点的度:一个节点含有的子树的个数称为该节点的度;
叶节点:度为0的节点称为叶节点;
分支节点:度不为0的节点;
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
兄弟节点:具有相同父节点的节点互称为兄弟节点;
树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6;
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次;
堂兄弟节点:双亲在同一层的节点互为堂兄弟;
节点的祖先:从根到该节点所经分支上的所有节点;
森林:由m(m>0)棵互不相交的树的集合称为森林;
2、二叉树
二叉树(Binary Tree):n(n≥0)个结点的有限集合,或为空树(n=0),或为非空树,对于非空树:
有且仅有一个结点称为“根结点”;
除根结点以外的其余结点分为两个不相交的有限子集,每个子集又是一棵二叉树(分别称为左子树、右子树)。
二叉树的第i层(i≥1)至多有2i-1个结点。
深度为k(k≥1)的二叉树至多有2k-1个结点。
对于二叉树,设叶子结点个数为n0,度为2的结点个数为n2,则n0=n2+1
满二叉树:深度为k且有2k-1个结点的二叉树。满二叉树可以对结点自上而下,从左向右顺序编号。
完全二叉树:深度为k结点个数为n的二叉树,当且仅当每一个结点都于深度为k的满二叉树编号从1到n的结点一一对应时,称为完全二叉树。
具有n个结点的完全二叉树的深度为 。
完全二叉树:
对于完全二叉树编号为i的结点:
如果i=1,则该结点为根结点;
如果i>1,则i号结点的双亲结点编号为i/2取整;
如果2i>n,则i号结点是叶子结点,无左孩子;否则其左孩子编号为2i;
如果2i+1>n,则i号结点无右孩子;否则其右孩子编号为2i+1。
遍历:
遍历二叉树:按某种路径巡访树中每个结点(均被访问且仅被访问一次)。
遍历的三个阶段:访问根结点、遍历左子树、遍历右子树。
依据不同排列顺序,分为三种基本的遍历算法:
先序遍历
中序遍历
后序遍历
先序遍历:如果二叉树非空,则
访问根结点
先序遍历左子树
先序遍历右子树
递归算法:
void PreOrder(BTree T) {
if(T) {
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
中序遍历:如果二叉树非空,则
中序遍历左子树
访问根结点
中序遍历右子树
递归算法:
void InOrder(BTree T) {
if(T) {
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
后序遍历:如果二叉树非空,则
后序遍历左子树
后序遍历右子树
访问根结点
递归算法:
void PostOrder(BTree T) {
if(T) {
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
按层次遍历:对于非空二叉树
(1)初始化空队列
(2)将根结点指针入队
(3)当队列非空,则循环:
将一个结点指针出队,并访问该结点
如果其左孩子非空,则将其左孩子指针入队
如果其右孩子非空,则将其右孩子指针入队
(4)销毁队列
按层次遍历:
void LevelOrder(BTree T) {
if(T == NULL) return;
Queue q;
InitQueue(q); EnQueue(q, T);
while(!QueueEmpty(q)) {
BTree p = DeQueue(q);
visit(p);
if(p->lchild != NULL) EnQueue(p->lchild);
if(p->rchild != NULL) EnQueue(p->rchild);
}
}
3、森林与二叉树的转换
4、哈夫曼树
哈夫曼树(最优树):带权路径长度最小的树。
路径:从一个结点到另一个结点之间的分支。
路径长度:路径上的分支数目。
树的路径长度:从根到每一结点的路径长度之和。
权:赋予某一实体的量,描述实体的某个或某些属性。有结点权和边权两类,含义取决于具体应用。
结点的带权路径长度:该结点到根的路径长度 * 结点权。
树的带权路径长度:所有叶子结点的带权路径长度之和。
哈夫曼树:假设有n个权值{w1, w2, …, wn},可以构造含n个叶子的二叉树,每个叶子的权为wi,则其中带权路径长度WPL最小的二叉树就是哈夫曼树。
最优二叉树不存在度为1的结点。
构造哈夫曼树
基本思路:
(1)根据给定的n个权值,构造一个森林(含n棵只有根结点的二叉树,n个权值依次赋予这些二叉树的根);
(2)在森林中选择根的权值最小的2棵二叉树,分别作为左右子树,构造一个双亲(权值为左右孩子的权值之和),合并为一棵新的二叉树;
(3)重复第(2)步,直到森林中仅包含一棵二叉树为止。这棵二叉树即为哈夫曼树。
算法实现:
存储结构(有n个叶子):
(1)采用顺序存储结构,数组元素数为2n,下标为0的元素不用,实际元素数2n-1,下标1~n的元素依次存放n个叶子,下标n+1~2n的元素依次存放后构造的分支结点。
(2)结点结构
typedef struct {
int weight; // 存结点的权值
int parent, lchild, rchild;
// 存双亲、左孩子、右孩子的下标
} HTNode, *HuffmanTree;
算法步骤:
(1)初始化:
申请2n个结点构成数组,将1~2n-1号结点的parent、lchild、rchild赋值为0;将n个叶子结点的权值依次输入1~n号结点的weight域,这n个叶子构成n棵树的森林。
(2)构造树:
构造n-1个分支结点,需循环n-1次,每次的处理:从森林中选择2个权值最小的树根(树根的parent域为0),构造一个双亲结点(在n+1~2n位置),其weight值为2个孩子的weight之和。
算法描述(n>=2):
void CreateHuffmanTree(HuffmanTree &HT, int n) {
HT = new HTNode[2*n];
for(int i = 1; i <= n; i ++) {
cin >> HT[i].weight;
HT[i].parent = HT[i].lchild = HT[i].rchild = 0;
}
for(int i = n+1; i < 2*n; i ++) {
SelectMinRoot(HT, i-1, s1, s2);
HT[s1].parent = HT[s2].parent = i; HT[i].parent = 0;
HT[i].lchild = s1; HT[i].rchild = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
}
void SelectMinRoot(HuffmanTree HT, int n, int &s1, int &s2) {
s1 = s2 = 0;
for(int i = 1; i <= n; i ++) {
if(HT[i].parent != 0) continue;
if(s2 == 0) s2 = i;
else if(s1 == 0) {
if(HT[i].weight <= HT[s2].weight) s1 = i;
else { s1 = s2; s2 = i; }
}
else if(HT[i].weight <= HT[s1].weight) {
s2 = s1; s1 = i;
}
else if(HT[i].weight < HT[s2].weight) s2 = i;
}
}
哈夫曼编码:
不定长编码的基本思想:为出现次数多的字符编以较短的编码。
前缀编码:在一个编码方案中,任意一个编码都不是其他任何编码的前缀。
例如:0、10、110、11100、11101、11110、11111是前缀编码。
例如:0、01、10、11、101、110、111不是前缀编码,会产生二义性解码。
哈夫曼编码:将哈夫曼树中的每个左分支赋予0、右分支赋予1,从根到叶子的路径上的二进制串即为该叶子的哈夫曼编码。
哈夫曼编码是最优前缀编码。
文件的编码:在哈夫曼编码表中查表进行编码(哈夫曼编码表是通过构建哈夫曼树之后获得的,算法5.10和算法5.11)。
文件的译码:需借助哈夫曼树,从根开始。读入编码串,如果是“0”则走向左孩子,否则走向右孩子,直到走到叶子,则译码出叶子对应的字符。然后再从哈夫曼树的根重新开始。
六、图
1、概念
图G由V和E组成,记为G=(V,E),V是有穷非空顶点集合,E是V中顶点偶对(边)的有穷集合(可空)。
有向图:若E(G)是有向边(弧)的集合,则G为有向图。有向图中的顶点对(弧)用<x,y>表示,其中x为弧尾,y为弧头。
无向图:若E(G)是无向边的集合,则G为无向图。无向图中的定点对用(x,y)表示。
无向完全图:有n(n-1)/2条边。
有向完全图:有n(n-1)条弧。
稀疏图:边或弧很少。稠密图:边或弧很多。
网:边或弧带权的图。
邻接点:一条边的两个端点互为邻接点。对于弧,弧头是弧尾的邻接点,弧尾是弧头的逆邻接点。
度:和一个顶点相关联的顶点个数。
入度和出度:以顶点v为弧尾的弧数是v的出度,以v为弧头的弧数是v的入度。
路径:从顶点v1到顶点v2所经历的顶点序列(含v1和v2)。路径上的边或弧的数量称为路径长度。
回路或环:起点和终点相同的路径。
简单路径:顶点不重复的路径。
简单回路:除起点(终点)以外的顶点不重复的回路。
连通:无向图中,顶点v1、v2之间存在路径,则称v1和v2是连通的。
2、邻接矩阵
邻接矩阵:用矩阵表示顶点之间的邻接关系。
对于不带权的无向图(或有向图),如果(vi,vj)或<vi,vj>存在,则邻接矩阵元素Aij=1,否则Aij=0。
对于带权的无向网(或有向网),如果(vi,vj)或<vi,vj>存在,则邻接矩阵元素Aij=wij,否则Aij= ∞。
代码:
//类型定义
#define MaxVertexNumber 100
typedef struct {
// 顶点一维数组
VertexType vertices[MaxVertexNumber];
// 邻接矩阵,表示边(弧)的二维数组
ArcType arcs[MaxVertexNumber][MaxVertexNumber];
// 顶点数、边(弧)数
int vexnum, arcnum;
} Graph;
//创建无向网的邻接矩阵
//注:顶点类型char、边类型int
void CreateGraph(Graph &G) {
cin >> G.vexnum >> G.arcnum;
for(i = 0; i < G.vexnum; i ++)
cin >> G.vertices[i];
for(i = 0; i < G.vexnum; i ++)
for(j = 0; j < G.vexnum; j ++)
G.arcs[i][j] = MaxInt;
for(k = 0; k < G.arcnum; k ++) {
cin >> i >> j >> w;
G.arcs[i][j] = G.arcs[j][i] = w;
}
}
3、邻接表
具有n个顶点的图就有n个上述的单链表。
把n个顶点的信息及邻接点单链表头指针再建立一个顺序表。
顶点表(表头结点表):顺序存储,每个元素包含:data(数据域,存顶点本身信息)、firstarc(链接域,指向与该顶点相关联的边的单链表首结点)。
边表(n个单链表):每个边结点含:adjvex(邻接点域,存邻接点序号)、info(数据域,存边本身的信息)、nextarc(链接域,存下一个边结点的指针)。
优点:
便于增删顶点。
便于统计边的树数目,时间复杂度O(n+e)。
空间效率高。
缺点:
不便于判断顶点之间是否有边。
便于计算有向图各个顶点的度。
4、图的遍历
遍历:从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次。
遍历的实质:找每个顶点的邻接点。
需注意的问题:图中可能存在回路,且图的任一顶点都可能与其它顶点相通,在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点。
避免顶点被重复访问的方法:
设置辅助数组visited,数组元素初值均为0;
当k号顶点访问过后,置visited[k]=1;
在遍历过程中,对于m号顶点,如果visited[m]=1,则不再访问m号顶点。
深度优先搜索DFS:仿树的先序(先根)遍历。
搜索策略:
深度优先:从某一顶点v出发,走向第一个未访问过的邻接点w1,再走向w1的第一个未访问过的邻接点u1,……,直到无法继续;
回退一步,如果有其他未访问过的邻接点,则从该邻接点开始继续同样的访问过程;如果没有,则继续回退。
想象一下走迷宫的一种策略:能走则走,不能走则退。
能走则走:在某个路口,如果存在未走过的岔路,则选一条走下去;
不能走则退:如果走到了死路(前方无路,或所有岔路都走过了),则回退一步继续寻找其他可走且未走过的岔路。
算法演示:
用于邻接矩阵的深度优先搜索算法:
// 注:visited数组为全局的,所有元素初值为0
// 注:visited[v]==0表示v顶点未被访问过
void DFS(Graph G, int v) {
cout << G.vertices[v];
visited[v] = 1;
for(int k = 0; k < G.vexnum; k ++) {
if(G.arcs[v][k] != 0 && visited[k] == 0)
DFS(G, k);
}
}
用于邻接表的深度优先搜索算法:
// 注:visited数组为全局的,所有元素初值为0
// 注:visited[v]==0表示v顶点未被访问过
void DFS(Graph G, int v) {
cout << G.vertices[v].data;
visited[v] = 1;
ArcNode *p = G.vertices[v].firstarc;
while(p != NULL) {
if(visited[p->adjvex] == 0)
DFS(G, p->adjvex);
p = p->nextarc;
}
}
5、最小生成树
从“远交近攻逐个击破”策略理解普里姆算法思想
从任意一个顶点v1(秦国)开始,U={v1}(版图),TE={}(最小生成树的边集)。
将距离U最近的顶点vi和相应的边ej分别并入U(新版图)和TE。注:vi原本不在U中。(秦从目前版图攻击兼并最近、最易、最有价值的国家)
循环上述过程,直到U包含所有顶点。(U, TE)即为原来无向网(V, E)的最小生成树。
普里姆算法的步骤——MinSpanTree(G, u)
将初始顶点加入U,对其余每个顶点vj,将closedge[j]初始化为u到vj的边信息。
循环n-1次:
从closedge中选出最小边closedge[k],输出此边,k加入U。
更新剩余的每组最小边信息closedge[j]:如果k到j的边权值小于原closedge[j].lowcost,则将closedge[j]更新为k到j的边信息。
注:closedge[j]含两个成员:adjvex——边对应的U中的顶点(另一个顶点是j)、lowcost——adjvex到j的最小边的权值。
初始的closedge[j]={u, u到j的边的权值}。
——设顶点数据为字符型,边权值为整型,无向网采用邻接矩阵存储,最多50个顶点
#define MAXVEXNUM 50
#define MAXINT 32767
typedef struct {
char vexs[MAXVEXNUM];
int arcs[MAXVEXNUM][MAXVEXNUM];
int vexnum, arcnum;
} Graph;
typedef struct {
int adjvex; // 顶点序号
int lowcost; // 最小边权值
} Edge;
void MinSpanTree(Graph G, int u) {
Edge closedge[MAXVEXNUM]; int i, j, k;
/* 将初始顶点加入U,对其余每个顶点vj,
将closedge[j]初始化为u到vj的边信息 */
for(j = 0; j < G.vexnum; j ++)
if(j != u) closedge[j] = { u, G.arcs[u][j] };
closedge[u].lowcost = 0;
// 循环n-1次
for(i = 1; i < G.vexnum; i ++) {
/* 从closedge中选出最小边closedge[k],
输出此边,k加入U */
k = MinEdge(closedge, G.vexnum);
OutEdge(G, closedge[k].adjvex, k);
closedge[k].lowcost = 0;
/* 更新剩余的每组最小边信息closedge[j]:
如果k到j的权值小于原closedge[j].lowcost,
则将closedge[j]更新为k到j的边信息 */
for(j = 0; j < G.vexnum; j ++)
if(G.arcs[k][j] < closedge[j].lowcost)
closedge[j] = { k, G.arcs[k][j] };
}
}
注:算法时间复杂度O(n2)
6、最短路径
无向网或有向网中v1(源点)到v2(终点)可能存在多条路径,定义路径长度为路径上所有边(弧)的权值之和,其中路径长度最小的称为最短路径。
迪杰斯特拉(Dijkstra)算法
——从某个源点v0到其余各顶点的最短路径
定义两个顶点集合:
S:已经求出最短路径的终点集合(初始为{v0})
V-S:尚未求出最短路径的终点集合(初始为V-{v0})
S用一个数组来表示,元素初始值全为0,当vi隶属于S时,S[i]=1;当vi隶属于V-S时,S[i]=0。
v0到其余顶点的距离用D数组来表示,D[i]的初始值为G.arcs[v0][i]。
v0到V-S中的终点x的最短路径只有2种情况:
要么是(v0, x)
要么是中间仅经过S中顶点最终到达x
数组path:
path[i]表示最短路径中i号顶点的直接前驱
例如:path[5] = 3、path[3] = 4、path[4] = 0,则表示0号顶点到5号顶点的最短路径为0435
迪杰斯特拉算法的基本思路:
根据D数组选出V-S中距离v0最近的顶点k,将k加入S;
对于V-S中顶点x,如果v0经k中转再到x的路径短于原来v0到x的路径,则更新D[x]且path[x]=k;
重复上述过程,直到V-S={}。
迪杰斯特拉算法:
迪杰斯特拉算法步骤
初始化
将源点v0加入S中:S[v0]=1;
将v0到V-S中各个终点i的路径长度初始化为权值:D[i]=G.arcs[v0][i];
如果v0到i之间有弧,则将i的直接前驱置为v0:path[i]=v0,否则path[i]=-1。
循环n-1次:
选择下一条最短路径的终点k使得D[k]=Min{D[i]|i属于V-S};
k加入S中:S[k]=1;
对于V-S中的任意i,如果D[k]+G.arcs[k][i]<D[i],则D[i]=D[k]+G.arcs[k][i],path[i]=k。
代码:
void ShortestPath_DJ(Graph G, int v0, int path[]) {
int S[MaxVexNum], D[MaxVexNum], i, k, w;
for(i = 0; i < G.vexnum; i ++) {
S[i] = 0;
// 初始化v0到其余顶点的路径长度
D[i] = G.arcs[v0][i];
// 初始化i顶点在路径中的直接前驱
path[i] = (D[i] < MaxInt ? v0 : -1);
}
S[v0] = 1; // v0加入S
D[v0] = 0;
for(w = 1; w < G.vexnum; w ++) {
// 选择k使得D[k]=Min{D[i]|i属于V-S}
min = MaxInt;
for(i = 0; i < G.vexnum; i ++)
if(S[i] == 0 && D[i] < min)
k = i, min = D[i];
S[k] = 1; // k加入S
// 对V-S中的i,如果D[k]+G.arcs[k][i]<D[i],
// 则D[i]=D[k]+G.arcs[k][i],path[i]=k
for(i = 0; i < G.vexnum; i ++)
if(S[i]==0 && D[k]+G.arcs[k][i]<D[i]) {
D[i] = D[k] + G.arcs[k][i];
path[i] = k;
}
}
}
迪杰斯特拉算法的时间复杂度:O(n2)
用迪杰斯特拉算法求任意一对顶点间的最短路径
int path[MaxVexNum][MaxVexNum];
void SortestPath(Graph G, int path[MaxVexNum][]) {
int i;
for(i = 0; i < G.vexnum; i ++)
ShortestPath_DJ(G, i, path[i]);
}
时间复杂度:O(n3)
弗洛伊德(Floyd)算法:
弗洛伊德(Floyd)算法步骤:
初始化:对任意的vi和vj,D[i][j]=G.arcs[i][j],如果vi到vj有弧则path[i][j]=i,否则path[i][j]=-1;
在vi和vj之间加入v0(序号为0的顶点),D[i][j]取(vi,vj)和(vi,v0,vj)中最短者,同时更新path[i][j];
在vi和vj之间加入v1,得到(vi,…,v1)和(v1,…,vj),其中(vi,…,v1)是vi到v1且中间顶点序号不大于0的最短路径,(v1,…,vj)是v1到vj且中间顶点序号不大于0的最短路径,D[i][j]取(vi,vj)和(vi,…,v1)+(v1,…,vj)中最短者,同时更新path[i][j];
同理,在vi和vj之间加入vk(k=2,3,…,n-1), D[i][j]取(vi,vj)和(vi,…,vk)+(vk,…,vj)中最短者,同时更新path[i][j]。
弗洛伊德算法:
void ShortestPath(Graph G, int path[MaxVexNum][]) {
int D[MaxVexNum][MaxVexNum], i, j, k;
for(i = 0; i < G.vexnum; i ++)
for(j = 0; j < G.vexnum; j ++) {
D[i][j] = G.arcs[i][j];
path[i][j]=(i!=j&&D[i][j]<MaxInt?i:-1);
}
for(k = 0; k < G.vexnum; k ++)
for(i = 0; i < G.vexnum; i ++)
for(j = 0; j < G.vexnum; j ++)
if(D[i][k] + D[k][j] < D[i][j]) {
D[i][j] = D[i][k] + D[k][j];
path[i][j] = path[k][j];
}
}
#未完,待
更新。。。