巧用二分法_leetcode刷題總結

二分法巧解題

談到二分法,你會想到啥?

嗯,本人作爲一名非ACMer的小菜雞,一開始的時候,腦子想到的就是上過的算法課上老師說可以用二分法在有序數組裏面進行快速查找,並且又一個十分顯著的特徵,時間複雜度是log2n 量級的。

記憶中二分查找的代碼模版:

public void find(int[] num,int target){
	int left = 0,right = num.lenth;
	while(left<right){
		int mid = (left+mid)/2;
		if(num[mid]<target){
			left = mid+1;
		}else if(num[mid]>target){
			right = mid-1;
		}else{
			return mid;
		}
	}
	if(num[left==target])	{
		return left;
	}
	return -1;
}

哈哈哈,其實也就是順手一寫的,嗯,大致也就這個樣子。用while循環裏面包裹的代碼,是不斷的讓left\right來逼近我們最有可能的結果。

但是刷完一些leetcode上面的題目,才發現原來二分法不只是應用在一個有序數組中進行查找,嗯,我準備先下個結論

二分查找,首先得明確查找的是什麼目標,他滿足什麼條件,如何進行逼近,並且確保最終能夠得到結果。

二分查找,可以認爲是有序數組查找的一種加速策略,如果只存在局部有序,便可以考慮局部加速嘛

當然我們之前學習的二分法,是整個數組都是有序的嘛,那麼問題來了,如果不是整體有序,該怎麼做呢?來,我們來撕撕幾個題目。(儘可能的循序漸進)

題目:
給定一個排序數組和一個目標值,在數組中找到目標值,並返回其索引。如果目標值不存在於數組中,返回它將會被按順序插入的位置。

你可以假設數組中無重複元素。

示例 1:

輸入: [3,4,5,1,2]
輸出: 1

示例 2:

輸入: [4,5,6,7,0,1,2]
輸出: 0

上代碼,再剖析:

public static int searchInsert(int[] nums, int target) {
		int left = 0,right = nums.length;
		while(left<right){
			int mid = (left+right)/2;
			if(num[mid]<target){
				left = mid+1;
			}else{
				right = mid;
			}
		}
		return left;
}

emmm,上面的代碼可以說是十分的簡潔,分析起來,我們可以這樣想,left是搜索的最左邊界,right是 搜索的最右邊界。重點是在判斷裏面

			if(num[mid]<target){
				left = mid+1;
			}else{
				right = mid;
			}

首先是如果num[mid]<target的情況下,結果的左邊界一定得爲mid的右邊一個,所以left = mid+1,然後else代表的意思的是 num[mid]>=target的情況,此時此刻,我們能夠確定的事結果的右邊界爲 mid。

  • 嗯,這裏需要去思考的一個問題是,爲什麼是要在滿足 “<” 的時候執行 left = mid+1,在">" 的時候執行 right = mid; 提示下,這裏指的是插入的位置噢

這塊的代碼,需要着重理解下,原因是接下來你會看到重要的變種主要是在這裏做手腳的,嘖嘖嘖。

給定一個按照升序排列的整數數組 nums,和一個目標值 target。找出給定目標值在數組中的開始位置和結束位置。

你的算法時間複雜度必須是 O(log n) 級別。

如果數組中不存在目標值,返回 [-1, -1]。

示例 1:

輸入: nums = [5,7,7,8,8,10], target = 8
輸出: [3,4]
示例 2:

輸入: nums = [5,7,7,8,8,10], target = 6
輸出: [-1,-1]

這裏我們該怎麼做呢,如果用常規的二分查找的話,並沒有要求返回元素的第一個位置和最後一個位置,代碼如下:

public int[] searchRange(int[] nums, int target) {
	 int[] res = {-1,-1};
	int left = 0,right = nums.length-1;
	//首先求第一個位置
	while(left<right){
		int mid = (left+right)/2;
		if(nums[mid]>target){
			right = mid-1;
		}else{
			left = mid;
		}
	}
	if(nums[left]==target){
		res[0] = left;
	}else{
		return res;//不存在
	}
	//然後求最後一個位置
	right = nums.length -1;
	while(left<right){
		int mid = (left+right+1)/2;
		if(nums[mid]<target){
			left = mid+1;
		}else{
			right = mid;
		}
	}
	res[1] = right;
	return res;
}

emmm,這裏我們注意兩個地方哈:
第一個地方是:如果求最左邊,其實我們想要的動作是不斷的利用最右邊界來靠近最左邊界,這樣求得的就是第一個;反之,利用最左邊界不斷靠近最右邊界,就是求得最後一個;
第二個地方:如果記得求中間值的時候,我們默認是靠左邊的,所以如果想要求最後一個的時候,需要(left+right+1)/2;

( 例如,數組 [0,1,2,4,5,6,7] 可能變爲 [4,5,6,7,0,1,2] )。

搜索一個給定的目標值,如果數組中存在這個目標值,則返回它的索引,否則返回 -1 。

你可以假設數組中不存在重複的元素。

你的算法時間複雜度必須是 O(log n) 級別。

emmmm,分析下,這個屬於局部有序嘛,都提示了要O(log n) ,上代碼:

//	大體思路是:我們先找出來是是從哪裏開始旋轉的,找出來之後,再開始
public static int search(int[] nums, int target) {

		//這裏的left\right是指旋轉點的最左邊界、最右邊界
		int left = 0,right = nums.length;
		while(left<right){
			int mid  = (left+right)/2;
			if(nums[mid]<nums[right]){
					right = mid;
			}else {
					left  = mid+1;
			}
		}
		//接下里我們按照0-left-1,和 left-num.length-1兩段來進行二分搜索嘛,
}

emmm,上面的代碼,我們來分析,如果if(nums[mid]<nums[right])條件成立時,即是我們可以確定的是,當前mid-right是屬於同一段,所以我們可以知道那個旋轉點的最右邊界爲mid,反之,可以知道mid到righr之間一定不屬於同一段,所以那個旋轉點一定在mid到right之間,且不是mid,所以left = mid+1;

假設按照升序排序的數組在預先未知的某個點上進行了旋轉。

( 例如,數組 [0,0,1,2,2,5,6] 可能變爲 [2,5,6,0,0,1,2] )。

編寫一個函數來判斷給定的目標值是否存在於數組中。若存在返回 true,否則返回 false。

示例 1:

輸入: nums = [2,5,6,0,0,1,2], target = 0
輸出: true
示例 2:

輸入: nums = [2,5,6,0,0,1,2], target = 3
輸出: false

這裏和上面的思路還是一樣,但是如何處理這些重複的值,還是十分麻煩的,我們的解決思路是,再進行遍歷的時候,從left向右遍歷去掉相同的值,然後right向左遍歷去掉相同的值,哈哈哈,就這樣,還是一樣,找到旋轉點,就可以啦

public boolean search(int[] nums, int target) {
	int left  =0,right = nums.length-1;
	while(left<right){
		while(left+1<right&&nums[left]==nums[left+1]){
			left++;
		}
		while(right-1>left && nums[right]==nums[right-1]){
			right --;
		}
		int mid = (left+right)/2;
		if(nums[mid]<=nums[right]){
			right = mid;
		}else{
			left = mid+1;
		}
	}
	//嗯,這裏的right/left就是我們旋轉點,剩下的和上面一樣,運用常規的二分查找就OK啦
}

當然我覺得這裏值得思考的地方是這裏,當nums[mid]=nums[right]是應該算在哪裏,我認爲是處在同一個有序序列中,所以和 <的情況一致,這裏很重要噢

		if(nums[mid]<=nums[right]){
			right = mid;
		}else{
			left = mid+1;
		}

