So finden Sie das k-te größte Element in Java

1. Einleitung

In diesem Artikel werden verschiedene Lösungen vorgestellt, um das k- te größte Element in einer Folge eindeutiger Zahlen zu finden. Für unsere Beispiele verwenden wir eine Reihe von Ganzzahlen.

Wir werden auch über die durchschnittliche und Worst-Case-Zeitkomplexität jedes Algorithmus sprechen.

2. Lösungen

Lassen Sie uns nun einige mögliche Lösungen untersuchen - eine mit einer einfachen Sortierung und zwei mit dem von Quick Sort abgeleiteten Quick Select-Algorithmus.

2.1. Sortierung

Wenn wir über das Problem nachdenken, ist es vielleicht die naheliegendste Lösung, das Array zu sortieren .

Definieren wir die erforderlichen Schritte:

  • Sortieren Sie das Array in aufsteigender Reihenfolge
  • Da das letzte Element des Arrays das größte Element wäre, würde sich das k -größte Element am x- ten Index befinden, wobei x = Länge (Array) - k

Wie wir sehen können, ist die Lösung unkompliziert, erfordert jedoch das Sortieren des gesamten Arrays. Daher ist die zeitliche Komplexität O (n * logn) :

public int findKthLargestBySorting(Integer[] arr, int k) { Arrays.sort(arr); int targetIndex = arr.length - k; return arr[targetIndex]; }

Ein alternativer Ansatz besteht darin, das Array in absteigender Reihenfolge zu sortieren und das Element einfach auf den (k-1) -ten Index zurückzugeben:

public int findKthLargestBySortingDesc(Integer[] arr, int k) { Arrays.sort(arr, Collections.reverseOrder()); return arr[k-1]; }

2.2. Schnellauswahl

Dies kann als Optimierung des vorherigen Ansatzes angesehen werden. Hier wählen wir den QuickSort zum Sortieren aus. Bei der Analyse der Problemstellung stellen wir fest, dass wir nicht das gesamte Array sortieren müssen, sondern nur den Inhalt neu anordnen müssen, sodass das k- te Element des Arrays das k- te größte oder kleinste ist.

In QuickSort wählen wir ein Pivot-Element aus und verschieben es an die richtige Position. Wir partitionieren auch das Array darum herum. In QuickSelect besteht die Idee darin, an der Stelle anzuhalten, an der der Drehpunkt selbst das k -größte Element ist.

Wir können den Algorithmus weiter optimieren, wenn wir nicht sowohl für die linke als auch für die rechte Seite des Pivots wiederkehren. Wir müssen nur für einen von ihnen entsprechend der Position des Drehpunkts wiederholen.

Schauen wir uns die Grundideen des QuickSelect-Algorithmus an:

  • Wählen Sie ein Pivot-Element aus und partitionieren Sie das Array entsprechend
    • Wählen Sie das Element ganz rechts als Drehpunkt
    • Mischen Sie das Array neu, sodass das Pivot-Element an der richtigen Stelle platziert wird. Alle Elemente, die kleiner als der Pivot sind, befinden sich an niedrigeren Indizes, und Elemente, die größer als der Pivot sind, befinden sich an höheren Indizes als der Pivot
  • Wenn der Pivot am k- ten Element im Array platziert ist, beenden Sie den Prozess, da der Pivot das k -größte Element ist
  • Wenn die Schwenkposition größer als k ist, setzen Sie den Vorgang mit dem linken Subarray fort. Andernfalls wiederholen Sie den Vorgang mit dem rechten Subarray

Wir können generische Logik schreiben, mit der auch das k -kleinste Element gefunden werden kann. Wir definieren eine Methode findKthElementByQuickSelect (), die das k- te Element im sortierten Array zurückgibt .

Wenn wir das Array in aufsteigender Reihenfolge sortieren, ist das k- te Element eines Arrays das k -kleinste Element. Um das k- te größte Element zu finden, können wir k = Länge (Array) - k übergeben.

