Wprowadzenie do analizy i projektowania algorytmów - Zbigniew J. Czech

Kup ebooka

79.00 zł
63.20 zł (79,00 zł najniższa cena z 30 dni)

-
Proszę czekać

Rozdział 2 Poprawność algorytmu

Jedną z najważniejszych własności algorytmu jest jego poprawność (nazywana również semantyczną poprawnością). W tym rozdziale odpowiemy na pytania: co oznacza, że algorytm jest poprawny i w jaki sposób można to udowodnić.

2.1 Etapy rozwiązywania problemu

Opracowanie algorytmu oraz jego implementacji, które rozwiązują zadany problem, przebiega zazwyczaj w następujących etapach:

(a)

sformułowanie algorytmu,

(b)

udowodnienie, że algorytm jest poprawny,

(c)

wyznaczenie jego złożoności obliczeniowej,

(d)

implementacja w wybranym języku programowania, dokonanie jego kompilacji, zainicjowanie wykonania i otrzymanie wyników.

Dalsze rozważania w tym rozdziale dotyczą etapu (b).

Nieformalnie, algorytm jest poprawny, jeśli dla wszystkich poprawnych danych wejściowych kończy działanie i zwraca dane wyjściowe będące rozwiązaniem zadanego problemu. W opisie problemu zazwyczaj precyzuje się, jakie są dane wejściowe, na podstawie których należy obliczyć dane wyjściowe stanowiące rozwiązanie. Podane są także warunki, które muszą spełniać zarówno dane wejściowe, jak i wyjściowe. Spełnienie warunków dotyczących danych wyjściowych gwarantuje, że są one poprawnymi rozwiązaniami problemu. Warunki te zapisuje się formalnie za pomocą asercji wejściowej oraz asercji wyjściowej, które tworzą razem specyfikację problemu. Asercja jest funkcją przyjmującą wartość logiczną (boolowską) prawda (ang. true) albo fałsz (ang. false), której argumentami są zmienne używane w algorytmie (lub programie). Zwykle wyraża się ją jako zdanie logiczne zawierające zmienne algorytmu. Każda asercja odnosi się do określonego punktu algorytmu. Gdy obliczenia osiągają ten punkt, asercja powinna być spełniona, czyli jej wartość powinna wynosić true. Innymi słowy, w miarę jak algorytm zmienia stan pamięci, wartości zmiennych w asercji powinny zapewnić jej spełnienie w odpowiednim momencie. Algorytm rozwiązujący problem jest poprawny, jeśli dla każdego poprawnego egzemplarza danych wejściowych zakończy swoje działanie i zwróci wynik spełniający warunki specyfikacji, to jest, przekształci dane wejściowe spełniające asercję wejściową w dane wyjściowe spełniające asercję wyjściową.

2.2 Częściowa i całkowita poprawność algorytmu

Algorytm jest całkowicie poprawny, jeśli spełnia:

-

warunek częściowej poprawności, oraz

-

warunek stopu, czyli kończy swoje działanie.

Warunek częściowej poprawności algorytmu A zapisujemy jako:

{p} ?A?{q},

gdzie p to asercja wejściowa (warunek początkowy), a q to asercja wyjściowa (warunek końcowy). Zgodnie z tym zapisem, algorytm A jest częściowo poprawny, jeśli dla każdego przypadku, gdy dane wejściowe spełniają {p} , algorytm - jeśli się zakończy - zwraca dane wyjściowe spełniające {q} .

Załóżmy, że algorytm A rozwiązuje następujący problem: dany jest zbiór n liczb całkowitych, gdzie n?1 . Należy znaleźć największy element tego zbioru. Zbiór jest reprezentowany przez tablicęD:?????[1..n] ????????? . Wówczas warunek częściowej poprawności algorytmu ma postać:

{n?1 } ?A?{M=max?{D?[i]:i?[1,n]}}.

To oznacza, że algorytm A jest częściowo poprawny, jeśli dla dowolnego n?1 , po zakończeniu działania algorytmu, zmienna M będzie zawierać największą wartość w tablicy D.

