数据结构笔记【基础篇】——二分查找

二分查找

  • 二分查找

    • 二分查找针对的是一个有序的数据集合,查找思想有点类似分治思想。每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为 0。

    • 二分查找的递归与非递归实现

      • 最简单的情况就是有序数组中不存在重复元素
      public int bsearch(int[] a, int n, int value) {
               
        int low = 0;
        int high = n - 1;
      
        while (low <= high) {
               
          int mid = (low + high) / 2;
          if (a[mid] == value) {
               
            return mid;
          } else if (a[mid] < value) {
               
            low = mid + 1;
          } else {
               
            high = mid - 1;
          }
        }
      
        return -1;
      }
      
      • 三个注意条件

        • 循环退出条件

          • 注意是 low<=high,而不是 low
        • mid 的取值

          • 实际上,mid=(low+high)/2 这种写法是有问题的。因为如果 low 和 high 比较大的话,两者之和就有可能会溢出。改进的方法是将 mid 的计算方式写成 low+(high-low)/2。更进一步,如果要将性能优化到极致的话,我们可以将这里的除以 2 操作转化成位运算 low+((high-low)>>1)。因为相比除法运算来说,计算机处理位运算要快得多。
        • low 和 high 的更新

          • low=mid+1,high=mid-1。注意这里的 +1 和 -1,如果直接写成 low=mid 或者 high=mid,就可能会发生死循环。比如,当 high=3,low=3 时,如果 a[3]不等于 value,就会导致一直循环不退出。
        • 递归代码

        // 二分查找的递归实现
        public int bsearch(int[] a, int n, int val) {
                   
          return bsearchInternally(a, 0, n - 1, val);
        }
        
        private int bsearchInternally(int[] a, int low, int high, int value) {
                   
          if (low > high) return -1;
        
          int mid =  low + ((high - low) >> 1);
          if (a[mid] == value) {
                   
            return mid;
          } else if (a[mid] < value) {
                   
            return bsearchInternally(a, mid+1, high, value);
          } else {
                   
            return bsearchInternally(a, low, mid-1, value);
          }
        }
        
      • 二分查找应用场景的局限性

        • 首先,二分查找依赖的是顺序表结构,简单点说就是数组。
        • 其次,二分查找针对的是有序数据。
        • 再次,数据量太小不适合二分查找。
        • 最后,数据量太大也不适合二分查找。
      • 虽然大部分情况下,用二分查找可以解决的问题,用散列表、二叉树都可以解决。但是,我们后面会讲,不管是散列表还是二叉树,都会需要比较多的额外的内存空间。如果用散列表或者二叉树来存储这 1000 万的数据,用 100MB 的内存肯定是存不下的。而二分查找底层依赖的是数组,除了数据本身之外,不需要额外存储其他信息,是最省内存空间的存储方式,所以刚好能在限定的内存大小下解决这个问题。

  • 课后思考

    • 如何编程实现“求一个数的平方根”?要求精确到小数点后 6 位。
      • 解题思路:
        设double low=0,double up=x
        double mid = (low + up) / 2
        如果mid * mid > x,则up = mid;如果mid * mid < x,则low = mid;如果fabs(mid * mid - x) <= 1e-6,则返回mid,否则继续迭代计算
  • 二叉查找变形问题

    • 数据结构笔记【基础篇】——二分查找_第1张图片

    • 变体一:查找第一个值等于给定值的元素

      • 数据结构笔记【基础篇】——二分查找_第2张图片

        public int bsearch(int[] a, int n, int value) {
                   
          int low = 0;
          int high = n - 1;
          while (low <= high) {
                   
            int mid = low + ((high - low) >> 1);
            if (a[mid] >= value) {
                   
              high = mid - 1;
            } else {
                   
              low = mid + 1;
            }
          }
        
          if (low < n && a[low]==value) return low;
          else return -1;
        }
        
        public int bsearch(int[] a, int n, int value) {
                   
          int low = 0;
          int high = n - 1;
          while (low <= high) {
                   
            int mid =  low + ((high - low) >> 1);
            if (a[mid] > value) {
                   
              high = mid - 1;
            } else if (a[mid] < value) {
                   
              low = mid + 1;
            } else {
                   
              if ((mid == 0) || (a[mid - 1] != value)) return mid;
              else high = mid - 1;
            }
          }
          return -1;
        }
        
    • 变体二:查找最后一个值等于给定值的元素

      • public int bsearch(int[] a, int n, int value) {
                   
          int low = 0;
          int high = n - 1;
          while (low <= high) {
                   
            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;
        }
        
    • 变体三:查找第一个大于等于给定值的元素

      • public int bsearch(int[] a, int n, int value) {
                   
          int low = 0;
          int high = n - 1;
          while (low <= high) {
                   
            int mid =  low + ((high - low) >> 1);
            if (a[mid] >= value) {
                   
              if ((mid == 0) || (a[mid - 1] < value)) return mid;
              else high = mid - 1;
            } else {
                   
              low = mid + 1;
            }
          }
          return -1;
        }
        
    • 变体四:查找最后一个小于等于给定值的元素

      • public int bsearch7(int[] a, int n, int value) {
                   
          int low = 0;
          int high = n - 1;
          while (low <= high) {
                   
            int mid =  low + ((high - low) >> 1);
            if (a[mid] > value) {
                   
              high = mid - 1;
            } else {
                   
              if ((mid == n - 1) || (a[mid + 1] > value)) return mid;
              else low = mid + 1;
            }
          }
          return -1;
        }
        
  • 课后思考

    • 我们今天讲的都是非常规的二分查找问题,今天的思考题也是一个非常规的二分查找问题。如果有序数组是一个循环有序数组,比如 4,5,6,1,2,3。针对这种情况,如何实现一个求“值等于给定值”的二分查找算法呢?

    • 有三种方法查找循环有序数组

      一、
      \1. 找到分界下标,分成两个有序数组
      \2. 判断目标值在哪个有序数据范围内,做二分查找

      二、
      \1. 找到最大值的下标 x;
      \2. 所有元素下标 +x 偏移,超过数组范围值的取模;
      \3. 利用偏移后的下标做二分查找;
      \4. 如果找到目标下标,再作 -x 偏移,就是目标值实际下标。

      两种情况最高时耗都在查找分界点上,所以时间复杂度是 O(N)。

      复杂度有点高,能否优化呢?

      三、
      我们发现循环数组存在一个性质:以数组中间点为分区,会将数组分成一个有序数组和一个循环有序数组。

      如果首元素小于 mid,说明前半部分是有序的,后半部分是循环有序数组;
      如果首元素大于 mid,说明后半部分是有序的,前半部分是循环有序的数组;
      如果目标元素在有序数组范围中,使用二分查找;
      如果目标元素在循环有序数组中,设定数组边界后,使用以上方法继续查找。

      时间复杂度为 O(logN)。

你可能感兴趣的:(数据结构/算法,数据结构,算法)