假設按照升序排序的數組在預先未知的某個點上進行了旋轉。

( 例如,數組 [0,1,2,4,5,6,7] 可能變爲 [4,5,6,7,0,1,2] )。

請找出其中最小的元素。

你可以假設數組中不存在重複元素。

示例 1:

輸入: [3,4,5,1,2]
輸出: 1
示例 2:

輸入: [4,5,6,7,0,1,2]
輸出: 0

這個思路其實更加的直接,emmm,就是直接求旋轉點

public int findMin(int[] nums) {
	int left=0,right = nums.length;
	while(left<right){
		int mid = (left+right)/2;
		if(num[mid]<nums[right]){
			right = mid;
		}else{
			left = mid+1;
		}
	}
	return nums[left];
}

給定兩個大小爲 m 和 n 的有序數組 nums1 和 nums2。

請你找出這兩個有序數組的中位數,並且要求算法的時間複雜度爲 O(log(m + n))。

你可以假設 nums1 和 nums2 不會同時爲空。

示例 1:

nums1 = [1, 3]
nums2 = [2]

則中位數是 2.0
示例 2:

nums1 = [1, 2]
nums2 = [3, 4]

則中位數是 (2 + 3)/2 = 2.5

剛剛開始的時候難免會一臉矇蔽,但是可以這樣想:我們求中位數,可以想像成我們是想把我們的兩個數組分割成爲兩個部分,num1,num2的兩個數組分別分成 num11,num12,num21,num22四個部分,然後我們認爲num11,num21是前一半部分,num12,num22是後一個部分。需要做的事情是求出這個兩個部分的分割,比如我們用i變量來標示 num1[0:i]、num1[i,num1.length];(左邊包含,右邊不包含)這個兩個部分,用變量j來標示num2[0:j]、num2[j,num2.length];兩個部分,其實我們是可以知道i+j是滿足一定的關係的,因爲我們是均分成兩個部分嘛,所以有 i+j = (m+n+1)/2的。也就是如果求出來了i,便可以求出j,所以我們只需要對於i,任意求一個就可以。當然我們是選擇是長度較小的那個來進行求解。

public double findMedianSortedArrays(int[] A, int[] B) {
        int length_A = A.length;
        int length_B = B.length;
        if(length_A>length_B){
            int [] temp = A;	A = B; B = temp;
            int temp_length= length_A;length_A = length_B;length_B = temp_length;
        }
        int left = 0, right = length_A;//這裏是針對i來進行二分的,所以left、right分別描述的是i的左右邊界
        while(left<=right){
            int i = (left+right)/2;
            int j = (length_A+length_B+1)/2-i;
            if(i<right&&A[i]<B[j-1]){
                left = i+1;
            }else if(i>left&&B[j]<A[i-1]){
                right = i-1;
            }else{
                int left_num=0;
                if(i==0){
                    left_num = B[j-1];
                }else if (j==0){
                    left_num = A[i-1];
                }else{
                    left_num = Math.max(A[i-1],B[j-1]);
                }
                if((length_A+length_B)%2==1) return left_num;
                int right_num = 0;
                if(i==length_A) right_num = B[j];
                else if(j==length_B) right_num= A[i];
			    else  right_num= Math.min(A[i],B[j]);
                return (left_num+right_num)/2.0;
            }
        }
        return 0.0;
    }

emmm,這裏使用二分法注意的是,while(left<=right)這個裏面用的是= 號,原因是結果肯定是存在的,當滿足想等的時候,肯定是會跳入else中,肯定會返回結果,所以直接就是答案啦。當然程序不知道,所以最後return 隨便一個double類型的數值就行惹

			 if(i<right&&A[i]<B[j-1]){
                left = i+1;
            }else if(i>left&&B[j]<A[i-1]){
                right = i-1;
            }else{
            	……
            }

針對二分法,複習就到這裏,之後有新的感受再舔舔補補。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章