un

guest
1 / ?
back to lessons

Co Kurs Zakręcił i Co Przegapił

Kurs Hamminga obejmował: przetwarzanie analogowo-cyfrowe, korekcję błędów, symulację, statystykę, analizę numeryczną oraz geometrię n-wymiarową. Uważał, że myślenie komputacyjne jest niezbędne w sygnałach, teorii kodowania, filtracji cyfrowej.

Nigdy systematycznie nie nauczaniał złożoności algorytmicznej.

Krzywe Złożoności Algorytmicznej: O(1) płaska, O(log N) delikatna, O(N) pozioma, O(N²) parabola

Dlaczego Istniała Luka

Mentalny model Hamminga powstał w epoce, kiedy przeszkody sprzętowe dominowały. Pytanie brzmiało: ile transistorów na jednym chipie? Algorytm działał na dostępnym sprzęcie. Gdy N=100, algorytm O(N²) kosztował 10 000 operacji. Gdy N=1 000, kosztował 1 000 000. Gdy N=10 000 (rutyna dzisiaj w zależnościach graficznych, sieciach społecznych i menedżerach zależności): kosztował 100 000 000. Różnica między O(N) a O(N²) była niewidoczna na skalach, na których pracował Hamming, a katastrofalna na skalach, na których żyli jego uczniowie.

Donald Knuth opublikował Sztukę Programowania Komputerowego w 1968 roku - w tym samym roku, w którym Hamming był na pełnym produktywności badawczej. Notacja O stała się standardowym językiem algorytmicznym w latach 70. i 80. XX wieku. Kurs Hamminga pochodzi z 1995 roku, ale jego mentalny model obliczeń powstał wcześniej. nigdy nie zaktualizował tego rozdziału.

Podstawowy według własnego testu Hamminga

Test Hamminga dla podstawy: (1) trwa bardzo długo, (2) reszta pola można wywnioskować za pomocą standardowych metod.

O(1) przekracza oba testy. Analiza szybkości wzrostu trwa od Knutha. Z niego wynika wybór algorytmów, wybór struktur danych oraz prognoza wydajności - większość praktycznych nauk o komputerze wynika z pytania: jak kosztuje się działania, gdy N rośnie? Hamming przegapił najtrwalszą podstawę swojego pola.

O(1) jako Podstawa

Hamming nauczający, że podstawy przetrwają konkretne technologie. Używał jako przykład matematyki: wynaleziona raz, stosowana przez stulecia w fizyce, inżynierii, ekonomii i biologii. Narzędzia poboczne przychodzą i odchodzą; podstawy się mnożą.

Hamming nauczający, że podstawy przetrwają konkretne technologie. Czy złożoność algorytmiczna spełnia kryteria Hamminga? Zastosuj obie jego kryteria: (1) trwałość, (2) możliwość wywnioskowania - czy reszta pola może być wywnioskowana z niej? Argumentuj swoją pozycję konkretne.

Jak Rosną Koszty

Big O mierzy, jak koszty rosną wraz ze wzrostem wejścia. A nie stały czynnik - szybkość wzrostu. Dwa algorytmy, które obie działają 'kilka milisekund' przy N=100, mogą się rozdzielić o czynnik 10 000 przy N=10 000, jeśli jeden działa w czasie O(N) a drugi w O(N²).

Najczęstsze Klasy Złożoności

O(1) - Stała. Ten sam koszt niezależnie od N. Przykłady: wyszukiwanie w tablicy haszowej przez klucz, dostęp do tablicy przez indeks, dodanie/usunięcie z stosu. Podwajanie N nie wpływa na nic.

Krzep O(1): pozioma prosta

O(log N) - Logarytmiczna. Każde działanie redukuje pozostałą pracę o połowę. Przykłady: wyszukiwanie binarne w uporządkowanej tablicy, kwerenda przestrzenna BVH w silniku gry, operacje na drzewie zbalansowanym binarnym. Przy N=1,000,000: log₂(1,000,000) ≈ 20 kroków.

O(log N) krzywa wzrostu: szybki wzrost, a potem poziomowanie się

