Suchalgorithmen in Python

Einführung

Die Suche nach Daten, die in verschiedenen Datenstrukturen gespeichert sind, ist ein entscheidender Teil so ziemlich jeder einzelnen Anwendung.

Es gibt viele verschiedene Algorithmen, die bei der Suche verwendet werden können, und jeder hat unterschiedliche Implementierungen und stützt sich auf verschiedene Datenstrukturen, um die Aufgabe zu erledigen.

Die Fähigkeit, einen bestimmten Algorithmus für eine bestimmte Aufgabe auszuwählen, ist eine Schlüsselqualifikation für Entwickler und kann den Unterschied zwischen einer schnellen, zuverlässigen und stabilen Anwendung und einer Anwendung bedeuten, die bei einer einfachen Anfrage zusammenbricht.

  • Mitgliedsoperatoren
  • Lineare Suche
  • Binäre Suche
  • Sprungsuche
  • Fibonacci-Suche
  • Exponentialsuche
  • Interpolationssuche

Membership-Operatoren

Algorithmen entwickeln sich und werden im Laufe der Zeit optimiert, da sie sich ständig weiterentwickeln und die effizientesten Lösungen für zugrunde liegende Probleme in verschiedenen Bereichen finden müssen.

Eines der häufigsten Probleme im Bereich der Informatik ist das Durchsuchen einer Sammlung und die Feststellung, ob ein bestimmtes Objekt in der Sammlung vorhanden ist oder nicht.

Nahezu jede Programmiersprache hat ihre eigene Implementierung eines grundlegenden Suchalgorithmus, gewöhnlich als Funktion, die einen Boolean Wert von True oder False zurückgibt, wenn ein Objekt in einer gegebenen Sammlung von Objekten gefunden wird.

In Python ist der einfachste Weg, nach einem Objekt zu suchen, die Verwendung von Zugehörigkeitsoperatoren – so genannt, weil sie uns erlauben, festzustellen, ob ein gegebenes Objekt ein Mitglied in einer Sammlung ist.

Diese Operatoren können mit jeder iterierbaren Datenstruktur in Python verwendet werden, einschließlich Strings, Listen und Tuples.

  • in – Gibt True zurück, wenn das gegebene Element ein Teil der Struktur ist.
  • not in – Gibt True zurück, wenn das angegebene Element nicht Teil der Struktur ist.
>>> 'apple' in True>>> 't' in 'stackabuse'True>>> 'q' in 'stackabuse'False>>> 'q' not in 'stackabuse'True

Mitgliedsoperatoren reichen aus, wenn wir nur herausfinden müssen, ob eine Teilzeichenkette in einer gegebenen Zeichenkette vorhanden ist, oder feststellen müssen, ob sich zwei Zeichenketten, Listen oder Tupel in Bezug auf die darin enthaltenen Objekte überschneiden.

In den meisten Fällen benötigen wir die Position des Elements in der Sequenz, zusätzlich zu der Feststellung, ob es existiert oder nicht; Zugehörigkeitsoperatoren erfüllen diese Anforderung nicht.

Es gibt viele Suchalgorithmen, die nicht von eingebauten Operatoren abhängen und verwendet werden können, um schneller und/oder effizienter nach Werten zu suchen. Außerdem können sie mehr Informationen liefern, wie z.B. die Position des Elements in der Sammlung, anstatt nur seine Existenz zu bestimmen.

Lineare Suche

Die lineare Suche ist einer der einfachsten Suchalgorithmen und am leichtesten zu verstehen. Wir können sie als eine erweiterte Version unserer eigenen Implementierung des in-Operators von Python betrachten.

Der Algorithmus besteht darin, über ein Array zu iterieren und den Index des ersten Vorkommens eines Elements zurückzugeben, sobald es gefunden wurde:

def LinearSearch(lys, element): for i in range (len(lys)): if lys == element: return i return -1

Wenn wir also die Funktion zum Berechnen verwenden:

>>> print(LinearSearch(, 2))

Wenn wir den Code ausführen, erhalten wir:

1

Dies ist der Index des ersten Vorkommens des gesuchten Elements – wobei zu beachten ist, dass Python-Indizes auf 0 basieren.

