前言
- 本文来自左程云左神分享的视频教程内容
- 中间有一些地方写的比较随便,后面懒得改了,就在基础上加的;
- 这一篇基本都是用的指针,你问我为什么要写成指针?我也不知道,大概是那会脑子抽了;
- 理解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
:
主要有几个步骤:
- 记录所有数据;
- 从
map
中找到节点数据,找不到就新建并添加到集合中去; - 最后根据已经有的节点信息创建一个新的边并添加到边集。
- 所有数据生成好以后,将新的图返回
// 图
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);
- 这里主要用的是宽度优先遍历策略
- 基本分为以下几步:
- 创建
map
容器和ret
容器,map
用于记录所有当前可以到达的节点以及距离,并且不断更新,ret
容器用于存储结果 - 先把起点记录到
map
中,进入循环 - 遍历哈希表,找到最小的边,并将其从
map
中移除,将这个边的数据添加到ret
中
为啥要这么干呢?其实就是要每次都从所有能走的节点中,找到可能存在最小值的那个节点,因为你不可能找到从其他节点经过到达这个节点更短的方法(除非距离值给个负的) - 将改节点衍生出的所有能到达的边(节点)进行遍历
在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;
}