二分:如何判断某个元素是否存在一个有序的数组中?

需求

有一个数组,大小1000w,数据由小到大,现在用户随便输入一个数字,如何快速的判断此元素是否在数组中存在?

看到上面的需求你可能马上就想到怎么做了,循环这个数组,如果用户输入的数据等于当前循环的数据,表示存在,数组循环结束还没有找到,代表数据不存在数组中,这种方法最简单,循环一次数组就能得到结果,空间复杂度:O(1),时间复杂度:O(n),注意这里的O(n)仅仅表示数组大小是变动的,如果恒定1000w,那么时间复杂度:O(1)。

那有没有更快的一种方式呢?让时间复杂度降低一点呢?方法肯定是有的,就是今天要介绍的主角:二分查找

什么是二分查找

不知道你们以前玩过才数字游戏没有?出题方随机选出一个数字,然后他会告诉你这个数字在一个区间内,让你以最小的次数猜到数字是多少,每猜一次都会提示你所猜数字与给定数字的大小,比如:选中的数字:150,给定区间:1 -- 500,你有9次机会,如果猜对,奖励电视机一台。你们以前在超市有遇到过这种活动吗?当时我就遇到过,精通各种数据结构以及算法的我(这句可以省略,明显是吹牛),怎么能受得了这种挑衅,当时就给超市老板上了一课。

上面的这个例子我们就不能一个一个的猜,否者猜到9就没然后了,那我们来看看二分是如何解决这个问题的?

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。

看了二分介绍是不是觉得他正好能解决猜数字的游戏呢?我们来还原一下当时是怎么通过8次就猜对所选数字的:
1.选择1--500中间的数,也就是250,由于选中的数字是150,所以出题方会告诉我猜的大了;
2.(250+1)/2=125,出题方提示猜小了;
3.(125+250)/2=187,出题方提示猜大了;
4.(125+187)/2=156,出题方提示猜大了;
5.(125=156)/2=140,出题方提示猜小了;
6.(140+156)/2=148,出题方提示猜小了;
7.(156+148)/2=152,出题方提示猜大了;
8.(148+152)/2=150,恭喜你答对了。

这就是今天要说的二分查找,现实生活中我们可以通过折半的方式找到对应的数字,程序中我们也能通过这样的方式找到我们需要的数字,二分查找的理论很简单,下面我们就通过代码实现文章开头的需求。

代码实现二分查找

1.初始化数据

/**
     * 初始化数据
     * @param length 数组长度
     * @return
     */
    private static int[] initData(int length){
        int[] arr = new int[length];
        for(int i = 1; i<=length; ++i){
            arr[i-1] = i;
        }
        return arr;
    }

2.使用数组查找数据

/**
     *使用二分查找数据
     * @param a 数组内容
     * @param n  数组长度
     * @param value   要查询的数字
     * @return
     */
    public  static  int bsearch(int[] a, int n, int value) {
        int low = 0;
        int high = n - 1;
        count = 0;
        while (low <= high) {
            int mid = (low + high) / 2;
            count++;
            if (a[mid] == value) {
                return mid;
            } else if (a[mid] < value) {
                low = mid + 1;
            } else {
                high = mid - 1;
            }

        }

        return -1;
    }

3.定义一个成员变量:count(记录查询了多少次) 

/**
     * 一共搜索了多少次
     */
    public  static  int count = 0;

4.main函数

public static void main(String[] args) {
        int[] data = initData(500);
        Scanner scanner = new Scanner(System.in);
        while (true){
            System.out.println("请输入要查询的数字:");
            int num = scanner.nextInt();
            if(num == -1){
                System.exit(0);
            }
            int bsearch = bsearch(data, data.length, num);
            System.out.println("搜索结果所在的下标:"+bsearch+",搜索了"+count+" 次");
        }
    }

5.运行

我随机输了5个数字,请看结果:

从结果来看,最多9次就能得出结果 ,是不是比循环整个数组快得多?这里面的逻辑其实也很简单,每次和取中的数据对比,然后得出下一个取中数据,继续对比,直到找到数据或者取完还找不到数据返回-1。

如何找到数据在数组中第一次出现的位置?

之前说过了通过二分法可以快速的找到某个元素,但是怎么做到找到这个元素第一次出现的位置呢?如数组a:{1,2,3,3,3,5,6};假设我们要查找元素3第一次出现的位置。该如何实现?还能继续使用二分查找吗?

我们分析一下,第一步使用3与数组中的a[3]对比,发现正好相等,直接返回了a[3]的下标(2),但这是元素3第一次出现的位置吗?我们看数组a,第一次出现3的位置在下标:2,所以很明显,通过二分查找的下标并不是我最终想得到的结果,那是不是意味着二分查找无法满足了呢?

其实我们还是可以通过二分来实现此功能,只需要在二分的基础上做一点点小改动就可以了,如何改动呢?我们想象一下,当我们使用3与数组中的a[3]对比的时候发现他们相等的同时判断一下上一个元素是否也和他相等(a[2]),如果相等就继续二分,否则返回当前下标,这样是不是就能找到某个元素第一次出现的位置了呢?

下面我们使用代码实现一下上面的需求

main函数:

public static void main(String[] args) {
        int[] data = {1,2,3,3,3,5,6};
        Scanner scanner = new Scanner(System.in);
        while (true){
            System.out.println("请输入要查询的数字:");
            int num = scanner.nextInt();
            if(num == -1){
                System.exit(0);
            }
            int bsearch = bsearch(data, data.length, num);
            System.out.println("第一次出现的下标:"+bsearch+",搜索了"+count+" 次");
        }
    }

二分查找:

/**
     *使用二分查找数据
     * @param a 数组内容
     * @param n  数组长度
     * @param value   要查询的数字
     * @return
     */
    private static int bsearch(int[] a, int n, int value) {
        int low = 0;
        int high = n - 1;
        count = 0;
        while (low <= high) {
            count++;
            int mid =  low + ((high - low) >> 1);
            if (a[mid] > value) {
                high = mid - 1;
            } else if (a[mid] < value) {
                low = mid + 1;
            } else {
                if ((mid == n - 1) || (a[mid - 1] != value)) return mid;
                else low = mid + 1;
            }
        }
        return -1;
    }

仔细看,你会发现这与之前的代码并没有什么差别,唯一一点变化在于判断的时候多了一个条件:a[mid-1] != value,这样就能实现找到某个元素在数组中第一次出现的位置,查找结果如下:

 是不是很简单?那我们再来看几种情况。

查找最后一个值等于给定值的元素

相信大家都知道怎么做了吧,和找到第一次出现的位置一样,加一个判断条件:当我们使用3与数组中的a[3]对比的时候发现他们相等的同时判断一下下一个元素是否也和他相等(a[4]),如果相等就继续二分,否则返回当前下标。

/**
     *使用二分查找数据
     * @param a 数组内容
     * @param n  数组长度
     * @param value   要查询的数字
     * @return
     */
    private static int bsearch(int[] a, int n, int value) {
        int low = 0;
        int high = n - 1;
        count = 0;
        while (low <= high) {
            count++;
            int mid =  low + ((high - low) >> 1);
            if (a[mid] > value) {
                high = mid - 1;
            } else if (a[mid] < value) {
                low = mid + 1;
            } else {
                if ((mid == n - 1) || (a[mid + 1] != value)) return mid;
                else low = mid + 1;
            }
        }
        return -1;
    }

 结果:

发布了41 篇原创文章 · 获赞 79 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_33220089/article/details/103973270