图论算法:最短路径——无权最短路径算法和Dijkstra算法C++实现

前言

        今天将给大家介绍的是图论算法中的另外一个基础部分——最短路径算法;其中又分为无权最短路径,单源最短路径,具有负边的最短路径以及无圈图等;而这次将介绍常见的两个——无权最短路径以及单源最短路径。接下来就开始我们的讲解吧~~
        首先还是惯例来一个: Github传送门~~

原理

        最短路径算法,顾名思义是一种用来找出从某地到另外某个地方所经过的路径长度最短的算法,如图一所示。

图论算法:最短路径——无权最短路径算法和Dijkstra算法C++实现_第1张图片
上图中所展示的正是Dijkstra算法查找后的结果,我们可以清楚的了解从v1点开始,到其他点的最短路径该如何选择,以及其所经过的没条边的权重;接下来我将具体介绍其实现方式:

无权最短路径:
        在无权图中因为边上不存在权重,因此我们可以把每条边的权重都当做1。当我们从某个顶点开始检索的时候,我们将该节点 压入一个队列中或者是栈中(当然你也可以通过for循环来检索,不过时间复杂度会上升到O(n * n) ),然后将该节点标位已知节点,接下来检索所有与该节点相连接的节点,更新他们的路径与距离,并标位已知,接下来重复刚才的步骤,只不过 我们只对未更新过的节点(即距离无穷的节点)进行操作,直到所有节点都已知。
        以下是伪代码:
int Unweighted(vertex start) {
    queue Q;
    vertex v, w;

    enqueue(start);
    // 遍历所有的节点
    while(all vertex is retrieved) {
        v = dequeue;
        v is known;

        // 遍历相邻节点
        for each w adjacent to v
            // 更新路径
            if Dist to w is infinity {
                Dist to w = Dist to v + 1;
                Path to w is v;
                enqueue(w);
            }
    }
}

伪代码中有几点需要说明:
1.遍历所有节点我们只需要保证当队列是空时即可,这和拓扑排序很相似,大家可以想一想;

2.更新路径时我们只需要保证路径未被更新过即可进行更新,因为只要我们 更新过得路径即为最短路径,关于这一点大家也可以想一想;

2.Dijkstra算法:
        在赋权图中情况看似变得很复杂了,但其实我们用和无权图中相似的思想,也可以解决它,这就是我接下来介绍的Dijkstra算法;同上面类似,我们也从某个顶点开始检索将其标位已知,并更新其链接的顶点的路径信息,不同之处是,我们 选择未知节点中路径距离最短的作为下一次检索的起始顶点,然后一直进行检索直到所有顶点都已知;
        以下是伪代码:
int Dijkstra(vertex start) {
    vertex v, w;

    // 遍历所有的节点
    while(true) {
        v = smallest unknown distance vertex;
        if v = novertex
            break;

        v is known;
        // 遍历相邻节点
        for each w adjacent to v
            // 更新路径
            if(w is unknown && Dist to v + v->w < Dist to w) {
                Dist to w = Dist ti v + v->w;
                Path to w is v;
            }
    }
}

当然上诉代码中也有几点需要注意的地方:
1.查找最短距离未知节点的方法会直接影响到我们的时间复杂度,最好的方法是使用优先队列来完成;当然使用不同优先队列也会有差距,其中我们按照时间复杂度来排序的话:斐波拉契堆(O(E + V * logV)) < 配对堆(O(E * logV)) < 二叉堆(O(E * logV + V * logV));

2. 查找最短距离未知节,并以此节点开始检索也是算法中最重要的东西,他保证了我们每次更新的起始节点v的距离一定是最短的,从而保证了算法的正确性;

C++实现:

        最后给出的是整个实现的代码,按照惯例先是.h文件:
#ifndef ALGRAPH_H
#define ALGRAPH_H

#include 
#include 
#include 
using namespace std;

// 重定义边节点,便于操作
typedef struct ArcNode *Position;

/* 边节点
 * 储存元素:
 * adjvex:该有向边连向的节点
 * Weight:该有向边的权重
 * Next:该有向边头节点的其他边节点
 */
struct ArcNode {
	int adjvex;
	int Weight;
	Position Next;
};

/* 顶点节点
 * 储存元素:
 * Name:该节点的姓名;
 * firstArc:该顶点链接的第一个有向边;
 */
struct VexNode {
	int Name;
	Position firstArc;
};

/* 表节点
 * 储存元素:
 * Known:该节点检测状态;
 * Dist:该节点到目标节点的最小距离;
 * Path:最小距离时其链接的上一个节点;
 */
