【题目记录】dfs与bfs

目录

岛屿数量

检查二叉树是否是镜像对称的。

图的基础知识:

邻接列表:

邻接矩阵:

十字链表

邻接多重表

Trie树

多源bfs

拓扑排序 


岛屿数量

给定一个由 '1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。

示例 1:

输入:
11110
11010
11000
00000

输出: 1
示例 2:

输入:
11000
11000
00100
00011

输出: 3

 

代码:这个思路很不错的,用dir定义四个方向,这样可以少写很多代码,并且用一个visit数组记录下此位置是否被访问过了

    int dir[4][2] = {{-1,0},{0,1},{1,0},{0,-1}};
    
    int numIslands(vector<vector<char>>& grid) {
        int m=grid.size();
        if(m<=0) return 0;
        int n=grid[0].size();
        vector<vector<int>> visit(m,vector<int>(n,0));
        int count=0;
        
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(visit[i][j] == 1)
                    continue;
                else if(grid[i][j] == '1'){
                    count++;
                    dfs(grid,i,j,visit);
                }  
            }
        }
        return count;
    }
    
    void dfs(vector<vector<char>>& grid,int i,int j,vector<vector<int>>& visit){
        visit[i][j] = 1;
        for(int k=0;k<4;k++){
            int ii = i+dir[k][0];
            int jj = j+dir[k][1];
            if(ii >= 0 && ii<grid.size() && jj>=0 && jj<grid[0].size() && grid[ii][jj] == '1' && visit[ii][jj] == 0)
                dfs(grid,ii,jj,visit);
        }
    }

 

检查二叉树是否是镜像对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。

    1
   / \
  2   2
 / \ / \
3  4 4  3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:

    1
   / \
  2   2
   \   \
   3    3
说明:

如果你可以运用递归和迭代两种方法解决这个问题,会很加分。

1.递归:

如果同时满足下面的条件,两个树互为镜像:

  1. 它们的两个根结点具有相同的值。
  2. 每个树的右子树都与另一个树的左子树镜像对称。
    bool isSymmetric(TreeNode* root) {
        return judge(root,root);
    }
    bool judge(TreeNode* t1,TreeNode* t2){
        if(t1==nullptr && t2==nullptr)
            return true;
        if(!t1 || !t2)
            return false;
        return t1->val==t2->val && judge(t1->left,t2->right) && judge(t1->right,t2->left);
    }

2.迭代:bfs,借助于队列来实现

    bool isSymmetric(TreeNode* root) {
        if(!root)
            return true;
        queue<TreeNode*> q;
        q.push(root);
        q.push(root);
        while(!q.empty()){
            TreeNode*t1=q.front();
            q.pop();
            TreeNode*t2=q.front();
            q.pop();
            if(t1->val != t2->val || (t1->left && !t2->right) || (!t1->left && t2->right) || (t1->right && !t2->left) || (!t1->right && t2->left))
                return false;
            if(t1->left && t2->right){
                q.push(t1->left);
                q.push(t2->right);
            }
            if(t1->right && t2->left){
                q.push(t1->right);
                q.push(t2->left);
            }
        }
        return true;
    }

 

 

 

图的基础知识:

链接:https://www.jianshu.com/p/bce71b2bdbc8

https://www.cnblogs.com/nevermoes/p/9872877.html

    图(Graph)是由顶点的有穷非空集合和顶点之间的集合组成,通常表示为:G(V, E),其中 G 表示一个图,V 是图 G 中顶点的集合,E 是图 G 中边的集合。

    图可以分为有向图和无向图,有两种标准的表示方法,即邻接表和邻接矩阵。

邻接列表

在邻接列表实现中,每一个顶点会存储一个从它这里开始的边的列表。比如,如果顶点A 有一条边到B、C和D,那么A的列表中会有3条边

定义:

typedef struct ArcNode{  // 边结点(表结点)
    int adjvex; // 边指向的点
    struct ArcNode *next; //指向的下一条边
    int otherinfo  //如权值大小
}ArcNode;

typedef struct VNnode{ //顶点节点(头节点)
    int data;
    ArcNode *first;    //指向第一条边    插入其他节点时使用头插法
}VNode, AdjList[MAX]

typedef struct { //邻接表
    AdjList vertices;        //顶点数组
    int vexnum, arcnum;
} ALGraph;

