1. 二分法查找
给定有序数组时,写一个函数搜索target,并返回其下标。
-
完整遍历数组 : 值相等就返回下标。
时间复杂度 O( n n n) -
二分法查找 : 前提是数组已经有序
时间复杂度 O( l o g n logn logn)
注意两点
一是 while循环的判断条件边界 问题:left是小于right,还是小于等于right时执行循环,这取决于整个数组的区间是左闭右闭还是左闭右开。
二是更新左右边界时是 取middle还是middle ± 1,这还是取决于整个数组的区间的开闭情况。
(以下提到middle默认其所指的值,target默认其本身的值)
如果区间是左闭右闭(搜索数组所有元素)
step1. 定义最初的边界,left = 0、right = size - 1 (size是数组长度)
step2. 写while循环,其中循环执行的条件是:left 小于等于 right (因为left = right时,区间仍然合法,需要继续在区间里搜索,如果不搜索,就会漏掉这一个点)
step3. 更新左右区间:判断target和middle指向的值大小,来对左右区间进行更新。端点处取middle 还是 middle ± 1 仍然取决于整个搜索区间的开闭。
step4. target = middle时,说明通过二分法找到了target,返回middle下标。
如果区间是左闭右开(不搜索数组末尾最后一个元素)
step1. 定义最初的边界, left = 0、right = size - 1
step2. 写while循环,其中循环执行条件是:left < right (left = right时不是合法区间)
step3. 区间端点更新,判断target和middle的大小,区间端点设置成 middle
step4. 直到target = middle, 返回middle下标。
// 二分查找
int search(int* nums, int numsSize, int target) {
// 初始化区间,此题中是搜索整个数组,所以是左闭右闭
int left = 0; // 二分搜索区间左端点
int right = numsSize - 1; // 二分搜索区间右端点
int middle = 0;
// 循环体执行的条件:只要区间是左闭右闭的合法区间,就继续搜索
while (left <= right) {
middle = (left + right) / 2;
// 如果target在左区间,更新左区间的右端点
if (nums[middle] > target) {
right = middle - 1;
}
// 如果target在右区间,更新右区间的左端点
else if (nums[middle] < target) {
left = middle + 1;
}
// 如果target就是middle所指,搜索结束
else return middle;
}
// 如果二分查找结束后一直找不到,返回-1
return -1;
}
// 暴力解法
int search(int* nums, int numsSize, int target) {
for (int i = 0; i < numsSize ; i++) {
if (nums[i] == target) {
return i; // 如果查找到,直接返回下标
}
}
return -1; // 如果没有查找到,返回-1
}
2. 移除数组中的元素
Leetcode例题:移除元素
原地移除所有数值等于val的元素。
-
暴力解法:直接遍历每个元素,如果是要删除的元素,就把后面的元素向前移动。
时间复杂度:O( n 2 n^2 n2)
step1. 循环遍历数组中的每个元素
step2. 条件判断:如果当前元素的值等于要删除的元素,则把后面的元素以此往前填充 -
双指针解法:用一个快指针去判断元素是否需要删除,用一个慢指针记录要保留的元素,从而得到新数组。
时间复杂度:O( n n n)
step1. 初始化慢指针来记录新数组。
step2. 快指针循环遍历数组:如果元素不等于删除的元素,赋值给慢指针,快指针和满指针都移动;如果等于,跳过赋值,快指针移动,慢指针不动。
错误分析:
- 最后数组长度的返回直接就是slow
- 暴力解法中要注意i.如果当前元素被删除,指针i不移动;ii.数组长度可以直接–
// 暴力解法
int removeElement(int* nums, int numsSize, int val) {
int count = 0;
// 遍历数组中的每个元素, i++是在特定条件才执行:当前元素不需被删除时
for (int i = 0; i < numsSize; ) {
// 如果当前元素等于要删除的元素,则把后续所有元素往前移动
if (nums[i] == val) {
for (int j = i; j < numsSize-1; j++) {
nums[j] = nums[j+1];
}
numsSize--; // 记录当前数组长度
continue; // 如果当前元素被删除,指针i不移动
}
i++;
}
return numsSize;
}
// 双指针
int removeElement(int* nums, int numsSize, int val) {
int slow = 0; // 慢指针负责保存新数组
for (int fast = 0; fast < numsSize; fast++) {
// 快指针元素不等于要删除元素时,赋值给慢指针
if (nums[fast] != val) {
nums[slow++] = nums[fast];
}
}
return slow;
}
3. 返回有序数组的平方
LeetCode例题:有序数组的平方
把一个有负数的单调不减数组中元素平方后返回新的数组,新数组也是单调不减。
-
暴力解法:先把数组每个元素平方后,再进行快速排序
时间复杂度:O( n l o g n nlogn nlogn)
step1. 遍历数组,每个元素都平方
step2. 对新数组进行快速排序 -
双指针:在数组两端用两个指针循环查找平方最大值,然后插入数组末尾。
时间复杂度:O( n n n)
step1. 初始化指针:一个指针指向首部,另一个指向尾部
step2. 比较两个指针的平方大小,取大者放到数组末尾
错误分析:
- Leetcode提交时最后注意返回数组长度 *returnSize = numsSize;
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* sortedSquares(int* nums, int numsSize, int* returnSize) {
// 初始化两个指针
int left = 0;
int right = numsSize - 1;
// 定义一个新数组
int *squaresArray = (int *)malloc(sizeof(int) * numsSize);
// 定义新数组的指针
int index = numsSize - 1;
// 用双指针遍历数组,由于不知道两个指针分别要遍历多少次,所以用while
while (left <= right) {
if (nums[left] * nums[left] > nums[right] * nums[right]) {
squaresArray[index--] = nums[left] * nums[left];
left++;
}
else {
squaresArray[index--] = nums[right] * nums[right];
right--;
}
}
*returnSize = numsSize; // 设置返回数组的长度
return squaresArray;
}
4. 长度最小的子数组
LeetCode例题:长度最小的子数组
找出数组中最短的连续元素,它们的和大于target,返回这个子数组的长度。
-
暴力解法:两层循环遍历所有的起点和终点可能。
时间复杂度:O( n 2 n^2 n2)
step1. 第一层循环遍历起点,第二层循环遍历终点
step2. 对于每一种起点和终点的组合,记录其中元素和。
step3. 进行条件判断,如果元素和大于等于target,记录长度。
step4. 将当前组合的长度与现有记录长度比较,取较小者更新。 -
滑动窗口思想:左指针负责收缩窗口,右指针负责拉抻窗口。滑动窗口在找到同一个起点的最小长度后可以提前终止该起点相应的终点的遍历。
时间复杂度:O( n n n) 右指针遍历一次数组,左指针最多遍历n次。
step1. 初始化左右指针处于原点。
step2. 拉抻窗口,直到元素和大于等于target。
step3. 收缩窗口,直到元素和小于target。
step4. 重复拉伸和收缩,直到右指针遍历整个数组。
错误分析:
- 滑动窗口代码实现时,拉伸窗口在每次for循环中完成,收缩窗口在一个while循环中完成
// 暴力解法
int minSubArrayLen(int target, int* nums, int numsSize) {
int length = numsSize + 1; // 记录当前符合要求长度
// 两层循环遍历各种起点和终点的组合
for (int i = 0; i < numsSize; i++) {
for (int j = i; j < numsSize; j++) {
// 如果该组合内的元素和大于target,记录长度
int sum = 0;
// 求和该组合的元素
for (int k = i; k <= j; k++) {
sum += nums[k];
}
// 条件判断赋值length
if (sum >= target) {
length = ((j - i + 1) < length) ? (j-i+1) : length;
}
}
}
if (length == numsSize + 1) return 0;
else return length;
}
// 滑动窗口
int minSubArrayLen(int target, int* nums, int numsSize) {
// 先初始化左指针和右指针到原点处
int left = 0;
int sum = 0; // 元素和
int length = numsSize + 1; // 记录长度
// 右指针拉抻窗口,元素和大于等于target时记录长度且左指针开始收缩窗口。
// 循环结束条件:当右指针遍历完数组时结束
for (int right = 0; right < numsSize; right++) {
sum += nums[right];
// 元素和小于target时拉伸窗口,右指针移动
// 元素和大于等于target时,记录长度、收缩窗口、再重复直到小于target
while (sum >= target) {
length = (right - left + 1) < length ? (right - left + 1) : length;
sum -= nums[left++];
}
}
return (length == numsSize + 1) ? 0 : length;
}
5. 螺旋矩阵II
LeetCode例题:螺旋矩阵II
给定一个n阶矩阵,按顺时针方向将1~
n
2
n^2
n2填入矩阵中。
这是一道面试常见的模拟题。
-
关键策略
不变量原则:每条边都是左闭右开进行处理。
变量管理:使用变量来存储每一层遍历的起点。 -
算法流程
step1. 确定n阶矩阵需要遍历的层数:n/2
step2. 如果n为奇数,最后还需要对中心点进行赋值。
step3. 开始每一层的遍历:四条边分别用一层循环来遍历赋值。
step4. 结束一层遍历后,移动起点变量。
int** generateMatrix(int n, int* returnSize, int** returnColumnSizes) {
int **res = (int **)malloc(n * sizeof(int *)); // 分配行
for (int i = 0; i < n; i++) {
res[i] = (int *)malloc(n * sizeof(int)); // 分配列
}
int startx = 0, starty = 0;
int loop = n / 2;
int mid = n / 2;
int count = 1;
int offset = 1;
int i, j;
// 设置 returnSize 和 returnColumnSizes
*returnSize = n;
*returnColumnSizes = (int *)malloc(n * sizeof(int));
for (int k = 0; k < n; k++) {
(*returnColumnSizes)[k] = n;
}
while (loop--) {
i = startx;
j = starty;
// 填充上行从左到右
for (j; j < n - offset; j++) {
res[i][j] = count++;
}
// 填充右列从上到下
for (i; 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
startx++;
starty++;
// offset 控制每一圈每一条边的遍历长度
offset += 1;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if (n % 2) {
res[mid][mid] = count;
}
return res;
}