leetcode -- 二分查詢

長安不亂發表於2020-07-04

之前在資料結構搜尋那章說過,折半(二分)一般適用於有序列表的查詢,但是在寫的時候需要注意演算法的細節。我在leetcode上總共寫了八道應用了二分演算法的題目,從中總結一下寫二分演算法需要注意什麼樣的細節

一般二分查詢

leetcode,第704題,binary search

Given a sorted (in ascending order) integer array nums of n elements and a target value, write a function to search target in nums. If target exists, then return its index, otherwise return -1.
Example 1:
Input: nums = [-1,0,3,5,9,12], target = 9
Output: 4
Explanation: 9 exists in nums and its index is 4

這道題就是最簡單的二分查詢演算法,我當時的解法也是二分法,

   public int search(int[] nums, int target) {
        int start = 0, end = nums.length - 1;
        while(start <= end) {
        	int middle = (end + start) / 2;
        	if(target > nums[middle]) {
        		start = middle + 1;
        	}else if(target < nums[middle]) {
        		end = middle - 1;
        	}else if(target == nums[middle]){
        		return middle;
        	}
        }
        return -1;
    }

對於二分演算法的寫法,這是其中一種,還有其他寫法,一共有三種模板寫法
Alt text

這上面的三種寫法中,第三種是使用最多的,因為很多時候mid最好還是不要跳過,還需要繼續使用。第一種寫法寫法更加簡潔,不過適用於沒有重複元素或者不需要尋找第一個、最後一個位置。我們可以發現模板寫法和我們的寫法有一點不同,這點就是mid的求法,模板中mid=left + (right - left) / 2,在我們的寫法中middle = (end + start) / 2,我們的寫法更容易出現問題,如果end和start這個時候都非常的大,超出了int的範圍(-2147483648, 2147483647),那麼值就會變成0,但是模板中mid = left + (right - left) / 2就不會出現這樣的問題。

leetcode,第278題,First Bad Version

You are a product manager and currently leading a team to develop a new product. Unfortunately, the latest version of your product fails the quality check. Since each version is developed based on the previous version, all the versions after a bad version are also bad.
Suppose you have n versions [1, 2, ..., n] and you want to find out the first bad one, which causes all the following ones to be bad.
You are given an API bool isBadVersion(version) which will return whether version is bad. Implement a function to find the first bad version. You should minimize the number of calls to the API.
Example:
Given n = 5, and version = 4 is the first bad version.
call isBadVersion(3) -> false
call isBadVersion(5) -> true
call isBadVersion(4) -> true
Then 4 is the first bad version.

這道題也是查詢位置,不過比較特殊,第一個版本錯了,那麼後面就會一直錯,這個對錯就是它的順序,判斷條件就是它對還是錯。

    public int firstBadVersion(int n) {
    	if(n == 1) return n;
        int start = 1;
        int end = n;
        int bad_version = 1;
        while(start <= end){
        	// 那種寫法不對,middle = (start + end) / 2,
        	// 這種寫法在小資料量下沒關係,但是資料量大,就是相加錯誤
            int middle = start + (end - start) / 2;
            if(isBadVersion(middle)){
                bad_version = middle;
                end = middle - 1;
            }else{
                start = middle + 1;
            }
        }
        return bad_version;
    }

當然對於二分查詢,也可以使用回溯法來實現它。

    // 使用回溯法
    public int firstBadVersion(int n) {
    	return helper(1,n);
    }
    public int helper(int start, int end) {
		if(start >= end) return start;
		int middle = start + (end - start) / 2;
		// 這裡和上面不同。這裡並沒有記錄下來,並且middle可能就是的
		if(isBadVersion(middle)) return helper(start, middle);
		else return helper(middle + 1, end);
    }

注意查詢位置

這種題目一般都是因為查詢的元素比較特殊,比如插入的位置,並且因為迴圈判定條件<=的原因,要格外注意位置在哪裡。

leetcode,第35題,Search Insert Position

Given a sorted array and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted in order.You may assume no duplicates in the array.
Example 1:
Input: [1,3,5,6], 2
Output: 1

這道題是一個插入題,其中應該注意的是返回值該是什麼,下面的程式碼使用的是start來作為返回值,如果使用middle作為返回值,在陣列也有相同元素的情況下沒什麼問題,一旦陣列中沒有這個元素,那麼插入的位置就會小一格,而start是正好在位置上。

    // 二分查詢位置
    public int searchInsert(int[] nums, int target) {
    	if(nums.length == 0) return 0;
    	int start = 0;
    	int end = nums.length - 1;
    	while(start <= end) {
    		int middle = (start + end) / 2;
    		if(target == nums[middle]) {
    			return middle;
    		}else if(target > nums[middle]) {
    			start = middle + 1;
    		}else if(target < nums[middle]) {
    			end = middle - 1;
    		}
    	}
        // 要注意返回的位置,因為它可能會比插入位置的值要小
    	return start;
    }

