前言
数组基础;二分法;双指针;暴力;数组覆盖
一、704. 二分查找
下面是三段代码,第一个是最初的,后两个为学习后的;难点有:
- 未考虑到 left + right 可能会溢出的问题;
- 在<与<=之间游移不定;
>>
与 + 的运算优先级不明确
//初代码
class Solution {
public int search(int[] nums, int target) {
int len = nums.length;
int first = 0,last = len-1;
while(first<=last){
// int mid = len<<2;
int mid = (last-first)/2 + first;
int num = nums[mid];
if(target==num){
return mid;
}
if(target<nums[mid]){
last = mid -1;
}
if(target>nums[mid]){
first = mid+1;
}
}
return -1;
}
}
//左闭右闭
class Solution{
public int search(int[] nums, int target){
int l = 0;
int r = nums.length-1;
while(l<=r){
int mid = ((r-l)>>1) +l; //+的优先级高于<<
if(nums[mid] == target){
return mid;
}
if(nums[mid] > target){
r = mid-1;
}
if(nums[mid] < target){
l = mid+1;
}
}
return -1;
}
}
//左闭右开
class Solution{
public int search(int[] nums,int target){
int l = 0;
int r = nums.length;
while(l<r){
int mid = ((r-l) >> 1) + l;
if(nums[mid] == target){
return mid;
}
if(nums[mid] < target){
l = mid +1;
}
if(nums[mid] > target){
r = mid;
}
}
return -1;
}
}
- 在数组的左闭右闭与左闭右开中,(左开右闭不常用)
判断条件是否存在等号,取决于能否让【left,right】在left=right的时候成立;
如:left=1,right=1,【1,1】成立,那么左闭右闭也成立;
引:
- 其实二分还有很多应用场景,有着许多变体,比如说查找第一个大于target的元素或者第一个满足条件的元素,都是一样的,根据是否满足题目的条件来缩小答案所在的区间,这个就是二分的本质。另外需要注意,二分的使用前提:有序数组
- 二分的最大优势是在于其时间复杂度是O(logn),因此看到有序数组都要第一时间反问自己是否可以使用二分。
- 关于二分mid溢出问题解答:
- mid = (l + r) / 2时,如果l + r 大于 INT_MAX(C++内,就是int整型的上限),那么就会产生溢出问题(int类型无法表示该数)
- 所以写成 mid = l + (r - l) / 2或者 mid = l + ((r - l) >> 1) 可以避免溢出问题
- 对于二进制的正数来说,右移x位相当于除以2的x几次方,所以右移一位等于➗2,用位运算的好处是比直接相除的操作快
- fast < nums.size() 和 fast <= nums.size()-1 没什么区别,为什么第二个会在空数组时报数组越界的错误?
vector的size()函数返回值是无符号整数,空数组时返回了0,再减个一会溢出
二、27. 移除元素
代码共两段如下:
没有暴力的代码,第一个仍是双指针,采用替换的策略;第二是快慢指针,和第一个的区别是覆盖,而非替换;
class Solution {
public int removeElement(int[] nums, int val) {
int len = nums.length;
int l = 0;
int r = len-1;
// 左闭右闭
while(l <= r){
if(nums[l] == val){
int m = nums[l];
nums[l] = nums[r];
nums[r] = m;
r--;
}
if(nums[l] != val){
l++;
}
}
return l;
}
}
//快慢指针
class Solution{
public int removeElement(int[] nums,int val){
int fast = 0;
int slow =0;
for(;fast<= nums.length-1;fast++){
if(nums[fast] != val){
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
}
引:
- 快指针可以理解成在旧数组中找非目标元素,然后赋值给慢指针指向的新数组,虽然都指向一个数组
- 关于二分法和移除元素的共性思考
这两题之间有点类似的,他们都是在不断缩小 left 和 right 之间的距离,每次需要判断的都是 left 和 right 之间的数是否满足特定条件。对于「移除元素」这个写法本质上还可以理解为,我们拿 right 的元素也就是右边的元素,去填补 left 元素也就是左边的元素的坑,坑就是 left 从左到右遍历过程中遇到的需要删除的数,因为题目最后说超过数组长度的右边的数可以不用理,所以其实我们的视角是以 left 为主,这样想可能更直观一点。用填补的思想的话可能会修改元素相对位置,这个也是题目所允许的。
三、附加题目
35.搜索插入位置
难点在于理清题目,找到规律:
// 分别处理如下四种情况
// 目标值在数组所有元素之前 [0, -1]
// 目标值等于数组中某一个元素 return middle;
// 目标值插入数组中的位置 [left, right],return right + 1
// 目标值在数组所有元素之后的情况 [left, right], 因为是右闭区间,所以 return right + 1
难点在于理解左右边界,随想录的java解将等于与小于或大于放在了一起,分开或许会更易理解,附上Leecode,爱做梦的鱼的代码:
// 两次二分查找,分开查找第一个和最后一个
// 时间复杂度 O(log n), 空间复杂度 O(1)
// [1,2,3,3,3,3,4,5,9]
public int[] searchRange2(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int first = -1;
int last = -1;
// 找第一个等于target的位置
while (left <= right) {
int middle = (left + right) / 2;
if (nums[middle] == target) {
first = middle;
right = middle - 1; //重点
} else if (nums[middle] > target) {
right = middle - 1;
} else {
left = middle + 1;
}
}
// 最后一个等于target的位置
left = 0;
right = nums.length - 1;
while (left <= right) {
int middle = (left + right) / 2;
if (nums[middle] == target) {
last = middle;
left = middle + 1; //重点
} else if (nums[middle] > target) {
right = middle - 1;
} else {
left = middle + 1;
}
}
return new int[]{first, last};
}
总结
感悟很多,数组为算法开了个好头。