Stabil sort

Den aktuella versionen av sidan har ännu inte granskats av erfarna bidragsgivare och kan skilja sig väsentligt från versionen som granskades den 8 januari 2020; kontroller kräver 2 redigeringar .

Stabil (stabil) sortering  - sortering , som inte ändrar den relativa ordningen för sorterade element som har samma nycklar, genom vilka sortering sker.

Stabilitet är en mycket viktig egenskap hos en sorteringsalgoritm, men den kan ändå nästan alltid uppnås genom att förlänga originalnycklarna med ytterligare information om deras ursprungliga ordning. Trots den uppenbara nödvändigheten som antyds av namnet, är stabilitet inte alls nödvändigt för korrekt sortering och observeras oftast inte, eftersom ytterligare minne och tid nästan alltid krävs för att säkerställa det.

Exempel

När du sorterar poster av formuläret [efternamn, förnamn, patronym] efter efternamn, kommer nyckelvärdena för Ivanov Sergey och Ivanov Ivan att vara desamma, så stabil sortering kommer inte att byta Sergey och Ivan ( Python 3 , timsort sort [1] ):

records = [ { 'surname' : 'Ivanov' , 'first name' : 'Sergey' , 'patronymic' : 'Mikhailovich' ,}, { 'surname' : 'Ivanov' , 'first name' : 'Ivan' , ' patronymic' : 'Borisovich' ,}, { 'surname' : 'Abramov' , 'first name' : 'Yuri' , 'patronymic' : 'Petrovich' ,}, ] poster . sortera ( nyckel = lambda x : x [ 'efternamn' ]) # sortera efter nyckel "efternamn" för r i poster : print ( ' %-12s %-12s %-12s ' % ( r [ 'efternamn' ] , r [ 'förnamn' ], r [ 'patronymic' ]))

Som ett resultat kommer poster att sorteras endast efter efternamn, samtidigt som den ursprungliga ordningen behålls bland poster med samma efternamn:

Abramov Yury Petrovich Ivanov Sergey Mikhailovich Ivanov Ivan Borisovich

Applikation

Sorteringen, som är den huvudsakliga kostnadsdelen av Burroughs-Wheeler-transformen , måste vara robust.

Stablesort-algoritmer

Nedan finns beskrivningar av robusta sorteringsalgoritmer med körtid som inte är sämre än O( n log 2 n ) (förutom de värsta fallen i algoritmen som använder stabil partitionering). I all pseudokod betyder ett par snedstreck // en kommentar till slutet av raden, som i C++.

Slå samman sortering med extra minne

Med denna sorteringsalgoritm delas den initiala arrayen först rekursivt i delar tills varje del av arrayen innehåller ett eller noll element (halvering utförs vid varje rekursionssteg). Därefter "slår de resulterande delarna samman i par". Algoritmens övergripande komplexitet är O( n log n ), och algoritmen kräver O( n ) ytterligare minne för att lagra de sammanslagna arrayerna.

Sortering med stabil partitionering

Denna algoritm liknar quicksort- algoritmen . Precis som i quicksort-algoritmen, i denna algoritm är den initiala arrayen uppdelad i två delar - i den ena är alla element mindre än eller lika med något pivotelement, och i den andra är de större än eller lika. Därefter sorteras de separerade delarna av arrayen rekursivt på samma sätt. Skillnaden mellan denna algoritm och quicksort-algoritmen är att den använder en stabil partition istället för en instabil. Nedan är implementeringen av denna algoritm i pseudokod:

Sortera(a[1..n]) om(n > 1) då N ← a[1]; m ← StablePartition(a[1..n], N); Sort(a[1..m]); Sortera(a[m+1..n]); StablePartition(a[1..n], N) om(n > 1) då m ← ⌊ n/2 ⌋; m1 ← StablePartition(a[1..m], N); m2 ← m+StabilPartition(a[m+1..n], N); Rotera(a[m1..m2], m); return(m1 + (m2-m)); annan om(a[1] < N) då return(1); annan return(0)

