代码随想录算法训练营第1天|LeetCode707.二分查找、LeetCode27.移除元素
1、数组理论基础
定义:数组是存放在连续内存空间上的相同类型数据的集合。
获取:下标索引的方式。从0开始。
删除/增添:需要移动其他元素的地址。不能删除,只能覆盖。
vector VS array:vector是容器,底层实现是array
Java中没有指针,且不对程序员暴露元素地址。
2、LeetCode 707.二分查找
题目链接:https://leetcode.cn/problems/binary-search/
文章讲解:https://programmercarl.com/0704.%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE.html
视频讲解:https://www.bilibili.com/video/BV1fA4y1o715
第一想法
(忘记是二分查找了…)存数组–设置pos=-1 – 从头遍历,知道找到,停止遍历并输出
看完之后的想法
damn!醍醐灌顶!!我困扰多年的边界取值问题!!!
笔记
前提条件
- 有序数组
- 无重复元素
常见问题
- while(left right)是<还是<=
- 解决:进入while的需要是合法区间
- if(…) right=middle还是middle+1
- 解决:已经判断middle>target,那么接下来的区间一定不包含middle这个值,因此right=middle-1。其他类似
解法
- 方法1:左闭右闭[left, right]
while(left<=right) :区间是合法区间[1, 1] - 方法2 左闭右开[left, right)
while(left<=right) :区间是非法的[1, 1)
因此应该为while(left<right)
代码:
int search(vector<int>& nums, int target) {
// 法1 左闭右闭
int left = 0;
int right = nums.size()-1;
int middle;
// 区间合法
while(left <= right){
middle = (left+right)/2;
if(target < nums[middle]){
right = middle - 1; //右闭,不包括middle
}
else if(target > nums[middle]){
left = middle + 1; //左闭,不包括middle
}
else
return middle;
}
// 法2 左闭右开
int left = 0;
int right = nums.size();
int middle;
while(left<right){
middle = (left+right)/2;
if(target < nums[middle]){
right = middle; //右开
}
else if(target > nums[middle]){
left = middle + 1;
}
else
return middle;
}
return -1;
}
除此之外,需要注意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,用位运算的好处是比直接相除的操作快。
3、LeetCode 27.移除元素
题目链接:27. 移除元素 - 力扣(LeetCode)
文章讲解:代码随想录 (programmercarl.com)
视频讲解:https://www.bilibili.com/video/BV12A4y1Z7LP/
第一想法
双指针:使用两个指针i, j,i从前往后遍历,指向val,j从后往前遍历,指向非val。如果i遇到val,那么nums[i]与nums[j]互换,直到i>j。每一个val就计数+1,最后数组总长度-计数。
注意j从右往左遍历时若遇到val,count需要++。
(这只是我的第一思路,但实际上有问题)
法1 暴力解法
犯错:最外层的while条件中的size应该是新数组的大小,否则会陷入死循环。
int removeElement(vector<int>& nums, int val) {
// 法1暴力解法
int i=0;
int size = nums.size();
while(i<size){ //注意这里的新的size
for(int k=0;k<size;k++){
cout<<nums[k]<<" ";
}
cout<<endl;
// 等于val,需要把后面的每一个元素都往前移动
if(nums[i] == val){
size--;
int j = i+1;
while(j<nums.size()){
nums[j-1] = nums[j];
j++;
}
//移动之后,i指向一个新的值,因此不需要往后移
continue;
}
i++;
}
return size;
}
法2 双指针
- 两个指针左右遍历
我的第一思路中的count有问题,如果用count计数,那么会有无法考虑到的案例,导致无法通过。
正确的解决方法是返回left值,最后一个left是一个分界,左边都是非val,右边都是val。
并且,需要考虑数组大小为1的情况。
int removeElement(vector<int>& nums, int val) {
// 法3 双指针(双向遍历)
int left=0;
int right=nums.size()-1;
while(left <= right){ // =考虑数组大小为1
// 找到==val的
while(left<nums.size()){
if(nums[left] == val){
break;
}
left++;
}
//找到!=val的
while(right>=0){
if(nums[right] != val){
break;
}
right--;
}
if(left < right){
// 交换
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right--;
}
}
return left;
}
- 两个指针同侧遍历(快慢法)
read指针读取新数组的元素,write指针新数组要更新的位置。更新/不更新。最后return write指针。
int removeElement(vector<int>& nums, int val) {
// 法2 双指针(快慢法)
int read=0; //用以找需要放到新数组中去的索引
int write=0; //新数组中此时记录到了哪个索引
for(read=0;read<nums.size();read++){
if(nums[read]!=val){
nums[write]=nums[read];
write++;
}
}
return write;
}
法3 vector的erase函数
复杂度O(n2),原理是删除后逐个往前移动。
首先需要得到val的索引,可以使用find函数。找到索引后使用erase(),注意的是每次find只能找到一个位置,因此需要不断地找直到找不到新的val。
刷算法题时使用库函数的时机:使用的库函数只是实现算法的一部分,并且已经掌握其底层原理以及复杂度时,可使用。
总结:
- 区间不变量
- 二分使用前提-有序数组
- 避免mid溢出,mid=left+( (right - left )>>1)。