Как создать визуализацию для проблемы Leetcode Two Sum – проект на HTML, CSS и JavaScript.

С текущим состоянием рынка труда многие люди посвящают много времени решению задач на LeetCode [https//leetcode.com/] в качестве подготовки к техническим собеседованиям. Но иногда было бы полезно иметь визуализацию, показывающую алгоритмы, лежащие в основе этих проблем. В этом руководстве мы...

С текущим состоянием рынка труда многие люди решают задачи на LeetCode, чтобы подготовиться к техническим собеседованиям.

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

В этом руководстве мы создадим визуализацию, демонстрирующую несколько подходов к популярной задаче на LeetCode, называемой Two Sum. Для создания этого проекта мы будем использовать чистый HTML, CSS и JavaScript.

Содержание

Необходимые навыки

Это руководство предполагает, что у вас есть базовые знания HTML, CSS и JavaScript. Если вы еще не прошли начинающий курс по одному из этих языков, рекомендуется начать с следующих ресурсов:

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

Настройка проекта

Вы можете создать этот проект в выбранном локальном редакторе кода или через онлайн-среду разработки (IDE) или редактор, такие как CodePen, CodeSandbox или Replit.

Проект будет состоять из трех файлов: index.html, index.js и styles.css. Так как проект в основном сосредоточен на JavaScript, в этом репозитории GitHub я предоставил все необходимые HTML- и CSS-файлы.

Вы можете свободно fork and clone the repository, или вы можете скопировать код найденный в файлах HTML и CSS и добавить его в свой проект.

Как только вы настроите среду проекта, тогда вы должны запустить локальный сервер и увидеть этот результат на экране:

Screenshot-2023-11-19-at-4.18.24-PM
Стилизация визуализации для задачи Two Sum на Leetcode

Как решить задачу “Two Sum” на LeetCode

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

Описание

В этой задаче вам будет предоставлен список чисел в любом порядке и целевое число. Цель состоит в том, чтобы найти пару чисел, которая в сумме дает целевое число, и вернуть массив индексов для этой пары чисел.

В этом примере у нас есть следующий список чисел и целевое число:

[2,7,11,15]target: 9

Числа 2 и 7 в сумме дают 9, поэтому мы вернем [0,1], потому что это представляет индексы, где можно найти пару чисел в массиве.

В этой задаче вы можете предположить, что в массиве будет как минимум два числа или более, и будет только одно возможное решение.

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

[1,2,3,4,5]target: 55

Вы также не получите входные данные с несколькими решениями. Следующий вход имеет два ответа [0,1] и [1,2], что противоречит правилам этой задачи.

[3,3,3]target: 6

Подход “перебор всех возможных вариантов”

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

Давайте рассмотрим пример:

[11, 15, 2, 7]target:9

Мы начинаем с первого числа в списке (11) и проверяем каждую возможную пару, чтобы увидеть, дает ли она в сумме целевое число (9).

11 + 15 = 9? НЕТ11 + 2 = 9? НЕТ11 + 7 = 9? НЕТ

Поскольку ни одна из этих пар не равна целевому числу (9), мы переходим ко второму числу в списке (15) и проверяем все возможные пары. Нет необходимости проверять 11+15, потому что мы уже это проверили ранее.

15 + 2 = 9? НЕТ15 + 7 = 9? НЕТ

Поскольку ни одна из этих пар не равна целевому числу (9), мы переходим к третьему числу в списке (2) и проверяем все возможные пары.

2 + 7 = 9? ДА!!!

Теперь мы нашли пару, дающую в сумме целевое число, и мы возвращаем [2,3], потому что это представляет индексы, где можно найти пару чисел в массиве.

Решение на JavaScript с помощью подхода “перебор всех возможных вариантов” и оценка временной сложности

Это решение использует вложенный цикл for, что даёт временную сложность O(n²). Внешний цикл используется для получения текущего числа в списке, а внутренний цикл используется для проверки, равна ли сумма текущего числа и других чисел в списке целевому числу.

/** * @param {number[]} nums * @param {number} target * @return {number[]} */var twoSum = function (nums, target) {  if (nums.length === 2) return [0, 1];  for (let i = 0; i < nums.length; i++) {    const currentNum = nums[i];    for (let j = i + 1; j < nums.length; j++) {      if (currentNum + nums[j] === target) return [i, j];    }  }};

Примечание: Я добавил дополнительную проверку в моем решении, чтобы проверить, содержит ли входной массив только два числа. В этом случае мы сразу же возвращаем [0,1], потому что это единственные возможные индексы для этого тестового случая.

 if (nums.length === 2) return [0, 1];

До сих пор наши входные массивы были очень маленькими. Но если бы у нас был входной массив из 100, 500 или 1000+ чисел, то поиск пары, дающей в сумме цель, мог занять некоторое время.

В следующем разделе мы рассмотрим решение, которое использует объект Map JavaScript и выполняется за линейное время O(n).

Решение с использованием Map

В подходе «грубой силы» мы начинали с начала массива и сравнивали все возможные пары чисел, пока не находили пару, дающую в сумме цель. Но в этом подходе мы можем использовать объект Map JavaScript и один цикл for, чтобы найти эту пару.

Объект Map JavaScript – это коллекция пар ключ-значение, которая позволяет быстро выполнять поиск и имеет встроенные методы, такие как set(), get() и has().

Давайте поработаем с тем же примером, что и раньше:

[11, 15, 2, 7]target:9

Мы можем начать проходить по массиву и смотреть на текущее число в списке, которое будет nums[i]. Мы также хотим создать новый объект map, который изначально будет пустым.

const map = new Map();for(let i=0; i<nums.length; i++){    }

Внутри нашего цикла нам нужно вычислить разность, которая равна цели минус текущее число.

    const map = new Map();    for(let i=0; i<nums.length; i++){        const difference = target - nums[i]    }

Поскольку мы знаем, что могут быть только два числа, дающих в сумме цель, мы можем проверить, содержится ли разность в map. Если да, мы нашли пару и можем вернуть индексы. В противном случае, мы можем добавить текущее число в map вместе с его индексом.

    const map = new Map();    for(let i=0; i < nums.length; i++) {        const difference = target - nums[i]        if(map.has(difference)) {            return [map.get(difference), i]        } else {            map.set(nums[i], i)        }    }

В следующем коде метод has() используется для проверки, содержится ли следующий key в объекте map. Этот метод вернет значение true или false.

map.has(difference)

Метод get() используется для возврата соответствующего элемента из map.

 if(map.has(difference)) {   return [map.get(difference), i] } 

Метод set() добавляет или обновляет запись в map с ключом и значением.

else {  map.set(nums[i], i)}

Вот полный код для этого подхода:

/** * @param {number[]} nums * @param {number} target * @return {number[]} */var twoSum = function(nums, target) {    if(nums.length === 2) return [0,1]    const map = new Map();    for (let i = 0; i < nums.length; i++) {        const difference = target - nums[i]        if (map.has(difference)) {            return [map.get(difference), i]        } else {            map.set(nums[i], i)        }    }    };

Временная сложность для решения с использованием Map

Это решение имеет линейную временную сложность O(n). Так как мы используем только один цикл вместо двух, мы улучшили временную сложность и больше не работаем в квадратичном времени O(n²), как раньше.

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

Обзор для визуализации Two Sum

Цель проекта – создать визуализации как для решений на основе карты, так и для решений на основе полного перебора.

Для решения на основе полного перебора покажем, как мы проходим через каждую пару чисел, пока не найдем пару, дающую в сумме целевое число.

Screenshot-2023-11-20-at-8.43.08-AM
Визуализация подхода полного перебора

Для решения на основе карты покажем, как построить карту и проверить, какая пара дает в сумме целевое число.

Screenshot-2023-11-20-at-8.51.40-AM
Визуализация подхода с использованием карты

Добавление визуализации метода полного перебора

Создание переменных const и let

Сначала нам нужно создать переменные const и присвоить им результат доступа к элементам HTML, отвечающим за отображение кнопки решения методом полного перебора и вывода.

const bruteForceSolutionBtn = document.getElementById("brute-force-visual-btn");const bruteForceNumbersOutput = document.querySelector(  "#brute-force-output > .numbers-array");const bruteForceTextOutput = document.querySelector(  "#brute-force-output > .result-text");

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

const testCaseArray = [11, 15, 2, 7];const target = 9;

Затем нам нужно создать переменные let, которые будут представлять текущее число, которое мы рассматриваем во внешнем цикле, и дополняющее число, которое мы рассматриваем во внутреннем цикле.

let currentNum;let currentCompliment;

Создание функции getClassName

В своей визуализации мы хотим показать пользователю текущую пару чисел, которую мы проверяем, добавляя разные цветные границы вокруг них. Текущее число будет иметь зеленую границу, а дополняющее число – синюю границу.

Screenshot-2023-11-20-at-10.02.46-AM

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

Сначала создайте новую функцию getClassName, которая принимает параметр num.

const getClassName = (num) => {    };

Внутри этой функции создайте оператор switch, внутри которого num будет использоваться в качестве выражения для проверки.

const getClassName = (num) => {  switch (num) {      }};

Первый case должен проверять currentNum и возвращать строку для класса current-num.

const getClassName = (num) => {  switch (num) {    case currentNum:      return "class='current-num'";  }};

Второй case должен проверять currentCompliment и возвращать строку для класса compliment-num.

const getClassName = (num) => {  switch (num) {    case currentNum:      return "class='current-num'";    case currentCompliment:      return "class='compliment-num'";  }};

Для default случая должна возвращаться пустая строка, потому что мы не собираемся применять какие-либо классы к этому элементу.

const getClassName = (num) => {  switch (num) {    case currentNum:      return "class='current-num'";    case currentCompliment:      return "class='compliment-num'";    default:      return "";  }};

Создание функции bruteForceApproach

Эта функция будет отвечать за выполнение решения методом перебора и отображение визуализации на странице.

Сначала нам нужно создать функцию bruteForceApproach, которая будет асинхронной.

const bruteForceApproach = async () => {};

Затем добавим внешний цикл для нашего массива тестовых случаев.

const bruteForceApproach = async () => {  for (let i = 0; i < testCaseArray.length; ++i) {    }};

Внутри цикла for обновите currentNum, чтобы присвоить ему значение текущего числа, которое мы рассматриваем в массиве с тестовыми случаями.

const bruteForceApproach = async () => {  for (let i = 0; i < testCaseArray.length; ++i) {    currentNum = testCaseArray[i];  }};

Затем создайте внутренний цикл for.

const bruteForceApproach = async () => {  for (let i = 0; i < testCaseArray.length; ++i) {    currentNum = testCaseArray[i];    for (let j = i + 1; j < testCaseArray.length; ++j) {            }  }};

Внутри внутреннего цикла for обновите число currentCompliment и присвойте ему значение testCaseArray[j]. Это представляет каждое число справа от текущего числа.

const bruteForceApproach = async () => {  for (let i = 0; i < testCaseArray.length; ++i) {    currentNum = testCaseArray[i];    for (let j = i + 1; j < testCaseArray.length; ++j) {      currentCompliment = testCaseArray[j];    }  }};

Затем нам нужно добавить setTimeout, который будет задерживать изменения разметки на одну секунду. Это поможет создать анимационный эффект показа различных пар чисел.

const bruteForceApproach = async () => {  for (let i = 0; i < testCaseArray.length; ++i) {    currentNum = testCaseArray[i];    for (let j = i + 1; j < testCaseArray.length; ++j) {      currentCompliment = testCaseArray[j];      await new Promise((resolve) => setTimeout(resolve, 1000));    }  }};

Затем нам нужно обновить HTML для вывода. Начните с присвоения массива с тестовыми случаями переменной bruteForceNumbersOutput.innerHTML.

const bruteForceApproach = async () => {  for (let i = 0; i < testCaseArray.length; ++i) {    currentNum = testCaseArray[i];    for (let j = i + 1; j < testCaseArray.length; ++j) {      currentCompliment = testCaseArray[j];      await new Promise((resolve) => setTimeout(resolve, 1000));      bruteForceNumbersOutput.innerHTML = testCaseArray;    }  }};

Затем мы хотим использовать метод map на массиве, чтобы создать новый массив элементов span, который представляет каждое число в массиве вместе с его стилями. Нам также нужно использовать метод join, чтобы удалить запятые, которые метод map добавляет при создании нового массива.

const bruteForceApproach = async () => {  for (let i = 0; i < testCaseArray.length; ++i) {    currentNum = testCaseArray[i];    for (let j = i + 1; j < testCaseArray.length; ++j) {      currentCompliment = testCaseArray[j];      await new Promise((resolve) => setTimeout(resolve, 1000));      bruteForceNumbersOutput.innerHTML = testCaseArray        .map(          (num, index) =>            `            <span ${getClassName(num)}>            ${testCaseArray[index]}            </span>          `        )        .join("");    }  }};

Если мы не находим пару чисел, дающих нужную сумму, то мы хотим показать пользователю сообщение. Обновите текстовое содержимое для bruteForceTextOutput и присвойте ему следующее сообщение:

const bruteForceApproach = async () => {  for (let i = 0; i < testCaseArray.length; ++i) {    currentNum = testCaseArray[i];    for (let j = i + 1; j < testCaseArray.length; ++j) {      currentCompliment = testCaseArray[j];      await new Promise((resolve) => setTimeout(resolve, 1000));      bruteForceNumbersOutput.innerHTML = testCaseArray        .map(          (num, index) =>            `            <span ${getClassName(num)}>            ${testCaseArray[index]}            </span>          `        )        .join("");      bruteForceTextOutput.textContent = `Does the sum of ${currentNum} + ${currentCompliment} equal ${target}? НЕТ!`;    }  }};

Последнее условие – это проверка, была ли найдена пара чисел, дающих нужную сумму. Если да, то мы можем отобразить этот конечный массив индексов и return из функции.

  if (currentNum + currentCompliment === target) {      bruteForceTextOutput.textContent = `Final indices: [${i}, ${j}]`;      return;  }

Чтобы протестировать визуализацию насильственного подхода, мы должны добавить прослушиватель событий к bruteForceSolutionBtn. Прослушиватель событий должен реагировать на событие "click" и должен ссылаться на функцию bruteForceApproach.

bruteForceSolutionBtn.addEventListener("click", bruteForceApproach);

Это должен быть ваш полный код на данный момент:

const bruteForceSolutionBtn = document.getElementById("brute-force-visual-btn");const bruteForceNumbersOutput = document.querySelector(  "#brute-force-output > .numbers-array");const bruteForceTextOutput = document.querySelector(  "#brute-force-output > .result-text");const testCaseArray = [11, 15, 2, 7];const target = 9;let currentNum;let currentCompliment;const getClassName = (num) => {  switch (num) {    case currentNum:      return "class='current-num'";    case currentCompliment:      return "class='compliment-num'";    default:      return "";  }};const bruteForceApproach = async () => {  for (let i = 0; i < testCaseArray.length; ++i) {    currentNum = testCaseArray[i];    for (let j = i + 1; j < testCaseArray.length; ++j) {      currentCompliment = testCaseArray[j];      await new Promise((resolve) => setTimeout(resolve, 1000));      bruteForceNumbersOutput.innerHTML = testCaseArray        .map(          (num, index) =>            `            <span ${getClassName(num)}>            ${testCaseArray[index]}            </span>          `        )        .join("");      bruteForceTextOutput.textContent = `Does the sum of ${currentNum} + ${currentCompliment} equal ${target}? НЕТ!`;      if (currentNum + currentCompliment === target) {        bruteForceTextOutput.textContent = `Final indices: [${i}, ${j}]`;        return;      }    }  }};bruteForceSolutionBtn.addEventListener("click", bruteForceApproach);

Попробуйте визуализацию, нажав кнопку “Показать визуализацию” для насильственного подхода.

Screenshot-2023-11-20-at-10.42.51-AM
Результат нажатия кнопки “Показать визуализацию” для насильственного подхода

Как отключить кнопку bruteForceSolutionBtn во время выполнения анимации

Если вы попытаетесь несколько раз нажать на кнопку bruteForceSolutionBtn подряд, вы увидите сбои в анимации. Чтобы исправить это, мы должны отключить кнопку, когда анимация выполняется, и затем включить ее, когда анимация завершена.

Внутри функции bruteForceApproach установите атрибут disabled для bruteForceSolutionBtn.

const bruteForceApproach = async () => {  bruteForceSolutionBtn.setAttribute("disabled", "");

Внутри оператора if удалите атрибут disabled для bruteForceSolutionBtn.

   if (currentNum + currentCompliment === target) {        bruteForceTextOutput.textContent = `Final indices: [${i}, ${j}]`;        bruteForceSolutionBtn.removeAttribute("disabled");        return;   }

Вот полный код с исправлением:

const bruteForceSolutionBtn = document.getElementById("brute-force-visual-btn");const bruteForceNumbersOutput = document.querySelector(  "#brute-force-output > .numbers-array");const bruteForceTextOutput = document.querySelector(  "#brute-force-output > .result-text");const testCaseArray = [11, 15, 2, 7];const target = 9;let currentNum;let currentCompliment;const getClassName = (num) => {  switch (num) {    case currentNum:      return "class='current-num'";    case currentCompliment:      return "class='compliment-num'";    default:      return "";  }};const bruteForceApproach = async () => {  bruteForceSolutionBtn.setAttribute("disabled", "");  for (let i = 0; i < testCaseArray.length; ++i) {    currentNum = testCaseArray[i];    for (let j = i + 1; j < testCaseArray.length; ++j) {      currentCompliment = testCaseArray[j];      await new Promise((resolve) => setTimeout(resolve, 1000));      bruteForceNumbersOutput.innerHTML = testCaseArray        .map(          (num, index) =>            `            <span ${getClassName(num)}>            ${testCaseArray[index]}            </span>          `        )        .join("");      bruteForceTextOutput.textContent = `Does the sum of ${currentNum} + ${currentCompliment} equal ${target}? NO!`;      if (currentNum + currentCompliment === target) {        bruteForceTextOutput.textContent = `Final indices: [${i}, ${j}]`;        bruteForceSolutionBtn.removeAttribute("disabled");        return;      }    }  }};bruteForceSolutionBtn.addEventListener("click", bruteForceApproach);

Попробуйте визуализацию еще раз, и теперь вы должны увидеть, что кнопка отключена при выполнении анимации.

Screenshot-2023-11-20-at-10.56.36-AM
Изображение, показывающее, что кнопка отключена при показе анимации

Добавление визуализации решения с помощью Map

Создание переменных const

Мы создадим несколько новых переменных const, которые представляют элемент кнопки оптимального решения, вывод и таблицу для анимации.

const optimalSolutionBtn = document.getElementById("optimal-visual-btn");const currentValueOutput = document.getElementById("current-value-output");const finalOptimalSolutionResult = document.getElementById(  "final-optimal-result");const table = document.getElementById("table-output");const tableBodyOutput = document.getElementById("map-table-body");

Вот полный список объявлений переменных для обеих визуализаций:

const bruteForceSolutionBtn = document.getElementById("brute-force-visual-btn");const bruteForceNumbersOutput = document.querySelector(  "#brute-force-output > .numbers-array");const bruteForceTextOutput = document.querySelector(  "#brute-force-output > .result-text");const optimalSolutionBtn = document.getElementById("optimal-visual-btn");const currentValueOutput = document.getElementById("current-value-output");const finalOptimalSolutionResult = document.getElementById(  "final-optimal-result");const table = document.getElementById("table-output");const tableBodyOutput = document.getElementById("map-table-body");const testCaseArray = [11, 15, 2, 7];const target = 9;let currentNum;let currentCompliment;

Создание асинхронной функции optimalApproach

Первым шагом является создание новой функции optimalApproach, которая будет async функцией. Вы можете добавить это ниже вашей функции bruteForceApproach.

const optimalApproach = async () => {    };

Как и функция bruteForceApproach, мы хотим отключить кнопку, когда анимация начинается, чтобы предотвратить многократное нажатие пользователем и нарушение анимации.

const optimalApproach = async () => {  optimalSolutionBtn.setAttribute("disabled", "");};

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

const optimalApproach = async () => {  optimalSolutionBtn.setAttribute("disabled", "");  table.style.display = "block";};

Каждый раз, когда мы запускаем эту анимацию, мы хотим выводить сообщения пользователю о том, нашли ли мы правильную пару или нет. В начале анимации мы хотим очистить предыдущий вывод.

const optimalApproach = async () => {  optimalSolutionBtn.setAttribute("disabled", "");  table.style.display = "block";  currentValueOutput.innerHTML = "";};

Затем нам нужно создать пустой объект map, который в конечном итоге будет обновляться со временем в цикле for.

const optimalApproach = async () => {  optimalSolutionBtn.setAttribute("disabled", "");  table.style.display = "block";  currentValueOutput.innerHTML = "";  const map = new Map();};

Далее нам нужно создать цикл for, который будет отвечать за перебор каждого числа в массиве и обновление анимации со временем.

const optimalApproach = async () => {  optimalSolutionBtn.setAttribute("disabled", "");  table.style.display = "block";  currentValueOutput.innerHTML = "";  const map = new Map();  for (let i = 0; i < testCaseArray.length; ++i) {      }};

Внутри цикла нам нужно добавить выражение для вычисления разности.

const optimalApproach = async () => {  optimalSolutionBtn.setAttribute("disabled", "");  table.style.display = "block";  currentValueOutput.innerHTML = "";  const map = new Map();  for (let i = 0; i < testCaseArray.length; ++i) {    const difference = target - testCaseArray[i];  }};

Затем нам нужно добавить setTimeout, который задержит изменения на 2 секунды в HTML разметке и поможет с эффектом анимации.

const optimalApproach = async () => {  optimalSolutionBtn.setAttribute("disabled", "");  table.style.display = "block";  currentValueOutput.innerHTML = "";  const map = new Map();  for (let i = 0; i < testCaseArray.length; ++i) {    const difference = target - testCaseArray[i];    await new Promise((resolve) => setTimeout(resolve, 2000));  }};

Затем нам нужно добавить оператор if, чтобы проверить, содержит ли карта значения разности.

const optimalApproach = async () => {  optimalSolutionBtn.setAttribute("disabled", "");  table.style.display = "block";  currentValueOutput.innerHTML = "";  const map = new Map();  for (let i = 0; i < testCaseArray.length; ++i) {    const difference = target - testCaseArray[i];    await new Promise((resolve) => setTimeout(resolve, 2000));    if (map.has(difference)) {            }  }};

Внутри оператора if нам нужно обновить содержимое текста, чтобы показать окончательный результат массива индексов на экране. Мы будем использовать метод get, чтобы получить значение индекса из map.

 if (map.has(difference)) {      finalOptimalSolutionResult.textContent = `Final indices: [${map.get(        difference      )}, ${i}]`; }

Нам также нужно обновить вывод, чтобы показать сообщение о том, что мы нашли пару чисел, дающих в сумме целевую величину.

  if (map.has(difference)) {      finalOptimalSolutionResult.textContent = `Final indices: [${map.get(        difference      )}, ${i}]`;      currentValueOutput.innerHTML = `      <p>Difference(${difference}) = target(${target}) - current number(${testCaseArray[i]})</p>      <p>Is the difference(${difference}) in our map? YES, we found that pair of numbers that add up to the target.</p>    `;  }

Также нам нужно удалить атрибут disabled из optimalSolutionBtn и вернуться из функции.

  if (map.has(difference)) {      finalOptimalSolutionResult.textContent = `Окончательные индексы: [${map.get(        difference      )}, ${i}]`;      currentValueOutput.innerHTML = `      <p>Разница(${difference}) = цель(${target}) - текущее число(${testCaseArray[i]})</p>      <p>Разница(${difference}) есть в нашей карте? ДА, мы нашли пару чисел, которые в сумме дают цель.</p>    `;      optimalSolutionBtn.removeAttribute("disabled");      return;  }

Если пара не найдена, то нам нужно добавить ветку else.

  if (map.has(difference)) {      finalOptimalSolutionResult.textContent = `Окончательные индексы: [${map.get(        difference      )}, ${i}]`;      currentValueOutput.innerHTML = `      <p>Разница(${difference}) = цель(${target}) - текущее число(${testCaseArray[i]})</p>      <p>Разница(${difference}) есть в нашей карте? ДА, мы нашли пару чисел, которые в сумме дают цель.</p>    `;      optimalSolutionBtn.removeAttribute("disabled");      return;  } else {          }

Внутри ветки else нужно обновить сообщение, чтобы показать, что мы не нашли пару и текущее число testCaseArray[i] будет добавлено в map вместе с его индексом.

else {      currentValueOutput.innerHTML = `        <p>Разница(${difference}) = цель(${target}) - текущее число(${testCaseArray[i]})</p>        <p>Разница(${difference}) есть в нашей карте? Нет.</p>        <p>Добавьте текущее число ${testCaseArray[i]} к нашей карте.</p>      `;}

Затем нужно обновить вывод таблицы с текущим числом и его индексом.

else {      currentValueOutput.innerHTML = `        <p>Разница(${difference}) = цель(${target}) - текущее число(${testCaseArray[i]})</p>        <p>Разница(${difference}) есть в нашей карте? Нет.</p>        <p>Добавьте текущее число ${testCaseArray[i]} к нашей карте.</p>      `;      tableBodyOutput.innerHTML += `      <tr>        <td>${testCaseArray[i]}</td>        <td>${i}</td>      </tr>    `;}

Наконец, используйте метод set, чтобы установить текущее число и его индексное значение в map.

else {      currentValueOutput.innerHTML = `        <p>Разница(${difference}) = цель(${target}) - текущее число(${testCaseArray[i]})</p>        <p>Разница(${difference}) есть в нашей карте? Нет.</p>        <p>Добавьте текущее число ${testCaseArray[i]} к нашей карте.</p>      `;      tableBodyOutput.innerHTML += `      <tr>        <td>${testCaseArray[i]}</td>        <td>${i}</td>      </tr>    `;      map.set(testCaseArray[i], i);}

Вот полный код для вашей функции optimalApproach.

const optimalApproach = async () => {  optimalSolutionBtn.setAttribute("disabled", "");  table.style.display = "block";  currentValueOutput.innerHTML = "";  const map = new Map();  for (let i = 0; i < testCaseArray.length; ++i) {    const difference = target - testCaseArray[i];    await new Promise((resolve) => setTimeout(resolve, 2000));    if (map.has(difference)) {      finalOptimalSolutionResult.textContent = `Окончательные индексы: [${map.get(        difference      )}, ${i}]`;      currentValueOutput.innerHTML = `      <p>Разница(${difference}) = цель(${target}) - текущее число(${testCaseArray[i]})</p>      <p>Разница(${difference}) есть в нашей карте? ДА, мы нашли пару чисел, которые в сумме дают цель.</p>    `;      optimalSolutionBtn.removeAttribute("disabled");      return;    } else {      currentValueOutput.innerHTML = `        <p>Разница(${difference}) = цель(${target}) - текущее число(${testCaseArray[i]})</p>        <p>Разница(${difference}) есть в нашей карте? Нет.</p>        <p>Добавьте текущее число ${testCaseArray[i]} к нашей карте.</p>      `;      tableBodyOutput.innerHTML += `      <tr>        <td>${testCaseArray[i]}</td>        <td>${i}</td>      </tr>    `;      map.set(testCaseArray[i], i);    }  }};

Чтобы протестировать эту визуализацию, добавьте прослушиватель событий к optimalSolutionBtn и передайте событие "click" и ссылку на функцию optimalApproach.

optimalSolutionBtn.addEventListener("click", optimalApproach);

При нажатии на кнопку “Показать визуализацию” для решения карты вы должны увидеть анимацию, как показано ниже:

Screenshot-2023-11-20-at-12.05.05-PM
Визуализация для решения карты

Как сбросить вывод таблицы для решения карты

Если вы попытаетесь запустить анимацию несколько раз, то увидите, что таблица показывает результаты предыдущего запуска.

Screenshot-2023-11-20-at-12.09.35-PM
Таблица, показывающая результаты предыдущего запуска

Чтобы исправить это, мы можем сбросить таблицу перед каждым запуском анимации. Начните с создания функции resetTable перед вашей функцией optimalApproach.

const resetTable = () => {    };const optimalApproach = async () => {    .........

Внутри этой функции очистите таблицу и окончательные текстовые результаты.

const resetTable = () => {  tableBodyOutput.innerHTML = "";  finalOptimalSolutionResult.textContent = "";};

Вызовите функцию resetTable внутри вашей функции optimalApproach, чтобы увидеть результаты сброса таблицы.

const optimalApproach = async () => {  resetTable();  optimalSolutionBtn.setAttribute("disabled", "");    ...........

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

Финальный код решения и живой пример

Вот полный код решения на JavaScript для обеих визуализаций:

const bruteForceSolutionBtn = document.getElementById("brute-force-visual-btn");const bruteForceNumbersOutput = document.querySelector(  "#brute-force-output > .numbers-array");const bruteForceTextOutput = document.querySelector(  "#brute-force-output > .result-text");const optimalSolutionBtn = document.getElementById("optimal-visual-btn");const currentValueOutput = document.getElementById("current-value-output");const finalOptimalSolutionResult = document.getElementById(  "final-optimal-result");const table = document.getElementById("table-output");const tableBodyOutput = document.getElementById("map-table-body");const testCaseArray = [11, 15, 2, 7];const target = 9;let currentNum;let currentCompliment;const getClassName = (num) => {  switch (num) {    case currentNum:      return "class='current-num'";    case currentCompliment:      return "class='compliment-num'";    default:      return "";  }};const bruteForceApproach = async () => {  bruteForceSolutionBtn.setAttribute("disabled", "");  for (let i = 0; i < testCaseArray.length; ++i) {    currentNum = testCaseArray[i];    for (let j = i + 1; j < testCaseArray.length; ++j) {      currentCompliment = testCaseArray[j];      await new Promise((resolve) => setTimeout(resolve, 1000));      bruteForceNumbersOutput.innerHTML = testCaseArray        .map(          (num, index) =>            `              <span ${getClassName(num)}>              ${testCaseArray[index]}              </span>            `        )        .join("");      bruteForceTextOutput.textContent = `Does the sum of ${currentNum} + ${currentCompliment} equal ${target}? NO!`;      if (currentNum + currentCompliment === target) {        bruteForceTextOutput.textContent = `Final indices: [${i}, ${j}]`;        bruteForceSolutionBtn.removeAttribute("disabled");        return;      }    }  }};const resetTable = () => {  tableBodyOutput.innerHTML = "";  finalOptimalSolutionResult.textContent = "";};const optimalApproach = async () => {  resetTable();  optimalSolutionBtn.setAttribute("disabled", "");  table.style.display = "block";  currentValueOutput.innerHTML = "";  const map = new Map();  for (let i = 0; i < testCaseArray.length; ++i) {    const difference = target - testCaseArray[i];    await new Promise((resolve) => setTimeout(resolve, 2000));    if (map.has(difference)) {      finalOptimalSolutionResult.textContent = `Final indices: [${map.get(        difference      )}, ${i}]`;      currentValueOutput.innerHTML = `      <p>Difference(${difference}) = target(${target}) - current number(${testCaseArray[i]})</p>      <p>Is the difference(${difference}) in our map? YES, we found that pair of numbers that add up to the target.</p>    `;      optimalSolutionBtn.removeAttribute("disabled");      return;    } else {      currentValueOutput.innerHTML = `        <p>Difference(${difference}) = target(${target}) - current number(${testCaseArray[i]})</p>        <p>Is the difference(${difference}) in our map? No.</p>        <p>Add the current number ${testCaseArray[i]} to our map.</p>      `;      tableBodyOutput.innerHTML += `        <tr>          <td>${testCaseArray[i]}</td>          <td>${i}</td>        </tr>      `;      map.set(testCaseArray[i], i);    }  }};bruteForceSolutionBtn.addEventListener("click", bruteForceApproach);optimalSolutionBtn.addEventListener("click", optimalApproach);

Вот ссылка еще раз на финальный результат на CodePen.

Заключение

Надеюсь, вам понравился этот проект и вы узнали немного больше о том, как работает проблема Two Sum.

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


Leave a Reply

Your email address will not be published. Required fields are marked *