如何在JavaScript中实现8种基本图算法

In this article, I will implement 8 graph algorithms that explore the search and combinatorial problems (traversals, shortest path and matching) of graphs in JavaScript.

在本文中,我将实现8种图形算法 ,以探讨JavaScript中图形的搜索和组合问题(遍历,最短路径和匹配)。

The problems are borrowed from the book, Elements of Programming Interviews in Java. The solutions in the book are coded in Java, Python or C++ depending on what version of the book you own.

这些问题是从《 Java编程面试的要素 》一书中借用的。 本书中的解决方案使用Java,Python或C ++进行编码,具体取决于您所拥有的本书的版本。

Although the logic behind the modeling of the problems is language-agnostic, the code snippets I provide in this article use some JavaScript caveats.

尽管问题建模背后的逻辑与语言无关,但是我在本文中提供的代码片段使用了一些JavaScript警告。

Every solution to each problem is broken down into 3 sections: an overview of the solution, the pseudocode, and lastly the actual code in JavaScript.

每个问题的每种解决方案都分为3个部分:解决方案概述,伪代码,最后是JavaScript中的实际代码。

To test the code and see it do what it is supposed to do, you can use Chrome’s Dev Tools to run the snippets on the browser itself or use NodeJS to run them from the command line.

要测试代码并查看其功能,可以使用Chrome的开发工具在浏览器本身上运行代码片段,也可以使用NodeJS从命令行运行它们。

图的实现 (Graph implementation)

The 2 most commonly used representations of graphs are the adjacency list and adjacency matrix.

图的2种最常用的表示形式是邻接表和邻接矩阵。

The problems I’ll be solving are for sparse graphs (few edges), and the vertex operations in the adjacency list approach take constant (adding a vertex, O(1)) and linear time (deleting a vertex, O(V+E)). So I’ll stick with that implementation for the most part.

我要解决的问题是稀疏图(边缘很少),并且邻接表方法中的顶点运算采用常数(添加顶点O(1))和线性时间(删除顶点O(V + E) ))。 因此,我将大部分时间坚持该实现。

Let’s knock this out with a simple undirected, unweighted graph implementation using adjacency list. We’ll maintain an object (adjacencyList) that will contain all the vertices in our graph as the keys. The values will be an array of all the adjacent vertices. In the example below, vertex 1 is connected to vertices 2 and 4, hence adjacencyList: { 1 : [ 2, 4 ] } and so on for the other vertices.