O(N) - Liniowa. Koszty rosną wraz z wejściem. Przykłady: przeskok liniowy po liście, odczytywanie pliku, liczenie elementów w zbiorze. Przy N=10,000: 10,000 operacji.

O(N) krzywa wzrostu: pozioma prostokątna linia

O(N log N) - Liniarno-logarytmiczna. Niemal liniowa, lekko gorzej. Przykłady: sortowanie przez łączenie, efektywne algorytmy najkrótszych ścieżek (Dijkstra z stosem). Przy N=10,000: ~133,000 operacji.

O(N log N) krzywa wzrostu: nieznacznie stomaższa niż liniowa

O(N²) - Kwadratowa. Koszty wybuchają. Przykłady: wywołanie list.contains() w pętli w tej samej liście, sortowanie bąbelkowe, bezpośrednie porównanie wszystkich par. Przy N=1,000: 1,000,000 operacji. Przy N=10,000: 100,000,000 operacji.

O(N²) krzywa wzrostu: wybuch paraboli

O(2^N) — Eksponencki. Niewykorzystywany w jakiejkolwiek przystępnej skali. Przykłady: siłowy kombinatorzyzm, znajdowanie wszystkich podzbiorów. Przy N=30: ponad 1,000,000,000 operacji.

Krzepła wzrostowa O(2^N): wybuch eksponencki

Skala, na której staje się to widoczne

N=10 porównań:
  O(N):   10
  O(N²):  100
  stosunek:  10x

N=1,000 porównań:
  O(N):   1,000
  O(N²):  1,000,000
  stosunek:  1,000x

N=10,000 porównań:
  O(N):   10,000
  O(N²):  100,000,000
  stosunek:  10,000x

Przy N=10 różnica jest prawie niezauważalna. Przy N=10,000 algorytm O(N²) jest 10,000 razy wolniejszy. Dlatego MOAD-0001 pozostawał niewidoczny przez dziesięciolecia: grafy, na których działał w 1993 roku, miały 50 węzłów. W 2020 roku ten sam kod działał na zależnościach graficznych o 50,000 węzłów.

Klasyfikacja trzech operacji

Zastosuj klasyfikacje złożoności do konkretnej operacji. Kluczowe pytanie dla każdej: ile operacji wymaga, gdy N rośnie?

Dla każdej operacji poniżej podaj złożoność Big O i wyjaśnij w jednej zdaniu, dlaczego: (a) Szyfrowanie hasłem w słowniku za pomocą numeru strony (b) Szukanie każdej strony słownika po słowie (c) Sortowanie rozdania karet, próbując każdą możliwą kolejność Napisz jedną zdaniową wyjaśnienie dla każdego.

Poprawny kod, błędna krzywa wzrostu

Poprawny kod działający w czasie O(N²) produkuje takie same wyniki, jak kod O(N). Testy się udają. Wyjście zgadza się. Brak wyjątków. Wada ukrywa się w krzywej wzrostu.

Ta własność sprawia, że wady mają skala O(N²): one osadzają się w kodzie działającym i stają się widoczne tylko gdy N przekroczy próg. Kod się nie zmieniał. Świat wokół się zmienił.

MOAD-0001: Wada Sedimentary

Najbardziej rozpowszechnione wystąpienie: visited = [] wewnętrznie w pętli przeglądu grafu.

visited = []
for node in graph:
    if node not in visited:  # O(N) skanowanie w każdym wywołaniu
        visited.append(node)
        process(node)

Każde wywołanie not in visited skanuje od 0 do len(visited)-1 elementów. N wywołań × średnio N/2 elementów skanowanych = O(N²) łącznie. Naprawa: jedna linia.

visited = set()  # O(1) test członkostwa
for node in graph:
    if node not in visited:  # O(1) wyszukiwanie w haśle
        visited.add(node)
        process(node)

Taka sama zachowanie. Taka sama wyjście. Te same testy przepływają. Skala wzrostu zmienia się z O(N²) na O(N). Na N=1,000: 1,000× szybciej. Na N=10,000: 10,000× szybciej.

Dlaczego Era Hamminga Zasiał To

