Рубрики
Без рубрики

Как реализовать 8 алгоритмов основных графов в JavaScript

В этой статье я буду реализовывать 8 алгоритмов графика, которые изучают поисковые и комбинаторные проблемы (обход, кратчайший путь и сопоставление) графов в JavaScript. Проблемы заимствованы из книги, элементы программирования интервью в Java. Решения в книге закодированы в Java, Python или C ++

Автор оригинала: Girish Ramloul.

В этой статье я буду реализовывать 8 алгоритмов графа Это исследовать поисковые и комбинаторные проблемы (обход, кратчайший путь и сопоставление) графов в JavaScript.

Проблемы заимствованы из книги, Элементы программирования интервью в Java Отказ Решения в книге закодированы в Java, Python или C ++ в зависимости от какой версии книги, которую вы владеете.

Хотя логика моделирования проблем является языковой агностик, фрагменты кода, которые я предоставляю в этой статье, использует некоторые предостережения JavaScript.

Каждое решение для каждой проблемы разбито на 3 раздела: обзор раствора, псевдокод и, наконец, фактический код в JavaScript.

Чтобы проверить код и увидеть его делать то, что он должен делать, вы можете использовать Инструменты DEV Chrome Чтобы запустить фрагменты на сам браузере или использовать NodeJ, чтобы запустить их из командной строки.

Реализация графа

2 чаще всего используются Представления графов являются списком соседних и матрица смежности.

Проблемы, которые я буду решать, предназначены для редких графов (несколько ребер), а операции вершин в подходе в списке соседних предпринят постоянные (добавление вершины, O (1)) и линейного времени (удаление вершины, O (V + E )). Поэтому я буду придерживаться этой реализации по большей части.

Давайте выбить это с простым Неужелированный, невзвешенный график Реализация Использование Список соседних Отказ Мы будем поддерживать объект (соседний список), который будет содержать все вершины на нашем графике в качестве клавиш. Значения станут массивом всех соседних вершин. В приведенном ниже примере Vertex 1 подключен к вершинам 2 и 4, отсюда смешно: {1: [2, 4]} и так далее для других вершин.

Чтобы построить график, у нас есть две функции: AddVertex и прибавлять . AddVertex используется для добавления вершины в список. Удаление используется для подключения вершин, добавив соседние вершины как к массивам источника, так и для назначения, поскольку это неопрятный график. Чтобы сделать направленный график, мы можем просто удалить строки 14-16 и 18 в код ниже.

Перед удалением вершины нам нужно повторить итерацию через массив соседних вершин и удалить все возможные соединения с этой вершиной.

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];
  }  
}

Графические обходы

Строительство нашей реализации графиков в предыдущем разделе, мы реализуем графические обходы: ширина первого поиска и первого поиска.

ПЕРВЫЙ ПОИСК ПО ИЩЕТ

BFS посещает узлы один уровень за раз Отказ Чтобы предотвратить посещение того же узла более одного раза, мы будем поддерживать посетил объект.

Поскольку нам нужно сначала обработать узлы в первую очередь, очередь является хорошим претенденцией для использования структуры данных. Сложность времени является 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 посещает глубину узлов. Поскольку нам нужно сначала обрабатывать узлы, мы будем использовать стек Отказ

Начиная с вершины, мы вытащим соседние вершины в наш стек. Всякий раз, когда вершина выскочина, она помечается посещенным в нашем посещенном объекте. Его соседние вершины толщины в стек. Поскольку мы всегда выскакиваем новую соседнюю вершину, наш алгоритм всегда будет Исследуйте новый уровень Отказ

Мы также можем использовать внутренние вызовы стека для рекурсивно реализации DFS. Логика одинакова.

Сложность времени такая же, как 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;
}

Поиск лабиринт

Постановка задачи:

Мы представляем белые записи с 0 и черными записями с 1. Белые записи представляют открытые зоны и черные записи стен. Вход и точки выхода представлены массивом, 1-й индекс и 1-й индекс, заполненный индексами строки и столбцов, соответственно.

Решение:

  • Перейти к другому положению, мы будем твердыми четыреми возможными движениями в Направления Array (справа, дно, левый и верх; без диагональных движений):
[ [0,1], [1,0], [0,-1], [-1,0] ]
  • Чтобы отследить клетки, которые мы уже посетили, мы будем заменить Белые записи ( 0 ) с черными записями ( 1’s ). Мы в основном используем DFS рекурсивно пройти лабиринт. Базовый случай, который закончится рекурсию, либо у нас есть достиг нашей точки выхода и вернуть истину Или у нас есть посетил каждый белый вход и вернуть ложь Отказ
  • Еще одна важная вещь, чтобы отслеживать – это обеспечить, чтобы мы были в пределах лабиринта все время и что мы только продолжить Если мы У белого входа Отказ Isfasible Функция позаботится об этом.
  • Сложность времени: O (V + E)

Псевдокод:

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]);

Покрасить булевую матрицу

Постановка задачи:

2 цвета будут представлены 0 и 1-х годов.

В приведенном ниже примере мы начинаем в центре массива ([1,1]). Обратите внимание, что из этой позиции мы можем добраться только к верхней левой треугольной матрице. Самое главное, низкое положение не может быть достигнуто ([2,2]). Следовательно, в конце процесса это единственный цвет, который не перевернут.

Решение:

  • Как и в предыдущем вопросе, мы будем кодировать массив для определения 4 возможных движений.
  • Мы будем использовать BFS для прохождения графика.
  • Мы немного изменим функцию Isfasible. Он все еще будет проверять, находится ли новая позиция в границах матрицы. Другим требованием является то, что новая позиция окрашивается так же, как предыдущая позиция. Если новая позиция соответствует требованиям, его цвет перевернут.
  • Сложность времени: O (Mn)