Die Zeitkomplexität der linearen Suche ist O(n), was bedeutet, dass die Ausführungszeit mit der Anzahl der Elemente in unserer Eingabeliste lys steigt.

Die lineare Suche wird in der Praxis nicht oft verwendet, da die gleiche Effizienz durch die Verwendung eingebauter Methoden oder vorhandener Operatoren erreicht werden kann und sie nicht so schnell und effizient ist wie andere Suchalgorithmen.

Die lineare Suche eignet sich gut, wenn das erste Vorkommen eines Elements in einer unsortierten Sammlung gefunden werden muss, da sie im Gegensatz zu den meisten anderen Suchalgorithmen nicht voraussetzt, dass eine Sammlung sortiert ist, bevor die Suche beginnt.

Binäre Suche

Die binäre Suche folgt einer “Divide et conquer”-Methode. Sie ist schneller als die lineare Suche, erfordert aber, dass das Feld sortiert ist, bevor der Algorithmus ausgeführt wird.

Angenommen, wir suchen nach einem Wert val in einem sortierten Feld, vergleicht der Algorithmus val mit dem Wert des mittleren Elements des Feldes, das wir mid nennen.

  • Wenn mid das gesuchte Element ist (im besten Fall), geben wir seinen Index zurück.
  • Wenn nicht, ermitteln wir, auf welcher Seite von mid val wahrscheinlicher ist, je nachdem, ob val kleiner oder größer als mid ist, und verwerfen die andere Seite des Arrays.
  • Wir folgen dann rekursiv oder iterativ den gleichen Schritten, wählen einen neuen Wert für mid, vergleichen ihn mit val und verwerfen die Hälfte der möglichen Übereinstimmungen in jeder Iteration des Algorithmus.

Der binäre Suchalgorithmus kann entweder rekursiv oder iterativ geschrieben werden. Die Rekursion ist in Python im Allgemeinen langsamer, weil sie die Zuweisung neuer Stackframes erfordert.

Da ein guter Suchalgorithmus so schnell und genau wie möglich sein sollte, betrachten wir die iterative Implementierung der binären Suche:

Wenn wir die Funktion verwenden, um zu berechnen:

>>> BinarySearch(, 20)

Wir erhalten das Ergebnis:

1

Das ist der Index des Wertes, nach dem wir suchen.

Die Aktion, die der Algorithmus als nächstes in jeder Iteration durchführt, ist eine von mehreren Möglichkeiten:

  • Rückgabe des Index des aktuellen Elements
  • Durchsuchen der linken Hälfte des Arrays
  • Durchsuchen der rechten Hälfte des Arrays

Wir können nur eine Möglichkeit pro Iteration wählen, und unser Pool möglicher Übereinstimmungen wird bei jeder Iteration durch zwei geteilt. Das macht die Zeitkomplexität der binären Suche O(log n).

Ein Nachteil der binären Suche ist, dass sie bei mehrfachem Vorkommen eines Elements im Array nicht den Index des ersten Elements zurückgibt, sondern den Index des Elements, das der Mitte am nächsten liegt:

>>> print(BinarySearch(, 4))

Wenn man diesen Code ausführt, erhält man den Index des mittleren Elements:

1

Im Vergleich dazu würde eine lineare Suche im gleichen Array folgendes ergeben:

0

Das ist der Index des ersten Elements. Wir können jedoch nicht kategorisch sagen, dass die binäre Suche nicht funktioniert, wenn ein Array zweimal dasselbe Element enthält – sie kann genau wie die lineare Suche funktionieren und in einigen Fällen das erste Vorkommen des Elements zurückgeben.

Wenn wir die binäre Suche beispielsweise auf dem Array durchführen und nach 4 suchen, würden wir 3 als Ergebnis erhalten.

Die binäre Suche wird in der Praxis häufig verwendet, da sie im Vergleich zur linearen Suche effizient und schnell ist. Sie hat jedoch einige Schwächen, wie z.B. die Abhängigkeit vom //-Operator. Es gibt viele andere Divide-and-Conquer-Suchalgorithmen, die von der binären Suche abgeleitet sind. Im Folgenden werden einige von ihnen untersucht.

Sprungsuche

