【データ構造とアルゴリズムC++実装】 2.二分探索と単純再帰

元のビデオはZuo ChengyunのBステーションの指導です



1 二項対立

二分探索は、順序付けられた配列内の特定の要素を見つけるための検索アルゴリズムです。基本的な考え方は、配列を中央から分割し、対象の要素と中央の要素の大小関係を判断して、対象の要素が左半分にあるか右半分にあるかを判断することです。次に、ターゲット要素が見つかるか、ターゲット要素が存在しないと判断されるまで、対応するサブ配列で同じ操作を実行し続けます。

具体的な手順は次のとおりです。

  • 配列の左境界を左に設定し、右境界を右に設定します。
  • 中間位置のmid、つまりmid = (左+右) / 2を計算します。
  • ターゲット要素と中間要素のサイズ関係を比較します。
  • ターゲット要素が中間要素と等しい場合、ターゲット要素が検索され、そのインデックスが返されます。
  • 対象要素が中央の要素より小さい場合は、右側の境界を right = mid - 1 に更新し、左半分の二分探索を続行します。
  • 対象要素が中央の要素より大きい場合は、左側の境界を left = Mid + 1 に更新し、右半分で二分探索を続行します。
  • ターゲット要素が見つかるか、左側の境界が右側の境界よりも大きくなるまで、手順 2 と 3 を繰り返します。

時間計算量O(logN)。n は配列の長さです。検索範囲は毎回半分になるため、アルゴリズムは非常に効率的です。ただし、配列は順序付けされている必要があり、そうでないと検索に二分法を適用できません。

1.1有序配列内の特定の要素を検索する

基本的な考え方は、目的の要素が見つかるか、検索範囲が空になるまで、中間要素目的の要素の大小関係を比較して、検索範囲を半分に減らすことです。

例えば配列数が N=16 なので、ワーストケースは 4 回に分けられます( [ 8 ∣ 8 ] → [ 4 ∣ 4 ] → [ 2 ∣ 2 ] → [ 1 ∣ 1 ] ) ( [8 |8] \to [4|4] \to [2|2] \to [1|1] )([ 8∣8 ][ 4∣4 ][ 2∣2 ][ 1∣1 ])、および4 = log 2 16 4 = log_2164=ログ_ _216つまり、時間計算量はO ( log N ) O(logN)O (ログN ) _ _

/* 注意:题目保证数组不为空,且 n 大于等于 1 ,以下问题默认相同 */
int binarySearch(std::vector<int>& arr, int value)
{
    
    
    int left = 0;
    int right = arr.size() - 1;
    // 如果这里是 int right = arr.size() 的话,那么下面有两处地方需要修改,以保证一一对应:
    // 1、下面循环的条件则是 while(left < right)
    // 2、循环内当 array[middle] > value 的时候,right = middle

    while (left <= right)
    {
    
    
        int middle = left + ((right - left) >> 1);  // 不用right+left,避免int溢出,且更快
        if (array[middle] > value)
            right = middle - 1;
        else if (array[middle] < value)
            left = middle + 1;
        else
            return middle;
        // 可能会有读者认为刚开始时就要判断相等,但毕竟数组中不相等的情况更多
        // 如果每次循环都判断一下是否相等,将耗费时间
    }
    return -1;
}

left + ((right - left) >> 1) (right + left) / 2 の結果などに注意してください。ただし、int オーバーフローがなければ高速です。

1.2 順序付き配列内の特定の数値以上の左端の位置を見つける

アイデアは依然として二分法であり、特定の値を見つけて、目標値が見つかったら二分法をやめるのとは異なります


int nearLeftSearch(const std::vector<int>& arr, int target)
{
    
    
	int left = 0;
	int right = arr.size() - 1;
	int result = -1;
	
	while (left <= right)
	{
    
    
		int mid = left + ((right - left) >> 1);
		if (target <= arr[mid]){
    
     // 目标值小于等于mid,就要往左继续找
			result = mid;// 暂时记录下这个位置,因为左边可能全都比目标值小了,就已经找到了
			right = mid - 1;
		} else{
    
    		// target > arr[mid]
			left = mid + 1;
		}
	}
	return result;
}

