总结:
-
162 看似简答,却很不容易想到:
- 即使开始 l = 0, r = nums.length - 1; while里还是l < r;
- 因为不是要全部查看所有元素,当nums[l] == nums[r]时,就是答案;
- 比较判断是 nums[mid] 和 nums[mid+1], 目的是为了判断此时mid处于局部递减区域还是局部递增区域;
- 若nums[mid] < nums[mid+1] , 处于局部递增,峰值在后面
- 若nums[mid] > nums[mid+1] , 处于局部递减,峰值在前面
- 即峰值在nums[l]或nums[l]的右边,在nums[r]或nums[r]的左边;
- 当 l == r 时,就是峰值,夹击思想,这方面我不行;
- 边界问题:nums[mid]与nums[mid-1]比较,注意会有越界问题;
- 即使开始 l = 0, r = nums.length - 1; while里还是l < r;
-
比较162和153题,弄懂边界
- 162题:
- 当用nums[mid]和nums[mid+1]比较时,int mid = left + (right - left)/2;
- mid在数组只有两个元素时,指向前一个,所以[mid+1]不会越界;
- 当用nums[mid]和nums[mid-1]比较时,int mid = left + (right - left + 1)/2;
- mid在数组只有两个元素时,指向后一个,所以[mid-1]不会越界;
- 153题:
- 因为题目既需要判断nums[mid]和nums[mid+1],也需要判断nums[mid]和nums[mid-1],所以int mid = left + (right - left)/2; / left + (right - left + 1)/2; 当数组长度为2时,用哪个都不行;
- 这里的解决办法是用int mid = left + (right - left)/2; 先判断if(nums[mid] > nums[mid+1]) return nums[mid+1]; 满足直接返回,不会去判断nums[mid]和nums[mid-1],所以不会越界。
- 那如果不满足呢?即数组长度为2,且是升序;解决办法:在代码之前加上一个升序判断if(nums[nums.length - 1] > nums[0]) return nums[0];
-
再说一句,int mid = left + (right - left)/2;和int mid = left + (right - left + 1)/2;的区别:
- 前一个mid,数组长度为双数时,有时指向前一个中位,有时指向后一个中位;为2时,指向前一个中位;
- 后一个mid, 数组长度为双数时,一直指向后一个中位;
162. 寻找峰值 中等
峰值元素是指其值大于左右相邻值的元素。
给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。
数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞。
示例 1:
输入: nums = [1,2,3,1]
输出: 2
解释: 3 是峰值元素,你的函数应该返回其索引 2。
示例 2:
输入: nums = [1,2,1,3,5,6,4]
输出: 1 或 5
解释: 你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。
- 线性
- 前提:题目说明 nums[-1] = nums[n] = -∞。
- 所以一直是升序的序列,最后一个元素可以是峰值。
- 一直是降序的序列,最开始的元素可以是峰值。
- 三种情况:
- 单调递增,nums[i]一直小于nums[i+1], 循环结束,返回最后一个index;
- 单调递减,nums[i]一直大于nums[i+1],第一个就满足条件,返回第一个index;
- 先升后降,碰到nums[i] > nums[i+1],就返回index=i
- 先降后升 , 第一个就满足,返回第一个元素的index
示例 1:
class Solution {
public int findPeakElement(int[] nums) {
for(int i = 0; i < nums.length - 1; i++) {
if(nums[i] > nums[i+1]) return i;
}
return nums.length - 1;
}
}
-
二分法,比较nums[mid] <>? nums[mid+1]
- 单调递增,则峰值在后面
- 单调递减,峰值在前面
- 当l == r时,返回
-
为什么当l == r时,返回,这一个就是答案峰值?
- 单调递增,则 l 一直向右移动,找比自己更大的值,直到,l == r;
- 单调递减,则 r 一直向左移动,找比自己更大的值,直到,r == l;
- 又因为题目特殊,左右边界是峰值,所有成立;
- 不是单调递增,或单调递减;题目有说没有重复元素,则肯定存在峰值;
- 当nums[mid] < nums[mid+1],l 右移到 mid + 1位置;nums[l] 左边元素小于nums[l];
- 当nums[mid] > nums[mid+1],r 左移到 mid 位置;nums[r] 右边元素小于nums[r] ;
- 当 l == r时,两边元素均小于nums[l] == nums[r] ,即为峰值
-
为什么要比较nums[mid]和nums[mid+1], 比较nums[mid]和nums[md-1]不行吗?
- 都可以,稍微改一次逻辑就可以;
-
二分法边界问题:
- 最普通的二分法查找指定元素的边界问题:
- nums[mid]和指定值比较
- nums.length 对应 l < r; r = mid +1; l = mid;
- nums.length - 1 对应 l <= r; r = mid+1; l = mid - 1;
- 边界问题还要根据具体题目变通,比如这道题的边界:
- nums[mid] 和nums[mid+1]比较,判断此时的nums[mid]处于局部递增还是局部递减;
- nums.length - 1 对应 l < r; r = mid;l = mid + 1;
- 为什么是l < r, 不应该时l <= r吗?
- 当l < r时推出,此时nums[l]没有进入比较,直接返回,因为就是答案。
- 普通查找,如果l < r,就会漏掉一个l == r;[]双闭l==r区间是有值的;
- r = mid;l = mid + 1;就是将l和r位置移动较大的那个元素的位置上;
- nums[mid] 和nums[mid+1]比较不会越界,因为mid = left + (right-left)/2是向下取整,不会溢出;而如果nums[mid] 和nums[mid-1]会越界:
- eg: [0,1]中,mid = left + (right-left)/2 = 0 + (1-0)/2 = 0, mid-1=0-1,越界;
- 因此要加上 mid = left + (right-left + 1)/2; 加上1;
class Solution {
public int findPeakElement(int[] nums) {
int left = 0, right = nums.length - 1;
while(left < right) {
int mid = left + (right - left) / 2;
if(nums[mid] < nums[mid + 1]) {
left = mid + 1;
}else {
right = mid;
}
}
return left;
}
}
public class Solution {
public int findPeakElement(int[] nums) {
int len = nums.length;
int left = 0;
int right = len - 1;
// [left, right]
while (left < right) {
int mid = left + (right - left + 1) / 2;
if (nums[mid - 1] < nums[mid]) {
// 下一轮搜索的区间 [mid, right]
left = mid;
} else {
right = mid - 1;
}
}
// left == right
return left;
}
}
852. 山脉数组的峰顶索引 简单
我们把符合下列属性的数组 A 称作山脉:
A.length >= 3
存在 0 < i < A.length - 1 使得A[0] < A[1] < … A[i-1] < A[i] > A[i+1] > … > A[A.length - 1]
给定一个确定为山脉的数组,返回任何满足 A[0] < A[1] < … A[i-1] < A[i] > A[i+1] > … > A[A.length - 1] 的 i 的值。
示例 1:
输入:[0,1,0]
输出:1
示例 2:
输入:[0,2,1,0]
输出:1
- 可以套用前一个题的解,前提是答案一定在中间,不可能在数组两端;
- 上一个题解前提是数组两端也是峰值;看 没有这个前提就是错的;
class Solution {
public int peakIndexInMountainArray(int[] A) {
int l = 0, r = A.length - 1;
while(l < r) {
int mid = l + (r-l) / 2;
if(A[mid] < A[mid+1]) {
l = mid + 1;
}else {
r = mid;
}
}
return l;
}
}
153. 寻找旋转排序数组中的最小值
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
你可以假设数组中不存在重复元素。
示例 1:
输入: [3,4,5,1,2]
输出: 1
示例 2:
输入: [4,5,6,7,0,1,2]
输出: 0
// 有序数组从现在的nums[0]开始旋转,
// 分为两个部分:一部分比nums[0]大,一部分比nums[0]小
// 最小值就是这两部分的拐点,二分法
// 当nums[mid] > nums[0], 最小值在nums[mid]后面
// 当nums[mid] < nums[0], 最小值是nums[mid]或者在nums[mid]前面
// 因为最小值就是拐点,其他地方都是升序,只有拐点是局部降序,前面一个元素大于后面一个元素。
// 所以当nums[mid-1] > nums[mid] 或者 nums[mid] > nums[mid+1]时,nums[mid]/nums[mid+1]
// 是最小值
class Solution {
public int findMin(int[] nums) {
if(nums.length == 1) return nums[0];
if(nums[nums.length - 1] > nums[0]) return nums[0];
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] > nums[mid+1]) return nums[mid+1];
if(nums[mid - 1] > nums[mid]) return nums[mid];
if(nums[mid] > nums[0]) {
left = mid + 1;
}else if(nums[mid] < nums[0]) {
right = mid - 1;
}
}
return -1;
}
}
出错:
原因:
int mid = left + (right - left) / 2;
if(nums[mid-1] > nums[mid]) return nums[mid]; // 这里出错
if(nums[mid] > nums[mid+1]) return nums[mid+1];
解决:
调换位置,如果是两个元素的降序,直接直接就返回了,不会去判断nums[mid-1]
if(nums[mid] > nums[mid+1]) return nums[mid+1];
if(nums[mid-1] > nums[mid]) return nums[mid];
原因:
int mid = left + (right - left) / 2;
if(nums[mid] > nums[mid+1]) return nums[mid+1];
if(nums[mid-1] > nums[mid]) return nums[mid]; // 执行到这里出错
解决办法:
添加
if(nums[nums.length - 1] > nums[0]) return nums[0];
33. 搜索旋转排序数组
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是 O(log n) 级别。
示例 1:
输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4
示例 2:
输入: nums = [4,5,6,7,0,1,2], target = 3
输出: -1
出错代码:
- 原因:寻找山峰的前提条件是
class Solution {
public int search(int[] nums, int target) {
// 查找指定元素
// 数组: 先单调递增,突然下降,再单调递增
// 先二分找到这个拐点,前面数组的元素全部大于后面的数组,
// 先在前面的数组中二分找,找到返回;若没找到,到达right,返回-1;到达left, 在后面一组中找
// 怎么找拐点,就是找峰值
if(nums == null || nums.length == 0) return -1;
int guaidian = fengzhi(nums);
int res;
res = getNum(nums, 0, guaidian, target);
if(res != -1) {
return res;
}else {
res = getNum(nums, guaidian + 1, nums.length - 1, target);
}
return res;
}
private int fengzhi(int[] nums) {
int l = 0, r = nums.length - 1;
while(l < r) {
int mid = l + (r-l)/2;
if(nums[mid] < nums[mid+1]) {
l = mid +1;
}else if(nums[mid] > nums[mid+1]){
r = mid;
}
}
return l;
}
private int getNum(int[] nums, int l, int r, int target) {
while(l <= r) {
int mid = l + (r-l)/2;
if(nums[mid] < target) {
l = mid + 1;
}else if(nums[mid] > target) {
r = mid - 1;
}else {
return mid;
}
}
return -1;
}
}
- 错误原因
- 我用寻找峰值的思路去找到拐点,寻找山峰的前提是峰值一定不在数组收尾,所以代码一定不会返回收尾元素;数组寻找峰值的前提是数组的收尾元素是峰值,返回收尾元素可以;单调递增会返回尾元素,单调递减会返回首元素,只要首元素或尾元素大于相邻元素,收尾元素也是峰值;
- 但这里不行,如果用那套代码,有可能返回5,也有可能返回3,都是正确答案,但3不符合这题的意思。
- 所以报错,正解,寻找最小元素;
正解,寻找最小值
- 找到拐点最小值,将数组分为两个有序部分
- 分别用二分法去寻找
class Solution {
public int search(int[] nums, int target) {
// 找到拐点最小值,将数组分为两个有序部分
// 分别用二分法去寻找
if(nums == null || nums.length == 0) return -1;
if(nums[nums.length - 1] > nums[0]) return getNum(nums, 0, nums.length - 1, target);
int minNumIndex = min(nums);
if(target == nums[0]) {
return 0;
}else if(target > nums[0]){
return getNum(nums, 0, minNumIndex - 1, target);
}else {
return getNum(nums, minNumIndex, nums.length -1, target);
}
}
private int min(int[] nums) {
// 特殊情况[1,2] [2,1]
// 通过nums[mid]和nums[0]比较,看现在nums[mid]处在哪里,再往哪移动
// 如果是拐点,立即返回
if(nums.length == 1) return 0;
if(nums[nums.length - 1] > nums[0]) return 0;
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] > nums[mid+1]) return mid+1;
if(nums[mid-1] > nums[mid]) return mid;
if(nums[mid] > nums[0]) {
left = mid + 1;
}else if(nums[mid] < nums[0]) {
right = mid - 1;
}
}
return -1;
}
private int getNum(int[] nums, int left, int right, int target) {
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] < target) {
left = mid + 1;
}else if(nums[mid] > target) {
right = mid - 1;
}else {
return mid;
}
}
return -1;
}
}