Graph (图)
什么是图?
如果说允许某个二叉树之间的节点全部随意连接或者断开,那么这就是一个图。
什么是 E, V,什么是 G 本身
E: edges
V: vertices
where V is a set whose elements are called vertices (singular: vertex), and E is a set of paired vertices, whose elements are called edges (sometimes links or lines).
为什么 graph 的数据结构是这个样子
class Node {
public:
int val;
vector<Node*> neighbors;
}
它的本质就是一个 Node,这个 Node 周围有与它相同的 Node neighbors,这个 vector<Node*> neighbors 就表示与这个 Node 相邻的其它 Node 的 vector,对于 cpp 来说,它是存储相邻节点的指针
我们可以类比二叉树的数据结构
// binary tree
class TreeNode {
int val;
TreeNode* left;
TreeNode* right;
}
// N-ary tree
class NaryTreeNode {
int value;
vector<NaryTreeNode*> children;
}
我们还可以知道的是
邻接表
邻接表就是把刚才的 Node 的 neighbors 存在一个列表里,然后把这个列表和这个 Node 关联起来,这就是邻接表
邻接矩阵
邻接矩阵实质就是一个 bool[][]
,它做的是把相邻的 Node 在矩阵中的交叉点值设为真,这就是邻接矩阵
我们可以比较的是
显然,对于一个邻接矩阵,要查找某两个 Node 是否相邻,只需要判断 graph[x][y]
是否为真就可以,速度极快
而,对于一个邻接表,要查找某两个 Node 是否相邻,要遍历其中一个 Node 对应的列表中是否有两外一个 Node 的值,效率上不如邻接矩阵
连通图 & 非连通图?
任意两个 Node 之间都存在连接的就是连通图,非则非莲图
有向加权图?
上面的实现实际上是个有向图,所谓的权重就是某个 Node 到另外一个 Node 所需的 代价
,不可到达则代价为 ∞,Node 到本身代价为 0
有向图还有 入度
和 出度
的概念,就是 a -> b,那么 a 就是 b 的入度,b 就是 a 的出度
实现加权图只需要将刚才的 bool[][]
变为 int[][]
就可以了,如果这个矩阵某处为 0,那么就代表这两点是不连通的,graph[x][y]
就是这两个 Node 之间的 权重
成环?
有向图中可能存在环,那么对有向图构造一个 Node 的拓扑序列,如果图的所有 Node 都在这个序列中,那么这个图是有向无环图
无向图?
把刚才的 graph[x][y]
和 graph[y][x]
都变为 true 就可以,那么这两个 Node 之间就相互连接了
邻接表表示法也是这样,在对应的 Node 列表里添加其它 Node 就可以
如何遍历图?
在上面提到了图就是一个二叉树随意连接的产物,那么我们可以考虑多叉数的遍历
const traverse = (treeNode: TreeNode) => {
if (!TreeNode) {
return
}
for (const neighbor of treeNode.children) {
traverse(neighbor)
}
}
用这个遍历图的话,会产生的问题是产生闭环,可以使用一个 visited
数组解决这个问题,在遍历时记录哪个 Node 被访问过了,然后就不用访问了,返回访问其它节点,类似于回溯
// dfs
type Graph = Record<string, string[]>
const dfs = (graph: Graph, node: string, visited: Record<string, boolean>) => {
if (!node || visited[node]) {
return
}
visited[node] = true
console.log(node)
for (const neighbor of graph[node]) {
dfs(graph, neighbor, visited);
}
}
需要注意的是,这里我们的 console.log 是在 for 循环外部的,这关注的是 Node 的 value 本身,如果将 console.log 放在 for 循环内部,那么这关注的是 Node 的 child,也就是 path 问题,这就变成回溯算法了
bfs 是从起始 Node 开始逐层访问其它相邻节点,本质还是树的问题,可以使用 bfs 求两个 Node 之间的最短路径
Q1: 已知邻接矩阵,构造图并返回根节点
这个图是一个有向图,没有入度的 Node 就是它的 rootNode
class GraphNode {
val: number
neighbors: GraphNode[]
constructor(val?: number, neighbors?: GraphNode[]) {
this.val = val === undefined ? 0 : val
this.neighbors = neighbors === undefined ? [] : neighbors
}
}
class Graph {
nodes: GraphNode[]
constructor(adjacencyMatrix: number[][]) {
const numNodes = adjacencyMatrix.length
this.nodes = []
// create node
for (let i = 0; i < numNodes; i++) {
this.nodes.push(new GraphNode(i))
}
// link node
for (let i = 0; i < numNodes; i++) {
for (let j = 0; j < numNodes; j++) {
if (adjacencyMatrix[i][j] === 1) {
this.nodes[i].neighbors.push(this.nodes[j])
}
}
}
}
findRootNode(): GraphNode | null {
for (const node of this.nodes) {
if (this.isRoot(node)) {
return node
}
}
return null
}
isRoot(node: GraphNode): boolean {
for (const otherNode of this.nodes) {
if (otherNode.neighbors.includes(node)) {
return false
}
}
return true
}
}
const adjacencyMatrix: number[][] = [
[0, 0, 0, 0],
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
]
const noRootAdjacencyMatrix: number[][] = [
[0, 0, 0, 1],
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
]
const graph = new Graph(adjacencyMatrix)
// const graph = new Graph(noRootAdjacencyMatrix)
const rootNode = graph.findRootNode()
if (rootNode) {
console.log('Root Node! ', rootNode.val)
} else {
console.log('No root node! ')
}
output
Root Node! 3
No root node!
Q2: 根据某个图的根节点构造一个邻接矩阵
我们直接用上面创建好的图
const dfsCountNodes = (rootNode: GraphNode) => {
const visited = new Set<GraphNode>()
const stack = [rootNode]
while (stack.length) {
const currentNode = stack.pop()
if (!currentNode) {
break
}
visited.add(currentNode)
for (const neighbor of currentNode.neighbors) {
if (!visited.has(neighbor)) {
stack.push(neighbor)
}
}
}
return visited.size
}
const createAdjacencyMatrix = (rootNode: GraphNode | null): number[][] => {
if (!rootNode) {
return []
}
// Method1: calculate node sum ,because this.nodes.push(new GraphNode(i))
// const numNodes = rootNode.val + 1
const adjacencyMatrix: number[][] = []
// Method2: calculate node sum
const numNodes = dfsCountNodes(rootNode)
// initialize
for (let i = 0; i < numNodes; i++) {
adjacencyMatrix.push(Array(numNodes).fill(0))
}
const dfs = (node: GraphNode) => {
for (const neighbor of node.neighbors) {
adjacencyMatrix[node.val][neighbor.val] = 1
dfs(neighbor)
}
}
dfs(rootNode)
return adjacencyMatrix
}
console.table(adjacencyMatrix)
console.table(createAdjacencyMatrix(rootNode))
这里我们上面在创造图的时候使用了 this.nodes.push(new GraphNode(i)),因此 root Node 的 val + 1 就是 Node 的总数
倘若我们不使用 i 来作为默认值,使用其他值来作为图的权重,我们可以使用一个 dfs 来计算图的 Node 个数,这是 Method2
Q3: 求某个图任意两节点的最短距离
这个问题我们可以使用 bfs 处理
为什么 bfs 可以解决最短路径问题?
- bfs 的原理是从 root Node 的层面逐层遍历,逐渐扩散至更远的 Node
- 这种方法意味着,当首次遍历 Node 到达 endNode 时距离是最短的
class GraphNode {
val: number
neighbors: GraphNode[]
constructor(val?: number, neighbors?: GraphNode[]) {
this.val = val === undefined ? 0 : val
this.neighbors = neighbors === undefined ? [] : neighbors
}
}
class Graph {
nodes: GraphNode[]
constructor(adjacencyMatrix: number[][]) {
const numNodes = adjacencyMatrix.length
this.nodes = []
// create node
for (let i = 0; i < numNodes; i++) {
this.nodes.push(new GraphNode(i + 1))
}
// link node
for (let i = 0; i < numNodes; i++) {
for (let j = 0; j < numNodes; j++) {
if (adjacencyMatrix[i][j] === 1) {
this.nodes[i].neighbors.push(this.nodes[j])
}
}
}
}
findNodeByValue(value: number): GraphNode | null {
for (const node of this.nodes) {
if (node.val === value) {
return node
}
}
return null
}
}
const adjacencyMatrix: number[][] = [
[0, 1, 0, 0],
[1, 0, 1, 0],
[1, 1, 0, 0],
[0, 0, 1, 0],
]
const graph = new Graph(adjacencyMatrix)
const minDistance = (
startNode: GraphNode | null,
endNode: GraphNode | null
) => {
if (!startNode || !endNode) {
return -1
}
if (startNode === endNode) {
return 0
}
const queue = [startNode]
const visited = new Set<GraphNode>()
visited.add(startNode)
let distance = 0
while (queue.length) {
const size = queue.length
for (let i = 0; i < size; i++) {
const currentNode = queue.shift()
if (!currentNode) {
return
}
for (const neighbor of currentNode.neighbors) {
if (neighbor === endNode) {
return distance + 1
}
if (!visited.has(neighbor)) {
visited.add(neighbor)
queue.push(neighbor)
}
}
}
distance++
}
return -1
}
const node2 = graph.findNodeByValue(1)
const node1 = graph.findNodeByValue(3)
const distance =
minDistance(node1, node2) === -1
? minDistance(node2, node1)
: minDistance(node1, node2)
if (distance === -1) {
console.log('The two nodes are not reachable')
} else {
console.log(`The shortest distance is: ${distance}`)
}
我们还可以用 Dijkstra 等算法实现两点最短路径,它适用于有权重的有向图,它不适用于无向图和无负权的有向图,然而 bfs 是适合无向图的
如果要找出所有 Node 之间的最短路径,我们可以使用 Floyed 算法,它的思想上试探每一个 Node,从任意两个 Node 之间所有可能存在的路径中选出一条最短的路径
感谢真红姐姐的指导
感谢真红姐姐出题及指导,给我思考问题的机会,GitHub Repo
肯定还有不对或者尚待改进的地方,欢迎联系我指出我的错误,大家的每一次纠错都是对我的最大帮助,谢谢