1、数组理论基础
数组的特点:
- 组下标都是从0开始的。
- 数组内存空间的地址是连续的
正是由于第二个特点,我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
注:数组的元素是不能删除的,只能覆盖,比如我要删除第二个元素,本质过程是把第三个开始的元素全部都往前覆盖一个位置。
ps:C++中二维数组在地址空间上是连续的,但是其他语言中不一定,例如Java就不是连续的。
2、704. 二分查找
视频解析:
这道题目的两个基本条件是:
1、前提是数组为有序数组;
2、数组中无重复元素
如果题目不满足上述两个基本条件的时候,就要考虑题目是否适合使用二分法了。
二分法书写的关键点在于:将一个数组一分为二时的区域边界。
写法一:target再左闭右开的区间里 [left,right]
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right=nums.size() -1;
while(left<=right) {
int middle = left + (right - left)/2;
if(nums[middle]>target){
right = middle -1;
}else if(nums[middle]<target){
left = middle +1;
}else{
return middle;
}
}
return -1;
}
};
注意点:
1、当认为target在闭区间里的时候,left=right就有意义,所以while的判断条件就是<=
2、找中点下标时,为了避免left+right会内存溢出,使用
middle = left + (right - left)/2;
3、当middle的大于target,也就是target在左边区间(假设样例是递增的),因为已知了middle!=target,所以此时就要让检测范围的right= middle-1;middle小于target同理。
写法二:target再左闭右开的区间里 [left,right)
class Solution {
public: //这是[left,right)左闭右开版本的写法
int search(vector<int>& nums, int target) {
int left = 0;
int right=nums.size();
while(left<right) {
int middle = left + (right - left)/2;
if(nums[middle]>target){
right = middle;
}else if(nums[middle]<target){
left = middle +1; //这里+1是因为下一个循环中left仍是是闭的,
//而已经知道此时的middle不是target,所以就要让left从middle+1开始
}else{
return middle;
}
}
return -1;
}
};
注意点:
1、当认为target在左闭右开区间里的时候,left=right就无意义,所以while的判断条件就是<;
2、当middle的大于target,也就是target在左边区间(假设样例是递增的),因为已知了middle!=target,但因为右边界是开的,所以直接让right= middle就可以了,此时左区间是不包含middle的;而middle小于target的时候,因为左边是闭区间,所以要想让middle不在闭区间,就要让 left = middle +1 。
总结:两种写法的复杂度都相同,注意边界处理即可
- 时间复杂度:O(log n)
- 空间复杂度:O(1)
27. 移除元素
数组移除元素的本质是:覆盖元素
写法一:暴力解法(两个for循环)
外层for循环遍历数组,内层for循环找到需要删除的数字位置,把所以后面的数字向前覆盖一位。
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size();
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--; // 因为下标i以后的数值都向前移动了一位,所以i也向前移动一位
size--; // 此时数组的大小-1
}
}
return size;
}
};
复杂度:
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
注:关于 i--的解释,比如删掉了第二个数字,后面的第三个变成了第二个,第四个变成了第三个...,此时要让i=2退一格,然后进入下一次循环i++,这时候i=2,判断的数字就是原来的第三个数。
写法二:双指针法(重点,非常常用)
核心思路:通过一个快指针和一个满指针,在一个for循环下完成两个for循环的工作。(减少时间复杂度)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (val != nums[fastIndex]) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
}
};
删除过程:起始两个指针同位置0;进入循环(快指针遍历元素),当快指针指向的元素不是要删除的数字,则将这个数字赋值给此时慢指针指向的位置,然后满指针++,进入下一次循环;如果此时快指针指向的数字需要删除,则直接跳过,不与慢指针产生关系,进入下一次循环。循环结束后,慢指针指向的下标应该是此时实际数组长度+1(例如原数组4个数字(下标0-3),删除两个数字后还剩两个,慢指针最后指向的下是2,虽然指向的是第三个下标吗,但是正好是剩下数组的长度)
ps:虽然叫慢指针,但如果从过程上来看,慢指针在没有删除元素之前都是比快指针走的快一步
写法三:相对双指针法(基于本题中允许改变元素相对位置)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int leftIndex = 0;
int rightIndex = nums.size() - 1;
while (leftIndex <= rightIndex) {
// 找左边等于val的元素
while (leftIndex <= rightIndex && nums[leftIndex] != val){
++leftIndex;
}
// 找右边不等于val的元素
while (leftIndex <= rightIndex && nums[rightIndex] == val) {
-- rightIndex;
}
// 将右边不等于val的元素覆盖左边等于val的元素
if (leftIndex < rightIndex) {
nums[leftIndex++] = nums[rightIndex--];
}
}
return leftIndex; // leftIndex一定指向了最终数组末尾的下一个元素
}
};