性质

  1. 若G为无向图,则所需的存储空间为O(|V|+2|E|),若G为有向图,则所需的存储空间为O(|V|+|E|)。前者倍数是后者两倍是因为每条边在邻接表中出现了两次。
  2. 邻接表法比较适合于稀疏图。
  3. 点找边很容易,边找点不容易。
  4. 邻接表的表示不唯一

 

邻接矩阵:

在邻接矩阵实现中,由行和列都表示顶点,由两个顶点所决定的矩阵对应元素表示这里两个顶点是否相连、如果相连这个值表示的是相连边的权重。例如,如果从顶点A到顶点B有一条权重为 5.6 的边,那么矩阵中第A行第B列的位置的元素值应该是5.6:

往这个图中添加顶点的成本非常昂贵,因为新的矩阵结果必须重新按照新的行/列创建,然后将已有的数据复制到新的矩阵中。

所以使用哪一个呢?大多数时候,选择邻接列表是正确的。下面是两种实现方法更详细的比较。

假设 V 表示图中顶点的个数,E 表示边的个数。

操作邻接列表邻接矩阵
存储空间O(V + E)O(V^2)
添加顶点O(1)O(V^2)
添加边O(1)O(1)
检查相邻性O(V)O(1)

“检查相邻性” 是指对于给定的顶点,尝试确定它是否是另一个顶点的邻居。在邻接列表中检查相邻性的时间复杂度是O(V),因为最坏的情况是一个顶点与每一个顶点都相连。

在稀疏图的情况下,每一个顶点都只会和少数几个顶点相连,这种情况下相邻列表是最佳选择。如果这个图比较密集,每一个顶点都和大多数其他顶点相连,那么相邻矩阵更合适。

定义:

# define MAXSIZE 10000
typedef struct {
    int vexs [MAXSIZE];        //顶点表
    int edges[MAXSIZE][MAXSIZE];    //边表
    int vexnum, arcnum; // 实际点和边的数量
}AMGraph;    //adjacency matrix graph

性质:

  1. 无向图的邻接矩阵为对称矩阵,可以只用上或下三角。
  2. 对于无向图,邻接矩阵的第 i 行(列)非零元素的个数正好是第 i 个顶点的度 。
  3. 对于有向图,邻接矩阵的第 i 行(列)非零元素的个数正好是第 i 个顶点的出度(入度)。
  4. 邻接矩阵容易确定点之间是否相连,但是确定边的个数需要遍历。
  5. 稠密图适合使用邻接矩阵,存储稀疏图会浪费空间。

 

用邻接表存储有向图和无向图时会有一些缺点,如要求有向图中某节点的度,需要遍历整张邻接表,这是因为它只保存了每个节点的出度,而入度信息没有保存。 保存无向图时,需要把每个边存储两遍,会造成空间浪费,且要删除的时候需要找到两次和删除两次。

为解决这些问题,引入十字链表和邻接多重表

十字链表

概念

有向图的一种表示方式。
十字链表中每个弧和顶点都对应有一个结点。

  • 弧结点:tailvex, headvex, hlink, tlink, info
    • headvex, tailvex 分别指示头域和尾域。
    • hlink, tlink 链域指向弧头和弧尾相同的下一条弧。
    • info 指向该弧相关的信息。
  • 点结点:data, firstin, firstout
    • 以该点为弧头或弧尾的第一个结点。

定义

typedef struct ArcNode{
    int tailvex, headvex;
    struct ArcNode *hlink, *tlink;
    //InfoType info;
} ArcNode;
typedef struct VNode{
    int data;
    ArcNode *firstin, *firstout;
}VNode;
typeder struct{
    VNode xlist[MAX];
    int vexnum, arcnum;
} GLGrapha;

邻接多重表

概念

邻接多重表是无向图的一种链式存储方式。

边结点:

  • mark 标志域,用于标记该边是否被搜索过。
  • ivex, jvex 该边的两个顶点所在位置。
  • ilink 指向下一条依附点 ivex 的边。
  • jlink 指向下一条依附点 jvex 的边。
  • info 边相关信息的指针域。

点结点:

  • data 数据域
  • firstedge 指向第一条依附于改点的边。

邻接多重表中,依附于同一点的边串联在同一链表中,由于每条边都依附于两个点,所以每个点会在边中出现两次。

 