Rotera procedur:

Rotera(a[1..n], m) if( n > m > 1 ) //matrisen har minst två element och skiftet är vettigt skift ← mn; //antal positioner som ska flyttas gcd ← GCD(m-1, n); //GCD är den största gemensamma delaren för i ← 0 till gcd-1 gör huvud ← i+1; headVal ← a[huvud]; nuvarande ← huvud; nästa ← huvud+skift; medan(nästa ≠ huvud) gör a[aktuell] ← a[nästa]; nuvarande ← nästa; nästa ← nästa+skift; om(nästa>n) nästa ← nästa-n; a[current] ← headVal;

Det krävs O( n log n ) elementära operationer för att partitionera arrayen hållbart . Eftersom "djupet" för den utförda rekursiva nedstigningen är O(log n ) i genomsnitt, är den totala komplexiteten för algoritmen O( n log² n ). I det här fallet kommer algoritmen att kräva O(log n ) stackminne för att utföra rekursion, även om den totala komplexiteten i värsta fall är O( n ² log n ) och O( n ) minne krävs .

Slå samman sorteringsalgoritmer utan extra minne

Alla algoritmer som beskrivs nedan är baserade på att slå samman redan sorterade arrayer utan att använda ytterligare minne utöver O(log² n ) stackminne (detta är det så kallade minimala minnesvillkoret [2] ) och skiljer sig endast i sammanslagningsalgoritmen. Följande är pseudokoden för algoritmerna (sammanslagningsalgoritmer - Sammanfogningsproceduren - ges separat för varje metod):

Sortera(a[1..n]) om(n > 1) då m ← n/2; Sort(a[1..m]); Sortera(a[m+1..n]); Merge(a[1..n], m); Pratts algoritm

I Pratt-algoritmen finns två element α och β i sorterade arrayer , som är medianerna för en array som består av element i båda arrayerna. Då kan hela arrayen representeras som aαbxβy . Därefter utförs en cyklisk förskjutning av element, som ett resultat av vilket vi får följande sekvens: axβαby . Vidare kommer sammanslagningsalgoritmen att upprepas rekursivt för arrayer ax och by.

Merge(a[1..n], m) if(m ≠ 1 och m ≠ n) //detta villkor betyder att arrayen måste ha minst 2 element if( n-1 > 2 ) //fallet när det finns exakt två element måste beaktas separat if( m-1 > nm ) leftBound←1; rightBound←m; while( leftBound < rightBound ) gör //leta efter medianer i denna loop m1 ← (vänstergräns+högergräns)/2; m2 ← FindFirstPosition(a[m..n], a[ m1 ]); //implementering av proceduren FindFirstPosition se nästa. paragraf om( m1 + (m2-m) > n/2) då högerbunden ← m1-1; annan leftBound ← m1+1; Rotera(a[m1..m2], m); Merge(a[1..ml+(m2-m)], ml); Merge(a[ml+(m2-m)+1..n], m2); annan if(a[m] < a[1]) a[m]⟷a[1];

Att flytta runt element kräver O( n ) elementoperationer och O(1) ytterligare minne, medan att hitta medianer i två redan sorterade arrayer kräver O(log² n ) elementoperationer och O(1) extra minne. Eftersom det finns O(log n ) rekursionssteg är komplexiteten för en sådan sammanslagningsalgoritm O( n log n ), och den totala komplexiteten för sorteringsalgoritmen är O( n log² n ). I det här fallet kommer algoritmen att kräva O(log² n ) stackminne.

Algoritm utan att söka efter medianer

Du kan bli av med sökningen efter medianer i den tidigare algoritmen enligt följande. I den större av de två arrayerna väljer du mittelementet α (pivotelementet). Därefter, i den mindre arrayen, hitta positionen för den första förekomsten av ett element som är större än eller lika med α. Låt oss kalla det β. Därefter skiftas elementen cykliskt, som i Pratt-algoritmen ( aαbxβy → axβαby ), och de erhållna delarna slås rekursivt samman. Pseudokoden för sammanslagningsalgoritmen ges nedan.