让我们使用邻接表通过简单的无向,无权图实现将其淘汰掉。 我们将维护一个对象(adjacencyList),该对象将包含图形中的所有顶点作为键。 值将是所有相邻顶点的数组。 在下面的示例中,顶点1连接到顶点2和4,因此相邻列表的邻接表为{1:[2,4],依此类推。

To build the graph, we have two functions: addVertex and addEdge. addVertex is used to add a vertex to the list. addEdge is used to connect the vertices by adding the neighboring vertices to both the source and destination arrays since this is an undirected graph. To make a directed graph, we can simply remove lines 14–16 and 18 in the code below.

要构建图,我们有两个功能: addVertexaddEdge 。 addVertex用于将顶点添加到列表中。 addEdge用于通过将相邻顶点添加到源数组和目标数组来连接这些顶点,因为这是无向图。 要制作有向图,我们只需在下面的代码中删除第14–16和18行即可。

Before removing a vertex, we need to iterate through the array of neighboring vertices and remove all possible connections to that vertex.

在删除顶点之前,我们需要遍历相邻顶点的数组,并删除到该顶点的所有可能连接。

class Graph {
  constructor() {
    this.adjacencyList = {};
  }
  addVertex(vertex) {
    if (!this.adjacencyList[vertex]) {
      this.adjacencyList[vertex] = [];
    }
  }
  addEdge(source, destination) {
    if (!this.adjacencyList[source]) {
      this.addVertex(source);
    }
    if (!this.adjacencyList[destination]) {
      this.addVertex(destination);
    }
    this.adjacencyList[source].push(destination);
    this.adjacencyList[destination].push(source);
  }
  removeEdge(source, destination) {
    this.adjacencyList[source] = this.adjacencyList[source].filter(vertex => vertex !== destination);
    this.adjacencyList[destination] = this.adjacencyList[destination].filter(vertex => vertex !== source);
  }
  removeVertex(vertex) {
    while (this.adjacencyList[vertex]) {
      const adjacentVertex = this.adjacencyList[vertex].pop();
      this.removeEdge(vertex, adjacentVertex);
    }
    delete this.adjacencyList[vertex];
  }  
}

图遍历 (Graph traversals)

Building on our implementation of graphs in the previous section, we’ll implement the graph traversals: breadth first search and depth first search.

基于上一节中图的实现,我们将实现图遍历:广度优先搜索和深度优先搜索。

BFS visits the nodes one level at a time. To prevent visiting the same node more than once, we’ll maintain a visited object.

BFS一次访问节点一次 。 为了防止多次访问同一节点,我们将维护一个被访问的对象。

Since we need to process the nodes in a First In First Out fashion, a queue is a good contender for the data structure to use. The time complexity is O(V+E).

由于我们需要以先进先出的方式处理节点,因此队列是要使用的数据结构的良好竞争者。 时间复杂度为O(V + E)。

function BFS
   Initialize an empty queue, empty 'result' array & a 'visited' map
   Add the starting vertex to the queue & visited map
   While Queue is not empty:
     - Dequeue and store current vertex
     - Push current vertex to result array
     - Iterate through current vertex's adjacency list:
       - For each adjacent vertex, if vertex is unvisited:
         - Add vertex to visited map
         - Enqueue vertex
   Return result array

DFS visits the nodes depth wise. Since we need to process the nodes in a Last In First Out manner, we’ll use a stack.

DFS深度访问节点。 由于我们需要以后进先出的方式处理节点,因此我们将使用stack

Starting from a vertex, we’ll push the neighboring vertices to our stack. Whenever a vertex is popped, it is marked visited in our visited object. Its neighboring vertices are pushed to the stack. Since we are always popping a new adjacent vertex, our algorithm will always explore a new level.

从顶点开始,我们将相邻的顶点推入堆栈。 每当弹出顶点时,它就会在我们的访问对象中标记为已访问。 它的相邻顶点被推入堆栈。 由于我们总是弹出新的相邻顶点,因此我们的算法将始终探索新的水平

We can also use the intrinsic stack calls to implement DFS recursively. The logic is the same.

我们还可以使用内部堆栈调用来递归地实现DFS。 逻辑是一样的。

The time complexity is the same as BFS, O(V+E).

时间复杂度与BFS,O(V + E)相同。

function DFS
   Initialize an empty stack, empty 'result' array & a 'visited' map
   Add the starting vertex to the stack & visited map
   While Stack is not empty:
     - Pop and store current vertex
     - Push current vertex to result array
     - Iterate through current vertex's adjacency list:
       - For each adjacent vertex, if vertex is unvisited:
         - Add vertex to visited map
         - Push vertex to stack
   Return result array
Graph.prototype.bfs = function(start) {
    const queue = [start];
    const result = [];
    const visited = {};
    visited[start] = true;
    let currentVertex;
    while (queue.length) {
      currentVertex = queue.shift();
      result.push(currentVertex);
      this.adjacencyList[currentVertex].forEach(neighbor => {
        if (!visited[neighbor]) {
          visited[neighbor] = true;
          queue.push(neighbor);
        }
      });
    }
    return result;
}
Graph.prototype.dfsRecursive = function(start) {
    const result = [];
    const visited = {};
    const adjacencyList = this.adjacencyList;
    (function dfs(vertex){
      if (!vertex) return null;
      visited[vertex] = true;
      result.push(vertex);
      adjacencyList[vertex].forEach(neighbor => {
          if (!visited[neighbor]) {
            return dfs(neighbor);
          }
      })
    })(start);
    return result;
}
Graph.prototype.dfsIterative = function(start) {
    const result = [];
    const stack = [start];
    const visited = {};
    visited[start] = true;
    let currentVertex;
    while (stack.length) {
      currentVertex = stack.pop();
      result.push(currentVertex);
      this.adjacencyList[currentVertex].forEach(neighbor => {
        if (!visited[neighbor]) {
          visited[neighbor] = true;
          stack.push(neighbor);
        }
      });
    }
    return result;
}

搜索迷宫 (Search Maze)

Problem Statement:

问题陈述:

Given a 2D array of black and white entries representing a maze with designated entrance and exit points, find a path from the entrance to the exit, if one exists. – Aziz, Adnan, et al. Elements of Programming Interviews

给定二维阵列,黑白阵列代表具有指定入口和出口点的迷宫,请找到从入口到出口的路径(如果存在)。 – Aziz,Adnan等。 编程面试的要素

We’ll represent the white entries with 0’s and black entries with 1’s. The white entries represent open areas and the black entries walls. The entrance and the exit points are represented by an array, the 0th index and the 1st index filled with the row and column indices, respectively.

我们将用0表示白色条目,用1表示黑色条目。 白色条目代表开放区域,黑色条目代表壁。 入口和出口点由一个数组表示,第0个索引和第一个索引分别填充有行索引和列索引。

Solution:

解:

  • To move to a different position, we’ll hardcode the four possible movements in the directions array (right, bottom, left and top; no diagonal moves):

    为了移动到另一个位置,我们将对Directions 数组中的四个可能的移动进行硬编码(右,下,左和上;没有对角线移动):

[ [0,1], [1,0], [0,-1], [-1,0] ]
  • To keep track of the cells we have already visited, we will replace the white entries (0’s) with black entries (1's). We are basically using DFS recursively to traverse the maze. The base case, that will end the recursion, is either we have reached our exit point and return true or we have visited every white entry and return false.

    要跟踪我们已经到过的细胞,我们将取代白项( 均为0),黑色项(1级的 )。 我们基本上是递归地使用DFS来穿越迷宫。 将结束递归的基本情况是我们已经到达出口点并返回true,或者我们访问了每个白色条目并返回false

  • Another important thing to keep track of is to ensure that we are within the boundaries of the maze all the time and that we only proceed if we are at a white entry. The isFeasible function will take care of that.

    要注意的另一件事是确保我们一直处于迷宫的边界内 ,并且只有在进入白宫时才继续前进isFeasible函数将解决此问题。

  • Time Complexity: O(V+E)

    时间复杂度:O(V + E)

Pseudocode:

伪代码:

function hasPath
   Start at the entry point
   While exit point has not been reached
     1. Move to the top cell
     2. Check if position is feasible (white cell & within boundary)
     3. Mark cell as visited (turn it into a black cell)
     4. Repeat steps 1-3 for the other 3 directions
var hasPath = function(maze, start, destination) {
    maze[start[0]][start[1]] = 1;
    return searchMazeHelper(maze, start, destination);
};
function searchMazeHelper(maze, current, end) { // dfs
    if (current[0] == end[0] && current[1] == end[1]) {
        return true;
    }
    let neighborIndices, neighbor;
    // Indices: 0->top,1->right, 2->bottom, 3->left 
    let directions = [ [0,1] , [1,0] , [0,-1] , [-1,0] ];
    for (const direction of directions) {
        neighborIndices = [current[0]+direction[0], current[1]+direction[1]];
        if (isFeasible(maze, neighborIndices)) {
            maze[neighborIndices[0]][neighborIndices[1]] = 1;
            if (searchMazeHelper(maze, neighborIndices, end)) {
                return true;
            }
        }
    }
    return false;
}
function isFeasible(maze, indices) {
    let x = indices[0], y = indices[1];
    return x >= 0 && x < maze.length && y >= 0 && y < maze[x].length && maze[x][y] === 0;
}
var maze = [[0,0,1,0,0],[0,0,0,0,0],[0,0,0,1,0],[1,1,0,1,1],[0,0,0,0,0]]
hasPath(maze, [0,4], [3,2]);

绘制布尔矩阵 (Paint a Boolean Matrix)

Problem Statement:

问题陈述:

Implement a routine that takes an n X m Boolean array A together with an entry (x, y) and flips the color of the region associated with (x, y). – Aziz, Adnan, et al. Elements of Programming Interviews

实现一个例程,该例程采用n X m布尔数组A和条目(x,y)并翻转与(x,y)关联的区域的颜色。 Aziz,Adnan等。 编程面试的要素

The 2 colors will be represented by 0’s and 1’s.

2种颜色将由0和1表示。

In the example below, we start in the center of the array ([1,1]). Note that from that position, we can only reach the upper, leftmost triangular matrix. The rightmost, lowest position cannot be reached ([2,2]). Hence, at the end of the process, it’s the only color that is not flipped.

在下面的示例中,我们从数组([1,1])的中心开始。 请注意,从该位置开始,我们只能到达最左上角的三角矩阵。 无法到达最右边的最低位置([2,2])。 因此,在过程结束时,这是唯一不会翻转的颜色。

Solution:

解:

  • Like in the previous question, we will code an array to define the 4 possible moves.

    像上一个问题一样,我们将对数组进行编码以定义4种可能的移动。
  • We’ll use BFS to traverse the graph.

    我们将使用BFS遍历图形。
  • We’ll modify the isFeasible function slightly. It will still check if the new position is within the boundaries of the matrix. The other requirement is that the new position is colored the same as the previous position. If the new position fits the requirements, its color is flipped.

    我们将稍微修改isFeasible函数。 它将仍然检查新位置是否在矩阵的边界内。 另一个要求是,新职位的颜色应与先前职位的颜色相同。 如果新位置符合要求,则将其颜色翻转。
  • Time complexity: O(mn)

    时间复杂度:O(mn)

Pseudocode:

伪代码:

function flipColor
   Start at the passed coordinates and store the color
   Initialize queue
   Add starting position to queue
   While Queue is not empty:
     - Dequeue and store current position
     - Move to the top cell
       1. Check if cell is feasible
       2. If feasible,
          - Flip color
          - Enqueue cell
       3. Repeat steps 1-2 for the other 3 directions
function flipColor(image, x, y) {
    let directions = [ [0,1] , [1,0] , [0,-1] , [-1,0] ];
    let color = image[x][y];
    let queue = [];
    image[x][y] = Number(!color);
    queue.push([x,y]);
    let currentPosition, neighbor;
    while (queue.length) {
        currentPosition = queue.shift();
        for (const direction of directions) {
            neighbor = [currentPosition[0]+direction[0], currentPosition[1]+direction[1]];
            if (isFeasible(image, neighbor, color)) {
                image[neighbor[0]][neighbor[1]] = Number(!color);
                queue.push([neighbor[0], neighbor[1]]);
            }
        }
    }
    return image;
}
function isFeasible(image, indices, color) {
    let x = indices[0], y = indices[1];
    return x >= 0 && x < image.length && y >= 0 && y < image[x].length && image[x][y] == color;
}
var image = [[1,1,1],[1,1,0],[1,0,1]];
flipColor(image,1,1);

计算封闭区域 (Compute Enclosed Regions)

Problem Statement:

问题陈述:

Let A be a 2D array whose entries are either W or B. Write a program that takes A, and replaces all Ws that cannot reach the boundary with a B. – Aziz, Adnan, et al. Elements of Programming Interviews

设A为一个二维数组,其条目为W或B。编写一个程序,该程序接受A,并将所有无法到达边界的W替换为B。– Aziz,Adnan等。 编程面试的要素

Solution:

解:

  • Instead of iterating through all the entries to find the enclosed W entries, it is more optimal to start with the boundary W entries, traverse the graph and mark the connected W entries. These marked entries are guaranteed to be not enclosed since they are connected to a W entry on the border of the board. This preprocessing is basically the complement of what the program has to achieve.

    与其遍历所有条目以查找包含的W条目,不如从边界W条目开始 ,遍历图形并标记连接的W条目 ,这是最佳选择。 由于这些标记的条目已连接到板边框上的W条目,因此保证不会将其封闭 。 这种预处理基本上是程序必须实现的补充

  • Then, A is iterated through again and the unmarked W entries (which will be the enclosed ones) are changed into the B entries.

    然后,再次遍历A,将未标记的 W条目(将成为封闭的条目 )更改为B条目

  • We’ll keep track of the marked and unmarked W entries using a Boolean array of the same dimensions as A. A marked entry will be set to true.

    我们将使用尺寸与A相同的布尔数组来跟踪标记的和未标记的W条目。标记的条目将设置为true。
  • Time complexity: O(mn)

    时间复杂度:O(mn)

Pseudocode:

伪代码:

function fillSurroundedRegions
   1. Initialize a 'visited' array of same length as the input array
      pre-filled with 'false' values
   2. Start at the boundary entries
   3. If the boundary entry is a W entry and unmarked:
         Call markBoundaryRegion function
   4. Iterate through A and change the unvisited W entry to B
function markBoundaryRegion
   Start with a boundary W entry
   Traverse the grid using BFS
   Mark the feasible entries as true
function fillSurroundedRegions(board) {
    if (!board.length) {
        return;
    }
    const numRows = board.length, numCols = board[0].length;
    let visited = [];
    for (let i=0; i<numRows; i++) {
        visited.push(new Array(numCols).fill(false, 0, numCols));
    }
    for (let i=0; i<board.length; i++) {
        if (board[i][0] == 'W' && !visited[i][0]) {
            markBoundaryRegion(i, 0, board, visited);
        }
        if (board[i][board.length-1] == 'W' && !visited[i][board.length-1]) {
            markBoundaryRegion(i, board.length-1, board, visited);
        }
    }
    for (let j=0; j<board[0].length; j++) {
        if (board[0][j] == 'W' && !visited[0][j]) {
            markBoundaryRegion(0, j, board, visited);
        }
        if (board[board.length-1][j] == 'W' && !visited[board.length-1][j]) {
            markBoundaryRegion(board.length-1, j, board, visited);
        }
    }
    for (let i=1; i<board.length-1; i++) {
        for (let j=1; j<board.length-1; j++) {
            if (board[i][j] == 'W' && !visited[i][j]) {
                board[i][j] = 'B';
            }
        }
    }
    return board;
}
function markBoundaryRegion(i, j, board, visited) {
    let directions = [ [0,1] , [1,0] , [0,-1] , [-1,0] ];
    const queue = [];
    queue.push([i,j]);
    visited[i][j] = true;
    let currentPosition, neighbor;
    while (queue.length) {
        currentPosition = queue.shift();
        for (const direction of directions) {
            neighbor = [i+direction[0], j+direction[1]];
            if (isFeasible(board,visited,neighbor)) {
                visited[neighbor[0]][neighbor[1]] = true;
                queue.push(neighbor);
            }
        }
    }
}
function isFeasible(board, visited, neighbor) {
    let x = neighbor[0], y = neighbor[1];
    return x >= 0 && x < board.length && y >= 0 && y < board[x].length && board[x][y] == 'W';
}
var board = [['B','B','B','B'],['W','B','W','B'],['B','W','W','B'],['B','B','B','B']];
fillSurroundedRegions(board);

死锁检测(在有向图中循环) (Deadlock Detection (Cycle In Directed Graph))

Problem Statement:

问题陈述:

One deadlock detection algorithm makes use of a “wait-for” graph to track which other processes a process is currently blocking on. In a wait-for graph, processes are represented as nodes, and an edge from process P to 0 implies 0 is holding a resource that P needs and thus P is waiting for 0 to release its lock on that resource. A cycle in this graph implies the possibility of a deadlock. This motivates the following problem.

一种死锁检测算法利用“等待”图来跟踪一个进程当前正在阻塞的其他进程。 在等待图中,进程表示为节点,并且从进程P到0的边表示0持有P所需的资源,因此P等待0释放对该资源的锁定。 此图中的一个周期表示存在死锁的可能性。 这引起了以下问题。

One deadlock detection algorithm makes use of a “wait-for” graph to track which other processes a process is currently blocking on. In a wait-for graph, processes are represented as nodes, and an edge from process P to 0 implies 0 is holding a resource that P needs and thus P is waiting for 0 to release its lock on that resource. A cycle in this graph implies the possibility of a deadlock. This motivates the following problem.Write a program that takes as input a directed graph and checks if the graph contains a cycle. – Aziz, Adnan, et al. Elements of Programming Interviews

一种死锁检测算法利用“等待”图来跟踪进程当前正在阻塞的其他进程。 在等待图中,进程表示为节点,并且从进程P到0的边表示0持有P所需的资源,因此P等待0释放对该资源的锁定。 此图中的一个周期表示存在死锁的可能性。 这引起了以下问题。 编写一个程序,将有向图作为输入,并检查该图是否包含循环。 Aziz,Adnan等。 编程面试的要素

In the wait-for graph above, our deadlock detection program will detect at least one cycle and return true.

在上面的等待图中,我们的死锁检测程序将检测至少一个周期并返回true。

For this algorithm, we’ll use a slightly different implementation of the directed graph to explore other data structures. We are still implementing it using the adjacency list but instead of an object (map), we’ll store the vertices in an array.

对于此算法,我们将使用有向图的稍有不同的实现来探索其他数据结构。 我们仍在使用邻接列表来实现它,但是我们将顶点存储在array中 ,而不是对象(地图)。

The processes will be modeled as vertices starting with the 0th process. The dependency between the processes will be modeled as edges between the vertices. The edges (adjacent vertices) will be stored in a Linked List, in turn stored at the index that corresponds to the process number.

第0个过程开始,这些过程将被建模为顶点 。 过程之间的依赖性进行建模为顶点之间的 (相邻顶点)将存储在“ 链表”中 ,而链表又存储在与进程号相对应的索引处。

class Node {
    constructor(data) {
        this.data = data;
        this.next = null;
    }
}
class LinkedList {
    constructor() {
        this.head = null;
    }
    insertAtHead(data) {
        let temp = new Node(data);
        temp.next = this.head;
        this.head = temp;
        return this;
    }
    getHead() {
        return this.head;
    }
}
class Graph {
    constructor(vertices) {
        this.vertices = vertices;
        this.list = [];
        for (let i=0; i<vertices; i++) {
            let temp = new LinkedList();
            this.list.push(temp);
        }
    }
    addEdge(source, destination) {
        if (source < this.vertices && destination < this.vertices) {
            this.list[source].insertAtHead(destination);
        }
        return this;
    }
}

Solution:

解:

  • Every vertex will be assigned 3 different colors: white, gray and black. Initially all vertices will be colored white. When a vertex is being processed, it will be colored gray and after processing black.

    每个顶点将被分配3种不同的颜色 :白色,灰色和黑色。 最初,所有顶点将被着色为白色 。 处理顶点时,它将被着色为灰色,而处理为黑色之后

  • Use Depth First Search to traverse the graph.

    使用“深度优先搜索”遍历图形。
  • If there is an edge from a gray vertex to another gray vertex, we’ve discovered a back edge (a self-loop or an edge that connects to one of its ancestors), hence a cycle is detected.

    如果从灰色顶点到另一个灰色顶点之间存在一条边,则我们发现了一条后边 (自环或连接到其祖先之一的边),因此可以检测到一个循环

  • Time Complexity: O(V+E)

    时间复杂度:O(V + E)

Pseudocode:

伪代码:

function isDeadlocked
   Color all vertices white
   Run DFS on the vertices
     1. Mark current node Gray
     2. If adjacent vertex is Gray, return true
     3. Mark current node Black
   Return false
const Colors = {
    WHITE: 'white', 
    GRAY: 'gray', 
    BLACK: 'black'
}
Object.freeze(Colors);
function isDeadlocked(g) {
    let color = [];
    for (let i=0; i<g.vertices; i++) {
        color[i] = Colors.WHITE;
    }
    for (let i=0; i<g.vertices; i++) {
        if (color[i] == Colors.WHITE) {
             if (detectCycle(g, i, color)) {
                return true;
             }   
        }
    }
    return false;
};
function detectCycle(g, currentVertex, color) {
    color[currentVertex] = Colors.GRAY;
    let neighbor;
    let nextNode = g.list[currentVertex].getHead();
    while (nextNode !== null) {
        neighbor = nextNode.data;
        if (color[neighbor] == Colors.GRAY) {
            return true;
        }
        if (color[neighbor] == Colors.WHITE && detectCycle(g, neighbor, color)) {
            return true;
        }
    }
    color[currentVertex] = Colors.BLACK;
    return false;
}
let g = new Graph(3);
g.addEdge(0,1);
g.addEdge(0,2);
isDeadlocked(g);

克隆图 (Clone Graph)

Problem Statement:

问题陈述:

Consider a vertex type for a directed graph in which there are two fields: an integer label and a list of references to other vertices. Design an algorithm that takes a reference to a vertex u, and creates a copy of the graph on the vertices reachable from u. Return the copy of u. – Aziz, Adnan, et al. Elements of Programming Interviews

考虑有向图的顶点类型,其中有两个字段:整数标签和对其他顶点的引用列表。 设计一个算法,该算法引用顶点u,并在从u可以到达的顶点上创建图的副本。 返回u的副本。 Aziz,Adnan等。 编程面试的要素

Solution:

解:

  • Maintain a map that maps the original vertex to its counterpart. Copy over the edges.

    保持地图 原来的顶点与其对应映射 。 复制边缘。

  • Use BFS to visit the adjacent vertices (edges).

    使用BFS访问相邻的顶点(边)。
  • Time Complexity: O(n), where n is the total number of nodes.

    时间复杂度:O(n),其中n是节点总数。

Pseudocode:

伪代码:

function cloneGraph
   Initialize an empty map
   Run BFS
   Add original vertex as key and clone as value to map
   Copy over edges if vertices exist in map
   Return clone
class GraphVertex {
    constructor(value) {
        this.value = value;
        this.edges = [];
    }
}
function cloneGraph(g) {
    if (g == null) {
        return null;
    }
    let vertexMap = {};
    let queue = [g];
    vertexMap[g] = new GraphVertex(g.value);
    while (queue.length) {
        let currentVertex = queue.shift();
        currentVertex.edges.forEach(v => {
            if (!vertexMap[v]) {
                vertexMap[v] = new GraphVertex(v.value);
                queue.push(v);
            }
            vertexMap[currentVertex].edges.push(vertexMap[v]);
        });
    }
    return vertexMap[g];
}
let n1 = new GraphVertex(1);
let n2 = new GraphVertex(2);
let n3 = new GraphVertex(3);
let n4 = new GraphVertex(4);
n1.edges.push(n2, n4);
n2.edges.push(n1, n3);
n3.edges.push(n2, n4);
n4.edges.push(n1, n3);
cloneGraph(n1);

进行有线连接 (Making Wired Connections)

Problem Statement:

问题陈述:

Design an algorithm that takes a set of pins and a set of wires connecting pairs of pins, and determines if it is possible to place some pins on the left half of a PCB, and the remainder on the right half, such that each wire is between left and right halves. Return such a division, if one exists. – Aziz, Adnan, et al. Elements of Programming Interviews

设计一种算法,该算法采用一组引脚和一组连接成对引脚的导线,并确定是否有可能在PCB的左半部分上放置一些引脚,而在右半部分上放置其余引脚,从而使每条导线在左右两半之间。 如果存在,请返回这样的划分。 Aziz,Adnan等。 编程面试的要素

Solution:

解:

  • Model the set as a graph. The pins are represented by the vertices and the wires connecting them are the edges. We’ll implement the graph using an edge list.

    将集合建模为图形。 引脚由顶点表示,连接它们的导线为边。 我们将使用边列表实现该图。

The pairing described in the problem statement is possible only if the vertices (pins) can be divided into “2 independent sets, U and V such that every edge (u,v) either connects a vertex from U to V or a vertex from V to U.” (Source) Such a graph is known as a Bipartite graph.

只有将顶点(引脚)分为“ U和V”两个独立集合,使得每个边(u,v)要么将U的顶点连接到V,要么将V的顶点连接起来,问题陈述中描述的配对才可能到U。” ( 来源 )这样的图被称为二分图

To check whether the graph is bipartite, we’ll use the graph coloring technique. Since we need two sets of pins, we have to check if the graph is 2-colorable (which we’ll represent as 0 and 1).

为了检查图是否为二分图,我们将使用图着色技术。 由于我们需要两组引脚,因此我们必须检查图形是否为2色(我们将其表示为0和1)。

Initially, all vertices are uncolored (-1). If adjacent vertices are assigned the same colors, then the graph is not bipartite. It is not possible to assign two colors alternately to a graph with an odd length cycle using 2 colors only, so we can greedily color the graph.

最初,所有顶点均为未着色(-1)。 如果相邻顶点分配了相同的颜色,则该图不是二分图。 不可能仅使用2种颜色将两种颜色交替分配给具有奇长周期的图,因此我们可以贪婪地为图着色。

Extra step: We will handle the case of a graph that is not connected. The outer for loop takes care of that by iterating over all the vertices.

额外的步骤:我们将处理未连接图形的情况。 外部for循环通过遍历所有顶点来解决这一问题。

  • Time Complexity: O(V+E)

    时间复杂度:O(V + E)

Pseudocode:

伪代码:

function isBipartite
   1. Initialize an array to store uncolored vertices
   2. Iterate through all vertices one by one
   3. Assign one color (0) to the source vertex
   4. Use DFS to reach the adjacent vertices
   5. Assign the neighbors a different color (1 - current color)
   6. Repeat steps 3 to 5 as long as it satisfies the two-colored     constraint
   7. If a neighbor has the same color as the current vertex, break the loop and return false
function isBipartite(graph) {
    let color = [];
    for (let i=0; i<graph.length; i++) {
        color[i] = -1;
    }
    for (let i=0; i<graph.length; i++) {
        if (color[i] == -1) {
            let stack = [];
            stack.push(i);
            color[i] = 0;
            let node;
            while (stack.length) {
                node = stack.pop();
                for (const neighbor of graph[node]) {
                    if (color[neighbor] == -1) {
                        stack.push(neighbor);
                        color[neighbor] = 1 - color[node];
                    }
                    else if (color[neighbor] == color[node]) {
                        return false;
                    }
                }
            }
        }
    }
    return true;
}
isBipartite([[],[2,4,6],[1,4,8,9],[7,8],[1,2,8,9],[6,9],[1,5,7,8,9],[3,6,9],[2,3,4,6,9],[2,4,5,6,7,8]]);

将一个字符串转换为另一个 (Transform one string to another)

Problem Statement:

问题陈述:

Given a dictionary D and two strings s and f, write a program to determine if s produces t. Assume that all characters are lowercase alphabets. If s does produce f, output the length of a shortest production sequence; otherwise, output -1. – Aziz, Adnan, et al. Elements of Programming Interviews

给定字典D和两个字符串s和f,编写一个程序以确定s是否产生t。 假定所有字符均为小写字母。 如果s确实产生f,则输出最短生产序列的长度; 否则,输出-1。 阿齐兹,阿德南等人。 编程面试的要素

For example, if the dictionary D is ["hot", "dot", "dog", "lot", "log", "cog"], s is "hit" and t is "cog", the length of the shortest production sequence is 5."hit" -> "hot" -> "dot" -> "dog" -> "cog"

例如,如果字典D为[“ hot”,“ dot”,“ dog”,“ lot”,“ log”,“ cog”],则s为“ hit”,t为“ cog”,则字典的长度最短的生产顺序是5。“命中”->“热”->“点”->“狗”->“齿轮”

Solution:

解:

  • Represent the strings as vertices in an undirected, unweighted graph, with an edge between 2 vertices if the corresponding strings differ in one character at most. We'll implement a function (compareStrings) that calculates the difference in characters between two strings.

    表示字符串作为顶点在无向,非加权曲线图,2个顶点之间的边缘 ,如果相应的字符串中的一个字符至多不同。 我们将实现一个函数(compareStrings),该函数计算两个字符串之间的字符差。

  • Piggybacking off the previous example, the vertices in our graph will be

    带上一个示例,我们图中的顶点将是
{hit, hot, dot, dog, lot, log, cog}
  • The edges represented by the adjacency list approach we discussed in section 0. Graph Implementation, will be:

    我们在第0节“图形实现”中讨论的邻接表方法所表示的边为:
{
    "hit": ["hot"],
    "hot": ["dot", "lot"],
    "dot": ["hot", "dog", "lot"],
    "dog": ["dot", "lot", "cog"],
    "lot": ["hot", "dot", "log"],
    "log": ["dog", "lot", "cog"],
    "cog": ["dog", "log"]
}
  • Once we finish building the graph, the problem boils down to finding the shortest path from a start node to a finish node. This can be naturally computed using Breadth First Search.

    一旦完成图的构建,问题就归结为找到从起点到终点的最短路径。 这自然可以使用“ 广度优先搜索”来计算。

  • Time Complexity: O(M x M x N), where M is the length of each word and N is the total number of words in the dictionary.

    时间复杂度:O(M x M x N),其中M是每个单词的长度,N是字典中单词的总数。

Pseudocode:

伪代码:

function compareStrings
   Compare two strings char by char
   Return how many chars differ
function transformString
   1. Build graph using compareStrings function. Add edges if and only if  the two strings differ by 1 character
   2. Run BFS and increment length
   3. Return length of production sequence
function transformString(beginWord, endWord, wordList) {
    let graph = buildGraph(wordList, beginWord);
    if (!graph.has(endWord)) return 0;
    let queue = [beginWord];
    let visited = {};
    visited[beginWord] = true;
    let count = 1;
    while (queue.length) {
        let size = queue.length;
        for (let i=0; i<size; i++) {
            let currentWord = queue.shift();
            if (currentWord === endWord) {
                return count;
            }
            graph.get(currentWord).forEach( neighbor => {
                if (!visited[neighbor]) {
                    queue.push(neighbor);
                    visited[neighbor] = true;
                }
            })
        }
        count++;
    }
    return 0;
};

function compareStrings (str1, str2) {
    let diff = 0;
    for (let i=0; i<str1.length; i++) {
        if (str1[i] !== str2[i]) diff++
    }
    return diff;
}

function buildGraph(wordList, beginWord) {
    let graph = new Map();
    wordList.forEach( (word) => {
        graph.set(word, []);
        wordList.forEach( (nextWord) => {
            if (compareStrings(word, nextWord) == 1) {
                graph.get(word).push(nextWord);
            }
        })
    })
    if (!graph.has(beginWord)) {
        graph.set(beginWord, []);
        wordList.forEach( (nextWord) => {
            if (compareStrings(beginWord, nextWord) == 1) {
                graph.get(beginWord).push(nextWord);
            }
        })
    }
    return graph;
}

然后去哪儿? (Where to go from here?)

Hopefully, by the end of this article, you have realized that the most challenging part in graph problems is identifying how to model the problems as graphs. From there, you can use/modify the two graph traversals to get the expected output.

希望到本文结尾,您已经意识到图问题中最具挑战性的部分是确定如何将问题建模为图。 从那里,您可以使用/修改两个图形遍历以获取预期的输出。

Other graph algorithms that are nice to have in your toolkit are:

您的工具包中还不错的其他图形算法是:

  • Topological Ordering

    拓扑顺序
  • Shortest Path Algorithms (Dijkstra and Floyd Warshall)

    最短路径算法(Dijkstra和Floyd Warshall)
  • Minimum Spanning Trees Algorithms (Prim and Kruskal)

    最小生成树算法(Prim和Kruskal)

If you found this article helpful, consider buying me a coffee. It will keep me awake when I work on a video tutorial of this article :)                                        

如果您觉得这篇文章对您有帮助,请考虑给我买杯咖啡 。 当我处理本文的视频教程时,它会让我保持清醒:)

参考文献: (References:)

Aziz, Adnan, et al. Elements of Programming Interviews. 2nd ed., CreateSpace Independent Publishing Platform, 2012.

阿齐兹(Aziz),阿德南(Adnan)等。 编程面试的要素。 第2版​​,CreateSpace独立发布平台,2012年。

翻译自: https://www.freecodecamp.org/news/8-essential-graph-algorithms-in-javascript/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值