Wczesne wersje Java i C miały ArrayList (dynamiczny tablicy) jako domyślny kontener sekwencyjny. Zbiory istniały, ale nie były idiomaticzne - programiści sięgali po to, co było znajome. W 1993 roku przegląd grafu o N=50 trwał w mikrosekundach z visited = []. Ten kod wszedł do systemu kontroli wersji, został skopiowany, zwrócony w bibliotekach, dostarczony przez menedżery pakietów. Do 2020 roku ten sam wzorzec działał na zależnościach grafów z 50,000 węzłów.

Kod był poprawny w 1993 roku. Stał się drogi, gdy świat wokół się zmienił. Era Hamminga zasiał tę klasę wad, nie nazwując pytania o skala wzrostu.

Diagnose & Fix

Zastosuj diagnozę: znajdź wzorzec O(N²), zidentyfikuj naprawę.

Znajdziesz ten kod w bazie danych produkcyjnych: `if item not in visited_list: visited_list.append(item)` wewnętrznie w pętli o 50,000 elementów. Ile operacji wykonywania testu członkostwa średnio przez całą pętlę, a co zastąpiłbyś `visited_list` by go naprawić?

Jakie Zmiany Nazw

Przed tym, gdy Big O miało nazwę, programiści zauważyli: 'to działa wolno'. Zaprofilingowali. Naprawiali. Ale nie mogli nauczyc tego wzorca - mogli tylko wskazać konkretne przypadki. Wzorzec bez nazwy nie może się rozprzestrzeniać.

Po tym, gdy Big O miało nazwę, doświadczony inżynier mógł powiedzieć: 'to jest O(N²), zastąp to kolekcją' a młodszy inżynier mógł zrozumieć, zastosować i nauczyć tego wzorca na przestrzeni czasu. Nazwa uczyniła wzorzec jednostką wiedzy, która może być przekazywana.

Hamming znał tę dynamikę w innych dziedzinach. Opisał, jak nazwanie 'makaronowego kodu' w latach 60. XX wieku pozwoliło społeczności rozpoznawać, omawiać i ostatecznie wyeliminować klasę błędów, które wszyscy doświadczyli, ale nikt nie nazwał. Ten sam mechanizm obowiązuje w przypadku klas złożoności.

Unhamming dodaje wiedzę leksykalną, którą Hamming pomija: O(1), O(log N), O(N), O(N log N), O(N²), O(2^N). Te nazwy pozwalają zobaczyć krzywą wzrostu w kodzie, przewidzieć wydajność na dużą skalę i dokładnie komunikować poprawki. Spełniają własne kryterium Hamminga dla podstawowych: trwałe i generujące większość reszty pola.

Z Teorii Liczb do Nauki o Komputerach

Oznaczenie Big O nie pochodzi z nauki o komputerach. Pochodzi z matematyki czystej - konkretnie z teorii liczb - 74 lata przed tym, gdy Donald Knuth przeniósł je do algorytmów.

Paul Bachmann, 1894

Paul Bachmann, niemiecki matematyk, wprowadził oznaczenie O w swojej książce z 1894 roku Die Analytische Zahlentheorie (Teoria Liczb Analityczna). Potrzebował krótkiego sposobu, aby wyrazić, że jedna wielkość nie rośnie szybciej niż druga, z wyjątkiem stałego czynnika. Napisanie 'f(n) = O(g(n))' powiedzie się: f rośnie nie więcej niż jak g. To pozwoliło matematykom zajmującym się liczbami rozważać błędy w przybliżeniach bez śledzenia dokładnych stałych.

Bachmann nie myślał o pętlach, strukturach danych czy szybkości wykonania. Myślał o rozkładzie liczb pierwszych - konkretnie o błędach w Teorii Liczb Pierwszych.

Edmund Landau, 1909

Edmund Landau, kolejny niemiecki matematyk, upowszechnił oznaczenie w swojej książce z 1909 roku Handbuch der Lehre von der Verteilung der Primzahlen (Podręcznik Teorii Rozprzestrzeniania Liczb Pierwszych). Landau wprowadził również pokrewne oznaczenia o (małe-o) i użył całości oznaczeń asypmptycznych tak szeroko, że stały się znane jako Bachmann-Landau notation (oznaczenie Bachmann-Landau) lub po prostu Landau notation (oznaczenie Landau).