Aby wykazać całkowitą poprawność algorytmu, należy także udowodnić, że spełnia on warunek stopu. Jeśli algorytm nie kończy działania, nie można ocenić, czy asercja wyjściowa jest spełniona - ponieważ wynik nigdy nie zostaje zwrócony.

Rozważmy instrukcję while przedstawioną na rys. 2.1.

1 b:=-77; 2 while b?50 do 3 print(b) ; 4 b:=b+2; 5 end while;
Rysunek 2.1: Instrukcja while o nieskończonym czasie działania

Wykonanie tej instrukcji nigdy się nie kończy, ponieważ warunek w wierszu 22.1 jest źle sformułowany - zmienna b przyjmuje tylko wartości nieparzyste. W ogólnym przypadku, wykazanie, że obliczenia algorytmu doprowadzą do punktu końcowego, może nie być łatwe. Rozważmy algorytm, którego celem jest przekształcenie dowolnej dodatniej liczby całkowitej n do wartości 1 (rys. 2.2).

Przeanalizujmy kilka przykładów:

n=1 , stop n=2 , 1, stop n=3 , 10, 5, 16, 8, 4, 2, 1, stop n=4 , 2, 1, stop n=5 , itd. od wartosci 5 w wierszu n=3n=6 , 3, itd. od wiersza n=3n=7 , 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, itd. od wartosci 5 w wierszu n=3...

Zauważmy, że dla wartości początkowych n z zakresu [1, 7]obliczenia algorytmu się kończą, ale nie jest jasne, czy sprawdzając kolejne wartości n, natrafimy na wartość, dla której algorytm nie kończy swego działania. Według obecnego stanu wiedzy, brakuje dowodu, że algorytm ten spełnia warunek stopu1.

1 read(n) ; 2 while n>1 do 3 if n jest parzyste then 4 n:=n?div?2 {zmniejsz liczbę n}5 else 6 n:=3?n +1 {zwiększ n do liczby parzystej} 7 end if; 8 end while;
Rysunek 2.2: Algorytm o nieznanym warunku stopu

Rozdział 1 Wstęp

W tym rozdziale zdefiniujemy kilka podstawowych pojęć związanych z analizą i projektowaniem algorytmów. Aby można było badać działanie algorytmu, konieczne jest zdefiniowanie modelu komputera, w którym ten będzie wykonywany. W tym celu omówimy model obliczeń sekwencyjnych o swobodnym dostępie do pamięci.

1 {Sito Eratostenesa - szukanie liczb pierwszych} 2 {Dane wejsciowe: tablica A:?????[2..n] of (pierwsza, złożona)} 3 {Dane wyjsciowe: jesli A?[i]=???????? , to i jest liczbą pierwszą,  w przeciwnym razie i jest liczbą złożoną} 4 for i:=2 to n do A?[i]:=???????? ; end for; 5 for i:=2 to n do 6 if A?[i]=???????? then 7 for j:=i?i to n step i do A?[j]:=złożona ; end for; 8 end if; 9 end for;
Rysunek 1.1: Przykładowy algorytm zapisany w pseudokodzie

Nieformalnie, algorytm jest skończonym, uporządkowanym zbiorem ściśle określonych operacji, które prowadzą do rozwiązania zadanego problemu obliczeniowego. Algorytm przekształca dane wejściowe zdefiniowane w opisie problemu w dane wyjściowe stanowiące jego rozwiązanie. Algorytmy można przedstawiać za pomocą języka naturalnego lub różnych notacji formalnych. W niniejszej książce będziemy korzystać z pseudokodu (rys. 1.1), który umożliwia opis algorytmu na różnych poziomach abstrakcji, czyli z pominięciem większej lub mniejszej liczby szczegółów. Operacje algorytmu powinny być zdefiniowane na tyle precyzyjnie, aby nie budziły wątpliwości w czasie ich implementacji. Algorytm zazwyczaj cechuje ogólność. Na przykład, zamiast projektować algorytm rozwiązujący konkretne równanie 2?x2 -13?x+5 =0 , możemy opracować algorytm dla ogólnej klasy równań kwadratowych a?x2 +b?x +c =0 , przy założeniu, że współczynniki a, b i c będą danymi wejściowymi. Graficznym sposobem przedstawienia algorytmu jest schemat działania, zwany również schematem blokowym lub schematem przepływu sterowania (ang. flowchart) (rys. 1.2).

