目录
图(Graph)
图(Graph):一种比树更复杂的非线性表结构
顶点(vertex):图中的元素
边(edge):顶点之间的连接关系
无向图:边没有方向的图
度(degree):跟顶点相连的边的条数
有向图:边有方向的图
入度(In-degree):在有向图中,有多少条边指向这个顶点,例如微博用户的粉丝数
出度(Out-degree):在有向图中,有多少条边以这个顶点为起点指向其他顶点,例如微博用户的关注数
带权图(weighted graph):每条边都有一个权重的图
边权重(weight):带权图中边的权重值,例如QQ好友间的亲密度
连通图:图中的所有顶点都是连通的,此时边数E>>顶点数V
图的存储
1.邻接矩阵(Adjacency Matrix)
邻接矩阵是一个二维数组,
- 对于无向图,如果顶点i和顶点j之间有边,那么A[i][j]=1;
- 对于有向图,如果顶点i到顶点j之间有边,那么A[i][j]=1;
- 对于带权图,如果顶点i和顶点j之间有边,边权重为w,那么A[i][j]=w。
优点:
- 简单直观
- 获取顶点关系高效
- 计算方便,可以将很多图运算转换成矩阵之间的运算
缺点:
- 对于无向图、稀疏图来说,浪费存储空间
代码:
// 以邻接矩阵存储的图
class Graph
{
private:
int vertexNum; // 顶点个数
int edgeNum; // 边个数
vector<vector<int>> adjMatrix; //邻接矩阵
public:
Graph(vector<int> nums); // 构造函数
~Graph() {}; // 析构函数
void addEdge(int s, int t); // 无向图添加顶点s到顶点t的一条边,
void DFS(int s, int t);// 深度优先遍历图,搜索结点s到结点t的一条路径
void BFS(int s, int t);// 广度优先遍历图,搜索结点s到结点t的一条路径
};
Graph::Graph(vector<int> nums)
{
int n = nums.size();
vertexNum = n;
adjMatrix.resize(n); // n行
for(int i = 0; i < n; ++i)
{
adjMatrix[i].resize(n); //n列
}
}
void addEdge(int s, int t)
{
adjMatrix[s][t] = 1;
adkMatrix[t][s] = 1; //无向图
++edgeNum;
}
2. 邻接表(Adjacency List)
邻接表类似散列表,每个顶点对应一条链表,链表中存储的是与这个顶点相连接的其他顶点,
- 对于无向图,相连接指的是有边相连;
- 对于有向图,相连接指的是这个指向其他节点的边(逆邻接表是存的指向这个节点的边)。
优点:
- 存储节省空间(如果仍存不进内存的话,可以对顶点哈希分片)
缺点:
- 查询两个顶点之间关系时低效 -> 可以用平衡二叉查找树、跳表、散列表、有序动态数组等替换链表,防止链过长,以实现更为高效的查找
代码:
// 邻接表里链表的节点
struct ListNode
{
int val; // 第val个顶点
ListNode *next;
};
// 邻接表里的一个元素
struct VertexNode
{
int vertex; // 顶点值
ListNode *firstedge; // 边
};
// 以邻接表存储的图
class Graph
{
private:
int vertexNum = 0; // 顶点的个数
int edgeNum = 0; // 边的个数
vector<VertexNode> adjList; // 邻接表
public:
Graph(vector<int> nums);// 构造函数建立图,顶点的值在nums里
~Graph() {}; // 析构函数
void addEdge(int s, int t); // 无向图添加顶点s到顶点t的一条边
void DFS(int s, int t);// 深度优先遍历图,搜索结点s到结点t的一条路径
void BFS(int s, int t);// 广度优先遍历图,搜索结点s到结点t的一条路径
};
// 构造函数
Graph::Graph(vector<int> nums)
{
int n = nums.size();
vertexNum = n;
for(int i = 0; i < n; ++i)
{
adjList[i].vertex = nums[i]; // 指针用->,对象用.
adjList[i].firstedge = NULL;
}
}
// 添加顶点s到顶点t的一条边
void Graph::addEdge(int s, int t)
{
ListNode *node = new ListNode; // 创建一个Node,这里是node对象的指针
// 将这个Node插入到adjlist[s]这个链表里
node->val = t; // 目标节点是t
node->next = adjList[s].firstedge;
adjList[s].firstedge = node;
// 边数+1
++edgeNum;
}
图的搜索算法
搜索算法是指:从图中找出从一个顶点出发到另一个顶点的路径。
- 暴力搜索算法(适合图不大的搜索):广度优先、深度优先
- 启发式搜索算法:A*、IDA*等
广度优先搜索(BFS)
广度优先搜索(Breadth-First-Search),简称BFS,先找离其实顶点最近的、然后是次近的、依次往外搜索。
BFS可以找到顶点s和顶点t之间的最短路径。
BFS通过队列来实现,假设顶点数是V、边数是E,
- 时间复杂度:普通图的最坏时间复杂度是 O ( V + E ) O(V+E) O(V+E),连通图(E>>V)的时间复杂度是 O ( E ) O(E) O(E)
- 空间复杂度: O ( V ) O(V) O(V),主要消耗再visited数组、queue队列、prev数组,这三个数组的存储空间大小都不会超过V。
邻接矩阵存储下的BFS
void Graph::BFS(int s, int t)
{
if(s == t) return; //同一个顶点(s和t不是顶点的值,而是第几个顶点)
vector<bool> visited(vertexNum, false); // visited记录顶点i是否被访问过
visited[s] = true;
queue<int> q; // q存储第k层的所有顶点
q.push(s);
vector<int> prev(vertexNum, -1); // prev记录从顶点s到顶点t的搜索路径,prev[i]存储的是顶点i是从哪个前驱顶点遍历过来的,所以需要递归地打印
// bfs
while(q.size() != 0)
{
int i = q.front();
q.pop();
for(int j = 0; j < vertexNum; ++j)
{
if(adjMatrix[i][j] == 1 && !visited[j]) //是i的相邻顶点、且没有被访问过
{
// 加入到搜索结果路径中
prev[j] = i;
// 判断是否到达了顶点t
if(j == t) // 如果搜到了顶点t,就打印出搜索路径
{
print_route(prev, s, t);
return;
}
// 如果还没搜到顶点t,将这层搜到的顶点加到q里,等下层继续搜
q.push(j);
// 也要将它的状态设为已被搜过
visited[j] = true;
}
}
}
}
print_route(vector<int> prev, int s, int t)
{
// 递归打印顶点s->t的路径
// 因为prev[i]里存的是哪个顶点到的顶点i,所以s->t的路径 = s->prev[t]的路径 + t
if(prev[t] != -1 && s != t)
{
print_route(prev, s, prev[t]);
}
cout << t << " ";
}
邻接表存储下的BFS
void Graph::BFS(int s, int t)
{
if(s == t) return;
// visited
vector<bool> visited(vertexNum, false);
visited[s] = true;
// queue
queue<int> q;
q.push(s);
// prev
vector<int> prev(vertexNum, -1);
// bfs begin
while(q.size() != 0)
{
int i = q.front();
q.pop();
ListNode* p = adjList[i].firstedge;
while(p)
{
int j = p->val;
if(!visited[j])
{
prev[j] = i;
if(j == t)
{
print_res(prev, s, t);
return;
}
q.push(j);
visited[j] = true;
}
p = p->next;
}
}
}
print_route(vector<int> prev, int s, int t)
{
// 递归打印顶点s->t的路径
// 因为prev[i]里存的是哪个顶点到的顶点i,所以s->t的路径 = s->prev[t]的路径 + t
if(prev[t] != -1 && s != t)
{
print_route(prev, s, prev[t]);
}
cout << t << " ";
深度优先搜索(DFS)
深度优先搜索(Depth-First-Search),简称DFS,类似走迷宫,一直走到底,不行再返回(回溯)。
DFS得到的不是最短路径。
DFS通过栈或递归来实现,假设顶点数是V、边数是E,
- 时间复杂度:每条边最多被访问两遍,一次是遍历,一次是回退,所以时间复杂度是 O ( E ) O(E) O(E)
- 空间复杂度: O ( V ) O(V) O(V),主要消耗再visited数组、递归调用栈、prev数组,这三个数组的存储空间大小都不会超过V。
邻接矩阵存储下的DFS
(1)递归实现
bool found = false; // 全局变量,或者类成员变量。作为递归终止条件,如果为true,表明找到了顶点t,不用再继续递归查找了 (不能直接用s==t来判断return吗)???
void Graph::DFS(int s, int t)
{
if(s == t) return;
found = false; // 递归终止条件:找到了顶点t
vector<bool> visited(vertexNum, false); // 是否访问过该顶点
visited[s] = true;
vector<int> prev(vertexNum, -1); // prev[i]表示搜索结果路径中是哪个顶点到顶点i的
recursiveDFS(s, t, visited, prev);
print_res(prev, s, t);
}
void recursiveDFS(int s, int t, vector<bool> & visited, vector<int> & prev)
{
// 递归终止条件
if(found) return;
if(s == t)
{
found = true;
return;
}
// 子问题
for(int j = 0; j < vertexNum; ++j)
{
if(adjMatrix[s][j] == 1 && !visited[j])
{
prev[j] = s;
visited[j] = true;
recursiveDFS(j, t, visited, prev);
}
}
}
(2)栈实现
// 不确定对不对
void Graph::DFS(int s, int t)
{
if(s == t) return;
vector<bool> visited(vertexNum, false);
visited[s] = true;
vector<int> prev(vertexNum, -1);
stack<int> s;
s.push(s);
while(!s.empty())
{
int i = s.top(); //取栈顶元素
s.pop();
// 找它的下一个元素
for(int j = 0; j <= vertexNum; ++j)
{
if(adjMatrix[i][j] == 1 && !visited[j])
{
visited[j] = true;
if(j == t)
{
print_res(prev, s, t);
return;
}
s.push(j);
}
}
}
}
邻接表存储下的DFS
(1)递归实现
bool found = false; // 全局变量,或者类成员变量。作为递归终止条件,如果为true,表明找到了顶点t,不用再继续递归查找了 (不能直接用s==t来判断return吗)???
void Graph::DFS(int s, int t)
{
found = false; // 递归终止条件:找到了顶点t
vector<bool> visited(vertexNum, false); // 是否访问过该顶点
visited[s] = true;
vector<int> prev(vertexNum, -1); // prev[i]表示搜索结果路径中是哪个顶点到顶点i的
recursiveDFS(s, t, visited, prev);
print_res(prev, s, t);
}
void recursiveDFS(int s, int t, vector<bool> & visited, vector<int> & prev)
{
// 递归终止条件
if(found) return;
if(s == t)
{
found = true;
return;
}
// 子问题
ListNode * p = adjList[s].firstedge;
while(p)
{
int j = p->val;
if(!visited[j])
{
prev[j] = s;
visited[j] = true;
recursiveDFS(j, t, visited, prev);
}
p = p->next;
}
}
(2)栈实现
// 不确定对不对
void Graph::DFS(int s, int t)
{
if(s == t) return;
vector<bool> visited(vertexNum, false);
visited[s] = true;
vector<int> prev(vertexNum, -1);
stack<int> s;
s.push(s);
while(!s.empty())
{
int i = s.top(); //取栈顶元素
s.pop();
// 找它的下一个元素
ListNode * p = adjList[i];
while(p)
{
int j = p->val;
if(!visited[j])
{
visited[j] = true;
if(j == t)
{
print_res(prev, s, t);
return;
}
s.push(j);
}
p = p->next;
}
}
}
A*
IDA*
问题:求三度好友
方法一:广度优先
广度优先遍历到第3层即可,以邻接表存储的图为例:
void Graph::BFS(int s, int k)
{
// g: 图
// s: 起始顶点
// k: k度好友
if(k > vertexNum) return;
// visited
vector<bool> visited(vertexNum, false);
visited[s] = true;
// queue
queue<int> q;
q.push(s);
// cur_k: 当前度数
int cur_k = 0;
// bfs begin
while(!q.empty())
{
int i = q.front();
q.pop();
ListNode* p = adjList[i].firstedge;
++cur_k;
while(p)
{
int j = p->val;
if(!visited[j])
{
prev[j] = i;
q.push(j);
visited[j] = true;
if(cur_k == k) cout << j << ' ';
}
p = p->next;
}
}
}
方法二:深度优先
- 对于单向路径可行:DFS递归时传多一个离初始节点的距离值,访问节点时,递归层数超过3的不再继续递归;
- 对于多连通图不可行:在深度递归时可能会先往距离远的节点搜索再往距离近的节点搜索,这样出来的搜索结果就是错的。
附录
代码实现参考:
- https://www.cnblogs.com/yellowgg/p/7875459.html
- https://blog.csdn.net/mdjxy63/article/details/80397944
- http://t.zoukankan.com/liugl7-p-11489184.html