算法3--二分查找

二分查找

原理

二分查找一般用于数组有序的情况,但不仅仅限于这种情形,更加普遍地来说,它适用于可以将一个整体切分为两个具有不一样特征的部分的情况,即问题具有二段性。它的思路很简单,就是将数组分为两部分,一部分是不存在目标的部分,另一部分是目标可能存在的部分,按照这个思路,其实遍历查找也是二分查找,只不过它每次只能排除一个数据,如果是这样的话,那么我们为什么不进行类似于1/4切分或者2/3这种切分,而是以1/2进行切分呢?可以这样想,数据是不确定的,如果进行1/4切分的话,有1/2的概率一次就排除3/4的数据,但也有1/2的概率一次只排除1/4的数据,从整体概率考虑,进行1/2切分是最优的。

二分问题可以细分为:一般的二分切分、寻找左边界的二分切分、寻找右边界的二分切分

在实现时需要特别注意:

  1. 循环结束条件
  2. 中间值mid的求取方式
  3. 左右指针每一次的步长

一、一般的二分切分

即在有序数组中寻找目标值

cpp 复制代码
int BinarySearch(vector<int>& num, int target){
	int left = 0;
	int right = num.size() - 1;
	while (left <= right) {
		int mid = left + (right - left) / 2;
		//也可以是int mid = left + (right - left) / 2;
		if (num[mid] == target) {
			return mid;
		}
		else if (num[mid] < target) {
			left = mid + 1;
		}
		else {
			right = mid - 1;
		}
	}

	return -1;
}

二、寻找左边界的二分切分

cpp 复制代码
int BinarySearch(vector<int>& num, int target){
	int left = 0;
	int right = num.size() - 1;
	while (left < right) {
		int mid = left + (right - left) / 2;
		if (num[mid] >= target) {
			right = mid;
		}
		else {
			left = mid +1;
		}
	}

	if (num[left] == target) {
		return left;
	}

	return -1;
}
  1. 步长:在num[mid] == target时,不能让right = mid-1,因为我们现在在寻找左边界,如果mid位置已经是左边界了,此时就会错过该位置,为了简便,直接在num[mid] >= target时设置---right = mid;
  2. 循环条件:如果循环结束条件为left <= right,最后ledt==right时如果走了right=mid就会导致死循环,因此循环条件为----:left <= right
  3. mid取值方式: 当只剩两个元素时,由于mid取较左边的值,无论走哪个条件都能出循环

三、寻找右边界的二分切分

cpp 复制代码
int BinarySearch(vector<int>& num, int target){
	int left = 0;
	int right = num.size() - 1;
	while (left < right) {
		int mid = left + (right - left + 1) / 2;
		if (num[mid] <= target) {
			left = mid;
		}
		else {
			right = mid - 1;
		}
	}

	if (num[left] == target) {
		return left;
	}

	return -1;
}

求解二分问题的一般思路为:

  1. 根据问题分析出二段性
  2. 选择合适的切分模板
  3. 根据问题分析处理细节

经典例题

704. 二分查找

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

cpp 复制代码
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        return -1;
    }
};

34. 在排序数组中查找元素的第一个和最后一个位置

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

cpp 复制代码
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        vector<int> ans(2,-1);
        if(0==nums.size()){
            return ans;
        }
        int left=0;
        int right=nums.size()-1;
        
        //寻找左端点
        while(left<right){
            int mid=left+(right-left)/2;
            if(nums[mid]<target){
                left=mid+1;
            }else{
                right=mid;
            }
        }
        if(target!=nums[left]){
            return ans;
        }
        ans[0]=left;

        //寻找右端点
        left=0;
        right=nums.size()-1;
        while(left<right){

            int mid=left+(right-left+1)/2;
            if(nums[mid]>target){
                right=mid-1;
            }else{
                left=mid;
            }
        }
        ans[1]=right;

        return ans;
    }
};

35. 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

cpp 复制代码
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int left=0;
        int right=nums.size()-1;
        int mid=0;
        while(left<=right){
            mid=left+(right-left)/2;
            if(nums[mid]==target){
                return mid;
            }
            else if(nums[mid]>target){
                right=mid-1;
            }else{
                left=mid+1;
            }
        }

        if(nums[mid]<target){
            return mid+1;
        }

        return mid;
    }
};

69. x 的平方根

给你一个非负整数 x ,计算并返回 x 的 算术平方根 。

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。

