算法之数组| 青训营
数组理论基础:
数组是一种存储相同类型元素的集合,它们在内存中是连续存储的。 数组中的元素通过索引访问,索引从0开始。 数组元素的内存地址是连续的。
在C++中,数组是固定大小的,无法删除元素,只能覆盖。 而在Java中,没有指针的概念,程序员无法直接访问元素的地址,内存寻址操作完全由虚拟机处理。
二分查找:
二分查找是一种用于在有序数组中查找特定元素的搜索算法。 其核心思想是不断将搜索范围划分为两半,直到找到目标元素或搜索范围为空。 基本思路是通过定义左右边界和中间索引来检查中间元素与目标元素的关系。
循环不变量原则:
循环不变量原则是证明算法正确性的关键概念,确保循环在每次迭代中都保持一定的不变性。 对于二分查找来说,循环不变量是目标元素(如果存在)位于当前搜索范围内。
有两种常见的二分查找实现方式:
-
左闭右闭范围:[left, right]
- 使用
while (left <= right)
- 当 时,更新
nums[middle] > target``right = middle - 1
- 使用
-
左闭右开范围:[left, right)
- 使用
while (left < right)
- 当 时,更新
nums[middle] > target``right = middle
- 使用
移除元素:
暴力解法(O(n^2)):
移除数组中的元素,使用两个嵌套循环。外层循环遍历数组元素,内层循环更新数组以移除目标元素。
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
}
}
双指针法(O(n)):
双指针技术是一种高效地在数组中移除元素的方法。它使用两个指针:一个快指针用于查找要移除的元素,一个慢指针用于更新新数组的下标位置。慢指针指向新数组中要放置下一个非目标元素的位置。
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (val != nums[fastIndex]) {
nums[slowIndex++] = nums[fastIndex];
}
}
有序数组的平方:
给定一个有序整数数组,任务是将每个元素平方,并返回一个按非递减顺序排列的新数组。
暴力排序解法(O(n + nlogn)):
一种解决方法是首先将数组中的每个元素平方,然后使用任何标准排序算法(如快速排序或归并排序)对结果数组进行排序。
双指针法(O(n)):
更高效的方法是使用双指针。一个指针从左侧(最小值端)开始,另一个指针从右侧(最大值端)开始。比较两个指针所指元素的平方,并将较大的平方值放入结果数组的末尾,然后相应地更新指针。
int k = A.size() - 1;
vector<int> result(A.size(), 0);
for (int i = 0, j = A.size() - 1; i <= j;) { // 注意这里要i <= j,因为最后要处理两个元素
if (A[i] * A[i] < A[j] * A[j]) {
result[k--] = A[j] * A[j];
j--;
}
else {
result[k--] = A[i] * A[i];
i++;
}
}
长度最小的子数组:
给定一个包含 n 个正整数的数组和一个正整数 s,找出该数组中满足其和 ≥ s 的长度最小的连续子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
暴力解法(O(n^2)):
暴力解法使用两个嵌套循环,找到所有可能的子数组并检查它们的和是否满足条件。同时记录满足条件的最小长度子数组。
滑动窗口解法(O(n)):
滑动窗口技术是一种高效找到最小长度子数组的方法。它使用两个指针(起始和结束指针)创建一个窗口,该窗口在数组上滑动。当窗口内元素的和小于目标值时,增加窗口大小,使结束指针向右移动;当窗口内元素的和大于等于目标值时,缩小窗口大小,使起始指针向右移动。在过程中记录满足条件的最小窗口大小。
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,每次更新 i(起始位置),并不断比较子序列是否符合条件
while (sum >= s) {
subLength = (j - i + 1); // 取子序列的长度
result = result < subLength ? result : subLength;
sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
}
}
螺旋矩阵II:
给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
螺旋顺序是指按照顺时针螺旋的方式填充矩阵:
- 从左到右填充顶部行。
- 从上到下填充右侧列。
- 从右到左填充底部行。
- 从下到上填充左侧列。
重复上述过程,直到所有元素都填充完毕。
螺旋矩阵生成(O(n^2)):
该算法通过遵循上述四个步骤生成螺旋矩阵。它使用 "loop" 变量来控制需要填充矩阵的圈数。"offset" 变量控制每个圈中每一边的遍历长度。"count" 变量用于填充矩阵中的整数,从1到 n^2。
算法通过循环来填充所有必要的圈,并在 n 为奇数时单独处理矩阵中心的元素。
vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
int count = 1; // 用来给矩阵中每一个空格赋值
int offset = 1; // 需要控制每一条边遍历的长度,每次循环右边界收缩一位
int i,j;
while (loop --) {
i = startx;
j = starty;
// 下面开始的四个for就是模拟转了一圈
// 模拟填充上行从左到右(左闭右开)
for (j = starty; j < n - offset; j++) {
res[startx][j] = count++;
}
// 模拟填充右列从上到下(左闭右开)
for (i = startx; i < n - offset; i++) {
res[i][j] = count++;
}
// 模拟填充下行从右到左(左闭右开)
for (; j > starty; j--) {
res[i][j] = count++;
}
// 模拟填充左列从下到上(左闭右开)
for (; i > startx; i--) {
res[i][j] = count++;
}
// 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
startx++;
starty++;
// offset 控制每一圈里每一条边遍历的长度
offset += 1;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if (n % 2) {
res[mid][mid] = count;
}
return res;
}