什么是二分查找
二分查找是计算机科学中最基本、最有用的算法之一。 它描述了在有序集合中搜索特定值的过程。
二分查找中使用的术语:
- 目标 Target —— 你要查找的值
- 索引 Index —— 你要查找的当前位置
- 左、右指示符 Left,Right —— 我们用来维持查找空间的指标
- 中间指示符 Mid —— 我们用来应用条件来确定我们应该向左查找还是向右查找的索引
它是如何工作的?
在最简单的形式中,二分查找对具有指定左索引和右索引的连续序列进行操作。这就是所谓的查找空间。二分查找维护查找空间的左、右和中间指示符,并比较查找目标或将查找条件应用于集合的中间值;如果条件不满足或值不相等,则清除目标不可能存在的那一半,并在剩下的一半上继续查找,直到成功为止。如果查以空的一半结束,则无法满足条件,并且无法找到目标。
注意:
二进制搜索可以采用许多替代形式,并且可能并不总是直接搜索特定值。有时您希望应用特定条件或规则来确定接下来要搜索的哪一侧(左侧或右侧)。
leetCode 704. 二分查找
给定一个 n
个元素有序的(升序)整型数组 nums
和一个目标值 target
,写一个函数搜索 nums
中的 target
,如果目标值存在返回下标,否则返回 -1
。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示:
- 你可以假设
nums
中的所有元素是不重复的。 n
将在[1, 10000]
之间。nums
的每个元素都将在[-9999, 9999]
之间。
Solution1. C
int search(int* nums, int numsSize, int target) {
int low = 0;
int high = numsSize;
while(low < high)
{
int mid = (low + high) / 2;
if(nums[mid] == target)
return mid;
else if(nums[mid] > target)
high = mid;
else
low = mid + 1;
}
return -1;
}
Solution2. Java
class Solution {
public int search(int[] nums, int target) {
int low = 0;
int high = nums.length;
while(low < high) {
int mid = (low + high) / 2;
if(nums[mid] == target)
return mid;
else if(nums[mid] > target)
high = mid;
else
low = mid + 1;
}
return -1;
}
}
Solution3. Python3
class Solution:
def search(self, nums: List[int], target: int) -> int:
low, high = 0, len(nums)
while low < high:
mid = (low + high) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
low = mid + 1
else:
high = mid
return -1
Solution4. Python3
class Solution:
def search(self, nums: List[int], target: int) -> int:
index = bisect.bisect_left(nums, target)
return index if index < len(nums) and nums[index] == target else -1
识别和模板简介
- 如何识别二分查找?
如前所述,二分查找是一种在每次比较之后将查找空间一分为二的算法。每次需要查找集合中的索引或元素时,都应该考虑二分查找。如果集合是无序的,我们可以总是在应用二分查找之前先对其进行排序。
- 成功的二分查找的 3 个部分
二分查找一般由三个主要部分组成:
1.预处理 —— 如果集合未排序,则进行排序。
2.二分查找 —— 使用循环或递归在每次比较后将查找空间划分为两半。
3.后处理 —— 在剩余空间中确定可行的候选者。
- 3 个二分查找模板
当我们第一次学会二分查找时,我们可能会挣扎。我们可能会在网上研究数百个二分查找问题,每次我们查看开发人员的代码时,它的实现似乎都略有不同。尽管每个实现在每个步骤中都会将问题空间划分为原来的 1/2,但其中有许多问题:
- 为什么执行方式略有不同?
- 开发人员在想什么?
- 哪种方法更容易?
- 哪种方法更好?
经过许多次失败的尝试并拉扯掉大量的头发后,我们找到了三个主要的二分查找模板。为了防止脱发,并使新的开发人员更容易学习和理解,我们在接下来的章节中提供了它们。
- 二分查找模板 I
模板 #1:
C++:
int binarySearch(vector<int>& nums, int target){
if(nums.size() == 0)
return -1;
int left = 0, right = nums.size() - 1;
while(left <= right){
// Prevent (left + right) overflow
int mid = left + (right - left) / 2;
if(nums[mid] == target){ return mid; }
else if(nums[mid] < target) { left = mid + 1; }
else { right = mid - 1; }
}
// End Condition: left > right
return -1;
}
Java:
int binarySearch(int[] nums, int target){
if(nums == null || nums.length == 0)
return -1;
int left = 0, right = nums.length - 1;
while(left <= right){
// Prevent (left + right) overflow
int mid = left + (right - left) / 2;
if(nums[mid] == target){ return mid; }
else if(nums[mid] < target) { left = mid + 1; }
else { right = mid - 1; }
}
// End Condition: left > right
return -1;
}
Python:
def binarySearch(nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
if len(nums) == 0:
return -1
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
# End Condition: left > right
return -1
模板 #1 是二分查找的最基础和最基本的形式。这是一个标准的二分查找模板,大多数高中或大学会在他们第一次教学生计算机科学时使用。模板 #1 用于查找可以通过访问数组中的单个索引来确定的元素或条件。
- 关键属性
二分查找的最基础和最基本的形式。
查找条件可以在不与元素的两侧进行比较的情况下确定(或使用它周围的特定元素)。
不需要后处理,因为每一步中,你都在检查是否找到了元素。如果到达末尾,则知道未找到该元素。
- 区分语法
初始条件:left = 0, right = length-1
终止:left > right
向左查找:right = mid-1
向右查找:left = mid+1
leetCode 374. 猜数字大小
我们正在玩一个猜数字游戏。 游戏规则如下:我从 1 到 n 选择一个数字。 你需要猜我选择了哪个数字。每次你猜错了,我会告诉你这个数字是大了还是小了。你调用一个预先定义好的接口 guess(int num)
,它会返回 3 个可能的结果(-1
,1
或 0
):
-1 : 我的数字比较小
1 : 我的数字比较大
0 : 恭喜!你猜对了!
示例 :
输入: n = 10, pick = 6
输出: 6
Solution1. C++
// Forward declaration of guess API.
// @param num, your guess
// @return -1 if my number is lower, 1 if my number is higher, otherwise return 0
int guess(int num);
class Solution {
public:
int guessNumber(int n) {
int left = 0;
int right = n;
while(left <= right)
{
int mid = left + (right - left) / 2;
if(!guess(mid))
return mid;
else if(guess(mid) == -1)
right = mid - 1;
else if(guess(mid) == 1)
left = mid + 1;
else
return -1;
}
return -1;
}
};
Solution2. Java
/* The guess API is defined in the parent class GuessGame.
@param num, your guess
@return -1 if my number is lower, 1 if my number is higher, otherwise return 0
int guess(int num); */
public class Solution extends GuessGame {
public int guessNumber(int n) {
int left = 0;
int right = n;
while(left <= right) {
int mid = left + (right - left) / 2;
if(guess(mid) == -1)
right = mid - 1;
else if(guess(mid) == 0)
return mid;
else if(guess(mid) == 1)
left = mid + 1;
else
return -1;
}
return -1;
}
}
Solution3. Python3
# The guess API is already defined for you.
# @param num, your guess
# @return -1 if my number is lower, 1 if my number is higher, otherwise return 0
# def guess(num):
class Solution(object):
def guessNumber(self, n):
"""
:type n: int
:rtype: int
"""
left, right = 0, n
while left <= right:
mid = (left + right) // 2
if guess(mid) == 0:
return mid
elif guess(mid) == -1:
right = mid - 1
elif guess(mid) == 1:
left = mid + 1
else:
return -1
return -1
- 二分查找模板 II
模板 #2:
C++:
int binarySearch(vector<int>& nums, int target){
if(nums.size() == 0)
return -1;
int left = 0, right = nums.size();
while(left < right){
// Prevent (left + right) overflow
int mid = left + (right - left) / 2;
if(nums[mid] == target){ return mid; }
else if(nums[mid] < target) { left = mid + 1; }
else { right = mid; }
}
// Post-processing:
// End Condition: left == right
if(left != nums.size() && nums[left] == target) return left;
return -1;
}
Java:
int binarySearch(int[] nums, int target){
if(nums == null || nums.length == 0)
return -1;
int left = 0, right = nums.length;
while(left < right){
// Prevent (left + right) overflow
int mid = left + (right - left) / 2;
if(nums[mid] == target){ return mid; }
else if(nums[mid] < target) { left = mid + 1; }
else { right = mid; }
}
// Post-processing:
// End Condition: left == right
if(left != nums.length && nums[left] == target) return left;
return -1;
}
Python:
def binarySearch(nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
if len(nums) == 0:
return -1
left, right = 0, len(nums)
while left < right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid
# Post-processing:
# End Condition: left == right
if left != len(nums) and nums[left] == target:
return left
return -1
模板 #2 是二分查找的高级模板。它用于查找需要访问数组中当前索引及其直接右邻居索引的元素或条件。
关键属性
一种实现二分查找的高级方法。
查找条件需要访问元素的直接右邻居。
使用元素的右邻居来确定是否满足条件,并决定是向左还是向右。
保证查找空间在每一步中至少有 2 个元素。
需要进行后处理。 当你剩下 1 个元素时,循环 / 递归结束。 需要评估剩余元素是否符合条件。
区分语法
- 初始条件:
left = 0, right = length
- 终止:
left == right
- 向左查找:
right = mid
- 向右查找:
left = mid+1
leetCode 278. 第一个错误的版本
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。假设你有 n
个版本 [1, 2, ..., n]
,你想找出导致之后所有版本出错的第一个错误的版本。你可以通过调用 bool isBadVersion(version)
接口来判断版本号 version
是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
示例:
给定 n = 5,并且 version = 4 是第一个错误的版本。
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本。
Solution1. C
// Forward declaration of isBadVersion API.
bool isBadVersion(int version);
int firstBadVersion(int n) {
int left = 1;
int right = n;
while(left < right)
{
int mid = left + (right - left) / 2;
if(isBadVersion(mid))
right = mid;
else
left = mid + 1;
}
return left;
}
Solution2. Java
/* The isBadVersion API is defined in the parent class VersionControl.
boolean isBadVersion(int version); */
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
int left = 1;
int right = n;
while(left < right) {
int mid = left + (right - left) / 2;
if(isBadVersion(mid))
right = mid;
else
left = mid + 1;
}
return left;
}
}
Solution3. Python3
# The isBadVersion API is already defined for you.
# @param version, an integer
# @return a bool
# def isBadVersion(version):
class Solution:
def firstBadVersion(self, n):
"""
:type n: int
:rtype: int
"""
left, right = 1, n
while left < right:
mid = (left + right) // 2
if isBadVersion(mid):
right = mid
else:
left = mid + 1
return left
leetCode 162. 寻找峰值
峰值元素是指其值大于左右相邻值的元素。给定一个输入数组 nums
,其中 nums[i] ≠ nums[i+1]
,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。你可以假设 nums[-1] = nums[n] = -∞
。
示例 1:
输入: nums = [1,2,3,1]
输出: 2
解释: 3 是峰值元素,你的函数应该返回其索引 2。
示例 2:
输入: nums = [1,2,1,3,5,6,4]
输出: 1 或 5
解释: 你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。
说明:
你的解法应该是 O(logN) 时间复杂度的。
Solution1. C
int findPeakElement(int* nums, int numsSize) {
int left = 0;
int right = numsSize - 1;
while(left < right)
{
int mid = left + (right - left) / 2;
int midNext = mid + 1;
if(nums[mid] > nums[midNext])
right = mid;
else
left = midNext;
}
return left;
}
Solution2. Java
class Solution {
public int findPeakElement(int[] nums) {
int left = 0, right = nums.length - 1;
while(left < right) {
int mid = left + (right - left) / 2;
int midNext = mid + 1;
if(nums[mid] < nums[midNext])
left = midNext;
else
right = mid;
}
return left;
}
}
Solution3. Python3
class Solution:
def findPeakElement(self, nums: List[int]) -> int:
left, right = 0, len(nums)-1
while left < right:
mid = (left + right) // 2
midNext = mid + 1
if nums[mid] > nums[midNext]:
right = mid
else:
left = midNext
return left
leetCode 153. 寻找旋转排序数组中的最小值
假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [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
Solution1. C
int findMin(int* nums, int numsSize) {
int left = 0;
int right = numsSize - 1;
while(left < right)
{
if(nums[left] < nums[right])
return nums[left];
int mid = left + (right - left) / 2;
if(nums[mid] >= nums[left])
left = mid + 1;
else
right = mid;
}
return nums[left];
}
Solution2. Java
class Solution {
public int findMin(int[] nums) {
int left = 0, right = nums.length - 1;
while(left < right) {
if(nums[left] < nums[right])
return nums[left];
int mid = left + (right - left) / 2;
if(nums[mid] >= nums[left])
left = mid + 1;
else
right = mid;
}
return nums[left];
}
}
Solution3. Python3
class Solution:
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
while left < right:
if nums[left] < nums[right]:
return nums[left]
mid = (left + right) // 2
if nums[mid] >= nums[left]:
left = mid + 1
else:
right = mid
return nums[left]
二分查找模板 III
模板 #3:
C++:
int binarySearch(vector<int>& nums, int target){
if (nums.size() == 0)
return -1;
int left = 0, right = nums.size() - 1;
while (left + 1 < right){
// Prevent (left + right) overflow
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid;
} else {
right = mid;
}
}
// Post-processing:
// End Condition: left + 1 == right
if(nums[left] == target) return left;
if(nums[right] == target) return right;
return -1;
}
Java:
int binarySearch(int[] nums, int target) {
if (nums == null || nums.length == 0)
return -1;
int left = 0, right = nums.length - 1;
while (left + 1 < right){
// Prevent (left + right) overflow
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid;
} else {
right = mid;
}
}
// Post-processing:
// End Condition: left + 1 == right
if(nums[left] == target) return left;
if(nums[right] == target) return right;
return -1;
}
Python:
def binarySearch(nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
if len(nums) == 0:
return -1
left, right = 0, len(nums) - 1
while left + 1 < right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid
else:
right = mid
# Post-processing:
# End Condition: left + 1 == right
if nums[left] == target: return left
if nums[right] == target: return right
return -1
模板 #3 是二分查找的另一种独特形式。 它用于搜索需要访问当前索引及其在数组中的直接左右邻居索引的元素或条件。
关键属性
实现二分查找的另一种方法。
搜索条件需要访问元素的直接左右邻居。
使用元素的邻居来确定它是向右还是向左。
保证查找空间在每个步骤中至少有 3 个元素。
需要进行后处理。 当剩下 2 个元素时,循环 / 递归结束。 需要评估其余元素是否符合条件。
区分语法
- 初始条件:
left = 0, right = length-1
- 终止:
left + 1 == right
- 向左查找:
right = mid
- 向右查找:
left = mid
leetCode 34. 在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 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]
Solution1. C++ 两轮二分查找 先找左边界 再找右边界
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
vector<int> ret(2, -1);
while(left < right)
{
int mid = (left + right) / 2;
if(nums[mid] < target)
left = mid + 1;
else
right = mid;
}
if(!nums.size() || nums[left] != target)
return ret;
else
ret[0] = left;
right = nums.size() - 1;
while(left < right)
{
int mid = (left + right) / 2 + 1;
if(nums[mid] > target)
right = mid - 1;
else
left = mid;
}
ret[1] = right;
return ret;
}
};
Solution2. C
/**
* Return an array of size *returnSize.
* Note: The returned array must be malloced, assume caller calls free().
*/
int* searchRange(int* nums, int numsSize, int target, int* returnSize) {
int left = 0, right = numsSize - 1;
*returnSize = 2;
int* result = (int*)malloc(sizeof(int) * 2);
result[0] = -1;
result[1] = -1;
while(left < right) {
int mid = left + (right - left) / 2;
if(nums[mid] < target)
left = mid + 1;
else
right = mid;
}
if(numsSize==0 || nums[left]!=target)
return result;
result[0] = left;
right = numsSize - 1;
while(left < right) {
int mid = left + (right - left) / 2 + 1;
if(nums[mid] > target)
right = mid - 1;
else
left = mid;
}
result[1] = right;
return result;
}
Solution3. Java
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] result = new int[2];
result[0] = -1;
result[1] = -1;
int left = 0, right = nums.length - 1;
while(left < right) {
int mid = left + (right - left) / 2;
if(nums[mid] < target)
left = mid + 1;
else
right = mid;
}
if(nums.length==0 || nums[left]!=target)
return result;
result[0] = left;
right = nums.length - 1;
while(left < right) {
int mid = left + (right - left) / 2 + 1;
if(nums[mid] > target)
right = mid - 1;
else
left = mid;
}
result[1] = right;
return result;
}
}
Solution4. Python3
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
result = [-1, -1]
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid
if len(nums)==0 or nums[left]!=target:
return result
result[0] = left
right = len(nums) - 1
while left < right:
mid = (left + right) // 2 + 1
if nums[mid] > target:
right = mid - 1
else:
left = mid
result[1] = right
return result
leetCode 658. 找到 K 个最接近的元素
给定一个排序好的数组,两个整数 k
和 x
,从数组中找到最靠近 x
(两数之差最小)的 k
个数。返回的结果必须要是按升序排好的。如果有两个数与 x
的差值一样,优先选择数值较小的那个数。
示例 1:
输入: [1,2,3,4,5], k=4, x=3
输出: [1,2,3,4]
示例 2:
输入: [1,2,3,4,5], k=4, x=-1
输出: [1,2,3,4]
说明:
- k 的值为正数,且总是小于给定排序数组的长度。
- 数组不为空,且长度不超过 104
- 数组里的每个元素与 x 的绝对值不超过 104
更新(2017/9/19):
这个参数 arr 已经被改变为一个整数数组(而不是整数列表)。 请重新加载代码定义以获取最新更改。
Solution1. C
/**
* Return an array of size *returnSize.
* Note: The returned array must be malloced, assume caller calls free().
*/
int* findClosestElements(int* arr, int arrSize, int k, int x, int* returnSize) {
int left = 0, right = arrSize - k;
while(left < right) {
int mid = left + (right - left) / 2;
if(abs(arr[mid]-x) > abs(arr[mid+k]-x))
left = mid + 1;
else
right = mid;
}
int* result = (int*)malloc(sizeof(int) * k);
for(int i=0; i<k; ++i)
result[i] = arr[left+i];
*returnSize = k;
return result;
}
Solution2. Java
class Solution {
public List<Integer> findClosestElements(int[] arr, int k, int x) {
int left = 0, right = arr.length - k;
while(left < right) {
int mid = left + (right - left) / 2;
if(Math.abs(arr[mid] - x) > Math.abs(arr[mid+k] - x))
left = mid + 1;
else
right = mid;
}
List<Integer> result = new LinkedList<Integer>();
for(int i=left; i<left+k; ++i)
result.add(arr[i]);
return result;
}
}
Solution3. Python3
class Solution:
def findClosestElements(self, arr: List[int], k: int, x: int) -> List[int]:
left, right = 0, len(arr) - k
while left < right:
mid = (left + right) //2
if abs(arr[mid]-x) > abs(arr[mid+k]-x):
left = mid + 1
else:
right = mid
return arr[left:left+k]
二分查找模板分析
模板说明:
你在网上看到的 99% 的二分查找问题会归结于这 3 个模板中的一个。有些问题可以使用多个模板来实现,但是当你做更多的练习时,你会注意到一些模板比其他模板更适合某些问题。
注意:模板和它们的差异已被彩色标注如下。
这 3 个模板的不同之处在于:
- 左、中、右索引的分配。
- 循环或递归终止条件。
- 后处理的必要性。
模板 #1 和 #3 是最常用的,几乎所有二分查找问题都可以用其中之一轻松实现。模板 #2 更 高级一些,用于解决某些类型的问题。这 3 个模板中的每一个都提供了一个特定的用例:
模板 #1 (left <= right):
- 二分查找的最基础和最基本的形式。
- 查找条件可以在不与元素的两侧进行比较的情况下确定(或使用它周围的特定元素)。
- 不需要后处理,因为每一步中,你都在检查是否找到了元素。如果到达末尾,则知道未找到该元素。
模板 #2 (left < right):
- 一种实现二分查找的高级方法。
- 查找条件需要访问元素的直接右邻居。
- 使用元素的右邻居来确定是否满足条件,并决定是向左还是向右。
- 保证查找空间在每一步中至少有 2 个元素。
- 需要进行后处理。 当你剩下 1 个元素时,循环 / 递归结束。 需要评估剩余元素是否符合条件。
模板 #3 (left + 1 < right):
- 实现二分查找的另一种方法。
- 搜索条件需要访问元素的直接左右邻居。
- 使用元素的邻居来确定它是向右还是向左。
- 保证查找空间在每个步骤中至少有 3 个元素。
- 需要进行后处理。 当剩下 2 个元素时,循环 / 递归结束。 需要评估其余元素是否符合条件。
时间和空间复杂度:
时间:O(log n)
—— 算法时间
因为二分查找是通过对查找空间中间的值应用一个条件来操作的,并因此将查找空间折半,在更糟糕的情况下,我们将不得不进行 O(log n) 次比较,其中 n 是集合中元素的数目。
为什么是
log n?
- 二分查找是通过将现有数组一分为二来执行的。
- 因此,每次调用子例程(或完成一次迭代)时,其大小都会减少到现有部分的一半。
- 首先
N
变成N/2
,然后又变成N/4
,然后继续下去,直到找到元素或尺寸变为1
。 - 迭代的最大次数是
log N
(base 2) 。
空间:O(1)
—— 常量空间
虽然二分查找确实需要跟踪 3 个指标,但迭代解决方案通常不需要任何其他额外空间,并且可以直接应用于集合本身,因此需要 O(1)
或常量空间。
其他类型的二分查找:双数组的二分查找
leetCode 4. 寻找两个有序数组的中位数
给定两个大小为 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
Solution1. C++ O(n)的算法 没有使用二分查找
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int size1 = nums1.size();
int size2 = nums2.size();
double result = 0.00;
int order=0;
int order1=0;
int order2=0;
vector<int> nums;
if((size1+size2) % 2)
order = (size1+size2)/2;
else
{
order1 = (size1+size2)/2 - 1;
order2 = (size1+size2)/2;
}
vector<int>::iterator iter1 = nums1.begin();
vector<int>::iterator iter2 = nums2.begin();
while(iter1!=nums1.end() || iter2!=nums2.end())
{
if(iter1!=nums1.end() && iter2!=nums2.end())
{
if(*iter1 <= *iter2)
nums.push_back(*(iter1++));
else if(*iter1 > *iter2)
nums.push_back(*(iter2++));
}
else if(iter2 == nums2.end())
nums.push_back(*(iter1++));
else if(iter1 == nums1.end())
nums.push_back(*(iter2++));
int count = nums.size() - 1;
if(((size1+size2) % 2) && (count == order))
{
result = nums[count];
return result;
}
else if(!((size1 + size2)%2) && (count == order2))
{
result = (nums[count] + nums[count-1]) / 2.00;
return result;
}
}
return result;
}
};
以下都是使用二分查找的解法,二分查找的思路:
求有序数组A和B的中位数
将A、B分成左右两部分:
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]
如果可以保证下面两个条件:
1. len(left_part) == len(right_part)
2. max(left_part) <= min(right_part)
则表示我们将{A,B}中所有的元素分成了两个相等长度的部分,而且右半边始终大于左半边,此时中位数=(max(left_part)+min(right_par))/2。
为了保证上面提出的两个条件,我们只需要保证:
1. i + j = m - i + n - j + 1 如果n >= m,我们只需要设置 i = 0 ~ m, j = (m+n+1)/2-i
2. B[j-1] <= A[i] and A[i-1] <= B[j]
使用二分查找描述算法描述如下:
1. imin = 0, imax = m, 在[imin, imax]范围内搜索i∈[0,m],j=(m+n+1)/2-i,满足B[j-1]<=A[i]且A[i-1]<=B[j]
2. 设置i=(imin+imax)/2, j=(m+n+1)/2-i
3. 此时len(left_part)==len(right_part),会出现下述三种情况:
(1)B[j-1] <= A[i] and A[i-1] <= B[j]:找到满足的i,停止检索
(2)B[j-1] > A[i]:i太小,需要增加i,减小j,此时B[j-1]将减小而A[i]将增加,以满足B[j-1] <= A[i]
(3)A[i-1] > B[j]:i太大,需要减小i,增大j,此时B[j]将增加而A[i-1]将减小,以满足A[i-1]<=B[j]
4. 当检索到满足条件的i之后,则可以计算中位数:
max(A[i-1], B[j-1]) (when m + n is odd)
or (max(A[i-1], B[j-1]) + min(A[i], B[j]))/2 (when m + n is even)
Solution2. C
double findMedianSortedArrays(int* nums1, int nums1Size, int* nums2, int nums2Size) {
if(nums1Size > nums2Size)
return findMedianSortedArrays(nums2, nums2Size, nums1, nums1Size);
int imin = 0, imax = nums1Size, half_len = (nums1Size+nums2Size+1)/2;
int maxLeft = 0, minRight = 0;
while(imin <= imax) {
int i = (imin + imax) / 2;
int j = half_len - i;
if(i < nums1Size && nums2[j-1] > nums1[i])
imin = i + 1;
else if(i > 0 && nums1[i-1] > nums2[j])
imax = i - 1;
else
{
if(i == 0)
maxLeft = nums2[j-1];
else if(j == 0)
maxLeft = nums1[i-1];
else
maxLeft = nums1[i-1] > nums2[j-1] ? nums1[i-1] : nums2[j-1];
if((nums1Size+nums2Size) % 2)
return maxLeft;
if(i == nums1Size)
minRight = nums2[j];
else if(j == nums2Size)
minRight = nums1[i];
else
minRight = nums1[i] > nums2[j] ? nums2[j] : nums1[i];
return (maxLeft + minRight) / 2.0;
}
}
return 1;
}
Solution3. Java
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
int maxLeft = 0, minRight = 0;
if(m > n)
return findMedianSortedArrays(nums2, nums1);
int imin = 0, imax = m, half_len = (m+n+1)/2;
while(imin <= imax) {
int i = (imin + imax) / 2;
int j = half_len - i;
if(i < m && nums2[j-1] > nums1[i])
imin = i + 1;
else if(i > 0 && nums1[i-1] > nums2[j])
imax = i - 1;
else {
if(i == 0)
maxLeft = nums2[j-1];
else if(j==0)
maxLeft = nums1[i-1];
else
maxLeft = Integer.max(nums1[i-1], nums2[j-1]);
if((m+n)%2 == 1)
return maxLeft;
if(i == m)
minRight = nums2[j];
else if(j == n)
minRight = nums1[i];
else
minRight = Integer.min(nums1[i], nums2[j]);
return (maxLeft + minRight) / 2.0;
}
}
return 1;
}
}
Solution4. Python3
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
m, n = len(nums1), len(nums2)
if m > n:
nums1, nums2, m, n = nums2, nums1, n, m
if n == 0:
raise ValueError
imin, imax, half_len = 0, m, (m+n+1)//2
while imin <= imax:
i = (imin + imax) // 2
j = half_len - i
if i < m and nums2[j-1] > nums1[i]:
imin = i + 1
elif i > 0 and nums1[i-1] > nums2[j]:
imax = i - 1
else:
if i == 0:
max_of_left = nums2[j-1]
elif j == 0:
max_of_left = nums1[i-1]
else:
max_of_left = max(nums1[i-1], nums2[j-1])
if (m+n) % 2 == 1:
return max_of_left
if i == m:
min_of_right = nums2[j]
elif j == n:
min_of_right = nums1[i]
else:
min_of_right = min(nums1[i], nums2[j])
return (max_of_left + min_of_right) / 2.0