Hurtig sortering | |
---|---|
| |
Forfatter | Hoare, Charles Anthony Richard [1] |
formål | Sorteringsalgoritme |
værste tid | O( n 2 ) |
Bedste tid |
O( n log n ) (normal division) eller O( n ) (tredeling) |
Gennemsnitlig tid | O( n log n ) |
Hukommelsesomkostninger |
O( n ) hjælpere O(log n ) hjælpere (Sedgwick 1978) |
Mediefiler på Wikimedia Commons |
Quick sort , Hoares sort ( eng. quicksort ), ofte kaldet qsort (efter navnet i C-standardbiblioteket ) er en sorteringsalgoritme udviklet af den engelske datalog Tony Hoare under sit arbejde på STU i 1960 .
En af de hurtigste kendte universelle array-sorteringsalgoritmer: gennemsnitlig udveksling ved bestilling af elementer; på grund af tilstedeværelsen af en række mangler i praksis, bruges det normalt med nogle modifikationer.
QuickSort er en væsentligt forbedret version af den direkte udvekslingssorteringsalgoritme (varianter af disse er kendt som " Bubble Sort " og " Shaker Sort "), som også er kendt for sin lave effektivitet. Den grundlæggende forskel er, at de størst mulige permutationer laves først, og efter hver gennemgang opdeles elementerne i to uafhængige grupper (hvilket forbedre den mest ineffektive direkte sorteringsmetode resulterede i en af de mest effektive forbedrede metoder).
Den generelle idé med algoritmen er som følger:
I praksis er arrayet normalt ikke opdelt i tre, men i to dele: for eksempel "mindre reference" og "lige og større"; denne tilgang er generelt mere effektiv, da den forenkler separationsalgoritmen (se nedenfor).
Hoare udviklede denne metode i forhold til maskinoversættelse ; ordbogen blev gemt på magnetbånd , og sorteringen af ordene i den bearbejdede tekst gjorde det muligt at få deres oversættelser på én gang af båndet, uden at spole det tilbage. Algoritmen blev opfundet af Hoare under sit ophold i Sovjetunionen , hvor han studerede computeroversættelse ved Moskva Universitet og udviklede en russisk-engelsk parlør.
Quicksort refererer til " divide and conquer "-algoritmer.
Algoritmen består af tre trin:
I den mest generelle form ser pseudokode -algoritmen (hvor A er det array, der skal sorteres, og lav og høj er de nedre og øvre grænser for henholdsvis den sorterede sektion af dette array) sådan ud:
algoritme quicksort(A, lav, høj) er hvis lav < høj så p:= partition(A, lav, høj) quicksort(A, lav, p) quicksort(A, p + 1, høj)Her antages det, at array A sendes ved reference, det vil sige, at sortering sker "på samme sted", og den udefinerede partitionsfunktion returnerer indekset for pivotelementet.
Der er forskellige tilgange til valget af pivotelementet og partitioneringsoperationen, som påvirker algoritmens ydeevne.
Følgende implementering af quicksort er også mulig:
Algoritmen quicksort(A) er, hvis A er tom, returnerer A pivot:= A.pop() (træk sidste eller første element fra array) lA:= A.filter(hvor e <= pivot) (opret array med elementer mindre end pivot) rA := A.filter(hvor e > pivot) (opret array med elementer større end pivot) return quicksort(lA) + [pivot] + quicksort(rA) (retur en matrix bestående af den sorterede venstre side, pivot og sorterede højre side).I praksis bruges den ikke, men tjener kun til uddannelsesformål, da den bruger ekstra hukommelse, hvilket kan undgås.
I tidlige implementeringer blev det første element som regel valgt som reference, hvilket reducerede ydeevnen på sorterede arrays. For at forbedre effektiviteten kan det midterste, tilfældige element eller (for store arrays) medianen af det første, midterste og sidste element vælges. [3] Medianen af hele sekvensen er et optimalt omdrejningspunkt, men dens beregning er for tidskrævende at bruge til sortering.
Valg af et pivot med medianen af tre for Lomuto-partitionen:
mid := (lav + høj) / 2 hvis A[mid] < A[lav] skift A[lav] med A[mid] hvis A[høj] < A[lav] skift A[lav] med A[høj] hvis A[høj] < A[midt] skift A[høj] med A[mid] pivot:= A[mid]Denne opdelingsalgoritme blev foreslået af Nico Lomuto [4] og populariseret i bøger af Bentley (Programming Pearls) og Cormen (Introduction to Algorithms). [5] I dette eksempel er det sidste element valgt som pivot. Algoritmen gemmer indekset i variablen i . Hver gang der findes et element, der er mindre end eller lig med pivoten, øges indekset, og elementet indsættes før pivoten. Selvom dette partitioneringsskema er enklere og mere kompakt end Hoare-skemaet, er det mindre effektivt og bruges i tutorials. Kompleksiteten af denne quicksort øges til O ( n2 ), når arrayet allerede er sorteret, eller alle dets elementer er ens. Der er forskellige metoder til at optimere denne sortering: pivotvalgsalgoritmer, ved hjælp af indsættelsessortering på små arrays. I dette eksempel er elementerne i array A sorteret fra lav til høj (inklusive) [5] :
algoritme partition (A, lav, høj) er pivot := A[høj] i := lav for j := lav til høj - 1 gør hvis A[j] ≤ drejer derefter skift A[i] med A[j] i := i + 1 skift A[i] med A[high] returnere iSortering af et helt array kan gøres ved at udføre quicksort(A, 1, længde(A)) .
Dette skema bruger to indekser (et i begyndelsen af arrayet, det andet i slutningen), som nærmer sig hinanden, indtil der er et par elementer, hvor det ene er større end pivoten og placeret før det, og det andet er mindre og placeret efter den. Disse elementer er udvekslet. Udvekslingen finder sted, indtil indeksene skærer hinanden. Algoritmen returnerer det sidste indeks. [6] Hoares skema er mere effektivt end Lomutos skema, da der i gennemsnit er tre gange færre udvekslinger (swap) af elementer, og opdeling er mere effektiv, selv når alle elementer er ens. [7] I lighed med Lomutos skema viser dette skema også O ( n2 ) effektivitet, når input-arrayet allerede er sorteret. Sortering ved hjælp af denne ordning er ustabil. Bemærk, at slutpositionen af ankerelementet ikke nødvendigvis er den samme som det returnerede indeks. Pseudokode [5] :
algoritme quicksort(A, lo, hej) er hvis lo < hej da p:= partition(A, lo, hej) quicksort(A, lo, p) quicksort(A, p + 1, hej) algoritme partition (A, lav, høj) er pivot:= A[(lav + høj) / 2] i:= lav j:= høj sløjfe for evigt mens A[i] < pivot i:= i + 1 mens A[j] > drejer j:= j - 1 hvis i >= j så returner j skift A[i++] med A[j--]Bogen [5] nævner, at en sådan implementering af algoritmen har "mange subtile pointer". Her er en: Valg af et allerede eksisterende element i arrayet som referenceelement kan føre til et call stack overflow på grund af det faktum, at elementnummeret beregnes som et gennemsnit, som ikke er et heltal. Derfor er det mere logisk i denne algoritme at vælge gennemsnitsværdien mellem de indledende og afsluttende elementer som referenceelement:
pivot := (A[lav] + A[høj])/2
For at forbedre ydeevnen med et stort antal identiske elementer i arrayet kan proceduren til at opdele arrayet i tre grupper anvendes: elementer mindre end referencen, lig med den og større end den. (Bentley og McIlroy kalder dette en "fedt partition". Denne partition bruges i qsort- funktionen i Unix 7 [8] ). Pseudokode:
algoritme quicksort(A, lav, høj) er hvis lav < høj så p := pivot(A, lav, høj) venstre, højre := partition(A, p, lav, høj) // returnerer to værdier quicksort (A, lav, venstre) quicksort (A, højre, høj)Quicksort bruges i sprogstandarden C++, især i sorteringsmetoden for listedatastrukturen.
#include <iostream> #inkluder <liste> int main () { // quicksort std :: liste < int > liste ; const int N = 20 ; for ( int i = 0 ; i < N ; i ++ ) { hvis ( i % 2 == 0 ) liste . push_back ( N - i ); andet liste . push_front ( N - i ); } for ( std :: liste < int >:: iterator it = liste . begynde (); it != liste . slut (); it ++ ) { std :: cout << ( * it ) << " " ; } std :: cout << std :: endl ; liste . sortere (); for ( std :: liste < int >:: iterator it = liste . begynde (); it != liste . slut (); it ++ ) { std :: cout << ( * it ) << " " ; } //hurtig sorteringsafslutning std :: cout << std :: endl ; // sortere større liste . sort ( std :: større < int > ()); for ( std :: liste < int >:: iterator it = liste . begynde (); it != liste . slut (); it ++ ) { std :: cout << ( * it ) << " " ; } std :: cout << std :: endl ; // sortere større ende std :: cin . ignorere (); }Det er klart, at operationen med at opdele et array i to dele i forhold til referenceelementet tager tid . Da alle partitioneringsoperationer, der udføres ved den samme rekursionsdybde, behandler forskellige dele af det originale array, hvis størrelse er konstant, vil der i alt også være behov for operationer på hvert rekursionsniveau. Derfor bestemmes den overordnede kompleksitet af algoritmen kun af antallet af divisioner, det vil sige dybden af rekursion. Dybden af rekursionen afhænger til gengæld af kombinationen af inputdata og hvordan pivoten bestemmes.
Bedste sag. I den mest afbalancerede version er arrayet ved hver splitoperation opdelt i to identiske (plus eller minus et element) dele, derfor vil den maksimale rekursionsdybde, hvor størrelserne af de behandlede underarrays når 1, være . Som et resultat heraf ville antallet af sammenligninger udført af quicksort være lig med værdien af det rekursive udtryk , hvilket giver den overordnede kompleksitet af algoritmen . Gennemsnit. Den gennemsnitlige kompleksitet med en tilfældig fordeling af inputdata kan kun estimeres sandsynligt. Først og fremmest skal det bemærkes, at det i virkeligheden ikke er nødvendigt, at pivotelementet deler arrayet i to identiske dele hver gang. For eksempel, hvis der på hvert trin vil være en opdeling i arrays med en længde på 75% og 25% af originalen, vil rekursionsdybden være lig med , og dette giver stadig kompleksitet . Generelt, for ethvert fast forhold mellem venstre og højre del af divisionen, vil kompleksiteten af algoritmen være den samme, kun med forskellige konstanter. Vi vil overveje en "succesfuld" opdeling, således at referenceelementet vil være blandt de centrale 50 % af elementerne i den delte del af arrayet; klart, sandsynligheden for held med en tilfældig fordeling af elementer er 0,5. Med vellykket adskillelse vil størrelserne af de valgte underarrays være mindst 25 % og ikke mere end 75 % af originalen. Da hvert udvalgt underarray også vil have en tilfældig fordeling, gælder alle disse overvejelser for ethvert sorteringstrin og ethvert indledende arrayfragment. En vellykket split giver en rekursionsdybde på ikke mere end . Da sandsynligheden for held er 0,5, vil det tage rekursive kald i gennemsnit at få pivotelementet k gange i midten 50% af arrayet for at få lucky splits. Ved at anvende disse overvejelser kan vi konkludere, at i gennemsnit vil rekursionsdybden ikke overstige , hvilket er lig med A, da der stadig ikke udføres flere operationer på hvert rekursionsniveau , vil den gennemsnitlige kompleksitet være . Værste tilfælde. I den mest ubalancerede version giver hver opdeling to subarrays af størrelserne 1 og , hvilket betyder, at med hvert rekursivt kald vil det større array være 1 kortere end forrige gang. Dette kan ske, hvis enten det mindste eller det største af alle behandlede elementer vælges som referenceelement på hvert trin. Med det enkleste valg af et referenceelement - det første eller sidste i arrayet - vil en sådan effekt blive givet af et allerede sorteret (i direkte eller omvendt rækkefølge) array, for det midterste eller ethvert andet fast element, "worst case-arrayet" ” kan også vælges specielt. I dette tilfælde vil opdelingsoperationer være påkrævet, og den samlede køretid vil være operationer, det vil sige, at sortering vil blive udført i kvadratisk tid. Men antallet af ombytninger og dermed driftstiden er ikke den største ulempe. Værre, i dette tilfælde vil rekursionsdybden under udførelsen af algoritmen nå n, hvilket vil betyde n-fold lagring af returadressen og lokale variabler for array-partitioneringsproceduren. For store værdier af n kan det værste tilfælde føre til hukommelsesudmattelse ( stack overflow ), mens programmet kører.Fordele:
Fejl:
Algoritmeforbedringer er hovedsageligt rettet mod at eliminere eller afbøde de ovennævnte ulemper, som et resultat af, at de alle kan opdeles i tre grupper: at gøre algoritmen stabil, eliminere ydeevneforringelse ved et særligt valg af pivotelementet og beskytte mod opkaldsstack overløb på grund af den store rekursionsdybde i tilfælde af mislykkede inputdata.
Sorteringsalgoritmer | |
---|---|
Teori | Kompleksitet O notation Ordreforhold Sorter typer bæredygtige Indre Ekstern |
Udveksling | |
Valg | |
indsætter | |
fusion | |
Ingen sammenligninger | |
hybrid | |
Andet | |
upraktisk |