Tag Archives: 二分搜索

LeetCode Sqrt(x)

69. Sqrt(x)

Implement int sqrt(int x).

Compute and return the square root of x, where x is guaranteed to be a non-negative integer.

Since the return type is an integer, the decimal digits are truncated and only the integer part of the result is returned.

Example 1:

Input: 4
Output: 2

Example 2:

Input: 8
Output: 2
Explanation: The square root of 8 is 2.82842..., and since 
             the decimal part is truncated, 2 is returned.

本题要对一个数开平方根。相当于LeetCode Valid Perfect Square的逆过程,很多方法都可以通用。 解法1。直接借用LeetCode Valid Perfect Square提到的牛顿法开方,代码如下:

class Solution {
public:
    int mySqrt(int x)
    {
        long long y = x;
        while (y * y > x) {
            y = (y + x / y) / 2;
        }
        return y;
    }
};

本代码提交AC,用时6MS。

解法2。二分搜索,由于sqrt(x)不可能超过x/2+1(查看两个函数图像),所以可以在[0,x/2+1]范围内二分搜索,因为是向下取整,所以返回的是r,代码如下:

class Solution {
public:
    int mySqrt(int x)
    {
        long long l = 0, r = x / 2 + 1;
        while (l <= r) {
            long long mid = (l + r) / 2;
            long long prod = mid * mid;
            if (prod == x)
                return mid;
            if (prod < x)
                l = mid + 1;
            else
                r = mid – 1;
        }
        return r;
    }
};

本代码提交AC,用时6MS。

三刷。暴力枚举:

class Solution {
public:
	int mySqrt(int x) {
		long long xx = x;
		if (x == 1)return 1;
		long long i = 1;
		for (; i < xx; ++i) {
			if (i*i > xx)break;
		}
		return i - 1;
	}
};

本代码提交AC,用时16MS,还是用前两个方法吧。

LeetCode Valid Perfect Square

LeetCode Valid Perfect Square Given a positive integer num, write a function which returns True if num is a perfect square else False. Note: Do not use any built-in library function such as sqrt. Example 1:

Input: 16
Returns: True
Example 2:
Input: 14
Returns: False

本题要判断一个数是否是完全平方数。有很多种解法,现列举如下: 解法1。牛顿法。牛顿法本是用来求解函数f(x)=0的根的,可以用来开方。$$f(x)=x^2-num$$等于0的正根就是num的根号。下面就是牛顿法求解f(x)=0的根的更新公式,$$x_{n+1}$$比$$x_n$$更接近f(x)=0的真实根。初始的时候可以随便代一个$$x_0$$到公式中。 对于函数$$f(x)=x^2-num$$,$$f'(x)=2x$$,所以牛顿递推式为$$!x_{n+1}=x_n-\frac{x_n^2-num}{2x_n}=(x_n+num/x_n)/2$$ 所以我们可以初始代入$$x_n=num$$,然后不断用牛顿法,直到x*x<=num时,如果x*x==num,则num为完全平方数,否则不是。 关于牛顿法用于开放的原理介绍,果壳网上有个人介绍得很详细。 代码如下: [cpp] class Solution { public: bool isPerfectSquare(int num) { long long x = num; while (x*x > num) { x = (x + num / x) / 2; } return x*x == num; } }; [/cpp] 本代码提交AC,用时0MS。 解法2。对于num,如果$$x=\frac{num}{2}$$的平方依然大于num,则$$x=\frac{x}{2}$$,如果x的平方依然大于num,则继续对x除以2,不断除,直到x平方小于等于num时,遍历[x,2*x]之间的数,看看x*x==num是否成立,如果成立,说明num是完全平方数,否则不是。这种解法在高级算法里好像讲过。完整代码如下: [cpp] class Solution { public: bool isPerfectSquare(int num) { if (num == 1)return true; long long x = num >> 1; while (x*x > num)x >>= 1; for (int y = x; y <= (x << 1); ++y) { if (y*y == num)return true; } return false; } }; [/cpp] 本代码提交AC,用时3MS。 解法3。观测下面的等式发现完全平方数都是由连续奇数相加而来,所以我们也可以把连续奇数加起来,直到超过num时,看看和与num是否相等。 1 = 1 4 = 1 + 3 9 = 1 + 3 + 5 16 = 1 + 3 + 5 + 7 25 = 1 + 3 + 5 + 7 + 9 36 = 1 + 3 + 5 + 7 + 9 + 11 …. 1+3+…+(2n-1) = (2n-1 + 1)n/2 = n*n 代码如下: [cpp] class Solution { public: bool isPerfectSquare(int num) { long long sum = 1; for (int i = 3; sum < num; i += 2) sum += i; return sum == num; } }; [/cpp] 本代码提交AC,用时3MS。 解法4。二分搜索。l=0,r=num,mid=(l+r)/2,根据mid*mid和num的大小关系一步步缩小范围。复杂度为O(lgn),完整代码如下: [cpp] class Solution { public: bool isPerfectSquare(int num) { long long l = 0, r = num; while (l <= r) { long long mid = (l + r) / 2; long long prod = mid*mid; if (prod == num)return true; if (prod < num)l = mid + 1; else r = mid – 1; } return l*l == num; } }; [/cpp] 本代码提交AC,用时0MS。 注意这个题的代码中,为了防止int越界,都需要用long long。 参考: http://bookshadow.com/weblog/2016/06/26/leetcode-valid-perfect-square/ http://www.cnblogs.com/grandyang/p/5619296.html]]>