leetcode,第74題,Search a 2D Matrix,

Write an efficient algorithm that searches for a value in an m x n matrix. This matrix has the following properties:
&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp Integers in each row are sorted from left to right.
&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp The first integer of each row is greater than the last integer of the previous row.
Example 1:
Input:
matrix = [
[1, 3, 5, 7],
[10, 11, 16, 20],
[23, 30, 34, 50]
]
target = 3
Output: true

這道題我是用的是倆次二分查詢,因為它在行列上是有順序的,我們可以先在行上二分,看目標數在哪一行中,找出特定行,但是要注意這個數在這個行中哪個位置。下面的程式碼用的是end來表示,這裡如果用start,因為判斷條件是<=,那麼end最終會多出一格,因為執行了end++。

    // 這裡是用了倆次二分查詢
    // 也可以將二維陣列併成一維陣列
    public boolean searchMatrix(int[][] matrix, int target) {
    	if(matrix.length == 0 || matrix[0].length == 0) return false;
        int start = 0,end = matrix.length - 1;
        int matrix_line = 0;
        while(start <= end) {
        	int middle = (start + end) / 2;
    		if(target == matrix[middle][0]) {
    			return true;
    		}else if(target > matrix[middle][0]) {
    			start = middle + 1;
    		}else if(target < matrix[middle][0]) {
    			end = middle - 1;
    		}	
        }
        // 注意查詢的位置。這裡不是插入,不應該用start
        matrix_line = Math.max(end, 0);
        start = 0;
        end = matrix[matrix_line].length - 1;
        while(start <= end) {
        	int middle = (start + end) / 2;
    		if(target == matrix[matrix_line][middle]) {
    			return true;
    		}else if(target > matrix[matrix_line][middle]) {
    			start = middle + 1;
    		}else if(target < matrix[matrix_line][middle]) {
    			end = middle - 1;
    		}	
        }
        return false;
    }

半有序

這種題目都是將陣列旋轉為前提,陣列的全部元素並不是有序的,但是以某個元素為分界,倆邊都是有序,當然也可以完全有序。
leetcode,第153題,Find Minimum in Rotated Sorted Array

Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand.
(i.e., [0,1,2,4,5,6,7] might become [4,5,6,7,0,1,2]).
Find the minimum element.
You may assume no duplicate exists in the array.
Example 1:
Input: [3,4,5,1,2]
Output: 1

上面就是以0為界限,前面有序,後面也有序。開始的時候,我的想法以第一個元素為標杆,這裡就是4,然後二分,這裡就是7與4進行比較,如果middle大於標杆元素,那麼說明還在一個序列中,start = middle+1,直到找到了另一個有序,但是這種方法在面對序列中的元素都有序的時候就出錯了,比如[1,2,3,4,5,6],那麼它肯定找不到了,因為都比標杆元素大。

