English· Español· Deutsch· Nederlands· Français· 日本語· ქართული· 繁體中文· 简体中文· Português· Русский· العربية· हिन्दी· Italiano· 한국어· Polski· Svenska· Türkçe· Українська· Tiếng Việt· Bahasa Indonesia

nu

гость
1 / ?
назад к урокам

Что делает список полезным

Списки: первая структура данных

Список (в некоторых языках называется массивом) хранит элементы в определённом порядке. Ты можешь:

- Получить доступ к любому элементу по позиции: names[0] берёт первый элемент. O(1) — мгновенно.

- Добавить элемент в конец: names.append("Zoe"). O(1) — мгновенно.

- Пройти по каждому элементу в порядке: for name in names. O(N) — посещает каждый элемент один раз.

Списки сохраняют порядок, который ты им дал. Первый элемент остаётся первым. Последний — последним.

# A list of pets
pets = ["cat", "dog", "cat", "fish", "dog"]

print(pets[0])   # "cat"  — O(1) instant
print(pets[3])   # "fish" — O(1) instant
print(len(pets)) # 5 — duplicates kept

Заметь: "cat" появляется дважды. "dog" появляется дважды. Списки допускают дубликаты. Они хранят ровно то, что ты им дал, в том порядке, в котором ты это дал.

Но у списков есть слабость. Посмотри, что происходит, когда ты спрашиваешь: находится ли "fish" в списке?

# Membership check — is "fish" in the list?
if "fish" in pets:    # scans ["cat", "dog", "cat", "fish"...] one by one
    print("found!")   # had to check 4 items before finding it

Компьютер проверяет "cat" — нет. "dog" — нет. "cat" — нет. "fish" — да! Он просканировал 4 элемента, чтобы найти ответ. С 1000 элементами он может просканировать все 1000. Такая проверка принадлежности стоит O(N).

Предскажи стоимость сканирования

У тебя есть список из 500 имён студентов в случайном порядке.

Ты хочешь проверить, появляется ли "Zara" в списке.

students = ["Alex", "Maria", ..., "Zara", ...]  # 500 names
if "Zara" in students:
    print("enrolled")
Какое лучшее, худшее & среднее количество имён, которые компьютер должен проверить, чтобы найти "Zara"? Какой класс Big O описывает эту операцию? Объясни своё рассуждение.

Как множества работают иначе

Множества: встроены для проверок принадлежности

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

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

Список против множества: как данные живут в памяти — список сканирует, множество прыгает

# A set of pets
pets = {"cat", "dog", "cat", "fish", "dog"}

print(pets)      # {"cat", "dog", "fish"} — duplicates removed
print(len(pets)) # 3 — only unique items

Заметь три отличия от списков:

1. Без дубликатов. "cat" появился дважды в входных данных, но множество хранит только одну копию.

2. Без гарантированного порядка. Множество может вывести элементы в любом расположении.

3. Нет доступа по индексу. Ты не можешь написать pets[0] — множества не имеют позиций.

Компромисс: ты теряешь порядок & дубликаты. Ты выигрываешь O(1) проверку принадлежности — проверка, находится ли элемент, занимает одно и то же время, есть ли в множестве 5 элементов или 5 миллионов.

# Membership check — is "fish" in the set?
if "fish" in pets:    # hash("fish") → jump to bucket → found
    print("found!")   # checked exactly 1 spot, not 4

Множество против списка для проверки принадлежности

У тебя есть 500 имён студентов, хранящихся в множестве вместо списка.

students = {"Alex", "Maria", ..., "Zara", ...}  # 500 names
if "Zara" in students:
    print("enrolled")
Сколько элементов компьютер проверяет, чтобы определить, существует ли "Zara" в множестве из 500 имён? Какой класс Big O описывает это? Почему это отличается от версии со списком?

Сравнение рядом

Шпаргалка по операциям

Стоимость операций: список против множества — принадлежность, добавление, индекс, дедупликация

Используй список, когда тебе нужно:

- Элементы в определённом порядке (плейлист, рецепт, очередь)

- Доступ по позиции (items[3])

- Дубликаты сохранены ("cat" появляется 3 раза = 3 кошки)

Используй множество, когда тебе нужно:

- Быстрые проверки принадлежности ("видел ли я это раньше?")

- Автоматическое удаление дубликатов

- Порядок не важен

Используй оба, когда тебе нужны порядок И быстрый поиск:

seen = set()     # O(1) membership checks
result = []      # preserves insertion order
for item in data:
    if item not in seen:  # O(1) check
        seen.add(item)    # O(1) add
        result.append(item)  # O(1) append
# Total: O(N) — one pass, each step O(1)

Этот паттерн "множество + список" даёт тебе лучшее от обоих: упорядоченные результаты с быстрым обнаружением дубликатов.

Выбери правильную структуру

Три задачи. Для каждой, решу: список, множество или оба?

1. Тебе нужно хранить плейлист песен в том порядке, в котором их добавил пользователь.

2. Тебе нужно быстро проверить, было ли имя пользователя уже занято.

