【算法导论】标准二分查找及其变式小结

44 阅读4分钟

虽然对于我们每个人来说二分查找都是耳熟能详的存在,但在实际刷题和开发过程中还是经常性地会写错二分查找(多为出现死循环)。故在此整理一下有关二分查找的常用模板。

标准二分查找

标准二分查找会找到非降序数组中的一个与目标值target相等的元素的位置下标,若未找到则返回-1

为了确保二分查找算法的正确性,我们必须在每一次算法迭代缩小查找区间的过程中,保证每一次缩小原区间而得到的新的待查找区间都符合相同的特征。常见的区间特征有左闭右闭区间([i, j])和左闭右开区间([i, j))两种。

[i, j]区间式

在采用左闭右闭区间的二分查找实现中,我们认为区间最右侧j指针所指的元素也是被算在查找范围之内的,因此循环条件为i <= j,且区间向左缩小时的表达式为j = mid - 1mid所指的元素被排除,但mid-1所指的元素仍然保留在待查找区间内)。

int search(vector<int>& ar, int target) {
    int i = 0;
    int j = ar.size() - 1;
    while (i <= j) {
        int mid = i + ((j - i) >> 1);
        if (ar[mid] == target) {
            return mid;
        }
        else if (ar[mid] < target) {
            i = mid + 1;
        }
        else {
            j = mid - 1;
        }
    }
    return -1;
}

在有些资料中,会将计算中点下标的表达式写作mid = i + ((j - i) >> 1),而非mid = (i + j) >> 1。这是为了防止在计算i + j出现溢出。很容易证明,这两个表达式在数学上是等价的。

[i, j)区间式

与前者类似,在左闭右开区间的二分查找实现中,待查找区间并不包括指针j所指元素,因此循环条件和待查找区间向左收缩的表达式也要作相应的修改。

int search(vector<int>& ar, int target) {
    int i = 0;
    int j = ar.size();
    while (i < j) {
        int mid = i + ((j - i) >> 1);
        if (ar[mid] == target) {
            return mid;
        }
        else if (ar[mid] < target) {
            i = mid + 1;
        }
        else {
            j = mid;
        }
    }
    return -1;
}

虽然在这种实现中j所指元素并不包括在待查找区间内,但我们却不用修改计算mid的表达式。这是因为只有某次进入循环时恰有ij两者相等,才有mid=j这个不安全的结果,但根据这个实现的循环条件i < j可知,这种情况是不可能发生的。

变式

以一个具体的数组nums=[1, 3, 5, 7, 7, 7, 8, 9]和目标值target=7为例,我们可能碰到针对二分查找综合运用的如下几种变式:

变式1:寻找最后一个小于target的元素的下标

在本例中正确答案应为2.

int search(vector<int>& ar, int target) {
    int i = 0;
    int j = ar.size() - 1;
    while (i <= j) {
        int mid = i + ((j - i) >> 1);
        if (ar[mid] < target) {
            i = mid + 1;
        }
        // ar[mid] ≥ target
        // 如果ar[mid]=target,则继续将区间向左缩小
        else {
            j = mid - 1;
        }
    }
    // 循环退出时的情形:
    // [1, 3, 5, 7, 7, 7, 8, 9]
    //        ↑  ↑
    //        j  i
    return j;
}

试一试:如果将target值由7修改为1,会得到什么结果?这个结果说明了什么?

变式2:寻找第一个大于target的元素的下标

在本例中正确答案应为6.

int search(vector<int>& ar, int target) {
    int i = 0;
    int j = ar.size() - 1;
    while (i <= j) {
        int mid = i + ((j - i) >> 1);
        // 如果ar[mid]=target,则继续将区间向右缩小
        if (ar[mid] <= target) {
            i = mid + 1;
        }
        // ar[mid] < target
        else {
            j = mid - 1;
        }
    }
    // 循环退出时的情形:
    // [1, 3, 5, 7, 7, 7, 8, 9]
    //                 ↑  ↑
    //                 j  i
    return i;
}

变式3:寻找第一个≥target的元素的下标

这个变式等价于寻找数组中第一个值为target的元素的下标

在本例中正确答案应为3.

int search(vector<int>& ar, int target) {
    int i = 0;
    int j = ar.size() - 1;
    while (i <= j) {
        int mid = i + ((j - i) >> 1);
        if (ar[mid] < target) {
            i = mid + 1;
        }
        // ar[mid] ≥ target
        // 如果ar[mid]=target,则继续将区间向左缩小
        else {
            j = mid - 1;
        }
    }
    // 循环退出时的情形:
    // [1, 3, 5, 7, 7, 7, 8, 9]
    //        ↑  ↑
    //        j  i
    return i;
}

变式4:寻找最后一个≤target的元素的下标

这个变式等价于寻找数组中最后一个值为target的元素的下标

在本例中正确答案应为5.

int search(vector<int>& ar, int target) {
    int i = 0;
    int j = ar.size() - 1;
    while (i <= j) {
        int mid = i + ((j - i) >> 1);
        // 如果ar[mid]=target,则继续将区间向右缩小
        if (ar[mid] <= target) {
            i = mid + 1;
        }
        // ar[mid] < target
        else {
            j = mid - 1;
        }
    }
    // 循环退出时的情形:
    // [1, 3, 5, 7, 7, 7, 8, 9]
    //                 ↑  ↑
    //                 j  i
    return j;
}

相关LeetCode题目

leetcode.cn/problems/bi…

leetcode.cn/problems/ch…

leetcode.cn/problems/fi…