Skip Navigation
Telegram
Разбор полетов: как правильно убивать верблюдов 🐪
Разбор полетов: как правильно убивать верблюдов 🐪

В рамках универсального перевода строки в snake_case такой подход с регулярками, наверное, самый хороший вариант:

import re

def to_snake_case(s: str) -> str:
s = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', s)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s).replace('-', '_').lower()


🧐 Как это работает?

1️⃣ re.sub('(.)([A-Z][a-z]+)', r'\1_\2', s) заменяет каждую заглавную букву, предшествующую строчной, на ту же букву, но с подчеркиванием перед ней.

2️⃣ re.sub('([a-z0-9])([A-Z])', r'\1_\2', s) меняет каждую заглавную букву, предшествующую строчной или цифре, на эту же букву, но с подчеркиванием перед ней.

3️⃣ Потом всё приводим к нижнему регистру и заменяем дефис на подчеркивание.

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

from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel, to_snake

class FrontendData(BaseModel):
model_config = ConfigDict(
alias_generator=to_camel, # Автоматом ждет camelCase на вход
populate_by_name=True
)

python_talk: str # В коде работаем с православным snake_case

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

А если Pydantic в проекте нет, есть микро-библиотеки вроде inflection, которые делают то же самое:
import inflection

inflection.underscore("CamelCaseAnd-kebab-case")
# -> 'camel_case_and_kebab_case'

inflection.camelize("python_talk", uppercase_first_letter=False)
# -> 'pythonTalk'


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

#алгособес
Telegram
Изгоняем верблюдов из кодовой базы 🐫
Изгоняем верблюдов из кодовой базы 🐫

Представьте классическую боль: вы интегрируетесь со сторонним API (или парсите данные от фронтендеров), и вам прилетает JSON, где ключи названы кто во что горазд.

Ваша задача — написать функцию, которая берет строку в kebab-case (dash-notation), camelCase или UpperCamelCase и приводит это непотребство к православному PEP8-совместимому snake_case.

Примеры:
"python-talk" -> "python_talk"
"pythonTalk" -> "python_talk"
"PythonTalk" -> "python_talk"


Пишите свои сниппеты в комменты, посмотрим, кто во что горазд 👇

#алгособес
Telegram
Разбираем сдвиги ноликов 0️⃣
Разбираем сдвиги ноликов 0️⃣

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

Конечно, классический паттерн для решения тут — два указателя.
Вот идеальное решение со сложностью O(n) по времени и O(1) по памяти:

def move_zeroes(nums: list[int]) -> None:
insert_pos = 0

for i in range(len(nums)):
if nums[i] != 0:
# Свапаем ненулевой элемент с "левым" нулем
nums[insert_pos], nums[i] = nums[i], nums[insert_pos]
insert_pos += 1


Никаких аллокаций. Никаких pop() из середины списка. Проходим по массиву ровно один раз. Переменная insert_pos всегда указывает на первый доступный ноль. Как только находим число отличное от нуля — просто меняем их местами благодаря встроенному механизму распаковки кортежей в Python.

🐍 Но было бы не интересно без чего-то более хитрого. Вот вам питонячий флекс в одну строку без генераторов:
nums.sort(key=bool, reverse=True)


Как это работает? bool(0) дает False, остальные числа — True. Встроенная сортировка Timsort в Python стабильна — она строго сохраняет исходный порядок равных элементов. Все True (числа) съедутся влево, сохраняя свой порядок, а все False (нули) улетят вправо.

Да, формально тут сложность O(n log n), что алгоритмически хуже двух указателей. Но из-за того, что Timsort написан на зубодробительном C, на реальных списках небольшого объема этот код физически порвет циклы на чистом Python по скорости выполнения.

#алгособес
Telegram
Двигаем нолики 0️⃣
Двигаем нолики 0️⃣

📝На вход подается список целых чисел nums. Нужно сдвинуть все нули в конец списка, сохранив исходный порядок ненулевых элементов.

