今日主要内容:数组基础(数组理论基础,704. 二分查找,27. 移除元素)
前置准备
1:不知道二分法是什么了,康复训练
2:回顾数组的基本知识(array和vector,虽然才看不久,但感觉掌握的非常有限)
based on C++ 学习依据代码随想录:)
内容学习
1:二分法
看了两个简单的博客,回想起印象中的二分法:大致是首先给一组数按照大小进行排序,然后每次都通过跟区间的最中间的元素进行对比,将需要查找的区间缩小为之前的一半。看上去还挺容易的,不知道实际做起来如何。
2:回顾数组基础
简单复习了下最基础的数组内容,一些语法语句基本上。
题目1
给定一个 n
个元素有序的(升序)整型数组 nums
和一个目标值 target
,写一个函数搜索 nums
中的 target
,如果目标值存在返回下标,否则返回 -1
。
示例 1:
输入:nums
= [-1,0,3,5,9,12],target
= 9 输出: 4 解释: 9 出现在 nums 中并且下标为 4
愚蠢的尝试(该部分都是杂乱想法,不一定正确)
总体思路是这样的,首先获得输入数组的长度n,使用array.size()函数。然后设定一个left(l);right(r)和middle(mid)分别代表左右和中间位置。起始将中间元素设置为l和r的平均数,l设置为0,r设置为n-1(数组长度-1)。
其后使用while,条件是r大于l(?),如果目标target大于mid下标的数值,就将l修改为mid+1,再重新计算mid的数值;如果目标target小于mid下标的数值,就将r修改为mid-1;如果二者相等,也就说明找到了该数值,即返回当前mid的值。如果一直无法找到,就会出现r不大于l的情况,此时也就说明没有找到。但这里就该注意到判断的边界,如果r=l会怎么样(在这里已经很晕了,但是直觉上来说感觉应该考虑上一次循环,这里的想法我也不知道对不对。上一次循环中,应该只有两个元素,且应该是使用l判断是否与target相等(因为除法向下取整),这样的话,再取一次平均一定会选到l,但一直l和target不相等,所以只要相等的时候就可以判断找不到了?)先写代码尝试一下结果:
class Solution {
public:
int search(vector<int>& nums, int target) {
int n;
n = nums.size();
int l = 0;
int r = n-1;
int result = 0;
while (r > l){
int mid = (r + l)/2;
if (target > nums[mid]){
l = mid+1;
}
else if (target < nums[mid]){
r = mid-1;
}
else if (target == nums[mid]){
return mid;
}
}
cout << result << endl;
return -1;
}
};
运行时通过了,喜出望外;
提交时错误了,悲从中来;
检查一下结果发现错完了,出现了预期结果0而出现最后结果为-1的情况,为什么呢,是哪里i想的有问题。实验实例是[5]只有一个元素,这样的话第一时间l=r=mid,如果r=l的时候直接快进到-1就出错了。所以{条件是r大于l(?)}那里肯定出错了 ,应该是r>=l才合理,为什么呢?学一下。
视频学习
两个易错点:
1:while(left<还是<=right)
2:如果nums[middle]>target;那么right<=middle还是(middle-1)
[l,r]和[l,r):
在其中要坚定一个区间,不能随意改动
1:左闭右闭
讨论第一个易错点,由区间有效性的角度考虑,l=r时区间有效,因此应该是l<=r;
讨论第二个易错点,由重复代入的角度考虑,当左闭右闭时,当取mid = (l + r)/ 2时,如果有判断target与nums【mid】不等,则此时已知target与nums【mid】不等,在调整l和r的时候,也不计入mid这个位置了,而是直接选择mid后/前的一个。
于是容易写出:
class Solution {
public:
int search(vector<int>& nums, int target) {
int n;
n = nums.size();
int l = 0;
int r = n-1;
int result = 0;
while (r >= l){
int mid = (r + l)/2;
if (target > nums[mid]){
l = mid+1;
}
else if (target < nums[mid]){
r = mid-1;
}
else if (target == nums[mid]){
return mid;
}
}
cout << result << endl;
return -1;
}
};
2:左闭右开
此时,区别是什么呢,首先,l和r相等的可能性不再成立了。其次,当左闭右开时,如果target<mid时,此时右边的开区间可以到达mid,如果延伸到mid-1的话就相当于减少了一个可选的位置。
同样的,也可以很容易的写出:
class Solution {
public:
int search(vector<int>& nums, int target) {
int l = 0;
int r = nums.size();
int mid = 0;
while(l < r){
mid = (l + r) / 2;
if (target > nums[mid]){
l = mid + 1;
}
else if (target < nums[mid]) {
r = mid;
}
else {
return mid;
}
}
return -1;
}
};
理清思路还是很好理解的是吧,那我们快进到下一个问题:
题目2
给你一个数组 nums
和一个值 val
,你需要 原地 移除所有数值等于 val
的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1)
额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝 int len = removeElement(nums, val); // 在函数里修改输入数组对于调用者是可见的。 // 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。 for (int i = 0; i < len; i++) { print(nums[i]); }
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2
, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
愚蠢的尝试(该部分都是杂乱想法,不一定正确)
好像没什么太多的想法,计划是这样。首先获得数组的长度n,之后遍历数组,如果发现再第i个位置有值等于所给的val,则将i+1位置的数赋值给第i个位置,后面也是一样。同时做n--运算,最后输出。
错误的代码,失败了,为什么呢?
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int n = nums.size();
for (int i = 0 ; i < n ; i++){
if (nums[i] == val){
for (int a = i; a < n; a++){
nums[a] = nums[a + 1];
n--;
}
}
}
return n;
}
};
视频学习
暴力解决
暴力拆解的方法基本上思路是对的,但是代码写的很混乱,不知道自己要干嘛:很多细节也没有把握好:首先,在第二个循环中, a < n这里就明显不对,i和a都小于n,但是随着每个数往前挪一个位置,下标也要重新回到刚刚覆盖的位置(因为移到前面来的那个数也有可能等于val),所以就造成了两个问题,其一,a应该< n-1;其二,完成内部循环后,i应-1来回到初始位置。
更新的代码如下:显然该方法比较复杂,复杂度为O(n方)嵌套循环。
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int n = nums.size();
for (int i = 0 ; i < n ; i++){
if (nums[i] == val){
for (int a = i; a < n - 1; a++){
nums[a] = nums[a + 1];
// for (int j = i + 1; j < n; j++ ){
// nums[ j - 1 ] = nums[ j ];
}
i--;
n--;
}
}
return n;
}
};
双指针法(感觉关键)
主题思路就是使用快指针和慢指针,其中快指针用来检查每个元素是否是更新后的数组所需要的元素,而慢指针则指向处理过后该元素的实际位置。这样说明之后就很清楚了。给人的主观感受是慢指针先走,但不是一直走;快指针后走,但是保持移动:如果选择的元素不是我们要找的元素,就把该元素给slow的位置(一开始slow和fast在一起,此时没有实际意义),之后呢,如果出现了需要删除的元素,慢指针就停留在原地,表示新数组没有增加该元素,快指针则继续向前寻找。
代码说明如下,非常的简洁,suki
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slow = 0;
for (int fast = 0; fast < nums.size(); fast ++){
if (nums[fast] != val){
nums[slow] = nums[fast];
slow ++;
}
}
return slow;
}
};
确实是很简单的方法,大巧不工是这样说的吧。 这也是库函数erase方法,其时间复杂度为O(n),相当于节省了一层for循环就完成了数组的删除,真的很巧妙吧。
总结
感觉学习这一部分一方面要能想到这种做法,另一方面就是要掌握基本的语法,这样做起来可能会快很多。今天在输出那里其实有很多问题:我想着在最后cout或者return一个数组,这肯定是不对的,那为什么能输出数组,前面也说到了:
问题一
输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
我的理解就是,(我鸡毛理解没有)调用一下别人的理解,说不定我哪天就理解了:
但是数组你已经改了啊
妈的
因为数组是引用
数组里面的数据已经被你改成新的了
(试图理解)数组名就是首元素的地址,所以实际上传入函数的不是数组名,而是首元素的地址。使用函数时都是将数组名作为参数。如果在函数内部对传入的数组进行了修改,该数组本身的值也会改变,有点像引用,这是因为前面提到过传入的是地址,我们是直接对地址上的元素进行修改。
(理解了)我一直迷惑的地方就是为什么return个int会打印出一个数组。意思是leetcode底层封装了代码导致的,可能编译器上做出来ruturn就是一个数字。鉴定为钻死胡同里去了,还是搞清楚基本的方法和语言为主Orz
问题二
关于二分mid溢出问题解答:
mid = (l + r) / 2时,如果l + r 大于 INT_MAX(C++内,就是int整型的上限),那么就会产生溢出问题(int类型无法表示该数)所以写成 mid = l + (r - l) / 2或者 mid = l + ((r - l) >> 1) 可以避免溢出问题,很合理很好理解不是吗?