算法基础练习总结入口:我的算法地图
一、基本概念
1、数组: 是内存中一段连续的存储空间,通过索引直接访问元素。它是所有数据结构中最基础的形式,其他很多数据结构都是基于数组构建的。典型适用场景:需要随机访问、知道元素数量或大致范围的情况。
2、字符串:字符串是字符的序列,本质上是一种特殊的数组(字符数组)。特殊终止符(C语言风格)以’\0’结尾。
最常见的算法数据结构,由于字符串本质也是数组,二者很多题目和做法都是一致的,我们放在一起讨论。
二、场景分析
1、有哪些题目类型。 数组和字符串,属于线性表,成员可以随机访问,为算法题中最基础的数据结构,衍生出的题目类型非常丰富。以下是其典型的算法题类型分类。注意许多题目会交叉融合多种类型。
| 序号 | 题目类型 | 核心特点 | 典型例题 | 常用算法模式 |
|---|---|---|---|---|
| 1 | 子数组/子串最值问题 | 求满足条件的最长/最短/最大和 | 无重复字符的最长子串 (LC 3)、最大子数组和 (LC 53) | 滑动窗口、动态规划 |
| 2 | 元素查找与匹配 | 在数组/字符串中查找目标元素、判断 | 两数之和 (LC 1)、实现 strStr() (LC 28) | 哈希表、双指针、字符串匹配 |
| 3 | 元素操作与修改 | 原地修改数组(增删改查)、调整元素 | 移除元素 (LC 27)、移动零 (LC 283) | 双指针、原地替换 |
| 4 | 区间求和与统计 | 计算子数组/子矩阵的和、统计满足条件的子序列数量 | 和为K的子数组 (LC 560)、区域和检索 (LC 304) | 前缀和、差分、哈希表 |
| 5 | 区间修改与更新 | 对连续区间进行批量增减操作,最后获取结果 | 航班预订统计 (LC 1109)、拼车 (LC 1094) | 差分数组 |
| 6 | 字符频率与变形 | 基于字符出现次数解决问题,对字符串进行特定变形 | 有效的字母异位词 (LC 242)、字符串的排列 (LC 567) | 哈希表、排序 |
| 7 | 回文相关问题 | 判断或构造回文串、回文子串,利用回文的对称性 | 验证回文串 (LC 125)、最长回文子串 (LC 5) | 双指针、中心扩展法 |
| 8 | 多指针组合问题 | 利用3个或更多指针解决复杂匹配问题 | 三数之和 (LC 15)、四数之和 (LC 18) | 排序+双指针、多指针遍历 |
| 9 | 矩阵相关问题 | 二维数组的遍历、搜索、旋转、转置等操作 | 螺旋矩阵 (LC 54)、旋转图像 (LC 48) | 模拟法、分层遍历 |
2、总结涉及最用的算法模式。 从算法模式的角度来看,数组和字符串总结如下:
| 序号 | 算法模式 | 核心思想 | 优势/为什么适用 | 适用场景 | 典型案例 |
|---|---|---|---|---|---|
| 1 | 滑动窗口 | 维护动态变化的子数组/子串窗口,通过调整左右边界优化计算 | 利用连续性只需处理新增/移除元素 | 连续子序列问题 | 无重复字符的最长子串 (LC 3)、长度最小的子数组 (LC 209) |
| 2 | 双指针 | 用两个指针(同向/反向)在数组上移动,通过策略性移动优化效率 | 利用有序性或对称性快速缩小范围,O(n)时间复杂度,节省空间 | 有序数组查找(两数之和)、原地修改(移除元素) | 两数之和 II (LC 167)、移除元素 (LC 27) |
| 3 | 前缀和 | 预处理前缀和数组,将子数组和计算优化为 O(1) | 避免重复计算,将 O(n²) 优化为 O(n) | 频繁计算子数组和、子数组和等于目标值 | 和为K的子数组 (LC 560)、二维区域和检索 (LC 304) |
| 4 | 差分 | 构建差分数组,将区间修改从 O(n) 优化为 O(1) | 将区间操作转化为两点操作,大幅减少重复计算,适合批量区间修改 | 频繁区间增减操作、区间更新与单点查询 | 航班预订统计 (LC 1109)、拼车 (LC 1094) |
| 5 | 哈希表 | 用哈希表存储元素信息(值/索引/次数),将查找优化为 O(1) | 利用 O(1) 查找特性降低时间复杂度,适合"边遍历边查询"场景 | 元素存在性判断(去重)、记录元素位置/次数、字符频率问题 | 两数之和 (LC 1)、有效的字母异位词 (LC 242) |
| 6 | 排序+双指针 | 先排序再用双指针,利用有序性优化查找和去重 | 排序后可通过双指针高效解决多数问题,便于跳过重复元素,避免结果集重复 | 元素大小关系问题(三数之和)、需要去重的问题 | 三数之和 (LC 15)、最接近的三数之和 (LC 16) |
| 7 | 字符串匹配 | 针对子串查找问题的专用算法(KMP/Rabin-Karp 等) | 比通用方法更高效,可将复杂度降至 O(n+m),适合长字符串处理 | 主串中查找模式串的位置 | 实现 strStr() (LC 28)、重复的子字符串 (LC 459) |
三、典型题目
1、基础练习题-《程序员面试经典》题目
1)判定字符是否唯一
实现一个算法,确定一个字符串 s 的所有字符是否全都不同。(加分:不要用额外空间)
bool isUnique(string astr) {
// 方法1:使用表记录字符是否出现,复杂度 时间O(n)/空间O(1)
// 题目限制的输入为小写字母,最多26个,可用u32记录
int bitMap = 0;
for (int i = 0; i < astr.size(); i++) {
if ((bitMap & (1 << (astr[i] - 'a'))) == 0) {
bitMap |= 1 << (astr[i] - 'a');
} else {
return false;
}
}
return true;
}
思考:最先想到的就是用 额外的空间记录是否出现(题目限制小写字母,最多26个,可以用一个u32的位操作记录),遍历一次,复杂度 时间O(n)/空间O(1)。扩展:如果数据量大一些,如未限制字符内容,可以用boot map[256]。
2)品牌判定是否互为字符重复
给定两个由小写字母组成的字符串 s1 和 s2,请编写一个程序,确定其中一个字符串的字符重新排列后,能否变成另一个字符串。(题解:省)
思考:一种是统计每个string的各个字符数量,再对比结果。复杂度 时间O(n)/空间O(1);一种是先排序,再对比。
3)URL化
URL化。编写一种方法,将字符串中的空格全部替换为%20。假定该字符串尾部有足够的空间存放新增字符,并且知道字符串的“真实”长度。(加分:不要用额外控制)
string replaceSpaces(string S, int length) {
int new_pos = S.size() - 1;
for (int old_pos = length - 1; old_pos >= 0; old_pos--) {
if (S[old_pos] == ' ') {
S[new_pos--] = '0';
S[new_pos--] = '2';
S[new_pos--] = '%';
} else {
S[new_pos--] = S[old_pos];
}
}
return S.substr(new_pos + 1);
}
4)回文排列
给定一个字符串,编写一个函数判定其是否为某个回文串的排列之一。回文串是指正反两个方向都一样的单词或短语。排列是指字母的重新排列。回文串不一定是字典当中的单词。
bool canPermutePalindrome(string s) {
// 回文排列要求:只能有一个或零个,奇数字符存在;复杂度 时间O(n)/空间O(1)
// 最小记录空间,由于只需要判断奇偶,可以用bool
bool oddmap[256] = {0};
for (int i = 0; i < s.size(); i++) {
oddmap[s[i]] = !oddmap[s[i]];
}
int oddcnt = 0;
for (int j = 0; j < 256; j++) {
if (oddmap[j] == true) {
oddcnt++;
}
}
return oddcnt <= 1 ? true : false;
}
5)一次编辑
字符串有三种编辑操作:插入一个英文字符、删除一个英文字符或者替换一个英文字符。 给定两个字符串,编写一个函数判定它们是否只需要一次(或者零次)编辑。
bool oneEditAway(string first, string second) {
int lenA = first.size();
int lenB = second.size();
// 长度差别大于1必然不可能系一次处理
if (abs(lenA - lenB) > 1) {
return false;
}
bool EditFlag = false;
// 如果两个相等,只能最多有一个字符不同,两个一起跳过继续遍历
// 如果零个不等,那必然是短的那个少一个字符,跳过一个
int i = 0, j = 0;
for (;i < lenA && j < lenB; i++,j++) {
if (first[i] != second[j]) {
if (EditFlag == false) {
EditFlag = true;
if (lenA > lenB) {
j--; // first长,second回退位置
} else if (lenA < lenB) {
i--; // second长,first回退位置
} else {
// 长度相同 一起跳
}
} else {
return false;
}
}
}
return true;
}
思考:双指针的一种,两个序列往前走,当遇到首个不同的时候,根据条件调整指针位置;
6)字符串压缩
字符串压缩。利用字符重复出现的次数,编写一种方法,实现基本的字符串压缩功能。比如,字符串aabcccccaaa会变为a2b1c5a3。若“压缩”后的字符串没有变短,则返回原先的字符串。你可以假设字符串中只包含大小写英文字母(a至z)。
string compressString(string S) {
int len = S.size();
if (len == 0) {
return S;
}
string cmpStr;
int i = 0;
while (i < len) {
int j = i + 1;
while (j < len && S[j] == S[i]) {
j++;
}
cmpStr += S[i] + to_string(j - i);
i = j;
}
return cmpStr.size() < len ? cmpStr : S;
}
7) 旋转矩阵
给你一幅由 N × N 矩阵表示的图像,其中每个像素的大小为 4 字节。请你设计一种算法,将图像旋转 90 度。不占用额外内存空间能否做到?
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
// 从外圈向内圈旋转找坐标交换规律
for (int i = 0; i < n / 2; ++i) {
for (int j = 0; j < (n + 1) / 2; ++j) {
int temp = matrix[i][j];
matrix[i][j] = matrix[n - j - 1][i];
matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
matrix[j][n - i - 1] = temp;
}
}
}
};
8)零矩阵
编写一种算法,若M × N矩阵中某个元素为0,则将其所在的行与列清零。
void setZeroes(vector<vector<int>>& matrix) {
int N = matrix.size();
int M = matrix[0].size();
vector <bool> rowzero(N);
vector <bool> colzero(M);
// 做两轮循环,第一轮记录 有哪些列存在0;哪些行存在0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (matrix[i][j] == 0) {
rowzero[i] = true;
colzero[j] = true;
}
}
}
// 第二轮循环,如果此行有0就全部清零,此行无零,就把存在0的列情理掉。
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (rowzero[i] == true || colzero[j] == true) {
matrix[i][j] = 0;
}
}
}
}
9)字符串旋转
字符串轮转。给定两个字符串s1和s2,请编写代码检查s2是否为s1旋转而成(比如,waterbottle是erbottlewat旋转后的字符串)。
// 解法一:遍历位置匹配翻转规律
bool checkStrSame(string &s1, string &s2,
int s1_pos, int s2_pos, int len){
for (int i = 0; i < len; i++) {
if (s1[s1_pos + i] != s2[s2_pos + i]) {
return false;
}
}
return true;
}
bool isFlipedString(string s1, string s2) {
int len1 = s1.size();
int len2 = s2.size();
if (len1 != len2) {
return false;
}
if (s1 == s2) {
return true;
}
for (int i = 0 ; i < len1; i++) {
// 找选择中心,从i位置开始转
// i之后的字符串为新串的head,0-i的字符串为新串尾部
if (checkStrSame(s1, s2, i + 1, 0, len1 - i - 1) == true &&
checkStrSame(s1, s2, 0, len1 - i - 1, i + 1) == true) {
return true;
}
}
return false;
}
// 解法二 合并查找
bool isFlipedString(string s1, string s2) {
return s1.size()==s2.size()&&(s1+s1).find(s2)!=-1;
}
2、模拟练习题-leetcode类型高频题目
1)盛最多雨水的容器
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。返回容器可以储存的最大水量。说明:你不能倾斜容器。
int maxArea(vector<int>& height) {
// 双指针。移动策略:优先移动短板指针, 复杂度 时间O(n)/空间O(1)
// 雨水面积area=min(height[m], heght[n])*(m-n)
int left = 0;
int right = height.size() - 1;
int ret = 0;
while (left < right) {
int ans = min(height[right], height[left]) * (right - left);
ret = max(ret, ans);
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return ret;
本题核心在于为什么双指针做法是对的。先推导基本雨水公式area=min(height[m], heght[n])*(m-n),思考把指针放到两侧,如果要移动一边的柱子大小变化趋势,1)无论移动左边还是右边,宽度m-n一定是变小的;2)高度变化规律 假如移动的是高的一侧,高度由左右最短的决定 而 短的没有动,因此移动面积宽变小高变小只会更小,选择任意一个柱子结果都如此,因此当前短柱子和任意柱子组合排除,想要更大只能尝试移动另一侧。3)如果移动的是短的这侧,则由可能出现更高的。
2)三数之和
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
// 思路:排序加双指针
// 三个数加起来为0,特殊情况3个都为0,否则一定有正有负
// K指向末尾为目标值,i和j求和-num[K];用双指针发简化
vector<vector<int>> result;
if (nums.size() < 3) {
return result;
}
sort(nums.begin(), nums.end());
int i = 0, j = 0, k = 0;
for (k = nums.size() - 1; k >= 2; k--) {
if (k < nums.size() - 1 && (nums[k] == nums[k + 1])) {
continue;
}
int i= 0, j = k - 1;
while (i < j) {
if (nums[i] + nums[j] < -nums[k]) {
i++;
} else if (nums[i] + nums[j] > -nums[k]) {
j--;
} else {
vector<int> tmp = {nums[i], nums[j], nums[k]};
result.push_back(tmp);
j--;
while (j > 0 && nums[j] == nums[j+1]) {
j--;
}
}
}
}
return result;
}
};
3)除自身以外的数组乘积
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。
题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 不要使用除法,且在 O(n) 时间复杂度内完成此题。
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
// 思路:单独算前缀 后缀,累加乘法
// 需要O(n)完成,就不能排序。如果全乘一遍 又需要用到除法
// 就只能单独记录前缀和后缀,然后用乘法得结果
// 由于前缀和之间存在迭代关系,可以用一个变量记录。后缀类似
int premul = 1;
int aftermul = 1;
int len = nums.size();
vector<int> result(len, 1);
for (int i = 0; i < len; i++) {
result[i] *= premul;
premul *= nums[i];
result[len - 1 - i] *= aftermul;
aftermul *= nums[len - 1 - i];
}
return result;
}
};
4) 买卖股票的最佳时间
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
class Solution {
public:
int maxProfit(vector<int>& prices) {
// 思路:遍历序列,记录之前最低价格,计算当前点卖得到当天最大收益
int minPrice = prices[0];
int result = 0;
for (int i = 0; i < prices.size(); i++) {
int profit = prices[i] - minPrice;
if (result < profit) {
result = profit;
}
if (prices[i] < minPrice) {
minPrice = prices[i];
}
}
return result;
}
};
5) 数组拆分
给定长度为 2n 的整数数组 nums ,你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), …, (an, bn) ,使得从 1 到 n 的 min(ai, bi) 总和最大。返回该 最大总和 。
int arrayPairSum(vector<int>& nums) {
// 思路:重点在推导,贪心思路
// 先排序,最小这个值a0和谁比都更小,肯定会放到结果中
// 那么和他选一起一组的就必须丢弃一个,选最小就是挨着的a1
// 继续在剩下里面找策略不变,最终a0、a2...an-2被选上
sort(nums.begin(), nums.end());
int result = 0;
for (int i = 0; i < nums.size(); i += 2) {
result += nums[i];
}
return result;
}
6)寻找两个有序数组的中位数
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
int getKthElement(vector<int>& nums1, vector<int>& nums2, int k) {
// pos1和pos2为两个数组的index,通过移动pos1和pos2找到第K大的数
// 随着pos加大 丢掉pos前小于K的数,相应K减少 从新的pos开找K大的
int len1 = nums1.size();
int len2 = nums2.size();
int pos1 = 0;
int pos2 = 0;
while (true) {
// 其中一个数组找到边界
if (pos1 == len1) {
return nums2[pos2 + k - 1];
}
if (pos2 == len2) {
return nums1[pos1 + k - 1];
}
if (k == 1) {
return min(nums1[pos1], nums2[pos2]);
}
int newpos1 = min(pos1 + k / 2 - 1, len1 - 1);
int newpos2 = min(pos2 + k / 2 - 1, len2 - 1);
if (nums1[newpos1] < nums2[newpos2]) {
k -= newpos1 - pos1 + 1;
pos1 = newpos1 + 1;
} else {
k -= newpos2 - pos2 + 1;
pos2 = newpos2 + 1;
}
}
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
// 思路:二分查找逻辑 来实现log()时间效率
// 如果总数是奇数为中间数,总数为偶数为中间两个数的均值
// 定义getKthElement为两个数组一起第K大的数
int len = nums1.size() + nums2.size();
if (len % 2 == 1) {
return getKthElement(nums1, nums2, (len + 1) / 2);
} else {
return (getKthElement(nums1, nums2, len / 2) + getKthElement(nums1, nums2, len / 2 + 1)) / 2.0;
}
}



1046

被折叠的 条评论
为什么被折叠?