Пример:
Input:  [1, 0, 1, 2, 0, 1, 3]
Output: [1, 1, 2, 1, 3, 0, 0]


Сделать можно десятком способов, но какой оптимальный и лаконичный? Ваши варианты в комментах 👇

#алгособес
Telegram
Разбор: Охота на смайлики 🕵️‍♂️
Разбор: Охота на смайлики 🕵️‍♂️

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

import re

def num_emojis(arg):
return sum([re.match("^[:;][~-]?[)D]$",i) is not None for i in arg])


Работает? Да. Оставлять такое в проде? Нет.

1️⃣ Грех аллокации
Квадратные скобки внутри sum([ ... ]) означают, что Питон сначала создаст в памяти полный список из True/False размером с исходный массив, и только потом его просуммирует. Если на вход прилетит лог на 10 миллионов строк, ваша память выйдет из чата.
Убираем скобки — получаем генераторное выражение. Память O(1) вместо O(n).

2️⃣ Избыточные проверки
Конструкция match(...) is not None имеет право на жизнь, но в Питоне принято использовать встроенную "правдивость" (truthiness) объектов. Плюс, нам нужно просто посчитать количество совпадений. Идиоматичный подход: sum(1 for ... if ...)

3️⃣ Перекомпиляция
Вызов re.match внутри цикла каждый раз заставляет Python лезть в кэш регулярок. Если строк много, паттерн нужно компилировать один раз до цикла.

Выкидываем мусор и пишем так:
import re

# Выносим компиляцию наверх
SMILEY_PATTERN = re.compile(r"^[:;][~-]?[)D]$")

def count_smileys(faces: list[str]) -> int:
return sum(1 for face in faces if SMILEY_PATTERN.match(face))


☝️Но главная проблема этого решения не в синтаксисе.
Зачем нам вообще тут регулярки?

Давайте посчитаем: у нас 2 варианта глаз, 3 варианта носа (включая его отсутствие) и 2 варианта рта.
Существует ровно 12 валидных смайликов. Как и написал @archimage_wiz, вместо того чтобы на каждой строке запускать тяжелую машину состояний регулярных выражений, нам достаточно проверить вхождение строки в заранее подготовленное множество (set).

Поиск в множестве работает за константное время O(1) (это хэш-таблица).

Решение здорового человека:
from typing import List

VALID_SMILES = {
':)', ';)', ':-)', ';-)', ':~)', ';~)',
':D', ';D', ':-D', ';-D', ':~D', ';~D'
}

def num_emojis_pro(arr: List[str]) -> int:
return sum(s in VALID_SMILES for s in arr)


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

Сложные инструменты — это круто. Но умение обходиться простыми — это уже скилл.

#алгособес
Telegram
Охота на смайлики 😀
Сегодня очередная классика.
Задача:
Написать функцию, которая принимает на вход список строк и возвращает число равное количеству смайликов в этом списке.

Что считаем смайликом:
🟢у смайлика обязательно должны быть глаза. Варианта 2: : и ;
🟢у смайлика может быть нос (а может и не быть). Носы такие: - и ~
🟢 смайлик должен улыбаться (грустные не принимаются!), улыбки такие: ) и D
🟢если в строке есть ещё что-то кроме этого, то это уже не смайлик.