Псевдокод:

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);

Вычислить закрытые регионы

Постановка задачи:

Решение:

  • Вместо того, чтобы итерация по всем записям, чтобы найти прилагаемые записи W, это более оптимально Начните с границы W-записей пройти график и Отметьте подключенные записи W . Эти отмеченные записи гарантированы не закрыл Поскольку они связаны с записью W на границе доски. Это предварительная обработка в основном Дополнение из того, что должна достичь программа.
  • Тогда a снова и потенциал и не отмечал W заводы (которые будут закрытыми) изменяются в B Заявления .
  • Мы отслеживаем маркированные и немаркированные записи W с использованием логического массива одинаковых размеров, что и отмеченная запись в True.
  • Сложность времени: O (Mn)

Псевдокод:

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= 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);

Обнаружение тупика (цикл на направленном графике)

Постановка задачи:

В ожидании графа выше, наше Программа обнаружения тупика обнаружит хотя бы один цикл и вернуть правда.

Для этого алгоритма мы будем использовать немного другой реализации Направленный график Чтобы исследовать другие структуры данных. Мы все еще реализуем его, используя Список соседних Но вместо объекта (карта) мы будем хранить вершины в массив Отказ

процессы будет смоделирован как вершины начиная с 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

Решение:

  • Каждая вершина будет назначена 3 разных цвета : белый, серый и черный. Первоначально все вершины будут окрашены белый . Когда вершина обрабатывается, она будет окрашен серый и после обработки черный Отказ
  • Используйте глубину сначала поиска, чтобы пройти график.
  • Если есть край от серой вершины к другой серой вершине, мы обнаружили задний край (Самоклассник или край, соединяющийся с одним из его предков), следовательно, A цикл обнаружен.
  • Сложность времени: O (V + E)

Псевдокод:

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

Граф клона

Постановка задачи:

Решение:

  • Поддерживать карта что Карты оригинальная вершина к своему аналову Отказ Скопируйте по краям.
  • Используйте BFS для посещения соседних вершин (ребра).
  • Сложность времени: O (n), где n – общее количество узлов.

Псевдокод:

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);

Изготовление проводных соединений

Постановка задачи:

Решение:

  • Модель набор в виде графика. Булавки представлены вершинами и проводами, соединяющими их, – это края. Мы реализуем график, используя Список по краю Отказ

Сопряжение, описанное в операторе задачи, возможна только в том случае, если вершины (штифты) можно разделить на «2 независимых набора, U и V такое, что каждый край (u, v) либо соединяет вершину от u к V или вершину от V к U. ” ( Источник ) Такой график известен как Бипартитный график Отказ

Чтобы проверить, является ли график бипартитом, мы будем использовать График раскраски техника. Поскольку нам нужны два набора штифтов, мы должны проверить, является ли график 2-красным (который мы представляем как 0 и 1).

Первоначально все вершины не связаны (-1). Если соседние вершины присваиваются одни и те же цвета, то график не является бипартитом. Невозможно назначить два цвета попеременно на график с нечетным циклом длины, используя только 2 цвета, поэтому мы можем жадно окрашивать график.

Дополнительный шаг: Мы будем обрабатывать случай графика, который не подключен. Наружная для цикла позаботится об этом, путем итерации по всем вершинам.

  • Сложность времени: O (V + E)

Псевдокод:

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

Преобразовать одну строку в другую

Постановка задачи:

Например, если словарь d – [«горячий», «точка», «собака», «лот», «бревно», «COG»], S «HIT» и T – «COG», длина Краткая производственная последовательность – 5. «Хит» -> «Горячий» -> «Точка» -> «Собака» -> «COG»

Решение:

  • Представлять Строки как вершины в неопрященном, невосстанавливающемся графике, с край Между 2 вершинами, если соответствующие строки отличаются один персонаж в большинстве. Мы реализуем функцию (Comparts), которые рассчитывают разницу в символах между двумя строками.
  • POGGYALING от предыдущего примера, Вершины на нашем графике будут
{hit, hot, dot, dog, lot, log, cog}
  • Края, представленные подходом списка соседних соседних, мы обсуждали в разделе 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"]
}
  • Как только мы закончим построение графика, проблема сводится к поиску кратчайшего пути от начального узла к финишему узлу. Это может быть естественным образом вычислено с использованием Ширина первого поиска Отказ
  • Сложность времени: o (m x m x n), где m – длина каждого слова, а n – общее количество слов в словаре.

Псевдокод:

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 {
                if (!visited[neighbor]) {
                    queue.push(neighbor);
                    visited[neighbor] = true;
                }
            })
        }
        count++;
    }
    return 0;
};

function compareStrings (str1, str2) {
    let diff = 0;
    for (let i=0; i {
        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;
}

Куда пойти отсюда?

Надеюсь, к концу этой статьи вы поняли, что самая сложная часть в графических задачах определяется, как моделировать проблемы как графики. Оттуда вы можете использовать/изменить два диаграммах, чтобы получить ожидаемый вывод.

Другие алгоритмы графика, которые приятно иметь в вашем инструментарии:

  • Топологический заказ
  • Самые короткие алгоритмы пути (Dijkstra и Floyd Warshall)
  • Минимальные охватывающие деревья алгоритмы (Prim и Kruskal)

Если вы нашли эту статью полезную, рассмотрим Покупая меня кофе . Это будет держать меня спать, когда я работаю над видеоуправлением этой статьи:)

Использованная литература:

Азиз, adnan et al. Элементы программирования интервью. 2-й ред. , CreateSpace Независимая публикация платформы 2012 года.