Merge(a[1..n], m) if(m ≠ 1 och m ≠ n) //detta villkor betyder att arrayen måste ha minst 2 element if( n-1 > 2 ) //fallet när exakt två element måste betraktas separat if( m-1 > nm ) ml ← (m+1)/2; m2 ← FindFirstPosition(a[m..n], a[ m1 ]); annan m2 ← (n+m)/2; m1 ← FindLastPosition(a[1..m], a[ m2 ]); Rotera(a[m1..m2], m); Merge(a[1..ml+(m2-m)], ml); Merge(a[ml+(m2-m)+1..n], m2); annan if(a[ m ] < a[ 1 ]) a[m]⟷a[1];

Procedurerna FindFirstPosition och FindLastPosition är nästan identiska med den binära sökproceduren:

FindFirstPosition(a[1..n], x) leftBound ← 1; högergräns ← n; aktuell ← 1; while(leftBound <rightBound) gör aktuell ← ⌊(vänstergräns+högergräns)/2⌋; if(a[ aktuell ] < x) alltså leftBound ← aktuell+1 annan rightBound ← ström; return(aktuell); FindLastPosition(a[1..n], x) leftBound ← 1; högergräns ← n; aktuell ← 1; while(leftBound <rightBound) gör aktuell ← ⌊(vänstergräns+högergräns)/2⌋; if( x < a[ aktuell ] ) då rightBound ← ström; annan leftBound ← aktuell+1 return(aktuell);

I motsats till Pratt-algoritmen kan partitioneringen i denna algoritm vara väsentligt ojämlik. Men även i värsta fall kommer algoritmen att dela upp hela området [ a .. y ] i förhållandet 3:1 (om alla element i det andra området är större eller mindre än referensen och båda områdena har lika många av element). Detta garanterar ett logaritmiskt antal rekursionssteg vid sammanslagning av alla arrayer. Således får vi att, precis som i Pratt-algoritmen, är komplexiteten för sammanslagningsalgoritmen O( n log n ), komplexiteten för sorteringsalgoritmen är O( n log² n ), och det nödvändiga minnet är O(log² n ). ).

Stabil sortering utan extra minne i O( n log n ) tid

Sätt att förbättra algoritmer

  • I alla ovanstående algoritmer, när den ursprungliga arrayen delas upp i delar, kan rekursiv delning stoppas om storleken på delningsarrayen blir tillräckligt liten. Sedan kan man använda vilken som helst av de enkla sorteringsalgoritmerna (t.ex. insättningssort ), som är kända för att vara snabbare än komplexa om storleken på inmatningsmatrisen är liten. Faktum är att den här tekniken inte bara är tillämpbar för de algoritmer som presenteras här, utan också för alla andra algoritmer som använder rekursiv partitionering av den ursprungliga matrisen (till exempel den vanliga instabila snabbsorteringen ). Det specifika antalet inmatningselement där det är nödvändigt att byta till en enkel sorteringsalgoritm beror på vilken dator som används.
  • I Pratt-algoritmen, icke-medianalgoritmen och den robusta partitionsalgoritmen, istället för att anropa sammanslagningen rekursivt varje gång, kan du dynamiskt allokera en uppsättning temporära variabler. Då kommer det att vara möjligt att fortsätta dela intervallet endast tills antalet element i det större intervallet är mindre än eller lika med kapaciteten för den temporära arrayen. Faktum är att vid något steg av rekursionen förvandlas Pratt-algoritmen och algoritmen utan att söka efter medianer till en enkel sammanslagningsalgoritm. Den där. om systemet har tillräckligt med dynamiskt minne, kan den asymptotiska körtiden bringas till O( n log n ) istället för O( n log² n ).

Anteckningar

  1. Tim Sort, c2.com . Hämtad 18 november 2012. Arkiverad från originalet 14 juni 2013.
  2. Knut D., Konsten att programmera. v. 3, Sortering och sökning, M., Williams, 2000