二分查找算法详解

1.介绍

二分查找,也称折半查找(Binary Search),它是一种效率较高的查找方法,实现原理简单,但细节相对复杂的算法。关于二分查找,有个经典的理解,思路很简单,细节是魔鬼 。 二分查找的常用场景一般包括:寻找一个数、寻找左侧边界、寻找右侧边界。而细节,主要体现在,while循环中用 < 还是 <=mid是否应该加1等。 下面从常用场景,结合 leetcode 题目,简单介绍下。

2. 寻找一个数

这是最简单的二分查找应用场景,即从有序数组中搜索一个目标数。

题目描述

leetcode 704.二分查找

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

示例:

输入: nums = [-1,0,3,5,9,12], target = 9

输出: 4

解释: 9 出现在 nums 中并且下标为 4

题目解析

这个题目即是典型的二分查找,为有序数组中,查找目标值,我们定义 mid 为数组中点,即是比较 nums[mid] 与 tagert 大小:

如果 nums[mid] = target,则下标 mid 即为要寻找的下标;

如果 nums[mid] > target,则 target 只可能在下标 mid 的左侧;

如果 nums[mid] < target,则 target 只可能在下标 mid 的右侧。

代码实现

int search(int* nums, int numsSize, int target) {
    int l = 0;
    int r = numsSize - 1;
    while (l <= right) {
        int mid = l + (r - l) / 2;
        if (nums[mid] == target)
            return mid; // 找到目标值,返回下标
        else if (nums[mid] < target)
            l = mid + 1;
        else if (nums[mid] > target)
            r = mid - 1;
    }
    return -1; //目标值不存在,返回-1
}

现在关于细节问题,做下分析理解:

2.1 为什么while循环里要用 <=

我们注意到,在初始化时,left 值为 0,而 right 值为 numsSize - 1,即最后一个元素的索引为 numsSize - 1,相当于我们的搜索区间为 [left, right],两端都闭的区间。如果 right初值为 numsSize时,while 循环语句里需要用 <作为条件判断。

2.2 为什么 left = mid + 1 和 right = mid - 1

如上文描述,我们的搜索区间为 [left, right],以mid为中点,逐步缩小查找范围,如果在索引 mid里没有找到 target,接下来就要搜索 [left, mid - 1] 和 [mid + 1, right] 区间范围内是否有目标值。

3.寻找左侧边界

题目描述<一>

我们对上面 leetcode 704题目做下修改,在有序数组中,存在元素相等的情况,找到数组中和target相等的 最左侧元素下标

示例:

输入: nums = [-1,0,3,5,9,9,12], target = 9

输出: 4

解释: 9 出现在 nums 中 最左侧下标为 4

题目解析 结合上面寻找一个数思路分析,在比较 nums[mid] 与 tagert 大小时,如果 nums[mid] = target,下标 mid 即为要寻找的下标,此时不返回,收缩右侧边界接着找下一个r = mid - 1,其他情况和寻找一个数实现相同。

代码实现

int search(int* nums, int numsSize, int target) {
    int l = 0;
    int r = numsSize - 1;

    while (l <= right) {
        int mid = l + (r - l) / 2;
        if (nums[mid] == target)
            r = mid - 1; // 找到目标值,收缩右侧边界
        else if (nums[mid] < target)
            l = mid + 1;
        else if (nums[mid] > target)
            r = mid - 1;
    }

    if (l < 0 || nums[l] != target)
        return -1;
    return l;
}

题目描述<二>

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 ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

题目解析

l 和 r 指针不断向中间移动,进行二分,则有2种情况:

当前节点 比 r 节点的值小, 即 nums[mid] < nums[r],则 r 等于 mid

当前节点大于等于 r 节点的时候,即 nums[mid] >= nums[r],则让 l 等于 mid + 1

代码实现

int findMin(int* nums, int numsSize){
    int l = 0;
    int r = numsSize - 1;

    while (l < r) {
        int mid = l + (r - l) / 2;
        if (nums[mid] < nums[r])
            r = mid;
        else
            l = mid + 1;
    }

    return nums[l];
}

4.寻找右侧边界

我们对上面 leetcode 704题目做下修改,在有序数组中,存在元素相等的情况,找到数组中和target相等的 最右侧元素下标

示例:

输入: nums = [-1,0,3,5,9,9,12], target = 9

输出: 5

解释: 9 出现在 nums 中最右侧下标为 5

题目解析 结合上面寻找一个数思路分析,在比较 nums[mid] 与 tagert 大小时,如果 nums[mid] = target,下标 mid 即为要寻找的下标,此时不返回,收缩左侧边界接着找下一个l = mid + 1,其他情况和寻找一个数实现相同

代码实现

int search(int* nums, int numsSize, int target){
    int l = 0;
    int r = numsSize - 1;

    while (l <= right) {
        int mid = l + (r - l) / 2;
        if (nums[mid] == target)
            l = mid + 1; // 找到目标值,收缩左侧边界
        else if (nums[mid] < target)
            l = mid + 1;
        else if (nums[mid] > target)
            r = mid - 1;
    }

    if (r < 0 || nums[r] != target)
        return -1;
    return r;
}

题目描述

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

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

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

示例 1:

输入:nums = [5,7,7,8,8,10], target = 8

输出:[3,4]

题目解析

这道题目,要找到目标值在数组中的开始位置和结束位置,就是找到 左边界和右边界,可以先找左侧边界,再以左边界为起始点,找右边界。

代码实现

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* searchRange(int* nums, int numsSize, int target, int* returnSize){
    int *res = (int*)malloc(sizeof(int) * 2);
    res[0] = -1;
    res[1] = -1;
    int left = 0;
    int right = numsSize - 1; 

    *returnSize = 2;
/* 找左边界 */
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target)
            left = mid + 1;
        else if(nums[mid] > target)
            right = mid - 1;
        else
            right = mid - 1;
    }
/* 以左边界,为起始,找右边界 */
    if (left < numsSize && nums[left] == target) {
        res[0] = left;
        while (left < numsSize - 1) {
            if (nums[left + 1] == target) 
                left++;
            else
                break;
        }
        res[1] = left;
    }
    return res;
}

你可能感兴趣的:(#算法和数据结构,leetcode,二分查找)