二分查找

二分查找

二分查找(Binary Search)算法,也叫折半查找算法

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

时间复杂度:O(logn)

假设数据大小是 n,每次查找后数据都会缩小为原来的一半,也就是会除以 2。最坏情况下,直到查找区间被缩小为空,才停止。

其中 n/2k=1 时,k 的值就是总共缩小的次数。

而每一次缩小操作只涉及两个数据的大小比较,所以,经过了 k 次区间缩小操作,时间复杂度就是 O(k)。

通过 n/2k=1,我们可以求得 k=log2n,所以时间复杂度就是 O(logn)

指数时间复杂度的算法在大规模数据面前是无效的

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

非递归实现

有序数组中不存在重复元素,用二分查找值等于给定值的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;

while (low <= high) {
//int mid = (low + high) / 2;
int mid = low+(high-low)/2;
if (a[mid] == value) {
return mid;
} else if (a[mid] < value) {
low = mid + 1;
} else {
high = mid - 1;
}
}

return -1;
}

注意:

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

  2. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 二分查找的递归实现
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);
}
}

二分查找应用场景的局限性

  1. 首先,二分查找依赖的是顺序表结构,简单点说就是数组。

    二分查找只能用在数据是通过顺序表来存储的数据结构上。如果你的数据是通过其他数据结构存储的,则无法应用二分查找。

  2. 其次,二分查找针对的是有序数据。

    二分查找只能用在插入、删除操作不频繁,一次排序多次查找的场景中

  3. 再次,数据量太小不适合二分查找。

    例外:如果数据之间的比较操作非常耗时,不管数据量大小,推荐使用二分查找。比如,数组中存储的都是长度超过 300 的字符串,如此长的两个字符串之间比对大小,就会非常耗时。我们需要尽可能地减少比较次数,而比较次数的减少会大大提高性能,这个时候二分查找就比顺序遍历更有优势。

  4. 最后,数据量太大也不适合二分查找。

二分查找变形问题

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

问题:有序数据集合中存在重复的数据,希望找到第一个值等于给定值的数据

下面这样一个有序数组,其中,a[5],a[6],a[7]的值都等于 8,是重复的数据。希望查找第一个等于 8 的数据,也就是下标是 5 的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//写法一:
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;
}

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

问题:有序数据集合中存在重复的数据,希望查找最后一个值等于给定值的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

查找第一个大于等于给定值的元素

问题:在有序数组中,查找第一个大于等于给定值的元素

数组中存储的这样一个序列:3,4,6,7,10。如果查找第一个大于等于 5 的元素,那就是 6。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

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

问题:查找最后一个小于等于给定值的元素

数组中存储了这样一组数据:3,5,6,8,9,10。最后一个小于等于 7 的元素就是 6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

LeetCode实战经验

  1. target 是在一个在左闭右闭的区间,[left, right]

    left = 0;
    right = nums.length - 1;

    二分细节

    循环体:当left==right,区间[left, right]依然有效,所以用 <=

    1
    while (left <= right){};

    target 在左区间

    1
    right = middle - 1;

    target 在右区间

    1
    left = middle + 1;
  2. target 是在一个在左闭右开的区间里,[left, right)

    left = 0;
    right = nums.length;

    二分细节

    循环体:因为left == right的时候,在[left, right)是无效的空间,所以使用 <

    1
    while (left < right){};

    target 在左区间

    1
    right = middle;

    target 在右区间

    1
    left = middle + 1;

Leetcode实战

34. 在排序数组中查找元素的第一个和最后一个位置

  1. 先找到位置,遍历左右

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    class Solution {
    public int[] searchRange(int[] nums, int target) {
    int len = nums.length;
    if(len == 0 || nums[0] > target || nums[len-1] < target){
    return new int[]{-1,-1};
    }
    int result = -1;
    result = binarySearch(nums,target);
    if(result != -1){
    int left = result;
    int right = result;
    while(left - 1 >= 0 && nums[left-1] == target ){
    left--;
    }
    while(right + 1 < len && nums[right+1] == target ){
    right++;
    }
    return new int[]{left,right};
    }else{
    return new int[]{-1,-1};
    }
    }

    public int binarySearch(int[] nums, int target) {
    int left = 0;
    int right = nums.length;
    while(left < right){
    int mid = left + (right - left)/2;
    if(nums[mid] > target){
    right = mid;
    }else if(nums[mid] < target){
    left = mid + 1;
    }else{
    return mid;
    }
    }
    return -1;
    }
    }
  2. 直接找到左右边界

    1. 寻找右边界(不包括target)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      int getRightBorder(int[] nums, int target) {
      int left = 0;
      int right = nums.length - 1;
      int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
      while (left <= right) {
      int middle = left + ((right - left) / 2);
      if (nums[middle] > target) {
      right = middle - 1;
      } else { // 寻找右边界,nums[middle] == target的时候更新left
      left = middle + 1;
      rightBorder = left;
      }
      }
      return rightBorder;
      }
    2. 寻找左边界(不包括target)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      int getLeftBorder(int[] nums, int target) {
      int left = 0;
      int right = nums.length - 1;
      int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况
      while (left <= right) {
      int middle = left + ((right - left) / 2);
      if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right
      right = middle - 1;
      leftBorder = right;
      } else {
      left = middle + 1;
      }
      }
      return leftBorder;
      }
    3. 主函数

      1
      2
      3
      4
      5
      6
      7
      int[] searchRange(int[] nums, int target) {
      int leftBorder = getLeftBorder(nums, target);
      int rightBorder = getRightBorder(nums, target);
      if (leftBorder == -2 || rightBorder == -2) return new int[]{-1, -1};
      if (rightBorder - leftBorder > 1) return new int[]{leftBorder + 1, rightBorder - 1};
      return new int[]{-1, -1};
      }

35. 搜索插入位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public int searchInsert(int[] nums, int target) {
int len = nums.length;
if(len == 0 || nums == null){
return 0;
}
int left = 0;
int right = nums.length;
while(left < right){
int mid = left + (right - left)/2;
if(nums[mid] > target){
right = mid;
}else if(nums[mid] < target){
left = mid + 1;
}else{
return mid;
}
}
// 分别处理如下四种情况
// 目标值在数组所有元素之前 [0,0)
// 目标值等于数组中某一个元素 return middle
// 目标值插入数组中的位置 [left, right) ,return right 即可
// 目标值在数组所有元素之后的情况 [left, right),因为是右开区间,所以 return right
return right;
}
}
查看评论