之後想法是先找分界線,那麼以最後一個元素為標杆比較好,因為如果一個序列中,那麼最後元素都會比前面的大,如果中間元素middle大於最後元素end,說明倆者不在一個序列中,這個時候就可以將start往後移動,如果小於,就說明在一個序列中,可以將end往前移動。最後不確定start和end誰最小的最好方法就是比較一下。這裡的判定條件已經發生了改變,變成了starrt + 1 < end,這是因為下面的不用再+1和-1,直接用了middle,如果還是以前的start <= end,整個程式就會沒有發生變化,一直迴圈處理下去。

    // 使用二分的思想,不至於全部遍歷一下
    // 速度很快
    public int findMin(int[] nums) {
    	if(nums.length == 1) return nums[0]; 
        int start = 0;
        int end = nums.length - 1;
        int target = nums[end];
        while(start + 1 < end) {
        	int middle = start + (end - start)/2;
        	if(nums[middle] > target) {
        		start = middle;
        	}else if(nums[middle] < target){
        		end = middle;
        		target = nums[end];
        	}
        }
        return Math.min(nums[start], nums[end]);

leetcode,第154題,Find Minimum in Rotated Sorted Array II

Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand.
(i.e., [0,1,2,4,5,6,7] might become [4,5,6,7,0,1,2]).
Find the minimum element.
The array may contain duplicates.
Example 2:
Input: [2,2,2,0,1]
Output: 0

這道題最大的不同就是有了重複元素,但是其實解法都是一樣的,只不過需要將重複元素進行判定消去,如果周邊有相同的元素,那麼就是移動一格。

    // 大概的思路與之前的一樣,重複元素,就簡單的辦法就是直接去除重複元素
    // 但是時間太久,速度太慢,但是其他的思想都差不多。
    public int findMin(int[] nums) {
    	if(nums.length == 1) return nums[0]; 
        int start = 0;
        int end = nums.length - 1;
        int target = nums[end];
        while(start + 1 < end) {
        	while(start < end && nums[end] == nums[end - 1]) {
        		end--;
        	}
        	while(start < end && nums[start] == nums[start + 1]) {
        		start++;
        	}
        	int middle = start + (end - start)/2;
        	if(nums[middle] > target) {
        		start = middle;
        	}else if(nums[middle] < target){
        		end = middle;
        		target = nums[end];
        	}
        }
        return Math.min(nums[start], nums[end]);
    }

leetcode,第33題,Search in Rotated Sorted Array

Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand.
(i.e., [0,1,2,4,5,6,7] might become [4,5,6,7,0,1,2]).
You are given a target value to search. If found in the array return its index, otherwise return -1.
You may assume no duplicate exists in the array.
Your algorithm's runtime complexity must be in the order of O(log n).
Example 1:
Input: nums = [4,5,6,7,0,1,2], target = 0
Output: 4

這道題與上面倆道題差不多,只不過上面都是查詢最值,這道題是查詢相應的元素,一開始想法是雙指標法,前後倆個序列,然後依次進行比較,因為這倆個序列也是有序,因而知道結束條件是什麼。但是這種寫法速度非常的慢。

        // 將之變成倆個序列,分別使用順序查詢,速度有點慢
	public int search(int[] nums, int target) {
		if(nums.length == 0) return -1;
		int one_point = 0;
		int two_point = nums.length - 1;
		boolean is_one_search = true, is_two_search = true;
		while(one_point < nums.length && two_point >= 0 && (is_one_search || is_two_search)) {
			if(target == nums[one_point]) {
				return one_point;
			}else if(target > nums[one_point]) {
				one_point++;
			}else if(target < nums[one_point]) {
				is_one_search = false;
			}
			
			if(target == nums[two_point]) {
				return two_point;
			}else if(target < nums[two_point]) {
				two_point--;
			}else if(target > nums[two_point]) {
				is_two_search = false;
			}	
		}
		return -1;
    }

之後看了別人的想法,發現二分也是可以解決這道題,不過二分需要判斷四種情況:

  • middle元素大於start元素,說明前面元素都是有序的。
  1. start < target < middle,那麼該元素就在前面的序列中,這時end = middle,來縮小範圍。
  2. 如果不在, 就得縮小到後面序列中,start = middle。
  • middle元素小於end元素,說明後面元素都是有序的。
  1. middle < target < end,那麼說明元素在後面的序列中,這時需要start = middle,來縮小範圍。
  2. 如果不在,就得縮小到前面序列中,end = middle.
        // 使用二分法,分四種情況進行討論,速度可以,記憶體消耗大
	public int search(int[] nums, int target) {
		if(nums.length == 0) return -1;
		int start = 0;
		int end = nums.length - 1;	
		// 這種start = middle寫法中,判斷條件不能是start < end,這樣會導致它不變
		while(start + 1 < end) {
			int middle = start + (end - start)/2;
			if(target == nums[middle]) return middle;
			if(nums[start] < nums[middle]) {
				if(target <= nums[middle] && target >= nums[start]) {
					end = middle;
				}else {
					start = middle;
				}
				
			}else if(nums[end] > nums[middle]){
				if(target >= nums[middle] && target <= nums[end]) {
					start = middle;
				}else {
					end = middle;
				}
			}
		}
		// 這樣判斷最好,分清楚
		if(nums[start] == target) return start;
		if(nums[end] == target) return end;
		return -1;
	}

leetcode,第81題,Search in Rotated Sorted Array II

Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand.
(i.e., [0,0,1,2,2,5,6] might become [2,5,6,0,0,1,2]).
You are given a target value to search. If found in the array return true, otherwise return false.
Example 1:
Input: nums = [2,5,6,0,0,1,2], target = 0
Output: true

這道題和上面一樣,只不過這道題多了重複元素,和之前一樣的思路,先去除重複元素,再使用二分法來進行判斷。

    public boolean search(int[] nums, int target) {
    	if(nums.length == 0) return false;
		int start = 0;
		int end = nums.length - 1;
		
		// 這種start = middle寫法中,判斷條件不能是start < end,這樣會導致它不變
		while(start + 1 < end) {
			if(start < end && nums[start] == nums[start + 1]) start++;
			if(start < end && nums[end] == nums[end - 1]) end--;
			int middle = start + (end - start)/2;
			if(target == nums[middle]) return true;
			
			
			if(nums[start] < nums[middle]) {
				if(target <= nums[middle] && target >= nums[start]) {
					end = middle;
				}else {
					start = middle;
				}
			// 一定要加上這句話。不能直接寫else,不然對於{3,1,1}這種無法判斷
			}else if(nums[end] > nums[middle]){
				if(target >= nums[middle] && target <= nums[end]) {
					start = middle;
				}else {
					end = middle;
				}
			}
		}
		if(nums[start] == target || nums[end] == target) return true;
		return false;
    }

總結

二分法(折半)思路較為簡單,並且可以用在元素有序的情形下,但是二分法需要注意細節,停止條件,查詢位置,判定條件,還有中間位置的計算。如果可以的話,先演示一下,要特別注意那些特殊情況下的寫法。

相關文章