二分查找--折半查找--看完这篇学不会你来打我

0 阅读7分钟

二分查找


前言

二分查找(binary search) 也叫折半查找,是一种在有序数组中基于分治策略的高效搜索算法,因为它的有序性,使得我们可以用 “减而治之” 的策略来进行查找。 本文将大家讲一下二分查找的原理和代码

1为什么要用二分查找

1.1顺序查找(暴力搜索)

最简单的查找,也就是顺序查找,只需要在数组中逐一的将 target从前往后判断,直到找到相等元素,这个想必都很容易想到。 在无序数组中查找指定元素target时,由于没有更多的信息,所以在最坏的情况下(比如数组中不含target)——只有依次遍历到最后一个元素后,才可以得到结论。时间复杂度此时是o(n)。

  • 如果是无序数组,这样做似乎无可厚非
  • 如果是有序数组这样做似乎就太过于老实了,那么就有人要问了主播主播有没有查找效率更高的方法,有的兄弟有的——二分查找
int search(int* nums, int numsSize, int target) {
    for (int i = 0; i < numsSize; i++) {
        if (nums[i] == target) {//找到了
            return i;
        }
    }
    return -1;//没找到,没有走到if心里
}
int search(vector<int>& nums,int target){
    for(int i=0;i<nums.size();i++){
        if(nums[i] == target){
            return i;
        }
    }
    return -1;
}
1.1.2 时间复杂度

在有序数组中

查找方法时间复杂度
顺序查找o(n)
二分查找o(logn)

二分查找由于用到了数组的有序性,大大提高了算法效率

1.2 二分查找的原理及代码

1.2.1二分查找

原理其实大多数人应该很容易想明白 举个例子 要在这个 {2,4,5,7,8,9,12} 里面用二分查找元素 8 我采用左右都是闭区间这种方式仔细说一下 [left,right]

在这里插入图片描述

  • 这种写法左右都是闭区间 [left,right]
  • while (left <= right) 这里用 <= ,因为 left == right是有意义的,所以用 <=,或者想一下如果换了<上面这个例子,如果到最后8了,left == right,退出循环返回-1,这不坏了吗
  • if (nums[mid] > target)里面 right 要赋值为 mid - 1,因为现在这个nums[mid]一定不是target,那接下来要查找的左区间最后的位置就是 mid - 1,nums[mid] < target的情况是一个道理,要选mid+1
int binsearch(int* nums, int numsSize, int target) {
    int left = 0;
    int right = numsSize - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else {
            return mid;
        }
    }
    return -1;
}
int binsearch(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){
            right = mid - 1;
        }
        else if(nums[mid] < target){
            left = mid + 1;
        }
        else{
            return mid;
        }
    }
    return -1;
}
  • 时间复杂度 o(n)
  • 空间复杂度 o(1)

大多数初学者或许有疑问这里为什么要这么写 int mid = left + (right - left) / 2;而不是 int mid = (right + left) / 2; 其实为了防止left+right这个加起来的结果int越界,为了避免越界,我们通常采用left + (right - left) / 2;

另一种开区间的写法我放在最后
1.2.2 最坏情况
  • 逐行分析一下代码,很容易发现,如果我们先判断的是nums[mid] > target 也就是成功判定的话接下来我们要往左半部分,如果失败呢,我们还要继续判断 nums[mid] < target 从而判断它能否往右,这样看往左需要一次判断,而往右一共需要两次判断,这样就造成了不同情况所对应的查找长度的不均衡,一直往左和一直往右查找长度相差约两倍

  • 即使目前来看它在最坏的情况下也可以保证O(log n),但是经过我们仔细观察一下还略有不足

  • 那么就有人要问了,主播主播,这样的二分查找的确很强,但是还是不够平衡稳定,而且还是太长了,那么有没有又简单好理解,又平衡的二分查找呢,有的兄弟有的——二分查找pro版

1.3 二分查找 pro和promax

上面这种二分查找在时间复杂度上看的确可以了,还是略有瑕疵,

白圭之玷,尚可磨也