3. Тебе нужно удалить дублирующиеся письма из списка рассылки, но сохранить их в порядке, в котором они зарегистрировались.

Для каждой из трёх задач, какую структуру данных ты должен использовать (список, множество или оба)? Объясни почему для каждой.

Проблема дедупликации

Задача: удалить дубликаты, сохранить порядок

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

Попытка 1: только список (медленно)

signups = ["alice@a.com", "bob@b.com", "alice@a.com", "carol@c.com"]
unique = []
for email in signups:
    if email not in unique:  # O(N) scan of unique list
        unique.append(email)
# Works! But: O(N) check × N items = O(N²)

С 100 подписками, это делает ~5000 проверок. С 10000 подписками: ~50000000 проверок.

Попытка 2: множество + список (быстро)

signups = ["alice@a.com", "bob@b.com", "alice@a.com", "carol@c.com"]
seen = set()
unique = []
for email in signups:
    if email not in seen:   # O(1) hash lookup
        seen.add(email)     # O(1) add to set
        unique.append(email) # O(1) append to list
# O(1) check × N items = O(N)

Тот же результат. Тот же порядок. Но: 10000 подписок теперь требует ~10000 проверок вместо ~50000000.

Подсчитай ускорение

Версия со списком работает при O(N²). Версия с множеством+списком работает при O(N).

У тебя есть 10000 подписок на письма для дедупликации.

Сколько примерно проверок выполняет каждая версия при N=10000? Во сколько раз версия с множеством быстрее? Покажи свои вычисления.

Поиск общих элементов

Задача: найди элементы в обоих списках

У тебя есть два списка учебных классов. Тебе нужно найти студентов, записанных в оба класса.

Медленный подход: вложенные циклы O(N × M)

class_a = ["Alice", "Bob", "Carol", "Diana"]
class_b = ["Bob", "Diana", "Eve", "Frank"]
both = []
for student in class_a:        # N iterations
    if student in class_b:     # M scans each time
        both.append(student)
# O(N × M) — each student scanned against all of class_b

Быстрый подход: преобразуй один список в множество O(N + M)

class_a = ["Alice", "Bob", "Carol", "Diana"]
class_b_set = set(class_b)     # O(M) to build
both = []
for student in class_a:        # N iterations
    if student in class_b_set: # O(1) each time
        both.append(student)
# O(N + M) — build the set once, check N times at O(1) each

Или даже проще, используя встроенное пересечение множества Python:

both = set(class_a) & set(class_b)  # O(N + M)

Оператор & находит все элементы, которые появляются в обоих множествах.

Переписать для быстроты

В школе есть два клуба с 200 членами каждый. Этот код находит студентов в обоих клубах:

chess_club = [...]   # 200 names
art_club = [...]     # 200 names
both_clubs = []
for student in chess_club:
    if student in art_club:
        both_clubs.append(student)
Какой Big O этого кода? Переписать его для быстроты, используя множество. Какой Big O твоей улучшенной версии? Объясни разницу.

Твой фреймворк принятия решений

Твой фреймворк принятия решений о структурах данных

Каждый раз, когда ты пишешь код, который хранит коллекцию элементов, спроси:

1. Нужны ли мне элементы в порядке? → список

2. Нужны ли мне быстрые проверки принадлежности? → множество

3. Нужны ли мне И порядок И быстрые проверки? → множество + список вместе

4. Нужны ли мне дубликаты? → список (множества удаляют дубликаты)

---

Ключевое понимание этого урока: одна и та же программа, решающая одну и ту же задачу, может работать в 1000× или даже в 1000000× раз быстрее просто выбрав правильную структуру данных. Нет хитрых приёмов. Нет сложной математики. Просто спроси: какие операции выполняет мой код чаще всего? Потом выбери структуру, где эти операции стоят O(1) вместо O(N).

Этот урок охватил списки & множества. Существуют другие структуры данных (словари, деревья, кучи), которые решают другие задачи. Фреймворк принятия решений остаётся одинаковым: разберись в своих операциях, потом выбери структуру, которая их дешёвые.

---

Хочешь идти глубже? Наш урок Big O охватывает темпы роста & вычисления масштабирования в деталях. Смотри Big O: How Fast Is Fast Enough?. Наш курс unhamming охватывает реальный дефект (MOAD-0001), где этот точный список-в-множество прирост дал 1000× ускорение в production программном обеспечении. Смотри The Missing Chapter: Algorithmic Complexity.

Отладь этот код

Студент написал этот код для подсчёта, сколько уникальных слов появляется в книге:

words = book.split()           # list of all words
unique_count = 0
checked = []
for word in words:             # N words
    if word not in checked:    # scans checked list
        checked.append(word)
        unique_count += 1
print(unique_count)

Книга имеет 100000 слов.

Какой Big O этого кода? Почему он работает медленно на большой книге? Переписать его для решения той же задачи за O(N). Потом объясни: мог бы ты решить это в одной строке, используя множество? Как бы это выглядело?