Die Sprungsuche ähnelt der binären Suche insofern, als sie mit einem sortierten Array arbeitet und einen ähnlichen Divide-and-Conquer-Ansatz für die Suche verwendet.

Es kann als eine Verbesserung des linearen Suchalgorithmus eingestuft werden, da es von der linearen Suche abhängt, um den tatsächlichen Vergleich bei der Suche nach einem Wert durchzuführen.

Anstatt die Elemente eines sortierten Arrays inkrementell zu durchsuchen, suchen wir in Sprüngen. Wenn wir also in unserer Eingabeliste lys eine Sprunggröße von jump haben, wird unser Algorithmus Elemente in der Reihenfolge lys, lys, lys, lys und so weiter berücksichtigen.

Bei jedem Sprung speichern wir den vorhergehenden Wert, den wir betrachtet haben, und seinen Index. Wenn wir eine Menge von Werten finden, bei denen lys<Element<lys ist, führen wir eine lineare Suche mit lys als dem am weitesten links stehenden Element und lys als dem am weitesten rechts stehenden Element in unserer Suchmenge durch:

Da dies ein komplexer Algorithmus ist, betrachten wir die schrittweise Berechnung der Sprungsuche mit dieser Eingabe:

>>> print(JumpSearch(, 5))
  • Die Sprungsuche würde zunächst die Sprunggröße durch Berechnung von math.sqrt(len(lys)) bestimmen. Da wir 9 Elemente haben, wäre die Sprunggröße √9 = 3.
  • Als nächstes berechnen wir den Wert der Variablen right, der das Minimum der Länge des Arrays minus 1 ist, oder den Wert von left+jump, der in unserem Fall 0+3= 3 wäre. Da 3 kleiner als 8 ist, verwenden wir 3 als Wert von right.
  • Nun überprüfen wir, ob unser Suchelement, 5, zwischen lys und lys liegt. Da 5 nicht zwischen 1 und 4 liegt, gehen wir weiter.
  • Als Nächstes führen wir die Berechnungen erneut durch und prüfen, ob unser Suchelement zwischen lys und lys liegt, wobei 6 3+Sprung ist. Da 5 zwischen 4 und 7 liegt, führen wir eine lineare Suche auf den Elementen zwischen lys und lys durch und erhalten den Index unseres Elements als:
4

Die Zeitkomplexität der Sprungsuche ist O(√n), wobei √n die Sprunggröße und n die Länge der Liste ist, womit die Sprungsuche in Bezug auf die Effizienz zwischen der linearen Suche und dem binären Suchalgorithmus liegt.

Der wichtigste Vorteil der Sprungsuche im Vergleich zur binären Suche ist, dass sie nicht auf den Divisionsoperator (/) angewiesen ist.

In den meisten CPUs ist die Verwendung des Divisionsoperators im Vergleich zu anderen grundlegenden arithmetischen Operationen (Addition, Subtraktion und Multiplikation) kostspielig, da die Implementierung des Divisionsalgorithmus iterativ ist.

Die Kosten sind an sich sehr gering, aber wenn die Anzahl der zu durchsuchenden Elemente sehr groß ist und die Anzahl der durchzuführenden Divisionsoperationen steigt, können sich die Kosten schrittweise erhöhen. Daher ist die Sprungsuche besser als die binäre Suche, wenn es eine große Anzahl von Elementen in einem System gibt, in dem selbst eine kleine Geschwindigkeitssteigerung von Bedeutung ist.

Um die Sprungsuche schneller zu machen, könnte man die binäre Suche oder eine andere interne Sprungsuche verwenden, um die Blöcke zu durchsuchen, anstatt sich auf die viel langsamere lineare Suche zu verlassen.

Fibonacci-Suche

Die Fibonacci-Suche ist ein weiterer Divide-and-Conquer-Algorithmus, der Ähnlichkeiten mit der binären Suche und der Sprungsuche aufweist. Er heißt so, weil er Fibonacci-Zahlen verwendet, um die Blockgröße oder den Suchbereich in jedem Schritt zu berechnen.

Die Fibonacci-Zahlen beginnen mit Null und folgen dem Muster 0, 1, 1, 2, 3, 5, 8, 13, 21…, wobei jedes Element die Addition der beiden Zahlen ist, die ihm unmittelbar vorausgehen.

