2.2. Демонстрация - Разбор задачи валидации скобочных выражений
Шаг 1
Эту задачу частенько можно встретить на собеседованиях. Возможно, с такой проблемой вы уже сталкивались и так или иначе успешно решали. Звучит она довольно просто: в данном нам тексте нужно провалидировать, что все скобки корректно расставлены. Например, (((())())) является корректной расстановкой, а ()( — нет. Если вы ещё не решали эту задачу, остановитесь и попробуйте сами.
import {test} from "./tester"
/*
Примеры:
validateparenthesis('(((())()))') => true
validateparenthesis('())') => false
*/
function validateparenthesis(text) {
return true;
}
test(validateparenthesis('(((())()))'), true);
test(validateparenthesis('())'), false);
Шаг 2
Решение, которое первым приходит в голову — просто посчитать количество открывающих и закрывающих скобок и сравнить эти числа. Наши тесты на этом решении проходят.
import {test} from "./tester"
/*
Примеры:
validateparenthesis('(((())()))') => true
validateparenthesis('())') => false
*/
function validateparenthesis(text) {
const openingparenthesisCount = text.split('').filter(character => character === '(').length;
const closingparenthesisCount = text.split('').filter(character => character === ')').length;
return openingparenthesisCount === closingparenthesisCount;
}
test(validateparenthesis('(((())()))'), true);
test(validateparenthesis('())'), false);
Шаг 3
Однако если мы введем ещё один пример, то наше решение не сработает и сломается — например, об )(. Это, кстати, ещё одно важное правило. Если вы думаете, что пришли к решению, попробуйте придумать ещё несколько примеров входных данных: возможно, вы рассмотрели не все случаи.
import {test} from "./tester"
/*
Примеры:
validateparenthesis('(((())()))') => true
validateparenthesis('())') => false
validateparenthesis(')(') => false
*/
function validateparenthesis(text) {
const openingparenthesisCount = text.split('').filter(character => character === '(').length;
const closingparenthesisCount = text.split('').filter(character => character === ')').length;
return openingparenthesisCount === closingparenthesisCount;
}
test(validateparenthesis('(((())()))'), true);
test(validateparenthesis('())'), false);
test(validateparenthesis(')('), false);
Шаг 4
Давайте подумаем, как можно решить эту задачу. По мере продвижения по тексту у нас могут встречаться скобки. Если мы встречаем открывающую скобку, то увеличиваем уровень вложенности на один, а если закрывающую — уменьшаем. Например: (вот здесь мы имеем первый уровень вложенности скобок (тут второй), здесь снова первый), а тут уже вложенности нет совсем.
import {test} from "./tester"
/*
Примеры:
validateparenthesis('(((())()))') => true
validateparenthesis('())') => false
validateparenthesis(')(') => false
*/
function validateparenthesis(text) {
return true;
}
test(validateparenthesis('(((())()))'), true);
test(validateparenthesis('())'), false);
test(validateparenthesis(')('), false);
Шаг 5
Тогда можно сделать довольно простой алгоритм, который будет увеличивать и уменьшать уровень вложенности в зависимости от того, какую скобку мы встречаем. Если в конце мы будем иметь нулевую вложенность и по ходу алгоритма не уйдём в минус — то будем считать, что скобки расставлены верно.
import {test} from "./tester"
/*
Примеры:
validateparenthesis('(((())()))') => true
validateparenthesis('())') => false
validateparenthesis(')(') => false
*/
function validateparenthesis(text) {
let parenthesisCount = 0;
for (const character of text) {
if (character === '(') {
parenthesisCount++;
}
if (character === ')') {
if (parenthesisCount === 0) {
return false;
}
parenthesisCount--;
}
}
return parenthesisCount === 0;
}
test(validateparenthesis('(((())()))'), true);
test(validateparenthesis('())'), false);
test(validateparenthesis(')('), false);
Шаг 6
А ещё эту задачу можно решить при помощи стека: каждая скобка, увеличивающая вложенность, будет отправляться наверх стека, а каждая закрывающая — доставать её оттуда. Выглядеть это будет примерно так же, как и в решении со счётчиком, но вместо счётчика появится вспомогательный массив.
import {test} from "./tester"
/*
Примеры:
validateparenthesis('(((())()))') => true
validateparenthesis('())') => false
validateparenthesis(')(') => false
*/
function validateparenthesis(text) {
const stack = [];
for (const character of text) {
if (character === '(') {
stack.push(character)
}
if (character === ')') {
if (!stack.length) {
return false;
}
stack.pop();
}
}
return !stack.length;
}
test(validateparenthesis('(((())()))'), true);
test(validateparenthesis('())'), false);
test(validateparenthesis(')('), false);
Шаг 7
Кажется, что для решения задачи в текущей постановке гораздо проще использовать счётчики, и это действительно так. Однако решение через счётчик будет работать только с одним видом скобок. Что если мы хотим также валидировать ещё и строки вида ([)]? Тогда можно доделать наше решение со стеком под работу с различными скобками. Учитывая, что из стека мы всегда будем доставать самую последнюю встретившуюся открывающую скобку, мы легко сможем проверить, подходят ли скобки друг другу. А ещё плюс такого решения в том, что мы не просто можем масштабировать его на новые скобки, а можем и кавычки туда воткнуть! Например, для строчки [()) мы сначала встретим [ и положим её в стек. Потом положим туда же ( — в стеке окажется ['[', '(']. Затем встретим закрывающую скобку. Достанем из стека последнюю попавшуюся нам скобку ( и увидим, ага, () формируют парные скобки. Идём дальше, встречаем ещё скобку ) и опять достаём скобку из стека — [. [) — непарные скобки, а значит, и строка построена неправильно, возвращаем false.
import {test} from "./tester"
/*
Примеры:
validateparenthesis('(((())()))') => true
validateparenthesis('())') => false
validateparenthesis(')(') => false
validateparenthesis('([])') => true
validateparenthesis('([)]') => false
*/
const parantesisPairs = {
'(': ')',
'[': ']',
};
// А для более быстрой проверки, является ли символ нужной нам скобкой, можем заиспользовать уже знакомый Set!
const openingParenthesis = new Set(Object.keys(parantesisPairs));
const closingParenthesis = new Set(Object.values(parantesisPairs));
function validateparenthesis(text) {
const stack = [];
for (const character of text) {
if (openingParenthesis.has(character)) {
stack.push(character)
}
if (closingParenthesis.has(character)) {
if (!stack.length) {
return false;
}
const parenthesis = stack.pop();
if (character !== parantesisPairs[parenthesis]) { // Если наша скобка — не закрывающая текущей открывающей
return false;
}
}
}
return !stack.length;
}
test(validateparenthesis('(((())()))'), true);
test(validateparenthesis('())'), false);
test(validateparenthesis(')('), false);
test(validateparenthesis('([])'), true);
test(validateparenthesis('([)]'), false);
File tester.js
const results = document.getElementById('results')
export function test(left, right) {
const result = document.createElement('li');
if (left === right) {
result.innerHTML = "Тест пройден!";
result.style.color = "green";
} else {
result.innerHTML = `Тест не пройден! ${left} не равно ${right}`;
result.style.color = "red";
}
results.appendChild(result);
}