1.3 順序付き配列内の特定の数値以下の右端の位置を見つける

  • 中央の要素がターゲット値より大きい場合は、ターゲット値が左半分にあるはずであることを意味するため、検索を左半分に絞り、右をmid - 1に更新します。
  • 中央の要素がターゲット値以下の場合は、ターゲット値が右半分または現在の位置にある必要があることを意味するため、結果を現在の中央のインデックス Mid に更新して、見つかった右端の位置を記録し、範囲を狭めます。検索範囲を右半分に、左を中央 + 1 に更新します。
int nearRightSearch(const std::vector<int>& arr, int target) 
{
    
    
    int left = 0;
    int right = arr.size() - 1;
    int result = -1;

    while (left <= right) {
    
    
        int mid = left + (right - left) / 2;

        if (target < arr[mid]) {
    
    
            right = mid - 1;
        } else {
    
    	// target >= arr[mid]
            result = mid;
            left = mid + 1;
        }
    }

    return result;
}

1.4 局所最小問題(二分法を使用した順序なし配列の場合)

配列 arr は順序付けされておらず、隣接する 2 つの数値が等しくなく、極小位置 (最小値) を見つけます時間計算量はO(N) よりも優れている必要があります。

無秩序は二分化することもできます。対象となる問題の一方の側に解決策が必要で、もう一方の側は問題ではない限り、二分化を使用できます。

1. まず配列の 2 つの境界を判断します

  • 左境界 arr[0] < arr[1] の場合に検出されます
  • 境界 arr[n-1] < arr[n-2] がある場合に検出されます。
  • 2 つの境界のどちらも局所最小値ではなく、隣接する 2 つの数値が等しくない場合、左側の境界は局所的に単調減少し、右側の境界は局所的に単調増加しますしたがって、配列には最小値点が存在する必要があります。
    ここに画像の説明を挿入

2. 二分法を実行し、中間位置と隣接位置の関係を 3 つの状況に分けて判断します: (注意: 配列内の 2 つの隣接する要素は等しくありません!) 3. 最小値が見つかるまでプロセス 2 を繰り返します
ここに画像の説明を挿入

int LocalMinimumSearch(const std::vector<int>& arr) 
{
    
    
    int n = arr.size();
    // 先判断元素个数为0,1的情况,如果题目给出最少元素个>1数则不需要判断
    if (n == 0) return -1;
    if (n == 1) return 0; // 只有一个元素,则是局部最小值
	
	if (arr[0] < arr[1]) return 0;
	
	int left = 0;
    int right = n - 1;
	// 再次提醒,数组中相邻两个元素是不相等的!
    while (left < right) 
    {
    
    
        int mid = left + ((right - left) >> 1);

        if (arr[mid] < arr[mid - 1] && arr[mid] < arr[mid + 1]) {
    
    
            return mid;  // 找到局部最小值的位置
        } else if (arr[mid - 1] < arr[mid]) {
    
    
            right = mid - 1;  // 局部最小值可能在左侧
        } else {
    
    
            left = mid + 1;  // 局部最小值可能在右侧
        }
    }

    // 数组中只有一个元素,将其视为局部最小值
    return left;
}

2 単純な再帰的思考

Zuoge P4の初めから、マージ ソートの予備知識に属します。ここでも二項対立が使われています。主に実行プロセスを理解するため

例: 再帰を使用して、配列の指定された範囲内の最大値を検索します。

#incldue <vector>
#include <algorithm>
int process(const std::vector<int>& arr, int L, int R)
{
    
    
	if (L == R) return arr[L]; 
	
	int mid = L + ((R - L) >> 1);	// 求中点,右移1位相当于除以2
	int leftMax = process(arr, L, mid);
	int rightMax = process(arr, mid + 1, R);
	return std::max(leftMax, rightMax);
}

