代码随想录一刷(Day01 ~ Day04)
代码随想录一刷
数组
1. 数组基础理论
对于C/C++,无论是一维还是二维,地址都是连续的
2. 二分查找
单点查找
闭区间写法
区间查找
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int nums_size = nums.size();
int l = 0, r = nums_size - 1;
while (l <= r) {
int mid = l + ((r - l) >> 1);
if (nums[mid] == target) return mid;
else if (nums[mid] > target) r = mid - 1;
else l = mid + 1;
}
return r + 1; // 或return l; 此时闭区间对应 l = r + 1
}
};
区间左闭右开写法
区间查找
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int nums_size = nums.size();
int l = 0, r = nums_size;
while (l < r) {
int mid = l + ((r - l) >> 1);
if (nums[mid] == target) return mid;
else if (nums[mid] > target) r = mid;
else l = mid + 1;
}
return r; // 或 return l; 此时 l = r
}
};
查找区间
大于某个数的最小位置和最大位置
class Solution {
int get_left_border(vector<int> &nums, int target, int n) {
int l = 0, r = n;
int left_border = -2;
while (l <= r) {
int mid = l + ((r - l) >> 1);
if (nums[mid] >= target) { /* 寻找比目标值小的第一个位置 */
r = mid - 1; // 寻找比目标值小的第一个下标位置
left_border = r; // 记录比目标值小的第一个下标位置
}
else { /* 查找区间变为[mid + 1, r] */
l = mid + 1;
}
}
return left_border;
}
int get_right_border(vector<int> &nums, int target, int n) {
int l = 0, r = n;
int right_border = -2;
while (l <= r) {
int mid = l + ((r - l) >> 1);
if (nums[mid] <= target) { /* 寻找比目标值大的第一个位置 */
l = mid + 1; // 寻找比目标值大的第一个下标
right_border = l; // 记录比目标值大的第一个下标
} else { /* 查询区间变为[l, mid - 1] */
r = mid - 1;
}
}
return right_border;
}
public:
vector<int> searchRange(vector<int>& nums, int target) {
int nums_size = nums.size();
int left_border = -2, right_border = -2;
left_border = get_left_border(nums, target, nums_size - 1); // 找到小于目标的第一个元素下标
right_border = get_right_border(nums, target, nums_size - 1); // 找到大于目标的第一个元素下标
// 情况(1): 目标小于数组最小元素,或大于数组最大元素
if (left_border == -2 || right_border == -2) return {-1, -1};
// 情况(2): 如果存在目标值(或多个目标值),返回正确的闭区间
if (right_border - left_border > 1) return {left_border + 1, right_border - 1};
// 情况(3): target应该插入数组中,但是不在数组中
return {-1, -1};
}
};
例题实战
1. 69. x 的平方根
思路1 取对数
$a^2 = x$ 由对数运算性质则 $a = e^{\frac{1}{2}lnx}$
class Solution {
#define ll long long
public:
int mySqrt(int x) {
if (x == 0) return 0;
double ans = exp(0.5 * log(x));
ll ret = (ll)ans;
// (ll)ans强转之后会出现四舍五入,而此题要求去尾
return ((ret + 1) * (ret + 1) <= x) ? ret + 1 : ret;
}
};
思路2 二分查找
在区间[0, x]中查找a,使得 $a^2 <= x$,返回第一个a
找到a^2 == x的第一个大于a的ans,返回ans-1
class Solution {
#define ll long long
public:
int mySqrt(int x) {
int l = 0, r = x;
int ans = 0;
while (l <= r) {
int mid = l + ((r - l) >> 1);
if ((ll)mid * mid <= x) {
l = mid + 1;
ans = l; /* 记录a^2大于x的第一个下标位置 */
} else {
r = mid - 1;
}
}
return ans - 1;
}
};
思路3 泰勒级数
利用泰勒级数展开,逼近目标值
$f(x) = x^2$,方法如下图所示:从$x_0 = C$开始,找到在曲线$f(x_0)$做切线,交$x$轴为$(x_1, 0)$点,反复迭代,直到$|x_i - \sqrt{x}| < 1e-6$,结束迭代,返回结果
](https://img-blog.csdnimg.cn/ef5c2f7fe23848db953832ae5cc4aa18.png)
class Solution {
public:
int mySqrt(int x) {
if (x == 0) return 0;
double C = x, x0 = x;
while (true) {
double xi = 0.5 * (x0 + C / x0);
if (fabs(xi - x0) < 1e-6) break;
x0 = xi;
}
return int(x0);
}
};
2. 367. 有效的完全平方数
思路
区间[0, num]内二分查找,找到返回true,否则返回false。
class Solution {
#define ll long long
public:
bool isPerfectSquare(int num) {
int l = 0, r = num;
while (l <= r) {
int mid = l + ((r - l) >> 1);
if ((ll)mid * (ll)mid == num) return true;
else if ((ll)mid * (ll)mid > num) r = mid - 1;
else l = mid + 1;
}
return false;
}
};
3. 移除元素
例题实战
1. 27. 移除元素
思路
利用双指针$O(n)$时间解决问题。
利用快指针fast_index遍历数组,用慢指针slow_index指向当前新数组的最后一位。
(1) 当删除一个元素时(nums[fast_index] == val),新数组不用扩容,slow_index不动,fast_index继续向后遍历;
(2) 不用删除的元素,向前移动到slow_index位置,新数组扩容,slow_index++。
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slow_index = 0;
int nums_size = nums.size(), fast_index;
for (fast_index = 0; fast_index < nums_size; ++fast_index) {
if (nums[fast_index] != val) {
nums[slow_index++] = nums[fast_index];
}
}
return slow_index;
}
};
2. 26.删除排序数组中的重复项
思路
利用一个慢指针指向新数组的最后一个元素后一位,用快指针遍历数组,时间复杂度$O(n)$
(1) 出现重复数字,++fast_index,继续遍历,但是新数组不扩展
(2) 出现不同数字,将新的不同数字加入新数组,并将slow_index++,更新到新数组最后一个元素后。
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int nums_size = nums.size();
int slow_index = 1, fast_index = 1; // num[0]不用移动,直接保留即可
for (fast_index = 1; fast_index < nums_size; ++fast_index) {
if (nums[fast_index] != nums[fast_index - 1]) { /* 跳过重复元素,遇到不同元素向前移动 */
nums[slow_index++] = nums[fast_index];
}
}
return slow_index;
}
};
3. 283.移动零
思路
利用双指针,一个指向最低位的零,一个指向零后的第一个数字,交换二者,继续向后更新指针。时间复杂度$O(n)$
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int nums_size = nums.size();
int zero_pos = 0, nums_pos = 0;
while (nums_pos < nums_size && zero_pos < nums_size) {
if (nums[nums_pos] == 0) nums_pos++;
if (nums[zero_pos] != 0) zero_pos++;
if (nums_pos >= nums_size || zero_pos >= nums_size) break; // 防止地址访问越界
if (nums[nums_pos] != 0 && nums[zero_pos] == 0 && nums_pos > zero_pos) swap(nums[nums_pos++], nums[zero_pos++]); // 零和其之后的第一数字交换位置
else if (nums[nums_pos] != 0 && nums[zero_pos] == 0 && nums_pos <= zero_pos) nums_pos = zero_pos + 1; // 数字在零前面,不满足条件,继续向后找零之后的数字,可以避免无用的访问(访问多个零之前的非零数字)
}
}
};
4. 844.比较含退格的字符串
思路
双指针,慢指针指向读入回退之后新字符串的结尾处,快指针遍历原字符串。快指针遇到'#',慢指针-1,表示删除新字符串末尾字符。
class Solution {
public:
bool backspaceCompare(string s, string t) {
int s_len = s.length();
int t_len = t.length();
int l = 0, r = 0;
while (r < s_len) { /* 获取读入回退后的s */
if (s[r] != '#') s[l++] = s[r++];
else {
r++;
l = l >= 1 ? l - 1 : 0;
}
}
s_len = l; // 更新处理后的s长度
l = 0, r = 0;
while (r < t_len) { /* 获取读入回退后的t */
if (t[r] != '#') t[l++] = t[r++];
else {
r++;
l = l >= 1 ? l - 1 : 0;
}
}
t_len = l; // 更新处理后的t长度
// 判断两字符串是否相等
if (s_len != t_len) return false;
for (int i = 0; i < s_len; ++i) if (s[i] != t[i]) return false;
return true;
}
};
4. 有序数组的平方
例题实战
1. 977.有序数组的平方
思路
考虑到数组一开始有序,有正有负,那么平方后的值肯定是两边大,中间小,于是,利用双指针,一个指向最左边,一个指向最右边,比较两指针位置数值平方的大小,将较大值存为答案,并将指针向中间移动。只需遍历一边数组,时间复杂度为$O(n)$
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
int nums_size = nums.size();
vector<int> result(nums_size, 0);
int l = 0, r = nums_size - 1, k = nums_size - 1;
while (l <= r) {
int left_s = nums[l] * nums[l];
int right_s = nums[r] * nums[r];
if (left_s <= right_s) {
r--;
result[k--] = right_s;
} else {
l++;
result[k--] = left_s;
}
}
return result;
}
};
5. 长度最小的子数组
例题实战
1. 209. 长度最小的子数组
思路
维护一个区间和小于target的滑动窗口
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int nums_size = nums.size(), i;
int l = 0, r = 0;
int sum = 0, ans = 1e5 + 10;
for (i = 0; i < nums_size; ++i) {
sum += nums[i];
r = i;
while (sum >= target) { /* 如果区间和大于target不断去掉窗口起始位置的值,直到区间和小于target */
ans = min(ans, r - l + 1);
sum -= nums[l++];
}
cout << ans << endl;
}
return ans == 1e5+10 ? 0 : ans;
}
};
2. 904. 水果成篮
思路
题意翻译的什么玩意儿?应该是数组中的每个数字代表该树生产的水果是哪一种,比如1代表苹果,2代表香蕉,3代表橘子,每个果篮只能放一种水果,两个果篮如果第一个放入了苹果,那该篮子就只能放苹果,除非拿出所有苹果才能放其他水果。
利用滑动窗口解题,维护窗口内只有两种水果的最长窗口。即只有两种不同数字的最长连续子数组。
方法一: 窗口左侧删除某一种水果不好维护,所以使用unordered_map,当窗口中的水果数量大于两种,则收缩左侧窗口直到窗口内的水果种类等于两种,更新答案,继续向右扩展窗口,加入水果。
class Solution {
public:
int totalFruit(vector<int>& fruits) {
int n = fruits.size(), i;
int type_max = 2;
unordered_map<int, int> window;
int l = 0, r = 0;
int ans = 0;
for (r = 0; r < n; ++r) {
window[fruits[r]]++; // 记录当前种类水果采摘的次数
while (window.size() > type_max) { // 种类大于2,窗口左端收缩,直到种类小于等于2
window[fruits[l]]--;
if (window[fruits[l]] == 0) window.erase(fruits[l]); // 从窗口中删除一种水果
l++;
}
ans = max(ans, r - l + 1);
}
return ans;
}
};
方法二: 不使用unordered_map容器。利用hash表记录每种水果出现的次数,窗口的维护同方法一。
class Solution {
public:
int totalFruit(vector<int>& fruits) {
int n = fruits.size(), i;
int type_max = 2;
int type_tot = 0;
vector<int> hash(n, 0);
int l = 0, r = 0;
int ans = 0;
for (r = 0; r < n; ++r) {
hash[fruits[r]]++; // hash表记录当前窗口中fruits[r]这种水果出现的次数
if (hash[fruits[r]] == 1) { // 当前水果在窗口中第一次出现
type_tot++; // 窗口中的水果种类+1
}
while (type_tot > type_max) { /* 从左侧收缩窗口直到窗口中的水果种类等于2 */
hash[fruits[l]]--;
if (hash[fruits[l++]] == 0) { /* 从收缩左侧窗口,并将l+1 */
type_tot--;
break;
}
}
ans = max(ans, r - l + 1); // 更新答案
}
return ans;
}
};
3. 76. 最小覆盖子串
思路
和前两道例题的思路异曲同工,记录每种需求的每种字符的出现次数,维护一个滑动窗口,要求窗口内的s的子串中的字符包含t中的所有字符。实现细节见代码注释。
class Solution {
#define minn -100010
public:
string minWindow(string s, string t) {
int s_len = s.length(), i;
int t_len = t.length();
int hash[60]; // 每个字符在t中是否出现,若出现,记录出现次数;若未出现,记为-1
for (i = 0; i < 60; ++i) hash[i] = minn; // 将t中未出现过的字符初始化为无穷小
int l = 0, r = 0;
int type_num = 0; // 记录还需要出现的字符种类数
string ans = ""; // 记录答案
for (i = 0; i < t_len; ++i) { /* 求出t中每个字符出现的次数 */
if (hash[t[i] - 'A'] == minn) {
hash[t[i] - 'A'] = 1;
type_num++; // 记录出现的字符的种类数
}
else hash[t[i] - 'A']++; // 记录每种字符需要的数量
}
for (r = 0; r < s_len; ++r) { /* 遍历s中的字符,寻找答案 */
if (hash[s[r] - 'A'] == minn) continue; // t中未出现的字符,跳过即可
hash[s[r] - 'A']--; // 对s[r]的需求量-1
if (hash[s[r] - 'A'] == 0) type_num--; // 如果s[r]的需求量为0,即在当前窗口中出现的数量与t中相同,则该种类的字符满足要求
if (type_num == 0) { // t中的所有字符都出现并且数量足够时,更新答案
while (hash[s[l] - 'A'] == minn || hash[s[l] - 'A'] < 0) { /* 窗口中的某些字符数量可能已超过了需求量,可以从左侧收缩窗口长度 */
hash[s[l++] - 'A']++;
}
if (ans == "" || ans.size() > r - l + 1) { /* 更新答案 */
ans = s.substr(l, r - l + 1); // 获取区间[l, r]的字串
}
}
while (type_num == 0 && l <= r) { /* 更新完一次答案后,首先要从左侧收缩,使一种字符的数量不够,再继续向右扩展窗口 */
if (hash[s[l] - 'A'] != minn) {
hash[s[l] - 'A']++;
if (hash[s[l] - 'A'] == 1) type_num++;
l++;
while (l <= r && hash[s[l] - 'A'] == minn) l++; // 将窗口左侧的t中未出现的字符从窗口中丢出
if (type_num) break;
}
}
}
return ans;
}
};
6. 螺旋矩阵 II
例题实战
1. 59. 螺旋矩阵 II
思路
此题没有什么算法,主要是在敲代码之前捋清逻辑,逻辑清晰了,就不容易出问题。
依次进行上右下左的填充,每次填充都选择左闭右开的区间填充形式,防止产生边界处的冲突。示意图如下:

红、绿、橙、棕四种颜色均是前闭后开的填充形式,将最后一个块留给下一次填充,利用for循环中的边界判断即可实现。
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> ans(n, vector<int>(n, 0));
int start_x = 0, start_y = 0;
int loop = n / 2;
int mid = n / 2;
int i, j;
int cnt = 1;
while (loop--) {
for (j = start_y; j < n - start_y - 1; ++j) ans[start_x][j] = cnt++; // 上
for (i = start_x; i < n - start_x - 1; ++i) ans[i][j] = cnt++; // 右
for (; j > start_y; --j) ans[i][j] = cnt++; // 下
for (; i > start_x; --i) ans[i][j] = cnt++; // 左
start_x++;
start_y++;
}
if (n & 1) ans[mid][mid] = cnt; // n为奇数时中心要特殊处理
return ans;
}
};
2. 54.螺旋矩阵
思路
是第三题省掉n == 0的简化版,思路见第三题。
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int n = matrix.size();
int m = matrix[0].size();
vector<int> ans(n * m, 0);
int start_x = 0, start_y = 0;
int mid = min(n, m) >> 1;
int loop = mid;
int cnt = 0;
int i, j;
while (loop--) {
for (j = start_y; j < m - start_y - 1; ++j) ans[cnt++] = matrix[start_x][j];
for (i = start_x; i < n - start_x - 1; ++i) ans[cnt++] = matrix[i][j];
for (; j > start_y; --j) ans[cnt++] = matrix[i][j];
for (; i > start_x; --i) ans[cnt++] = matrix[i][j];
start_x++;
start_y++;
}
if ((n & 1) || (m & 1)) {
if (n % 2 == 0 && n < m) return ans;
if (m % 2 == 0 && m < n) return ans;
if (n < m) for (i = start_x, j = start_y; j < m - start_y; ++j) ans[cnt++] = matrix[i][j]; // 行较小
else for (i = start_x, j = start_y; i < n - start_x; ++i) ans[cnt++] = matrix[i][j]; // 列较小
}
return ans;
}
};
3. 剑指Offer 29.顺时针打印矩阵
思路
思路和第一题相同, 都是顺时针遍历数组,不过此时行n和列数m可能不相等,要考虑奇偶和大小问题。
(1) n,m均为奇数,最后较小一方的中间行(列);(特殊情况是n==m,此时剩中间一格)
(2) n,m一奇、一偶,偶数小,此时没有剩余
(3) n,m一奇、一偶,偶数大,此时剩余奇数对应的中间行(列)
(4) n,m均为偶数,没有剩余
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int n = matrix.size();
if (n == 0) return {};
int m = matrix[0].size();
vector<int> ans(n * m, 0);
int start_x = 0, start_y = 0;
int mid = (min(n, m) / 2);
int loop = mid;
int i, j;
int cnt = 0;
while (loop--) {
for (j = start_y; j < m - start_y - 1; ++j) ans[cnt++] = matrix[start_x][j];
for (i = start_x; i < n - start_x - 1; ++i) ans[cnt++] = matrix[i][j];
for (; j > start_y; --j) ans[cnt++] = matrix[i][j];
for (; i > start_x; --i) ans[cnt++] = matrix[i][j];
start_x++;
start_y++;
}
if (n & 1 || m & 1) {
// (1) 如果较小的数是偶数,则没有剩余行(列)直接返回结果
if (n % 2 == 0 && n < m) return ans;
if (m % 2 == 0 && m < n) return ans;
// (2) 否则,存在剩余行(列),该行是较小奇数的中间行(列)
if (n < m) for (i = start_x, j = start_y; j < m - start_y; ++j) ans[cnt++] = matrix[i][j];
else for (i = start_x, j = start_y; i < n - start_x; ++i) ans[cnt++] = matrix[i][j];
}
return ans;
}
};

浙公网安备 33010602011771号