cards-deck: 100-199_university::111-120_theoretic_cs::111_algorithms_datastructures

| Suchen in Mengen |

anchored to [[111.00_anchor]]


| Overview

Wir möchten im Folgenden die Suche von spezifischen, angefragten, Elementen in einer gewissen Menge betrachten.

| Suchen in geordneter Menge

Als einfachste Suche können wir zuerst die Suche eines Wertes x in einer Menge betrachten, wobei hier eine geordnete Menge ist . Dabei ist die Ordnung domain-spezifisch, also abhängig von dem Kriterium, nach welchem wir Sortieren wollen ( etwa größe einer Zahl, Alphabetisch etc).

[!Important] Grunddefinitionen zur Suche in geordneten Menge: Also Grundlage für die weiteren Betrachtungen der Suche definieren wir folgend:

  • ein Universum , welches eine lineare Ordnung aufweist. –> deckt dabei alle möglichen Einträge ab, die auftreten könnten
  • eine Teilmenge , welche wir als Schlüsselmenge definieren wollen
  • ein beliebiges zu suchendes Element ( also ein im Universum enthaltenes ELement)

Zur Suche und Arbeit in geordneten Mengen möchten wir jetzt ferner noch bestimmte Operationen definieren:

[!Definition] Grundlegende Operationen welche Operationen brauch es für eine Menge, die geordnet sein sollte? #card Wir definieren folgende Operationen in Abhängigkeit zu unserem Kontext:

Suche: -> schaut nach, ob sich Element in unsere Schlüsselmenge befindet ( könnte Bool zurückgeben )

Einfügen/Insert: -> fügt Eintrag in die Menge ein ( dabei muss die Sortierung beibehalten werden!) Streiche/remove: -> entfernt Eintrag aus Menge

-> findet das größte Element in der gegebenen Menge

Sortieren: -> sortiert die Menge gemäß der linearen Ordnung, die vom Universum vorgegeben ist! ^1704708938339

Es lassen sich jetzt bestimmte Grundprinzipien zur Suche von Elementen in einer geordneten Menge betrachten.

| lineare Suche

Die lineare Suche ist relativ simpel und straight forward in ihrer Implementation und Umsetzung

[!Definition] Definition Lineare Suche für eine Menge , die geordnet ist, wie können wir einen Eintrag finden? #card Wir nehmen folgend ein geordnetes Array an, welches wir jetzt auf bestimmte Inhalte durchsuchen möchten.

Sei hierbei jetzt der Suchindex, welcher immer bei 0 beginnt

Wir wollen jetzt die lineare Suche nach einem Inhalt einfach als PseudoCode darstellen:

while (a < S[next]){ 
  next += 1
} # inkrementiere also den Index so lange, bis wir am Index von unserem gesuchten Element angekommen sind.
if (a = S[next]) # Wert gefunden!
  return next --> also Index, den wir gefunden haben 
else 
  return null --> Eintrag nicht gefunden

Intuition wir traversieren also einfach vom anfang bis zum Ende, und wenn wir auf dem Weg auf unser Element stoßen, dann geben wir den entsprechenden Index zurück ^1704708938350

[!Important] Laufzeit der linearen Suche wie äußert sich jetzt die Laufzeit der linearen Suche? #card Wir können hier einfach die Best und Worst-Case Szenarien betrachten / vorstellen: –> Best-Case: gesuchtes Item ist an 0-ter Stelle ->

–> Worst-Case: gesuchtes Item nicht vorhanden oder am Ende der Menge ->

Es gibt ein ungefähres Mittel, welches suggeriert, dass: Bei Iterationen eine Laufzeit von hat

^1704708938358

| Binäre Suche |

Bei der linearen Suche nutzen wir nicht die Sortierung der Menge, die wir durchsuchen, aus und sind somit quasi genauso schnell, wie wenn wir in einer unsortierten Menge suchen würden.

Die Binäre Suche versucht das zu verbessern:

