- ✓
Pierwiastek zamiast pełnego zakresu: test pierwszości i rozkład na czynniki chodzą do pierwiastka z liczby, bo dzielniki chodzą w parach. Ta jedna zmiana skraca czas z minut do ułamka sekundy.
- ✓
Sito, gdy liczb jest wiele: jedna liczba do sprawdzenia — test do pierwiastka. Wszystkie liczby pierwsze w zakresie — sito Eratostenesa. Wewnętrzna pętla sita startuje od kwadratu, nie od podwojenia.
- ✓
Euklides i Horner to schematy do przepisania z głowy: cztery linijki każdy, a wracają niemal w każdej sesji — NWD w zadaniach z teorii liczb, Horner przy konwersji systemów i wartości wielomianu.
- ✓
Sortowanie własne piszesz tylko na wyraźne żądanie treści. W części praktycznej
sorted()jest szybsze i pewniejsze niż ręczny quicksort; bąbelkowe i przez wstawianie znasz na potrzeby części teoretycznej i pytań o złożoność. - ✓
Złożoność szacujesz z zagnieżdżenia pętli. Jedna pętla po danych to koszt liniowy, pętla w pętli — kwadratowy, połowienie zakresu — logarytmiczny. To wystarcza do odpowiedzi na wszystkie pytania z arkusza.
Lista algorytmów maturalnych jest krótka i od lat ta sama. Problem polega na tym, że sama lista nic nie daje: uczeń wie, że „trzeba znać sito", ale nie wie, czym się różni od testu pierwszości, dlaczego wewnętrzna pętla zaczyna się akurat od kwadratu i co odpowiedzieć, gdy arkusz pyta o złożoność.
Ten wpis idzie po każdym algorytmie osobno i przy każdym odpowiada na cztery pytania: kiedy pojawia się na maturze, jak wygląda kod, jaka jest jego złożoność i skąd się bierze, oraz na czym uczniowie się na nim wykładają. Na końcu jest sekcja o rozpoznawaniu, którego algorytmu oczekuje treść zadania — bo to zwykle trudniejsze niż samo napisanie kodu.
Złożoność obliczeniowa: jak ją czytać i jak ją oszacować#
Złożoność to odpowiedź na pytanie „ile razy więcej pracy wykona program, gdy danych będzie dziesięć razy więcej". Nie mierzy się jej w sekundach, tylko w liczbie operacji zależnej od rozmiaru danych, oznaczanego zwykle jako n.
| Złożoność | Nazwa | Przykład z matury | n = 1 000 000 to około |
|---|---|---|---|
| O(1) | stała | odczyt t[5], sprawdzenie parzystości | 1 operacja |
| O(log n) | logarytmiczna | wyszukiwanie binarne, algorytm Euklidesa | 20 operacji |
| O(√n) | pierwiastkowa | test pierwszości do pierwiastka | 1 000 operacji |
| O(n) | liniowa | suma elementów, minimum, Fibonacci iteracyjnie | 1 000 000 operacji |
| O(n log n) | liniowo-logarytmiczna | sorted(), sortowanie szybkie, sito | 20 000 000 operacji |
| O(n²) | kwadratowa | sortowanie bąbelkowe, porównywanie par | 1 000 000 000 000 operacji |
| O(2ⁿ) | wykładnicza | Fibonacci naiwną rekurencją | liczba nie do zapisania |
Jak oszacować złożoność, patrząc na pętle
Nie trzeba niczego liczyć na kartce. Wystarczy przejrzeć kod i zadać sobie trzy pytania.
Pytanie 1: ile pętli jest zagnieżdżonych jedna w drugiej? Każdy poziom zagnieżdżenia, w którym pętla przechodzi przez wszystkie dane, mnoży koszt przez n. Jedna pętla to n, pętla w pętli to n², trzy poziomy to n³.
Trzy fragmenty, trzy różne złożoności
# O(n) - jedna petla po danych
for x in dane:
suma += x
# O(n) - dwie petle OBOK siebie, koszt sie dodaje: n + n = 2n
for x in dane:
suma += x
for x in dane:
if x > maks:
maks = x
# O(n^2) - petla W petli, koszt sie mnozy
for i in range(len(dane)):
for j in range(len(dane)):
if dane[i] == dane[j]:
pary += 1Pytanie 2: czy licznik pętli rośnie o jeden, czy zmienia się razy dwa? Jeżeli zakres w każdym kroku dzieli się na pół albo zmienna mnoży się przez dwa — złożoność jest logarytmiczna. Milion podzielony na pół dwadzieścia razy daje jedynkę, i stąd bierze się „dwadzieścia kroków dla miliona elementów".
Pętla, która połowi zakres
ile = 0
n = 1000000
while n > 1:
n = n // 2
ile += 1
print(ile) # 19Pytanie 3: czy pętla wewnętrzna zależy od zewnętrznej? Jeśli druga pętla
startuje od i + 1 albo kończy się na i, liczba obiegów to nie n², tylko
mniej więcej połowa tego — ale nadal jest to złożoność kwadratowa. Stałych
i podziału przez dwa się nie zapisuje: interesuje nas tempo wzrostu, a nie
dokładna liczba.
- ✓
Stałe znikają: koszt 3n zapisujemy jako O(n), a 100 operacji niezależnych od danych to O(1). Dwukrotnie szybszy program ma tę samą złożoność.
- ✓
Zostaje tylko najsilniejszy człon: n² plus n plus 5 to O(n²), bo dla dużych danych reszta przestaje mieć znaczenie.
- ✓
Operacje na kolekcjach też kosztują:
x in listato przejście całej listy, czyli O(n), ax in zbiordla zbioru (set) to praktycznie O(1). Ten sam zapis, zupełnie inny koszt — to najczęściej przeoczana pętla w kodzie ucznia.
Wniosek: zanim uruchomisz program na właściwym pliku, pomnóż w głowie liczbę wierszy przez liczbę zagnieżdżonych przejść. Wynik rzędu milionów jest w porządku, rzędu miliardów — nie. Systematycznie temat rozwija dział algorytmiki i logiki.
Sprawdzanie pierwszości: dlaczego pierwiastek naprawdę wystarczy#
Test pierwszości pojawia się albo jako samodzielny podpunkt („ile liczb pierwszych jest w pliku"), albo jako cegiełka w większym zadaniu — przy liczbach będących sumą cyfr, przy palindromach pierwszych, przy bliźniakach. Wraca niemal w każdej sesji.
Wersja naiwna, której należy unikać
def pierwsza_naiwnie(n):
if n < 2:
return False
for d in range(2, n):
if n % d == 0:
return False
return TrueDowód intuicyjny: dzielniki chodzą w parach
Weź liczbę 36. Jej dzielniki to 1, 2, 3, 4, 6, 9, 12, 18, 36. Zapisz je w pary, których iloczyn daje 36:
| Dzielnik mniejszy | Dzielnik większy | Iloczyn |
|---|---|---|
| 1 | 36 | 36 |
| 2 | 18 | 36 |
| 3 | 12 | 36 |
| 4 | 9 | 36 |
| 6 | 6 | 36 — para się skleja, bo 6 to pierwiastek z 36 |
Rozumowanie w jednym zdaniu: jeżeli n rozkłada się na iloczyn a razy b, to niemożliwe jest, żeby oba czynniki były większe od pierwiastka z n — ich iloczyn byłby wtedy większy niż n. Zatem co najmniej jeden z nich jest mniejszy lub równy pierwiastkowi. Jeśli więc nie znalazłeś żadnego dzielnika do pierwiastka włącznie, żadnego nie ma w ogóle.
Jeśli oraz i , to — sprzeczność. Stąd .Wersja poprawna
def pierwsza(n):
if n < 2:
return False
d = 2
while d * d <= n:
if n % d == 0:
return False
d += 1
return True- ✓
Zgubiona równość przy pierwiastku: zapis
range(2, int(n ** 0.5))bez dodania jedynki pomija sam pierwiastek. Skutek: dziewiątka, dwudziestka piątka i każdy kwadrat liczby pierwszej wychodzą jako liczby pierwsze. - ✓
Brak obsługi liczb mniejszych od dwóch: zero, jedynka i liczby ujemne nie są pierwsze, a pętla dla nich w ogóle się nie wykona i funkcja zwróci prawdę. Jedynka jako liczba pierwsza to klasyczny sposób na wynik różniący się od klucza o jeden.
- ✓
Złożoność: O(√n) na jedną liczbę, bo pętla dochodzi do pierwiastka, wykonując po jednej operacji dzielenia z resztą na obieg.
Sito Eratostenesa i pytanie o kwadrat#
Sito odpowiada na inne pytanie niż test pierwszości: nie „czy ta liczba jest pierwsza", tylko „które liczby w całym zakresie są pierwsze". Typowa treść maturalna brzmi „wypisz wszystkie liczby pierwsze mniejsze od miliona" albo „sprawdź, ile liczb z pliku jest pierwszych", gdzie plik ma dziesiątki tysięcy wierszy.
Idea: robisz listę wszystkich liczb, a potem wykreślasz wielokrotności. To, co zostanie niewykreślone, jest pierwsze.
Sito Eratostenesa
def sito(n):
czy_pierwsza = [True] * (n + 1)
czy_pierwsza[0] = czy_pierwsza[1] = False
i = 2
while i * i <= n:
if czy_pierwsza[i]:
for j in range(i * i, n + 1, i):
czy_pierwsza[j] = False
i += 1
return [x for x in range(n + 1) if czy_pierwsza[x]]
print(sito(30))
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]Dlaczego wewnętrzna pętla startuje od i razy i
To jedno z ulubionych pytań w części teoretycznej, bo sprawdza zrozumienie, a nie pamięć. Odpowiedź jest prosta: wszystko poniżej kwadratu zostało już wykreślone wcześniej.
Weź i równe 5. Wielokrotności piątki to 5, 10, 15, 20, 25, 30, ... Ale:
- 10 to 5 razy 2 — wykreśliła je już pętla dla dwójki,
- 15 to 5 razy 3 — wykreśliła je pętla dla trójki,
- 20 to 5 razy 4, a czwórka jest wielokrotnością dwójki — też już wykreślone,
- 25 to 5 razy 5 — pierwsza wielokrotność piątki, której mniejszy czynnik nie jest mniejszy od piątki.
Ogólnie: każda wielokrotność i postaci i razy k, gdzie k jest mniejsze od i, ma mniejszy czynnik k i została skreślona, gdy pętla zewnętrzna była przy k (albo przy dzielniku pierwszym liczby k). Pierwsza wielokrotność wymagająca uwagi to i razy i.
| i | Start od 2i (naiwnie) | Start od i² | Zaoszczędzone kroki |
|---|---|---|---|
| 2 | 4, 6, 8, 10, ... | 4, 6, 8, 10, ... | żadne — to ten sam start |
| 3 | 6, 9, 12, 15, ... | 9, 12, 15, ... | szóstka była już skreślona przez 2 |
| 5 | 10, 15, 20, 25, ... | 25, 30, 35, ... | 10, 15, 20 skreślone wcześniej |
| 7 | 14, 21, 28, ..., 49 | 49, 56, 63, ... | wszystko poniżej 49 |
- ✓
Rozmiar tablicy:
[True] * (n + 1), a nie[True] * n. Chcesz indeksować liczbą n włącznie, więc miejsc musi być o jedno więcej. - ✓
Zero i jedynka: trzeba je ustawić na fałsz ręcznie, bo żadna pętla ich nie dotknie.
- ✓
Warunek
if czy_pierwsza[i]: bez niego program wykreśla wielokrotności liczb złożonych, robiąc pracę już wykonaną. Działa, ale wolniej. - ✓
Złożoność: około O(n log log n), czyli w praktyce niemal liniowa. Sprawdzanie każdej liczby osobno testem do pierwiastka kosztowałoby O(n√n) — dla miliona to różnica między ułamkiem sekundy a minutami.
- ✓
Kiedy sito, kiedy test: jedna albo kilka liczb — test do pierwiastka. Wiele liczb z jednego zakresu — sito raz, a potem odczyt z tablicy w czasie stałym.
Rozkład na czynniki pierwsze#
Zadanie brzmi zwykle: „dla każdej liczby z pliku podaj jej największy czynnik pierwszy", „znajdź liczby mające dokładnie trzy różne dzielniki pierwsze" albo „wypisz liczby będące iloczynem dwóch liczb pierwszych". Wszystkie sprowadzają się do jednej procedury.
Rozkład na czynniki pierwsze
def czynniki(n):
wynik = []
d = 2
while d * d <= n:
while n % d == 0:
wynik.append(d)
n = n // d
d += 1
if n > 1:
wynik.append(n)
return wynik
print(czynniki(60)) # [2, 2, 3, 5]
print(czynniki(97)) # [97]
print(czynniki(1024)) # osiem dwojekDwa miejsca w tym kodzie wyglądają na drobiazgi, a decydują o poprawności.
Warunek if n > 1 na końcu. Pętla chodzi tylko do pierwiastka z bieżącej
wartości. Jeśli po wszystkich podziałach zostaje coś większego od jedynki, jest
to liczba pierwsza większa od pierwiastka — i to ostatni czynnik. Bez tej linijki
rozkład liczby 14 dałby samą dwójkę, a siódemka przepadłaby. Dla liczby pierwszej
funkcja zwróciłaby pustą listę.
Dzielenie podwójnym ukośnikiem. Zapis n = n / d zamienia liczbę na typ
zmiennoprzecinkowy. Po kilku obiegach reszta z dzielenia przestaje wychodzić
dokładnie zerowa i rozkład się rozjeżdża — po cichu, bez komunikatu o błędzie.
Złożoność: O(√n) w najgorszym przypadku, czyli gdy n jest pierwsze i pętla
przechodzi cały zakres. W typowym przypadku znacznie szybciej, bo każdy
znaleziony czynnik zmniejsza liczbę, a razem z nią pierwiastek będący granicą
pętli. Dlatego warunek d * d <= n musi porównywać się z bieżącą wartością,
a nie z zapamiętaną kopią pierwotnej liczby.
Algorytm Euklidesa: odejmowanie, modulo i NWW#
NWD pojawia się przy skracaniu ułamków, przy zadaniach o wspólnej mierze, o synchronizacji cykli, a także jako narzędzie w podpunktach, które wprost o niego nie proszą. Sam algorytm ma dwie wersje i różnica między nimi jest częstym pytaniem egzaminacyjnym.
Podstawa obu: NWD dwóch liczb nie zmienia się, gdy większą zastąpimy różnicą lub resztą z dzielenia. Skoro wspólny dzielnik dzieli a i b, to dzieli również a minus b.
Wersja z odejmowaniem
def nwd_odejmowanie(a, b):
while a != b:
if a > b:
a = a - b
else:
b = b - a
return a
print(nwd_odejmowanie(48, 18)) # 6Wersja z resztą z dzielenia
def nwd(a, b):
while b != 0:
a, b = b, a % b
return a
print(nwd(48, 18)) # 6
print(nwd(1000000, 3)) # 1 - dwa obiegi petli| Cecha | Wersja z odejmowaniem | Wersja z resztą |
|---|---|---|
| Warunek pętli | dopóki a różne od b | dopóki b różne od zera |
| Wynik | wspólna wartość a i b | wartość a po wyzerowaniu b |
| Liczba kroków dla (1000000, 3) | ponad trzysta tysięcy odejmowań | dwa obiegi |
| Złożoność | O(a + b) w najgorszym razie | O(log min(a, b)) |
| Zachowanie dla zera | pętla nieskończona, gdy jedna liczba to 0 | poprawnie zwraca drugą liczbę |
NWW przez NWD
Najmniejszej wspólnej wielokrotności nie liczy się osobnym algorytmem. Wynika z zależności: iloczyn dwóch liczb równa się iloczynowi ich NWD i NWW.
NWW oraz NWD dla wielu liczb
def nww(a, b):
return a // nwd(a, b) * b
print(nww(4, 6)) # 12
# dla calej listy - skladanie parami
def nwd_listy(liczby):
wynik = liczby[0]
for x in liczby[1:]:
wynik = nwd(wynik, x)
return wynik
print(nwd_listy([24, 36, 60])) # 12Pułapka: dla pary zawierającej zero wzór na NWW dzieli przez zero. Jeśli w danych mogą być zera, warunek trzeba dopisać jawnie.
Schemat Hornera: konwersja systemów i wartość wielomianu#
Horner to jeden pomysł w dwóch zastosowaniach, które na pierwszy rzut oka nie mają ze sobą nic wspólnego. Warto je zobaczyć razem, bo wtedy schemat zostaje w głowie na dobre.
Pomysł: zamiast liczyć potęgi, wyciągasz podstawę przed nawias tyle razy, ile się da. Liczba 1101 w systemie dwójkowym to zwykle zapisywane jako suma potęg:
Po wyciągnięciu dwójki przed nawias:
Potęgowanie zniknęło. Zostały same mnożenia i dodawania, po jednym na cyfrę.
Konwersja z dowolnego systemu na dziesiętny
def horner(zapis, podstawa):
wynik = 0
for cyfra in zapis:
wynik = wynik * podstawa + int(cyfra)
return wynik
print(horner("1101", 2)) # 13
print(horner("777", 8)) # 511
print(horner("2024", 10)) # 2024
# system szesnastkowy
CYFRY = "0123456789ABCDEF"
def horner16(zapis):
wynik = 0
for znak in zapis.upper():
wynik = wynik * 16 + CYFRY.index(znak)
return wynik
print(horner16("FF")) # 255Wartość wielomianu w punkcie
def wartosc(wspolczynniki, x):
wynik = 0
for w in wspolczynniki:
wynik = wynik * x + w
return wynik
# 2x^3 - 6x^2 + 2x - 1 dla x = 3
print(wartosc([2, -6, 2, -1], 3)) # 5- ✓
Gdzie się pojawia: w części teoretycznej jako pseudokod do uzupełnienia lub prześledzenia krok po kroku, w części praktycznej przy nietypowych podstawach — systemie trójkowym, siódemkowym, a bywa że z podstawą ujemną.
- ✓
Złożoność O(n) względem liczby cyfr: jedno mnożenie i jedno dodawanie na cyfrę. Wersja z potęgowaniem liczona naiwnie ma O(n²), bo podstawa do potęgi k to k mnożeń.
- ✓
Kierunek przeglądania: cyfry idą od najbardziej znaczącej, czyli od lewej. Odwrócenie kolejności daje wynik zupełnie inny i nadal wyglądający sensownie — to najczęstszy błąd przy tym schemacie.
- ✓
Kiedy wolno użyć wbudowanej funkcji: w części praktycznej
int("1101", 2)jest dozwolone i szybsze. Własna implementacja jest potrzebna, gdy treść wprost każe zapisać algorytm albo gdy podstawa wykracza poza to, co obsługuje funkcja wbudowana.
Wyszukiwanie binarne: dwa błędy, które je psują#
Wyszukiwanie binarne działa wyłącznie na danych posortowanych. Za to wtedy jest nie do pobicia: dla miliona elementów odpowiada po około dwudziestu porównaniach, bo każdy krok odrzuca połowę pozostałego zakresu.
Na maturze pojawia się w dwóch rolach: jako samodzielne zadanie z pseudokodem w części teoretycznej i jako narzędzie w części praktycznej, gdy trzeba wielokrotnie sprawdzać obecność wartości w dużym zbiorze danych.
Wyszukiwanie binarne — wersja iteracyjna
def szukaj(t, x):
lewy = 0
prawy = len(t) - 1
while lewy <= prawy:
srodek = (lewy + prawy) // 2
if t[srodek] == x:
return srodek
elif t[srodek] < x:
lewy = srodek + 1
else:
prawy = srodek - 1
return -1
dane = [1, 4, 7, 9, 12, 15, 20]
print(szukaj(dane, 12)) # 4
print(szukaj(dane, 13)) # -1Błąd pierwszy: warunek pętli bez równości
Zapis while lewy < prawy wygląda niewinnie, a gubi przypadek, w którym zakres
zwęził się do jednego elementu — czyli dokładnie ten, w którym często siedzi
szukana wartość. Program zwraca „nie znaleziono" dla elementów, które w tablicy
są. Najłatwiej to wyłapać na danych brzegowych: tablica jednoelementowa oraz
szukanie pierwszego i ostatniego elementu.
| Zapis | Skutek | Ocena |
|---|---|---|
| while lewy <= prawy | sprawdza także zakres jednoelementowy | poprawny |
| while lewy < prawy | gubi ostatni element zakresu | częsty błąd |
| lewy = srodek + 1 | odrzuca środek, zakres maleje | poprawny |
| lewy = srodek | środek zostaje w zakresie | pętla nieskończona |
| srodek = (lewy + prawy) // 2 | indeks całkowity | poprawny |
| srodek = (lewy + prawy) / 2 | indeks typu float | TypeError przy indeksowaniu |
Błąd drugi: liczenie środka pojedynczym ukośnikiem
(lewy + prawy) / 2 daje liczbę zmiennoprzecinkową, a listy nie da się
indeksować liczbą z kropką — program kończy się wyjątkiem. To akurat błąd
głośny, więc łatwy. Gorsza jest jego wersja z round(): zaokrąglenie w górę
przy pewnych układach granic sprawia, że zakres przestaje się kurczyć i pętla
nigdy się nie kończy. Środek liczy się podwójnym ukośnikiem — zawsze.
Wniosek: jeśli szukasz w danych wielokrotnie, masz dwie drogi — posortować
raz i szukać binarnie, albo wrzucić dane do zbioru set i sprawdzać operatorem
in. Druga jest krótsza i na maturze zwykle wystarcza; pierwszą musisz umieć
napisać, bo teoria o nią pyta. Wersje do przećwiczenia znajdziesz w
quizach programistycznych.
Sortowanie: bąbelkowe, przez wstawianie, szybkie — i kiedy wystarczy sorted()#
Zacznijmy od wniosku, bo jest zaskakująco jednoznaczny: w części praktycznej
sortujesz wbudowaną funkcją, chyba że treść zadania wyraźnie każe zapisać
własny algorytm. sorted() używa algorytmu o złożoności O(n log n),
jest przetestowany lepiej niż cokolwiek, co napiszesz pod presją czasu,
i obsługuje sortowanie po kluczu jedną linijką.
Sortowanie wbudowane — trzy warianty, które załatwiają większość zadań
liczby = [5, 2, 9, 1]
print(sorted(liczby)) # [1, 2, 5, 9]
print(sorted(liczby, reverse=True)) # [9, 5, 2, 1]
osoby = [("Kowalski", 3, 45), ("Nowak", 1, 45), ("Abacki", 2, 12)]
# po liczbie punktow malejaco
print(sorted(osoby, key=lambda o: o[2], reverse=True))
# po punktach malejaco, a przy remisie po nazwisku rosnaco
print(sorted(osoby, key=lambda o: (-o[2], o[0])))Po co więc uczyć się sortowań ręcznie? Bo część teoretyczna pyta o ich własności: ile porównań wykona algorytm, jak zachowa się na danych już posortowanych, który jest stabilny. Na te pytania nie odpowie się, znając tylko nazwę funkcji.
Sortowanie bąbelkowe
def babelkowe(t):
n = len(t)
for i in range(n - 1):
zamiana = False
for j in range(n - 1 - i):
if t[j] > t[j + 1]:
t[j], t[j + 1] = t[j + 1], t[j]
zamiana = True
if not zamiana:
break
return tZakres wewnętrznej pętli, range(n - 1 - i), bierze się stąd, że po i
przebiegach i największych elementów siedzi już na swoich miejscach na końcu
i nie ma sensu ich ruszać. Pominięcie odjęcia jedynki daje wyjście poza listę.
Sortowanie przez wstawianie
def przez_wstawianie(t):
for i in range(1, len(t)):
klucz = t[i]
j = i - 1
while j >= 0 and t[j] > klucz:
t[j + 1] = t[j]
j -= 1
t[j + 1] = klucz
return tSortowanie szybkie — wersja czytelna
def szybkie(t):
if len(t) <= 1:
return t
os = t[len(t) // 2]
mniejsze = [x for x in t if x < os]
rowne = [x for x in t if x == os]
wieksze = [x for x in t if x > os]
return szybkie(mniejsze) + rowne + szybkie(wieksze)
print(szybkie([5, 2, 9, 1, 5, 6])) # [1, 2, 5, 5, 6, 9]| Algorytm | Złożoność średnia | Przypadek najgorszy | Kiedy o niego pytają |
|---|---|---|---|
| bąbelkowe | O(n²) | O(n²) — dane odwrotnie posortowane | jako przykład algorytmu kwadratowego i przy liczeniu zamian |
| przez wstawianie | O(n²) | O(n²) | gdy pytanie dotyczy danych prawie posortowanych — wtedy O(n) |
| przez wybieranie | O(n²) | O(n²) | gdy pytanie dotyczy liczby porównań, zawsze tej samej |
| szybkie | O(n log n) | O(n²) — zły wybór elementu rozdzielającego | jako przykład metody dziel i zwyciężaj |
| przez scalanie | O(n log n) | O(n log n) | gdy liczy się gwarancja, a nie średnia |
| sorted() w Pythonie | O(n log n) | O(n log n) | w części praktycznej — domyślny wybór |
Wniosek: naucz się rozpoznawać trzy sortowania z kodu lub pseudokodu i umieć
powiedzieć, jaka jest ich złożoność oraz jak zachowają się na danych już
uporządkowanych. Do samego sortowania w zadaniu praktycznym używaj sorted().
Fibonacci: rekurencja wykładnicza kontra iteracja liniowa#
Ciąg Fibonacciego sam w sobie jest prosty — każdy wyraz to suma dwóch poprzednich. Na maturze służy do czegoś innego: jest standardowym przykładem pokazującym, że poprawny algorytm może być bezużyteczny.
Wersja rekurencyjna — poprawna i nieużywalna
def fib_rek(n):
if n <= 1:
return n
return fib_rek(n - 1) + fib_rek(n - 2)Policzmy, ile wywołań funkcji potrzeba, żeby dostać jedną liczbę. Wywołanie dla 5 wymaga wywołań dla 4 i 3; wywołanie dla 4 znowu wymaga dla 3 i 2 — a wynik dla trójki nie jest nigdzie zapamiętany, więc liczy się drugi raz od zera.
| n | Wynik | Liczba wywołań fib_rek | Liczba obiegów pętli w wersji iteracyjnej |
|---|---|---|---|
| 10 | 55 | 177 | 9 |
| 20 | 6 765 | 21 891 | 19 |
| 30 | 832 040 | 2 692 537 | 29 |
| 40 | 102 334 155 | 331 160 281 | 39 |
| 50 | 12 586 269 025 | ponad 40 miliardów | 49 |
Wersja iteracyjna — ta, której używasz
def fib(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
print(fib(50)) # 12586269025 - natychmiastWersja rekurencyjna ze spamiętywaniem
pamiec = {}
def fib_pam(n):
if n <= 1:
return n
if n in pamiec:
return pamiec[n]
pamiec[n] = fib_pam(n - 1) + fib_pam(n - 2)
return pamiec[n]Wniosek dla całej rekurencji, nie tylko dla Fibonacciego: rekurencja jest bezpieczna, gdy każde wywołanie generuje jedno wywołanie zależne (jak w silni czy w algorytmie Euklidesa). Gdy generuje dwa lub więcej, a wyniki nie są zapamiętywane, koszt rośnie wykładniczo. Rozpoznasz to po tym, że po prawej stronie znaku równości nazwa funkcji pojawia się więcej niż raz.
Algorytmy na napisach: palindrom, anagram, zliczanie wystąpień#
Zadania tekstowe wracają w każdej sesji, zwykle jako podpunkt większego zadania z plikiem: „ile słów w pliku to palindromy", „które pary wyrazów są anagramami", „jaka litera występuje najczęściej".
Palindrom
Palindrom — trzy podejścia
# 1. najkrocej - porownanie z odwroconym napisem
def palindrom(s):
return s == s[::-1]
# 2. dwa wskazniki od konca i od poczatku
def palindrom2(s):
i = 0
j = len(s) - 1
while i < j:
if s[i] != s[j]:
return False
i += 1
j -= 1
return True
# 3. wersja odporna na wielkosc liter i spacje
def palindrom3(s):
czysty = "".join(z.lower() for z in s if z.isalnum())
return czysty == czysty[::-1]
print(palindrom("kajak")) # True
print(palindrom3("Kobyla ma maly bok")) # TrueZapis s[::-1] to wycinek z krokiem minus jeden, czyli napis od końca. Warto go
umieć, bo skraca też odwracanie liczby czy listy. Pułapka: liczby nie da się
odwrócić wycinkiem — najpierw str(n), a przy porównaniu z powrotem int().
Anagram
Anagram — dwa sposoby
# 1. przez sortowanie liter - O(n log n)
def anagram(a, b):
return sorted(a.lower()) == sorted(b.lower())
# 2. przez zliczanie liter - O(n)
def anagram2(a, b):
if len(a) != len(b):
return False
licznik = {}
for z in a.lower():
licznik[z] = licznik.get(z, 0) + 1
for z in b.lower():
if z not in licznik:
return False
licznik[z] -= 1
if licznik[z] == 0:
del licznik[z]
return len(licznik) == 0
print(anagram("Barok", "Kobra")) # TruePułapka: porównywanie bez ujednolicenia wielkości liter. „Barok" i „kobra"
to anagramy, ale wielka litera na początku sprawia, że kod bez lower() odpowie
przecząco. To samo dotyczy polskich znaków — jeśli treść je dopuszcza, sprawdź,
czy plik czytasz z właściwym kodowaniem.
Zliczanie wystąpień
Zliczanie znaków, słów i elementów
tekst = "matura z informatyki"
licznik = {}
for znak in tekst:
if znak != " ":
licznik[znak] = licznik.get(znak, 0) + 1
# najczestszy znak
najczestszy = max(licznik, key=licznik.get)
print(najczestszy, licznik[najczestszy])
# posortowane malejaco po liczbie wystapien,
# a przy remisie alfabetycznie
for znak, ile in sorted(licznik.items(), key=lambda p: (-p[1], p[0])):
print(znak, ile)Zliczanie w słowniku ma złożoność liniową i jest właściwą odpowiedzią wszędzie tam, gdzie odruch podpowiada pętlę w pętli z licznikiem. Ten drugi zapis daje złożoność kwadratową i na dużym pliku przestaje się liczyć w rozsądnym czasie. Więcej o strukturach danych w Pythonie znajdziesz w dziale teorii programowania.
Jak rozpoznać, którego algorytmu chce od Ciebie zadanie#
To umiejętność, którą trenuje się osobno — i której brak jest częstszą przyczyną straty punktów niż nieznajomość kodu. Treści maturalne rzadko mówią wprost „użyj algorytmu Euklidesa". Mówią za to rzeczy, które jednoznacznie na niego wskazują.
| Co mówi treść zadania | Czego oczekuje | Sygnał rozpoznawczy |
|---|---|---|
| skróć ułamek, wspólna miara, podziel na równe części bez reszty | NWD — algorytm Euklidesa | dwie liczby i słowo wspólny |
| po ilu dniach oba zdarzenia zbiegną się ponownie | NWW policzone przez NWD | dwa cykle o różnych długościach |
| ile liczb pierwszych w przedziale, wypisz liczby pierwsze do N | sito Eratostenesa | zakres, a nie pojedyncza liczba |
| czy liczba z pliku jest pierwsza | test dzielników do pierwiastka | pojedyncze liczby, każda inna |
| iloczyn dwóch liczb pierwszych, największy dzielnik pierwszy | rozkład na czynniki | słowo czynnik lub dzielnik pierwszy |
| zapisz liczbę w systemie, oblicz wartość wyrażenia w systemie | schemat Hornera | podstawa inna niż dziesiątkowa |
| sprawdź, czy wartość występuje w posortowanym ciągu | wyszukiwanie binarne | słowo posortowany w treści |
| uporządkuj malejąco, a przy remisie alfabetycznie | sorted() z kluczem krotkowym | dwa kryteria porządku |
| ile razy występuje, najczęstszy, ile jest różnych wartości | słownik lub zbiór, jedno przejście | zliczanie w dużym zbiorze danych |
| każdy wyraz zależy od dwóch poprzednich | iteracja z dwiema zmiennymi | definicja rekurencyjna w treści |
Trzy dodatkowe wskazówki, które pomagają wybrać dobrze:
- Sprawdź rozmiar danych, zanim wybierzesz metodę. Plik z dwudziestoma liczbami wybaczy wszystko. Plik z setkami tysięcy wierszy wyklucza rozwiązania kwadratowe, a to często jedyna podpowiedź, że zadanie oczekuje sita, słownika albo wyszukiwania binarnego.
- Zwróć uwagę, czy pytanie dotyczy jednej wartości, czy zakresu. To rozstrzyga między testem pierwszości a sitem, między pojedynczym przejściem a przygotowaniem struktury pomocniczej.
- Sprawdź, czy podpunkty się na sobie opierają. Zadania maturalne mają zwykle cztery lub pięć podpunktów liczonych na tych samych danych. Jeśli pierwszy każe wczytać dane, a trzeci pyta o liczby pierwsze — sito policzone raz na początku obsłuży wszystkie kolejne podpunkty.
Plan powtórki: dwa tygodnie po pół godziny dziennie#
Algorytmów maturalnych jest tyle, że da się je przerobić w dwa tygodnie, jeśli robi się to systematycznie i pisząc kod z pamięci, a nie czytając gotowy. Sprawdzianem nie jest „rozumiem", tylko „potrafię odtworzyć na pustej kartce".
Tydzień pierwszy — schematy z pamięci. Każdego dnia jeden algorytm: piszesz go bez zaglądania, uruchamiasz na trzech zestawach danych (typowym, brzegowym i pustym), a potem porównujesz ze wzorcem.
- Test pierwszości do pierwiastka wraz z obsługą liczb mniejszych od dwóch.
- Sito Eratostenesa — ze świadomym uzasadnieniem startu od kwadratu.
- Rozkład na czynniki pierwsze wraz z warunkiem końcowym.
- Algorytm Euklidesa w obu wersjach oraz NWW.
- Schemat Hornera w dwóch zastosowaniach: konwersja i wielomian.
- Wyszukiwanie binarne — z testem na tablicy jedno- i dwuelementowej.
- Fibonacci iteracyjnie oraz trzy funkcje na napisach.
Tydzień drugi — zadania, nie algorytmy. Bierzesz arkusze z poprzednich sesji i przed napisaniem choćby jednej linijki zapisujesz na brudno: który algorytm, jaka złożoność, ile jest danych. Dopiero potem kodujesz. To ćwiczenie na rozpoznawanie, a nie na pisanie — i to ono najbardziej się opłaca.
- ✓
Przygotuj plik szablonowy. Wczytanie danych z pliku ze
strip(), funkcja sprawdzająca pierwszość, NWD, sito i szkielet zapisu wyników. Przepisanie tego na egzaminie zajmuje kilka minut, które lepiej przeznaczyć na właściwy algorytm. - ✓
Testuj na danych brzegowych. Liczba 1, liczba 2, kwadrat liczby pierwszej (na przykład 49), lista jednoelementowa, lista pusta, wartości powtórzone. Większość błędów opisanych w tym wpisie wychodzi właśnie na nich.
- ✓
Ćwicz na prawdziwych arkuszach, nie na wymyślonych zadaniach. Zestawy z poprzednich sesji znajdziesz w dziale arkuszy maturalnych — po trzech przerobionych sam zauważysz, że schematy się powtarzają.
- ✓
Jeśli kod piszesz, ale wciąż nie wiesz, który algorytm wybrać, problem leży w czytaniu treści, nie w programowaniu — i to da się ustawić w kilka spotkań. Napisz przez formularz kontaktowy i podaj, przy którym typie zadania najczęściej się zacinasz.
Korepetycje z informatyki
Zdajesz maturę rozszerzoną z informatyki?
Prowadzimy indywidualne przygotowanie do matury z informatyki — algorytmy, Python i C++, bazy danych i arkusz kalkulacyjny. Zajęcia online, na arkuszach CKE. Zobacz program i cennik.
Zobacz korepetycje z informatykiUczysz się w technikum informatycznym? Prowadzimy też przygotowanie do kwalifikacji INF.02, INF.03 i INF.04. Zobacz egzaminy zawodowe →
Podoba Ci się ten artykuł? 💻
To tylko wycinek wiedzy! Na naszych korepetycjach omawiamy te tematy jeszcze dokładniej. Zapisz się na lekcję próbną i zdaj egzamin na 100%.
🚀 Zadzwoń i umów darmową konsultacjęAutor: Alan Ostrowski
