查找——二分查找

  二分查找也叫做折半查找,查找的对象是已经排好序的序列(一般默认为升序)

  让我们来看看原理:顾名思义,就是先将中间数和目标key比较,如果相等则返回其索引,否则把序列分成两半,根据大小判断所查找的key在哪一半中,对这一半序列再重复上述步骤,直到找到目标key或查找完序列。

一般的二分查找

  被查找的序列arr中无重复的元素,在此序列中查找目标数target。

  被查找的序列示例:int[] arr1 = { 1, 2, 4, 5, 8, 13, 19, 20, 33, 38, 40, 48, 88 }。

public static int binarySearch(int[] arr, int target) {
    if (arr == null || arr.length == 0) {
        return -1;
    }
   // 确定别查找的区间
int left = 0; int right = arr.length - 1;
   // 开始查找【left,right】区间
while (left <= right) {
     // 找到中间数的索引
int mid = (left + right) / 2;
     // 判断 中间数 和 target 的大小
if (arr[mid] == target) {     // 若相等,则返回其索引 return mid; } else if (arr[mid] > target) {  // 若大于,确定前半段区间 right = mid - 1; } else if (arr[mid] < target) {  // 若小于,确定后半段区间 left = mid + 1; } }
   // 如果未查找到,则返回-1
return -1; }

  第一步,初始化原序列,left指向索引为0的位置,即此区间的第一个元素right指向索引为arr.length-1的位置,即此区间最后一个元素,也就是说这个区间是闭区间,[left,right]区间内的元素都是被查的元素。

  第二步找到中间元素的索引mid,判断中间元数和目标数target的值是否相等

  • 若中间数等于target,说明已经找到了目标数了查找成功,直接返回其索引mid;
  • 若中间数小于target,说明要查找的数在后半段,即[mid+1,right],所以将mid+1值赋给left,确定下次要查找的区间,重复第二步;
  • 若中间数大于target,说明要查找的数在前半段,即[left,mid-1],所以将mid-1值赋给right,确定下次要查找的区间,重复第二步;

   第三步当查找到left==right时是最后一个区间,即[left,left],此区间只有一个元素,即下标为left的元素。如果此元素是目标数查找成功返回其索引如果不是,会重新确定left或right的值使得left>right查找完毕退出循环,返回-1

有重复元素的二分查找

  被查找的序列arr中有重复的元素,在此序列中查找目标数target。

  被查找的序列示例:int[] arr1 = { 1, 2, 5, 5, 5, 13, 19, 33, 33, 38, 40, 48, 48 }。

1.寻找左侧边界的二分查找

  顾名思义,就是当序列中被查找的数有多个时,找到最左边的那个数的位置,然后返回其索引。当然,如果被查找的数只有一个,找到并返回其索引就好了,否则返回-1。

public static int binarySearch1(int[] arr, int target) {
    if (arr == null || arr.length == 0) {
        return -1;
    }
    int left = 0;
    int right = arr.length;               // 右侧未闭合
    while (left < right) {                // left==right时,退出循环
        int mid = (left + right) / 2;
        if (arr[mid] == target) {         // 找到时,right向右侧靠拢,记住此位置
            right = mid;
        }else if (arr[mid] < target) {    // 小于时,左侧闭合,向右靠拢
            left = mid + 1;
        }else if (arr[mid] > target) {    // 大于时,右侧不闭合,向左靠拢
            right = mid;
        }
    }
    if (left == arr.length) {            // 如果target大于所有元素,返回-1
        return -1;
    }
    return arr[left] == target ? left : -1;
}

   大框架并没有发生变化,改变的地方需要说一下:

  首先,while的条件变了, left <= righ 变成了 left < right ,当left == right时退出循环,为什么要变呢?因为我们这次所查找区间是左闭右开的,[left,right),left增长到left == right时,所查找的区间已查找完毕,这时我们就要退出循环了。为什么要这样设计?此中妙处,请往下看。

   if (left == arr.length) { return -1; } 这段代码是干啥的?这是当我们要查找的数大于此序列的所有数时,left会一直增加到left == right,而此时的right并未改变,就是arr.length。这段代码是为了处理这这种情况

  当中间数不是target时,调整下次要查询的区间的范围,大家应该可以理解,因为这是左闭右开的区间。

  当中间数等于target时, right = mid; ,怎么回事?找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。当然,如果此时right锁定的就是左边界,left会一直向右收缩,直到left == right。

  最后的 return arr[left] == target ? left : -1; 是为了应对target小于所有数时的情况和是否找到target时的情况。

 2.寻找右侧边界的二分搜索

  与上面的相反,就是当序列中被查找的数有多个时,找到最右边的那个数的位置,然后返回其索引。当然,如果被查找的数只有一个,找到并返回其索引就好了,否则返回-1。

public static int binarySearch2(int[] arr, int target) {
    if (arr == null || arr.length == 0) {
        return -1;
    }
    int left = 0;
    int right = arr.length;
    while (left < right) {
        int mid = (left + right) / 2;
        if (arr[mid] == target) {
            left = mid+1;
        }else if (arr[mid] < target) {
            left = mid+1;
        }else if (arr[mid] > target) {
            right = mid;
        }
    }
    if (left == 0) {
        return -1;
    }
    return arr[left-1] == target ? (left-1) : -1;
}

  当我们查找左边界的时候,我们是用right指针来锁定左侧边界的;同理,当我们查找右边界的时候,我们用left来锁定右边界,但有一点小区别,因为查找区间是左闭右开的,left指向的元素在查找范围内,因此我们用left的前一个元素来锁定右边界

  这就是为什么当中间元素等于target时,我们要 left = mid+1; 呢,left的前一个元素最终会锁定的右边界,这也就对应了后面的return语句中为什么是left - 1,因为left - 1指向的最右边的target元素

   if (left == 0) { return -1; } 这段代码是为了应对当target小于所有元素时的情况,因为后面有left-1,没有这条语句的话会造成索引越界的情况。

总结一下

  二分查找有一个实现框架:

public static int binarySearch(int[] arr, int target) {
    if (arr == null || arr.length == 0) {
        return -1;
    }
    int left = 0;
    int right = --------;
    while (--------) {
        int mid = (left + right) / 2;
        if (arr[mid] == target) {
            --------;
        } else if (arr[mid] > target) {
            right = --------;
        } else if (arr[mid] < target) {
            left = --------;
        }
    }
    return --------;
}

  当我们实现时根据题目的具体要求,来调整框架中所填部分的值。

  一般的二分查找最为简单;当有重复数字时,查找相对复杂一些,本文中只提到了查找左右边界的情况,但平常我们还会遇到一些变种的二分查找情况,比如:查找最后一个等于或者小于key的元素查找最后一个小于key的元素查找第一个等于或者大于key的元素查找第一个大于key的元素等。虽然变化很多,但万变不离其宗!读者只要理解查找的原理和每一步的过程,将其融会贯通,便可攻无不克。

 

posted @ 2020-09-18 19:17  城北有个混子  阅读(664)  评论(0编辑  收藏  举报