Sortowanie listy przez łączenie naturalne
Mariusz:
https://pastebin.com/1ZcN5jku
Rozdzielanie listy wydaję się działać dobrze
Na innym forum jeden próbował mi przedstawić algorytm łączenia naturalnego
Podał nawet pseudokod ale gdy spróbowałem na podstawie jego pseudokodu napisać
listową wersję tego algorytmu to dostałem błąd segmentation fault
23 mar 10:42
Pytający:
Znalazłem taki mój stary kod:
https://ideone.com/pb7FXG
Co prawda C++, ale pisany w stylu C, więc wystarczyłoby pozmieniać referencje do wskaźników na
podwójne wskaźniki, odpowiednie wywołania funkcji, konstruktory struktur na funkcje itp., i
będzie C.
Niby jest tam MergeSort, ale chyba bardziej to sortowanie przez łączenie naturalne (zwał jak
zwał):
• lista wejściowa jest rozdzielana na już posortowane listy, te posortowane fragmenty (listy)
są umieszczone w cyklicznej liście (nodeQ to węzły tejże listy) − to robi funkcja Split,
• następnie dopóki w ów cyklicznej liście jest więcej niż jeden posortowany fragment (while
(qhead−>next != qhead)), bierzemy 2 posortowane fragmenty (listy) z brzegu i je łączymy
(funkcja Merge), wrzucamy powstałą listę do jednego z węzłów (nodeQ), drugi jest zbędny więc
go usuwamy i przesuwamy głowę (qhead) tej cyklicznej listy do następnego elementu, coby mieć
do czynienia z potencjalnie krótszymi fragmentami (a nie tym dopiero co połączonym, ten trafia
na "koniec")
• na koniec nadpisujemy wskaźniki do pierwotnej listy, a ostatni węzeł listy cyklicznej można
usunąć.
Kolejne kroki są wypisywane, więc da radę ogarnąć co się dzieje.
A co do Twojego kodu, coś za długie wydają się te funkcje rozdzielająca i łącząca, aż czytać
ich się nie chce.
Pewnie znacznie mógłbyś je uprościć dopisując jakieś funkcje pomocnicze
(przykładowo u mnie w kodzie funkcja ShiftToEnd(node*& head1, node*& head, node*& tail) jest
wywoływana w tych funkcjach, a przenosi ona po prostu głowę jakiejś listy (head1) na koniec
jakiejś listy określonej przez argumenty head, tail (oczywiście zakładając poprawność danych
wejściowych).
A segmentation fault może być skutkiem np. próby wyłuskania wartości z nulla (dereferencji):
int* p = NULL;
*p = 1;
https://stackoverflow.com/a/2346849
23 mar 17:38
Mariusz:
"(zwał jak zwał)"
Są różne wersje sortowania przez scalanie więc odpowiednie nazewnictwo
pozwala rozpoznać o jaką wersję nam chodzi
Wersja rekurencyjna (nazywana czasem sortowaniem w dół)
Wersja iteracyjna
Łączenie proste (nazywana czasem sortowaniem w górę)
Tutaj zakładamy że przed sortowaniem posortowane podciągi są długości 1
Każde scalenie podwaja długość posortowanych podciągów
Wprowadzenie liczników byłoby przydatne ale spowodowałoby błąd przepełnienia zakresu
typu int i możliwe błędne działanie
Łączenie naturalne
Załóżmy że klucze węzłów listy tworzą ciąg
Przed sortowaniem występują w tym ciągu posortowane podciągi
(nie muszą być one długości jeden)
Podciągi te są rozdzielane na przemian między dwie pomocnicze listy
Jeśli po etapie rozdzielania druga lista będzie pusta to możemy zakończyć algorytm
Podczas scalania łączone są te posortowane podciągi i po jednorazowym
wywołaniu procedury scalającej nie mamy posortowanej listy tylko
na liście występują dłuższe podciągi
Koniec podciągu możemy wykryć np gdy
* osiągneliśmy koniec listy
* prevNode−>key > currNode−>key
To tak z grubsza co udało mi się przeczytać
Jeśli chodzi o rekurencyjne sortowanie przez scalanie to mam wersję tablicową
a jeśli chodzi o wersję listową to chętnie bym zobaczył funkcje
splitBefore oraz splitAfter bo byłyby przydatne po znalezieniu węzła przed/za którym
chcielibyśmy uciąć listę
Jeżeli chodzi o łączenie proste to wersję tablicową powinienem mieć
gdzieś na starym dysku (ostatnio skrobnąłem też dla tablic indeksowanych od jedynki)
Jeśli chodzi o iteracyjne sortowanie listy to wolałbym jednak łączenie naturalne bo
do rozdzielania i łączenia wykorzystuje się relacje porównania zamiast liczników
które to mogą przepełnić zakres typu int
Chciałem aby funkcja realizowała konkretne zadanie
Można byłoby wydzielić kilka dodatkowych funkcji jak np dla quicksorta
void quickSort(struct Node **head,struct Node **tail)
{
if((*head) != (*tail))
{
pivot = Select(head,tail);
curr = (*head)
while(curr != NULL)
{
y=delHead(&curr,tail);
if(y−>data < pivot−>data)
tailins(y,&lowf,&lowl);
else if(y−>data == pivot−>data)
tailins(y,&midf,&midl);
else
tailins(y,&highf,&highl);
}
quickSort(&lowf,&lowl);
quickSort(&highf,&highl);
concat(&lowf,&lowl,midf,midl);
concat(&lowf,&lowl,highf,highl);
(*head) = lowf;
(*tail) = lowl;
}
}
I tutaj mamy sporo funkcji wykorzystywanych tylko przez jedną konkretną procedurę sortującą
Koleś na innym forum podrzucił mi następujący pseudokod
na którym się wzorowałem pisząc funkcję scalającą
string rekordZtmp1
string rekordZtmp2
string poprzedniRekordZtmp1 = cosOdCzegoKazdyStringBedzieWiekszy
string poprzedniRekordZtmp2 = cosOdCzegoKazdyStringBedzieWiekszy
int koniecSeriiWtmp1 = 0
int koniecSeriiWtmp2 = 0
rekordZtmp1 = pobierzLinie( tmp1 );
rekordZtmp2 = pobierzLinie( tmp2 );
while( tmp1 != EOF || tmp2 != EOF )
while(!koniecSeriiWtmp1 && !koniecSeriiWtmp2)
if ( rekordZtmp1 > rekordZtmp2 )
wpiszLinie( plikDocelowy, rekordZtmp1 );
poprzedniRekordZtmp1 = rekordZtmp1;
rekordZtmp1 = pobierzLinie( tmp1 );
if ( poprzedniRekordZtmp1 > rekordZtmp1)
koniecSeriiWtmp1 = 1;
else
wpiszLinie( plikDocelowy, rekordZtmp2 );
poprzedniRekordZtmp2 = rekordZtmp2;
rekordZtmp2 = pobierzLinie( tmp2 );
if ( poprzedniRekordZtmp2 > rekordZtmp2 )
koniecSeriiWtmp2 = 2;
while(!koniecSeriiWtmp1)
wpiszLinie( plikDocelowy, rekordZtmp1 );
oprzedniRekordZtmp1 = rekordZtmp1;
rekordZtmp1 = pobierzLinie( tmp1 );
if ( poprzedniRekordZtmp1 > rekordZtmp1)
koniecSeriiWtmp1 = 1;
while(!koniecSeriiWtmp2)
wpiszLinie( plikDocelowy, rekordZtmp2 );
poprzedniRekordZtmp2 = rekordZtmp2;
rekordZtmp2 = pobierzLinie( tmp2 );
if ( poprzedniRekordZtmp2 > rekordZtmp2 )
koniecSeriiWtmp2 = 2;
koniecSeriiWtmp1 = koniecSeriiWtmp2 = 0;
string poprzedniRekordZtmp1 = cosOdCzegoKazdyStringBedzieWiekszy
string poprzedniRekordZtmp2 = cosOdCzegoKazdyStringBedzieWiekszy
23 mar 22:28
Mariusz:
A co do Twojego kodu, coś za długie wydają się te funkcje rozdzielająca i łącząca,
aż czytać ich się nie chce.
Pewnie znacznie mógłbyś je uprościć dopisując jakieś funkcje pomocnicze
https://pastebin.com/YuCPhiYv
Dopisanie tych funkcji niezbyt dużo skróciło ten kod
Teraz lepiej się czyta ?
24 mar 10:37
Mariusz:
Wydaję mi się że koleś źle wykrywał koniec serii a także przy porównywaniu nie
sprawdzał czy osiągnęliśmy koniec pliku / koniec listy
(do if trzeba było dopisać warunki sprawdzające czy nie trafiliśmy na NULLa)
i tak jak pisałeś dochodziło do próby wyłuskania wartości z nulla
Nadal nie jestem pewny czy algorytm działa dobrze ale już przynajmniej
udało mi się usunąć błędy typu segmentation fault
24 mar 19:14
Pytający:
"niezbyt dużo skróciło ten kod"?
Pierwotnie:
• Split − 74 linie,
• Merge − 129 linie.
Po zmianach:
• Split − 37 linie,
• Merge − 67 linie.
To znaczące uproszczenie. Tak, lepiej się czyta.
Oczywiście pozwoliłem sobie na mały refactoring, skoro już bawić się w poprawki:
https://ideone.com/UGPNVA
Funkcje Split i Merge jako tako skomentowałem, powinny być do ogarnięcia.
Nazwy funkcji zmieniłem coby trzymać się jakiejś konwencji.
Chyba wszystkie przekazywane argumenty pozmieniałem na wskaźniki, po co kopiować struktury.
Zresztą nawet przy wypisywaniu tak czy siak tworzyłeś kopię kopii wskaźnika, choć śmiało
mogłeś korzystać z kopii "głowy" przy przekazywaniu przez wartość.
Funkcje swap dodałem, coby mieć jedną gałąź kodu zamiast dwóch w odpowiednich funkcjach (mniej
kodu, mniejsza szansa na błąd, a i o zmiany/poprawki łatwiej).
Asercje coby uwidocznić niezmienniki funkcji − jeśli zakładamy, że jakiś wskaźnik będący
argumentem danej funkcji jest niezerowy (różny od nulla), to śmiało można dopisać taką
asercję, wtedy jeśli funkcja zostanie wywołana z nullem, to asercja wywoła funkcję abort(), a
Ty będziesz mógł przeczytać, że w linii k−tej programu asercja nie została spełniona i wiesz
mniej więcej, gdzie szukać błędu (i możliwe, że uprzedzisz Segmentation Fault).
Co do Twojego kodu − nawet mi się nie skompilował w Visual Studio, od razu kompilator słusznie
zauważył, że w linii 177:
if(tmpA−>data > tmpB−>data)
może nastąpić dereferancja niezainicjalizowanej zmiennej − zarówno tmpA, jak i tmpB mogło nie
być zainicjalizowane w tym miejscu. Więcej błędów nie szukałem − po uproszczeniu/zmienieniu
paru rzeczy zdaje się hulać...
24 mar 20:28
Pytający:
I byłbym zapomniał, jeśli masz wskaźnik p, to do pola jakieśPole chyba prościej odwołać się:
p−>jakieśPole
niż:
(*p).jakieśPole // takich odowołań było u Ciebie mnóstwo, też pozmieniałem (acz może tak wolisz
i to było umyślne, kto wie)
24 mar 20:36
Mariusz:
Ja próbowałem sam bawić się kodem i oto do czego doszedłem
https://ideone.com/9blNrM
Dopisałem kilka rzeczy które mogłbyby zapobiec dereferencji NULLa
Przeczytam twój kod i w miarę moich możliwości spróbuję porównać to co otrzymałem
24 mar 20:43
Mariusz:
Co do odwołania (*p).jakieśPole
to chciałem zaznaczyć że używam tutaj wskaźnika zamiast referencji której w C jeszcze nie ma
24 mar 20:45
Mariusz:
24 mar 20:43
Tutaj bawiłem się kodem jeszcze przed tym jak wysłałeś swoje rozwiązanie
Starałem się wyeliminować przypadki gdy następuje dereferencja NULLa i jakichś
niezainicjalizowanych wskaźników
Pytający w swoim kodzie zgubiłeś jeden węzeł ?
24 mar 21:54
Pytający:
Faktycznie, zgubiłem. W tym przypadku 1 węzeł, ale ogólnie tamten kod może zgubić całą "resztę"
serii, bo nie zamieniłem referencji do list (sam oszukałem się komentarzem − oto przykład na
to, że komentarze nie zawsze pomagają
).
Poprawione:
https://ideone.com/5cSdal // patrz linia 203
Co do "p−>x" to jest to właśnie skrócony zapis "(*p).x" i służy właśnie do dereferencji. Widząc
takową strzałkę wiesz, że masz do czynienia ze wskaźnikiem (mowa o C). W działaniu to jedno i
to samo.
//
https://stackoverflow.com/a/2575050
24 mar 22:45
25 mar 00:02
Pytający:
linie 186, 206 // warunek "poprzedniZtmpA == NULL" jest zbędny, poprzedniZtmpA nie może w tym
miejscu być nullem, przecież parę linijek wyżej to na co wskazuje dorzucasz do listy L
linie 196, 216 // jw. (ten sam kod tylko dla B)
Warunki zbędne, ale nijak szkodliwe.
Błąd jest w warunku w linii 176 − to, że listy są puste nie znaczy, że nie został węzeł w tmpA
lub w tmpB.
25 mar 00:47
Mariusz:
Błąd jest w warunku w linii 176
Jak się dalej pobawiłem tym kodem to też do tego doszedłem
Wiesz co mnie naprowadziło na ten błąd ?
Przypadki brzegowe np lista z jednym węzłem
W kodach źródłowych jakie widziałem często pomijano poprawność przypadków brzegowych itp
poprzedniZtmpA == NULL
Jeszcze w tym samym warunku wyłuskuje dane z adresu i to było po to aby upewnić się
że nie będziemy wyłuskiwać danych z nulla aczkolwiek masz rację że jest zbędny
25 mar 08:10
Mariusz:
A jest jeszcze taka dość ważna umiejętność jak czytanie kodu często pomijana w tych
tutorialach itp
Gdy zacząłem czytać kod tej funkcji scalającej od początku to zauważyłem że dla
przypadków brzegowych węzły nie zostaną wstawione do listy wynikowej
25 mar 08:31
Mariusz:
Tak sobie pomyślałem że w przypadku listy dwukierunkowej każdy węzeł
zawiera wskaźnik na poprzednika więc wydaje się że zmienne lokalne wskazujące na
poprzednika można by usunąć wyodrębniając jakiś przypadek brzegowy
Jeśli chodzi o uogólnianie to w C mamy możliwość
zmiany typu danych z int na void * oraz podania funkcji porównującej
jako argument funkcji sortujących
W C++ mielibyśmy referencję, szablony, a z przeciążaniem operatorów byłbym ostrożny
bo jak widzieliśmy w przypadku otoczki tam funkcja porównująca pobierała trzy argumenty
25 mar 10:02