二分查找
说明
本文记录了笔者做leetcode二分查找题(冲一个二分查找勋章)的过程,实现语言为JAVA。
leetcode35题和704题是最基础的二分查找,用了三个方法/模版:二分查找基础版,二分查找改动版和二分查找平衡版。首先解决这两道题,从而熟悉二分查找的模版以及模版之间的区别。
35. 搜索插入位置/704. 二分查找
(1) 二分查找基础版
基础版查找区间包括j(右指针),查找区间内没有值的时候跳出循环。
class Solution {
public int searchInsert(int[] nums, int target) {
int i = 0, j = nums.length - 1; //设置指针的初始值
while( i <= j){//如果区间内有值
int m = (i + j) >>> 1;//计算中间值
if(target < nums[m]){ //目标在左边
j = m - 1;
}else if( target > nums[m]){//目标在右边
i = m + 1;
}else{
return m;//找到了
}
}return i ;//找不到返回将会被按顺序插入的位置,如果是704,就返回-1
}
}
要点:
- 为什么是while (i <= j) ,而不是(i < j) ?
因为最后一次比较时,i = j = m,如果此时a[m] = target就返回m,如果不等于就说明找不到该元素;如果只是(i < j) ,则漏掉了最后一次比较,可能造成本来数组中有这个元素却找不到。 - 为什么在计算m的时候用m = (i + j) >>>1, 而不是 m = (i + j) / 2?
因为二进制数有两种表现形式,一种是把最高位看成符号位,另一种是看成普通数字,但JAVA中默认是符号位,所以两个正数相加有可能是负数,进一步(i + j) / 2的结果也是负数。如果使用 > > > >>> >>>(无符号右移运算符),右移的效果就是整除2(向下取整)。
(2) 二分查找改动版
改动版要点(三个改动):
- j = a.length,j不参与循环内的比较,扮演区间边界的角色;
- 循环条件(i < j)而不是(i <= j),是为了避免进入j = m,m = j的死循环;
- j = m 同1的解释。
class Solution {
public int searchInsert(int[] nums, int target) {
int i = 0, j = nums.length;// 第一处改动,j不指向任何数
while( i < j){//第二处改动,i<j以避免陷入死循环
int m = (i + j) >>> 1;
if(target < nums[m]){
j = m ;//第三处改动,j占到m的位置
}else if( target > nums[m]){
i = m + 1;
}else{
return m;
}
}return i ;//找不到返回将会被按顺序插入的位置,如果是704,就返回-1
}
}
(3)二分查找平衡版
二分查找基础版和改动版都存在一个问题:向左查找和向右查找的比较次数不平衡,所以平衡版通过减少循环内的平均比较次数解决了这个问题,数据量很大的时候可以体现这个优势。
class Solution {
public int searchInsert(int[] nums, int target) {
int i = 0, j = nums.length; //j不在查找区间
while( j - i > 1){ // 当区间内有多于1个值时,循环只是为了缩小区间
int m = (i + j) >>> 1;
if(target < nums[m]){
j = m;
}else{//去掉else if,只需要比较一次,比较次数就平衡了
i = m;//target >= nums[m],所以i要包括中间值
}
}if(target <= nums[i]){//当查找区间只有一个元素(i指向的元素)时退出循环
return i;
}else{
return i+1;//找不到返回将会被按顺序插入的位置,如果是704,就返回-1
}
}
}
744. 寻找比目标字母大的最小字母
这里用的是二分查找的第三个模版,在while循环中,如果target == a[m]时令i = m,就是一直网友找,如果是令j = m,就是一直往左找。最后找到target的左右邻居,然后根据条件判断返回值。
class Solution {
public char nextGreatestLetter(char[] letters, char target) {
int i = 0, j = letters.length - 1;//i,j是查找范围的两端
while(j - i > 1){ //当查找范围内至少有三个值的时候
int m = (i+j) >>> 1;
if(target >= letters[m]){
i = m;//一直往右找
}else{
j = m;
}
}if(letters[i] > target){//进行一系列条件判断
return letters[i];
}else if(letters[i] <= target && target < letters[j]){
return letters[j];
}else{
return letters[0];
}
}
}
69. x的平方根
利用二分查找基础版。
首先考虑,0和1的平方根都是它本身,所以当x<2时,返回x。从2开始,使用二分查找基础版,一直缩小查找范围,当i=j时,i也等于m,相当于判断i/j是不是x的平方根,如果i比x的实际平方根小,即i<x/i,则i右移一位。直到i>j,此时退出循环,返回的j的值就是要找的平方根,如果j比实际平方根大,则左移一位,i>j退出循环,还是返回j。
要点:x与m*m的比较需要占用更大的内存,所以要写成x/m与m的比较。此外,因为x是>=2的整数,所以m不会等于0。
class Solution {
public int mySqrt(int x){
if(x < 2){
return x;
}
int i = 0, j = x;
while(i <= j){
int m = (i + j) >>> 1;
if(x/m < m){
j = m - 1;
}else if(x/m > m){
i = m + 1;
}else{
return m;
}
}return j;
}
}
374. 猜数字大小
这道题用的是二分查找基础版,注意循环结束还要返回一个值。
/**
* Forward declaration of guess API.
* @param num your guess
* @return -1 if num is higher than the picked number
* 1 if num is lower than the picked number
* otherwise return 0
* int guess(int num);
*/
public class Solution extends GuessGame {
public int guessNumber(int n){
int i = 1, j = n;
while(i <= j){
int m = (i + j) >>> 1;
if(guess(m) == -1){//判断pick与m的大小,如果pick小于m返回-1
j = m - 1;
}else if(guess(m) == 1){//判断pick与m的大小,如果pick大于m返回1
i = m + 1;
}else{
return m;}
}return 0;//因为是1~n的数字,如果它输入有误的话就返回0
}
}
278. 第一个错误的版本
使用二分查找改动版进行解答。
/* The isBadVersion API is defined in the parent class VersionControl.
boolean isBadVersion(int version); */
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
int i = 0, j = n; //i是第一个版本的索引,j指向查找范围的右侧。
while(i < j){
int m = (i + j) >>> 1;
if(isBadVersion(m + 1) == true){//注意m+1是索引为m对应的版本号
j = m;
}else{
i = m + 1;
}
}return i + 1;//返回版本号
}
}
162. 寻找峰值
class Solution {
public int findPeakElement(int[] nums) {
int i = 0;
int j = nums.length - 1; //j在查找区间内
while(i < j){ //查找区间至少有两个值
int m = (i + j) >>> 1;
if(nums[m] > nums[m + 1]){//如果是下坡
j = m;//让j位于m这个顶
}
else{
i = m + 1;//如果是上坡,让i位于顶
}
}
return i;//当i = j时,上坡坡顶和下坡坡顶汇于一点,就是峰值
}
}
852.山脉数组的峰顶索引
class Solution {
public int peakIndexInMountainArray(int[] arr) {
int i = 0 , j = arr.length - 1; //i,j都在查找范围内
while(j - i > 1){ //查找范围内两个以上的数值
int m = (i + j) >>> 1;
if(arr[m] > arr[m + 1]){//如果中间值大于右值,向左继续查找
j = m;
}else{//如果中间值小于右值,向右继续查找
i = m;
}
}if(arr[i] > arr[j]){//最后剩下两个相邻的值进行判断
return i;
}else{
return j;
}
}
}
34. 在排序数组中查找元素的第一个和最后一个位置
说明leftmost和rightmost函数的功能:
找到与目标相等的重复元素最左侧值的索引(Leftmost)应用场景:求排名(Leftmost+1),求前任(Leftmost -1),求小于target的所有值(索引0到索引Leftmost -1),求大于等于target的所有值(leftmost到最后一个元素)。
找到与目标相等的重复元素最右侧值的索引(Rightmost)应用场景:求后任(rightmost +1),求大于target的所有值(索引rightmost +1到最后一个元素),求小于等于target的所有值(索引0到索引rightmost)。
class Solution {
public int[] searchRange(int[] nums, int target) {
int x = leftMost(nums,target);
if(x == -1){
return new int[]{-1,-1};
}else{
return new int[]{x, rightMost(nums,target)};
}
}
public int leftMost(int[] nums, int target){
int i = 0, j = nums.length - 1;
int candidate = -1;
while(i <= j){
int m = (i+j) >>> 1;
if(target < nums[m]){
j = m - 1;
}else if(target > nums[m]){
i = m + 1;
}
else{
j = m - 1;
candidate = m;
}
}return candidate;
}
public int rightMost(int[] nums, int target){
int i = 0, j = nums.length - 1;
int candidate = -1;
while(i <= j){
int m = (i+j) >>> 1;
if(target < nums[m]){
j = m - 1;
}else if(target > nums[m]){
i = m + 1;
}else{
i = m + 1;
candidate = m;
}
}return candidate;
}
}