图的概念
图中包含两个元素:顶点vertex、边edge
图的分类:
1、有向图:边有方向,(比如微博)
2、无向图:边没有方向(比如微信)
3、带权图:每条边都有一个权重weight
度的概念:
无向图的度表示一个顶点有多少条边,有向图的度又分为入度和出度
图的存储
1、邻接矩阵:Adjaceny Matrix
优点:简单,直观,获取两个顶点的关系时非常高效,计算方便可以将图的运算转换成矩阵之间的运算
缺点:浪费存储空间
2、邻接表存储:Adjacency List
每个顶点对应一条链表,链表中存储的是与这个顶点相连接的其他顶点
优点:比较节省空间
缺点:使用起来比较耗时,链表存储方式对缓存不友好
图相关算法
广度优先搜索BFS
思想:是一种地毯式层层推进的搜索策略,先查找离起始顶点最近的,然后是次近的,依次往外搜索。
复杂度:时间复杂度是O(V+E),V是顶点个数,E是边数;空间消耗主要在几个辅助变量 visited 数组、queue 队列、prev 数组上。这三个存储空间的大小都不会超过顶点的个数,所以空间复杂度是 O(V)。
总结:广度优先搜索需要借助队列来实现,遍历得到的路径就是,起始顶点到终止顶点的最短路径。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <list>
#include <iostream>
using namespace std;
class Graph {
private:
int v; //顶点个数
std::list<int>* adj; //邻接表
public:
Graph(int v) {
this->v = v;
adj = new std::list<int>[v];
}
~Graph() {
delete [] adj;
}
void AddEdge(int v, int w);
void Bfs(int s, int t); // from s to t, print path
void PrintPrev(int *prev, int s, int t);
};
void Graph::Bfs(int s, int t) {
if (s == t) return;
//visited 用来记录已经被访问的顶点,用来避免顶点被重复访问
bool* visited = new bool[v];
//prev 用来记录搜索路径,prev[w]存储的是顶点w是从那个前驱顶点遍历过来的
int* prev = new int[v];
//队列存储已经被访问,但是其相连的顶点还没有被访问的顶点。
list<int> queue;
//首先进行初始化
for (int i=0; i<v; i++) {
visited[i] = false;
prev[i] = -1;
}
visited[s] = true;
queue.push_back(s);
bool found = false;
while(!queue.empty() && !found) {
int k = queue.front();
queue.pop_front();
list<int>::iterator it;
for (it=adj[k].begin(); it!=adj[k].end(); ++it){
if(!visited[*it]){
visited[*it] = true;
queue.push_back(*it);
prev[*it] = k;
if (*it == t){
found = true;
break;
}
}
}
}
PrintPrev(prev, s, t);
delete[] visited;
delete[] prev;
}
void Graph::PrintPrev(int *prev, int s, int t){
if (prev[t] != -1 && t != s){
printPrev(prev, s, prev[t]);
}
cout << t << " ";
}
int main() {
Graph g(8);
g.AddEdge(0, 3);
g.AddEdge(1, 2);
g.AddEdge(2, 5);
g.AddEdge(5, 7);
g.Bfs(0, 2);
}
深度优先搜索DFS
思想:一条路走到黑,走不通,则回退到上一个岔口,重新选择一条路继续走
复杂度:每条边最多会被访问两次,一次是遍历,一次是回退。所以,图上的深度优先搜索算法的时间复杂度是 O(E),E 表示边的个数。内存消耗主要是 visited、prev 数组和递归调用栈。visited、prev 数组的大小跟顶点的个数 V 成正比,递归调用栈的最大深度不会超过顶点的个数,所以总的空间复杂度就是 O(V)。
总结:深度优先搜索用的是回溯思想,适合用递归实现。也可以说,深度优先搜索是借助栈来实现的。
void Graph::Dfs(int s, int t){
//visited 用来记录已经被访问的顶点,用来避免顶点被重复访问
bool* visited = new bool[v];
//prev 用来记录搜索路径,prev[w]存储的是顶点w是从那个前驱顶点遍历过来的
int* prev = new int[v];
//初始化
for(int i=0; i<v; i++){
visited[i] = false;
prev[i] = -1;
}
visited[s] = true;
RecurDfs(s, t, visited, prev);
PrintPrev(prev, s, t);
delete[] visited;
delete[] prev;
}
void Graph::RecurDfs(int s, int t, bool* visited, int* prev){
if (found) return;
visited[s] = true;
if (s == t) {
found = true;
return;
}
list<int>::iterator it;
for(it=adj[s].begin(); it!=adj[s].end(); ++it){
if(!visited[*it]){
prev[*it] = s;
//visited[*it] = true;
if (*it == t) {
found = true;
break;
}
RecurDfs(*it, t, visited, prev);
}
}
}
拓扑排序kahn
思想:贪心算法。如果s需要先于t执行,那就添加一条s指向t的边,如果某个顶点入度为0,那这个顶点就可以执行了。
步骤:首先从图中找到一个入度为0的顶点,将其输出到拓扑排序的结果序列中,然后删除该顶点,并将该顶点可到达的顶点的入度都减一,循环执行这个步骤直到所有的顶点都被输出,最后输出的序列就是满足局部依赖关系的拓扑排序。
总结:还可以检测环的存在,对于kahn算法,如果最后输出出来的顶点个数少于图中顶点个数,图中还有入度不是0的顶点,那就说明图中存在环
void Graph::TuopuSortByKahn() {
int * indegree = new int[v](); //统计每个顶点的入度
//初始化
for (int i = 0 ; i < v; ++i) {
list<int>::iterator it;
for (it = adj[i].begin(); it != adj[i].end(); ++it) {
int w = *it;
indegree[w]++;
}
}
//将入度为0的顶点插入队列中
queue<int> q;
for (int i = 0; i < v; ++i) {
if (indegree[i] == 0) {
q.push(i);
}
}
//对队列中的每个顶点进行处理
while (!q.empty()) {
int curr = q.front();
q.pop();
list<int>::iterator it;
for (it = adj[curr].begin(); it != adj[curr].end(); ++it) {
indegree[*it]--;
//如果与之相邻的顶点入度为0则插入到队列中
if (indegree[*it] == 0) {
q.push(*it);
}
}
}
}
拓扑排序DFS
思想:深度优先遍历,遍历图中的所有顶点,而非只是搜索一个顶点到另一个顶点的路径。
步骤:首先通过邻接表构造逆邻接表,然后递归处理每个顶点。对于顶点vertex来说,我们先输出它可达的所有顶点,然后再输出它自己
复杂度:时间复杂度是o(e+v)
void Graph::TuopuSortByDfs() {
// 构造逆邻接表
std::list<int>* reverse_adj = new list<int>[v];
for (int i = 0 ; i < v; ++i) {
list<int>::iterator it;
for (it = adj[i].begin(); it != adj[i].end(); ++it) {
reverse_adj[*it].push_back(i);
}
}
//对于每个顶点调用dfs获取入度顶点信息
bool* visited = new bool[v]();
for (int i = 0; i < v; i++) {
if (visited[i] == false) {
visited[i] = true;
DfsForTuopu(i, reverse_adj, visited);
}
}
}
void Graph::DfsForTuopu(int i, list<int>* reverse_adj, bool* visited) {
list<int>::iterator it;
for (it = adj[i].begin(); it != adj[i].end(); ++it) {
if (visited[*it] == true) continue;
visited[*it] = true;
DfsForTuopu(*it, reverse_adj, visited);
}
cout << " ->" << i << " ";
}