c++ 实现Djkstra寻路算法

前言

  • 本文来自左程云左神分享的视频教程内容
  • 中间有一些地方写的比较随便,后面懒得改了,就在基础上加的;
  • 这一篇基本都是用的指针,你问我为什么要写成指针?我也不知道,大概是那会脑子抽了;
  • 理解Djkstra的基本思想之后,如果需要做抽象的话,可以将Node节点的value改成泛型,也可以加一个变量在里面,记录距离长度,再重载一下运算符,就可以实现自定义排序;这里就先写成这样,需要的时候再改
  • 阅读本文前你需要先了解图的相关概念,还有STL的基本知识

一、构建图

通常我们做寻路相关的题目,给出的数据常常会是二维数组,给出节点信息和距离信息;
下面简单封装了一个图的数据结构,是基于无向图来实现的,有向图需要自己手动改
Node节点代表一个路径点,它记录每一个可能的路径点的信息

  • value 代表这个节点的名称:这个不影响,可以随便改,改成泛型也可
  • in代表这个节点的入度:有多少个节点可以不经其他节点,直接到达本节点
  • out代表这个节点的出度:从本节点出发可以直接到达的节点数量
  • nexts记录从本节点出发,所能到达的所有节点
  • edges记录要到达其他节点,所经过的边
    其中nexts.size() == edges.size()记录的节点和边是一一对应的关系

下面来看看边Edge

  • weight 代表这个边它有多长,一般管这个叫权值
  • from 代表这个边的起点节点,从哪里开始
  • to 代表这个边的终点,即到哪里结束
  • make_edge函数即为通过两个节点和其之间距离生成一条边,更新相关数据并返回
class Edge;
// 节点
class Node
{
public:
	int value;
	int in;
	int out;
	vector<Node*> nexts;		//直接邻居,记录出度点
	vector<Edge*> edges;		//到直接邻居的边,记录出度边

	Node(int value)
	{
		this->value = value;
		in = 0;
		out = 0;
	}
};
//边
class Edge	//只记录有向边,无向边可以通过两条有向边实现
{
	Edge(int weight, Node* from, Node* to)
	{
		this->weight = weight;
		this->from = from;
		this->to = to;
	}
public:
	int weight;
	Node* from;
	Node* to;

	static Edge* make_edge(int weight, Node* _from, Node* _to)
	{
		Edge* newEdge = new Edge(weight, _from, _to);
		_from->nexts.push_back(_to);
		_from->out++;
		_to->in++;
		_from->edges.push_back(newEdge);
		return newEdge;
	}
};

再来看一下图Graph的结构,这也是我们唯一可以从外部操作的接口,上面的外部都不能动。

  • nodes 记录所有存在于图结构中的节点
  • edges 记录所有存在于图结构中的边
  • ~Graph()析构函数,用于释放其所有保存过的节点和边
  • createGraph构建图函数(注意我的构造函数设成了私有方法,只能通过调用这个函数来生成图类)
  • 我写了一组数据用于测试;
    其中0号位是from起点,1号位是to终点,2号位是权值weight
	vector<vector<int>> matrix{
		{1, 2, 3},
		{1, 4, 10},
		{2, 3, 1},
		{3, 4, 1} };
	Graph* gra = Graph::createGraph(matrix);
  • 关于 createGraph
    主要有几个步骤:
  1. 记录所有数据;
  2. map中找到节点数据,找不到就新建并添加到集合中去;
  3. 最后根据已经有的节点信息创建一个新的边并添加到边集。
  4. 所有数据生成好以后,将新的图返回
// 图
class Graph {
	unordered_map<int, Node*>* nodes;
	unordered_set<Edge*>* edges;
	
	~Graph()
	{
		//释放所有子节点
		for (pair<int, Node*> ele : *nodes)
		{
			if(ele.second) delete ele.second;
			ele.second = nullptr;
		}
		delete nodes;
		nodes = nullptr;
		//释放所有边
		for (Edge* ele : *edges)
		{
			if (ele) delete ele;
			ele = nullptr;
		}
		delete edges;
		edges = nullptr;
	}
	Graph()
	{
		nodes = new unordered_map<int, Node*>();
		edges = new unordered_set<Edge*>();
	}	
public:	
	//矩阵转化图结构:
	static Graph* createGraph(vector<vector<int>>& matrix)
	{
		Graph* graph = new Graph();

		for (vector<int> ele : matrix)
		{
			int from = ele[0];
			int to = ele[1];
			int weight = ele[2];
			Node* fromN = nullptr;
			Node* toN = nullptr;
			//没找到就添加
			if (graph->nodes->find(from) == graph->nodes->end())
			{
				fromN = new Node(from);
				graph->nodes->insert(make_pair(from, fromN));
			}else
				fromN = graph->nodes->find(from)->second;
				
			if (graph->nodes->find(to) == graph->nodes->end())
			{
				toN = new Node(to);
				graph->nodes->insert(make_pair(to, toN));
			}
			else
				toN = graph->nodes->find(to)->second;
			//把边添加到 边集合
			graph->edges->insert(Edge::make_edge(weight, fromN, toN));
		}
		return graph;
	}
};

