【題目記錄】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) 的空間存儲節點入度、最終答案等。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章