Oblicz przystosowanie

Rysunek 1.2: Przykładowy schemat działania algorytmu genetycznego należącego do grupy algorytmów ewolucyjnych; kluczowe operacje algorytmu to krzyżowanie (ang. crossover) oraz mutacja (ang. mutation)

Program to implementacja algorytmu w wybranym języku programowania, która umożliwia jego wykonanie przez komputer. Algorytm i odpowiadający mu program powinny być semantycznie równoważne, czyli opisywać te same obliczenia.

Proces sekwencyjny to ciąg operacji wykonywanych w czasie, w którym każda operacja oi+1 może się rozpocząć dopiero po zakończeniu operacji oi. Proces jest wykonywany w komputerze przez procesor1, który stanowi jeden z elementów architektury komputera. Na rys. 1.3 pokazano przykład ilustrujący proces sekwencyjny. Każda operacja oi procesu wynika z wykonania określonej instrukcji z listy instrukcji procesora wykonującego ten proces (por. tab. 1.1).

Rysunek 1.3: Ilustracja procesu sekwencyjnego ? (t oznacza oś czasu)

Program sekwencyjny definiuje klasę procesów sekwencyjnych, które powstają w wyniku jego wykonania przy uwzględnieniu różnych zestawów danych wejściowych.

Tabela 1.1: Przykładowa lista instrukcji procesora. Argument a może mieć postać =i, i lub ?i, gdzie i oznacza liczbę całkowitą, w?(a) oznacza wartość argumentu a zdefiniowaną następująco:w( =i) =i, w?(i)=Mi,w(?i)=MMi, gdzie '?' oznacza adresowanie pośrednie, w którym argument jest pobierany z komórki pamięci o adresie zawartym w komórce Mi. Argument b może mieć postać i lub ?i, o znaczeniach jak powyżej, k jest indeksem rejestru arytmetycznego. Argument c może mieć postać =l lub l, gdzie w?(c) jest zdefiniowane jako: w( =l) =l oraz w?(l)=Rl
     Operacja      Argument      Znaczenie      LOAD      k,a      Rk := w?(a)      STORE      k,b      Mw?(b) := Rk      ADD      k,c      Rk := Rk+w?(c)      SUB      k,c      Rk := Rk-w?(c)      MULT      k,c      Rk := Rk×w ?(c)      DIV      k,c      Rk := ?Rk/w ?(c)?      JUMP      i      L := i      JPOS      k,i      if Rk>0 then L := i else L := L+1      JZERO      k,i      if Rk=0 then L := i else L := L+1      JNEG      k,i      if Rk<0 then L := i else L := L+1      READ      k      Wczytaj daną z urządzenia zewnętrznego      do rejestru Rk      WRITE      k      Wypisz daną zawartą w rejestrze Rk      na urządzeniu zewnętrznym      HALT      Zakończ obliczenia

Model obliczeń sekwencyjnych

Zanim przedstawimy model obliczeń sekwencyjnych, pokrótce omówimy, jak obecnie działają programy tego typu. Współczesne komputery wyposażone są w procesory, które wykonując program sekwencyjny, mogą realizować niektóre operacje programu równolegle. Przykładowo, rozważmy następujące instrukcje:

x:=a+b; (1.1) y:=c?d.

W wykonaniu sekwencyjnym tych instrukcji najpierw przebiega operacja dodawania, a po jej zakończeniu, po przypisaniu wyniku do zmiennej x, wykonywana jest operacja mnożenia i przypisanie wyniku do zmiennej y. Instrukcje te mogą być wykonane równolegle, ponieważ współczesne procesory potrafią przetwarzać więcej niż jedną instrukcję w jednym cyklu pracy (ang. instruction-level parallelism). W tym celu procesory wyposaża się w odpowiednią liczbę jednostek funkcjonalnych, które umożliwiają potokowe wykonanie instrukcji (ang. pipelining).