这个图结构是比较完善的,虽然有些东西暂时用不到,但是大部分情况都是可以处理的,推荐使用

二、Djkstra算法的哈希版本

这里我的基础实现思路是:
输入一个起点,以map形式返回所有可以到达节点的最短距离

unordered_map<int, int>* findAllRoad_1(int from);
  • 这里主要用的是宽度优先遍历策略
  • 基本分为以下几步:
  1. 创建map容器和ret容器,map用于记录所有当前可以到达的节点以及距离,并且不断更新,ret容器用于存储结果
  2. 先把起点记录到map中,进入循环
  3. 遍历哈希表,找到最小的边,并将其从map中移除,将这个边的数据添加到ret
    为啥要这么干呢?其实就是要每次都从所有能走的节点中,找到可能存在最小值的那个节点,因为你不可能找到从其他节点经过到达这个节点更短的方法(除非距离值给个负的)
  4. 将改节点衍生出的所有能到达的边(节点)进行遍历
    ret结果中的节点不做操作,因为此节点的路径已经是最小值(不可能找到更小的)
    如果map中没有数据,就新添加;有数据,就选出两个数据中较小的那个。

	//哈希表版DJ特斯拉
	//返回值map中key为路点, value为距离
	unordered_map<int, int>* findAllRoad_1(int from)
	{
		unordered_map<int, Node*>::iterator p = nodes->find(from);
		if (p == nodes->end()) return nullptr;	//找不到就返回空

		unordered_map<Node*, int> map;
		map.insert(make_pair(p->second, 0));					//插入头
		unordered_map<int, int>* ret = new unordered_map<int, int>;

		while (!map.empty())
		{
			//找最小边
			pair<Node*, int> minP = *(map.begin());
			for (pair<Node*, int> ele : map)
			{
				minP = ele.second < minP.second ? ele : minP;
			}
			//从表中删除
			map.erase(minP.first);
			//添加到结果表中
			ret->insert(make_pair(minP.first->value, minP.second));

			//更新
			for (Edge* ele : minP.first->edges)
			{
				Node* to = ele->to;
				if (ret->find(to->value) != ret->end()) continue;	//已经走过的节点不参与计算
				//节点边长
				int weight = minP.second + ele->weight;

				if (map.find(to) == map.end())//添加
					map.insert(make_pair(to, weight));
				else //更新
					map[to] = weight < map[to] ? weight : map[to];
			}
		}
		return ret;
	}
  • 哈希表版本中,每次进入循环时,都需要遍历哈希表,来找到当前可以走的边中,最小的那个;
  • 这就产生了额外的时间复杂度,这一步时完全可以进行优化的,于是就有了小顶堆的优化策略。

三、小顶堆(优先队列)版本

  • 堆的操作如果还没有接触过,先点击这里:
    C语言实现堆排序
  • C++自带的优先队列中,只有当你添加数据和弹出数据的时候,这个堆才进行堆的顺序的重新调整;而我们需要的实时更新堆上数据的权值,并且堆能够对于更新实时调整堆的位置。
  • 所以为了满足这个需求,我们需要重新实现一个堆以应对新的需求
    这里贴出我之前写的代码,其中添加和修改了一些东西:
	// 记录当前所有堆上的节点
	vector<T> nodes;
	// 这是一个下标索引器,专门用于通过节点寻找下标
	unordered_map<T, int> heapIndexMap;
	// 这是距离索引器,用于保存所有距离数据
	unordered_map<T, int> distanceMap;
	//主要方法:
	//从上往下找,你从堆的某个节点改写数据,就从该节点下重新更新所有排序,
	//小顶堆上表现为,你将堆定改成可能比较大的值,并进行重新排列
	void heapIfy(int index);
	// 从下往上找,通过下标更新数据,改写了一个数据,就根据这个数据,挨个往上找
	// 将比他大的数据挨个交换到下面去
	void insertHeapIfy(int index);
	// 在堆中,就更新,不在堆中但已经进入过,表示该值已经锁定,否则表示该节点没有进来过,新建一个即可
	void addOrUpdate(T node, int distance);
	//返回最小的节点数据(堆顶的节点)
	pair<T, int> pop();
  • 把这个堆当成一个优先队列就可以,只不过自己加了一个方法;
  • 首先我们需要知道,我们是在寻找最小值,更新也是不可能写入一个比原来大的值,所以我们的跟新操作就只需要往上寻找就可以,于是就有了insertHeapIfy取代之前的只在尾部插入的方法;
  • 至于heapIfy,就是把一个较大值卸载了堆的上面,需要在上放开始,向下重新进行排列;
  • addOrUpdate这个方法就是课上的addOrUpdateOrIgnore,传入节点在堆上,就更新;不在堆上,但是已经进入过堆,就表示已锁定;从没进入过堆,就新建,然后向上更新
  • swap注意swap交换时需要同时交换下标记录表和堆中数据;

