[Data structure and algorithm C++ implementation] 2. Binary search and simple recursion

The original video is Zuo Chengyun's B station teaching



1 dichotomy

Binary Search is a search algorithm for finding a specific element in an ordered array. Its basic idea is to divide the array from the middle, and then judge the size relationship between the target element and the middle element to determine whether the target element is in the left half or the right half. Then continue to perform the same operation in the corresponding sub-array until the target element is found or it is determined that the target element does not exist.

Specific steps are as follows:

  • Sets the left boundary of the array to left and the right boundary to right.
  • Calculate the middle position mid, that is, mid = (left + right) / 2.
  • Compare the size relationship between the target element and the intermediate element:
  • If the target element is equal to the middle element, the target element is found, and its index is returned.
  • If the target element is smaller than the middle element, update the right boundary right = mid - 1, and continue binary search on the left half.
  • If the target element is larger than the middle element, update the left boundary left = mid + 1, and continue binary search on the right half.
  • Repeat steps 2 and 3 until the target element is found or the left boundary is larger than the right boundary.

Time complexity O(logN) , where n is the length of the array. Since the search range is halved each time, the algorithm is very efficient. But the array is required to be ordered, otherwise the dichotomy cannot be applied for searching.

1.1 有序Find a specific element in an array

The basic idea is to reduce the search range by half by comparing the size relationship between the intermediate element and the target element until the target element is found or the search range is empty.

Because, for example, the number of arrays is N=16, the worst case is divided into 4 times ( [ 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 ]) , and4 = log 2 16 4 = log_2164=log216 . That is, the time complexity isO ( log N ) O(logN)O(logN)

/* 注意:题目保证数组不为空,且 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;
}

Watch out for left + ((right - left) >> 1) results etc for (right + left) / 2, but faster without int overflow

1.2 Find the leftmost position of >= a certain number in an ordered array

The idea is still the dichotomy method, which is different from finding a certain value and stopping the dichotomy when the target value is found. The problem of finding the leftmost/rightmost position must be dichotomous to the end


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 Find the rightmost position of <= a certain number in an ordered array

  • If the middle element is greater than the target value, it means the target value should be in the left half, so we narrow the search to the left half and update right to mid - 1.
  • If the middle element is less than or equal to the target value, it means that the target value should be in the right half or the current position, so we update result to the current middle index mid to record the rightmost position found and narrow the search range to the right half , update left to 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 Local minimum problem (a case of unordered array using dichotomy)

The array arr is unordered, any two adjacent numbers are not equal , find a local minimum position (minimum value), and the time complexity is required to be better than O(N)

Disorder can also be dichotomized , as long as the target problem must have a solution on one side, and the other side does not matter, dichotomy can be used

1. First judge the two boundaries of the array

  • Found if left bound arr[0] < arr[1]
  • Found if there is a bound arr[n-1] < arr[n-2]
  • If neither of the two boundaries is a local minimum, and because any two adjacent numbers are not equal, the left boundary is locally monotonically decreasing , and the right boundary is locally monotonically increasing . Therefore, in the array, there must be a minimum value point
    insert image description here

2. Carry out dichotomy and judge the relationship between mid and adjacent positions, which are divided into 3 situations: (Reminder: two adjacent elements in the array are not equal!) 3. Repeat process
insert image description here
2 until the minimum value is found

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 Simple recursive thinking

From the beginning of Zuoge P4 , it belongs to the pre-knowledge of merge sort. Dichotomy is also used here. Mainly to understand the execution process

Example: Find the maximum value in the specified range of the array, using recursion to achieve

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

When the process(arr, L, R) function is called, it does the following:

  1. First, the recursion termination condition is checked. If L and R are equal, it means that the minimum interval of the array has been recursed, and there is only one element. At this time, the element arr[L] is returned directly.
  2. If the termination condition is not met, the recursion needs to continue. First, calculate the midpoint mid, and divide the interval [L, R] into two subintervals [L, mid] and [mid+1, R].
  3. Then, recursively call process(arr, L, mid) to process the left subinterval. This step pushes the current function onto the stack, entering a new level of recursion.
  4. In the new recursive level, execute steps 1-3 again until the termination condition is satisfied, and return the maximum value leftMax of the left subinterval.
  5. Next, recursively call process(arr, mid+1, R) to process the right subinterval. Again, this step pushes the current function onto the stack, entering a new level of recursion.
  6. In the new recursive level, execute steps 1-3 again until the termination condition is satisfied, and return the maximum value rightMax of the right subinterval.
  7. Finally, compare the maximum values ​​leftMax and rightMax of the left and right subintervals, take the larger value as the
    maximum value of the entire interval [L, R], and return it as the result.
  8. When recursively returning to the previous layer, pass the obtained maximum value to the previous layer until returning to the original call point to obtain the maximum value of the entire array.

In the process of recursion, each recursive call will create a new function stack frame to save the local variables and parameters of the function. When the termination condition is met, the recursion starts backtracking, and the final result is returned layer by layer. At the same time, the local variables and parameters of each layer are also destroyed, and the function stack frames are popped out of the stack in turn.

dependency graph

  • Suppose the target array is [3, 2, 5, 6, 7, 4], call process(0,5) to find the maximum value, and the parameter arr is omitted
  • The red number is the execution flow of the process, which omits the process of comparing and returning std::max
    insert image description here

Human words: If you want to get the return value of the array, that is, 1the return value of the first step, you must first get 2the maximum value, and its maximum value must be executed first according to the code 3, and it 3must also be executed first 4. When the two points reach 4this step, There is only one element L==R, look at the code, this element is the maximum value of the current (0,0) interval, return to, 3after 3getting leftMax, you need the maximum value of the right part, execute it 5, get rightMax, and finally Compare the maximum value of the left and right intervals, get the maximum value of the (0,1) interval, and return to it 2, 2and it is necessary to 6return to get the maximum value of (0,2) after comparison 6, and 2return to it 1. Then do the one on the right. . . . later omitted

2.1 Time Complexity of Recursive Algorithm (Master Formula)

In programming, recursion is a very common algorithm. It is widely used because of its concise code. However, compared with sequential execution or cyclic programs, recursion is difficult to calculate. The master formula is used to calculate the time complexity of recursive programs.

Conditions of use: All subproblems must be of the same size . To put it bluntly, it is basically the recursive algorithm created by binary and binary trees.

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

  • N N N : The data size of the parent process is N
  • N / b N/b N / b : Sub-process data size
  • a a a : the number of calls to the subroutine
  • O ( N d ) O(N^d) O ( Nd ): Time complexity of other processes except the invocation of subproblems

After getting abd, get the time complexity according to the following different situations

  • l o g b a > d log_ba > d logba>d : time complexity isO ( N logba ) O(N^{log_ba})O ( Nlogba)
  • log = d log_ba = dlogba=d : The time complexity isO ( N d ⋅ log N ) O(N^d · logN)O ( Ndlog N ) _ _
  • l o g b a < d log_ba < d logba<d : Time complexity isO ( N d ) O(N^d)O ( Nd)

For example, the example of finding the maximum value mentioned above can use this formula:

N = 2 ⋅ T ( N 2 ) + O ( 1 ) N = 2·T(\frac{N}{2}) + O(1) N=2T(2N)+O ( 1 ) . where a = 2; b = 2; d = 0

l o g 2 2 = 1 > 0 log_22 = 1 > 0 log22=1>0 Therefore, the time complexity is:O ( N ) O(N)O ( N )

Guess you like

Origin blog.csdn.net/Motarookie/article/details/131382340