Der Algorithmus arbeitet mit drei Fibonacci-Zahlen auf einmal. Nennen wir die drei Zahlen fibM, fibM_minus_1 und fibM_minus_2, wobei fibM_minus_1 und fibM_minus_2 die beiden Zahlen sind, die in der Folge unmittelbar vor fibM stehen:

fibM = fibM_minus_1 + fibM_minus_2

Wir initialisieren die Werte auf 0, 1 und 1 oder die ersten drei Zahlen in der Fibonacci-Folge, um einen Indexfehler zu vermeiden, wenn unser Suchfeld lys nur eine sehr geringe Anzahl von Elementen enthält.

Dann wählen wir die kleinste Zahl der Fibonacci-Folge, die größer oder gleich der Anzahl der Elemente in unserem Suchfeld lys ist, als den Wert fibM und die beiden Fibonacci-Zahlen unmittelbar davor als die Werte fibM_minus_1 und fibM_minus_2. Solange das Array noch Elemente enthält und der Wert von fibM größer als eins ist, vergleichen wir:

  • val mit dem Wert des Blocks im Bereich bis fibM_minus_2 und geben bei Übereinstimmung den Index des Elements zurück.
  • Ist der Wert größer als das Element, das wir gerade betrachten, verschieben wir die Werte von fibM, fibM_minus_1 und fibM_minus_2 in der Fibonacci-Sequenz um zwei Schritte nach unten und setzen den Index auf den Index des Elements zurück.
  • Ist der Wert kleiner als das Element, das wir gerade betrachten, verschieben wir die Werte von fibM, fibM_minus_1 und fibM_minus_2 in der Fibonacci-Sequenz um einen Schritt nach unten.

Schauen wir uns die Python-Implementierung dieses Algorithmus an:

Wenn wir die Funktion FibonacciSearch verwenden, um zu berechnen:

>>> print(FibonacciSearch(, 6))

Schauen wir uns den schrittweisen Ablauf dieser Suche an:

  • Bestimmen der kleinsten Fibonacci-Zahl, die größer oder gleich der Länge der Liste fibM ist; in diesem Fall ist die kleinste Fibonacci-Zahl, die unsere Anforderungen erfüllt, 13.
  • Die Werte würden wie folgt zugeordnet:
    • fibM = 13
    • fibM_minus_1 = 8
    • fibM_minus_2 = 5
    • index = -1
  • Als nächstes prüfen wir das Element lys, wobei 4 das Minimum von -1+5 ist. Da der Wert von lys 5 ist, was kleiner ist als der gesuchte Wert, verschieben wir die Fibonacci-Zahlen in der Folge um einen Schritt nach unten, so dass die Werte:
    • fibM = 8
    • fibM_minus_1 = 5
    • fibM_minus_2 = 3
    • index = 4
  • Als nächstes überprüfen wir das Element lys, wobei 7 das Minimum von 4+3 ist. Da der Wert von lys 8 ist, was größer ist als der gesuchte Wert, verschieben wir die Fibonacci-Zahlen in der Folge um zwei Schritte nach unten.
    • fibM = 3
    • fibM_minus_1 = 2
    • fibM_minus_2 = 1
    • index = 4
  • Nun prüfen wir das Element lys, wobei 5 das Minimum von 4+1 ist. Der Wert von lys ist 6, das ist der Wert, nach dem wir suchen!

Das Ergebnis ist wie erwartet:

5

Die Zeitkomplexität für die Fibonacci-Suche ist O(log n); die gleiche wie die binäre Suche. Das bedeutet, dass der Algorithmus in den meisten Fällen schneller ist als die lineare Suche und die Sprungsuche.

Die Fibonacci-Suche kann verwendet werden, wenn eine sehr große Anzahl von Elementen durchsucht werden muss und die Ineffizienz, die mit der Verwendung eines Algorithmus verbunden ist, der auf dem Divisionsoperator beruht, verringert werden soll.

Ein zusätzlicher Vorteil der Fibonacci-Suche besteht darin, dass sie mit Eingabefeldern umgehen kann, die zu groß sind, um im CPU-Cache oder im RAM gespeichert zu werden, da sie die Elemente in zunehmenden Schritten und nicht in einer festen Größe durchsucht.

