【0】README
1)本文旨在给出 拓扑排序+最短路径算法(有权+无权) 的源码实现 和 分析,内容涉及到 邻接表, 拓扑排序, 循环队列,无权最短路径(广度优先搜索),有权最短路径,二叉堆,迪杰斯特拉算法 等知识;
2)其实,说白了:广度优先搜索算法(计算无权最短路径) 是基于 拓扑排序算法的,而 迪杰斯特拉算法(计算有权最短路径) 是基于 广度优先搜索算法或者说是它的变体算法;上述三者不同点在于: 拓扑排序算法 和 广度优先搜索算法 使用了 循环队列, 而迪杰斯特拉算法使用了 二叉堆优先队列作为其各自的工具;相同点在于:他们都使用了 邻接表来表示图;
3)o. m. g. 差点忘记了,如何计算所有点对最短路径?
3.1)邻接表表示的稀疏图:因为 迪杰斯特拉算法是 计算 单源有权最短路径,故运行顶点个数 次用二叉堆优先队列编写 的 迪杰斯特拉算法即可 计算所有点对最短路径;
3.2)邻接矩阵表示的稠密图:floyd 算法,该算法有三层for 循环 ,内两层循环用于遍历 邻接矩阵的每个元素,最外层循环 用于遍历中转节点,在中转节点的作用下,d[i][j] 之间的路径是否可以减小,伪代码如下:
for(k=1;k<=n;k++) for(i=1;i<=n;i++) for(j=1;j<=n;j++) { if(d[i][k]+d[k][j]<d[i][j]){ d[i][j]=d[i][k]+d[k][j]; path[i][j]=path[i][k]; } }
【1】邻接表是图的标准表示方法
0)图的表示方法:稀疏图用邻接表,而稠密图用邻接矩阵,而现实生活中 大多数图都是稀疏的;
1)邻接表的结构体
// 顶点的结构体.
struct Vertex;
typedef struct Vertex* Vertex;
struct Vertex
{
// ElementType value; // 要知道顶点是一个标识符,并不是真正的value(而是对value的抽象).
int index;
Vertex next;
};
// 邻接表的结构体
struct AdjList;
typedef struct AdjList* AdjList;
struct AdjList
{
int capacity;
Vertex* array;
};
2)代码实现如下:
// create vertex with index.
Vertex create(int index)
{
Vertex v = (Vertex)malloc(sizeof(struct Vertex));
if(v==NULL)
{
Error("failed create() for out of space.");
return NULL;
}
v->index=index;
v->next=NULL;
return v;
}
// 插入 顶点标识符index 到邻接表下标为 start 的位置.
void insertAdjList(AdjList adjList, int start, int index)
{
Vertex temp = adjList->array[start];
while(temp->next)
{
temp = temp->next;
}
temp->next = create(index);;
if(temp->next ==NULL)
{
return ;
}
}
#include "adjList.h"
void main()
{
int capacity=7;
AdjList adjList;
int row=7, col=3, i, j;
int adjArray[7][3] =
{
{2, 4, 3},
{4, 5, 0},
{6, 0, 0},
{6, 7, 3},
{4, 7, 0},
{0, 0, 0},
{6, 0, 0}
};
// init adjacency list.
adjList = init(7);
if(adjList==NULL)
{
return;
}
printf("\n\n\t ====== review for adjacency list ======\n");
for(i=0;i<row;i++)
{
for(j=0;j<col;j++)
{
if(adjArray[i][j])
{
insertAdjList(adjList, i, adjArray[i][j]);
}
}
}
printAdjList(adjList);
}
【2】拓扑排序
0)定义:拓扑排序是对有向无圈图的顶点的一种排序,它使得如果存在一条从 vi 到 vj 的路径,则在排序中 vj 出现在 vi 的后面;
1)用到的技术: 邻接表, 循环队列, 拓扑排序算法;且使用邻接表的拓扑排序时间复杂度为 O(|E| + |V|);
2)拓扑排序的steps
step1)把入度为0 的顶点入队列;
step2)当队列不为空时,顶点出队,其邻接顶点的入度减1;
step3)每一个邻接顶点的入度减1 后,若其入度为0 则入队列,转向step2;(别忘记在拓扑排序前,还要统计各个顶点的入度,入度数组indegreeArray 作为输入参数传给 拓扑排序算法)
// 拓扑排序 void topSort(AdjList adjList, Queue queue, int* indegreeArray) { int i; Vertex* array = adjList->array; Vertex temp; int index; int adjVertex; // step1: 把入度为0的顶点放入队列. for(i=0; i<adjList->capacity; i++) // 切记: 这里入队的value(或i) 从 0 开始取. { if(indegreeArray[i]==0) { enQueue(queue, i); } } //step2: 当队列不为空时,一个顶点v出队,并将与v邻接的所有顶点的入度减1. printf("\n\t top sorting result: "); while(!isEmpty(queue)) { index = deQueue(queue); // while 循环已经保证了 队列不可能为空. printf("v[%d] ", index+1); // 注意: 这里的index 要加1,因为元素入队是从 0 开始取的,见上面的入队操作. temp = array[index]; while(temp->next) { adjVertex = temp->next->index; // 因为 temp->next->index 从1 开始取的, indegreeArray[adjVertex-1]--; // adjVertex 要减1, 而indegreeArray数组从0开始取. if(indegreeArray[adjVertex-1]==0) // step3: 把与顶点v(标识符=index)相邻的,且入度为0的顶点放入队列. { enQueue(queue, adjVertex-1); //入队的value(或index) 从 0 开始取. } temp = temp->next; } } // 循环结束后: 拓扑排序就是顶点出队的顺序. }
3)拓扑排序的结果不唯一
4)对于上图的分析(Analysis)
5)排序结果如下:A1)对于上图而言,排序结果可以是 V1, V2, V5, V4, V3, V7, V6 也可以是 V1, V2, V5, V4,V7, V3, V6;
A2)千万不要以为拓扑排序的结果就一定顺着箭头的方向走;
【3】最短路径算法
【3.1】无权最短路径算法(计算节点间的边数)
0)算法描述: 给定一个无权图,使用某个顶点s作为 起始顶点,我们想要找出从 s 到 所有其他顶点的最短路径;
1)算法所用技术(techs):
tech1)邻接表;
tech2)循环队列:为什么无权最短路径算法要用到循环队列? 因为 要保存遍历到的当前节点的邻接节点,且以 先进先出的顺序输出;
tech3)无权最短路径算法(基于拓扑排序的算法 idea);
tech4)计算无权最短路径的记录表;
// 记录表的表项 struct Entry; typedef struct Entry* Entry; struct Entry { int known;// 出队后设置为1,表示已处理; int dv; // 起始顶点到当前顶点的距离; int pv; // 引起dv 变化的最后顶点; }; // 记录表的数组 struct UnweightedTable; typedef struct UnweightedTable* UnweightedTable; struct UnweightedTable { int size; Entry* array; };
补充)记录表表项中成员的初始化
//allocate the memory for initializing unweighted table
UnweightedTable initUnweightedTable(int size)
{
int i;
UnweightedTable table = (UnweightedTable)malloc(sizeof(struct UnweightedTable));
if(table==NULL)
{
Error("failed initUnweightedTable() for out of space.");
return NULL;
}
table->size = size;
table->array = (Entry*)malloc(size * sizeof(Entry));
if(table->array==NULL)
{
Error("failed initUnweightedTable() for out of space.");
return NULL;
}
for(i=0; i<size; i++)
{
table->array[i] = (Entry)malloc(sizeof(struct Entry));
if(table->array[i]==NULL)
{
Error("failed initUnweightedTable() for out of space.");
return NULL;
}
table->array[i]->known = 0; // known=0 or 1表示 未知 或 已知.
table->array[i]->dv= INT_MAX; // dv==distance 等于 INT_MAX 表示不可达. 而 dv=0 表示它自己到自己的path==0.
table->array[i]->pv = 0; // pv==path 等于 0 表示不可达,因为pv从1开始取。
}
return table;
}
2)算法步骤(借用了拓扑排序的算法 idea):
step1)起始顶点进队,设置其 dv,pv 等于 0;
step2)队列不为空,顶点出队;出队顶点的known设置为1,表明已处理;
step3)遍历出队顶点的每一个邻接顶点,若 邻接顶点的dv 等于 -1 (表不可达),则该邻接顶点进队,设置其 dv,pv;转向step2;
//计算 startVertex顶点 到其他顶点的无权最短路径 void unweighted_shortest_path(AdjList adj, UnweightedTable table, int startVertex, Queue queue) { int capacity=adj->capacity; Vertex* arrayVertex = adj->array; Vertex temp; Entry* arrayEntry = table->array; int index; // 顶点标识符(从0开始取) int adjVertex; //step1(初始状态): startVertex 顶点进队. enQueue(queue, startVertex-1); // 切记: 这里入队的value(或i) 从 0 开始取. arrayEntry[startVertex-1]->dv=0; arrayEntry[startVertex-1]->pv=0; // 初始状态over. // step2: 出队. 并将其出队顶点的邻接顶点进队. while(!isEmpty(queue)) { index = deQueue(queue); // index从0开始取,因为出队value从0开始取,不需要减1. arrayEntry[index]->known=1; // 出队后,将其 known 设置为1. temp = arrayVertex[index]; while(temp->next) { adjVertex = temp->next->index; // 邻接节点标识符adjVertex 从1开始取. if(arrayEntry[adjVertex-1]->dv == INT_MAX) // 注意: 下标是adjVertex-1, 且 dv==INT_MAX 表明 index 到 adjVertex 还处于不可达状态,所以adjVertex入队. { enQueue(queue, adjVertex-1); // 入队的value 从 0 开始取,所以减1. arrayEntry[adjVertex-1]->dv = arrayEntry[index]->dv + 1; arrayEntry[adjVertex-1]->pv=index+1; // index 从0开始取,所以index加1. } temp = temp->next; } } }
Attention)上述搜索图的算法被称为“广度优先搜索”,因为要遍历出队顶点的每一个邻接顶点,注意是每一个,所以叫做广度;
【3.2】有权最短路径算法(迪杰斯特拉算法)
0)算法描述:给定一个有权图,使用某个顶点s作为 起始顶点,我们想要找出从 s 到 所有其他顶点的有权最短路径;
1)算法所用技术(techs):
tech1)邻接表;
tech2)二叉堆(优先队列);堆中节点类型为 HeapNode(vertex 和 weight);为什么迪杰斯特拉算法要用到二叉堆? 因为 需要保存 当前节点的所有邻接顶点,且 有最小weight 的 邻接顶点先输出,这可以通过小根堆来实现,deleteMin() 就是一个输出的过程;
// 二叉堆的节点类型的结构体. struct HeapNode; typedef struct HeapNode* HeapNode; struct HeapNode { int vertex; int weight; }; // 二叉堆的结构体. struct BinaryHeap; typedef struct BinaryHeap* BinaryHeap; struct BinaryHeap { int capacity; int size; HeapNode array; };
tech3)迪杰斯特拉算法;
tech4)计算有权最短路径的记录表(同无权的记录表);
// 邻接表的表项结构体. struct Entry; typedef struct Entry* Entry; struct Entry { int known; int dv; int pv; }; // 有权路径记录表的结构体. struct WeightedTable; typedef struct WeightedTable* WeightedTable; struct WeightedTable { int size; Entry* array; };
//allocate the memory for initializing unweighted table(计算有权路径的记录表项的初始化) WeightedTable initWeightedTable(int size) { int i; WeightedTable table = (WeightedTable)malloc(sizeof(struct WeightedTable)); if(table==NULL) { Error("failed initUnweightedTable() for out of space."); return NULL; } table->size = size; table->array = (Entry*)malloc(size * sizeof(Entry)); if(table->array==NULL) { Error("failed initUnweightedTable() for out of space."); return NULL; } for(i=0; i<size; i++) { table->array[i] = (Entry)malloc(sizeof(struct Entry)); if(table->array[i]==NULL) { Error("failed initUnweightedTable() for out of space."); return NULL; } table->array[i]->known = 0; // known 等于 0 or 1 表示 未知 or 已知. table->array[i]->dv= INT_MAX; // dv==distance 等于 INT_MAX 表示不可达.(有权路径表示weight) table->array[i]->pv = 0; // pv==path 等于 0 也表示不可达. } return table; }
2)算法步骤
step1)从未知(known==0)顶点中选取 dv 最小的顶点作为初始顶点;初始顶点对应的HeapNode节点类型插入堆(insert操作);
step2)堆不为空,执行deleteMin()操作;设置被删除顶点为已知(其known=1);遍历被删除顶点的每一个邻接顶点;
step2.1)如其未知(known==0)
step2.1.1)且若更新后的权值(路径长)比更新前的权值(路径长)小的话
补充)迪杰斯特拉算法的结束标志是:当 所有顶点的状态都是已知(known==1)的时候,算法结束;step2.1.1.1)构建该节点的HeapNode节点类型并插入堆且更新路径长;
//计算 startVertex顶点 到其他顶点的无权最短路径 // adj:邻接表(图的标准表示方法), table: 计算有权最短路径的配置表,heap:用于选取最小权值的邻接顶点的小根堆. void dijkstra(AdjList adj, WeightedTable table, int startVertex, BinaryHeap heap) { int capacity=adj->capacity; Vertex* arrayVertex = adj->array; Vertex temp; Entry* arrayEntry = table->array; int index; // 顶点标识符(从0开始取) int adjVertex; struct HeapNode node; int weight; int i=0; // 记录已知顶点个数( known == 1 的 个数). //step1(初始状态): startVertex 顶点插入堆. startVertex 从1 开始取. node.vertex=startVertex-1; // 插入堆的 node.vertex 从 0 开始取,所以startVertex-1. node.weight=0; insert(heap, node); // 插入堆. arrayEntry[startVertex-1]->dv=0; arrayEntry[startVertex-1]->pv=0; // 初始状态over. // step2: 堆不为空,执行 deleteMin操作. 并将被删除顶点的邻接顶点插入堆. while(!isEmpty(heap)) { if(i == capacity) // 当所有 顶点都 设置为 已知(known)时,退出循环. { break; } index = deleteMin(heap).vertex; // index表示邻接表下标,从0开始取,参见插入堆的操作. arrayEntry[index]->known=1; // 从堆取出后,将其 known 设置为1. i++; // 记录已知顶点个数( known == 1 的 个数). temp = arrayVertex[index]; while(temp->next) { adjVertex = temp->next->index; // 邻接节点标识符adjVertex 从1开始取. weight = temp->next->weight; // 取出该邻接顶点的权值. if(arrayEntry[adjVertex-1]->known == 0) // 注意: 下标是adjVertex-1, 且known==0 表明 adjVertex顶点还处于未知状态,所以adjVertex插入堆. { if(arrayEntry[adjVertex-1]->dv > arrayEntry[index]->dv + weight ) // [key code] 当当前权值和 比 之前权值和 小的时候 才更新,否则不更新. { node.vertex=adjVertex-1; // 插入堆的 node.vertex 从 0 开始取. node.weight=weight; insert(heap, node); // 插入堆. arrayEntry[adjVertex-1]->dv = arrayEntry[index]->dv + weight; // [also key code] arrayEntry[adjVertex-1]->pv=index+1; // index 从0开始取,所以index加1. } } temp = temp->next; } printWeightedtable(table, 1); // 取消这行注释可以 follow 迪杰斯特拉 alg 的运行过程. } }
【补充】以HeapNode 为数据类型的二叉堆优先队列
0)为什么我要提及这个二叉堆?因为 这个二叉堆 和他家的不一样,其数据类型不是 单纯的 int, 而是一个结构体类型,我们只需要在 二叉堆中 将 HeapNode 类型定义为 ElementType 即可。
1)intro: 迪杰斯特拉法用到了 二叉堆优先队列,而二叉堆优先队列 的 数组(节点)类型是 HeapNode;
2)结构体介绍
// 二叉堆的节点类型的结构体.
struct HeapNode;
typedef struct HeapNode* HeapNode;
struct HeapNode
{
int vertex;
int weight;
};
// 二叉堆的结构体.
struct BinaryHeap;
typedef struct BinaryHeap* BinaryHeap;
struct BinaryHeap
{
int capacity;
int size;
HeapNode array;
};
3)二叉堆优先队列的 操作(重点关注其 基于上滤的插入操作 和 基于下滤的deleteMin操作)
// judge whether the heap is empty or not.
int isEmpty(BinaryHeap heap)
{
return heap->size == 0;
}
// build binary heap with capacity.
BinaryHeap buildHeap(int capacity)
{
BinaryHeap heap = (BinaryHeap)malloc(sizeof(struct BinaryHeap));
if(!heap)
{
Error("failed buildHeap() for out of space.");
return NULL;
}
heap->capacity = capacity;
heap->size = 0; // startup of the heap is 1. so ++size when insert.
// allocate memory for heap->array.
heap->array = (ElementType*)malloc(sizeof(ElementType) * capacity);
if(!heap->array)
{
Error("failed buildHeap() for out of space.");
return NULL;
}
heap->array[0].weight = INT_MIN; // heap->array starts from 1 not 0, so let heap->array[0] = INT_MIN.
return heap;
}
// 插入操作 based on 上滤操作.
void insert(BinaryHeap heap, ElementType data)
{
if(heap->size == heap->capacity-1)
{
Error("failed insert() for heap is full.");
}
percolateUp(heap, data);
}
// 上滤操作(key operation)
void percolateUp(BinaryHeap heap, ElementType data)
{
int i;
// 必须将size++.
for(i=++heap->size; data.weight < heap->array[i/2].weight; i/=2)
{
heap->array[i] = heap->array[i/2];
}
heap->array[i] = data;
}
// delete minimal from heap based on percolateDown().
ElementType deleteMin(BinaryHeap heap)
{
ElementType temp;
if(heap->size==0)
{
Error("failed deleteMin() for the heap is empty");
return temp;
}
swap(&heap->array[1], &heap->array[heap->size--]); // 将二叉堆的根和二叉堆的最后一个元素交换,size--。
percolateDown(heap, 1); // 执行下滤操作.
return heap->array[heap->size+1];
}
// 下滤操作(key operation)
void percolateDown(BinaryHeap heap, int i)
{
int child;
ElementType temp;
for(temp=heap->array[i]; (child=leftChild(i))<=heap->size; i=child)
{
if(child<heap->size && heap->array[child].weight > heap->array[child+1].weight)
{
child++;
}
if(temp.weight > heap->array[child].weight) // 比较是和 temp=heap->array[index] 进行比较.
{
heap->array[i] = heap->array[child];
}
else
{
break;
}
}
heap->array[i] = temp;
}
int leftChild(int index)
{
return 2*index;
}
// swap a and b.
void swap(ElementType* a, ElementType* b)
{
ElementType t;
t = *a;
*a = *b;
*b = t;
}
void printBinaryHeap(ElementType *array, int size)
{
int i;
for(i=1; i<=size; i++)
{
printf("\n\t heap->array[%d] = ", i);
printf("%d", array[i].weight);
}
printf("\n");
}