struct TNode {
	bool Known;
	int Dist;
	int Path;
};

/* ALGraph类
 * 接口:
 * Creat:创建功能,在选定节点之间创建有向边;
 * MakeEmpty:置空功能,将所有有向边删除,初始化各个顶点;
 * Unweighted:排序功能,找出选定顶点对于其他顶点的最短无权路径;
 * Display:展示功能,展示该图的路径信息;
 * Dijkstra:Dijkstra算法,用于计算赋权图最短路径
 * WeightNegative:排序功能,用于计算具有负边值的图的最短路径
 */
class ALGraph
{
public:
	// 构造函数
	ALGraph(int = 10);
	// 析构函数
	~ALGraph();

	// 接口函数
	// 基础函数
	void Creat();
	void MakeEmpty();
	void Display();

	// 最短路径函数
	void Unweighted(int);
	void Dijkstra(int);
	void WeightNegative(int);

private:
	// 辅助函数
	void InitTable();

	// 数据成员
	int VexNum; // 储存顶点数
	int ArcNum; // 储存边数
	VexNode *AdjList; // 储存邻接表
	TNode *Table; // 储存距离表
};


#endif // !ALGRAPH_H 

然后是.cpp文件,不过在这个文件中还有计算带负边的有向图的最短路径算法,有兴趣的朋友可以自己看下:
#include "stdafx.h"
#include "ALGraph.h"


/* 构造函数:初始化对象
 * 返回值:无
 * 参数:vnum:图中需要的顶点数
 */
ALGraph::ALGraph(int vnum)
: VexNum(vnum), ArcNum(0){
	// 申请邻接表
	AdjList = new VexNode[VexNum + 1];
	// 申请距离表
	Table = new TNode[VexNum + 1];

	// 判断是否申请成功
	if (AdjList == NULL || Table == NULL)
		cout << "邻接表申请失败!" << endl;

	else {
		for (int i = 0; i < VexNum + 1; i++) {
			// 初始化邻接表
			AdjList[i].Name = i;
			AdjList[i].firstArc = NULL;

			// 初始化距离表
			Table[i].Dist = INT_MAX;
			Table[i].Known = false;
			Table[i].Path = 0;
		}
	}
	
}

/* 析构函数:对象消亡时回收储存空间
 * 返回值:无
 * 参数:无
 */
ALGraph::~ALGraph()
{
	// 置空所有边
	MakeEmpty();

	// 删除邻接表
	delete AdjList;
	AdjList = NULL;

	// 删除距离表
	delete Table;
	Table = NULL;
}

/* 创建函数:在指定的顶点之间创建有向边
 * 返回值:无
 * 参数:无
 */
void ALGraph::Creat() {
	// 储存此次创建的边数
	// 并更新到总边数中
	int tmp;
	cout << "请输入要建立的边数:";
	cin >> tmp;
	ArcNum += tmp;

	// 创建所有新的有向边
	for (int i = 0; i < tmp; i++) {
		// v:有向边的头结点
		// w:有向边的尾节点
		// weight:有向边的权值
		int v, w, weight;
		cout << "请输入要建立有向边的两个顶点(v,w): ";
		cin >> v >> w;
		cout << "请输入其权值:";
		cin >> weight;

		// 创建新的阶段
		Position P = new ArcNode();
		if (P == NULL) {
			cout << "有向边创建失败!" << endl;
			return;
		}

		// 更新节点信息
		P->adjvex = w;
		P->Weight = weight;
		P->Next = AdjList[v].firstArc;

		// 链接到邻接表上
		AdjList[v].firstArc = P;
	}

}

/* 初始化函数:初始化距离表
 * 返回值:无
 * 参数:无
 */
void ALGraph::InitTable() {
	// 遍历距离表
	for (int i = 0; i < VexNum + 1; i++) {
		// 初始化参数
		Table[i].Dist = INT_MAX;
		Table[i].Known = false;
		Table[i].Path = 0;
	}
}

/* 置空函数:将所有的有向边置空
 * 返回值:无
 * 参数:无
 */
void ALGraph::MakeEmpty() {
	// 暂时储存中间节点
	Position P;

	// 遍历邻接表
	for (int i = 1; i < VexNum + 1; i++) {
		P = AdjList[i].firstArc;

		// 遍历所有链接的边
		while (P != NULL) {
			AdjList[i].firstArc = P->Next;
			delete P;
			P = AdjList[i].firstArc;
		}
	}
}

/* 排序函数:找出指定节点对于其他节点的最短无权路径
 * 返回值:无
 * 参数:Start:想要进行查找的节点
 */
