目錄
島嶼數量
給定一個由 '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.遞歸:
如果同時滿足下面的條件,兩個樹互爲鏡像:
- 它們的兩個根結點具有相同的值。
- 每個樹的右子樹都與另一個樹的左子樹鏡像對稱。
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;
性質
- 若G爲無向圖,則所需的存儲空間爲O(|V|+2|E|),若G爲有向圖,則所需的存儲空間爲O(|V|+|E|)。前者倍數是後者兩倍是因爲每條邊在鄰接表中出現了兩次。
- 鄰接表法比較適合於稀疏圖。
- 點找邊很容易,邊找點不容易。
- 鄰接表的表示不唯一
鄰接矩陣:
在鄰接矩陣實現中,由行和列都表示頂點,由兩個頂點所決定的矩陣對應元素表示這裏兩個頂點是否相連、如果相連這個值表示的是相連邊的權重。例如,如果從頂點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
性質:
- 無向圖的鄰接矩陣爲對稱矩陣,可以只用上或下三角。
- 對於無向圖,鄰接矩陣的第 i 行(列)非零元素的個數正好是第 i 個頂點的度 。
- 對於有向圖,鄰接矩陣的第 i 行(列)非零元素的個數正好是第 i 個頂點的出度(入度)。
- 鄰接矩陣容易確定點之間是否相連,但是確定邊的個數需要遍歷。
- 稠密圖適合使用鄰接矩陣,存儲稀疏圖會浪費空間。
用鄰接表存儲有向圖和無向圖時會有一些缺點,如要求有向圖中某節點的度,需要遍歷整張鄰接表,這是因爲它只保存了每個節點的出度,而入度信息沒有保存。 保存無向圖時,需要把每個邊存儲兩遍,會造成空間浪費,且要刪除的時候需要找到兩次和刪除兩次。
爲解決這些問題,引入十字鏈表和鄰接多重表
十字鏈表
概念
有向圖的一種表示方式。
十字鏈表中每個弧和頂點都對應有一個結點。
- 弧結點: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) 的空間存儲節點入度、最終答案等。