目录
岛屿数量
给定一个由 '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) 的空间存储节点入度、最终答案等。