本篇是前几篇数组题解的总结篇和理论篇,题目链接:二分查找 、 移除元素 27 、有序数组的平方 977 、长度最小的子数组 209 。包含数组的简单知识、二分查找总结,双指针总结和选择排序。
一. 数组理论知识
首先,数组是一种最基本的数据结构,是个非常重要的主题。在这里,借用**《算法图解》**中的比喻辅助理解。既将计算机内存比作抽屉,每一个抽屉放一个待办事项,而数组就是一系列相连的抽屉。
但是,鉴于数组是相连的,就会出现一个问题是数组不方便添加新元素。一个长度为四的数组就好比四个相连着的抽屉,当想添加一个元素进去,会发现不管是中间还是末尾都没有空闲抽屉了,因为后面的抽屉有其他东西占着,而一开始数组只分配到了四个抽屉。
遇到这种情况,就需要整个数组转移,移到一个可以连着放下这些元素的地方,很显然这样比较麻烦。另一种解决办法就是提前 "预留抽屉" ,即便现在只需要四个,也要让计算机划出十个位置,以防需要时添加。事实上,这也是使用数组时经常会用到的方法。
但同时这种方法也存在两个缺点:
- 额外划定的位置可能用不上,而这将浪费内存。
- 当数组元素超出十个后,又需要转移。
下面补充几条数组的基本知识:
- 数组时存放在连续内存空间上的相同类型数据的集合。
- 数组可以通过下标索引的方式获取对应数据,但要注意数组下标都是从
0开始。 - 数组内存空间的地址都是连续的。

正是因为数组在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作。也就是说数组的元素是不能删的,只能覆盖。
二. 二分查找
二分查找的关键在于边界的打磨,确定好边界值和条件,二分查找的代码就水到渠成。
模板一 左闭右闭
C++
class Solution {
public:
int search(vector<int>&nums,int target){
int left = 0;
int right = nums.size() - 1;
while(left <= right){ // 注意这里由于左闭右闭,所以左可以等于右
int mid = left + (right - left) / 2; // 防止mid溢出
if(nums[mid] == target)
return mid;
else if(nums[mid] > target)
right = mid - 1;
else
left = mid + 1;
}
return -1;
}
};
模板二 左闭右开
c++
class Solution {
public:
int search(vector<int>&nums,int target){
int left = 0;
int right = nums.size() ;
while(left < right){ // 这里不能等于,关键在于左闭右开
int mid = (right + left) / 2;
if(nums[mid] == target)
return mid;
else if(nums[mid] > target)
right = mid ;
else
left = mid + 1;
}
return -1;
}
};
可以发现两个模板思路及复杂度一致,只是边界值处理不同,刷题时应多注意这一点,对区间的定义理解清楚自然能形成代码。
三. 双指针
所谓双指针法是指通过两个指针------快指针和慢指针,在一个for循环下完成两个for循环完成的任务,全程只遍历一次数组。通常而言,双指针拥有更简单的时间复杂度,也是暴力解法的一种优化方法。
类型一 快慢指针
对应 移除元素 27 ,两个指针同方向,一快一慢,原地修改数组。
C++
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;
}
};
其中:
- 快指针
fastIndex:寻找新数组的元素 ,新数组就是不含有目标元素的数组 - 慢指针
slowIndex:指向更新新数组下标的位置
算法逻辑:
- 快指针从左往右遍历数组
- 当快指针指向元素不等于目标值时,赋给慢指针指向位置
- 慢指针自增后移至下一位置等待赋值
- 返回慢指针
适用场景:
- 原地删除指定值,不能新开数组
- 保留符合条件的元素,覆盖到数组前面
类型二 左右对撞指针
对应有序数组的平方 977,两个指针一头一尾相向而行,用于有序数组。
c++
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
int k = nums.size() - 1; //使 k 在新数组最后一位
vector<int> result(nums.size(),0); // 定义新数组存放排序后的平方值
for(int i = 0,j = nums.size()-1;i <= j;){ // 双指针 i 在起始,j 在末尾
if(nums[i]*nums[i] < nums[j]*nums[j]){
result[k--] = nums[j]*nums[j]; //利用后置自减实现 k 的前移
j--; // j 平方填入后就前移
}
else{
result[k--] = nums[i]*nums[i];
i++; // i 平方填入后就后移
}
}
return result;
}
};
其中:
- 左指针
i:从左往右移动 - 右指针
j:从右往左移动
算法逻辑:
- 先定义一个新数组
result,并使变量 k 位于新数组末尾位置 - 比较 左指针和右指针指向的数组元素的大小
- 如果左指针指向元素的平方大于右指针的,那么将左指针处元素的平方赋给
k,左指针自增右移,k左移 - 反之,将右指针处元素平方赋给
k,右指针左移,k左移 - 最后返回新数组
result
适用场景:
数组整体有序 ,最大值 / 最小值一定出现在数组左右两端,中间数值不会出现极值。
类型三 滑动窗口
对应长度最小的子数组 209,指针同向左右边界,求子数组区间。
c++
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0;
int i = 0;
int subLength = 0;
for (int j = 0; j < nums.size(); j++) {
sum += nums[j];
while (sum >= s) {
subLength = (j - i + 1);
result = result < subLength ? result : subLength;
sum -= nums[i++];
}
}
return result == INT32_MAX ? 0 : result;
}
};
其中:
left:窗口左边界right:窗口右边界(不断向右扩张)
算法逻辑:
-
right 不断右移,累加窗口内总和 sum
-
当
sum ≥ target,持续收缩左边界:- 记录当前窗口长度,更新最小值
- sum 减去 nums left,left++
-
right 走完数组结束
适用场景:
- 数组全部为正整数
- 寻找满足区间和≥target 的最短连续子数组
四. 选择排序
很多题目都以有序为解题前提,因此有必要学会排序。选择排序是一种简单易理解的排序算法。
选择排序的思路比较简单,就是不断遍历待排列表不断找出最大/最小值并不断交换。
c++
#include <vector>
using namespace std;
void selectionSort(vector<int>& nums) {
int len = nums.size();
// i 代表未排序区间起始位置
for (int i = 0; i < len - 1; ++i) {
// 记录最小值下标,初始为区间首位
int minIndex = i;
// 遍历未排序区间,寻找最小值
for (int j = i + 1; j < len; ++j) {
if (nums[j] < nums[minIndex]) {
minIndex = j;
}
}
// 交换最小值到未排序区间头部
swap(nums[i], nums[minIndex]);
}
}
选择排序是一种简单易懂的排序算法,但其时间复杂度为 O(n^2),也就是速度不是很快。