Sortierroutine mit sukzessiver Approximation

Procedure.l SortiereIndex(Array Liste.i(1), &
                                     IndexAnzahl.l)
  Define.l
  AktWort = 1
  VerglWort = 0
  Sprungweite = 1
  NochDurchlaufe = 1
  While (AktWort < IndexAnzahl)
    Select(strcmp(Liste(VerglWort),Liste(AktWort)))
;Gerade untersuchtes Datum links ist zu klein. Wenn
;es das letzte ist, ist AktWort automatisch Ende
;der Liste.
      Case −1:
        If (VerglWort<AktWort−1 And NochDurchlaufe)
          VerglWort + Sprungweite
          If (VerglWort >= AktWort)
            VerglWort = AktWort−1
          EndIf
          Sprungweite = (Sprungweite+1) >> 1
        Else
          dummy = Liste(AktWort)
          MoveMemory(@Liste(VerglWort+1), &
            @Liste(VerglWort+2), &
            (AktWort−VerglWort−1)*SizeOf(Integer))
          Liste(VerglWort+1) = dummy
          AktWort + 1
          VerglWort = AktWort >> 1
          Sprungweite = (VerglWort+1) >> 1
          NochDurchlaufe = 0
          dummy = AktWort
          Repeat
            NochDurchlaufe + 1
            dummy >> 1
          Until dummy = 0
        EndIf
;Gerade untersuchtes Datum ist mit dem einzusortie
;renden identisch.
      Case 0:
        IndexAnzahl − 1
        Liste(AktWort) = Liste(IndexAnzahl)
        Liste(IndexAnzahl) = 0
        VerglWort = AktWort >> 1
        Sprungweite = (VerglWort+1) >> 1
        NochDurchlaufe = 0
        dummy = AktWort
        Repeat
          NochDurchlaufe + 1
          dummy >> 1
        Until dummy = 0
;Gerade untersuchtes Datum links ist zu groß. Wenn
;es das erste ist, ist AktWort automatisch Beginn
;der Liste.
      Case 1:
        If (VerglWort > 0 And NochDurchlaufe)
          VerglWort − Sprungweite
          If (VerglWort < 0)
            VerglWort = 0
          EndIf
          Sprungweite = (Sprungweite+1) >> 1
        Else
          dummy = Liste(AktWort)
          MoveMemory(@Liste(VerglWort), &
            @Liste(VerglWort+1), &
            (AktWort−VerglWort)*SizeOf(Integer))
          Liste(VerglWort) = dummy
          AktWort + 1
          VerglWort = AktWort >> 1
          Sprungweite = (VerglWort+1) >> 1
          NochDurchlaufe = 0
          dummy = AktWort
          Repeat
            NochDurchlaufe + 1
            dummy >> 1
          Until dummy = 0
        EndIf
    EndSelect
    NochDurchlaufe − 1
  Wend
  ProcedureReturn(IndexAnzahl)
EndProcedure

Abb. 1: PureBasicQuelltext. Die EtZeichen bei den MoveMemory()Funktionen sind MehrzeilenVerbinder.

Eine mit 10 Elementen befüllte Liste soll sortiert werden. Die ersten 6 Einträge sind bereits richtig plaziert. Der nächste, 33, ist nun dran.

78122735773318434

Die Mitte des bereits sortierten Teils der Liste wird berechnet (3, beginnend mit 0) und die nächste Schrittweite (2) bestimmt. Die 27 und alle folgenden Daten würden verdrängt werden.

7812??27357718434

Der Vergleich ergibt −1. Die Schrittweite (2) wird auf die Suchposition aufgeschlagen (nun 5) und die Schrittweite auf 1 halbiert.

78122735??7718434

Nun würde die 77 verdrängt werden, was im Vergleich 1 ergibt. Die Suchposition wird um die Schrittweite vermindert (nun 4) und der Vergleich mit der 35 vollzogen.

781227??357718434

Da das Ergebnis 1 lautet, wir aber die Anzahl der notwendigen Durchläufe erreichten, wird nun unsere 33 hier eingefügt.

78122733357718434

Der sortierte Teil der Liste ist nun 7 Einträge lang. Wir kommen zum nächsten fraglichen Wert, der 18.

78122733357718434

Abb. 2: Funktionsweise anhand eines kleinen Beispiels.