这个逻辑理清确实需要一些脑筋,下面直接上代码:

template<class T>
class MyHeap
{
	// 记录当前所有堆上的节点
	vector<T> nodes;
	// 这是一个下标索引器,专门用于通过节点寻找下标
	unordered_map<T, int> heapIndexMap;
	// 这是距离索引器,用于保存所有距离数据
	unordered_map<T, int> distanceMap;
	// 是否进入过堆
	bool isEntered(T node)
	{
		return heapIndexMap.find(node) != heapIndexMap.end();
	}
	// 是否在堆中
	bool isInHeap(T node)
	{
		return isEntered(node) && heapIndexMap[node] != -1;
	}
	//根据下标交换数据
	void swap(int index1, int index2)
	{
		heapIndexMap[nodes[index1]] = index2;
		heapIndexMap[nodes[index2]] = index1;
		T tmp = nodes[index1];
		nodes[index1] = nodes[index2];
		nodes[index2] = tmp;
	}
	//从上往下找
	void heapIfy(int index)
	{
		int left = index * 2 + 1;
		while (left < nodes.size())
		{
			int largest = (left + 1 < nodes.size() && nodes[left + 1] < nodes[left]) ? left : left + 1;
			if (distanceMap[nodes[largest]] < distanceMap[nodes[index]]) return;

			swap(largest, index);
			index = largest;
			largest = index * 2 + 1;
		}
	}
	// 通过下标更新数据
	void insertHeapIfy(int index)
	{
		while (distanceMap[nodes[index]] < distanceMap[nodes[(index - 1) / 2]])
		{
			swap(index, (index - 1) / 2);
			index = (index - 1) / 2;
		}
	}
public:
	// 在堆中,就更新,不在堆中但已经进入过,表示该值已经锁定,否则表示该节点没有进来过,新建一个即可
	void addOrUpdate(T node, int distance)
	{
		if (isInHeap(node))
		{
			distanceMap[node] = min(distanceMap[node], distance);
			insertHeapIfy(heapIndexMap[node]);
		}
		else if (!isEntered(node))
		{
			nodes.push_back(node);
			heapIndexMap[node] = nodes.size() - 1;
			distanceMap[node] = distance;
			insertHeapIfy(nodes.size() - 1);
		}
	}
	//返回最小的节点数据(堆顶的节点)
	pair<T, int> pop()
	{
		pair<T, int> ret;
		ret.first = nodes[0];
		ret.second = distanceMap[ret.first];
		heapIndexMap[ret.first] = -1;
		distanceMap.erase(ret.first);
		swap(0, nodes.size() - 1);
		nodes.pop_back();
		heapIfy(0);
		return ret;
	}

	bool empty()
	{
		return nodes.size() == 0;
	}
};

下面我们就可以根据已经写好的堆,来进行数据的优化;相比哈希表版本而言,这个版本的主要数据处理是放在堆上,所以我们只需要在通过堆进行数据的存取即可,跟哈希表是差不多的思路

	//小顶堆版DJ特斯拉
	//返回值map中key为路点, value为距离
	unordered_map<int, int>* findAllRoad_2(int from)
	{
		unordered_map<int, Node*>::iterator p = nodes->find(from);
		if (p == nodes->end()) return nullptr;	//找不到就返回空

		unordered_map<int, int>* ret = new unordered_map<int, int>;
		MyHeap<Node*> heap;
		heap.addOrUpdate(p->second, 0);
		while (!heap.empty())
		{
			pair<Node*, int> pir = heap.pop();
			(*ret)[pir.first->value] = pir.second;
			for (Edge* ele : pir.first->edges)
			{
				heap.addOrUpdate(ele->to, pir.second + ele->weight);
			}
		}
		return ret;
	}

测试:

int main()
{
	vector<vector<int>> matrix{
		{1, 2, 3},
		{1, 4, 10},
		{2, 3, 1},
		{3, 4, 1} };
	Graph* gra = Graph::createGraph(matrix);
	
	//unordered_map<int, int>* mp = gra->findAllRoad_1(1);
	unordered_map<int, int>* mp = gra->findAllRoad_2(1);
	
	for_each(mp->begin(), mp->end(), [](pair<int, int> pir) {
		cout << pir.first << " " << pir.second << endl;
		});

	system("pause");
	return 0;
}
  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KamikazePilot

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值