一、数组理论基础
1. 什么是数组?
数组是存放在连续内存空间上的相同类型数据的集合。
(1) 连续存储 -> 插入、删除元素时需要移动其他元素 -> 增加时间复杂度
(2) 数组元素不能删除,只能覆盖。
2. 二维数组在内存空间中如何存储?
不同编程语言的内存管理不同;在 C++ 中,二维数组是按行连续存储的;在 Java 中,寻址操作交给虚拟机完成,不一定连续。
二、数组题目、思路和解答
二分查找
1.题目:给定一个 n
个元素有序的(升序)整型数组 nums
和一个目标值 target
,写一个函数搜索 nums
中的 target
,如果目标值存在返回下标,否则返回 -1
。
2.思路:在有序数组中,每次查找都可以根据中间元素和目标值的大小关系将查找范围缩小为原来的一半。以升序数组为例,中间元素若比目标值小,则目标值可能在中间元素的左半边,反之。
3.思考:以往都是根据左闭右闭区间的方法做的,没有想过还有左闭右开的方法,看了题解后自己尝试了一遍。
法一:左闭右闭,即 target 应为数组下标为 [ low, high ] 中的元素
// nums数组名,numsSize数组长度,target要找的目标值
int search(int* nums, int numsSize, int target) {
int low = 0; // low,high表示区间范围
int high = numsSize-1;
int mid = (low+high)/2; // mid 即中点
while (low <= high) {
// 注意,不是 mid 和 target比较,而是数组中下标为 mid 的元素和 target 比较
if (nums[mid] == target){
return mid; // 找到目标值
} else if (nums[mid] > target){
high = mid - 1;
mid = (low+high)/2;
} else{
low = mid+1;
mid = (low+high)/2;
}
}
return -1; // 当 low > high 时,就是目标值不在数组中
}
法二:左闭右开,即 target 应为数组下标为 [ low, high ) 中的元素
int search(int* nums, int numsSize, int target) {
int high = numsSize;
int low = 0;
int mid = (low+high)/2;
while (low < high){ // low = high时表示目标值已不在数组中
if (nums[mid] == target){
return mid;
} else if (nums[mid] > target){
high = mid; // 此时不需要 mid+1,因为右边是开区间,target不会在high位置
mid = (low+high)/2;
} else{
low = mid+1; // 仍需要 mid+1,因为左边是闭区间,target有可能在low位置
mid = (low+high)/2;
}
}
return -1;
}
移除元素
1.题目:给你一个数组 nums
和一个值 val
,你需要原地移除所有数值等于 val
的元素。元素的顺序可能发生改变。然后返回 nums
中与 val
不同的元素的数量。
假设 nums
中不等于 val
的元素数量为 k
,要通过此题,您需要执行以下操作:
- 更改
nums
数组,使nums
的前k
个元素包含不等于val
的元素。nums
的其余元素和nums
的大小并不重要。 - 返回
k
。
2.思路:用下标 i 从前往后遍历数组查找需要移除的元素 val,若找到则用下标 j 在区间 ( i , 数组长度) 之中从后往前遍历数组,查找是否有不同于 val 的元素可以交换,若找到则交换位置 i 和 j 的元素;若没有找到可交换的则说明位置 i 之后的所有元素都是 val,说明到此为止数组内所有不同于 val 的元素已经被记录过,可直接终止遍历数组。
3.思考:看了题解以后,学习了另外两种方法。
(1) 暴力法:当发现需要移除的元素时,让之后的元素从后往前依次覆盖,数组长度随之减一。两个for循环实现,一个for循环遍历数组查找待移除的元素,另一个for循环将元素前移覆盖 val ,时间复杂度O(n^2),空间复杂度O(1)。
(2) 双指针法:双指针法在一个 for 循环中完成了 查找和覆盖 两个工作,时间复杂度O(n),空间复杂度O(1)。
定义新数组:只包含需要保留下来的元素的数组,数组下标范围为 [0, slowIndex)。
定义快慢指针:
-
快指针 fastIndex:寻找待移除元素 val 以外的其他元素,即寻找需要保留下来的元素。
-
慢指针 slowIndex:慢指针指向的是新数组之后的一个位置。若 fast 后续找到了新的要保留下的元素,则填充在慢指针所指位置中,相当于插入新数组之后,类似“尾插法”。
4.错误记录:
(1) 第一次提交执行出错:有两个测试用例通过,但当输入数组为 [3, 3],val为 3 时出错。
AddressSanitizer: heap-buffer-overflow on address。
AddressSanitizer 是一种用于检测内存错误的工具,错误之一是"heap-buffer-overiow"。
它检测堆上的缓冲中区溢出错误,即程序超出了分配给堆缓冲区的内存范围。
(待完善。。)
(2) 第二次提交解答出错:解决了第一次提交的问题,但是之前的解答出错。
(3)第三次提交成功:成功通过
第一次提交的代码:
int removeElement(int* nums, int numsSize, int val) {
int i; // 从前往后遍历数组,查找待移除元素
// 让 j 从后往前遍历数组,若前面有元素需要移除,而后面的元素也不是需要移除的,则前后交换
int j = numsSize-1;
int count = 0; // 与移除元素不同的元素个数
for (i = 0; i < numsSize; ++i) {
if (j <= i){
break;
}
// 碰到了需要移除的元素
if (nums[i] == val){
while (nums[j] == val){
j--;
}
// while循环从后往前找到和 val不同的元素,可以和前面的 val 交换
nums[i] = nums[j];
j--;
count++;
} else{
// 不需要移除,则 count+1
count++;
}
}
if (j == i){
if (nums[i] != val){
count++;
}
}
return count;
}
第二次提交的代码:修改了 j 的用法
int removeElement(int* nums, int numsSize, int val) {
int i; // 从前往后遍历数组,查找待移除元素
int j; // 从后往前遍历数组,若前面有元素需要移除,而后面的元素也不是需要移除的,则前后交换
int count = 0; // 与移除元素不同的元素个数
for (i = 0; i < numsSize && i<j; ++i) {
// 碰到了需要移除的元素
if (nums[i] == val){
// 找到 i 之后与 val不同的元素位置 j
for (j = numsSize-1; j > i; --j) {
if (nums[j] != val){
count++;
// 元素前后交换
nums[i] = nums[j];
nums[j] = val;
break;
}
}
// 遍历一遍后如果没找到,此时 j == i,说明之后的元素全部是 val,直接退出 for 循环
} else{
// 不需要移除,则 count+1
count++;
}
}
return count;
}
第三次提交的代码:多加了flag标志判断,运行通过
int removeElement(int* nums, int numsSize, int val) {
int i; // 从前往后遍历数组,查找待移除元素
int j; // 从后往前遍历数组,若前面有元素需要移除,而后面的元素也不是需要移除的,则前后交换
int count = 0; // 与移除元素不同的元素个数
int flag = 0;
// 第三次修改的地方:flag 表示是否记录过所有 val 之外的元素,0表示没有,1表示查询完毕
// 在 for 循环中,循环条件修改为 i < numsSize && flag==0
for (i = 0; i < numsSize && flag==0; ++i) {
// 碰到了需要移除的元素
if (nums[i] == val){
// 找到 i 之后与 val不同的元素位置 j
for (j = numsSize-1; j > i; --j) {
if (nums[j] != val){
count++;
// 元素前后交换
nums[i] = nums[j];
nums[j] = val;
break;
}
}
// 遍历一遍后如果没找到,此时 j == i,说明之后的元素全部是 val,直接退出 for 循环
//第三次修改的地方:
if (j == i){
flag = 1;
}
} else{
// 不需要移除,则 count+1
count++;
}
}
return count;
}
暴力解法:
int removeElement(int* nums, int numsSize, int val){
for (int i = 0; i < numsSize; ++i) {
if (nums[i] == val){
// 找到了需要移除的元素 val,将之后的元素都往前移动一位,覆盖掉原来的 val
for (int j = i+1; j < numsSize; ++j) {
nums[j-1] = nums[j];
}
i--; // 原来的第 i+1 位元素还没有判断过是否为 val,但其向前移动了一位,故 i 要减一
numsSize--; // 从后往前移动元素后,数组长度减 1
}
}
return numsSize; // 最后的数组长度就是剩余的不同于 val 的元素个数
}
双指针法:
int removeElement(int* nums, int numsSize, int val){
int slowIndex = 0; // 慢指针指向 新数组的后一个位置
int fastIndex; // 快指针 查找需要保留下来的元素
for (fastIndex = 0; fastIndex < numsSize; ++fastIndex) {
if (nums[fastIndex] != val){
nums[slowIndex] = nums[fastIndex];
slowIndex++;
}
}
return slowIndex;
}
自己的理解: