Data Structures and Algorithms (nine) - binary search

First, the binary search

  • Binary search reports, realized
  • Binary search usage scenarios
  • Find the problem of binary variant

Second, the basic binary search

  1. Definitions : binary search is for an ordered set of data and look somewhat similar ideological divide and conquer idea. By comparing every element with middle range, the range will be reduced by half for the look before, until you find the element you want to find, or interval is reduced to zero.

  2. Time complexity: O (logN) , the greater the amount of data, the higher the efficiency

By n / 2k = 1, we obtain k = log2n, so the time complexity is O (logn).

3, binary search to achieve:

  • Non-recursive

    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;
    }
1. 循环退出条件

      注意是 low<=high,而不是 low

  2. mid 的取值

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

  3. low 和 high 的更新

      low=mid+1,high=mid-1。注意这里的 +1 和 -1,如果直接写成 low=mid 或者 high=mid,就可能会发生死循环。
  • Recursive

    // 二分查找的递归实现
    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);
      }
    }
  • Binary search usage scenarios

 1. 二分算法查找依赖的是顺序表结构,即**数组**,利用数据可以实现随机访问的数据的读取;换句话说,利用链表也是可以实现二分查找的,只是二分查找访问数据的时间复杂度会退化为O(n);

 2. 二分查找的针对的是有序数组,如果数据是无序的,需要先进行排序,如果在后面在需要进行查找时,有新的数据插入,则不论是插入数据的时间复杂度还是重新的排序都会有很极大的提高,所以二分查找针对的是:**插入、删除操作不频繁的数据**;

 3. 数据量太小不适合二分查找:

     ​      如果要处理的数据量很小,完全没有必要用二分查找,顺序遍历就足够了。比如我们在一个大小为 10 的数组中查找一个元素,不管用二分查找还是顺序遍历,查找速度都差不多。
     ​      不过,这里有一个例外。如果数据之间的比较操作非常耗时,不管数据量大小,我都推荐使用二分查找。比如,数组中存储的都是长度超过 300 的字符串,如此长的两个字符串之间比对大小,就会非常耗时。我们需要尽可能地减少比较次数,而比较次数的减少会大大提高性能,这个时候二分查找就比顺序遍历更有优势。

    4、数据量太大,不适合二分查找:比如,我们有 1GB 大小的数据,如果希望用数组来存储,那就需要 1GB 的连续内存空间,而这空间在内存中需要是连续的,换句话说,如果有2G的内存,连续空间不足1G,空间申请也是不成功的;
  1. Today's question: how to quickly find an integer in the 10 million integers?

    Our memory limit is 100MB, each data size is 8 bytes, the easiest way is to store data in an array, the memory footprint is almost 80MB, comply with the limits of memory. With today about the contents, we can first of these 10 million data from small to large, and then use binary search algorithm, you can quickly find the desired data.

    This problem does not seem difficult, it can be solved easily. In fact, it is the hidden "mystery." If you have some understanding of data structures and algorithms, we know the hash table, binary tree data structure which supports dynamic fast lookup. You may feel, use hash tables and binary tree can solve this problem. In fact, it is not acceptable.

    Although in most cases, using a binary search problem can be solved with a hash table, binary tree can be resolved. However, as we will talk about, whether it is a binary tree or hash table, you will need more extra memory space. If you use a hash table or a binary tree to store 10 million of this data, with 100MB of memory it is certainly no less than the deposit. The binary search is dependent on the underlying array, in addition to the data itself, no additional store other information, is the best way to save memory storage space, so just to solve this problem in a limited memory size.

Third, the binary search Adv.

    唐纳德·克努特(Donald E.Knuth)在《计算机程序设计艺术》的第 3 卷《排序和查找》中说到:“尽管第一个二分查找算法于 1946 年出现,然而第一个完全正确的二分查找算法实现直到 1962 年才出现。”

    你可能会说,我们上一节学的二分查找的代码实现并不难写啊。那是因为上一节讲的只是二分查找中最简单的一种情况,在不存在重复元素的有序数组中,查找值等于给定值的元素。最简单的二分查找写起来确实不难,但是,二分查找的变形问题就没那么好写了。

Common variants have binary search

A variant: a first lookup value equal to the given value element

  1. About wording but not well understood

    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;
    }
  2. Good point:

    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;
    }

    我来稍微解释一下这段代码。a[mid] 跟要查找的 value 的大小关系有三种情况:大于、小于、等于。对于 a[mid]>value 的情况,我们需要更新 high= mid-1;对于 a[mid]<value 的情况,我们需要更新 low=mid+1。这两点都很好理解。那当 a[mid]=value 的时候应该如何处理呢?

    如果我们查找的是任意一个值等于给定值的元素,当 a[mid] 等于要查找的值时,a[mid] 就是我们要找的元素。但是,如果我们求解的是第一个值等于给定值的元素,当 a[mid] 等于要查找的值时,我们就需要确认一下这个 a[mid] 是不是第一个值等于给定值的元素。

    我们重点看第 11 行代码。如果 mid 等于 0,那这个元素已经是数组的第一个元素,那它肯定是我们要找的;如果 mid 不等于 0,但 a[mid] 的前一个元素 a[mid-1] 不等于 value,那也说明 a[mid] 就是我们要找的第一个值等于给定值的元素。

    如果经过检查之后发现 a[mid] 前面的一个元素 a[mid-1] 也等于 value,那说明此时的 a[mid] 肯定不是我们要查找的第一个值等于给定值的元素。那我们就更新 high=mid-1,因为要找的元素肯定出现在 [low, mid-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;
}

我们还是重点看第 11 行代码。如果 a[mid] 这个元素已经是数组中的最后一个元素了,那它肯定是我们要找的;如果 a[mid] 的后一个元素 a[mid+1] 不等于 value,那也说明 a[mid] 就是我们要找的最后一个值等于给定值的元素。

如果我们经过检查之后,发现 a[mid] 后面的一个元素 a[mid+1] 也等于 value,那说明当前的这个 a[mid] 并不是最后一个值等于给定值的元素。我们就更新 low=mid+1,因为要找的元素肯定出现在 [mid+1, high] 之间。

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

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;
}

如果 a[mid] 小于要查找的值 value,那要查找的值肯定在 [mid+1, high] 之间,所以,我们更新 low=mid+1。

对于 a[mid] 大于等于给定值 value 的情况,我们要先看下这个 a[mid] 是不是我们要找的第一个值大于等于给定值的元素。如果 a[mid] 前面已经没有元素,或者前面一个元素小于要查找的值 value,那 a[mid] 就是我们要找的元素。这段逻辑对应的代码是第 7 行。

如果 a[mid-1] 也大于等于要查找的值 value,那说明要查找的元素在 [low, mid-1] 之间,所以,我们将 high 更新为 mid-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;
}

五:如何快速定位出一个 IP 地址的归属地?

现在这个问题应该很简单了。如果 IP 区间与归属地的对应关系不经常更新,我们可以先预处理这 12 万条数据,让其按照起始 IP 从小到大排序。如何来排序呢?我们知道,IP 地址可以转化为 32 位的整型数。所以,我们可以将起始地址,按照对应的整型值的大小关系,从小到大进行排序。

然后,这个问题就可以转化为我刚讲的第四种变形问题“在有序数组中,查找最后一个小于等于某个给定值的元素”了。

当我们要查询某个 IP 归属地时,我们可以先通过二分查找,找到最后一个起始 IP 小于等于这个 IP 的 IP 区间,然后,检查这个 IP 是否在这个 IP 区间内,如果在,我们就取出对应的归属地显示;如果不在,就返回未查找到。

Guess you like

Origin www.cnblogs.com/liweiweicode/p/12008049.html