process(arr, L, R) 関数が呼び出されると、次の処理が行われます。

  1. まず、再帰の終了条件がチェックされます。L と R が等しい場合、配列の最小間隔が再帰され、要素が 1 つだけ存在することを意味します。このとき、要素 arr[L] が直接返されます。
  2. 終了条件が満たされない場合は、再帰を続行する必要があります。まず、中点 mid を計算し、区間 [L, R] を 2 つの部分区間 [L, mid] と [mid+1, R] に分割します。
  3. 次に、再帰的に process(arr, L, Mid) を呼び出して、左の部分間隔を処理します。このステップでは、現在の関数をスタックにプッシュし、新しいレベルの再帰に入ります。
  4. 新しい再帰レベルでは、終了条件が満たされるまでステップ 1 ~ 3 を再度実行し、左部分区間の最大値 leftMax を返します。
  5. 次に、再帰的に process(arr, Mid+1, R) を呼び出して、右の部分間隔を処理します。このステップでも、現在の関数がスタックにプッシュされ、再帰の新しいレベルに入ります。
  6. 新しい再帰レベルでは、終了条件が満たされるまでステップ 1 ~ 3 を再度実行し、右部分区間の最大値 rightMax を返します。
  7. 最後に、左右の部分区間の最大値leftMaxとrightMaxを比較し、大きい方の値を
    区間[L,R]全体の最大値として取り、結果として返します。
  8. 再帰的に前の層に戻る場合は、元の呼び出し点に戻るまで、得られた最大値を前の層に渡して配列全体の最大値を取得します。

再帰のプロセスでは、再帰呼び出しごとに新しい関数スタック フレームが作成され、関数のローカル変数とパラメーターが保存されます。終了条件が満たされると再帰が後戻りを開始し、最終結果が層ごとに返されますが、同時に各層のローカル変数やパラメータも破棄され、関数スタックフレームがスタックから飛び出します。順番に。

依存関係グラフ

  • ターゲット配列が [3, 2, 5, 6, 7, 4] で、 process(0,5) を呼び出して最大値を見つけ、パラメーター arr が省略されているとします。
  • 赤い数字は処理の実行フローで、std::maxを比較して返す処理を省略しています。
    ここに画像の説明を挿入

人間の言葉: 配列の戻り値、つまり1最初のステップの戻り値を取得したい場合は、まず2最大値を取得する必要があり、その最大値はコードに従って最初に実行されなければなりませ32 つの点がこのステップに到達すると3要素 L==R が 1 つだけあります。コードを見てください。この要素は現在の (0,0) 間隔の最大値であり、leftMax を取得した後に戻ります。 、右側の部分の最大値が必要なので、それを実行し、rightMax を取得し、最後に左右の間隔の最大値を比較し、(0,1) 間隔の最大値を取得して、それを に返しますが、また、が必要なので戻った後、比較後の(0,2)の最大値を取得し、それを返します次に、右側のことを実行します。後で省略44335226621

2.1 再帰的アルゴリズム(マスター式)の計算量

プログラミングでは、再帰は非常に一般的なアルゴリズムです。コードが簡潔であるため、広く使用されています。ただし、逐次実行または循環プログラムと比較すると、再帰の計算は困難です。マスター公式は、再帰的プログラムの時間計算量の計算に使用されます。

使用条件:すべての部分問題は同じサイズでなければなりません率直に言うと、基本的には二分木と二分木によって作成された再帰的アルゴリズムです。

T ( N ) = a T ( N / b ) + O ( N d ) T(N) = aT(N/b) + O(N^d)T ( N )=a T ( N / b )+O ( Nd )

  • NNN : 親プロセスのデータサイズはNです
  • N/b N/bN / b : サブプロセスのデータサイズ
  • ああa : サブルーチンの呼び出し数
  • O ( N d ) O(N^d)O ( Nd ): 部分問題の呼び出しを除く他のプロセスの時間計算量

abd を取得した後、次のさまざまな状況に従って時間計算量を取得します。

  • ログバ > d ログバ > dログ_ _bある>d : 時間計算量はO ( N logba ) O(N^{log_ba})O ( Nログ_ _b _
  • log = d log_ba = dログ_ _bある=d : 時間計算量はO ( N d ⋅ log N ) O(N^d · logN)O ( Nd⋅ _ログN ) _ _
  • logba < d log_ba < dログ_ _bある<d : 時間計算量はO ( N d ) O(N^d)O ( Nd )

たとえば、上記の最大値を求める例では、次の式を使用できます。

N = 2 ⋅ T ( N 2 ) + O ( 1 ) N = 2・T(\frac{N}{2}) + O(1)N=2 T (2N)+O(1)。其中,a = 2; b = 2; d = 0

log 2 2 = 1 > 0 log_22 = 1 > 0ログ_ _22=1>0したがって、時間計算量は次のようになります:O ( N ) O(N)O ( N )

おすすめ

転載: blog.csdn.net/Motarookie/article/details/131382340