Załóżmy, że proces wykonywania instrukcji składa się z sześciu kolejno wykonywanych operacji: pobrania instrukcji z pamięci, jej dekodowania, wyznaczenia adresów argumentów, pobrania argumentów, wykonania operacji określonej przez kod instrukcji oraz zapisania otrzymanego wyniku do pamięci. Ciąg tych operacji tworzy potok. Poszczególne operacje w potoku nazywamy etapami, zatem rozważamy potok 6-etapowy. Dla uproszczenia przyjmujemy, że każdy etap potoku wykonany jest w jednym cyklu pracy procesora2. Załóżmy, że procesor wyposażony jest w jednostki funkcjonalne J1, J2, ..., J6, z których każda odpowiada za wykonanie jednego etapu potoku. Wykonanie potokowe kolejnych instrukcji pokazano na rys. 1.4. W pierwszym cyklu pracy jednostka J1 pobiera z pamięci instrukcję i1 i przekazuje ją jednostce J2 do dekodowania.

Rysunek 1.4: Potokowe wykonywanie strumienia instrukcji: i1, i2, i3, ..., itd.

W drugim cyklu jednostka J2 dekoduje instrukcję i1, a J1 pobiera instrukcję i2. Pod koniec cyklu jednostki J1 i J2 przekazują wyniki swego działania odpowiednio do jednostek J2 oraz J3, i tak dalej. Zauważmy, że przy ciągłym strumieniu instrukcji kolejne z nich opuszczają potok w każdym cyklu pracy procesora. Na przykład, na rys. 1.4 instrukcja i1 jest wykonana w cyklu 6, instrukcja i2 w cyklu 7, i tak dalej. Wykonując instrukcje (1.1) sekwencyjnie, potrzebowalibyśmy 2?6 =12  cykli.

Dalszy wzrost szybkości obliczeń umożliwiają procesory superskalarne, które pozwalają na jednoczesne wykonywanie wielu instrukcji w jednym cyklu pracy. W takich procesorach zwielokrotnia się jednostki funkcjonalne, na przykład arytmetyczno-logiczne dla liczb zmiennopozycyjnych i całkowitych, a także inne wyspecjalizowane jednostki. Ważnym elementem procesora superskalarnego jest specjalna jednostka pobierania instrukcji, zdolna do pobierania z pamięci wielu instrukcji jednocześnie, pracując być może w sposób potokowy. Jednostka ta przekazuje instrukcje do jednostki dekodującej, która określa, czy instrukcje mogą być wykonane jednocześnie (czyli są niezależne), czy też istnieją między nimi zależności uniemożliwiające ich równoległe wykonanie.

Jednym z głównych problemów projektowania architektur komputerów jest różnica w szybkości działania procesorów i pamięci operacyjnej. Współczesne procesory są znacznie szybsze niż pamięć główna. Rozwiązaniem tego problemu jest zastosowanie hierarchicznego systemu pamięci, składającego się z pamięci głównej oraz dwóch pamięci podręcznych L1 i L2 (ang. cache memory)3. Pamięci podręczne mają znacznie mniejszą objętość od pamięci głównej, ale są wielokrotnie szybsze. Niższy poziom (L1) jest szybszy, ale mniejszy od poziomu wyższego (L2), który jest nieco wolniejszy, ale ma większą pojemność. Szybsze przetwarzanie danych przy użyciu pamięci podręcznych jest możliwe dzięki zjawisku lokalności odwołania. Jeśli w programie wystąpiło odwołanie do pewnej komórki pamięci, to istnieje duże prawdopodobieństwo, że w niedalekiej przyszłości nastąpi kolejne odwołanie do tej samej lub sąsiadującej komórki. Wówczas do pamięci podręcznej przenoszony jest cały blok komórek zawierający adres wykorzystywany aktualnie przez program. Pamięci podręczne przechowują instrukcje oraz dane programu, które mogą być z nich pobierane szybciej niż z pamięci głównej, a równocześnie z wszystkich modułów podręcznych, co dodatkowo przyspiesza obliczenia.