LeetCode Longest Increasing Subsequence

300. Longest Increasing Subsequence

Given an unsorted array of integers, find the length of longest increasing subsequence.

Example:

Input: [10,9,2,5,3,7,101,18]
Output: 4 
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4. 

Note:

  • There may be more than one LIS combination, it is only necessary for you to return the length.
  • Your algorithm should run in O(n2) complexity.

Follow up: Could you improve it to O(n log n) time complexity?


给定一个数组,求最长上升子序列(Longest Increasing Subsequence, LIS)的长度。子序列可以是不连续的,和子串不一样。
此题有两种解法,一种$O(n^2)$,另一种$O(nlgn)$。
先来看$O(n^2)$。设length[i]表示到nums[i]时的LIS,那么此时已知了length[0,…,i-1],对于$j\in[0,…,i-1]$,如果nums[i]>nums[j],则在i处的LIS至少是length[j]+1,因为可以在nums[j]后面接nums[i],构成LIS,所以在i处的LIS加一。我们尝试所有的$j\in[0,…,i-1]$,找一个最大的length[j]+1赋给length[i]。
递推式如下:

length(i) =  max  { length(j) + 1 : if nums[j] < nums[i] }
           0≤j≤i-1

这种思路的代码如下,因为需要两层循环,所以时间复杂度为$O(n^2)$。

class Solution {
public:
    int lengthOfLIS(vector<int>& nums)
    {
        int n = nums.size();
        if (n < 1)
            return 0;
        vector<int> length(n, 1);
        int ans = 1;
        for (int i = 1; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
                if (nums[i] > nums[j]) {
                    length[i] = max(length[i], length[j] + 1);
                }
            }
            ans = max(ans, length[i]);
        }
        return ans;
    }
};

本代码提交AC,用时33MS。
接下来看看$O(nlgn)$的解法。
对于数组d(代码中的数组nums的简称),我们定义一个辅助数组B,B[j]保存了到目前为止LIS=j时的最后一个数的最小的数,比如B[3]=8表示到目前为止长度为3的上升子序列的最后一个数的最小值为8。我们不断更新B的长度和数值,最后B的长度就是最长的上升子序列的长度,但是数组B并不是一个LIS。
举例说明。假设数组下标从1开始。数组d=[3, 5, 6, 2, 5, 4, 19, 5, 6, 7, 12],初始时B的长度为1,且B[1]=3表明此时最长上升子序列的长度为1,且该LIS的最后一个数的最小值是3。
d[2]=5,因为d[2]>B[1],所以5可以加入最长上升子序列,导致此时的最长上升子序列长度为2,且最后一个数的最小值就是5,所以B[2]=5。此时B[1]=3不变,因为长度为1的LIS的末尾数值还是3不变。B=[3,5]
d[3]=6,同上,d[3]>B[2],所以6也加入最长上升子序列,导致LIS=3,且B[3]=6B=[3,5,6]
d[4]=2,注意此时d[4]<B[3],所以2不能增加以B[3]=6结尾的最长上升子序列,所以数组B的长度不变。查找B,发现2比B[1]=3小,所以替换B[1]=3为B[1]=2,此含义为长度为1的LIS的末尾最小数由3变更为2。这很好理解,因为2自身可以作为一个LIS,且比3自身作为LIS要小。B=[2,5,6]
d[5]=5,同上,d[5]<B[3],查找B,更新B[2]=5,因为B[2]本身就是5,所以更新前后一样。B=[2,5,6]
d[6]=4,d[6]<B[3],无法增长最长上升子序列,查找B,发现4在B[1]=2和B[3]=6之间,更新B[2]=4,含义为长度为2的LIS的最后一个数的最小值由5更新为4。B=[2,4,6]。其实这样更新的目的就是为了把各个长度的LIS的末尾的最小数变小,使得后续的数更有可能接在之前LIS的末尾来增加LIS的长度。
d[7]=19,d[7]>B[3],贡献LIS长度,直接加入数组B,即B[4]=19B=[2,4,6,19]
d[8]=5,查找B,在4,19之间,所以更新B[3]=5B=[2,4,5,19]
d[9]=6,查找B,更新B[4]=6B=[2,4,5,6]
d[10]=7,d[10]>B[4],贡献LIS长度,直接加入数组B,即B[5]=7B=[2,4,5,6,7]
d[11]=12,d[11]>B[5],贡献LIS长度,直接加入数组B,即B[6]=12B=[2,4,5,6,7,12]
以上算法过程涉及到在数组B中查找当前元素d[i]的替换位置,因为数组B是有序的,所以可以使用二分查找,最终算法的时间复杂度降为了O(nlgn)。
STL中自带了在有序数组中二分查找的函数,lower_bound输入为一个数组B和目标target,返回数组B中不小于target的最小下标。我们也可以自己用二分查找实现。
这种思路的代码如下:

class Solution {
public:
    int lowerbound(vector<int>& B, int target)
    {
        int l = 0, r = B.size() – 1;
        while (l < r) {
            int m = (l + r) / 2;
            if (B[m] == target)
                return m;
            else if (B[m] > target)
                r = m – 1;
            else
                l = m + 1;
        }
        return B[l] < target ? (l + 1) : l;
    }
    int lengthOfLIS(vector<int>& nums)
    {
        int n = nums.size();
        if (n < 1)
            return 0;
        vector<int> B = { nums[0] };
        for (int i = 1; i < n; ++i) {
            if (nums[i] > B.back())
                B.push_back(nums[i]);
            else {
                //*lower_bound(B.begin(), B.end(), nums[i]) = nums[i];
                B[lowerbound(B, nums[i])] = nums[i];
            }
        }
        return B.size();
    }
};

本代码提交AC,用时3MS,速度一下提高了不少。
参考:

二刷。第二种解法,可以直接调stl的lower_bound,最好自己写,和 http://code.bitjoy.net/2020/03/21/leetcode-find-first-and-last-position-of-element-in-sorted-array/ 这题代码FindFirstIndex一致,记住二分搜索的标准写法。

class Solution {
public:
	int lengthOfLIS(vector<int>& nums) {
		int n = nums.size();
		if (n == 0)return 0;

		vector<int> curmin;
		for (int i = 0; i < n; ++i) {
			if (curmin.empty() || nums[i] > curmin.back()) {
				curmin.push_back(nums[i]);
			}
			else {

				//vector<int>::iterator it = lower_bound(curmin.begin(), curmin.end(), nums[i]);
				//*it = nums[i];

				int l = 0, r = curmin.size() - 1;
				while (l <= r) {
					int m = l + (r - l) / 2;
					if (nums[i] <= curmin[m]) {
						r = m - 1;
					}
					else {
						l = m + 1;
					}
				}
				curmin[r + 1] = nums[i];


			}
		}
		return curmin.size();
	}
};

本代码提交AC,用时8MS。

LeetCode Search in Rotated Sorted Array II

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

Example 2:

Input: nums = [2,5,6,0,0,1,2], target = 3
Output: false

Follow up:

  • This is a follow up problem to Search in Rotated Sorted Array, where nums may contain duplicates.
  • Would this affect the run-time complexity? How and why?

这一题在LeetCode Search in Rotated Sorted Array的基础上,增加了数组有重复元素的约束,其实相当于LeetCode Search in Rotated Sorted ArrayLeetCode Find Minimum in Rotated Sorted Array II的结合,搜索框架采用前者(即先找到有序的半个部分,再决定丢弃哪半个部分),边界判断采用后者(即遇到中间和边缘相等时,移动边缘)。代码上只是在前者代码的基础上增加了一个else分支。完整代码如下:

class Solution {
public:
    bool search(vector<int>& nums, int target)
    {
        int l = 0, r = nums.size() – 1;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (nums[mid] == target)
                return true;
            if (nums[mid] < nums[r]) {
                if (target > nums[mid] && target <= nums[r]) {
                    l = mid + 1;
                }
                else {
                    r = mid – 1;
                }
            }
            else if (nums[mid] > nums[r]) {
                if (target >= nums[l] && target < nums[mid]) {
                    r = mid – 1;
                }
                else {
                    l = mid + 1;
                }
            }
            else {
                –r;
            }
        }
        return false;
    }
};

本代码提交AC,用时12MS。因为多了一个else分支,算法最坏情况下,会达到O(n)的复杂度。

二刷。思路相同,使用递归实现,代码如下:

class Solution {
public:
	bool SearchRange(vector<int>& nums, int l, int r, int target) {
		if (l > r)return false;
		if (l == r)return nums[l] == target;

		int mid = l + (r - l) / 2;
		if (nums[mid] == target)return true;

		if (nums[l] < nums[mid]){
			if (target >= nums[l] && target < nums[mid])return SearchRange(nums, l, mid - 1, target);
			else return SearchRange(nums, mid + 1, r, target);
		}
		else if (nums[l] > nums[mid]) {
			if (target > nums[mid] && target <= nums[r])return SearchRange(nums, mid + 1, r, target);
			else return SearchRange(nums, l, mid - 1, target);
		}
		else {
			++l;
			return SearchRange(nums, l, r, target);
		}
	}
	bool search(vector<int>& nums, int target) {
		return SearchRange(nums, 0, nums.size() - 1, target);
	}
};

