Leetcode 二分查找 2
题号直达: 34 查找区间 | 33 搜索旋转排序数组 | 153 旋转数组最小值 | 4 两数组中位数
LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置 ↑
题目
中等。 给定按非递减顺序排序的整数数组 nums 和目标值 target,找出 target 在数组中开始和结束的位置。
若 target 不存在,返回 [-1, -1]。
要求: 算法时间复杂度为 O(log n)。
测试样例
-
示例 1:
nums = [5,7,7,8,8,10],target = 8
输出:[3,4] -
示例 2:
nums = [5,7,7,8,8,10],target = 6
输出:[-1,-1] -
示例 3:
nums = [],target = 0
输出:[-1,-1]
提示
0 ≤ len(nums) ≤ 10^5-10^9 ≤ nums[i], target ≤ 10^9,nums非递减
代码(Python)解法一:先找任意命中,再向两侧二分边界
- 第一次二分:在整段上找任意一个
nums[mid] == target,记下ini_ind;若无则返回[-1,-1]。 - 左边界:在
[0, ini_ind]上二分,找最靠左的target:若nums[mid] == target且(mid == 0或nums[mid-1] < nums[mid]),则mid为左端点;若nums[mid] == target且左侧仍为target,则向左半继续。 - 右边界:在
[ini_ind, len(nums)-1]上二分,找最靠右的target:若nums[mid] == target且(mid == len-1或nums[mid+1] > nums[mid]),则mid为右端点;否则向右半继续。
class Solution(object):
def searchRange(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
left = 0
right = len(nums) - 1
ini_ind = -1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
ini_ind = mid
break
elif nums[mid] > target:
right = mid - 1
else:
left = mid + 1
if ini_ind == -1:
return [-1, -1]
right = ini_ind
left = 0
left_ind = -1
while left <= right:
mid = (left + right) // 2
if mid == 0 and nums[mid] == target:
left_ind = 0
break
elif nums[mid] < target:
left = mid + 1
elif nums[mid - 1] < nums[mid] and nums[mid] == target:
left_ind = mid
break
else:
right = mid - 1
right = len(nums) - 1
left = ini_ind
right_ind = -1
while left <= right:
mid = (left + right) // 2
if mid == len(nums) - 1 and nums[mid] == target:
right_ind = len(nums) - 1
break
elif nums[mid] > target:
right = mid - 1
elif nums[mid + 1] > nums[mid] and nums[mid] == target:
right_ind = mid
break
else:
left = mid + 1
return [left_ind, right_ind]
思路解析(解法一)
- 三次二分:第一次在全局定位
target是否存在;后两次在缩小的区间内分别找「第一个」与「最后一个」target,均保持 O(log n)(常数因子为 3,仍为 O(log n))。 - 左边界:
nums[mid] == target且左邻更小(或已在下标 0),即为区间左端。 - 右边界:
nums[mid] == target且右邻更大(或已在下标len-1),即为区间右端。 - 空数组:第一次二分
len(nums)==0时right=-1,循环不进入,ini_ind保持 -1,返回[-1,-1]。
代码(Python)解法二:lower_bound / upper_bound 两次二分
抽象出 binarySearch(nums, target, lower):在有序数组上找第一个满足条件的下标并写入 ans。
lower=True:找第一个nums[i] >= target(即lower_bound),对应区间左端点。lower=False:找第一个nums[i] > target(即upper_bound),右端点为该下标 - 1。
最后校验 leftIdx ≤ rightIdx 且两端均为 target,否则无命中。
class Solution(object):
def binarySearch(self, nums, target, lower):
left, right = 0, len(nums) - 1
ans = len(nums)
while left <= right:
mid = (left + right) // 2
if nums[mid] > target or (lower and nums[mid] >= target):
right = mid - 1
ans = mid
else:
left = mid + 1
return ans
def searchRange(self, nums, target):
leftIdx = self.binarySearch(nums, target, True)
rightIdx = self.binarySearch(nums, target, False) - 1
if (
leftIdx <= rightIdx
and rightIdx < len(nums)
and nums[leftIdx] == target
and nums[rightIdx] == target
):
return [leftIdx, rightIdx]
return [-1, -1]
思路解析(解法二)
- 统一模板:
nums[mid] > target或(lower且nums[mid] >= target)时向左收缩并记录ans,否则向右;结束时ans为第一个「大于 / 大于等于」分界位置。 - 右端点:
upper_bound是第一个> target的位置,-1即最后一个== target。 - 合法性:若不存在
target,leftIdx可能落在第一个> target处或越界侧,通过区间与端点值判断返回[-1,-1]。 - 复杂度:两次二分,O(log n) 时间,O(1) 额外空间。
LeetCode 33. 搜索旋转排序数组 ↑
题目
中等。 整数数组 nums 原本升序且互不相同,在某个未知下标处旋转后得到当前数组(例如 [0,1,2,4,5,6,7] 在下标 3 处旋转得到 [4,5,6,7,0,1,2])。给定 nums 与整数 target,若 target 在数组中则返回其下标,否则返回 -1。
要求: 时间复杂度 O(log n)。
测试样例
-
示例 1:
nums = [4,5,6,7,0,1,2],target = 0
输出:4 -
示例 2:
nums = [4,5,6,7,0,1,2],target = 3
输出:-1 -
示例 3:
nums = [1],target = 0
输出:-1
提示
1 ≤ len(nums) ≤ 5000-10^4 ≤ nums[i] ≤ 10^4,元素互不相同nums由升序数组在某一未知下标处旋转得到-10^4 ≤ target ≤ 10^4
代码(Python):先找旋转点(最小值下标),再二分
步骤 1: 用二分找最小值下标 pivot(nums[mid] > nums[right] 则最小值在右半段,否则在左半段含 mid)。步骤 2: 比较 target 与 nums[-1]:若 target ≤ nums[-1],则 target 只可能落在右半段有序区间 [pivot, n-1],否则落在左半段 [0, pivot-1]。步骤 3: 在选定区间上做普通二分。
class Solution(object):
def search(self, nums, target):
if not nums:
return -1
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] > nums[right]:
left = mid + 1
else:
right = mid
pivot = left
if target <= nums[-1]:
left, right = pivot, len(nums) - 1
else:
left, right = 0, pivot - 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
return -1
思路解析
- 旋转点:最小值所在位置把数组分成两段,各自内部仍升序;与
nums[right]比较可判断最小值在左还是右。 - 定区间:右段元素均
≤ nums[-1],左段元素均> nums[-1](无重复时成立),据此二选一做第三次二分。 - 空数组:直接返回
-1(与题目主数据len ≥ 1可并存)。 - 复杂度:两次二分过程均为 O(log n),总计 O(log n),O(1) 额外空间。
LeetCode 153. 寻找旋转排序数组中的最小值 ↑
题目
中等。 长度为 n 的数组原本升序且元素互不相同,经过 1~n 次「旋转」 得到 nums。一次旋转指把最后一个元素移到最前面。
给定旋转后的 nums,返回其中的最小值。
要求: 时间复杂度 O(log n)。
测试样例
-
示例 1:
nums = [3,4,5,1,2]
输出:1 -
示例 2:
nums = [4,5,6,7,0,1,2]
输出:0 -
示例 3:
nums = [11,13,15,17]
输出:11(旋转后仍整体升序,最小值即首元素)
提示
n == len(nums),1 ≤ n ≤ 5000-5000 ≤ nums[i] ≤ 5000,互不相同nums由升序数组经 1~n 次旋转得到
代码(Python):二分找最小值下标
与 33 题 中「找旋转点」相同:nums[mid] > nums[right] 时最小值在 mid 右侧,否则在 [left, mid]。循环结束时 left == right 即为最小值下标,返回 nums[pivot]。
class Solution(object):
def findMin(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
if not nums:
return -1
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] > nums[right]:
left = mid + 1
else:
right = mid
pivot = left
return nums[pivot]
思路解析
- 二分性质:右半段(含最小值)的数整体不超过
nums[right]一侧的「断点」;用与末尾比较可不断砍掉不含最小值的一半。 - 与 33 题关系:本题的
pivot就是 33 题里第一步求出的最小值下标,此处只需返回值nums[pivot]。 - 空数组:题目数据一般
n ≥ 1;若保留if not nums: return -1,需知这与部分 OJ 对返回类型的约定可能不一致,可按需要删去。 - 复杂度:O(log n) 时间,O(1) 额外空间。
LeetCode 4. 寻找两个正序数组的中位数 ↑
题目
困难。 给定两个大小分别为 m、n 的正序(升序)数组 nums1 与 nums2,求两个数组合并后中位数(若总个数为偶数,取中间两数的平均值)。
要求: 时间复杂度 O(log(m+n))。
测试样例
-
示例 1:
nums1 = [1,3],nums2 = [2]
输出:2.00000(合并为[1,2,3],中位数为2) -
示例 2:
nums1 = [1,2],nums2 = [3,4]
输出:2.50000(合并为[1,2,3,4],中位数为(2+3)/2)
提示
len(nums1) == m,len(nums2) == n0 ≤ m, n ≤ 1000,且1 ≤ m + n ≤ 2000-10^6 ≤ nums1[i], nums2[i] ≤ 10^6
代码(Python)解法一:合并后排序
将两数组合并为 nums1 + nums2,sort() 后取中位:长度为偶数时取中间两项的平均值(用 float(...)),为奇数时取正中间元素。
class Solution(object):
def findMedianSortedArrays(self, nums1, nums2):
"""
:type nums1: List[int]
:type nums2: List[int]
:rtype: float
"""
num = nums1 + nums2
num.sort()
if len(num) % 2 == 0:
return float(num[len(num) // 2] + num[len(num) // 2 - 1]) / 2
else:
return float(num[len(num) // 2])
思路解析(解法一)
- 正确性:合并排序后,中位数定义与题意一致。
- 复杂度:排序主导,为 O((m+n) log(m+n)) 时间,O(m+n) 额外空间(合并后的列表);不满足题面 O(log(m+n)) 要求,适合理解题意与对拍。
- 改进:需在两个有序数组上做划分 / 二分,使找第
k小或左右部分元素个数满足中位数定义,可达到 O(log(m+n))(可作为解法二单独整理)。

浙公网安备 33010602011771号