JavaでK番目に大きい要素を見つける方法

1前書き

この記事では、k番目に大きい要素を一連の固有番号で見つけるためのさまざまな解決策を紹介します。この例では整数の配列を使用します。

また、各アルゴリズムの平均および最悪の場合の時間の複雑さについても説明します。

2ソリューション

それでは、プレーンソートを使用する方法と、クイックソートから派生したクイック選択アルゴリズムを使用する方法の2つを考えてみましょう。

2.1. 並べ替え

私たちが問題について考えるとき、おそらく 頭に浮かぶ最も明白な解決策は 配列をソートすることです

必要な手順を定義しましょう。

  • 配列を昇順に並べ替える

  • 配列の最後の要素が最大の要素になるので、

_k 番目に大きい要素は xth インデックスになります。ここで、 x = length(array) - k_

ご覧のとおり、解決策は簡単ですが、配列全体をソートする必要があります。したがって、時間の複雑さは O(n ** logn) になります。

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

別の方法は、配列を降順にソートして、 __(k-1) __番目のインデックスの要素を返すことです。

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

2.2. クイック選択

これは、以前のアプローチの最適化と見なすことができます。ここでは、ソート用にhttps://www.geeksforgeeks.org/quick-sort/[QuickSort]を選択します。問題の文を分析すると、** 実際には配列全体をソートする必要はないことがわかります。配列の __k __番目の要素が最大または最小になるようにその内容を並べ替えるだけで済みます。

QuickSortでは、ピボット要素を選択して正しい位置に移動します。配列もその周りに分割します。 QuickSelectでは、ピボット自体が __k __番目に大きい要素になるところで停止するという考えです。

ピボットの左右両側で再帰しない場合は、アルゴリズムをさらに最適化できます。ピボットの位置に応じて、それらのうちの1つだけを繰り返す必要があります。

QuickSelectアルゴリズムの基本的な考え方を見てみましょう。

  • ピボット要素を選び、それに応じて配列を分割する

  • ** ピボットとして一番右の要素を選ぶ

  • ** ピボット要素が正しい位置に配置されるように配列を入れ替えます

place - ピボットより小さいすべての要素はより低いインデックスにあります ピボットよりも大きい要素は、ピボットよりも大きいインデックスに配置されます。 ピボット ** ピボットが配列の __k __番目の要素にある場合は、

pivotが __k 番目の最大要素であるため、process ** ピボット位置が k、__より大きい場合は、次のように処理を続けます。

それ以外の場合は、右側のサブアレイを使用してプロセスを繰り返し

__k 番目に小さい要素を見つけるためにも使える一般的な論理を書くことができます。ソート済み配列の k 番目の要素を返すメソッド findKthElementByQuickSelect()__を定義します。

配列を昇順にソートすると、配列の __k 番目の要素が k 番目に小さい要素になります。 k 番目に大きい要素を見つけるために、 k = length(Array) - kを渡すことができます。

このソリューションを実装しましょう。

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

それでは、一番右の要素をピボットとして選び、それを適切なインデックスに置き、より低いインデックスの要素がピボット要素より小さくなるように配列を分割する partition メソッドを実装しましょう。

同様に、インデックスの大きい要素はピボット要素よりも大きくなります。

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]< pivot)
      .map(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;
}

分割を実現するためのより単純で反復的なアプローチがあります。

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;
}

この解は、平均して O(n) 時間で機能します。ただし、最悪の場合、時間の複雑さは O(n ^ 2) になります。

2.3. ランダムパーティション付きQuickSelect

この方法は、以前の方法を少し変更したものです。配列がほぼ完全にソートされていて、右端の要素をピボットとして選択した場合、左右のサブ配列の分割は非常に不均一になります。

この方法では、** 最初のピボット要素をランダムに選択することが推奨されます。ただし、パーティション分割ロジックを変更する必要はありません。

partition を呼び出す代わりに、 randomPartition メソッドを呼び出します。これは、ランダムな要素を選択し、最後に partition メソッドを呼び出す前にそれを右端の要素と交換します。

randomPartition メソッドを実装しましょう。

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);
}

ほとんどの場合、この解決策は前のケースよりもうまく機能します。

ランダム化されたQuickSelectの予想される時間の複雑さは__O(n)です。

しかし、最悪の時間の複雑さは依然として O(n ^ 2) のままです。

3結論

この記事では、 __k 番目に大きい(または最小の)要素を一意の数値の配列で見つけるためのさまざまな解決策について説明しました。最も簡単な解決策は、配列をソートして k __th要素を返すことです。

この解は、 O(n ** logn) という複雑な時間を持ちます。

クイック選択の2つのバリエーションについても説明しました。このアルゴリズムは単純明快ではありませんが、平均的なケースでは O(n) という時間の複雑さがあります。

いつものように、このアルゴリズムの完全なコードはhttps://github.com/eugenp/tutorials/tree/master/algorithms-miscellaneous-1[GitHubへの追加]にあります。

前の投稿:Hibernateエンティティライフサイクル
次の投稿:Kotlinでリストをマップに変換する