搜索旋转排序数组I ——leetcode 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
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array
解题过程思考:
一、 首先想到的二分,因为二分的时间复杂度是log(N),但是好像不太行,就不多想了。又想到了堆,所以先把下面的算法先实现了。 建堆过程也是log(N)的时间复杂度,所以我觉得可以借着建堆的思想来做,所以就写了下面的代码
class Solution {
public int search(int[] nums, int target) {
int i;
if(nums.length == 0)
return -1;
for(i=0; i<=nums.length/2; i++){ //对于i的右边出错过,没有划定好
//原本我写的是当数据是i<=nums.length/2-1,但是用例196个错了一个在【1】1的时候。
if(nums[i] == target)
return i;
//原本在这里没有考虑2*i+1和2*i+2有没有越界的问题,所以应该在做提前把所有的要求先写下来,特别是第一时间想到的边界问题
if(2*i+1<=nums.length-1 && nums[2*i+1] == target)
return 2*i+1;
if(2*i+2<=nums.length-1 && nums[2*i+2] == target)
return 2*i+2;
}
return -1;
}
}
执行结果:通过
显示详情
执行用时 :2 ms, 在所有 Java 提交中击败了51.73%的用户
内存消耗 :35.7 MB, 在所有Java提交中击败了87.31%的用户
但是实际上,如果这个数据规模比较大,而且要找的又是最后一个数,那么这个算法的时间复杂度并不会小于logN,还是趋向于O(N),所以这个代码通过只是因为数据量设的不够,碰巧给过了。
二、看到这个执行时间有点差,不由得激起了我对问题的重新思考……
这题其实和二维数组的查找有点像,条件都有提示:有序,虽然这题是一部分是有序的,另一部分是无序的,本质都是有序这个关键。所以应该顺着这个思路走,可能会得到一个最优解。
我想了一个,但不知道时间复杂度:
想法是这样的,先用双指针找到突变点,然后target比较左数组的最小和最大,以及右数组的最小和最大,这样就能确定把target丢进哪一边。其次再进行二分查找。但还有的问题就是,这题不能用普通的二分+函数递归了。
这个细节抠了好久,还是思路没有理好,而且各种情况没有提前做分析,接下来理清思路再仔细代码:成功AC
先分有没有旋转过的,没旋转过的L,R不变;有旋转过的,第一次选择一个区域丢入,之后在丢入的有序数组中,二分得到target的索引。
class Solution {
public int search(int[] nums, int target) {
if(nums.length == 0 || (nums.length == 1 && target != nums[0]))
return -1;
if(nums.length == 1 && target == nums[0])
return 0;
int L = 0, R = nums.length-1;
int i=0, j=nums.length-1;
int mid=0;
int enterFlag=0;
while(L<=R) {
if(enterFlag == 0) {//得到初步的L,R
enterFlag=1;
if (nums[0] > nums[nums.length-1]){ //发生旋转
//说明发生旋转,先双指针找边界
while(i<nums.length-1 && j>0) {
if(nums[i] < nums[i+1])
i++;
if(nums[j] > nums[j-1])
j--;
if(i+1==j) {
L=i;//左部分右边界
R=j;//右部分左边界
break;
}
}
if(target == nums[L])
return L;
else if(target == nums[R])
return R;
else if(target > nums[L] || target < nums[R])
return -1;
//选择其中一个数据堆
else if(target > nums[0]) {
L=0;
R=i;
}else if(target < nums[nums.length-1]) {
L=j;
R=nums.length-1;
}
else if(target == nums[0])
return 0;
else if(target == nums[nums.length-1])
return nums.length-1;
}
}
mid = (L+R)/2;
if(nums[mid] == target)
return mid;
else if(target > nums[mid])
L = mid+1;
else if(target < nums[mid])
R = mid-1;
}
return -1;
}
}
执行结果:通过
显示详情
执行用时 :3 ms, 在所有 Java 提交中击败了51.73%的用户
内存消耗 :35.8 MB, 在所有 Java 提交中击败了87.10%的用户
三、我上面的代码虽然过了,但是执行时间一样很差,而且运行时间更久了,3ms!!! 我暂时想不到好的方法了,我选择学习一下大佬们的解法!!!
题目要求O(logN)的时间复杂度,基本可以断定本题是需要使用二分查找,怎么分是关键
由于题目说数字了无重复,举个例子
1 2 3 4 5 6 7 可以大致分为两类,
第一类 2 3 4 5 6 7 1这种,也就是nums[start] <= nums[mid]。此例子中就是2 <= 5
这种情况下,前半部分有序。因此如果 nums[start] <=target<nums[mid]。则在前半部分找,
否则去后半部分找。
第二类 6 7 1 2 3 4 5这种,也就是nums[start] > nums[mid]。此例子中就是6 > 2
这种情况下,后半部分有序。因此如果 nums[mid] <target<=nums[end]。则在后半部分找,
否则去前半部分找。
public int search(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return -1;
}
int start = 0;
int end = nums.length - 1;
int mid;
while (start <= end) {
mid = start + (end - start) / 2;
if (nums[mid] == target) {
return mid;
}
//前半部分有序,注意此处用小于等于
if (nums[start] <= nums[mid]) {
//target在前半部分
if (target >= nums[start] && target < nums[mid]) {
end = mid - 1;
} else {
start = mid + 1;
}
} else {
if (target <= nums[end] && target > nums[mid]) {
start = mid + 1;
} else {
end = mid - 1;
}
}
}
return -1;
}
执行结果:通过
显示详情
执行用时 :1 ms, 在所有 Java 提交中击败了99.84%的用户
内存消耗 :36.3 MB, 在所有 Java 提交中击败了85.46%的用户
看到大佬的代码和思路,突然豁然开朗,原来是自己太愚蠢了。上面第二种做法其实已经很接近了。第一步,其实都是为了分堆,target是在前面有序的还是后面的里面。
对比:
-
我的做法是先找到交界点,然后在有序的数据里查找,找交界点增加了很多的运算步骤,执行时间就会增加。
-
他们的思路是,既然是有部分有序的,无非就是两种情况,仍然采用二分的思路
总结:我对二分的理解限制了我思维的拓展。
搜索旋转排序数组II——leetcode 81
题目描述:(难度中等)
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。
编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。
示例 1:
输入: nums = [2,5,6,0,0,1,2], target = 0
输出: true
示例 2:
输入: nums = [2,5,6,0,0,1,2], target = 3
输出: false
进阶:
这是 搜索旋转排序数组 的延伸题目,本题中的 nums 可能包含重复元素。
这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?
解题过程思考:
这题和上面的是有点区别的,因为涉及到重复的元素,用第二种做法就会有点问题。所以我先试了第一种堆的做法,结果惊人,执行效率很高,但内存消耗很大。但我不太懂,我用的都是题目给的内存数组,为啥内存消耗这么大?不过看其他的代码,内存消耗也挺大。
这个时间复杂度和空间复杂度得找时间好好学一波!!!
第一种:
class Solution {
public boolean search(int[] nums, int target) {
int i;
if(nums.length == 0)
return false;
for(i=0; i<=nums.length/2; i++){
if(nums[i] == target)
return true;
if(2*i+1<=nums.length-1 && nums[2*i+1] == target)
return true;
if(2*i+2<=nums.length-1 && nums[2*i+2] == target)
return true;
}
return false;
}
}
执行结果:通过
显示详情
执行用时 :1 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗 :39.7 MB, 在所有 Java 提交中击败了10.72%的用户
第二种:
上面的二分虽然有点问题,但核心是一样的,只要遇到首尾相等的时候,某一边退一个或者进一个,为什么?一定要注意一个特别重要的信息,“两部分分别有序”,如果说中间和最左边的相等了,比如【1,2,1】,这说明右部分都是1或者已经没有数了。这时start前进一个,因为可能存在这种情况【1,1,2,1】,所以直到左边和中间不相等了,就可以进行后面的操作了。
大佬的清晰代码,很优秀啊!
public boolean search(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return false;
}
int start = 0;
int end = nums.length - 1;
int mid;
while (start <= end) {
mid = start + (end - start) / 2;
if (nums[mid] == target) {
return true;
}
if (nums[start] == nums[mid]) {
start++;
continue;
}
//前半部分有序
if (nums[start] < nums[mid]) {
//target在前半部分
if (nums[mid] > target && nums[start] <= target) {
end = mid - 1;
} else { //否则,去后半部分找
start = mid + 1;
}
} else {
//后半部分有序
//target在后半部分
if (nums[mid] < target && nums[end] >= target) {
start = mid + 1;
} else { //否则,去后半部分找
end = mid - 1;
}
}
}
//一直没找到,返回false
return false;
}