cpp 复制代码
class Solution {
public:
    int mySqrt(int x) {
        //确定量级
        int i=1;
        long long int j=10;
        while(x/j){
            j*=10;
            i+=1;
        }

        int left=0;
        int right=1;
        i=(i+1)/2;
        while(i--){
            right*=10;
        }

        while(left<=right){
            unsigned long long mid=left+(right-left)/2;
            unsigned long long tmp=mid*mid;
            if(tmp==x||(tmp<x&&(mid+1)*(mid+1)>x)){
                return mid;
            }else if(tmp<x){
                left=mid+1;
            }else{
                right=mid-1;
            }
        }

        return 0;
    }
};

852. 山脉数组的峰顶索引

给定一个长度为 n 的整数 山脉 数组 arr ,其中的值递增到一个 峰值元素 然后递减。

返回峰值元素的下标。

你必须设计并实现时间复杂度为 O(log(n)) 的解决方案。

cpp 复制代码
class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) {
        int left = 0;
        int right = arr.size() - 1;
        int mid = 0;
        while (left < right) {
            if (left + 1 == right) {
                return arr[left] > arr[right] ? left : right;
            }
            mid = left + (right - left) / 2;
            int lmid = left + (mid - left + 1) / 2;

            if (arr[left] < arr[lmid]) {
                if (arr[lmid] <= arr[mid]) {
                    left = lmid;
                } else {
                    right = mid;
                }
            } else {
                right = lmid;
            }
        }

        return mid;
    }
};

162. 寻找峰值

峰值元素是指其值严格大于左右相邻值的元素。

给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。

你可以假设 nums[-1] = nums[n] = -∞ 。

你必须实现时间复杂度为 O(log n) 的算法来解决此问题。

cpp 复制代码
class Solution {
public:
    int findPeakElement(vector<int>& nums) {
        int left = 0;
        int right = nums.size() - 1;
        while (left <= right) {
            int mid = left + (right - left + 1) / 2;
            if ((0 == mid || nums[mid - 1] < nums[mid]) &&
                (mid + 1 == nums.size() || nums[mid] > nums[mid + 1])) {
                return mid;
            }
            if (nums[mid - 1] > nums[mid]) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }

        return -1;
    }
};

153. 寻找旋转排序数组中的最小值

已知一个长度为 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) 的算法解决此问题。

cpp 复制代码
class Solution {
public:
    int findMin(vector<int>& nums) {
        int left = 0;
        int right = nums.size() - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[left] <= nums[mid]) {
                if (nums[mid] < nums[right]) {
                    return nums[left];
                }
                left = mid + 1;
            } else {

                right = mid;
            }
        }
        return nums[left];
    }
};

LCR 173. 点名

某班级 n 位同学的学号为 0 ~ n-1。点名结果记录于升序数组 records。假定仅有一位同学缺席,请返回他的学号。

  1. 根据问题分析出二段性

取中间点mid,如果records[mid]==mid,说明[left,mid]区间内的学生一定没有缺席,可以让left=mid+1;如果records[mid]!=mid,让right=mid,此时不能让right=mid-1,因为rnid的位置可能就是缺席学生的位置,让right=mid-1会直接错过该位置

  1. 选择合适的切分模板

根据前面的分析可以得知应该选用寻找左边界的切分模板

  1. 根据问题分析处理细节

有可能是最后一名学生缺席,在最后需要特别处理这种情况

cpp 复制代码
class Solution {
public:
    int takeAttendance(vector<int>& records) {
        int left=0;
        int right=records.size()-1;
        while(left<right){
            int mid=left+(right-left)/2;
            if(records[mid]!=mid){
                right=mid;
            }else{
                left=mid+1;
            }
        }
        if(left+1==records.size()&&left==records[left]){
            return left+1;
        }

        return left;
    }
};
相关推荐
xiaoshiguang32 小时前
LeetCode:222.完全二叉树节点的数量
算法·leetcode
爱吃西瓜的小菜鸡2 小时前
【C语言】判断回文
c语言·学习·算法
别NULL2 小时前
机试题——疯长的草
数据结构·c++·算法
TT哇2 小时前
*【每日一题 提高题】[蓝桥杯 2022 国 A] 选素数
java·算法·蓝桥杯
yuanbenshidiaos4 小时前
C++----------函数的调用机制
java·c++·算法
唐叔在学习4 小时前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
ALISHENGYA4 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
chengooooooo4 小时前
代码随想录训练营第二十七天| 贪心理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
算法·leetcode·职场和发展
jackiendsc4 小时前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法
游是水里的游6 小时前
【算法day20】回溯:子集与全排列问题
算法