C++
 Computer >> コンピューター >  >> プログラミング >> C++

C++で配列内の「次に大きい要素の次に小さい要素」を効率的に検索する方法

問題の概要

この問題では、n個の整数で構成される配列 arr[] が与えられます。各要素について、まず配列内でその要素より大きい「次に大きい要素(Next Greater)」を見つけ、続いてその大きい要素に対してさらに小さい「次に小さい要素(Next Smaller)」を求めるのがタスクです。

もし次に大きい要素、あるいはその次に小さい要素が配列内に存在しない場合は、-1 を返します。

入力例と出力例

入力

arr[] = {4, 2, 8, 3, 9, 1}

出力

{3, 3, 1, 1, -1, -1}

説明

まず各要素の「次に大きい要素」の配列を作ると {8, 8, 9, 9, -1, -1} となります。9 は配列の最大値、1 は最後の要素であるため、これらには次に大きい要素が存在しません。

次に、それぞれの「次に大きい要素」に対する「次に小さい要素」を求めると、結果は {3, 3, 1, 1, -1, -1} になります。

解法アプローチ①:素朴な方法(ブルートフォース)

最もシンプルな解法は、配列を反復処理し、各要素ごとに以下の手順を実行することです。

  • 配列の中から現在の要素より大きい「次に大きい要素」を探す。

  • その大きい要素以降の残りの配列から、それより小さい要素を探す。

この方法でも目的は達成できますが、二重ループが必要となるため、時間計算量は O(n²) となり、大きな配列では非効率です。

解法アプローチ②:スタックを使った効率的な方法

より優れた解法として、スタック要素のインデックスを活用する方法があります。

ここでは、nextGreater[] と nextSmaller[] という2つの配列を用意し、現在の要素の「次に大きい要素」「次に小さい要素」のインデックスをそれぞれ格納します。たとえば nextGreater[i] には、arr[nextGreater[i]] が arr[i] の次に大きい要素となるようなインデックスが保存されます。nextSmaller[] も同様です。

こうすることで、「次に大きい要素の次に小さい要素」は、nextGreater[i] の位置にある要素の次に小さい要素を調べることで取得できます。つまり、求める要素は arr[nextSmaller[nextGreater[i]]] となります。

スタックによる「次に大きい要素」の探索手順

  • 配列を末尾から先頭へ向かって走査します(i = n-1 から 0 へ)。

  • スタックが空でなく、スタックのトップが現在の要素以下である間、スタックからポップを繰り返します。より大きい要素が見つかるか、スタックが空になるまで続けます。

  • スタックが空になった場合 → 次に大きい要素は存在しないため、nextGreater[i] = -1 を格納します。

  • それ以外の場合 → 次に大きい要素はスタックのトップにあるため、nextGreater[i] = stack.top() を格納します。

  • 最後に現在の要素のインデックスをスタックにプッシュします。

比較条件を逆にすれば、同じ手法で「次に小さい要素」も求められます。両方のインデックス配列が完成すれば、それらを組み合わせて目的の要素を O(1) で参照できます。全体の時間計算量は O(n) に抑えられます。

C++での実装例

#include<bits/stdc++.h>
using namespace std;

// 各要素の「次に大きい要素」のインデックスを求める
void findNextGreater(int arr[], int n, int next[]) {
   stack<int> nextGreater;
   int i = n-1;
   while(i >= 0) {
      while (!nextGreater.empty() && arr[nextGreater.top()] <= arr[i])
         nextGreater.pop();
      if (!nextGreater.empty())
         next[i] = nextGreater.top();
      else
         next[i] = -1;
      nextGreater.push(i);
      i--;
   }
}

// 各要素の「次に小さい要素」のインデックスを求める
void findNextSmaller(int arr[], int n, int next[]) {
   stack<int> nextSmaller;
   int i = n-1;
   while(i >= 0){
      while (!nextSmaller.empty() && arr[nextSmaller.top()] >= arr[i])
         nextSmaller.pop();
      if (!nextSmaller.empty())
         next[i] = nextSmaller.top();
      else
         next[i] = -1;
      nextSmaller.push(i);
      i--;
   }
}

// 「次に大きい要素の次に小さい要素」を出力する
void findNextSmallerofNextGreaterElemenetArray(int arr[], int n) {
   int nextGreaterIndex[n];
   int nextSmallerIndex[n];
   findNextGreater(arr, n, nextGreaterIndex);
   findNextSmaller(arr, n, nextSmallerIndex);
   for (int i=0; i<n; i++){
      if (nextGreaterIndex[i] != -1 && nextSmallerIndex[nextGreaterIndex[i]] != -1)
         cout<<arr[nextSmallerIndex[nextGreaterIndex[i]]]<<"\t";
      else
         cout<<"-1"<<"\t";
   }
}

int main(){
   int arr[] = {4, 2, 8, 3, 9, 1};
   int n = sizeof(arr)/sizeof(arr[0]);
   cout<<"配列の各要素における「次に大きい要素の次に小さい要素」は ";
   findNextSmallerofNextGreaterElemenetArray(arr, n);
   return 0;
}

実行結果

配列の各要素における「次に大きい要素の次に小さい要素」は 3 3 1 1 -1 -1

まとめ

スタックとインデックス管理を組み合わせることで、一見 O(n²) かかりそうな問題を O(n) の線形時間で解くことができます。「次に大きい要素」「次に小さい要素」を求めるスタックベースのテクニックは、配列に関する多くの応用問題で役立つ重要なパターンなので、ぜひマスターしておきましょう。

  1. C++で配列内の各要素のサーパッサー(Surpasser)の数を求めるアルゴリズム

    ある配列Aが与えられたとき、各要素の「サーパッサー(surpasser)」の数を求める問題を考えてみましょう。サーパッサーとは、現在注目している要素よりも右側に存在する、その要素より大きい値のことです。 例えば、A = {2, 7, 5, 3, 0, 8, 1} という配列の場合、サーパッサーの数は {4, 1, 1, 1, 2, 0, 0} となります。これは、先頭の「2」の右側には「7・5・3・8」という4つの大きな値が存在するためです。その他の要素についても同じルールで数えていきます。 アルゴリズムの考え方 解法は非常にシンプルです。2重のループを使用し、外側のループで各要素を順に取り上

  2. C++で配列の最大要素とその位置を見つける方法

    配列の最大要素とは配列には複数の要素が格納されており、その中で他のすべての要素よりも大きい値を持つものが「最大要素」です。具体例51724上記の配列の場合、最大要素は7であり、インデックス2の位置に存在します。それでは、配列の最大要素を求めるC++プログラムを見ていきましょう。サンプルコード#include <iostream> using namespace std; int main() { int a[] = {4, 9, 1, 3, 8}; int largest, i, pos; largest = a[0]; for(i=1; i<