本篇文章为LeetCode数组模块关于二分查找内容的刷题笔记,仅供参考。
二分查找是一种低复杂度于解决有序数组的方法,主要的难点在于问题的终止条件,一旦处理不当极易陷入死循环。一般采用left、mid、right这3个指针,while(left<=right)
作外循环,每次判断后进行赋值:left=mid+1
或right=mid-1
,慎改变等号或者不作-1
,否则容易陷入死循环。
设计二分算法的时候,先宏观设计总体算法,后考虑终止条件下的特殊情况。在考虑终止条件的时候,一般只取两个元素和一个元素的情况,判断如何正确跳出循环。
看到一篇关于二分边界的文章,需要的时候再回来看。
Leetcode34.在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 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 <= nums.length <= 105
-109 <= nums[i] <= 109
nums 是一个非递减数组
-109 <= target <= 109
按照二分查找的思路,先根据left、mid、right三个指针不断二分确定某一个target的位置,然后再分别查找nums[left…mid]和nums[mid…right]两个部分的target开始位置和结束位置:
class Solution {
public:
int searchleft(vector<int>& nums,int left,int right){ //寻找左区间
int start=left;
int end=right;
int target=nums[right];
int mid;
while(start<=end){
mid=(start+end)/2;
if(nums[mid]==target){
end=mid-1;
if(end>=left&&nums[end]!=target) return mid;
}
else{
start=mid+1;
if(start<=right&&nums[start]==target) return start;
}
}
if(start<=right&&nums[start]==target) return start; //一定要保证start范围
else return right;
}
int searchright(vector<int>& nums,int left,int right){ //寻找右区间
int start=left;
int end=right;
int target=nums[left];
int mid;
while(start<=end){
mid=(start+end)/2;
if(nums[mid]==target){
start=mid+1;
if(start<=right&&nums[start]!=target) return mid;
}
else{
end=mid-1;
if(end>=left&&nums[end]==target) return end;
}
}
if(start<=right&&nums[start]==target) return start;
else return right;
}
vector<int> searchRange(vector<int>& nums, int target) {
int left=0;
int right=nums.size()-1;
int mid;
while(left<=right){
mid=(left+right)/2;
if(nums[mid]==target){ //寻找左右区间
int ans1=searchleft(nums,left,mid);
int ans2=searchright(nums,mid,right);
return {ans1,ans2};
}
else if(nums[mid]>target){
right=mid-1;
}
else{
left=mid+1;
}
}
return {-1,-1};
}
};
需要注意的是,寻找左右区间时由于mid+1/mid-1有可能使得start、end超出left、right,需要判断,否则会溢出。
Leetcode35.搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums 为 无重复元素 的 升序 排列数组
-104 <= target <= 104
考虑一下4种情况:
情况3可以提前判断,情况1、2可归入4。下考虑终止情况:
(1)若最后left+1=right或left=right,且target=nums[left],则mid=left可直接返回
(2)若最后left+1=right,target=nums[right],则nums[mid]
(5)若最后left=right,target
综上所述,终止条件为left>right,函数返回值为left
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left=0;
int right=nums.size()-1;
int mid;
while(left<=right){
mid=(left+right)/2;
if(nums[mid]==target) return mid;
else if(nums[mid]<target){
left=mid+1;
}
else{
right=mid-1;
}
}
return left;
}
};
Leetcode69.x的平方根
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
示例 1:
输入:x = 4
输出:2
示例 2:
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842…, 由于返回类型是整数,小数部分将被舍去。
提示:
0 <= x <= 231 - 1
二分思路同Leetcode35.搜索插入位置,函数返回值为right
class Solution {
public:
int mySqrt(int x) {
int left=1;
int right=x;
int mid;
while(left<=right){
mid=(left+right)/2;
long tmp=long(mid)*long(mid);
if(tmp==x){
return mid;
}
else if(tmp<x){
left=mid+1;
}
else{
right=mid-1;
}
}
return right;
}
};
然而该方法在最开始将mid*mid与x比较时相当于x2数量级,即264,即使使用long型也会溢出:
但计算mid=(left+right)/2
又出现加法溢出的问题:
因此需要用long转变数据类型,即mid=((long)left+(long)right)/2;
,AC代码如下:
class Solution {
public:
int mySqrt(int x) {
int left=1;
int right=x;
int mid;
while(left<=right){
mid=((long)left+(long)right)/2;
if(mid==x/mid){ //除法代替乘法以防止溢出
return mid;
}
else if(mid<x/mid){
left=mid+1;
}
else{
right=mid-1;
}
}
return right;
}
};
Leetcode33.搜索旋转排序数组
整数数组 nums 按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 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 <= nums.length <= 5000
-104 <= nums[i] <= 104
nums 中的每个值都 独一无二
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-104 <= target <= 104
这题显然是二分查找,因为题干中旋转数组的旋转点k的取值是0<=knums[left]>nums[right]
,则为旋转数组,否则是顺序数组。
对于旋转数组,有以下两种分布:
对应target位置的4种情况:
if(nums[left]<nums[mid]){
if(nums[left]<=target<=nums[mid]) search(nums[left...mid]);
else search(nums[mid...right]);
}
else{
if(nums[mid]<=target<=nums[right]) search(nums[mid...right]);
else search(nums[left...mid]);
}
因为题目给定函数形式,不便于递归,因此此类二分查找的题目尽量采用while循环:
class Solution {
public:
int search(vector<int>& nums, int target) {
int left=0;
int right=nums.size()-1;
int mid;
while(left<=right){ //left=right时也需要判断
mid=(left+right)/2;
//特殊处理防止遗漏
if(nums[left]==target) return left;
else if(nums[mid]==target) return mid;
else if(nums[right]==target) return right;
else if(nums[left]<nums[mid]){
if(nums[left]<=target && target<nums[mid]){
right=mid-1; //不直接取mid以防陷入死循环
}
else{
left=mid+1;
}
}
else{
if(nums[mid]<target && target<=nums[right]){
left=mid+1;
}
else{
right=mid-1;
}
}
}
return -1;
}
};
更为规范的写法如下,不必每次循环都判断nums[mid]、nums[left]、nums[right],只用最后跳出循环时判断left和right处是否有target:
class Solution {
public:
int search(vector<int>& nums, int target) {
int left=0;
int right=nums.size()-1;
int mid;
while(left<=right){
mid=(left+right)/2;
if(nums[mid]==target) return mid;
else if(nums[left]<=nums[mid]){ //相等时应归入此类
if(nums[left]<=target && target<nums[mid]){
right=mid-1;
}
else{
left=mid+1;
}
}
else{
if(nums[mid]<target && target<=nums[right]){
left=mid+1;
}
else{
right=mid-1;
}
}
}
if(left<nums.size()&&nums[left]==target) return left;
else if(right>=0&&nums[right]==target) return right;
else return -1;
}
};
上述代码还有一个改动就是在判断哪种旋转数组else if(nums[left]<=nums[mid])
处加入了等号,以保证类似{3,1}的样例取到正确结果:
Leetcode81.搜索旋转排序数组 II
已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。
给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 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
提示:
1 <= nums.length <= 5000
-104 <= nums[i] <= 104
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-104 <= target <= 104
思路同Leetcode33.搜索旋转排序数组,但本题数组内元素可以重复,因此需要解决类似{3,3,3,1,2,3,3,3,3,3,3}和{3,3,3,3,3,3,3,1,2,3,3}的问题,即nums[left]==nums[mid] && nums[mid]==nums[right]
,若出现此情况,别无他法,只能缩短搜索区间继续执行:
class Solution {
public:
bool search(vector<int>& nums, int target) {
int left=0;
int right=nums.size()-1;
int mid;
while(left<=right){
mid=(left+right)/2;
if(nums[mid]==target){
return true;
}
else if(nums[left]==nums[mid] && nums[mid]==nums[right]){
//类似{3,3,3,1,2,3,3,3,3,3,3}和{3,3,3,3,3,3,3,1,2,3,3}的情况无法判断旋转点的位置
left++;
right--;
}
else if(nums[left]<=nums[mid]){
if(nums[left]<=target && target<nums[mid]){
right=mid-1;
}
else{
left=mid+1;
}
}
else{
if(nums[mid]<target && target<=nums[right]){
left=mid+1;
}
else{
right=mid-1;
}
}
}
if(left<nums.size()&&nums[left]==target) return true;
else if(right>=0&&nums[right]==target) return true;
else return false;
}
};
Leetcode153.寻找旋转排序数组中的最小值
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。
给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
示例 2:
输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。
示例 3:
输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
提示:
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums 中的所有整数 互不相同
nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转
每次循环前先判断是否是顺序数组if(nums[left]<=nums[right])
,若顺序则可以直接返回nums[left];若不是则二分缩短区间。值得注意的是,在nums[left]>nums[mid]
的情况下,right不能取mid-1而只能取mid,否则容易略过最小值:
class Solution {
public:
int findMin(vector<int>& nums) {
int left=0;
int right=nums.size()-1;
int mid;
while(left<=right){
mid=(left+right)/2;
if(nums[left]<=nums[right]) return nums[left]; //顺序数组
else if(nums[left]<=nums[mid]){
left=mid+1;
}
else{
right=mid; //不能取mid-1否则容易错过最小值
}
}
if(left>=nums.size()) return nums[right];
else if(right<0) return nums[left];
else if(nums[left]<=nums[right]) return nums[left];
else return nums[right];
}
};
Leetcode154.寻找旋转排序数组中的最小值 II
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。
给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须尽可能减少整个过程的操作步骤。
示例 1:
输入:nums = [1,3,5]
输出:1
示例 2:
输入:nums = [2,2,2,0,1]
输出:0
提示:
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转
思路同 Leetcode81.搜索旋转排序数组 II 和 Leetcode153.寻找旋转排序数组中的最小值:
class Solution {
public:
int findMin(vector<int>& nums) {
int left=0;
int right=nums.size()-1;
int mid;
while(left<right){ //left=right时可以直接跳出
mid=(left+right)/2;
if(nums[left]<nums[right]){
return nums[left];
}
else if(nums[left]==nums[mid] && nums[mid]==nums[right]){
//类似{3,3,3,1,2,3,3,3,3,3,3}和{3,3,3,3,3,3,3,1,2,3,3}的情况无法判断旋转点的位置
left++;
right--;
}
else if(nums[left]<=nums[mid]){
left=mid+1;
}
else{
right=mid;
}
}
if(left>=nums.size()) return nums[right];
else if(right<0) return nums[left];
else if(nums[left]<=nums[right]) return nums[left];
else return nums[right];
}
};
但需要将终止条件改为left>right
,因为left=right时需要跳出,否则left++、right–可能出现段错误(如样例:{1} )。
Leetcode162.寻找峰值
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞ 。
你必须实现时间复杂度为 O(log 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。
提示:
1 <= nums.length <= 1000
-231 <= nums[i] <= 231 - 1
对于所有有效的 i 都有 nums[i] != nums[i +1]
因为题干中说nums[-1]=nums[n]=-∞,所以nums中必定存在峰值。循环过程中比较nums[mid]和nums[mid+1],取大的所在那一部分不断二分,一定可以找到某一个峰值。因为mid=(left+right)/2
,所以mid+1一定小于等于right,即不会超出范围。
循环进行到left和right相差1时就可以停止了,返回数值较大的位置。但为了避免只有1个元素的数组样例,还需要判断left==right的情况:
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int left=0;
int right=nums.size()-1;
int mid;
while(left<=right){
mid=(left+right)/2; //mid+1<=right显然成立
if(left+1==right){
if(nums[left]<nums[right]) return right;
else return left;
}
else if(left==right) return left; //防止n=1
else if(nums[mid]<nums[mid+1]){
if(mid+1==nums.size()-1||nums[mid+1]>nums[mid+2]){
return mid+1;
}
else{
left=mid+1;
}
}
else{
if(mid==0||nums[mid]>nums[mid-1]){
return mid;
}
else{
right=mid;
}
}
}
return -1;
}
};
Leetcode74.搜索二维矩阵
编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:
每行中的整数从左到右按升序排列。
每行的第一个整数大于前一行的最后一个整数。
示例 1:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true
示例 2:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13
输出:false
提示:
m = matrix.length
n = matrix[i].length
1 <= m, n <= 100
-104 <= matrix[i][j], target <= 104
先搜索行首元素确定target所在行,再搜索该行确定是否在矩阵中。注意搜索行结束后所在行取high,但需要判断high<0的情况(如:{{1,2}},0):
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int low=0;
int high=matrix.size()-1;
int level;
while(low<=high){
level=(low+high)/2;
if(matrix[level][0]==target) return true;
else if(matrix[level][0]<target){
low=level+1;
}
else{
high=level-1;
}
}
//此时high即为下面搜索的行号
if(high<0) return false; //防止{{1}}的情况
vector<int> v=matrix[high];
int left=0;
int right=v.size()-1;
int mid;
while(left<=right){
mid=(left+right)/2;
if(v[mid]==target) return true;
else if(v[mid]<target) left=mid+1;
else right=mid-1;
}
return false;
}
};
Leetcode240.搜索二维矩阵 II
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
每行的元素从左到右升序排列。
每列的元素从上到下升序排列。
示例 1:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
输出:true
示例 2:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 20
输出:false
提示:
m = matrix.length
n = matrix[i].length
1 <= n, m <= 300
-109 <= matrix[i][j] <= 109
每行的所有元素从左到右升序排列
每列的所有元素从上到下升序排列
-109 <= target <= 109
本题的矩阵具有排序上的特殊性,每行从左到右、每列从上到下、主对角线从左上到右下上的元素都是升序排列。一开始的思路是遍历主对角线,确定target范围[loc-1,loc],再在剩下的两块方形区域中(即图中阴影部分)遍历搜索。若loc没有被赋值,即target大于matrix[min{m-1,n-1}][min{m-1,n-1}]
,还有可能是矩阵非方阵,在多出来的长方形中搜索。搜索思路如图:
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int loc=-1;
int m=matrix.size();
int n=matrix[0].size();
if(matrix[0][0]>target || matrix[m-1][n-1]<target) return false;
else if(matrix[0][0]==target || matrix[m-1][n-1]==target) return true;
for(int i=1;i<m && i<n;i++){ //搜索位置
if(matrix[i-1][i-1]==target || matrix[i][i]==target) return true;
else if(matrix[i-1][i-1]<target && matrix[i][i]>target){
loc=i;
break;
}
}
//搜索完target在主对角线的位置
if(loc>=0){
for(int i=0;i<loc;i++){
for(int j=loc;j<n;j++){
if(matrix[i][j]==target) return true;
}
}
for(int i=loc;i<m;i++){
for(int j=0;j<loc;j++){
if(matrix[i][j]==target) return true;
}
}
}
//非方阵会出现遗漏
if(loc==-1){ //matrix[min{m-1,n-1}][min{m-1,n-1}]
if(m>n){ //下方部分未搜索
for(int i=n;i<m;i++){
for(int j=0;j<n;j++){
if(matrix[i][j]==target) return true;
}
}
}
if(m<n){ //右方部分未搜索
for(int i=0;i<m;i++){
for(int j=m;j<n;j++){
if(matrix[i][j]==target) return true;
}
}
}
}
return false;
}
};
该思路也能很艰难的通过,,,下面根据官方解答重新编写。
Z字形查找
我们可以从矩阵matrix的右上角 matrix[0][n−1] 进行搜索。在每一步的搜索过程中,如果我们位于位置 matrix[x][y],那么我们希望在以 matrix[m-1][0] 为左下角、以 matrix[x][y] 为右上角的矩阵中进行搜索,即行的范围为 [x,m−1],列的范围为 [0,y]:
在搜索的过程中,如果我们超出了矩阵的边界,那么说明矩阵中不存在 target:
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m=matrix.size();
int n=matrix[0].size();
int curx=0;
int cury=n-1;
while(curx<=m-1 && cury>=0){
if(matrix[curx][cury]==target){
return true;
}
else if(matrix[curx][cury]>target){
cury--;
}
else{
curx++;
}
}
return false;
}
};
官方解答巧妙将大问题划小,并且小问题与原问题具有相同性质与形状。通过x- -和y++不断缩小搜索范围,最终得到结果。