typedef struct ArcNode{
    bool mark;
    int ivex, jvex;
    struct ArcNode *ilink, *jlink;
    // InfoType info;
}ArcNode;
typedef struct VNode{
    int data;
    ArcNode *firstedge;
}VNode;
typedef struct {
    VNode adjmulist[MAX];
    int vexnum, arcnum;
} AMLGraph;

 

 

 

Trie树

链接:https://leetcode-cn.com/problems/implement-trie-prefix-tree/solution/shi-xian-trie-qian-zhui-shu-by-leetcode/    

    其他的数据结构,如平衡树和哈希表,使我们能够在字符串数据集中搜索单词。为什么我们还需要 Trie 树呢?尽管哈希表可以在 O(1)O(1) 时间内寻找键值,却无法高效的完成以下操作:

  • 找到具有同一前缀的全部键值。
  • 按词典序枚举字符串的数据集。

     Trie 树优于哈希表的另一个理由是,随着哈希表大小增加,会出现大量的冲突,时间复杂度可能增加到 O(n)O(n),其中 nn 是插入的键的数量。与哈希表相比,Trie 树在存储多个具有相同前缀的键时可以使用较少的空间。此时 Trie 树只需要 O(m)O(m) 的时间复杂度,其中 mm 为键长。而在平衡树中查找键值需要 O(m \log n)O(mlogn) 时间复杂度。

Trie 树的结点结构
Trie 树是一个有根的树,其结点具有以下字段:。

  • 最多 RR 个指向子结点的链接,其中每个链接对应字母表数据集中的一个字母。本文中假定 RR 为 26,小写拉丁字母的数量。
  • 布尔字段,以指定节点是对应键的结尾还是只是键前缀。

class Trie
{
private:
	bool is_string = false;
	Trie* next[26] = { nullptr };
public:
	Trie() {}

	void insert(const string& word)//插入单词
	{
		Trie* root = this;
		for (const auto& w : word) {
			if (root->next[w - 'a'] == nullptr)root->next[w - 'a'] = new Trie();
			root = root->next[w - 'a'];
		}
		root->is_string = true;
	}

	bool search(const string& word)//查找单词
	{
		Trie* root = this;
		for (const auto& w : word) {
			if (root->next[w - 'a'] == nullptr)return false;
			root = root->next[w - 'a'];
		}
		return root->is_string;
	}

	bool startsWith(string prefix)//查找前缀
	{
		Trie* root = this;
		for (const auto& p : prefix) {
			if (root->next[p - 'a'] == nullptr)return false;
			root = root->next[p - 'a'];
		}
		return true;
	}
};

 

多源bfs

给定一个由 0 和 1 组成的矩阵,找出每个元素到最近的 0 的距离。

两个相邻元素间的距离为 1 。

示例 1: 
输入:

0 0 0
0 1 0
0 0 0
输出:

0 0 0
0 1 0
0 0 0
示例 2: 
输入:

0 0 0
0 1 0
1 1 1
输出:

0 0 0
0 1 0
1 2 1
注意:

给定矩阵的元素个数不超过 10000。
给定矩阵中至少有一个元素是 0。
矩阵中的元素只在四个方向上相邻: 上、下、左、右。

思路:将原矩阵中元素为0的位置作为起点,push进队列中,然后一层层往下找

  • 我们需要对于每一个 1 找到离它最近的 0。如果只有一个 0 的话,我们从这个 0 开始广度优先搜索就可以完成任务了;
  • 但在实际的题目中,我们会有不止一个 0。我们会想,要是我们可以把这些 0 看成一个整体好了。有了这样的想法,我们可以添加一个「超级零」,它与矩阵中所有的 0 相连,这样的话,任意一个 1 到它最近的 00 的距离,会等于这个 1到「超级零」的距离减去一。由于我们只有一个「超级零」,我们就以它为起点进行广度优先搜索。这个「超级零」只和矩阵中的 0 相连,所以在广度优先搜索的第一步中,「超级零」会被弹出队列,而所有的 0 会被加入队列,它们到「超级零」的距离为 1。这就等价于:一开始我们就将所有的 0 加入队列,它们的初始距离为 0。这样以来,在广度优先搜索的过程中,我们每遇到一个 1,就得到了它到「超级零」的距离减去一,也就是 这个 1 到最近的 0 的距离。

 

