LeetCode Notes_#33#153#154
Contents
#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 {
int[] nums;
int target;
public int find_rotate_index(int left,int right){
//如果nums[0]<nums[n-1],说明没有被翻转,rotate_index为0
if(nums[left] < nums[right])
return 0;
//否则就寻找pivot的位置,注意这也是二分查找,反正就是查找一个数,刚好小于前一个数字,也就是第一次出现逆序的数
while(left <= right){
int pivot = (left + right)/2;
//如果直接找到满足条件的pivot,直接返回这个值
if(nums[pivot] > nums[pivot+1])
return pivot+1;
//否则
else{
//如果中间点小于最左侧点,说明在pivot位置之前就发生了逆序,调整右边界到pivot前一个位置
if(nums[pivot] < nums[left])
right = pivot - 1;
else
left = pivot + 1;
}
}
return 0;
}
//这里写了两个同名的search()函数,区别在于参数表不相同
public int search(int left,int right){
//不断地缩小查找的范围,直到左右边界相遇
while(left <= right){
//取中间点的方法就是直接求左右index的均值
int pivot = (left + right)/2;
if(nums[pivot] == target)
return pivot;
else{
//如果目标值小于中间点的值,那么将查找范围变成中间点的左边
if(target < nums[pivot])
right = pivot - 1;
//否则,将查找范围变成中间点的右边
else
left = pivot + 1;
}
}
//如果左右边界相遇后,还是没有返回值,说明找不到target,返回-1
return -1;
}
public int search(int[] nums, int target) {
this.nums = nums;
this.target = target;
int n = nums.length;
//首先考虑数组长度为0或者为1的情况
if(n == 0)
return -1;
if(n == 1)
return this.nums[0] == target?0:-1;
//left,right分别取为0,n-1,寻找最小的元素(翻转点)
int rotate_index = find_rotate_index(0,n-1);
//如果target就是最小的元素,直接返回这个位置的索引
if(nums[rotate_index] == target)
return rotate_index;
//如果没有被翻转(翻转位置为0)
if(rotate_index == 0)
return search(0,n-1);
//如果target比第一个数字小,说明一定在后半段,在后半段进行查找
if(target < nums[0])
return search(rotate_index,n-1);
return search(0,rotate_index);
}
}
题解Review
- 题目所谓的旋转,其实是找到一个位置,将这个位置之前的几个数平移到数组的最后。对于题目中的例子来说这个位置就是4所在的位置。这样做的效果就是,有且只有一个数组下标为pivot的地方,满足
nums[pivot]>nums[pivot+1]
。 - 所以第一步要找到这个pivot的位置。最朴素的想法就是从头到尾遍历,去找到满足条件的pivot,但是这样的复杂度就是O(n),不满足要求,O(log n)的复杂度应该是需要进行二分查找的。
- 首先考虑corner case:没有被翻转的情况,直接认为pivot = 0
- 二分查找算是一种模板型的代码吧,整体是while循环,循环体内部首先找到中间位置,然后判断目标位置和中间位置的关系,缩小一半的范围,继续循环。
- 找到pivot的位置后,比较target和nums[0],就可以知道target位于左半部分还是右半部分,分别在左半部分和右半部分进行二分搜索,即可找到target的索引,且整体的时间复杂度是O(log n)。
- 先考虑corner case:rotate_index=0或者rotate_index=1
- 二分查找
官方解答复现
class Solution {
int[] nums;
int target;
public int find_rotate_index(int left,int right){
//没有翻转的情况
if(nums[left] < nums[right])
return 0;
while(left <= right){
int pivot = (left + right)/2;
if(nums[pivot] > nums[pivot+1]){
//将最小的数字作为rotate_index
return pivot+1;
}else{
if(nums[pivot] >=nums[left]){
left = pivot + 1;
}else{
right = pivot - 1;
}
// if(nums[pivot] < nums[left])
// right = pivot - 1;
// else
// left = pivot + 1;
//注意这里两个条件是不可以互换位置的,因为在大于和小于之间还有一种等于的情况。
}
}
//循环结束还没有找到,也返回0
return 0;
}
public int search(int left,int right){
//到了调用这一步的时候,left和right之间的数组必然是顺序的,那么直接普通的二分查找就可以了
while(left <= right){
int pivot = (left + right)/2;
if(target == nums[pivot])
return pivot;
if(target > nums[pivot]){
left = pivot + 1;
}else
right = pivot - 1;
}
return -1;
}
public int search(int[] nums, int target) {
//为什么要定义两个类变量?因为定义完之后就可以直接被前面的两个函数访问到了
this.nums = nums;
this.target = target;
int len = nums.length;
if(len == 0)
return -1;
if(len == 1)
return target == nums[0]?0:-1;
int rotate_index = find_rotate_index(0, len-1);
if(nums[rotate_index] == target)
return rotate_index;
if(rotate_index == 0)
return search(0, len-1);
if(target >= nums[0])
return search(0, rotate_index);
return search(rotate_index, len-1);
}
}
更加简短的解法
class Solution {
public int search(int[] nums, int target) {
int len = nums.length;
int start = 0;
int end = len - 1;
if(nums == null || len == 0) return -1;
while(start + 1 < end){
int mid = start + (end - start) / 2;
if (nums[mid] == target) return mid;
else{
//因为题目强调了没有重复元素,所以不需要考虑等号
//其实这里也可以和end比较
if (nums[mid] > nums[start]){
if (target >= nums[start] && target < nums[mid]) end = mid;
else start = mid;
//Q:为什么这里不把mid排除掉,写成start = mid + 1
//1.start + 1有可能越界
//2.如果遇到start和right相邻的情况,nums[mid] == nums[start],边界应该向右,start = mid + 1,但是脑子乱了就容易想错,所以不如留着mid,防止脑抽把边界左移了
//3.综上,不把mid排除,可能会多出几轮循环,但是好处是防止出错,不需要去考虑边界情况,直接这么写就行。
}else if(nums[mid] < nums[start]){
if (target > nums[mid] && target <= nums[end]) start = mid;
else end = mid;
}
}
}
if(nums[start] == target) return start;
if(nums[end] == target) return end;
return -1;
}
}
参考了basketwangCoding
这位大佬的视频,感觉讲的很清晰,感觉逻辑捋清楚以后,实现代码并没有那么困难,这里边有几个Tips,我一并在LeetCode Notes_#81 搜索旋转排序数组II中整理。
学习方法
我感觉自己去想这些套路确实效率很低,自己想很久想不出来,无法通过,一是费时间,二是打击信心,时间又并不是很充分,是一种难以持续的方式。不如先用简单点的流程,看解答抄代码->review思路->自己尝试写一遍。这样速度相对快些,可以一天看两三道类似的题目,反复学习揣摩一个基本算法和它的变形(比如这题就是二分搜索算法的变形),反而会效率比较高。
#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
解答
这一题就是33题里边的find_rotate_index()这个函数...就不再分析了,直接贴解答。
class Solution {
public int findMin(int[] nums) {
int len = nums.length;
int start = 0;
int end = len - 1;
if (len == 0) return -1;//?空数组要怎么处理
if (len == 1) return nums[0];
if (nums[end] > nums[start]) return nums[start];
while (start + 1 < end){
int mid = start + (end - start) / 2;
if (nums [mid] < nums[mid - 1]) return nums[mid];
else {
if (nums[start] < nums[mid]) start = mid;
else end = mid;
}
}
//如果最后start和end相邻,那么直接返回小的那个
return nums[start] < nums[end]?nums[start]:nums[end];
}
}
#154 寻找旋转排序数组中的最小值 II
题目
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
注意数组中可能存在重复的元素。
示例 1:
输入: [1,3,5]
输出: 1
示例 2:
输入: [2,2,2,0,1]
输出: 0
解答
这个跟#153类似,同样只是多了一个可以重复的条件而已,遇到nums[mid] == nums[start]
,直接start++
即可,不过还是犯了错误,做过的题一定要避免再次做错。
class Solution {
public int findMin(int[] nums) {
int start = 0;
int end = nums.length - 1;
if (nums.length == 1) return nums[0];
if (nums.length == 0) return -1;
while(start + 1 < end){
//corner case:输入就是有序数组
//ERROR:这个判断不能写到循环外,每次循环都要判断一下,因为可能start++之后就变成有序数组了
if(nums[end] > nums[start]) return nums[start];
int mid = start + (end - start) / 2;
if (nums[mid] < nums[mid-1]) return nums[mid];
if (nums[mid] == nums[start]) start++;
else{
//ERROR:这里缩紧边界的方向写错了一次,每次写完先自己在纸上或者脑子里过一遍流程比较保险
if(nums[mid] > nums[start]) start = mid;
else end = mid;
}
}
//有可能left,right都是最小,所以考虑等号
return nums[start] <= nums[end]?nums[start]:nums[end];
}
}
旋转数组搜索类题目总结
流程
- 初始化start,end指针。
- 处理corner case
- 循环外:
- 输入空数组:返回一个错误值例如-1
- 输入数组长度为1:直接返回第一个元素
- 循环内:
- 输入数组有序:直接返回第一个元素
- 循环外:
- 进入循环
- 注意循环条件是
start + 1 < end
,并且在循环结束后单独判断start
和end
- 求中间位置索引
mid = start + (end - start)/2
- 判断
nums[mid]
是否满足条件,满足就返回,否则缩紧边界- 根据情况把
start
或者end
移动到mid
- 根据情况把
- 注意循环条件是
- 循环结束后,单独判断
left
,right
体会
- hard题一般就是medium的变形或者组合,所以要从相关的medium开始做,而不要一来就做hard
- 集中做同类型题目,刷起来也快,留下的印象也很深刻,直接做每道题下面推荐的相似题目就行