Comb Sort - sortowanie grzebieniowe

Comb Sort, czyli sortowanie grzebieniowe, wyrósł z bardzo prostego pomysłu: skoro sortowanie bąbelkowe radzi sobie gorzej z elementami znajdującymi się daleko od właściwej pozycji, można zacząć od porównywania liczb oddalonych od siebie o kilka miejsc. Dopiero później ten odstęp jest stopniowo zmniejszany. Na końcu algorytm rzeczywiście zaczyna przypominać klasyczne sortowanie bąbelkowe, ale wtedy tablica jest już częściowo uporządkowana. Dzięki temu wiele elementów trafia w okolice właściwych pozycji znacznie wcześniej.

Skąd wziął się pomysł na Comb Sort?

Żeby zrozumieć Comb Sort, najlepiej najpierw przypomnieć sobie, jak działa sortowanie bąbelkowe. Tam porównujemy element z jego bezpośrednim sąsiadem. Jeśli stoją w złej kolejności, zamieniamy je miejscami i przechodzimy dalej.

Taki sposób jest prosty, ale ma jedną irytującą cechę. Element, który znajduje się bardzo daleko od miejsca, w którym ostatecznie powinien się znaleźć, przesuwa się powoli. Szczególnie dobrze widać to na małych liczbach znajdujących się pod koniec tablicy.

Weźmy taki zestaw:

[8, 7, 6, 5, 4, 3, 2, 1]

Liczba 1 powinna znaleźć się na samym początku, ale w sortowaniu bąbelkowym nie przeskoczy od razu o kilka pozycji. Musi przesuwać się krok po kroku przez kolejne zamiany.

Comb Sort próbuje ominąć właśnie ten problem. Na początku pozwala porównywać elementy, które znajdują się daleko od siebie. Dzięki temu jedna zamiana może przesunąć liczbę o kilka miejsc, a nie tylko o jedno.

Stąd też określenie "sortowanie grzebieniowe". Można sobie wyobrazić grzebień o szeroko rozstawionych zębach, który z każdym kolejnym przejściem staje się coraz gęstszy.

Jak działa sortowanie grzebieniowe?

W sortowaniu bąbelkowym odległość pomiędzy porównywanymi elementami zawsze wynosi 1. Porównujemy więc:

tablica[0] z tablica[1]
tablica[1] z tablica[2]
tablica[2] z tablica[3]

Comb Sort wprowadza dodatkową wartość, nazywaną zazwyczaj gap. Określa ona odstęp pomiędzy dwoma elementami, które aktualnie ze sobą porównujemy.

Jeśli gap wynosi 5, pierwsze porównania mogą wyglądać tak:

tablica[0] z tablica[5]
tablica[1] z tablica[6]
tablica[2] z tablica[7]

Przy odstępie równym 3:

tablica[0] z tablica[3]
tablica[1] z tablica[4]
tablica[2] z tablica[5]

Za każdym przejściem odstęp maleje. W końcu dochodzi do 1. Wtedy Comb Sort przechodzi w etap bardzo podobny do sortowania bąbelkowego, ponieważ zaczyna porównywać sąsiadujące ze sobą elementy.

Różnica jest taka, że wcześniej zdążyliśmy już poprzesuwać najbardziej problematyczne wartości. Końcowe przejścia mają więc znacznie mniej pracy.

Odstęp między elementami - najważniejszy pomysł Comb Sort

Odstępu nie zmniejsza się zwykle o jeden. Gdyby tablica miała kilkaset czy kilka tysięcy elementów, taki sposób oznaczałby ogromną liczbę niepotrzebnych przejść.

W klasycznej wersji Comb Sort stosuje się współczynnik zmniejszania wynoszący około 1,3. Oznacza to, że po każdym przejściu aktualny odstęp dzielimy przez 1,3.

Dla tablicy zawierającej 20 elementów kolejne odstępy mogą wyglądać mniej więcej tak:

Etap Odstęp
Początek 20
1 15
2 11
3 8
4 6
5 4
6 3
7 2
8 1

Nie trzeba zapamiętywać tych konkretnych wartości. Istotny jest sam kierunek działania: najpierw duże skoki, później coraz dokładniejsze porządkowanie tablicy.

Kiedy po podzieleniu odstępu otrzymalibyśmy wartość mniejszą od 1, ustawiamy po prostu gap = 1. Od tej chwili algorytm porównuje już sąsiadujące elementy.

Comb Sort na konkretnym przykładzie

Spójrzmy na tablicę:

[9, 4, 7, 3, 8, 2, 6, 1]

Znajduje się w niej 8 elementów. Po pierwszym zmniejszeniu odstępu otrzymamy w przybliżeniu:

8 / 1.3 ≈ 6

Porównujemy więc pierwszy element z elementem oddalonym o sześć pozycji:

9 i 6

Są ustawione nieprawidłowo, więc wykonujemy zamianę:

[6, 4, 7, 3, 8, 2, 9, 1]

Następnie porównujemy:

4 i 1

Po kolejnej zamianie mamy:

[6, 1, 7, 3, 8, 2, 9, 4]

To właśnie tutaj dobrze widać sens Comb Sort. Liczba 1 znajdowała się na ostatniej pozycji, a po jednej zamianie trafiła prawie na początek tablicy. W zwykłym sortowaniu bąbelkowym potrzebowałaby do tego kilku kolejnych porównań i zamian.

W kolejnym przejściu odstęp jest mniejszy:

6 / 1.3 ≈ 4

Teraz algorytm wykonuje porównania co cztery miejsca. Później odstęp zmniejszy się do 3, następnie 2 i na końcu 1.

Gdy odstęp wynosi już 1, mechanizm jest praktycznie taki sam jak przy sortowaniu bąbelkowym. Nadal trzeba jednak wykonywać kolejne przebiegi do momentu, aż cały zostanie zakończony bez żadnej zamiany.

Po zakończeniu pracy otrzymujemy:

[1, 2, 3, 4, 6, 7, 8, 9]

Comb Sort w Pythonie

Kod nie jest szczególnie długi. W porównaniu z najprostszą implementacją sortowania bąbelkowego dochodzą właściwie dwa elementy: zmienny odstęp oraz informacja o tym, czy podczas ostatniego przejścia wykonaliśmy jakąkolwiek zamianę.

def comb_sort(tablica):
    gap = len(tablica)
    shrink = 1.3
    zamiana = True

    while gap > 1 or zamiana:
        gap = int(gap / shrink)

        if gap < 1:
            gap = 1

        zamiana = False
        i = 0

        while i + gap < len(tablica):
            if tablica[i] > tablica[i + gap]:
                tablica[i], tablica[i + gap] = tablica[i + gap], tablica[i]
                zamiana = True

            i += 1


tablica = [9, 4, 7, 3, 8, 2, 6, 1]

print("Przed sortowaniem:", tablica)

comb_sort(tablica)

print("Po sortowaniu:", tablica)

Wynik:

Przed sortowaniem: [9, 4, 7, 3, 8, 2, 6, 1]
Po sortowaniu: [1, 2, 3, 4, 6, 7, 8, 9]

Co właściwie dzieje się w kodzie?

Na początku odstęp jest równy długości tablicy:

gap = len(tablica)

Nie oznacza to jeszcze, że będziemy porównywać pierwszy element z elementem znajdującym się poza tablicą. Przed rozpoczęciem pierwszych porównań wartość zostaje zmniejszona:

gap = int(gap / shrink)

Współczynnik przechowujemy osobno:

shrink = 1.3

Po każdym pełnym przejściu przez tablicę gap maleje. Gdy zrobi się zbyt mały, ustawiamy go na 1:

if gap < 1:
    gap = 1

W środku porównujemy element znajdujący się pod indeksem i z elementem oddalonym właśnie o aktualny odstęp:

if tablica[i] > tablica[i + gap]:

Jeżeli wartości stoją w złej kolejności, następuje zamiana:

tablica[i], tablica[i + gap] = tablica[i + gap], tablica[i]

Przy okazji zapisujemy informację, że coś się zmieniło:

zamiana = True

To potrzebne szczególnie pod koniec. Kiedy gap osiągnie 1, samo dojście do końca tablicy nie oznacza jeszcze, że wszystko jest posortowane. Tak samo jak w sortowaniu bąbelkowym może być potrzebne kolejne przejście.

Dlatego główna pętla ma warunek:

while gap > 1 or zamiana:

Kończymy dopiero wtedy, gdy odstęp wynosi 1 i podczas ostatniego przebiegu nie doszło do żadnej zamiany.

Comb Sort a sortowanie bąbelkowe

Podobieństwo między tymi algorytmami nie jest przypadkowe. Comb Sort został pomyślany jako sposób na ograniczenie jednego z największych problemów, jakie ma sortowanie bąbelkowe: bardzo powolnego przesuwania elementów znajdujących się daleko od swojej docelowej pozycji.

Cecha Comb Sort Sortowanie bąbelkowe
Porównywane elementy Początkowo oddalone od siebie Zawsze sąsiadujące
Odstęp Zmniejsza się w czasie Zawsze wynosi 1
Przesuwanie mocno źle ustawionych elementów Możliwe o wiele pozycji jednocześnie Po jednej pozycji
Najgorszy przypadek O(n²) O(n²)
Dodatkowa pamięć O(1) O(1)

Największa różnica jest więc bardzo prosta. Sortowanie bąbelkowe od początku "patrzy" tylko na sąsiadów. Comb Sort najpierw patrzy dużo dalej, a dopiero na końcu zajmuje się drobnymi poprawkami.

