10. 二分搜索

一,二分法(Binary Search)

        前提:线性表采取顺序存储结构(即适用于数组,不适用于链表),表中元素有序排列
        优点:因为是有序排列,故每次查询后可减少一半的搜索范围,时间复杂度是 O(log N)
        场景:寻找一个数寻找左侧边界寻找右侧边界

二,细节提醒

        不同的搜索场景下,有些变量的初始值以及终止条件是不同的。
        1, while (left < right) 还是 while (left <= right)
        2, mid 是 +1 还是 -1
        3, ......

三,整体框架

        代码中 ... 代表细节处的不同,即不同的场景中此处代码可能会有所不同。

int binarySearch(vector<int>& nums, int target) {
    int left = 0, right = ...;		// 位置 1
    while (...) {			// 位置 2
        int mid = left + (right - left) >> 1;
	    if (nums[mid] == target) {
            ...				// 位置 3
	    }
	    else if (nums[mid] < target) {
	        left = ...		// 位置 4
	    }
	    else if (nums[mid] > target) {
            right = ...		// 位置 5
	    }
    }
    return ...		// 位置 6
}

        1, 此处决定搜索区间 [left, right] 或者是 [left, right)
                left = 0, right = nums.size() - 1;       ==>        [left, right]
                left = 0, right = nums.size();            ==>        [left, right)    此时右边界的下标是无法访问
        
        2, 此处决定终止条件 , left == right 或者 left == right + 1
                while (left < right)          ==>        left == right 为终止条件,终止时 {right, right}
                while (left <= right)        ==>        left == right + 1 为终止条件,终止时 {right+1, right}
                通常,位置2 的写法由 位置1 决定。(当我们决定了是 [ ][ ) 后,我们便能确定终止的条件了)
                比如,你的搜索区间是 [left, right] , 那么终止条件是选 [right, right] 还是 [right+1, right] 呢? 肯定是 [right+1, right] 呀,因为 [right, right] 终止时,下标是 right 的元素还没有被访问。
                再比如,你的搜索区间是 [left, right), 则终止时你的搜索区间一定是 [right, right), 那么终止条件可以是 while (left < right)while (left <= right)。不过后者显然没必要。
        
                当你明白后,你会发现:
                        当搜索区间是 [left, right] 时, 终止条件只能是 [right+1, right] , 即 while (left <= right)。 你若写 while (left < right) 则是错的,因为会漏元素。
                        当搜索区间是 [left, right) 时, 终止条件可以是 [right, right)[right+1, right)。即 while (left < right)while (left <= right) 都对,不过后者没必要。
        
        3, 这取决于应用场景
                若是找一个数,则找到后应该直接返回索引。
                若是找左边界,则找到目标值后,目标值的左边可能还存在目标值。故改变搜索的右边界,继续往左边寻找
                若是找右边界,则找到目标值后,目标值的右边可能还存在目标值。故改变搜索的左边界,继续往右边寻找
        
        4, 此处改变搜索区间的左边界,注意,左边界始终是搜索区间内的。
                所以这里 left = mid + 1;
    
        5, 此处改变搜索区间的右边界,注意,右边界可能是改变的,与 位置1 有关。 [left,right]  [left,right)
                所以这里 right = mid - 1; 或者 right = mid;

四,寻找一个数

        搜索一个数,若找到,则返回其索引,否则返回 -1。

int binarySearch(vector<int>& nums, int target) {
    int left = 0, right = nums.size() - 1;	// 搜索区间 [left, right]
    while (left <= right) {		// 终止条件 [right+1, right]
        int mid = left + (right - left) >> 1;
        if (nums[mid] == target) {
	        return mid;
        }
        else if (nums[mid] < target) {
	        left = mid + 1;     // [mid+1, right]
        }
        else {
	        right = mid - 1;    // [left, mid-1]
        }
    }
    return -1;
}
	
    解释:
	位置 1 : 我们搜索的区间是 [left,right],注意选择的是 左闭右闭 []
	位置 2 : 由于搜索区间是 [], 故终止时搜索区间是 [right+1, right],此区间为空,故不会遗漏元素没有遍历。
	位置 3 : 当找到目标值时,我们直接返回。
	位置 4 : [left, mid, right]  ==>	当目标值大于 nums[mid] 后,搜索区间变成了 [mid+1, right], 即 left = mid + 1;
	位置 5 : 同 4, [left, mid-1] ==>  right = mid - 1;