class Solution {
public:
    int dir[4][2] = {-1,0,0,1,1,0,0,-1};
    vector<vector<int>> updateMatrix(vector<vector<int>>& matrix) {
        int m = matrix.size();
        int n = matrix[0].size();
        vector<vector<int>> ans(m,vector<int>(n,0));
        vector<vector<int>> visit(m,vector<int>(n,0));
        queue<pair<int,int>> q;
        for(int i = 0;i<m;i++){
            for(int j = 0;j<n;j++){
                if(matrix[i][j] == 0){
                    q.emplace(i,j);
                    visit[i][j] = 1;
                }
            }
        }

        while(!q.empty()){
            auto [i,j] = q.front();
            q.pop();
            for(int k = 0;k<4;k++){
                int ii = i + dir[k][0];
                int jj = j + dir[k][1];
                if(ii>=0 && ii<m && jj>=0 && jj<n && !visit[ii][jj]){
                    ans[ii][jj] = ans[i][j] + 1;
                    visit[ii][jj] = 1;
                    q.emplace(ii,jj);
                }
            }
        }
        return ans;
    }
};

 

拓扑排序 

leetcode 210.

现在你总共有 n 门课需要选,记为 0 到 n-1。

在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]

给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。

可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。

示例 1:

输入: 2, [[1,0]] 
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:

输入: 4, [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
     因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
说明:

输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
你可以假定输入的先决条件中没有重复的边。
提示:

这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
通过 DFS 进行拓扑排序 - 一个关于Coursera的精彩视频教程(21分钟),介绍拓扑排序的基本概念。
拓扑排序也可以通过 BFS 完成。

链接:https://leetcode-cn.com/problems/course-schedule-ii/solution/ke-cheng-biao-ii-by-leetcode-solution/
我们考虑拓扑排序中最前面的节点,该节点一定不会有任何入边,也就是它没有任何的先修课程要求。当我们将一个节点加入答案中后,我们就可以移除它的所有出边,代表着它的相邻节点少了一门先修课程的要求。如果某个相邻节点变成了「没有任何入边的节点」,那么就代表着这门课可以开始学习了。按照这样的流程,我们不断地将没有入边的节点加入答案,直到答案中包含所有的节点(得到了一种拓扑排序)或者不存在没有入边的节点(图中包含环)。

上面的想法类似于广度优先搜索,因此我们可以将广度优先搜索的流程与拓扑排序的求解联系起来。

算法

我们使用一个队列来进行广度优先搜索。初始时,所有入度为 0 的节点都被放入队列中,它们就是可以作为拓扑排序最前面的节点,并且它们之间的相对顺序是无关紧要的。

在广度优先搜索的每一步中,我们取出队首的节点 u:

我们将 u 放入答案中;

我们移除 u 的所有出边,也就是将 u 的所有相邻节点的入度减少 1。如果某个相邻节点 v 的入度变为 0,那么我们就将 v 放入队列中。

在广度优先搜索的过程结束后。如果答案中包含了这 n 个节点,那么我们就找到了一种拓扑排序,否则说明图中存在环,也就不存在拓扑排序了。
 

    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        vector<int> ans;
        if(numCourses <= 0)
            return ans;
        queue<int> q;
        vector<vector<int>> edges(numCourses);
        vector<int> in_degree(numCourses,0);
        for(auto pair : prerequisites){
            edges[pair[1]].push_back(pair[0]);  //存放本节点指向的相邻节点
            in_degree[pair[0]]++;               //存放入度
        }
        for(int i = 0 ; i < numCourses; i++){
            if(in_degree[i] == 0){
                q.push(i);
            }
        }
        while(!q.empty()){
            int u = q.front();
            q.pop();
            ans.push_back(u);
            for(int v : edges[u]){
                in_degree[v]--;     //把本节点指向的所有节点入度减1
                if(in_degree[v] == 0){
                    q.push(v);
                }
            }
        }
        vector<int> emp;
        return ans.size() == numCourses ? ans : emp;
    }

复杂度分析

时间复杂度: O(n + m)O(n+m),其中 n 为课程数,m 为先修课程的要求数。这其实就是对图进行广度优先搜索的时间复杂度。

空间复杂度: O(n + m)O(n+m)。题目中是以列表形式给出的先修课程关系,为了对图进行广度优先搜索,我们需要存储成邻接表的形式,空间复杂度为 O(m)O(m)。在广度优先搜索的过程中,我们需要最多 O(n)O(n) 的队列空间(迭代)进行广度优先搜索,并且还需要若干个 O(n)O(n) 的空间存储节点入度、最终答案等。

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值