* Achtung Modem‐Nutzer: >200 kB!
** Achtung DSL‐Nutzer: >1 MB!
Alle Dateien, CSS=Standard, keine Silbentrennung
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: PureBasic‐Quelltext. Die Et‐Zeichen bei den MoveMemory()‐Funktionen sind Mehrzeilen‐Verbinder.
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.
7 | 8 | 12 | 27 | 35 | 77 | 33 | 18 | 43 | 4 |
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.
7 | 8 | 12 | ?? | 27 | 35 | 77 | 18 | 43 | 4 |
Der Vergleich ergibt −1. Die Schrittweite (2) wird auf die Suchposition aufgeschlagen (nun 5) und die Schrittweite auf 1 halbiert.
7 | 8 | 12 | 27 | 35 | ?? | 77 | 18 | 43 | 4 |
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.
7 | 8 | 12 | 27 | ?? | 35 | 77 | 18 | 43 | 4 |
Da das Ergebnis 1 lautet, wir aber die Anzahl der notwendigen Durchläufe erreichten, wird nun unsere 33 hier eingefügt.
7 | 8 | 12 | 27 | 33 | 35 | 77 | 18 | 43 | 4 |
Der sortierte Teil der Liste ist nun 7 Einträge lang. Wir kommen zum nächsten fraglichen Wert, der 18.
7 | 8 | 12 | 27 | 33 | 35 | 77 | 18 | 43 | 4 |
Abb. 2: Funktionsweise anhand eines kleinen Beispiels.
Listengröße | Zeit in s | Zeit in s |
---|---|---|
1.000 | 0,08 | 0,02 |
3.000 | 0,22 | 0,08 |
10.000 | 0,97 | 0,13 |
30.000 | 4,16 | 1,04 |
100.000 | 25,04 | 11,08 |
300.000 | 161,4 | 98,65 |
1.000.000 | 1460 | ca.1100 |
Tab. 1: Tabelle mit Rechenzeiten bei Verwendung von PureBasic 4.30 auf Windoof XP SP3 mit einem Intel Centrino (1,86 GHz) bei (mittig) Debug‐Modus 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 AD‐Wandler. Flash‐Wandler 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 (50 MHz) war die Sortierung (unter Verwendung von Assembler) nach sechs Sekunden erledigt; hätte ich BubbleSort benutzt, wären es 2–3 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
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: PureBasic‐Quelltext. Die Et‐Zeichen am Zeilenende sind Mehrzeilen‐Verbinder.
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 Levenshtein‐Algorithmus 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 1 kB, 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 Hirschberg‐Algorithmus, der eine Linearisierung des Levenshtein‐Algorithmus ist. Dieser ist allerdings erheblich aufwendiger zu implementieren.