Примеры для проверки:
[':)', ';(', ';}', ':-D']       # -> 2
[';D', ':-(', ':-)', ';~)`] # -> 3
[';]', ':[', ';*', ':$', ';-D'] # -> 1


Варианты решений кидайте в комменты 👇

#алгособес
Telegram
Как выжать максимум из однострочника 🧠
Как выжать максимум из однострочника 🧠

Что там с подсчетом ноликов?

Базовый инстинкт большинства — написать list comprehension:
max([len(i) for i in s.split('1')])

Работать будет. Но вы сначала сплитите строку (создавая список в памяти), а потом прогоняете по нему цикл, чтобы создать ещё один список целых чисел, чтобы скормить его max(). Двойной расход памяти на ровном месте.

Но можно и генератор:
max((len(chunk) for chunk in s.split('1')))

По памяти тут всё красиво — промежуточный список длин не создается. Но вот по скорости генераторные выражения внутри встроенных функций в Python работают медленнее из-за оверхеда на итерацию и вызовы функций на уровне байткода.

А так еще лучше:
max(map(len, s.split('1')))

Мы избавляемся от питонячьего цикла. map реализован на Си, он ленивый, работает быстро, память не жрет. Большинство на этом радостно успокаивается.

Но... и это не оч. Задайте себе вопрос: зачем мы вообще считаем длины всех кусков до того, как найдем максимальный?

Строки в Python сравниваются лексикографически. Символ по символу. Следовательно, строка "000" всегда больше, чем "00". Нам вообще не нужно вычислять длины подстрок для сравнения.

Надо так!
len(max(s.split('1')))


Почему это лучшее решение? 🤔
1. Вызов s.split('1') мгновенно отдает список строк.
2. max() проглатывает этот список и на уровне C сравнивает сами строки. Никаких map, никаких вызовов len для каждого элемента, никакого вызова функций в цикле.
3. Мы берем len() ровно один раз — от победившей строки.

Меньше телодвижений интерпретатора = быстрее код.

#алгособес
Telegram
🐍 Битва однострочников
🐍 Битва однострочников

Сегодня вам нужно проанализировать двоичную строку, состоящую только из нулей и единиц 0️⃣1️⃣1️⃣0️⃣

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

Например, если дана строка "1001101000110", то функция должна вернуть 3.

Конечно, такую простую задачу надо в однострочник. Да ещё и быстрый. Предлагайте 👇🏻

#алгособес
Telegram
🕳 Глубина скобочек
Для задачки было предложено такое однострочное решение с разными вариациями:
def solve(string):
s = ''.join([ch for ch in string if ch in '()'])
return len(max(s.split(')'), key=len))


Рабочее решение? Нет.
Если мы скормим скормим этой функции строку (()(())), то правильный ответ (глубина): 3, а функция вернет 2. Логика "количество открывающих скобок подряд" ломается на любых вложенных структурах, которые идут не первой веткой.

☝🏻 Как решать правильно
Скучное, но рабочее решение со сложностью O(n) подразумевает обычный счетчик. Мы идем по строке, увеличиваем счетчик на (, уменьшаем на ) и на каждом шаге обновляем глобальный максимум.

def max_depth(s: str) -> int:
current_depth = 0
max_depth = 0

for char in s:
if char == '(':
current_depth += 1
max_depth = max(max_depth, current_depth)
elif char == ')':
current_depth -= 1
# Валидация корректности (опционально)
if current_depth < 0:
return -1

return max_depth if current_depth == 0 else -1


Вариант для эстетов (функциональный):

Если уж очень хочется в одну строку, можно использовать accumulate. Мы мапим скобки в 1 и -1, считаем префиксные суммы (accumulate) и берем максимум.

from itertools import accumulate

def solve_poly(s: str) -> int:
# Превращаем '(' в 1, ')' в -1, остальное в 0
depths = accumulate(1 if c == '(' else -1 if c == ')' else 0 for c in s)
return max(depths, default=0)


#алгособес
Telegram
🙁Глубина скобочек 😌
🙁Глубина скобочек 😌

Задачи на скобочные последовательности переживут нас всех. Сегодняшняя вариация — на поиск глубины.

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

Примеры:
solve('1 + 2') 
# Результат: 0

solve('(1) + ((2))')
# Результат: 2 (максимум во второй части выражения)

solve('(3 * n + 1) / 2')
# Результат: 1


Кидайте свои однострочники (и не только) в комментарии 👇🏻

#алгособес