Lassen Sie uns diese Lösung implementieren:

public int findKthElementByQuickSelect(Integer[] arr, int left, int right, int k) { if (k >= 0 && k  k) { return findKthElementByQuickSelect(arr, left, pos - 1, k); } return findKthElementByQuickSelect(arr, pos + 1, right, k - pos + left - 1); } return 0; }

Implementieren wir nun die Partitionsmethode , bei der das am weitesten rechts stehende Element als Pivot ausgewählt, an den entsprechenden Index gesetzt und das Array so partitioniert wird, dass Elemente mit niedrigeren Indizes kleiner als das Pivot-Element sein sollten.

In ähnlicher Weise sind Elemente mit höheren Indizes größer als das Pivot-Element:

public int partition(Integer[] arr, int left, int right) { int pivot = arr[right]; Integer[] leftArr; Integer[] rightArr; leftArr = IntStream.range(left, right) .filter(i -> arr[i]  arr[i]) .boxed() .toArray(Integer[]::new); rightArr = IntStream.range(left, right) .filter(i -> arr[i] > pivot) .map(i -> arr[i]) .boxed() .toArray(Integer[]::new); int leftArraySize = leftArr.length; System.arraycopy(leftArr, 0, arr, left, leftArraySize); arr[leftArraySize+left] = pivot; System.arraycopy(rightArr, 0, arr, left + leftArraySize + 1, rightArr.length); return left + leftArraySize; }

Es gibt einen einfacheren, iterativen Ansatz, um die Partitionierung zu erreichen:

public int partitionIterative(Integer[] arr, int left, int right) { int pivot = arr[right], i = left; for (int j = left; j <= right - 1; j++) { if (arr[j] <= pivot) { swap(arr, i, j); i++; } } swap(arr, i, right); return i; } public void swap(Integer[] arr, int n1, int n2) { int temp = arr[n2]; arr[n2] = arr[n1]; arr[n1] = temp; }

Diese Lösung arbeitet im Durchschnitt in O (n) Zeit. Im schlimmsten Fall beträgt die zeitliche Komplexität jedoch O (n ^ 2) .

2.3. QuickSelect mit zufälliger Partition

Dieser Ansatz ist eine geringfügige Modifikation des vorherigen Ansatzes. Wenn das Array fast / vollständig sortiert ist und wir das Element ganz rechts als Drehpunkt auswählen, ist die Partition der linken und rechten Subarrays sehr ungleichmäßig.

Diese Methode schlägt vor , das anfängliche Pivot-Element auf zufällige Weise auszuwählen. Wir müssen die Partitionierungslogik jedoch nicht ändern.

Anstatt die Partition aufzurufen , rufen wir die randomPartition- Methode auf, die ein zufälliges Element auswählt und mit dem Element ganz rechts austauscht , bevor die Partitionsmethode endgültig aufgerufen wird.

Implementieren wir die randomPartition- Methode:

public int randomPartition(Integer arr[], int left, int right) { int n = right - left + 1; int pivot = (int) (Math.random()) * n; swap(arr, left + pivot, right); return partition(arr, left, right); }

Diese Lösung funktioniert in den meisten Fällen besser als der vorherige Fall.

Die erwartete zeitliche Komplexität von randomisiertem QuickSelect beträgt O (n) .

Die schlechteste Zeitkomplexität bleibt jedoch immer noch O (n ^ 2) .

3. Fazit

In diesem Artikel haben wir verschiedene Lösungen diskutiert, um das k- te größte (oder kleinste) Element in einem Array eindeutiger Zahlen zu finden. Die einfachste Lösung besteht darin, das Array zu sortieren und das k- te Element zurückzugeben. Diese Lösung hat eine zeitliche Komplexität von O (n * logn) .

Wir haben auch zwei Varianten von Quick Select besprochen. Dieser Algorithmus ist nicht einfach, hat aber im Durchschnitt eine zeitliche Komplexität von O (n) .

Wie immer finden Sie den vollständigen Code für den Algorithmus auf GitHub.