W tej książce będziemy określać złożoność czasową algorytmów, rozumianą jako liczbę operacji elementarnych wykonywanych przez algorytm, aby rozwiązać dany problem. Jak pokazaliśmy wcześniej, wiele operacji może być wykonanych równolegle dzięki potokom i zwielokrotnionym jednostkom funkcjonalnym. Dzięki pamięci podręcznej procesor może jednocześnie pobierać dane z różnych poziomów pamięci. Czy da się to uwzględnić w wyznaczaniu liczby operacji elementarnych? W praktyce jest to bardzo trudne, a na ogół - niemożliwe. Komplikację stanowi również to, że nie wiemy, na jakim komputerze algorytm będzie wykonywany. Architektury komputerów, a w szczególności procesory, są dziś bardzo zróżnicowane. Producenci sprzętu konkurują, tworząc coraz lepsze rozwiązania o wyższej mocy obliczeniowej. Podsumowując, aby możliwe było precyzyjne określanie złożoności czasowej algorytmów, konieczne jest przyjęcie prostego, ale uniwersalnego modelu obliczeń sekwencyjnych.

Model to obiekt teoretyczny lub fizyczny, którego analiza pozwala lepiej zrozumieć dane zjawisko lub proces. Model upraszcza rzeczywistość, uwzględniając jedynie jej najważniejsze cechy. Dzięki temu jest łatwiejszy do analizy niż oryginalny system. Nas interesują modele komputerów, które umożliwiają analizę procesów obliczeniowych. Są one znane jako modele obliczeń i stosuje się je zarówno do analizy, jak i projektowania algorytmów, oraz określania ich złożoności.

Najpowszechniej akceptowanym modelem obliczeń sekwencyjnych jest maszyna o swobodnym dostępie do pamięci (ang. random access machine, RAM). Model RAM4 składa się z procesora oraz pamięci złożonej z potencjalnie nieograniczonej liczby komórek pamięci oznaczanych jako Mi, i=1 , 2, 3, ...(rys. 1.5). Każda komórka pamięci, identyfikowana przez swój adres i, może przechowywać dowolną, skończoną liczbę całkowitą w zapisie binarnym, nawet bardzo dużą. Zakłada się, że czas odczytu lub zapisu wartości z lub do komórki Mi jest niezależny od jej adresu i stały, co implikuje dostęp swobodny, w odróżnieniu od dostępu cyklicznego, charakterystycznego dla pamięci dyskowych. Procesor realizuje instrukcje z określonego, skończonego zbioru, przypominające te spotykane we współczesnych komputerach (tab. 1.1).

W celu realizacji instrukcji, procesor zawiera zbiór rejestrów arytmetycznych R1, R2, ..., Rq, gdzie q>1 , które mogą przechowywać - podobnie jak komórki pamięci Mi - dowolne, skończone liczby w zapisie binarnym. Dodatkowo, procesor jest wyposażony w rejestr sterujący L, zwany licznikiem instrukcji. Po wykonaniu każdej instrukcji, z wyjątkiem JUMP, JPOS, JZERO oraz JNEG, jego wartość zwiększa się o 1. Dla tych instrukcji licznik przyjmuje nową wartość lub zwiększa się o 1, zgodnie z warunkiem instrukcji.

Rysunek 1.5: Model obliczeń sekwencyjnych RAM

Program RAM to ciąg instrukcji sterujących pracą procesora. Zapisywany jest w jednostce sterującej modelu, a nie w pamięci, co oznacza, że nie może być modyfikowany w czasie wykonywania. Podczas analizowania programu RAM wygodnie jest przyjąć, że składa się on z kroków obliczeniowych, z których każdy podzielony jest na trzy fazy. Dla analizy programu RAM przyjmujemy, że jest on złożony z kroków obliczeniowych, z których każdy składa się z trzech faz: (1) pobrania argumentów operacji z pamięci do rejestrów, (2) wykonania operacji (arytmetycznej, logicznej, sterującej lub wej/wyj), (3) zapisania wyniku operacji do określonej komórki pamięci. Niektóre fazy kroku obliczeniowego mogą być pominięte, jeśli są niepotrzebne w danej instrukcji.

Przykład 1.1.

Problem obliczenia sum prefiksowych polega na wyznaczeniu, dla zadanego ciągu elementów?a1 , a2, ..., an?oraz operatora "+", następujących wartości zwanych prefiksami:

s1=a1 , s2=a1+a2, s3=a1+a2+a3, ... sn=a1+a2+a3+...+an.

Przyjmijmy, że elementy ai są liczbami. Na rysunku a przedstawiono program RAM, który przekształca ciąg?a1 , a2, ..., an?w ciąg sum prefiksowych?s1 , s2, ..., sn? . W komórce M1 zapisany jest rozmiar ciągu wejściowego n, n>0 , a same elementy ai przechowywane są w komórkach M5, M6, ..., Mn+4. Komórki M2, M3 i M4 pełnią funkcję pomocniczą.

(a)

1  LOAD 1,=5 {krok 1} 14  ADD 1,2 2  STORE 1,2 15  STORE 1,? 3 3  LOAD 1,=6 {krok 2} 16  LOAD 1,2 {krok 7} 4  STORE 1,3 17  ADD 1,=1 5  LOAD 1,1 {krok 3} 18  STORE 1,2 6  ADD 1,=4 19  LOAD 1,3 {krok 8} 7  STORE 1,4 20  ADD 1,=1 8  LOAD 1,4 {krok 4} 21  STORE 1,3 9  LOAD 2,3 22  LOAD 1,4 {krok 9} 10  SUB 1,2 23  LOAD 2,3 11  JNEG 1,26 {krok 5} 24  SUB 1,2 12  LOAD 1,? 2 {krok 6} 25  JUMP 11 {krok 10} 13  LOAD 2,? 3 26  HALT {krok 11}

(b)

M_{1}M1 M_{2}M2 M_{3}M3 M_{4}M4 M_{5}M5 M_{6}M6 ... M_{n+4}Mn+4 n pom. pom. pom. a1 a2 ... an
Rysunek 1.6: (a) Program RAM obliczania sum prefiksowych; (b) przyporządkowanie zmiennych do komórek pamięci, gdzie pom. oznacza komórkę pomocniczą
1 M2 := 5 {ustal początkowe adresy} 2 M3 := 6 {elementow ai-1 oraz ai} 3 M4 := M1+4 {ostatni adres elementu ai} 4 R1 := M4-M35 if R1<0 then go to 11 6 MM3 := MM2+MM3 {ai=ai-1+ai} 7 M2 := M2+1 {zwiększ adresy elementow} 8 M3 := M3+1 {ai-1 oraz ai} 9 R1 := M4-M310 go to 5 11 stop {koniec obliczen}
Rysunek 1.7: Program obliczania sum prefiksowych zapisany na nieco wyższym poziomie abstrakcji w porównaniu do programu RAM z rys. 1.6a. Każdy krok programu składa się z trzech faz: pobrania argumentów, wykonania operacji oraz zapisania wyniku, przy czym niektóre fazy mogą być pominięte

Programy RAM, takie jak na rys. 1.6a, są precyzyjne i bliskie językowi maszynowemu, ale mało przyjazne dla użytkownika. Aby poprawić czytelność, można je przedstawić na wyższym poziomie abstrakcji z wykorzystaniem kroków obliczeniowych (rys. 1.7). Przy analizie złożoności programu zakładamy, że każdy krok obliczeniowy trwa tyle samo - umowną jednostkę czasu. Zwróćmy uwagę, że sumy prefiksowe są istotne w sortowaniu przez zliczanie (rys. 8.1) oraz w zastosowaniach praktycznych, takich jak ekonomia czy statystyka (rys. 3.15). -

Podsumowując, model RAM jest modelem uniwersalnym, ponieważ umożliwia opis i analizę obliczeń sekwencyjnych dla dowolnego współczesnego komputera z procesorem jednowątkowym.

Spis treści