之所以在它的平衡上出现问题,最根本的问题是往左边的一次判断和往右边的两次判断机会不相匹配 聪明的你一定可以想到 解决这个问题只需要把向右侧的次数减少或者让向右侧也只需要一次判定 所以解决问题的思路就这两种

  • 调整前后的宽度,适当加长(缩短)左(右)侧的数组(pro版)
  • 统一沿两个方向深入所需的比较次数,比如说都统一为一次(promax版)

比如像这样 在这里插入图片描述

  • 像第一种(pro版本)情况,就不细说了,大致可以把这个数组六四分这种样子,让 mid=(left+right)6/10mid=(left+right)*6/10
  • 总之是让右边占的比例小一点,总体效果就会更均衡,如果用斐波那契数列弄一下的话效果会更好,总之是一个进步

不过需要注意的是如果完全不要右边,那就变成从后往前的顺序查找了,那样就得不偿失了

  • 对于第二种(promax版)的情况,其实和前面的二分查找思路基本类似, 判断它是否在左侧,如果 target<num[mid] 那就让right到mid-1(因为此时mid!=target),否则target>=num[mid]
  • 关于target返回值是left,因为最后一定要走target>=num[mid]这里,target如果存在,那一定是在左边这个
  • 关于int mid = left + (right - left + 1) / 2;是为了让他向后取整,防止找数组中最后一个数的情况,比如在{3,5}里找5,带进去试一下就会发现它会往后取整,这样就可以取到数组中最后一个元素了了
int binsearch2(int* nums, int numsSize, int target){
    int left = 0;
    int right = numsSize - 1;   
    while(left < right){
        int mid = left + (right - left + 1) / 2;
        if(target < nums[mid]){
            right = mid - 1;
        } else {
            left = mid;
        }
    }
    return (target==nums[left])?left:-1;
}
int binsearch2(vector<int>& nums,int target){
    int left = 0;
    int right = nums.size()-1;   
    while(left<right){
        int mid =left +(right-left+1)/2;
        if(target<nums[mid]){
            right=mid-1;
        }else{
            left=mid;
        }
    }
    return (target==nums[left])?left:-1;
}

1.4 总结

二分查找在逻辑上很容易理解,但是只有真敲一遍才会出现各种小问题,建议理解后自己敲一遍

为山九仞,功亏一篑。

如果看完没学会那我很抱歉,是我标题党了,不要来打我

1.5完整测试代码

  • 完整测试代码
#include <stdio.h>
#include <windows.h>
int search(int* nums, int numsSize, int target) {
    for (int i = 0; i < numsSize; i++) {
        if (nums[i] == target) {
            return i;
        }
    }
    return -1;
}

int binsearch(int* nums, int numsSize, int target) {
    int left = 0;
    int right = numsSize - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else {
            return mid;
        }
    }
    return -1;
}

int binsearch2(int* nums, int numsSize, int target){
    int left = 0;
    int right = numsSize - 1;   
    while(left < right){
        int mid = left + (right - left + 1) / 2;
        if(target < nums[mid]){
            right = mid - 1;
        } else {
            left = mid;
        }
    }
    return (target==nums[left])?left:-1;
}


int main(){
    SetConsoleOutputCP(65001);
    int a[] ={2,4,5,7,8,9,12};
    int n = sizeof(a) / sizeof(a[0]);  // 计算数组长度
    int targets[] = {7, 2, 12, 6};  // 测试用例:存在和不存在
    int m = sizeof(targets) / sizeof(targets[0]);

    printf("Array: ");
    for(int i=0; i<n; i++) printf("%d ", a[i]);
    printf("\n\n");

    for(int j=0; j<m; j++){
        int target = targets[j];
        int index1 = search(a, n, target);
        int index2 = binsearch(a, n, target);
        int index3 = binsearch2(a, n, target);
        printf("目标值:%d\n", target);
        printf("  线性搜索结果:%d\n", index1);
        printf("  二分搜索结果:%d\n", index2);
        printf("  二分搜索promax结果:%d\n", index3);
        printf("\n");
    }
    return 0;

}