Exponentialsuche

Die Exponentialsuche ist ein weiterer Suchalgorithmus, der in Python recht einfach implementiert werden kann, verglichen mit der Sprungsuche und der Fibonacci-Suche, die beide ein wenig komplex sind. Er ist auch unter den Namen galoppierende Suche, Verdopplungssuche und Struzik-Suche bekannt.

Exponentielle Suche hängt von der binären Suche ab, um den endgültigen Vergleich der Werte durchzuführen. Der Algorithmus funktioniert folgendermaßen:

  • Bestimmen des Bereichs, in dem sich das gesuchte Element wahrscheinlich befindet
  • Binärsuche für den Bereich verwenden, um den genauen Index des Elements zu finden

Die Python-Implementierung des exponentiellen Suchalgorithmus ist:

Wenn wir die Funktion verwenden, um den Wert von zu finden:

>>> print(ExponentialSearch(,3))

Der Algorithmus funktioniert folgendermaßen:

  • Überprüfen, ob das erste Element in der Liste mit dem gesuchten Wert übereinstimmt – da lys 1 ist und wir nach 3 suchen, setzen wir den Index auf 1 und machen weiter.
  • Durchlaufen aller Elemente in der Liste, und während das Element an der Position des Index kleiner oder gleich unserem Wert ist, wird der Wert von index in Vielfachen von zwei exponentiell erhöht:
    • index = 1, lys ist 2, was kleiner als 3 ist, also wird der Index mit 2 multipliziert und auf 2 gesetzt.
    • index = 2, lysist 3, was gleich 3 ist, also wird der Index mit 2 multipliziert und auf 4 gesetzt.
    • Index = 4, lysist 5, was größer als 3 ist; die Schleife wird an dieser Stelle abgebrochen.
  • Dann wird eine binäre Suche durchgeführt, indem die Liste zerlegt wird; arr. In Python bedeutet dies, dass die Teilliste alle Elemente bis zum 4. Element enthält, so dass wir eigentlich:
>>> BinarySearch(, 3)

aufrufen, was zurückgeben würde:

2

Das ist der Index des Elements, das wir sowohl in der ursprünglichen Liste als auch in der zerteilten Liste suchen, die wir an den binären Suchalgorithmus weitergeben.

Die exponentielle Suche läuft in O(log i) Zeit, wobei i der Index des gesuchten Elements ist. Im schlimmsten Fall ist die Zeitkomplexität O(log n), wenn das letzte Element das gesuchte ist (n ist die Länge des Arrays).

Die exponentielle Suche funktioniert besser als die binäre Suche, wenn das gesuchte Element näher am Anfang des Arrays liegt. In der Praxis verwenden wir die exponentielle Suche, weil sie einer der effizientesten Suchalgorithmen für unbeschränkte oder unendliche Arrays ist.

Interpolationssuche

Die Interpolationssuche ist ein weiterer Divide-and-Conquer-Algorithmus, ähnlich der binären Suche. Im Gegensatz zur binären Suche beginnt die Suche nicht immer in der Mitte. Die Interpolationssuche berechnet die wahrscheinliche Position des gesuchten Elements mit Hilfe der Formel:

index = low + )*(high-low) / (lys-lys)]

Dabei sind die Variablen:

  • lys – unser Eingabe-Array
  • val – das gesuchte Element
  • index – der wahrscheinliche Index des gesuchten Elements. Dieser Wert ist höher, wenn val näher am Element am Ende des Arrays liegt (lys), und niedriger, wenn val näher am Element am Anfang des Arrays liegt (lys)
  • low – der Anfangsindex des Arrays
  • high – der letzte Index des Arrays

Der Algorithmus sucht durch Berechnung des Wertes von index:

  • Wenn eine Übereinstimmung gefunden wird (wenn lys == val), wird der Index zurückgegeben
  • Wenn der Wert von val kleiner als lys ist, wird der Wert für den Index unter Verwendung der Formel für das linke Sub-Array neu berechnet
  • Wenn der Wert von val größer als lys ist, wird der Wert für den Index mit Hilfe der Formel für das rechte Unterarray neu berechnet

Lassen Sie uns fortfahren und die Interpolationssuche mit Python implementieren:

Wenn wir die Funktion zum Berechnen verwenden:

>>> print(InterpolationSearch(, 6))

Unsere Ausgangswerte wären:

  • val = 6,
  • low = 0,
  • high = 7,
  • lys = 1,
  • lys = 8,
  • index = 0 + = 5

Da lys 6 ist, was der gesuchte Wert ist, beenden wir die Ausführung und geben das Ergebnis zurück:

5

Wenn wir eine große Anzahl von Elementen haben und unser Index nicht in einer Iteration berechnet werden kann, berechnen wir die Werte für den Index immer wieder neu, nachdem wir die Werte für hoch und niedrig in unserer Formel angepasst haben.

Die Zeitkomplexität der Interpolationssuche ist O(log log n), wenn die Werte gleichmäßig verteilt sind. Wenn die Werte nicht gleichmäßig verteilt sind, ist die Zeitkomplexität im schlimmsten Fall O(n), genau wie bei der linearen Suche.

Die Interpolationssuche funktioniert am besten bei gleichmäßig verteilten, sortierten Arrays. Während die binäre Suche in der Mitte beginnt und immer in zwei Teile teilt, berechnet die Interpolationssuche die wahrscheinliche Position des Elements und überprüft den Index, so dass es wahrscheinlicher ist, das Element in einer geringeren Anzahl von Iterationen zu finden.

Warum Python für die Suche verwenden?

Python ist im Vergleich zu älteren Programmiersprachen wie Java, Fortran, C, C++ usw. sehr gut lesbar und effizient. Ein entscheidender Vorteil der Verwendung von Python für die Implementierung von Suchalgorithmen ist, dass man sich keine Gedanken über Casting oder explizite Typisierung machen muss.

In Python funktionieren die meisten der besprochenen Suchalgorithmen genauso gut, wenn wir nach einem String suchen. Denken Sie daran, dass wir Änderungen am Code für Algorithmen vornehmen müssen, die das Suchelement für numerische Berechnungen verwenden, wie z. B. der Interpolationssuchalgorithmus.

Python ist auch ein guter Ausgangspunkt, wenn Sie die Leistung verschiedener Suchalgorithmen für Ihren Datensatz vergleichen möchten; die Erstellung eines Prototyps in Python ist einfacher und schneller, da Sie mit weniger Codezeilen mehr erreichen können.

Um die Leistung unserer implementierten Suchalgorithmen mit einem Datensatz zu vergleichen, können wir die Zeitbibliothek in Python verwenden:

import timestart = time.time()# call the function hereend = time.time()print(start-end)

Schlussfolgerung

Es gibt viele Möglichkeiten, nach einem Element innerhalb einer Sammlung zu suchen. In diesem Artikel haben wir versucht, einige Suchalgorithmen und ihre Implementierungen in Python zu besprechen.

Die Wahl des zu verwendenden Algorithmus hängt von den Daten ab, die Sie durchsuchen müssen; Ihr Eingabe-Array, das wir in allen unseren Implementierungen lys genannt haben.

  • Wenn Sie ein unsortiertes Array durchsuchen oder das erste Vorkommen einer Suchvariablen finden wollen, ist die beste Option die lineare Suche.
  • Wenn Sie ein sortiertes Array durchsuchen wollen, gibt es viele Möglichkeiten, von denen die einfachste und schnellste Methode die binäre Suche ist.
  • Wenn Sie ein sortiertes Array durchsuchen wollen, ohne den Divisionsoperator zu verwenden, können Sie entweder die Sprungsuche oder die Fibonaccisuche verwenden.
  • Wenn Sie wissen, dass das gesuchte Element wahrscheinlich näher am Anfang des Arrays liegt, können Sie die Exponentialsuche verwenden.
  • Wenn Ihr sortiertes Array auch gleichmäßig verteilt ist, wäre der schnellste und effizienteste Suchalgorithmus die Interpolationssuche.

Wenn Sie sich nicht sicher sind, welchen Algorithmus Sie bei einem sortierten Array verwenden sollen, probieren Sie einfach alle Algorithmen zusammen mit der Zeitbibliothek von Python aus und wählen Sie denjenigen, der bei Ihrem Datensatz am besten funktioniert.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.