ListengrößeZeit in sZeit in s
1.0000,080,02
3.0000,220,08
10.0000,970,13
30.0004,161,04
100.00025,0411,08
300.000161,498,65
1.000.0001460ca.1100

Tab. 1: Tabelle mit Rechenzeiten bei Verwendung von PureBasic 4.30 auf Windoof XP SP3 mit einem Intel Centrino (1,86GHz) bei (mittig) DebugModus und laufendem WinAmp und (rechts) fertigkompiliertem Programm ohne weitere Rechenzeitfresser. Es werden alle Listengrößen 1...n durchlaufen.

Diese Sortierroutine – damals noch in Assembler – schrieb ich etwa 1992 auf einer meiner Amigas, um eine 30.000 Zeilen lange Wortliste für PageStream dem Alphabet nach zu sortieren. Sie sollte möglichst schnell sein, wenig Speicher verbrauchen und dazu übersichtlich sein. Die mir damals vorhandenen Quellen erleichterten mir die Auswahl aus vorhandenen Verfahren nicht gerade. Somit begab ich mich an eine eigene Entwicklung. Gegenüber BubbleSort, ja sogar InsertionSort ergibt sich ein Geschwindigkeitsfaktor von bis zu 10.000.000, je nach Größe der zu sortierenden Liste.

Inspiriert zu diesem Algorithmus habe ich mich durch die Entwicklungen an einem schnellen, aber günstigen ADWandler. FlashWandler waren mir damals zu teuer, einfache Rampenwandler waren nicht schnell genug für meine Ansprüche. Mit der sukzessiven Approximation (schrittweisen Annäherung), bei der die Schritte immer kleiner wurden, brauchte man nur sehr wenige Durchläufe, um ein recht genaues Ergebnis zu erzielen. Ein entsprechend gebranntes GAL war schnell hergestellt. Dieses Prinzip übertrug ich in Assembler.

Es werden int(lb(n)+2) Durchläufe pro Eintrag benötigt. n ist die Länge der bereits bestehenden sortierten Liste, zu Beginn also immer 1. Mit fortschreitendem Einsortieren der neuen Werte wird die Liste um einen Eintrag länger. Bei einer Listengröße von 10 Einträgen sind das 5 Durchläufe – damit liegt der Aufwand im Mittel bei dem eines BubbleSort. Bei 100 Einträgen sind es 8 Durchläufe, bei 1000 Einträgen 11. So richtig in Fahrt kommt die Routine bei extremen Listengrößen: 1.000.000 Einträge: 21 Durchläufe (entspricht einem Geschwindigkeitsfaktor von 23.809), 1.000.000.000 Einträge: 31 Durchläufe (16.129.000fach). Leider ist die praktische Verwendbarkeitsgrenze ca. 1.000.000 Einträge, da dann immerhin ca. 18 Minuten lang gerechnet wird!

Der Algorithmus ist freilich etwas komplexer als BubbleSort. Selbst, wenn drei oder viermal so viel Rechenzeit pro Schleifendurchlauf benötigt werden sollte, lohnt sich der Einsatz dieser Routine bereits ab etwa 35 Einträgen. Das Funktionsprinzip ist folgendes: Der aktuelle Wert, den es in die Liste (linksstehend) einzusortieren gilt, wird zuerst mit einem in der Mitte der bereits sortiert vorhandenen Liste verglichen. Je nachdem, ob dieser Vergleichswert zu groß oder klein ist, wird nun ein Viertel des Listenumfangs addiert oder subtrahiert, dann ein Achtel, Sechzehntel usw.. Selbstverständlich muß auch immer aufgerundet werden, da sonst Positionen in der Liste ausgelassen würden. Nach einer gewissen Höchstzahl an Vergleichen ist die richtige Stelle gefunden und der Wert wird in die Liste eingefügt. Insgesamt wird (solange eine gewisse Mindestgröße der Liste gegeben ist) sehr viel weniger Zeit für Vergleiche verpraßt als bei BubbleSort.

Im Falle der etwa 30.000 Zeilen auf einer Amiga mit einem MC68030 (50MHz) war die Sortierung (unter Verwendung von Assembler) nach sechs Sekunden erledigt; hätte ich BubbleSort benutzt, wären es 23 Stunden gewesen. Wie die Tabelle rechts unten zeigt, läßt sich der Rechenzeitaufwand (auf meinem Klapprechner) mit n²÷900.000.000 [s] überschlagen.