void ALGraph::Unweighted(int Start) {
	// Q:储存队列,用于储存UnKnown节点
	// v:有向边的头结点
	// w:有向边的尾节点
	queue  Q;
	int v, w;
	
	// 初始化距离表
	InitTable();

	// 起始节点距离为0,并压入队列
	Table[Start].Dist = 0;
	Q.push(Start);

	while (!Q.empty()) {
		// 获取队列元素,并删除
		v = Q.front();
		Q.pop();

		// 该节点已知,不再需要使用
		Table[v].Known = true;

		// 遍历所以以该节点为头结点的有向边
		Position P = AdjList[v].firstArc;
		while (P != NULL) {
			// 获取尾节点
			w = P->adjvex;
			// 判断尾节点是否需要更新
			if (Table[w].Dist == INT_MAX) {
				// 更新信息
				Table[w].Dist = Table[v].Dist + 1;
				Table[w].Path = v;
				// 重新压入队列
				Q.push(w);
			}

			// 更新有向边位置
			P = P->Next;
		}
	}
}

/* 展示函数:展示该图的路径信息
 * 返回值:无
 * 参数:无
 */
void ALGraph::Display() {
	for (int i = 1; i < VexNum + 1; i++)
		cout << "Vex: " << i << "  ;Dist: " << Table[i].Dist << "  ; Path: " << Table[i].Path << endl;
}

/* Dijkstra算法:对赋权图进行单源最短路径排序
 * 返回值:无
 * 参数:Start:进行算法起始顶点
 */
void ALGraph::Dijkstra(int Start) {
	// v:单次排序的起始节点
	// w:单次排序的中值节点
	int v, w;

	// 初始化距离表
	InitTable();
	Table[Start].Dist = 0;

	// 遍历所有边
	while (true) {
		// Min:用于判断是否需要继续执行算法
		int Min = INT_MAX;

		// 特别注意:
		//     此处寻找最小节点使用的方法是我自己为了方便直接写的,如果
		// 用这种方法,时间复杂度应该比较高,达不到O(N * logN)的要求,所
		// 以正确的方法应该是把每个距离储存在优先队列中;
		//     当然,使用不同的优先队列也会有不同的效果,总体来说按照时
		// 间复杂度: 
		//     斐波拉契堆(O(E + V * logV)) < 配对堆(O(E * logV)) < 二叉堆(O(E * logV + V * logV))

		// 寻找最小的,且还未确定的有向边
		// 并将其头结点作为本次的起始节点
		for (int i = 1; i < VexNum + 1; i++)
			if (Table[i].Known == false && Min > Table[i].Dist) {
				v = i;
				Min = Table[i].Dist;
			}

		// 起始节点已知,不用再参与运算
		Table[v].Known = true;

		// 算法退出条件
		if (Min == INT_MAX)
			break;

		// 遍历所有以该起始节点为头结点的有向边
		Position P = AdjList[v].firstArc;
		while (P != NULL) {
			w = P->adjvex;
			// 判断尾节点是否已知
			if(Table[w].Known == false)
				if (Table[w].Dist > Table[v].Dist + P->Weight) {
					// 更新路径及距离
					Table[w].Path = v;
					Table[w].Dist = Table[v].Dist + P->Weight;
				}

			// 指向下一个节点
			P = P->Next;
		}
	}
}

/* 排序函数:用于计算具有负边值的图的最短路径
 * 返回值:无
 * 参数:Start:计算的起始节点
 */
void ALGraph::WeightNegative(int Start) {
	// Q:用于储存节点队列
	// v:单次排序的起始节点
	// w:单次排序的终止节点
	queue  Q;
	int v, w;

	// 初始化距离表
	InitTable();
	Table[Start].Dist = 0;
	Table[Start].Known = true;
	// 将起始节点压入队列
	Q.push(Start);

	// 遍历所有路径
	while (!Q.empty()) {
		v = Q.front();
		Q.pop();
		Table[v].Known = false; // 此处状态表示没有在队列中

		// 遍历所有已该节点为头结点的有向边
		Position P = AdjList[v].firstArc;
		while (P != NULL) {
			w = P->adjvex;

			// 更新路径距离
			if (Table[w].Dist > Table[v].Dist + P->Weight) {
				Table[w].Dist = Table[v].Dist + P->Weight;
				Table[w].Path = v;
				// 若不在队列中,则压入队列
				if (Table[w].Known = false)
					Q.push(w);
			}
		}
	}
}

        最后,最短路径的讨论我们到这里就结束啦,如果有什么问题欢迎大家一起讨论啊~~

参考文献:《数据结构与算法分析——C语言描述》

你可能感兴趣的:(数据结构)