Алгоритмы, связанные со строками

Поиск символа в строке

Реализую линейный поиск символа в строке.

Так как нужно найти один конкретный символ, а в строке - n символов, то проверить в худшем случае нужно будет все элементы. Значит, сложность будет равна O(n) - сколько элементов, столько и сравнений.

target = "a"
text = "mhtirogla" 
success = False  
 
for letter in text:
    if letter == target:
        success = True
        break
 
if success:
    print("Буква была найдена в строке")
else:
    print("Буквы в строке нет")

В строке было 9 букв, цикл сработал 9 раз, и мы нашли нужную букву. Всё сходится, и сложность O(n) подтвердилась, но что, если бы мы выполняли поиск в строке “algorithm”? Тогда бы мы нашли букву а на первой же итерации, цикл бы завершился и, получается, сложность была бы O(1)?
Нет. Big O говорит как раз о худшем случае для алгоритма. Когда пишем О(n), мы гарантируем, что, даже если всё будет плохо, мы найдём букву максимум за O(n).

Поиск подстроки в строке

Усложним предыдущую задачу: попробуем найти не один символ в строке, а маленькую строку внутри большой (подстроку в строке).

main_string = ‘Hello world!’ - основная строка.
sub_string = ‘ow’ - подстрока, которую алгоритм будет искать в основной строке.

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

main_length = len(main_string)
sub_length = len(sub_string)
success = False
 
# Мы можем проходить не по всей строке.
# Например, если мы ищем подстроку из 6 букв в слове из 10 букв, то проверить нужно только первые 4 буквы, так как подстрока просто не поместится в остатке строки, если поиск будет начинаться с 5-й буквы.  
# Пример:
# “1122334455” - весь текст;
# “123456” - подстрока, которую ищем.
# Надо проверить следующие варианты: начинаем с первого символа и проверяем шесть символов (столько, сколько их в подстроке)  
# “112233” == “123456”?
# строки не равны, берём второй символ
# “122334” == “123456”?
# тоже нет
# “223344” == “123456”?  
# “233445” == “123456”?  
# “334455” == “123456”?  
# А вот дальше двигаться нет смысла, так как в строке осталось всего 5 символов и они точно не могут быть равны подстроке из 6 символов:  
# “34455” != “123456”
# Значит, наш цикл должен был пройти от 0 до 4 (10 − 6 = 4) включительно.  
# Тогда, чтобы найти количество итераций в коде, нам нужно посчитать:(длина главной строки − длина подстроки + 1). +1 нужен, чтобы range дошёл до полученного числа включительно
count = main_length - sub_length + 1  
for i in range(count):  
    j = 0  
    # j - счётчик для подсчёта совпадений  
    while j < sub_length and main_string[i + j] == sub_string[j]:  
        # Цикл продолжается, пока:  
        # 1) количество совпадений меньше длины подстроки (если они будут равны, то дальше проверку делать нет смысла);  
        # 2) взятые символы равны (с помощью j мы перебираем символы в строке, i используем как сдвиг, чтобы брать из главной строки срез, начиная с текущего символа).  
        j += 1
 
    # Если после вложенного цикла число j равно длине подстроки, это значит, что мы нашли подстроку в нашей строке, цикл можно прерывать
    if j == sub_length:  
        success = True  
        break  
  
if success:  
    print('Подстрока найдена!')  
else:  
    print('Такой подстроки в строке нет :(')

Этот метод может быть неэффективным для длинных строк, так как его сложность составляет O(main_length * sub_length), где main_length - длина строки, а sub_length - длина подстроки. Однако это простой и понятный способ реализовать поиск подстроки в строке.

Проверка палиндрома

Палиндром - слово, фраза или последовательность символов, которые читаются одинаково ltr и rtl.
Например, слова «довод» и «комок» - палиндромы, потому что они читаются одинаково в обоих направлениях. Также палиндромами могут быть числа, такие как 121 или 12321.

Если есть строка text и нужно проверить, является ли она палиндромом, нужно убедиться, что символы её первой половины равны символам второй. Сделать это можно разными способами - рассмотрим несколько самых простых, которые можно реализовать без изучения дополнительных инструментов (кроме разве что индексов).

Вариант 1 (без использования индексов)

text = 'довод'  
reversed_text = ''  
  
for letter in text:  
    # Каждый символ строки добавляем В НАЧАЛО новой строки  
    reversed_text = letter + reversed_text
 
if text == reversed_text:  
    print('Это палиндром!')  
else:  
    print('Не палиндром!')

Так как для этого алгоритма нам надо пройти по каждому элементу строки, сложность такого алгоритма будет равна O(n).

Вариант 2 (с использованием индексов)

text = 'довод'    
text_length = len(text)  
success = True  
  
  
# За одну итерацию мы будем проверять два символа (с начала и с конца), поэтому итераций можно сделать в два раза меньше (text_length // 2):  
for i in range(text_length // 2):  
    # На каждой итерации сравниваем два символа:  
    # символ с начала text[i] с символом с конца text[-i - 1]  
    if text[i] != text[-i - 1]:  
        # Если хотя бы одна пара символов не совпадёт, то меняем флаг и завершаем цикл  
        success = False  
        break  
 
if success:  
    print('Это палиндром!')  
else:  
    print('Не палиндром!')

Здесь мы перебираем не все элементы строки, а половину (длина строки // 2). Потому что за одну итерацию проверяем сразу 2 символа, один - с начала строки (индекс i), другой - с конца (-i - 1), а центральный символ нам вообще проверять нет необходимости.

Получается, что этот алгоритм эффективнее предыдущего - его сложность примерно равна O(n/2). Она не дотягивает до O(log n), но это всё же лучше, чем O(n).


python