vlambda博客
学习文章列表

二分查找及其应用总结

1. 二分查找及应用

二分查找是一种常见的搜索算法,思想很简单,麻烦的地方在于边界细节处理。坊间戏言二分查找属于玄学编程,可见细节处理确实让大家比较头疼。网上也有不少二分查找模板,大家可以自行搜索。   

为了便于总结和学习二分查找算法,本文总结了一些常见使用场景,粗略分类如下:

  • 查找特定值

  • 查找特定值的左右边界

  • 查找比特定值大的值

  • 查找最小值

  • 查找峰值

本文第六部分也粗略总结了一个模板和注意事项,主要是便于个人理解,欢迎大家指正。


2 查找特定值

2.1 LCD33 查找旋转的排序数组中特定值

public int search(int[] nums, int target) {    return binarySearch(nums,0,nums.length-1,target);}int binarySearch(int[] nums, int low, int high, int target) { if (low > high) return -1; //非法区间,尝试了所有的查找区间仍为找到结果 int middle = (low + high) / 2;     if (nums[middle] == target) return middle;  //找到目标 if (nums[low] <= nums[middle]) { //若low到middle是有序的 if (nums[low] <= target && target < nums[middle]) return binarySearch(nums, low, middle - 1, target); else return binarySearch(nums, middle + 1, high, target); } else { //middle到high是有序的 if (nums[middle] < target && target <= nums[high]) return binarySearch(nums, middle + 1, high, target); else return binarySearch(nums, low, middle - 1, target); }}

2.2 LCD81 查找旋转的排序数组中特定值,有重复元素

public boolean search(int[] nums, int target) { return binarySearch(nums, 0, nums.length - 1, target);}boolean binarySearch(int[] nums, int low, int high, int target) { if (low > high) return false; int middle = (low + high) / 2; if (nums[middle] == target) return true; if (nums[low] < nums[middle]) { //low到middle是有序的 if (nums[low] <= target && target < nums[middle]) return binarySearch(nums, low, middle - 1, target); else return binarySearch(nums, middle + 1, high, target); } else if(nums[low] > nums[middle] ) { //middle到high是有序的 if (nums[middle] < target && target <= nums[high]) return binarySearch(nums, middle + 1, high, target); else return binarySearch(nums, low, middle - 1, target); } else  return binarySearch(nums, low + 1, high, target); //去重}

2.3 LCD74  查找有序矩阵的目标值,矩阵如下特性:

  • 每行中的整数从左到右按升序排列。

  • 每行的第一个整数大于前一行的最后一个整数

public boolean searchMatrix(int[][] matrix, int target) { if(matrix == null || matrix.length == 0 ) return false; int rows = matrix.length; int cols = matrix[0].length; int low = 0, high = rows*cols -1; int mid,midElement; while( low <= high){ mid = (low + high)/2; midElement = matrix[mid/cols][mid%cols]; //转换成行列 if( midElement == target) return true; else if(midElement > target) high = mid -1; else low = mid +1; } return false;}

2.4 LCD240 查找有序矩阵的目标值,矩阵如下特性:

  • 每行的元素从左到右升序排列

  • 每列的元素从上到下升序排列

public boolean searchMatrix(int[][] matrix, int target) { if( matrix == null || matrix.length == 0 || matrix[0].length == 0) return false; int row = 0, col = matrix[0].length -1; //选择从右上角落开始搜,往下变大,往左变小 int mid ; while( row <= matrix.length-1 && col >= 0 ){ if( matrix[row][col] == target) return true; else if(matrix[row][col] < target) row ++; else col --; } return false;}


3 查找特定值的左右边界

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

int findStart(int[] nums, int low, int high, int target) { if (low > high) return -1; int middle = (low + high) / 2; if (nums[middle] == target && (middle == 0 || nums[middle - 1] < nums[middle])) return middle; if (target <= nums[middle]) return findStart(nums, low, middle - 1, target); else return findStart(nums, middle + 1, high, target);}int findEnd(int[] nums, int low, int high, int target) { if (low > high) return -1; int middle = (low + high) / 2; if (nums[middle] == target && (middle == nums.length - 1 || nums[middle + 1] > nums[middle])) return middle; if (target >= nums[middle]) return findEnd(nums, middle + 1, high, target); else return findEnd(nums, low, middle - 1, target);}


4 查找比特定值大的值

4.1 LCD744 寻找比目标字母大的最小字母

public char nextGreatestLetter(char[] letters, char target) { return nextGreatestLetter(letters,target,0,letters.length -1 );}public char nextGreatestLetter(char[] letters, char target, int low, int high) { if (low > high) return letters[0]; //查找区间都没有找到比target大的字母,则返回letters中的第一个字母 int mid = low + (high - low) / 2; if( letters[mid] > target && (mid == 0 || letters[mid-1] <= target))  return letters[mid]; if( letters[mid] <= target) return nextGreatestLetter(letters,target,mid+1,high); else return nextGreatestLetter(letters,target,low,mid-1);}


5 查找最小值

5.1 LCD153  寻找旋转排序数组中的最小值

public int findMin(int[] nums) { if(nums.length == 1) return nums[0]; return findMin(nums,0, nums.length-1);}public int findMin(int[] nums, int l, int h) { if( l > h) return -1; //just for return, unreachable int m = (l + h) / 2; if( m >=1 && nums[m-1] > nums[m]) return nums[m]; // 解空间为[1, nums.length-1]  if( m==0 && nums[m] < nums[m+1]) return nums[m]; // 解为0  if ( nums[m] >= nums[h])  return findMin(nums,m+1,h); else  return findMin(nums,l,m-1); }

5.2 LCD154  寻找旋转排序数组中的最小值-ii, 可能存在重复元素

public int findMin(int[] nums) { if(nums.length == 1) return nums[0]; return findMin(nums,0, nums.length-1);}public int findMin(int[] nums, int low, int high){ if( l > h) return -1; //just for return, unreachable  int m = (l + h) / 2;    if( m >=1 && nums[m-1] > nums[m]) return nums[m];              if( m==0 && nums[m] <= nums[m+1]) return nums[m];    if ( nums[m] > nums[h])                   return findMin(nums,m+1,h); else if( nums[m] < nums[h])  return findMin(nums,l,m-1);  else return findMin(nums,l,h-1); }


五 查找峰值

5.1 LCD162 寻找峰值

public int findPeakElement(int[] nums) { if( nums.length == 1) return 0; return binarySearch(0, nums.length -1,nums);}public int binarySearch(int l, int h, int[] nums) {  if (l > h ) return -1;  //just for return, unreachable int m = (l + h)/2; if( m >=1 && nums[m] > nums[m-1] && m+1 <= nums.length -1 && nums[m] > nums[m+1]) return m; // 解空间为[1, nums.length-2] if( m == 0 && nums[m] > nums[m+1] || m == nums.length -1 && nums[m] > nums[m-1]) return m; // 解空间包含0和nums.length-1 if( nums[m] < nums[m + 1]) return binarySearch(m+1, h, nums); else return binarySearch(l,m-1,nums);}


六 要点总结

通过前面例子,我们可以总结出二分查找的基本步骤:

public int binarySearch(int l, int h, int[] nums, int target) { if( l > h) .....; // 注意点 1) int m = l + (h - l) / 2 ; // 注意点 2) if(.....) return m; // 注意点 3) if(.....) // 注意点 4) return binarySearch(m+1, h, nums); else return binarySearch(l,m-1,nums);}


1) 越界处理

如果步骤3)中处理逻辑覆盖了所有元素,那这里只用返回-1即可,表示所nums中所有元素都不符合算法目标,实际应用中,可以会返回-1、false、null等。

2) 求取中间值

 一般使用 int m = l + (h -l) / 2 代替 ( h+l ) / 2,后者会存在溢出情况。

3) 满足条件则返回解

nums数组中的所有元素都有可能是算法的一个解,需要注意返回结果中是否考虑到了所有元素,不要有遗漏的情况。

4) 收缩查找区间

查找特定值的,一般用nums[mid]和nums[target]的比较结果来决定收缩哪部分查找区间;其他类型,nums[mid]可能需要和nums[low]、或nums[high]、或nums[mid+1]、或nums[mid-1]比较。

5) 去重处理

如果数组中存在重复元素,当nums[low] == nums[mid]时,只用继续查找[low+1,high]即可,如LCD81和LCD154。


七 延申

LCD35题目是如下:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。你可以假设数组中无重复元素。按照第六部分总结得出解法如下:

public int searchInsert(int[] nums, int target) { return searchInsert(nums,target,0, nums.length-1);}public int searchInsert(int[] nums, int target,int l, int h) { if(l > h) return -1; // just for return ,unreachable int mid = l + (h - l) / 2 ;  if( nums[mid] == target) return mid; //找到返回索引  if( nums[mid] < target && mid + 1 <= nums.length -1 && target < nums[mid+1]) return mid+1; //解空间[1,nums.length-1] if( mid == 0 && target < nums[0]) return mid; //解空间0 if( mid == nums.length -1 && target > nums[mid]) return mid +1; //解空间nums.length if (nums[mid] > target) return searchInsert(nums,target,l,mid-1); else return searchInsert(nums,target,mid+1,h);}

上面这种写法略显复杂的,好处是考虑了所有的返回情况,比较好理解。其实有个更简洁的写法,如下:

public int searchInsert(int[] nums, int target) { int l = 0, h = nums.length-1; while(l <= h){ int mid = l + (h-l)/2; if( nums[mid] == target) return mid; if (nums[mid] > target) h = mid -1; else l = mid +1; } return l; //l = h +1,l的区间范围是[0,nums.length]}