Python · Синтаксис · Проміжний

Сортування і компаратори

10 завдань

sorted() та list.sort() з функціями key=, reverse та багатоключовими компараторами.

sorted() vs list.sort(), key=, reverse= та стабільність сортування

#
Python має дві вбудовані функції сортування. Різниця важлива: ```python nums = [3, 1, 4, 1, 5] sorted(nums) # [1, 1, 3, 4, 5] — повертає НОВИЙ список, nums незмінний nums.sort() # змінює nums НА МІСЦІ, повертає None # Поширена помилка: result = nums.sort() # result дорівнює None! ``` Використовуй `sorted()`, коли потрібно зберегти оригінал або коли вхідні дані — будь-який ітерований об'єкт (не лише список). Використовуй `.sort()` для сортування на місці та економії пам'яті. **Зворотний порядок** ```python sorted([3, 1, 2], reverse=True) # [3, 2, 1] [3, 1, 2].sort(reverse=True) # на місці: [3, 2, 1] # Альтернатива через reversed() — повертає ітератор, не список list(reversed(sorted([3, 1, 2]))) # [3, 2, 1] ``` **key= — сортування за похідним значенням** Функція `key` викликається один раз для кожного елемента, результат використовується для порівняння: ```python words = ['banana', 'fig', 'apple', 'cherry'] sorted(words, key=len) # ['fig', 'apple', 'banana', 'cherry'] sorted(words, key=str.lower) # алфавітно без урахування регістру sorted(words, key=lambda w: w[-1]) # за останнім символом ``` **Стабільність — рівні елементи зберігають початковий порядок** Сортування Python є *стабільним*: якщо два елементи рівні при порівнянні, вони з'являються в тому самому відносному порядку, що й у вхідних даних. Це гарантовано — не деталь реалізації. ```python students = [ {'name': 'Alice', 'grade': 'B'}, {'name': 'Bob', 'grade': 'A'}, {'name': 'Carol', 'grade': 'B'}, ] by_grade = sorted(students, key=lambda s: s['grade']) # [Bob(A), Alice(B), Carol(B)] # Alice стоїть перед Carol після сортування за оцінкою, # бо вони були в такому порядку спочатку. ``` Стабільність важлива для багатоключового сортування: спочатку сортуй за вторинним ключем, потім за первинним — стабільність зберігає вторинний порядок всередині рівних первинних. ```python # Сортування за оцінкою (первинне), потім за ім'ям (вторинне) step1 = sorted(students, key=lambda s: s['name']) # спочатку за ім'ям step2 = sorted(step1, key=lambda s: s['grade']) # потім за оцінкою (стабільно!) # Результат: всередині однієї оцінки студенти в алфавітному порядку ```

Ключі-кортежі, operator.itemgetter та багатопольове сортування

#
**Ключі-кортежі — багатопольове сортування за один прохід** Коли функція ключа повертає кортеж, Python порівнює поелементно — спочатку перше поле, потім друге як критерій вирішення нічиєї: ```python students = [ {'name': 'Alice', 'grade': 'B', 'age': 22}, {'name': 'Bob', 'grade': 'A', 'age': 20}, {'name': 'Carol', 'grade': 'B', 'age': 21}, ] # Первинне: оцінка за зростанням, вторинне: вік за зростанням sorted(students, key=lambda s: (s['grade'], s['age'])) # [Bob(A,20), Carol(B,21), Alice(B,22)] ``` **operator.itemgetter та operator.attrgetter** Для типових випадків функції модуля `operator` чистіші і трохи швидші за лямбда: ```python from operator import itemgetter, attrgetter # Сортування списку словників за ключем sorted(students, key=itemgetter('grade')) # те саме, що lambda s: s['grade'] sorted(students, key=itemgetter('grade', 'age')) # багатопольове одним викликом # Сортування об'єктів за атрибутом from dataclasses import dataclass @dataclass class Person: name: str age: int people = [Person('Alice', 30), Person('Bob', 25), Person('Carol', 30)] sorted(people, key=attrgetter('age', 'name')) # за віком, потім за ім'ям ``` **Зворотний порядок для одного поля в багатопольовому сортуванні** Немає вбудованого способу обернути лише одне поле в ключі-кортежі. Стандартний трюк — заперечення числових полів: ```python # Оцінка за зростанням, вік за СПАДАННЯМ sorted(students, key=lambda s: (s['grade'], -s['age'])) # [Bob(A,20), Alice(B,22), Carol(B,21)] ``` Для нечислових полів використовуй двопрохідний трюк зі стабільним сортуванням з Блоку 1. **Сортування рядків без урахування регістру** ```python names = ['banana', 'Apple', 'cherry', 'Date'] sorted(names) # ['Apple', 'Date', 'banana', 'cherry'] (великі літери спочатку) sorted(names, key=str.lower) # ['Apple', 'banana', 'cherry', 'Date'] (справжній алфавіт) sorted(names, key=str.casefold) # те саме, але краще для символів з діакритикою ``` **max() та min() використовують той самий key=** ```python words = ['banana', 'fig', 'apple'] max(words, key=len) # 'banana' min(words, key=len) # 'fig' youngest = min(students, key=itemgetter('age')) ```