[!Definition] Vorgehen und Idee binärer Suche wie gehen wir bei binärer Suche vor? #card auch hier haben wir eine geordnete Menge ( als Array) vorliegen: . Weiterhin suchen wir wieder nach Eintrag in der Menge. Wir möchten jetzt neben der Variable -> die den aktuellen Zeiger in der Menge angibt, noch und definieren, welche als Abgrenzung des Suchbereiches dienen. -> Sie geben also an, wie groß der Raum unserer Menge ist, die wir betrachten und nach einem Eintrag durchsuchen möchten Wir definieren sie dabei folgend: Beschrieben als PseudoCode jetzt:

binary_search(A,n,T): # A is the set we look at, n the length of this set, T the queried item
left = 0 
right = n-1
while left <= right:
  m = floor(  ( L + R ) / 2 ) # position of middle elemnent, the one we are going to observe!
  if A[m] < T # so our element is larger --> to the right of it 
  	left =  m+1 ( we can discard the left side )
  else if A[m] > T # our element is smaller than the observed one 
  	right= m-1 ( we can discard the right side) 
  else:
  	return m # we found the correct item! 
#left without solution 
return failure

Intuition: Wir wollen prinzipiell in einer gewählten Menge die Mitte als Element betrachten und anschließend schauen, ob wir das richtige Element getroffen haben oder nicht. Wenn nicht, dann verkleinern wir den Bereich, in dem wir suchen ( dabei gehen wir entweder nach rechts ( gesuchtes Item ist größer als derzeitiges ) oder links -> wir könenn so schrittweise verkleinern und uns einer Antwort annähnern ( wenn sie existiert) ^1704708938364

[!Important] Laufzeit von binärer Suche wie gestaltet sie sich, wie können wir das beweisen? #card Prinzipiell können wir die Laufzeit folgend konstruieren: (Unter Anwendung des Mastertheorems können wir mit der Laufzeit resultieren!)

Intuition: Wir wissen, dass wir nicht alle betrachten werden, weil wir quasi in einem Interall immer nur ein Item proben und diesen Intervall danach verkleinern und genauer schauen ^1704708938370

| Interpolationssuche |

Wir möchten noch eine Suche definieren, die ähnlich der Suche im Telefonbuch sein kann.

[!Important] Intuition Interpolationssuche was möchten wir hier tun, wie wird unser nächstes Element gewählt? #card Prinzipiell wird hier einfach die Wahl des gewählten Elementes anders berechnet, sodass wir schätzen, wo sich das gesuchte ELement relativ zu den anderen Einträgen befinden kann. Genauer möchten wir schätzen, wo sich relativ zu dem Eintrag bei und -> also den beiden Grenzbereichen, befindet. Mit dieser Approximation können wir jetzt immer abschätzen, wo sich das Element befinden könnte und wir betrachten es. Auch hier wenden wir den weiteren Verlauf der binary search an –> also reduzieren den Bereich, den wir durchsuchen nach und nach. ^1704708938377

[!Definition] Laufzeit von Interpolationssuche wie könnte jetzt die Laufzeit aussehen? #card Die Laufzeit ist

  • im Mittel / Durchschnitt etwa:
  • Im Worst-Case etwa:

Wir möchten noch die Mittel-Laufzeit aufschlüsseln / betrachten: ^1704708938385

| Verbesserung Interpolationssuche:

Wir haben hier den Worst-Case von , weil wir da wieder alle Elemente betrachten würden, um einen Eintrag zu finden.

Idee: Wir können jetzt den Suchbereich mit jedem Schritt verkleinern, sodas wir der Lösung immer näher kommen und weiterhin bereits durchsuchte Bereiche nicht nochmal anschauen müssen.

Wir möchten dafür den Algorithmus etwas anpassen:

[!Definition] Algorithmus was wollen wir jetzt einführen, sodass unser Algorithmus schneller wird und seinen Suchbereich weiter verkleinert? #card wir möchten zwei neue Elemente in unserer Menge aufnehmen . Nach dem Prinzip möchten wir jetzt immer: iterieren, bis gefunden wurde oder wir unseren Suchbereich auf 0 reduziert.

Wir definieren die Bestimmung des nächsten Eintages nun etwas anders: und weiterhin dann Wir nehmen folgende Bedinungen auf, die prüfen, ob unser Item links oder rechts vom derzeitigen Bereich ist:

if a == S[next]
  fertig, found 
if ( a > S[next]) vergleiche jetzt S[next + sqrt(n)], S[next + 2* sqrt(n)] ... --> also immer weiter suchen 
vergleiche bis: a < S [ next + ( i -1) sqrt(n)]

^1704708938393

[!Definition] Laufzeit Berechnen: unter Anwendung der besseren Vergleichsoperation, wie könenn wir diese Laufzeit jetzt neu berechnen? #card Wir wenden jetzt also Schritte an, um das Ergebnis schneller finden zu können und weniger Bereiche zu betrachten ( wir überspringen ja viele Elemente). Es erfolgt damit eine Reduktion von auf Schritten.

Betrachten wir noch , die die mittlere Anzahl von Vergleichen in einer Iteration angibt.

Wir behaupten jetzt, dass und erhalten daraus dann: und weiter dann:

Also braucht die Interpolationssuche Vergleiche im Average ^1704708938401

Dadurch ergibt sich ein neuer Worst-Case!

[!Important] Interpolationssuche Laufzeit ( und Probleme / Lösungen) wie könnte man die resultierende Laufzeit noch verbessern? #card Der Worst-Case ist jetzt mit beschrieben.

Man könnte das ganze noch verbessern: binäre Suche und Interpolationssuche läuft parallel durch! dann würde man eine worst-case Laufzeit von erhalten

Wichtig: die Gleichverteilungsannahme ist dann bei der Analyse wichtig, weil damit die Menge von avg-Schritten anders sein kann ^1704708938409

| Mediansuche |

Betrachten wir jetzt noch ein Prinzip der Suche, was sich etwas an dem Prinzip von [[111.33_quick_sort]] orientiert.

Wir betrachten hierbei eine Menge und eine spezielle Zahl

Wir suchen jetzt: wobei dann: -> Also wir suchen ein Element, was so viele kleinere Zahlen hat, wie unser betrachtetes

[!Definition] Umsetzung von Mediansuche Wir gehen jetzt folgend vor, um diese Suche durchzuführen:

  1. Wir sortieren S und jetzt wählen wir das -te Element
  2. Dieser Aufruf ist folgend:
  3. Wir werden jetzt analog zu QuickSort dann diese Menge aufteilen –> wir teilen nach dem pivot-element in zwei Mengen auf:
  4. Wir werden aber nur einen Teil der Menge weiter aufteilen! (verfeinert durchsuchen!)
  5. Das heißt wir entscheiden uns anfangs für eine der beiden groß aufgestellten Mengen und danach werden wir nur noch in eine Richtung suchen und nach und nach verkleinern!
  6. Wir haben jetzt die Mengen:
  7. Wir wiederholen den Vorgang und suchen weiter in der neuen Partition

[!Important] PseudoCode für Random-Partition Wir betrachten jetzt noch den Code, der entsprechend eine Partition unserer Menge aufbaut:

Random-Select(A,p,ri)
  q = Random-Partition(A,p,r) // erstellen also eine random partitionierung der Menge und setzen diese _random-Partition_ gleich q!
  k = q - p+1
  if (i = k) then // found the correct partition of our structure !
  	return A[q]
  else if (i < k) then 
  	return Random-Select(A,p,q-1,i) //recursively calling to decrease the set to look at!
  else // 
  	return Random-Select(A,q+1,r,i-k)

| Laufzeit - Betrachtung |

Wir möchten jetzt die Laufzeit dieses Suchverfahrens betrachten.

Worst-Case: Im worst-case spalten wir unsere Partition so auf, dass wir sie mit jedem Schritt von n auf n-1 verringern –> wir werden also mit jedem Schritt die betrachtete Partition nur um ein einziges Element verringern!

Best-Case: Im Best case werden wir mit jeder Partitionierung immer genau die Hälfte der Menge betrachten und die andere Hälfte loswerden. Es folgt hierbei dann:

Average Case: Ähnlich, wie es bei Quicksort notwendig war den avg-case durch die Betrachtung der Normalverteilung der Elemente in einer Menge zu bestimmen, müssen wir auch hier so vorgehen, um eine Aussage treffen zu können:

Sei hierbei jetzt eine Zufallsvariable für die Laufzeit des Algorithmus. Die Funktion Random-Partition zerlegt hierbei die Menge immer mit einer Wahrscheinlichkeit von in neue, kleinerer Teilmengen mit der Größe ( also hier zwei Mengen!)

Für entsprechende definieren wir jetzt noch eine -Zufallsvaraible mit , also immer dann, wenn Elemente aufweist! Es folgt aus dieser Betrachtung dann, dass: –> weil wir das ja für solche Variablen betrachten!

[!Tip] Beobachtung zur Laufzeit: Wir sehen jetzt hier, dass monoton ist! Damit ist also die Laufzeit größer, wenn in einer großen Teilmenge auftritt, weil man es dann entsprechend länger suchen muss!

[!Warning] Behauptung für die Laufzeit Unter Konstruktion einer oberen Schranke können wir jetzt noch zeigen, dass dieser Algorithmus für ein konstante s sein wird!

Sei dafür: und weiterhin können wir dann einsetzen und folgend bestimmen:

Wir möchten jetzt noch unsere Behauptung bestätigten / widerlegen. Betrachte also nochmal die Behauptung:

[!Tip] Behauptung zur Laufzeit Wir gehen davon aus, dass whenever ist konstant

Wir können diese Umstand jetzt durch Anwendung von Induktion und dem vorherigen Einsetzen beweisen:

Beweis: Wir wissen, dass , wenn konstant ist. Wir wollen das jetzt zeigen:

| Deterministische Auswahl treffen |

In der vorherigen Betrachtung haben wir nur mit einer randomisierten Suche gearbeitet, also einfach beliebig Elemente als Median genommen, um dann das entsprechende Element zu finden.

Jetzt möchten wir eine deterministische Variante betrachten: <<–Motivation–>>: Wenn wir den Median schnell finden können, dann sollte das auch für das i-te größte Element funktionieren!

[!Definition] Definition der deterministischen Auswahl | Wir adaptieren / verändern unsere Auswahl Funktion jetzt noch ein wenig und konstruieren sie mit folgenden Schritten

  1. Teile Menge S in Gruppen der Größe
  2. In jeder dieser Gruppen bestimmen wir jetzt den Median!
  3. Rekursiv bestimmen wir jetzt noch den Median x der ganzen Mediane ( die wir zuvor berechnet haben!)
  4. Wir teilen die Menge ( Eingabemenge!) unter Anwendung des Gesamt-Medianes in zwei Gruppen und hierbei dann:
  5. Folgende Fallentscheidung folgt jetzt:
  6. Wenn jetzt: dann geben wir x zurück!
  7. Wenn stattdessen , dann geben wir rekursiv zurück: –> also eine kleinere Menge, die wir betrachten werden
  8. Und wenn das auch nicht eintritt, folgt jetzt noch: als der neue Aufruf, um rekursiv zu partitionieren

[!Important] Motivation dieser Aufteilung in Teile: Durch dies Aufteilung können wir jetzt halt ganz einfach approximieren, wie viele Mediane - die wir in den kleineren Untergruppen entnehmen / konstruieren - dann größer sein werden als das gesuchte ! Also der Mediane werden die Eigenschaft aufweisen

Diese Gruppen werden dann immer alle Elemente aufweisen, die in ihrere Struktur sind! Daraus folgt jetzt => Es gibt größere und somit (analog auch) kleinere Mediane, als der Referenzwert Daraus folgt jetzt: Denn folgend sehen wir:

Wir wollen diese Folgerung ferner unter der vorherigen Betrachtung evaluieren. Schauen wir also nochmal darauf:

[!Info] Beweisen der Behauptung Wir beweisen diese Behauptung jetzt durch Anwendung der vorherigen Ergebnisse: Wieder stimmt sie für ein konstantes und somit ist der IA gesetzt. Induktionsschritt:

| Laufzeit Mediansuche |

[!Important] Folgerung / Definition Wir können in einer ungeordneten Menge der Große das **i-**tgrößte Element in einer Zeit von finden!