Rozdział 1. Wstęp Rozdział 2. Poprawność algorytmu 2.1 Etapy rozwiązywania problemu 2.2 Częściowa i całkowita poprawność algorytmu 2.3 Dowodzenie poprawności algorytmu 2.4 Zadania Rozdział 3. Złożoność obliczeniowa 3.1 Pojęcie złożoności obliczeniowej 3.2 Funkcje złożoności 3.3 Złożoność pesymistyczna i średnia, funkcje wrażliwości 3.4 Wybór operacji dominującej 3.5 Obliczanie maksimum 3.6 Obliczanie średnich wartości sum prefiksowych 3.7 Zadania Rozdział 4. Notacje asymptotyczne 4.1 Notacje O i ? 4.2 Notacje asymptotycznej równoważności ? i ? 4.3 Zadania Rozdział 5. Metoda "dziel i zwyciężaj" 5.1 Wyznaczanie maksimum oraz minimum 5.2 Równoważenie rozmiarów podproblemów 5.2.1 Sortowanie przez scalanie 5.2.2 Sortowanie szybkie 5.3 Metoda rekurencji uniwersalnej 5.4 Zadania Rozdział 6. Programowanie dynamiczne 6.1 Obliczanie wartości współczynnika dwumianowego 6.2 Rozwiązywanie problemów optymalizacyjnych 6.2.1 Mnożenie ciągu macierzy 6.2.2 Obliczanie najdłuższego wspólnego podciągu 6.3 Algorytmy pseudowielomianowe 6.4 Zadania Rozdział 7. Kopce i ich zastosowania 7.1 Definicja i własności kopca 7.2 Implementacja kopca 7.3 Operacje na kopcu 7.4 Kolejki priorytetowe 7.5 Sortowanie przez kopcowanie 7.6 Ograniczenie dolne złożoności algorytmów sortowania 7.7 Zadania Rozdział 8. Sortowanie w czasie liniowym 8.1 Sortowanie przez zliczanie 8.2 Sortowanie kubełkowe 8.3 Sortowanie pozycyjne 8.4 Zadania Rozdział 9. Wyszukiwanie 9.1 Problem wyszukiwania 9.2 Wyszukiwanie liniowe 9.3 Wyszukiwanie binarne 9.4 Drzewa wyszukiwań binarnych 9.5 Haszowanie 9.6 Zadania Rozdział 10. Algorytmy grafowe 10.1 Metody reprezentowania grafów w komputerze 10.2 Przeszukiwanie grafów 10.3 Cykle podstawowe w grafie 10.4 Minimalne drzewo rozpinające 10.5 Cykl Eulera 10.6 Cykl Hamiltona 10.7 Problem komiwojażera 10.8 Zadania Rozdział 11. Wprowadzenie do teorii złożoności obliczeniowej 11.1 Klasy problemów obliczeniowych 11.2 Problemy NP-zupełne 11.3 Problemy optymalizacyjne i decyzyjne 11.4 Redukowalność w czasie wielomianowym 11.5 Dowodzenie NP-zupełności wybranych problemów Bibliografia O autorze Przypisy

Punkty orientacyjne

Początek treści Okładka Strona tytułowa Kolofon Spis treści

Przedmowa

Analiza i projektowanie algorytmów to obszerny, intensywnie rozwijany dział informatyki. Podstawą każdego programu komputerowego jest algorytm, dlatego wraz z rosnącym stopniem zaawansowania tworzonego oprogramowania zagadnienia dotyczące struktur i własności algorytmów oraz metod ich projektowania nabierają kluczowego znaczenia.

Niniejsza książka stanowi wprowadzenie do współczesnych metod analizy algorytmów i jest przeznaczona głównie dla studentów informatyki na uczelniach o profilu inżynieryjno-technicznym. Może być ona także przydatna dla szerszego grona Czytelników zainteresowanych problemami algorytmicznymi oraz ich praktyczną implementacją.

Algorytmy omawiane w książce zostały przedstawione w postaci pseudokodu. Ich działanie staraliśmy się opisać w sposób przystępny i zrozumiały, również dla osób rozpoczynających naukę. Zakładamy, że Czytelnik ma podstawowe doświadczenie programistyczne oraz zna najważniejsze struktury danych, takie jak tablice, rekordy czy listy. W publikacji korzystamy z aparatu matematycznego niezbędnego do przeprowadzenia formalnej analizy algorytmów, na przykład ich poprawności, efektywności i innych własności. Dlatego wskazane jest, aby Czytelnik miał podstawową wiedzę z dziedziny matematyki dyskretnej. W przypadku studentów informatyki dobrym rozwiązaniem jest równoległe zapoznanie się z treścią książki i uczestnictwo w wykładach z zakresu podstaw programowania komputerów oraz analizy matematycznej.

