Day 1
1.二分查找
题目
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
两种写法:两种写法的区别主要是在于查找区间的不同。
第一种查找区间为左闭右闭,即[left , right],这就决定了后面的while判断里的条件为while(left <= right),因为当left = right时,在左闭右闭的查找区间里是有意义的。同时当区间缩小时,即middle的值大于target目标值时,middle已经确实不是需要查找的值,这说明要进一步查找左区间,由于左闭右闭,这时候right应该更新为middle-1。如果不这么做,可能导致两个问题:(1)查找次数变多,效率降低 (2)更严重的,当出现特殊情况时,即数组元素为[1,2],target为2,第一次查找middle指向的值为1 < target ,此时如果right更新为middle = 0时,会陷入死循环。
第二种查找区间为左闭右开,此时不同的是right的初始位置应该是数组最后一个元素的下一个位置,即right = num.length,while判断里面的条件就变成了while(left < right),当区间缩小时,即middle的值大于target目标值时,middle已经确实不是需要查找的值,这说明要进一步查找左区间,由于左闭右开,这时候right应该更新为middle。
//左闭右闭
class Solution {
public int search(int[] nums, int target) {
// 避免当 target 小于nums[0] nums[nums.length - 1]时多次循环运算
if (target < nums[0] || target > nums[nums.length - 1]) {
return -1;
}
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid - 1;
}
return -1;
}
}
//左闭右开
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid;
}
return -1;
}
}
几个注意点
- 1.mid = left + (right - left) / 2,之所以不使用mid = (left + right)/2,是因为当left和right都接近数据范围时,相加会出现溢出的情况。
- 2.注意两种查找区间代码写法的区别(边界初始条件,判断条件,更新边界条件)。
相关题目
1.搜索插入位置
题目:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
二分法:本题关键在于定义一个什么样的返回值,可以准确返回插入位置。
分析:
情况1.目标值在数组中,返回mid即可;
情况2.目标值不在数组中,并且小于数组最小值,返回0;
情况3.目标值不在数组中,并且大于数组最大值,返回nums.length;
情况4.目标值不在数组中,并且值属于该数组区间内。
==对情况234进行分析归纳:==很容易想象,经过每次while循环,都会使得[left,right]
的左闭右闭区间中的元素减少。
那就有一个问题,减少到最后会是什么情况?
那就是进入最后一次while循环前,[left,right]的左闭右闭区间中只有一个或者两个元素,即
left和right的位置有且仅有2
种情况
-
left = right
或
-
left = right -1
为什么? 举几个特例
- 数组有一个元素,那么left = right,还需要最后一次while循环
- 数组有两个元素,那么left = right -1, 分
两种情况
,需要一或两次while循环- 比如数组 [1,3] ,target=0,此时
left = right -1
, 然后还需最后一次while - 比如数组 [1,3] ,target=2,一次while循环后,left = right =1,并且变成了一个元素的情况,还要一次while,一共是2个while
- 比如数组 [1,3] ,target=0,此时
- 数组有三个,四个元素等等,都以此类推,最后都会变成以上的情况
综上,数组最后会变为一个元素
,或者两个元素中的情况1
以上就解释清楚了,在进入最后一次while循环前,数组会变为一个或两个元素。
以下是一个元素和两个元素(情况1
)的代码注释
-
数组只有一个元素,假设nums=[1],left = right
初始化left = rigth=0 while(left < =right){ //进入最后一次循环 int mid = (left + right) / 2; //mid =0, 此时`left = mid = right=0` if (target == nums[mid]) { return mid; //若找到,直接返回mid,下面两个else是没有找到target的情况 } else if (target < nums[mid]) { right = mid - 1; //此时数组中仅剩的最后一个数比target大,执行right=mid-1=-1, 而left=0正是要插入的位置 } else { left = mid + 1; //此时数组中仅剩的最后一个数比target小,执行left =mid+1=1, left=1正是要插入的位置 } }//循环结束
-
数组有两个元素(
情况1
),假设nums=[1,3],target =0, left = right -1
初始化left = 0, rigth=1
while(left < =right){ //进入最后一次循环
int mid = (left + right) / 2; //mid =0, 此时`left = mid =0 , right =1 `
if (target == nums[mid]) {
return mid; //若找到,直接返回mid,下面两个else是没有找到target的情况
} else if (target < nums[mid]) {
right = mid - 1; //此时target < nums[0],执行right=mid-1=-1, 而left=0正是要插入的位置
} else {
left = mid + 1; //此时target > nums[0],执行left =mid+1=1, left=1正是要插入的位置
}
}//循环结束
综上,除了目标值在数组中返回mid,其他情况返回left即可。
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0;
int right = nums.length-1;
while(left <= right){
int mid = left+(right-left)/2;
if(target<nums[mid]){
right = mid-1;
}else if(target > nums[mid]){
left = mid+1;
}else if(target == nums[mid]){
return mid;
}
}return left;
}
2.移除元素
题目
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1: 给定 nums = [3,2,2,3], val = 3, 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 你不需要考虑数组中超出新长度后面的元素。
示例 2: 给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
因为数组在内存空间中是连续的,因此不能删除数组的元素,只能覆盖。
解法一:两层for循环暴力解法,即先执行外循环遍历数组元素,查找是否有等于val的元素,如果找到了,那就执行内循环,将这个元素之后的每个元素都赋给前一个元素,最终的效果就是除了这个等于val的元素,其余元素都向前移动一位,数组容量减一。如下图所示。
//两层for循环暴力求解
class Solution {
public int removeElement(int[] nums, int val) {
int size = nums.length;
for(int i = 0;i < size;i++){
if(nums[i] == val){
for(int j =i+1;j< size; j++ ){
nums[j-1] = nums[j];
}i--;size--;
}
}return size;
}
}
几个注意点
- 1.为什么要定义一个size变量:因为要最终返回有效数组的容量,在本题中,实际上数组的容量是不变的,但是我们通过往前覆盖的方式,每覆盖一次,相当于去除一个无效值,即实际数组容量-1 = 有效数据容量;另外,在for循环的终止条件中,如果不是使用动态的size,而是使用nums.length的话,会出错,因为每覆盖一次,需要处理的数就少一个,就需要更新size值。这也解释了size–的意义。
- 2.为什么要执行 i-- ,当执行一次覆盖的时候,比如说在下标为2的位置找到了等于val的元素,执行完覆盖以后,原本下标为3的元素就到了下标为2的位置,此时因为第一个for循环的i++,如果不执行i–,i 就会变成3,这样就直接判断原本下标为4的元素了,而原本下标为3的元素就会漏掉,所以要执行i–去抵消i++,这样才会对原本下标为3的元素进行判断。
解法2:双指针(快慢指针)法 【重在理解】
定义快慢指针
-
快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
-
慢指针:指向更新 新数组下标的位置
初始时快慢指针都在数组起点,由快指针的移动带动慢指针的移动,即:如果快指针指向的元素是我们需要的元素,慢指针也向前移动一步,同时该元素后面的元素都向前覆盖一位,如果快指针指向的元素是不需要的,慢指针不动,当快指针遍历完整个数组时,慢指针的索引就是有效数据组的长度,并且从0到慢指针索引位置的元素都是需要的元素。可以理解为快指针寻找需要的元素赋值给慢指针指向的位置。
如下图所示。
//快慢指针法
class Solution {
public int removeElement(int[] nums, int val) {
// 快慢指针
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.length; fastIndex++) {
if (nums[fastIndex] != val) {
nums[slowIndex] = nums[fastIndex];
slowIndex++;
}
}
return slowIndex;
}
}
几个注意点
- 为什么返回的slowIndex就是有效数组的长度:slow是从0开始的,如果快指针最后一个元素是我们需要的,那就是覆盖以后slowIndex+1,正好是数组容量(slow在+1之前是有效数组的最后一个元素的索引,就是等于数组容量-1);如果不是我们需要的,那就是在上一次覆盖完以后的+1正好变成了数组容量。
解法3:
替换移除法:
- 主要思路是遍历数组 nums,遍历指针为 i,总长度为 ans
- 在遍历过程中如果出现数字与需要移除的值不相同时,则 i 自增 1 ,继续下一次遍历
- 如果相同的时候,则将 nums[i]与nums[ans-1] 交换,即当前数字和数组最后一个数字进行交换,交换后就少了一个元素,故而 ans 自减 1
//替换移除
class Solution {
public int removeElement(int[] nums, int val) {
int ans = nums.length;
for (int i = 0; i < ans;) {
if (nums[i] == val) {
nums[i] = nums[ans - 1];
ans--;
} else {
i++;
}
}
return ans;
}
}
int ans = nums.length;
for (int i = 0; i < ans;) {
if (nums[i] == val) {
nums[i] = nums[ans - 1];
ans--;
} else {
i++;
}
}
return ans;
}
}