In Wikipedia gibt es derzeit (25.7.2009) keinen eigenen Eintrag für dieses Sortierverfahren. Man könnte es mit InsertionSort mit Binärsuche bezeichnen


LevenshteinAlgorithmus zur Berechnung der Editierdifferenz zweier Zeichenketten

Procedure.l Levenshtein(s.s, t.s)
  Protected m.l = Len(t), n.l = Len(s), i.l, j.l, &
    s_i.s, cost.l
  If n = 0
    If m = 0 : ProcedureReturn 0
    Else     : ProcedureReturn 1
    EndIf
  EndIf
  If m = 0 : ProcedureReturn 0 : EndIf
  Dim d.l(n, m)
  For i = 0 To n : d(i, 0) = i : Next i
  For j = 0 To m : d(0, j) = j : Next j
  For i = 1 To n
    s_i = Mid(s, i, 1)
    For j = 1 To m
      If s_i = Mid(t, j, 1) : cost = 0
      Else : cost = 1 : EndIf
      d(i,j) = Minimum(d(i−1,j)+1, d(i,j−1)+1, &
        d(i−1,j−1)+cost)
    Next j
  Next i
  ProcedureReturn d(n, m)
EndProcedure
Procedure.l Minimum(a.l, b.l, c.l)
  If b < a : a = b : EndIf
  If c < a : a = c : EndIf
  ProcedureReturn a
EndProcedure

Abb. 3: PureBasicQuelltext. Die EtZeichen am Zeilenende sind MehrzeilenVerbinder.

Um die Ähnlichkeit zweier Zeichenketten zu ermitteln gibt es viele Methoden.

Diese Berechnung ist dann notwendig, wenn beispielsweise der Unterschied zweier unabhängig voneinander editierter Text mit gemeinsamem Ursprung ermittelt werden soll.

Der LevenshteinAlgorithmus ist sehr einfach zu implementieren, aber nur für kleine Zeichenketten geeignet, da die Laufzeit O(m*n) beträgt. Liegt die Länge beider Zeichenketten noch unterhalb etwa 1kB, so kann man in vielen Fällen darauf zurückgreifen. Bei dieser Menge ist jedoch der Speicherverbrauch erheblich, denn er folgt ebenfalls m*n.

Für größeres Datenaufkommen müssen andere Wege gefunden werden, etwa der HirschbergAlgorithmus, der eine Linearisierung des LevenshteinAlgorithmus ist. Dieser ist allerdings erheblich aufwendiger zu implementieren.


Umfragen
Ist die Seite „Quelltextfragmente“ sachlich hilfreich?
Ist die Seite „Quelltextfragmente“ detailliert genug?
NoteMengeMenge
00,0%
10,0%
20,0%
30,0%
40,0%
50,0%
60,0%
70,0%
80,0%
90,0%
100,0%

läuft seit dem 23.10.2011 (4 Jahre 6 Monate)

noch keine Stimmen

noch keine Stimmen

NoteMengeMenge
00,0%
10,0%
20,0%
30,0%
40,0%
50,0%
60,0%
70,0%
80,0%
90,0%
100,0%

läuft seit dem 23.10.2011 (4 Jahre 6 Monate)

noch keine Stimmen

noch keine Stimmen

0, gar nicht
3
6
9
10, absolut
0, gar nicht
3
6
9
10, absolut
Stö­rer stim­men bit­te mit 0 ab!

Ihr Kommentar

Es gibt noch keinen Kommentar zu dieser Seite. Seien Sie der erste Kommentator!

vionlink comments von vision impress webdesign

Wortwolke

erreichten verbrauchen −1 anhand BubbleSort überschlagen PureBasic-Quelltext Größe verglichen Eintrag Editierdifferenz geeignet Minuten Ergebnis sukzessiver notwendig Fällen 23809 Teils Funktionsweise Liste drei Stelle nächste hergestellt eingefügt extremen Vergleich zuerst aufwendiger beider PureBasic 1992 entspricht Verwendbarkeitsgrenze Om*n Achtel Funktionsprinzip mittig immerhin Vergleiche Zeichenketten Netzpräsenz Alphabet erledigt; Falle Vergleichen aufgeschlagen günstigen erleichterten