Od sześciu dziesięcioleci O oznaczenie żyło wyłącznie w matematyce. żaden programista nie myślał o nim.

Donald Knuth, 1968 & 1976

Donald Knuth przeniósł oznaczenie do nauki o obliczeniach. W Sztuce Programowania Komputerów (t. 1, 1968) Knuth użył oznaczenia O, aby opisać czas wykonywania algorytmów - bezpośredni przeszczep narzędzia Bachmanna do nowego dziedziny. Był on pierwszą osobą, która zastosowała je systematycznie do analizy algorytmów.

W 1976 roku Knuth opublikował krótki artykuł w SIGACT News o tytule 'Big Omicron and Big Omega and Big Theta'. Wprowadził omegę (Omega) dla dolnych granic i Theta dla ścisłych granic, dopełniając dzisiejszy trzy-znakowy wokabularz nauki o obliczeniach. Ponadto argumentował za standaryzowaniem użycia tych znaków w polu - czyn, który przyspieszył ich adaptację.

Wczesne lata 80. to epoka, gdy Big O pojawił się w każdej książce o algorytmach. W latach 90. stało się to standardowym słownictwem na wywiadach.

74-letnia podróż

1894 — Bachmann wprowadza O w teorii liczb
1909 — Landau popularizuje O, o i całą rodzinę oznaczeń
1953 — Hamming zaczyna badania w Bell Labs (nie nauczył się nigdy Big O jako narzędzia CS)
1968 — Knuth stosuje O do analizy algorytmów w TAOCP t. 1
1976 — Knuth dodaje Omega i Theta; argumentuje za standaryzacja
Lata 80. — Big O pojawia się we wszystkich programach studiów
1993 — warstwa osadnicza MOAD-0001 tworzy się w kodzie produkcyjnym
1995 — Hamming uczy ostatnią wersję swojego kursu; nigdy nie omawia Big O
2026 — MOAD-0001 znaleziony w 1000+ projektach open-source

Narzędzie Bachmanna spędziło 74 lata w czystej matematyce, widoczne dla każdego matematyka, ale niewidoczne dla każdego programisty. Knuth zauważył przeszczep. Hamming - działając w tym samym okresie, współpracując z tą samą społecznością komputerową - nigdy nie włączył tego do swojego kursu.

Dlaczego standardyzacja Knutha była ważna

Artykuł Knutha z 1976 roku zrobił coś ponadto wprowadzenie Omegi i Theta. Argumentował, że polu potrzebował stałego oznaczenia, a niezgodne oznaczenia były szkodliwe. Różne podręczniki używały O, aby oznaczać różne rzeczy - czasem jako górna granica, czasem jako przybliżenie, czasem bez określania, czy konstancje mają znaczenie. Knuth uporządkował to.

To jest dynamika nazewnictwa Hamminga zastosowana do samej notacji. Przed standardizacją symboli przez Knutha, inżynierowie nie mogli dokładnie komunikować roszczeń złożoności między podręcznikami, artykułami lub zespołami. Po standardizacji, 'to działa w O(N log N)' miało dokładnie jedno znaczenie niezależnie od tego, kto to powiedział.

Knuth również przyczynił się do nieformalnej tłumaczenia: 'O(f(n)) oznacza, że czas wykonania jest nie więcej niż stały czynnik f(n), dla wszystkich wystarczająco dużych n'. Ta interpretacja - górna granica, z uwzględnieniem czynnika stałego, dla dużych wejść - stała się standardową intuicją, którą każdy programista uczy się.

Bachmann wynalazł oznaczenie O dla teorii liczb w 1894 roku. Knuth przyjął je do analizy algorytmów w 1968 roku. Co musiało się zmienić - w komputerowaniu, w skali, czy w sposobie pracy programistów - dla żeby narzędzie czystego matematyka stało się niezbędnym słownictwem dla inżynierów oprogramowania? Podaj przynajmniej dwa czynniki.