本代码提交AC,用时4MS。

LeetCode Search in Rotated Sorted Array

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

Example 2:

Input: nums = [4,5,6,7,0,1,2], target = 3
Output: -1

这一题和LeetCode Find Minimum in Rotated Sorted Array类似,只不过不是查找最小值,而是查找某个target是否存在。也是采用二分查找的思路,首先判断nums[m]和nums[r]的关系,如果nums[m]<nums[r],说明右半部分是有序的,看看target是否在该部分范围内,如果在,则l=mid+1;否则r=mid-1。如果nums[m]>nums[r],说明右半部分无序,则左半部分有序,看看target是否在左半部分范围内,如果在,则r=mid-1;否则l=mid+1。如此循环下去。

完整代码如下:

class Solution {
public:
    int search(vector<int>& nums, int target)
    {
        int l = 0, r = nums.size() – 1;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (nums[mid] == target)
                return mid;
            if (nums[mid] < nums[r]) {
                if (target > nums[mid] && target <= nums[r]) {
                    l = mid + 1;
                }
                else {
                    r = mid – 1;
                }
            }
            else {
                if (target >= nums[l] && target < nums[mid]) {
                    r = mid – 1;
                }
                else {
                    l = mid + 1;
                }
            }
        }
        return -1;
    }
};

本代码提交AC,用时9MS。

二刷。用递归方法实现,每次判断区间的左边是否小于右边,如果是的话,说明是有序的,常规二分即可;如果不是,则取中点,递归在其左边和右边搜索。由于取了中点后,必定有一边是有序的,则要么在有序区间搜索,要么在另一边搜索,即每次是减半的,所以时间复杂度也是O(lgN)。

完整代码如下:

class Solution {
public:
	int BinarySearch(vector<int>& nums, int target, int left, int right) {
		if (left > right)return -1;
		if (left == right) {
			if (nums[left] == target)return left;
			else return -1;
		}
		if (nums[left] < nums[right]) {
			if (target >= nums[left] && target <= nums[right]) {
				int mid = left + (right - left) / 2;
				if (nums[mid] == target)return mid;
				else if (nums[mid] < target)return BinarySearch(nums, target, mid + 1, right);
				else return BinarySearch(nums, target, left, mid - 1);
			}
			else {
				return -1;
			}
		}
		else {
			int mid = left + (right - left) / 2;
			int lans = BinarySearch(nums, target, left, mid);
			int rans = BinarySearch(nums, target, mid + 1, right);
			if (lans != -1)return lans;
			if (rans != -1)return rans;
			return -1;
		}
	}
	int search(vector<int>& nums, int target) {
		return BinarySearch(nums, target, 0, nums.size() - 1);
	}
};

本代码提交AC,用时0MS。

三刷。迭代方法还是主流,和常规二分搜索的代码框架是一致的。但是一刷的时候是先判断右半部分是否有序,有点不符合直觉,如果简单的先判断左半部分,会出bug,比如[3,1],要找1的时候,此时m=0,和l=0是相同的,如果简单的用nums[l]<nums[m]来判断左边是否有序的话,会有问题,此时m==l,所以左边也可以看做是有序的,但不满足nums[l]<nums[s]。所以需要额外加一个l==m的判断。完整代码如下:

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int l = 0, r = nums.size() - 1;
        while(l <= r) {
            int m = l + (r - l) / 2;
            if(nums[m] == target) return m;
            
            if(l == m || nums[l] < nums[m]) { // l == m 左边只有一个元素,左边也是有序的
                if(target >= nums[l] && target < nums[m]) r = m - 1;
                else l = m + 1;
            } else {
                if(target > nums[m] && target <= nums[r]) l = m + 1;
                else r = m - 1;
            }
        }
        
        return -1;
    }
};

本代码提交AC, 用时8MS。

LeetCode Find Minimum in Rotated Sorted Array II

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 1:

Input: [1,3,5]
Output: 1

Example 2:

Input: [2,2,2,0,1]
Output: 0

Note:


这一题在LeetCode Find Minimum in Rotated Sorted Array的基础上,增加了元素可能重复的约束,此时二分判断时,会出现中间元素和边缘元素相等的情况,此时就不能判断应该丢弃哪半部分了。比如[3,3,1,3],中间元素3和右边元素3相等,其实应该丢弃左半部分;又比如[1,3,3],中间元素3和右边元素3相等,其实应该丢弃右半部分。所以遇到中间元素和边缘元素相等时,无法做决定,那么我们只能移动边缘元素,不断的缩小范围,直到中间元素和边缘元素不等。最坏情况是,所有元素都相等时,退化为线性查找了,所以时间复杂度变为了O(n)。
完整代码如下:

class Solution {
public:
    int findMin(vector<int>& nums)
    {
        int l = 0, r = nums.size() – 1;
        while (l < r) {
            int mid = (l + r) / 2;
            if (nums[mid] > nums[r])
                l = mid + 1;
            else if (nums[mid] < nums[r])
                r = mid;
            else
                –r;
        }
        return nums[l];
    }
};

本代码提交AC,用时6MS。

二刷。依然是更加鲁邦容易理解的代码,当nums[mid]==nums[r]时,退化为线性查找。

class Solution {
public:
	int findMin(vector<int>& nums) {
		int l = 0, r = nums.size() - 1;
		int ans = nums[0];
		while (l <= r) {
			if (r - l <= 1) {
				return min(nums[l], nums[r]);
			}
			int mid = l + (r - l) / 2;
			if (nums[mid] > nums[r]) {
				l = mid;
			}
			else if (nums[mid] == nums[r]) {
				--r;
			}
			else {
				r = mid;
			}
		}
		return nums[l];
	}
};

本代码提交AC,用时16MS。

LeetCode Find Minimum in Rotated Sorted Array

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

Example 2:

Input: [4,5,6,7,0,1,2]
Output: 0

一个已按升序排列的数组,被循环旋转了若干次,现在要找到其最小值。第一观测题目中的例子,因为数组是按升序排列的,但是旋转的分界点(7,0)却是降序,所以我们可以遍历数组,找到这个分界点,那么小的数就是最小值。为了防止旋转之后和未旋转是一样的,预先设置结果为第一个元素。代码如下:

class Solution {
  public: int findMin(vector < int > & nums) {
    int ans = nums[0];
    for (int i = 1; i < nums.size(); ++i) {
      if (nums[i] < nums[i– 1]) return nums[i];
    }
    return ans;
  }
};

本代码提交AC,用时3MS。

但是这个代码的最坏复杂度是O(n)的,和一个乱序的数组求最小值遍历一次的复杂度是一样的,本解法并没有利用到数组是已排序这个特点。
要想达到比O(n)更好的复杂度,自然的思路是二分查找的O(lgn)。数组4,5,6,7,0,1,2,中间元素为7,7>2,说明旋转分界点在右半部分,令l=mid+1,此时数组变为0,1,2,中间元素为1,1<2,说明右半部分是正常的,令r=mid。如此循环,直到不满足l<r。完整代码如下:

class Solution {
  public: int findMin(vector < int > & nums) {
    int l = 0, r = nums.size()– 1;
    while (l < r) {
      int mid = (l + r) / 2;
      if (nums[mid] > nums[r]) l = mid + 1;
      else r = mid;
    }
    return nums[l];
  }
};

本代码提交AC,用时3MS。

三刷。二分的代码其实很难写得bug free,尤其是上面的代码和正常的二分搜索不太一样。实际上,我们依然可以套用常规二分的方法,只不过需要做两点改动:

  1. 更新l或r时,只更新为mid,而不是mid+1或者mid-1。常规二分搜索是要找target,所以当target!=nums[mid]时,可以直接跳过mid,到mid+1或者mid-1,但是这里需要找最小值,那么,mid就有可能是最小值,所以不能把mid跳过。
  2. 不跳过mid又可能出现死循环的问题,比如[3,1,2],nums[mid]<nums[r],所以r=mid,之后的循环中l和mid都一直等于0,出现死循环。解决的办法也很简单,当r-l<=1时,说明此时最多只有两个数字,简单取min就可以得到最小值了。

这种修改方法解释起来逻辑很清楚,也很容易记忆,完整代码如下:

class Solution {
public:
	int findMin(vector<int>& nums) {
		int l = 0, r = nums.size() - 1;
		int ans = nums[0];
		while (l <= r) {
			if (r - l <= 1) {
				return min(nums[l], nums[r]);
			}
			int mid = l + (r - l) / 2;
			if (nums[mid] > nums[r]) {
				l = mid;
			}
			else {
				r = mid;
			}
		}
		return nums[l];
	}
};

本代码提交AC,用时0MS。

四刷。上述解法还是有点怪,为什么是比较nums[mid]和nums[r]呢,而不是比较nums[mid]和nums[l]呢?后者是否可行,事实上是可行的。

基本思想:如果数组没有旋转,则应该满足nums[l]<nums[m]<nums[r]。上述思路是,如果数组选择了,则找到的m可能不满足上述条件,如果不满足nums[m]<nums[r],即上述if(nums[mid]>nums[r]),说明右半部分违反顺序,最小值应该在右半部分l=mid。这种解法是找违反顺序的,然后在违反顺序的区间进一步查找。

另一种思路是,找不违反顺序的,比如如果满足nums[l]<nums[r],则说明左半部分满足顺序,所以最小值应该在右边,同样有l=mid。但是这里有一个问题,就是如果数组旋转之后和没旋转是一样的,比如是[1,2,3],则左边没违反顺序,在右边找最小值,只能找到2,答案错误。所以需要在开头加一个判断,即如果数组头<尾,则说明数组旋转前后是一样的,直接输出头元素为最小值。完整代码如下:

class Solution {
public:
    int findMin(vector<int>& numbers)
    {
        int n = numbers.size();
        int l = 0, r = n - 1;
        if (numbers[l] < numbers[r])
            return numbers[l]; // 注意这一句
        while (l <= r) {
            if (r - l <= 1)
                return min(numbers[l], numbers[r]);
            int m = l + (r - l) / 2;
            if (numbers[l] < numbers[m]) { // 左边有序,在右边找
                l = m;
            }
            else { // 左边无序,在左边找
                r = m;
            }
        }
        return 0;
    }
};

本代码提交AC,用时4MS。

LeetCode Search Insert Position

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], 5
Output: 2

Example 2:

Input: [1,3,5,6], 2
Output: 1

Example 3:

Input: [1,3,5,6], 7
Output: 4

Example 4:

Input: [1,3,5,6], 0
Output: 0

已经排好序的数组,找出target或者target应该插入的位置。
显然二分查找,简单题,完整代码如下:

class Solution {
public:
    int findPos(vector<int>& nums, int s, int t, int target)
    {
        if (s == t) {
            if (nums[s] >= target)
                return s;
            else
                return s + 1;
        }
        else if (s > t) { // caution!
            return t + 1;
        }
        else {
            int m = (s + t) / 2;
            if (nums[m] == target)
                return m;
            else if (nums[m] > target)
                return findPos(nums, s, m – 1, target);
            else
                return findPos(nums, m + 1, t, target);
        }
    }
    int searchInsert(vector<int>& nums, int target) { return findPos(nums, 0, nums.size() – 1, target); }
};

需要注意当s>t的分支,我也是遇到一个错误样例之后发现的。本代码提交AC,用时6MS。

本题也可以把递归改成非递归形式,而且好像不会有这种边界问题,代码如下:

class Solution {
public:
    int searchInsert(vector<int>& nums, int target)
    {
        int l = 0, r = nums.size() – 1;
        while (l <= r) {
            int m = l + (r – l) / 2;
            if (nums[m] == target)
                return m;
            if (nums[m] < target)
                l = m + 1;
            else
                r = m – 1;
        }
        return l;
    }
};

本代码提交AC,用时9MS。

LeetCode Median of Two Sorted Arrays

4. Median of Two Sorted Arrays

There are two sorted arrays nums1 and nums2 of size m and n respectively.

Find the median of the two sorted arrays. The overall run time complexity should be O(log (m+n)).

You may assume nums1 and nums2 cannot be both empty.

Example 1:

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

The median is 2.0

Example 2:

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

The median is (2 + 3)/2 = 2.5

这道题很有意思,虽然本科的时候就做过练习题,用二分查找递归的减少数据规模,但是当时只写过伪代码,这次真正写代码提交的时候,发现有很多BUG。

这个题的思路有3种:1)两个数组放一起排序,取中位数O((m+n)lg(m+n));2)用归并排序的方式把他们Merge到一块,取中间数O(m+n),其实边Merge边数数,Merge到(m+n)/2个数的时候就已经是中位数了;3)二分的思路,比较A,B数组的中位数,根据大小关系选择在前半段或后半段继续递归。
我开始尝试了二分的思路,很多测试样例不能通过,要考虑奇偶性等问题也导致代码不够优雅。查看网络发现可以把这一题转换为从两个已排序数组A,B中取第k小的元素的问题,如果是m+n是奇数,则取中间那个数,偶数则取中间两个数求平均。

关于从两个已排序数组A,B中取第k小的元素的问题,这篇文章有较详细的讨论

下面简要讲一下最后一种O(lgm+lgn)的方法。

如果A的某个元素$A_i$和B的某两个连续元素$B_j$和$B_{j-1}$有关系$B_{j-1}\leq A_i\leq B_j$,那么可以确定$A_i$就是AB合起来的第$i+j+1$小的元素,因为$A_i$大于A中前$i$个元素,$A_i$又大于$B_{j-1}$,而$B_{j-1}$又大于B前面$j-1$个元素,所以在AB合起来的数组中,$A_i$大于A前面$i$个元素和B前面$j$个元素,排在第$i+j+1$的位置。同理如果$A_{i-1}\leq B_j\leq A_i$,则$B_j$是第$i+j+1$小的元素。
如果我们令$k=i+j+1$,则可以容易得到第$k$小的元素。关于i,j的取值问题,理论上i,j可以取任意值,只要满足$i+j=k-1$就可以了。不过常见的取法是先取$i$为A的中点,然后取$j=k-1-i$;或者按A,B元素个数的比例取值,比如上述链接中的代码。

上述链接中给出了如果A,B中不存在相同元素时的代码,如下:

