本文总结数据结构图相关的知识和leetcode高频题。本文前半部分知识点总结和代码参考自JavaScript数据结构与算法。
什么是图
- 图是
网络结构
的抽象模型,是一组由边
连接的节点
。 - 图可以表示任何二元关系,比如道路,航班。
- JS中没有图,但是可以用Object和Array构建图。
- 图的表示法: 邻接矩阵,邻接表,关联矩阵。
- 图的深度优先遍历:尽可能深的搜索图的分支。
- 图的广度优先遍历:先访问离根节点最近的节点
图的深度优先遍历
- 访问根节点
- 对根节点的
没访问过的相邻节点
挨个进行深度优先遍历
const graph = {
0: [1, 2],
1: [2],
2: [0, 3],
3: [3]
};
const visited = new Set();
const dfs = (n) => {
visited.add(n);
graph[n].forEach(c => {
if (!visited.has(c)) {
dfs(c);
}
});
}
dfs(2); // 2 -> 0 -> 1 -> 3
图的广度优先遍历
- 新建一个队列,把根节点入队
- 把队头出队并访问
- 把队头的没访问过的相邻节点入队
- 重复第二,三步,直到队列为空
const visited = new Set();
const bfs = (root) => {
visited.add(root);
const q = [root];
while (q.length) {
const n = q.shift();
console.log(n);
graph[n].forEach(c => {
if (!visited.has(c)) {
q.push(c);
visited.add(c);
}
})
}
}
bfs(2); // 2 -> 0 -> 3 -> 1
leetcode题目
思路:
- 图的深度优先遍历
代码展示:
/**
* @param {number[][]} heights
* @return {number[][]}
*/
var pacificAtlantic = function(heights) {
if (!heights || !heights[0]) return [];
const m = heights.length;
const n = heights[0].length;
const flow1 = Array.from({ length: m }, () => new Array(n).fill(false));
const flow2 = Array.from({ length: m }, () => new Array(n).fill(false));
const dfs = (r, c, flow) => {
flow[r][c] = true;
[[r-1, c], [r+1, c], [r, c-1], [r, c+1]].forEach(([nr, nc]) => {
if (
// 保证在矩阵中
nr >= 0 && nr < m &&
nc >= 0 && nc < n &&
// 节点没有被访问过
!flow[nr][nc] &&
// 保证逆流而上
heights[nr][nc] >= heights[r][c]
) {
dfs(nr, nc, flow);
}
});
}
// 沿着海岸线逆流而上
for (let r = 0; r < m; r++) {
dfs(r, 0, flow1);
dfs(r, n-1, flow2);
}
for (let c = 0; c < n; c++) {
dfs(0, c, flow1);
dfs(m-1, c, flow2);
}
// 收集能流到两个大洋的坐标
const res = [];
for (let r = 0; r < m; r++) {
for (let c = 0; c < n; c++) {
if (flow1[r][c] && flow2[r][c]) {
res.push([r, c]);
}
}
}
return res;
};
复杂度分析:
- 时间复杂度:O(mn)。m代表矩阵的列,n代表矩阵的行。
- 空间复杂度:O(mn)
方法一:深度优先遍历
/**
* @param {Node} node
* @return {Node}
*/
var cloneGraph = function(node) {
if (!node) return;
const visited = new Map();
const dfs = (n) => {
const nCopy = new Node(n.val);
visited.set(n, nCopy);
(n.neighbors || []).forEach(ne => {
if (!visited.has(ne)) {
dfs(ne);
}
nCopy.neighbors.push(visited.get(ne));
});
}
dfs(node);
return visited.get(node);
};
方法二:广度优先遍历
/**
* @param {Node} node
* @return {Node}
*/
var cloneGraph = function(node) {
if (!node) return;
const visited = new Map();
const q = [node];
visited.set(node, new Node(node.val));
while (q.length) {
const n = q.shift();
(n.neighbors || []).forEach(ne => {
if (!visited.has(ne)) {
q.push(ne);
visited.set(ne, new Node(ne.val));
}
visited.get(n).neighbors.push(visited.get(ne));
})
}
return visited.get(node);
};
复杂度分析:
- 时间复杂度:O(n)。n代表图的节点数。
- 空间复杂度:O(n)
代码展示:
/**
* @param {number} numCourses
* @param {number[][]} prerequisites
* @return {boolean}
*/
var canFinish = function(numCourses, prerequisites) {
const inDegree = new Array(numCourses).fill(0); // 入度数组
const map = {}; // 邻接表
for (let i = 0; i < prerequisites.length; i++) {
inDegree[prerequisites[i][0]]++; // 求课程的初始入度值
if (map[prerequisites[i][1]]) { // 当前课程是否存在于邻接表中
map[prerequisites[i][1]].push(prerequisites[i][0]); // 添加依赖当前i课程的后续课
} else { // 当前课程不存在于邻接表中
map[prerequisites[i][1]] = [prerequisites[i][0]]; // 创建一个存放依赖的数组,并且将依赖课放进去
}
}
const queue = []; // 存放入度为0的课
for (let i = 0; i < inDegree.length; i++) { // 所有入度为0的课入列
if (inDegree[i] === 0) queue.push(i); // i 对应课,inDegree[i]是当前课的入度值
}
let count = 0;
while (queue.length) {
const n = queue.shift(); // 当前选的课出列
count++; // 选课数 +1
const toEnQueue = map[n]; // 获取这门课对应的后续课
toEnQueue && toEnQueue.length && toEnQueue.forEach(item => { // 如果确实有这门课
inDegree[item]--; // 依赖当前课的后续课 -1
if (inDegree[item] === 0) { // 一直减为0
queue.push(item); // 将依赖课也push到队列中
}
});
}
return count === numCourses; // 选了的课等于总课数,则返回true,否则返回false
};
复杂度分析:
- 时间复杂度:O(m+n):m代表课程数,n代表先修课程的要求数。
- 空间复杂度:O(m+n)
代码参考自拓扑排序思路一步步形成,类BFS
代码展示:
/**
* @param {number} numCourses
* @param {number[][]} prerequisites
* @return {number[]}
*/
var findOrder = function(numCourses, prerequisites) {
const inDegree = new Array(numCourses).fill(0); // 入度数组
const map = {}; // 邻接表
for (let i = 0; i < prerequisites.length; i++) {
inDegree[prerequisites[i][0]]++; // 求课程的初始入度值
if (map[prerequisites[i][1]]) { // 当前课程是否存在于邻接表中
map[prerequisites[i][1]].push(prerequisites[i][0]); // 添加依赖当前i课程的后续课
} else { // 当前课程不存在于邻接表中
map[prerequisites[i][1]] = [prerequisites[i][0]]; // 创建一个存放依赖的数组,并且将依赖课放进去
}
}
const queue = []; // 存放入度为0的课
for (let i = 0; i < inDegree.length; i++) { // 所有入度为0的课入列
if (inDegree[i] === 0) queue.push(i); // i 对应课,inDegree[i]是当前课的入度值
}
const res = []; // 存放结果数组
while (queue.length) {
const n = queue.shift(); // 当前选的课出列
res.push(n); // 将所有入度为0的课推入结果数组
const toEnQueue = map[n]; // 获取这门课对应的后续课
toEnQueue && toEnQueue.length && toEnQueue.forEach(item => { // 如果确实有这门课
inDegree[item]--; // 依赖当前课的后续课 -1
if (inDegree[item] === 0) { // 一直减为0
queue.push(item); // 将依赖课也push到队列中
}
});
}
return res.length === numCourses ? res : []; // 选了的课等于总课数,返回res,否则返回[]
};
复杂度分析:
- 时间复杂度:O(m+n):m代表课程数,n代表先修课程的要求数。
- 空间复杂度:O(m+n)
代码展示:
/**
* @param {number[][]} graph
* @return {boolean}
*/
var isBipartite = function(graph) {
const visited = new Array(graph.length); // undefined为未染色,1为蓝色,-1为黄色
for (let i = 0; i < visited.length; i++) { // 遍历每个顶点
if (visited[i]) continue; // 如果这个顶点已上色,继续遍历
const queue = [i]; // 队列初始推入顶点 i
visited[i] = 1; // 染为蓝色
while (queue.length) { // 遍历顶点 i 所有相邻的顶点
const cur = queue.shift(); // 考察出列的顶点
const curColor = visited[cur]; // 出列顶点的颜色
const neighborColor = -curColor; // 它的相邻顶点应该有的颜色
for (let i = 0; i < graph[cur].length; i++) { // 给相邻节点都上色
const neighbor = graph[cur][i];
if (visited[neighbor] == undefined) { // 这个相邻节点没上色的话
visited[neighbor] = neighborColor; // 上色
queue.push(neighbor); // 推入队中
} else if (visited[neighbor] != neighborColor) { // 上的颜色不对
return false;
}
}
}
}
return true; // 遍历完所有节点,并没有发现错误的颜色
};
复杂度分析:
- 时间复杂度:O(m+n):n是顶点个数,m是边数。
- 空间复杂度:O(n)
代码展示:
/**
* @param {number} n
* @param {number[][]} edges
* @param {number} distanceThreshold
* @return {number}
*/
var findTheCity = function(n, edges, distanceThreshold) {
const max = Number.MAX_SAFE_INTEGER;
const distance = Array.from({length: n}, () => new Array(n).fill(max)); // 构造一个n*n的二维数组
// console.log(distance);
for (let i = 0; i < edges.length; i++) {
distance[edges[i][0]][edges[i][1]] = distance[edges[i][1]][edges[i][0]] = edges[i][2];
}
for (let i = 0; i < n; ++i) {
for (let j = 0; j < n; ++j) {
if (i === j || distance[j][i] === max) continue;
for (let k = j + 1; k < n; ++k) {
distance[k][j] = distance[j][k] = Math.min(distance[j][k], distance[j][i] + distance[i][k]);
}
}
}
let city = 0;
let minNum = n;
for (let i = 0; i < n; ++i) {
let curNum = 0;
for (let j = 0; j < n; ++j) {
distance[i][j] <= distanceThreshold && ++curNum;
}
if (curNum <= minNum) { minNum = curNum; city = i; }
}
return city;
};
总结:
图和树很类似,树是分层数据结构的抽象模型,图是网络结构的抽象模型。图的可能性会更多更复杂一些。