数据结构-图-概念及DFS/BFS算法

图的概念

图中包含两个元素:顶点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 << " ";
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值