int findKthSmallest(int A[], int m, int B[], int n, int k)
{
    assert(m >= 0);
    assert(n >= 0);
    assert(k > 0);
    assert(k <= m + n);
    int i = (int)((double)m / (m + n) * (k – 1));
    int j = (k – 1) – i;
    assert(i >= 0);
    assert(j >= 0);
    assert(i <= m);
    assert(j <= n); // invariant: i + j = k-1 // Note: A[-1] = -INF and A[m] = +INF to maintain invariant
    int Ai_1 = ((i == 0) ? INT_MIN : A[i – 1]);
    int Bj_1 = ((j == 0) ? INT_MIN : B[j – 1]);
    int Ai = ((i == m) ? INT_MAX : A[i]);
    int Bj = ((j == n) ? INT_MAX : B[j]);
    if (Bj_1 < Ai && Ai < Bj)
        return Ai;
    else if (Ai_1 < Bj && Bj < Ai)
        return Bj;
    assert((Ai > Bj && Ai_1 > Bj) || (Ai < Bj && Ai < Bj_1)); // if none of the cases above, then it is either:
    if (Ai < Bj) // exclude Ai and below portion // exclude Bj and above portion
        return findKthSmallest(A + i + 1, m – i – 1, B, j, k – i – 1);
    else /* Bj < Ai */ // exclude Ai and above portion // exclude Bj and below portion
        return findKthSmallest(A, i, B + j + 1, n – j – 1, k – j – 1);
}

针对本题C++代码如下:

class Solution {
public:
    double findKthSmallest(vector<int>& nums1, vector<int>& nums2, int k) //find kth smallest
    {
        int m = nums1.size(), n = nums2.size(); //always assume that m is equal or smaller than n
        if (m > n)
            return findKthSmallest(nums2, nums1, k);
        if (m == 0)
            return nums2[k – 1];
        if (k == 1)
            return min(nums1[0], nums2[0]); //divide k into two parts
        int i = (int)((double)m / (m + n) * (k – 1));
        int j = (k – 1) – i;
        int Ai_1 = ((i == 0) ? INT_MIN : nums1[i – 1]);
        int Bj_1 = ((j == 0) ? INT_MIN : nums2[j – 1]);
        int Ai = ((i == m) ? INT_MAX : nums1[i]);
        int Bj = ((j == n) ? INT_MAX : nums2[j]);
        if (Bj_1 < Ai && Ai < Bj)
            return Ai;
        else if (Ai_1 < Bj && Bj < Ai)
            return Bj;
        if (Ai < Bj) {
            vector<int> v1(nums1.begin() + i + 1, nums1.end());
            vector<int> v2(nums2.begin(), nums2.begin() + j);
            return findKthSmallest(v1, v2, k – i – 1);
        }
        else if (Ai > Bj) {
            vector<int> v1(nums1.begin(), nums1.begin() + i);
            vector<int> v2(nums2.begin() + j + 1, nums2.end());
            return findKthSmallest(v1, v2, k – j – 1);
        }
        else
            return Ai;
    }
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2)
    {
        int total = nums1.size() + nums2.size();
        if (total & 0x01)
            return findKthSmallest(nums1, nums2, total / 2 + 1);
        else
            return (findKthSmallest(nums1, nums2, total / 2) + findKthSmallest(nums1, nums2, total / 2 + 1)) / 2;
    }
};

本代码提交AC,用时52MS。

二刷。
上面的解法真的太复杂了,完全记不住。从两个有序数组中求第K大的数,在http://www.geeksforgeeks.org/k-th-element-two-sorted-arrays/有非常详细的解法,归并O(m+n)的解法就不说了。
O(lgn+lgm)的解法。假设arr1和arr2的中位数分别是arr1[mid1]和arr2[mid2],如果mid1+mid2<k,说明两个中位数的位置都太小了,第k大的数比较大。如果arr1[mid1]<arr2[mid2],则mid1左边是这四块中最小的一块,k肯定不在这里,所以可以递归在[mid1+1,end1]和[start2,end2]之间找;arra1[mid1]>arr2[mid2]的情况类似。
如果mid1+mid2>k,说明第K大的数比较小,根据上面的分析,下次递归时,可以舍弃掉[start1,mid1], (mid1,end1], [start2,mid2], (mid2,end2]中较大的那块。比如arr1[mid1]>arr2[mid2],则可以舍弃掉(mid2,end2]。
这样每次都可以舍弃掉两个数组中的某个的一半,时间复杂度是O(lgm+lgn),代码在GeeksforGeeks中。
更优的O(lg(k))的方法是,每次我们不是取中值,而是取arr1[k/2]和arr2[k/2],直接根据这两个值的大小关系去分割递归。代码见GeeksforGeeks中最后一个版本的代码,简洁。
针对这一题,求两个有序数组的中位数,如果两个有序数组的总长度len是奇数,则中位数就是两个有序数组中的第len/2大数;如果len是偶数,则中位数是第len/2和len/2的平均数,所以最多需要两次调用findKth就可以得到结果。完整代码如下:

class Solution {
private:
    int findKth(vector<int>& nums1, int s1, int e1, vector<int>& nums2, int s2, int e2, int k)
    {
        int len1 = e1 – s1 + 1, len2 = e2 – s2 + 1;
        if (len1 > len2)
            return findKth(nums2, s2, e2, nums1, s1, e1, k);
        if (len1 == 0)
            return nums2[s2 + k – 1];
        if (k == 1)
            return min(nums1[s1], nums2[s2]);
        int i = s1 + min(k / 2, len1) – 1, j = s2 + min(k / 2, len2) – 1;
        if (nums1[i] > nums2[j])
            return findKth(nums1, s1, e1, nums2, j + 1, e2, k – (j – s2 + 1));
        else
            return findKth(nums1, i + 1, e1, nums2, s2, e2, k – (i – s1 + 1));
    }
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2)
    {
        int m = nums1.size(), n = nums2.size();
        if ((m + n) % 2) {
            return findKth(nums1, 0, m – 1, nums2, 0, n – 1, (m + n) / 2 + 1);
        }
        else {
            return (findKth(nums1, 0, m – 1, nums2, 0, n – 1, (m + n) / 2) + findKth(nums1, 0, m – 1, nums2, 0, n – 1, (m + n) / 2 + 1)) / 2.0;
        }
    }
};

本代码提交AC,用时65MS。

三刷。

这题有点难,还是看官方题解吧: https://leetcode.com/problems/median-of-two-sorted-arrays/solution/

          left_part          |        right_part
    A[0], A[1], ..., A[i-1]  |  A[i], A[i+1], ..., A[m-1]
    B[0], B[1], ..., B[j-1]  |  B[j], B[j+1], ..., B[n-1]

如上图所示。其实本质是我们要对数组A和B进行划分,使得left_part_A加left_part_B的元素个数等于right_part_A加right_part_B的元素个数。因为总元素个数是m+n,所以总的left_part的个数应该是(m+n)/2,当然为了统一处理奇偶问题,可以让left_part数目多一点为(m+n+1)/2。那么很自然的,如果A的划分点为i的话,为了满足总的left_part数目为(m+n+1)/2,则B的划分点就要为(m+n+1)/2-i。

也就是说,如果A的划分点为i,B的划分点为(m+n+1)/2-i时,我们就能保证left_part和right_part的数目相等。我们可以想象一下,i和j相当于两个隔板,当i向右移动时,left_part_A元素增加,为了维持平衡,则j一定要向左移动。那么问题就转换为这个i到底等于多少才能找到中位数。

中位数的含义是,所有左边的数都小于它,所有右边的数都大于它。因为A[i-1]<A[i]和B[j-1]<B[j]是天然成立的,为了找中位数,还需要保证A[i-1]<B[j]和B[j-1]<A[i]。所有,我们找i的过程就是在找满足A[i-1]<B[j]和B[j-1]<A[i]的i的过程。

那么,很自然的解法就是,假设i是A的中点,即二分m/2,求出对应的j的位置:(m+n+1)/2-i。然后看看A[i-1]<B[j]和B[j-1]<A[i]是否都满足,如果是的话,就找到了i和j,也就找到了中位数,中位数就是在i和j隔板附近的4个数中间。如果不满足A[i-1]<B[j],即A[i-1]>B[j],说明i的隔板太靠右了,导致A[i-1]太大了,所以要把i往左移一点。类似的,如果不满足B[j-1]<A[i],则把i往右移一点。

如果还不理解的话,可以看油管上的视频讲解: https://youtu.be/LPFhl65R7ww,下面的代码就是模仿这个视频写的。

class Solution {
public:
	double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
		int m = nums1.size(), n = nums2.size();
		if (m > n)return findMedianSortedArrays(nums2, nums1);
		int low = 0, high = m;
		while (low <= high) {
			int partition1 = (low + high) / 2;
			int partition2 = (m + n + 1) / 2 - partition1;
			
			int maxleft1 = (partition1 == 0) ? INT_MIN : nums1[partition1 - 1];
			int minright1 = (partition1 == m) ? INT_MAX : nums1[partition1];

			int maxleft2 = (partition2 == 0) ? INT_MIN : nums2[partition2 - 1];
			int minright2 = (partition2 == n) ? INT_MAX : nums2[partition2];

			if (maxleft1 <= minright2 && maxleft2 <= minright1) {
				if ((m + n) % 2 == 0) {
					int mid1 = maxleft1 > maxleft2 ? maxleft1 : maxleft2;
					int mid2 = minright1 < minright2 ? minright1 : minright2;
					return (mid1 + mid2) / 2.0;
				}
				else {
					return maxleft1 > maxleft2 ? maxleft1 : maxleft2;
				}
			}
			else if (maxleft1 > minright2) {
				high = partition1 - 1;
			}
			else if (maxleft2 > minright1) {
				low = partition1 + 1;
			}
		}
		return 0;
	}
};

本代码提交AC,用时24MS。