heapq, cmp_to_key та внутрішня будова Timsort

#
**heapq — ефективні операції з мін-купою** `heapq` підтримує список як мін-купу. Найменший елемент завжди за індексом 0. Використовуй, коли потрібен повторний доступ до мінімуму, а не повний відсортований список: ```python import heapq nums = [5, 1, 3, 8, 2] heapq.heapify(nums) # перетворити список на купу на місці: [1, 2, 3, 8, 5] heapq.heappush(nums, 0) # додати 0 і зберегти властивість купи heapq.heappop(nums) # видалити і повернути найменший: 0 heapq.heappop(nums) # наступний найменший: 1 # Отримати N найменших / найбільших без повного сортування heapq.nsmallest(3, nums) # [2, 3, 5] — O(n + k log n) heapq.nlargest(3, nums) # [8, 5, 3] # З функцією ключа: tasks = [{'priority': 3, 'name': 'писати тести'}, {'priority': 1, 'name': 'виправити баг'}] heapq.nsmallest(1, tasks, key=lambda t: t['priority']) # [{'priority': 1, ...}] ``` **functools.cmp_to_key — застарілі функції порівняння** Старий код (стиль Python 2) може використовувати функції порівняння, що повертають -1/0/1. `cmp_to_key` адаптує їх до сучасного інтерфейсу `key=`: ```python from functools import cmp_to_key def compare_last_char(a, b): if a[-1] < b[-1]: return -1 if a[-1] > b[-1]: return 1 return 0 sorted(['banana', 'fig', 'apple'], key=cmp_to_key(compare_last_char)) # ['banana', 'apple', 'fig'] — за останнім символом: a, e, g ``` За можливості надавай перевагу функціям `key=` — вони чистіші і швидші. **Timsort — чому сортування Python швидке** Вбудоване сортування Python використовує **Timsort** — гібрид сортування злиттям і сортування вставками. Ключові властивості: - Час: O(n log n) в найгіршому випадку, O(n) на вже відсортованих даних - Пам'ять: O(n) додаткової пам'яті - Стабільне: рівні елементи зберігають початковий порядок - Адаптивне: виявляє і використовує наявні відсортовані послідовності в даних Сортування майже відсортованого списку значно швидше, ніж випадкового. **Коли використовувати heapq vs sorted** ``` Потреба Використовуй ──────────────────────────────────── ────────────────── Всі елементи відсортовані один раз sorted() / .sort() Повторне отримання мінімуму heapq Топ-N з великого потоку heapq.nsmallest/nlargest Черга з пріоритетом heapq ```
01

Сортування за абсолютним значенням

#

Напишіть функцію, яка приймає список цілих чисел і повертає їх відсортованими за абсолютним значенням за зростанням.

def sort_by_abs(numbers):
    pass


print(sort_by_abs([-5, 3, -1, 4, -2, 8]))
Рішення
def sort_by_abs(numbers):
    return sorted(numbers, key=abs)


print(sort_by_abs([-5, 3, -1, 4, -2, 8]))
02

Сортування рядків без врахування регістру

#

Напишіть функцію, яка приймає список рядків і повертає їх відсортованими за алфавітом без врахування регістру.

def sort_words(words):
    pass


print(sort_words(["Banana", "apple", "Cherry", "date"]))
Рішення
def sort_words(words):
    return sorted(words, key=str.lower)


print(sort_words(["Banana", "apple", "Cherry", "date"]))
03

Сортування за прізвищем

#