Nie oznacza to jednak, że sortowanie grzebieniowe całkowicie odcina się od swojego prostszego odpowiednika. Gdy gap dochodzi do 1, końcówka działania Comb Sort wygląda bardzo podobnie do sortowania bąbelkowego.

Złożoność Comb Sort

Comb Sort potrafi zachowywać się sprawniej od klasycznego sortowania bąbelkowego, ale nadal nie jest algorytmem z gwarantowaną złożonością O(n log n).

W najgorszym przypadku złożoność czasowa wynosi O(n²). Przy większych zbiorach danych istnieją więc algorytmy o znacznie lepszych właściwościach.

Nie zmienia to faktu, że sam pomysł jest ciekawy. Wprowadzenie dużego odstępu pozwala stosunkowo tanio rozwiązać problem wolnego przemieszczania wartości, który dobrze znamy z sortowania bąbelkowego.

Algorytm pracuje bezpośrednio na przekazanej tablicy, dlatego nie potrzebuje dodatkowej tablicy o podobnym rozmiarze. Dodatkowe zapotrzebowanie na pamięć wynosi więc O(1).

Comb Sort nie jest natomiast algorytmem stabilnym. Zamiana dwóch oddalonych od siebie elementów może zmienić kolejność danych mających tę samą wartość.

Typowe błędy przy implementacji

Pierwszy błąd jest dość naturalny: ktoś widzi, że odstęp stopniowo spada do 1, więc zakłada, że właśnie wtedy należy zakończyć sortowanie. Nie zawsze.

Przy gap = 1 tablica może nadal wymagać kilku zamian. Trzeba więc kontynuować pracę tak długo, aż cały przebieg zostanie wykonany bez żadnej zmiany.

Drugi problem pojawia się przy obliczaniu odstępu:

gap = int(gap / 1.3)

Po pewnym czasie taki zapis może dać 0. Tymczasem odstęp równy zero nie ma tutaj sensu. Z tego powodu minimalną wartością pozostaje 1.

Trzeba też uważać na indeks:

tablica[i + gap]

Pętla nie może dopuścić do sytuacji, w której i + gap wyjdzie poza koniec tablicy. Stąd warunek:

while i + gap < len(tablica):

Gdy zrozumiemy te trzy rzeczy - malejący odstęp, sprawdzanie wykonanych zamian i pilnowanie indeksów - reszta Comb Sort jest już dość łatwa do prześledzenia.

FAQ - Comb Sort

Co to jest Comb Sort i jak działa?
Comb Sort, czyli sortowanie grzebieniowe, porównuje elementy znajdujące się w określonej odległości od siebie. Na początku odstęp jest duży, a później stopniowo maleje, aż osiągnie 1. Końcowy etap przypomina wtedy sortowanie bąbelkowe.
Czym Comb Sort różni się od sortowania bąbelkowego?
Sortowanie bąbelkowe od początku porównuje sąsiadujące elementy. Comb Sort zaczyna od większego odstępu, dlatego źle ustawiona liczba może podczas jednej zamiany przesunąć się o kilka pozycji zamiast tylko o jedną.
Co oznacza gap w Comb Sort?
gap oznacza odstęp pomiędzy aktualnie porównywanymi elementami tablicy. W kolejnych przebiegach jest on zmniejszany, aż ostatecznie wyniesie 1.
Dlaczego w Comb Sort używa się 1,3?
Wartość około 1,3 jest popularnym współczynnikiem zmniejszania odstępu. Dzięki niej algorytm dość szybko przechodzi od porównywania odległych elementów do sprawdzania tych znajdujących się obok siebie.
Jaka jest złożoność Comb Sort?
W najgorszym przypadku złożoność Comb Sort wynosi O(n²). Algorytm może jednak działać sprawniej niż zwykłe sortowanie bąbelkowe, ponieważ szybciej przesuwa elementy znajdujące się daleko od właściwego miejsca.
Kiedy Comb Sort kończy działanie?
Samo osiągnięcie odstępu równego 1 nie wystarcza. Algorytm kończy pracę wtedy, gdy gap wynosi 1 i podczas całego kolejnego przebiegu nie trzeba już wykonać żadnej zamiany.
Czy Comb Sort jest stabilny?
Nie. Ponieważ algorytm może zamieniać elementy znajdujące się daleko od siebie, wartości równe mogą zmienić swoją wcześniejszą kolejność.
Jak napisać Comb Sort w Pythonie?
Najprostsza wersja potrzebuje zmiennej przechowującej odstęp, współczynnika jego zmniejszania oraz informacji, czy podczas przebiegu wykonano zamianę. W Pythonie sam algorytm można zapisać w kilkunastu liniach i nie wymaga dodatkowej tablicy.

Komentarze