二分查找 基础不好还怎么继续塔塔开,这里再次好好分析一下两个模板
分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节。https://blog.csdn.net/xiao_jj_jj/article/details/106018702 当我们把二分的各种模板看作搜索区[left,right]的各种变体,就好理解很多了
分析 模板A 我喜欢叫做加减模板,在每次更新时都是对left/right进行加减处理 可以说是最常见的模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int binarySearch (int [] nums, int target) { if (nums == null || nums.length == 0 ) return -1 ; int left = 0 , right = nums.length - 1 ; while (left <= right){ int mid = left + (right - left) / 2 ; if (nums[mid] == target) { return mid; }else if (nums[mid] < target) { left = mid + 1 ; }else { right = mid - 1 ; } } return -1 ; }
和下面那位老兄相比,它的特征在于left <= right
以及left = mid + 1;
,left = mid + 1;
结束条件是left > right 也就是左侧超过右侧的时候
那么,我们一般用它做什么呢 查找,而且是数组中的单个索引 确定的元素/条件 其关键属性可以总结为: 1.查找条件可以在不与元素的两侧进行比较的情况下确定 2.不需要后续处理,因为在每一步中都在检查是否找到 ,到达末尾还没有得到结果,那就是找不到了
经典的“猜数字”就是这种模板最简单的体现
高级模板A 一种以查找元素右邻居 为核心的模板 或者说寻找左侧边界
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int binarySearch (int [] nums, int target) { if (nums == null || nums.length == 0 ) return -1 ; int left = 0 , right = nums.length; while (left < right){ int mid = left + (right - left) / 2 ; if (nums[mid] == target) { return mid; }else if (nums[mid] < target){ left = mid + 1 ; }else { right = mid; } } if (left != nums.length && nums[left] == target) return left; return -1 ; }
和经典模板最大的不同在于while(left < right)
,因为在二者重合的时候我们就会跳出来了 与之对应的是right = mid
,或者说在左移的时候,我们不令右界等于middle-1了结束条件是left == right
1.查找条件需要访问元素的直接右邻居。 2.使用元素的右邻居 来确定是否满足条件,并决定是向左还是向右。 3.保证查找空间在每一步中至少 有 2 个元素。 4.需要进行后处理。 当你剩下 1 个元素时,循环/递归结束。 需要评估剩余元素是否符合条件。
可以看作一个不断“在右侧元素”上分析并排除的过程,也因此是right = mid;
而非-1 当排除到只剩下left和right的时候,检查left是不是符合条件,如果是就可以了 如果不是,我们已经知道right = middle,已经判断过middle即right不行了。那此题肯定就无解了
典型例子是“寻找第一个错误版本”
高级模板C 再次加强版需要访问当前索引及其在数组中的直接左右邻居索引 来判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int binarySearch (int [] nums, int target) { if (nums == null || nums.length == 0 ) return -1 ; int left = 0 , right = nums.length - 1 ; while (left + 1 < right){ int mid = left + (right - left) / 2 ; if (nums[mid] == target) { return mid; } else if (nums[mid] < target) { left = mid; } else { right = mid; } } if (nums[left] == target) return left; if (nums[right] == target) return right; return -1 ; }
理解了模板B看它就还好了,它更进一步:判断的是左右两边的情况 所以,无论左右,更新都是等于middle了 在最后,我们会得到最终的3个元素,而进行后处理的时候剩下两个元素 那就是left和right了,检测它们是否符合条件即可
1.搜索条件需要访问元素的直接左右邻居。 2.使用元素的邻居来确定它是向右还是向左。 3.保证查找空间在每个步骤中至少有 3 个元素。 4.需要进行后处理。 当剩下 2 个元素时,循环 / 递归结束。 需要评估其余元素是否符合条件
三模板 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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 public static int binarySerach1 (int [] nums, int target) { if (nums == null || nums.length == 0 ) { return -1 ; } int left = 0 , right = nums.length-1 ; while (left <= right) { int mid = left + (right-left)/2 ; if (nums[mid] == target) { return mid; } else if (nums[mid] > target) { right = mid-1 ; } else { left = mid+1 ; } } return -1 ; } int binarySearch2 (int [] nums, int target) { if (nums == null || nums.length == 0 ) return -1 ; int left = 0 , right = nums.length; while (left < right){ int mid = left + (right - left) / 2 ; if (nums[mid] == target){ return mid; } else if (nums[mid] < target) { left = mid + 1 ; } else { right = mid; } } if (left != nums.length && nums[left] == target) return left; return -1 ; } int binarySearch3 (int [] nums, int target) { if (nums == null || nums.length == 0 ) return -1 ; int left = 0 , right = nums.length - 1 ; while (left + 1 < right){ int mid = left + (right - left) / 2 ; if (nums[mid] == target) { return mid; } else if (nums[mid] < target) { left = mid; } else { right = mid; } } if (nums[left] == target) return left; if (nums[right] == target) return right; return -1 ; }
分析二分查找代码时,不要出现 else,全部展开成 else if 方便理解。 注意「搜索区间」和 while 的终止条件,如果存在漏掉的元素,记得在最后检查。 如需要搜索左右边界,只要在 nums[mid] == target 时做修改即可。搜索右侧时需要减一。
对于ABC三者,可以简单归结为: A.在严格递增有序数组中寻找某个数 B.在有序数组中寻找某个数第一次出现的位置(或者在有序数组中寻找第一个大于等于某个数的位置) C.已知有一个先严格递增后严格递减的数组,找数组最大值的位置
例题 猜数字(最经典的二分) 猜数字游戏的规则如下:
每轮游戏,我都会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。
如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。
你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-1,1 或 0):
-1:我选出的数字比你猜的数字小 pick < num
1:我选出的数字比你猜的数字大 pick > num
0:我选出的数字和你猜的数字一样。恭喜!你猜对了!pick == num
返回我选出的数字。
猜数字题解 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 public class Solution extends GuessGame { public int guessNumber (int n) { int left = 1 ; int right = n ; int middle = 0 ; int result = 0 ; while (left<=right){ middle = left + (right - left)/2 ; if (guess(middle) == 0 ){ result = middle ; break ; }else { if (guess(middle) == -1 ){ right = middle - 1 ; }else if (guess(middle) ==1 ){ left = middle + 1 ; } } } return result ; } }
山脉函数题目 符合下列属性的数组 arr 称为 山脉数组 :
arr.length >= 3
存在 i(0 < i < arr.length - 1)使得:
arr[0] < arr[1] < ... arr[i-1] < arr[i]
arr[i] > arr[i+1] > ... > arr[arr.length - 1]
给你由整数组成的山脉数组 arr ,返回任何满足 arr[0] < arr[1] < … arr[i - 1] < arr[i] > arr[i + 1] > … > arr[arr.length - 1] 的下标 i 。
山脉函数题解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution { public int peakIndexInMountainArray (int [] arr) { int left = 0 ; int right = arr.length - 1 ; int result = 0 ; int mid = 0 ; while (left<=right){ mid = left + (right-left)/2 ; if (arr[mid]>arr[mid+1 ]){ result = mid ; right = mid -1 ; }else { left = mid +1 ; } } return result ; } }