Напишіть функцію, яка приймає список повних імен (рядки вигляду 'Ім'я Прізвище') і повертає їх відсортованими за прізвищем за алфавітом.

def sort_by_last_name(names):
    pass


names = ["Alice Smith", "Bob Johnson", "Carol Adams", "Dave Brown"]
print(sort_by_last_name(names))
Рішення
def sort_by_last_name(names):
    return sorted(names, key=lambda name: name.split()[-1])


names = ["Alice Smith", "Bob Johnson", "Carol Adams", "Dave Brown"]
print(sort_by_last_name(names))
04

Сортування словників за кількома полями

#

Напишіть функцію, яка приймає список словників продуктів (з ключами 'category' і 'price') і повертає їх відсортованими спочатку за категорією (А-Я), а потім за ціною (зростаючою) в межах кожної категорії.

def sort_products(products):
    pass


products = [
    {"name": "Bread", "category": "food", "price": 2},
    {"name": "TV", "category": "electronics", "price": 500},
    {"name": "Milk", "category": "food", "price": 1},
    {"name": "Phone", "category": "electronics", "price": 800},
]
for p in sort_products(products):
    print(p)
Рішення
def sort_products(products):
    return sorted(products, key=lambda p: (p["category"], p["price"]))


products = [
    {"name": "Bread", "category": "food", "price": 2},
    {"name": "TV", "category": "electronics", "price": 500},
    {"name": "Milk", "category": "food", "price": 1},
    {"name": "Phone", "category": "electronics", "price": 800},
]
for p in sort_products(products):
    print(p)
05

Сортування на місці vs повернення нового

#

Напишіть дві функції: одну, яка сортує список чисел на місці (змінюючи оригінал) і повертає None, та іншу, яка повертає відсортовану копію без зміни оригіналу.

def sort_inplace(numbers):
    pass


def sort_copy(numbers):
    pass


nums = [3, 1, 4, 1, 5]
sort_inplace(nums)
print(nums)

nums2 = [3, 1, 4, 1, 5]
result = sort_copy(nums2)
print(nums2)
print(result)
Рішення
def sort_inplace(numbers):
    numbers.sort()


def sort_copy(numbers):
    return sorted(numbers)


nums = [3, 1, 4, 1, 5]
sort_inplace(nums)
print(nums)

nums2 = [3, 1, 4, 1, 5]
result = sort_copy(nums2)
print(nums2)
print(result)
06

Топ N

#

Напишіть функцію, яка приймає список чисел та ціле число n і повертає n найбільших чисел у порядку спадання.

def top_n(numbers, n):
    pass


print(top_n([3, 1, 4, 1, 5, 9, 2, 6, 5, 3], 3))
Рішення
def top_n(numbers, n):
    return sorted(numbers, reverse=True)[:n]

# Або ефективніше для великих списків:
def top_n(numbers, n):
    import heapq
    return heapq.nlargest(n, numbers)


print(top_n([3, 1, 4, 1, 5, 9, 2, 6, 5, 3], 3))
07

Сортування за частотою

#

Напишіть функцію, яка приймає список чисел і повертає їх відсортованими за частотою появи — найчастіші першими. Елементи з однаковою частотою мають залишатися у вихідному відносному порядку.

def sort_by_frequency(numbers):
    pass


print(sort_by_frequency([4, 2, 2, 8, 3, 3, 1, 3]))
Рішення
def sort_by_frequency(numbers):
    from collections import Counter
    freq = Counter(numbers)
    return sorted(numbers, key=lambda n: -freq[n])


print(sort_by_frequency([4, 2, 2, 8, 3, 3, 1, 3]))
08

Стабільність сортування

#

Напишіть функцію, яка приймає список кортежів (ім'я, рахунок) і повертає їх відсортованими за рахунком за спаданням. При рівних рахунках зберегти початковий порядок імен (стабільне сортування).

def rank_players(players):
    pass


players = [("Alice", 90), ("Bob", 85), ("Carol", 90), ("Dave", 85)]
print(rank_players(players))
Рішення
def rank_players(players):
    return sorted(players, key=lambda p: -p[1])


players = [("Alice", 90), ("Bob", 85), ("Carol", 90), ("Dave", 85)]
print(rank_players(players))
09

Сортування з різними напрямками

#

Напишіть функцію, яка приймає список кортежів (ім'я, вік) і повертає їх відсортованими за іменем за зростанням і за віком за спаданням при рівних іменах.

def sort_people(people):
    pass


people = [("Alice", 30), ("Bob", 25), ("Alice", 25), ("Bob", 35)]
print(sort_people(people))
Рішення
def sort_people(people):
    return sorted(people, key=lambda p: (p[0], -p[1]))


people = [("Alice", 30), ("Bob", 25), ("Alice", 25), ("Bob", 35)]
print(sort_people(people))
10

Власне сортування через functools.cmp_to_key

#

Напишіть функцію, яка правильно сортує список рядків версій (наприклад '1.10.2', '1.9.0') як номери версій, а не як звичайні рядки. Використайте functools.cmp_to_key.

from functools import cmp_to_key


def sort_versions(versions):
    pass


print(sort_versions(["1.10.2", "1.9.0", "2.0.0", "1.9.10", "1.1.0"]))
Рішення
from functools import cmp_to_key


def sort_versions(versions):
    def compare(a, b):
        a_parts = list(map(int, a.split(".")))
        b_parts = list(map(int, b.split(".")))
        if a_parts < b_parts: return -1
        if a_parts > b_parts: return 1
        return 0
    return sorted(versions, key=cmp_to_key(compare))


print(sort_versions(["1.10.2", "1.9.0", "2.0.0", "1.9.10", "1.1.0"]))