五,寻找左侧边界

        现有一个数组 [1, 2, 2, 2, 4] ,利用上面算法寻找 2, 则得出的索引是 2。那么,如果我们想要得到第一次出现 target 值的索引,该如何修改 ?
        有种容易想到的方法,就是在上述的算法中,当找到 target 后,向左线性搜索便可。
        但是这样难以保证二分搜索对数级的复杂度。

int left_bound(vector<int>& nums, int target) {
    int left = 0, right = nums.size() - 1;	// 搜索区间 [left, right]
    while (left <= right) {
	    int mid = left + (right - left) >> 1;
	    if (nums[mid] == target) {	// 因为是找左边界,若找到了目标值,则应该缩小右边界,使之在左边寻找 [left, mid-1]
	        right = mid - 1;		// 很好奇为何不是 [left, mid] ? 因为 nums[mid] == target ,若 [left,mid-1] 区间没有 target,则最后 left == mid
	    }
	    else if (nums[mid] < target) {
	        left = mid + 1;		// 区间缩小到 [mid+1, right]
	    }
	    else {
	        right = mid - 1;		// 区间缩小到 [left, mid-1]
	    }
    }
		
    // 异常情况处理:
    // left >= num.size() : 当 target 大于所有数时,left 会等于 right + 1, 即会越界,没找到,返回 -1
    // nums[left] != target : 当 target 小于所有数时, left 仍然等于 0, 但需要判断 num[0] 是否等于 target
    // 这两个判断条件的顺序不可以调换,因为后者可能会导致非法访问
    if (left >= nums.size() || nums[left] != target) {
	    return -1;
    }
		
    return left;
}
// 另外一种写法,供思考
int left_bound(vector<int>& nums, int target) {
    int left = 0, right = nums.size();		// [left, right)
    while (left < right) {			// [right, right)
	    int mid = left + (right - left) >> 1;
	    if (nums[mid] == target) {
	        right = mid;			// [left, mid)
	    }
	    else if (nums[mid] < target) {
	        left = mid + 1;			// [mid+1, right)
	    }
	    else {
	        right = mid;			// [left, mid)
	    }
    }
		
    if (left >= nums.size() || nums[left] != target) {
	    return -1;
    }
    return left;
}

六,寻找右边界

int right_bound(vector<int>& nums, int target) {
    int left = 0, right = nums.size() - 1;	// [left, right]
    while (left <= right) {			// [right+1, right]
	    int mid = left + (right - left) >> 1;
	    if (nums[mid] == target) {
	        left = mid + 1; // [mid+1, right], 当该区间没有 target 时,最后 right 会变成 mid
	    }
	    else if (nums[mid] < target) {
	        left = mid + 1;
	    }
	    else if (nums[mid] > target) {
	        right = mid - 1;
	    }
    }
		
    if (right < 0 || nums[right] != target) {
	    return -1;
    }

    return right;
}
// 另一种写法,供思考
int right_bound(vector<int>& nums, int target) {
    int left = 0, right = nums.size();		// [left, right)
    while (left < right) {			// [right, right)
	    int mid = left + (right - left) >> 1;
	    if (nums[mid] == target) {
            left = mid + 1;			// [mid+1, right)
	    }
	    else if (nums[mid] < target) {
	        left = mid + 1;			// [mid+1, right)
	    }
	    else {
	        right = mid;			// [left, mid)
	    }
    }
		
    // 因为终止条件是 left == right
    // 当 target 小于所有数时, right 会不断缩小,最后 right == left 终止,即 right == 0 时,表明没有找到,返回 -1
    // 当 target 大于所有数时, left 会不断增大,最后 left == right 终止,而 right = nums.size() 的,取不到,故 nums[right-1] != target 表明没找到
    if (right == 0 || nums[right-1] != target) {
	    return -1;
    }
    return right - 1;	// 当找到后,需要 right - 1。因为 nums[mid] == target 时,将 left = left + 1。而最后 right == left,故 right 需要 -1。
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值