Książka powstała na podstawie notatek do wykładu "Algorytmy i struktury danych" prowadzonego przez jej autora dla studentów kierunku informatyki Politechniki Śląskiej w Gliwicach. Opracowanie składa się z 11 rozdziałów. W rozdziale 1. zostały zdefiniowane podstawowe pojęcia związane z analizą algorytmów. Omówiony został także model obliczeń sekwencyjnych o swobodnym dostępie do pamięci, który umożliwia opis oraz analizę działania algorytmu na dowolnym współczesnym komputerze. Rozdział 2. jest poświęcony jednej z głównych własności algorytmu, jaką jest poprawność semantyczna. Omawiamy, co to znaczy, że algorytm jest poprawny i jak można tego dowieść. Rozdział 3. zawiera definicje pojęć złożoności czasowej i pamięciowej, a także złożoności pesymistycznej i średniej oraz funkcji wrażliwości. Ponadto przedstawiamy typowe postacie funkcji złożoności obliczeniowej, a także przykłady wyznaczania złożoności wybranych algorytmów. Notacje asymptotyczne są pomocne przy upraszczaniu funkcji złożoności przez pomijanie mniej znaczących składowych, czyli inaczej - poprzez określanie rzędu wielkości funkcji. W rozdziale 4. zostały zdefiniowane notacje umożliwiające asymptotyczne oszacowanie górne oraz dolne zadanej funkcji, a także notacje asymptotycznej równoważności. W rozdziałach 5. i 6. omawiamy dwie podstawowe metody projektowania algorytmów, to jest metodę "dziel i zwyciężaj" oraz programowanie dynamiczne. Przedstawiamy także pseudowielomianowe algorytmy oparte na programowaniu dynamicznym, które są stosowane do rozwiązywania problemu plecakowego oraz problemów podziału i sumy podzbioru, należących do klasy problemów NP-zupełnych. Strukturą danych szczególnie przydatną w projektowaniu algorytmów jest kopiec. Własności, sposoby implementacji oraz przykłady zastosowania kopców prezentujemy w rozdziale 7. Algorytmom sortowania w czasie liniowym jest poświęcony rozdział 8., natomiast w rozdziale 9. rozważamy problem wyszukiwania, przede wszystkim omawiamy algorytmy wyszukiwania liniowego i binarnego, wyszukiwania w drzewach binarnych, a także w tablicach z użyciem funkcji haszujących. Rozdział 10. jest poświęcony wybranym algorytmom grafowym. Na początku przedstawiamy metody reprezentowania grafów w komputerze, przede wszystkim z wykorzystaniem macierzy i list sąsiedztwa wierzchołków. Następnie prezentujemy dwie metody przeszukiwania grafów - w głąb i wszerz - oraz wyjaśniamy, czym są cykle podstawowe w grafie, pokazując również algorytm ich znajdowania. Formułujemy problem wyznaczania minimalnego drzewa rozpinającego i przedstawiamy algorytmy Kruskala oraz Prima służące do jego rozwiązania. Na końcu rozdziału rozważamy problemy wyszukiwania cykli Eulera, Hamiltona oraz trasy komiwojażera. Rozdział 11. stanowi wprowadzenie do teorii złożoności obliczeniowej. Definiujemy w nim klasę problemów P, które są rozwiązywane w czasie wielomianowym oraz klasę NP, obejmującą problemy, dla których istnieje wielomianowy algorytm weryfikujący poprawność rozwiązania, nawet jeśli znalezienie go jest czasochłonne. Ponadto definiujemy problemy NP-zupełne oraz NP-trudne, a także dowodzimy NP-zupełności problemu spełnialności formuł logicznych, problemu kliki, problemu sumy podzbioru, problemu plecakowego, problemu pokrycia wierzchołkowego, problemu cyklu Hamiltona i problemu komiwojażera. Do większości rozdziałów dołączono zadania, których rozwiązanie ma na celu utrwalenie i poszerzenie zdobytej wiedzy.

Zbigniew J. Czech

Gliwice, styczeń 2026