第一天——数组
二分查找
首先需要注意的是,二分查找需要随机寻址和关键字有序,不然不可使用。
关于二分法,最重要的要点在于区间定义和二分中点的更新,现总结如下:
左右为闭区间的二分查找
循环条件:左浮标小于等于右浮标
左右区间更新:中间位置 + / - 1
代码示例:
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,所以[middle + 1, right]
} else { // nums[middle] == target
return middle; // 数组中找到目标值,直接返回下标
}
}
// 未找到目标值
return -1;
}
};
左闭右开的区间的二分查找
循环条件:左浮标小于右浮标
左右区间更新:左浮标更新为中间位置+1、右浮标更新为中间位置
代码示例:
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle; // target 在左区间,在[left, middle)中
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,在[middle + 1, right)中
} else { // nums[middle] == target
return middle; // 数组中找到目标值,直接返回下标
}
}
// 未找到目标值
return -1;
}
};
数组元素删除
由于数组的地址是连续的,因此要删除某一个元素,不可以直接将其删除,而是需要调整后续的元素位置来填补删除元素后留下的位置空缺(这里需要与链表区分开来)
如果采用每次扫过整个数组,每次删除一个元素,我们得到的算法解决效率较低
-
时间复杂度:
O ( n 2 ) O(n^2) O(n2) -
空间复杂度:
O ( 1 ) O(1) O(1)
比较高效的方法是采用双指针,两个指针同时往下扫描整个数组,快指针提取记录,慢指针写入记录,如果遇到要移除的记录就将快指针后跳以覆盖记录。
代码如下:
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;
}
};
- 时间复杂度:
O ( n ) O(n) O(n)
长度最小的子数组
这算是一道经典的滑动窗口题了,由于数组内均为正整数,我们只需要从数组开始位置设置一个起点和终点,当起点终点内的数字之和小于目标,向右扩展终点,否则从左缩小起点。这样一次遍历数组即可得到结果。
代码如下:
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,每次更新 i(起始位置),并不断比较子序列是否符合条件
while (sum >= s) {
subLength = (j - i + 1); // 取子序列的长度
result = result < subLength ? result : subLength;
sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
螺旋数组
这种题就挺搞人的,基本就是看你能不能搞明白边界条件。
为了统一循环条件,我们统一每条边的填充都是左闭右开。如题目的3阶矩阵,我们就先填充1,2、再填充3,4、再填充5,6、再填充7,8,最后补充9即可。因为统一了循环条件,每次填充的值的数量是固定的,最后要不要填充一个中间数则由起始的条件决定。这样我们就完成了边界条件的判断。
代码如下:
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0));
int psx = 0, psy = 0; // 定义起始位置
int loop = n / 2; // 循环的圈数
int mid = n / 2; // 螺旋数组的中间位置
int count = 1; // 赋值初始化
int offset = 1; // 数组循环时的边长控制
int i, j;
while (loop --)
{
i = psx;
j = psy;
// 填充外圈
for (j = psy; j < psy + n - offset; j++)
{
res[psx][j] = count++;
}
for (i = psx; i < psx + n - offset; i++)
{
res[i][j] = count++;
}
for (; j > psy; j--)
{
res[i][j] = count++;
}
for (; i > psx; i--)
{
res[i][j] = count++;
}
// 下一圈开始时调整起始位置和偏移量
offset += 2;
psx++;
psy++;
}
// 如果n为奇数在中间填充最后一个数
if (n % 2)
{
res[mid][mid] = count;
}
return res;
}
};