Перейти к содержанию

Longest substring without repeating characters

Продолжаю решать задачи на LeetCode. Сегодня мой мозг рубится с задачей Longest Substring Without Repeating Characters. На моем сайте в рубрике Решаем задачи вы можете посмотреть обзоры решения других задач с данной платформы.

Условия задачи

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

Что такое подстрока и подпоследовательность

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

Итак, уже в самом заголовке задачи есть термин «подстрока», которому необходимо дать определение.

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

Применительно к условиям задачи: “abcabcbb” – это строка, а “abc” – это подстрока, которую мне и необходимо найти.

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

Дана строка: “pwwkew”. Самой длинной не непрерывной подстрокой для данной строки будет подстрока “wke”. В примере авторы задачи обращают наше внимание на то, что  ответ должен быть подстрокой, а “pwke” — это подпоследовательность, а не подстрока.

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

Демонстрация подпоследовательности:

Демонстрация подпоследовательности

Первый подход (неверный)

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

Прогоняю через функцию три варианта входных данных (“abcabcbb”, “bbbbb”, “pwwkew”) и получаю следующие результаты: [‘a’, ‘b’, ‘c’], [‘b’] и [‘p’, ‘w’, ‘k’, ‘e’], соответственно.

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

Отрицательный результат – это тоже результат, так что продолжаю думать. На третьем примере входных данных видно, что символ “p” не должен войти в искомую подстроку, так как между “p” и “k” есть две повторяющиеся буквы “w”, что должно прервать подстроку.

Подробнее про третий случай

То есть, на первой итерации цикла я беру первый символ в строке “pwwkew” и добавляю его в список substring. На второй итерации я беру символ “w”, который является вторым по счету, и сравниваю его со списком substring. Совпадений не выявлено, добавляю его в подстроку.

А вот на третьей итерации, получив для сравнения третий символ “w” строки s я увижу совпадение, так как символ “w” уже есть в списке substring (был добавлен на второй итерации работы цикла for).

И вот в этот самый момент нужно сделать паузу и подумать, а что делать дальше. По сути, у меня получилась одна подстрока, которая состоит из “pw”, и вторая подстрока, которая состоит из “wke”. По хорошему, мне бы их как-то распознать и сохранить, чтобы затем выбрать из них самую длинную.

Второй подход (верный, но медленный)

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

Предлагаю сразу обработать два случая:

  1. когда метод на вход получает пустую строку (в этом случае возвращаем 0);
  2. когда метод получает строку, которая состоит из одинаковых символов (в такой ситуации возвращаем 1, так как длина уникальной подстроки будет равна единице).
if not s:
    return 0
if max(s) == min(s):
    return 1

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

  • substring – в этот список буду добавлять неповторяющиеся символы.
  • substring_length – в этом списке буду хранить длину неповторяющихся подстрок, чтобы в дальнейшем выбрать максимальное значение, то есть самую длинную подстроку.

На следующем этапе перебираю все символы в строке. Перебор буду осуществлять с помощью двух циклов for.

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

for index_symbol in range(len(s)):
    for symbol in s[index_symbol:]:
        pass

Далее делаю проверку, нет ли символа в списке substring. Если символ ранее не был добавлен в список substring, то методом append я его туда добавляю и перехожу к следующей итерации вложенного цикла for, на которой проверяю следующий символ итерируемого отрезка строки s.

Как только встречается символ, который уже есть в списке substring, в дело включается блок else.

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

if symbol not in substring:
    substring.append(symbol)
else:
    substring_length.append(len(substring))
    substring.clear()
    break

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

return max(substring_length)

Полный код решения задачи

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        if not s:
            return 0
        if max(s) == min(s):
            return 1

        substring = []
        substring_length = []
        
        for index_symbol in range(len(s)):
            for symbol in s[index_symbol:]:
                if symbol not in substring:
                    substring.append(symbol)
                else:
                    substring_length.append(len(substring))
                    substring.clear()
                    break

        return max(substring_length)

Результаты тестов на LeetCode

  • Runtime – 1161 ms — обгоняет 5,00% пользователей Python
  • Memory – 16,93 mb — обгоняет 7,42 % пользователей Python
Скриншот результатов работы решения с сайта LeetCode. Выше информация продублирована текстом

В качестве заключения

Как всегда, я открыт к предложениям о сотрудничестве, конструктивной критике и всему новому и интересному.

Я доступен для связи в Telegram @Kirill_Barabanshchikov или по почте: bks2408@mail.ru

Мой профиль на GitHub: https://github.com/BKSLab