算法题总结
- 回溯题,当需要输出很多个等长串且第一个字符都不一样时,回溯内部需要用到for循环,否则不用。回溯函数一般返回类型都是void
- mysql中使用三值逻辑true、false、UNKNOWN
- 二分搜索的几种特例(while循环的判断条件为le <= ri)
1)找到该键第一次出现的位置,一直在左半区间搜索,如果nums[mid]==target,用一个变量index记录该下标,然后hi = mid -1,继续在左半区搜,若左半区没有该元素,则index就是target第一次出现的位置。
2)查找该键最后一次出现的位置反之- 在排序数组中查找元素的第一个和最后一个位置3)在一个有序数组中找第一个大于等于target的下标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16int searchRangeLeft(vector<int>& nums, int target) {
int le = 0, ri = nums.size()-1;
int idx = -1;
while (le <= ri) {
int mid = le + (ri-le)/2;
if (nums[mid] == target) {
idx = mid;
ri = mid - 1;
} else if (nums[mid] < target){
le = mid + 1;
} else {
ri = mid - 1;
}
}
return idx;
}
- 在排序数组中查找元素的第一个和最后一个位置
- 剑指 Offer II 068. 查找插入位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14int searchInsert(vector<int>& nums, int target) {
int p = 0, q = nums.size()-1;
int ret = q+1;
while (p <= q) { // 这里的判断条件和while循环不一致
int mid = p + (q-p)/2;
if (nums[mid] >= target) {
ret = mid;
q = mid - 1;
} else {
p = mid + 1;
}
}
return ret;
}
- 单调栈
- 头部先push一个0,则不需要判断栈是否为空
- 将下标放在栈中,栈中下标对应的元素是单调增/减的
- 接雨水,每日温度,柱状图中最大的矩形
- 前缀和可以用
std::accumulate(nums.begin(), nums.end(), init_val);
会返回当前容器的前缀和 - 字符串
isspace(ch)
空格isalpha(ch)
字母isdigit(ch)
数字isalnum(ch)
字母和数字tolower(ch)
转化为小写toupper(ch)
转换为大写transform(nums.begin(), nums.end(), nums.begin(), tolower)
将容器内的元素都变为小写字母,第三个参数表示替换的起始位置
- dp背包问题
- 分类
- 0-1背包(空间优化时必须逆向遍历)
- dp[i][j]表示前i件物品在物品体积不超过j的情况下的最大价值
- 如果第i件物品不被放进背包,则
dp[i][j] = dp[i-1][j]
- 如果第i件物品被放进背包, 且该物品的体积为W,价值为V,则
dp[i][j] = dp[i-1][j-W] + V;
- 遍历过程只需要取这两种情况的最大值即可
- 时间复杂度和空间都为
O(NW)
- 完全背包(可以直接从一维优化,因为不确定物品具体的数目)
- 如果第i件物品不被放进背包,则
dp[i][j] = dp[i-1][j]
- 如果第i件物品被放进背包, 且该物品的体积为W,价值为V,则
dp[i][j] = dp[i][j-W] + V;
,注意与01背包的区别,当前状态也有同一行的状态决定 - 比如279完全平方数,322零钱兑换,直接用一维做就可以了ii
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount+1, INT_MAX-1);
dp[0] = 0; // 初始条件,0块钱所需要的最少硬币数
// 外层物品,内层体积
for (int i = 1; i <= amount; ++i) {
for (int j = 0; j < coins.size(); ++j) {
if (i >= coins[j]) { // 保证不发生越界
dp[i] = min(dp[i], dp[i-coins[j]] + 1); // 1为价值,coins[j]为体积
}
}
}
return dp[amount] == INT_MAX-1? -1: dp[amount];
}
};
- 如果第i件物品不被放进背包,则
- 一般01背包,外层物品内层体积;完全背包外层体积,内层物品
- 0-1背包(空间优化时必须逆向遍历)
- 分类
- 寻找[1,n]重复数的常见解题思路
- O1空间复杂度,以元素值为下标作为桶,遍历到就往桶里面(对应下标为nums[nums[i]%n],n为数组长度)加上数组的长度,最后再遍历一次check桶里面的值。
- dp股票问题
- 画状态图
- 记得设置初始状态,可能BUY和HOLD都要一起设置。如BUY[i]表示在第i天买入的最大利润,那么初始值BUY[0]应该设置为-prices[0]
- 最后的结果可能是两个状态的最大值
- 二维数组的二分查找,直接取矩阵的右上角作为mid来进行查找
- C++中优先队列priority_queue的根节点在尾部,因此建立小根堆比较器为greater(即lhs > rhs)
- 如果想使用lamda表达式来作为比较器,必须得用
decltype
,定义对象时还需要传入模板参数,注意lambda表达式是匿名函数,在定义比较器时需要返回bool变量1
2
3
4
5
6
7
8
auto cmp = [](int a, int b) {
// 比较器的实现
return a < b;
};
std::priority_queue<int, std::vector<int>, decltype(cmp)> pq(cmp); - 或者自定义对象,使用仿函数来实现
1
2
3
4
5
6struct cmp {
bool operator()(pair<int,int>& lhs, pair<int, int>& rhs) {
return lhs.second > rhs.second;
}
};
priority_queue<pair<int, int>, vector<pair<int, int>>, cmp> que;
- 如果想使用lamda表达式来作为比较器,必须得用
- 前缀树
- 每个node两个成员变量:isend判断该节点是否为结尾,
vector<Trie*>children(26);
,表示26个小写字母 - 每个node一个方法:查询前缀树中是否有该前缀,有的话返回该前缀的尾节点,否则返回nullptr
- 每个node两个成员变量:isend判断该节点是否为结尾,
- 快速排序(时间O(nlogn), 空间O(logn))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Solution {
public:
void quickSort(vector<int>& arr, int lo, int hi) {
if (lo >= hi) { // 记得这里递归要有结束条件
return;
}
int mid = partition(arr, lo, hi);
quickSort(arr, lo, mid-1);
quickSort(arr, mid+1, hi);
}
int partition(vector<int>& arr, int le, int ri) {
int pivot = arr[le];
int i = le, j = ri;
while (i < j) { // 这里和二分while不一致,因为相同位置没必要进行交换
while (i < ri && arr[i] <= pivot) i++;
while (j > le && arr[j] >= pivot) --j;
if (i >= j) break; // 关键
swap(arr[i], arr[j]);
}
swap(arr[j], arr[le]);
return j;
}
}; - 归并排序(时间O(nlogn), 空间O(n))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29// 传递参数时需要mid
void merge(vector<int>& arr, vector<int>& aux, int lo, int mid, int hi) {
// 两个子区间的起始下标i,j
int i = lo, j = mid+1;
// 这里一定要更新aux数组,因为bottom-up时,arr会发生变化
// 保存原始arr的一个副本,最后不断更改arr达到最后的效果
for (int k = lo; k <= hi; ++k) {
aux[k] = arr[k];
}
// 双指针合并数组
for (int k = lo; k <= hi; ++k) {
// 两种越界情况
if (i > mid) arr[k] = aux[j++];
else if (j > hi) arr[k] = aux[i++];
else if (aux[i] < aux[j]) arr[k] = aux[i++];
else arr[k] = aux[j++];
}
}
void mergeSort(vector<int>& arr, vector<int>& aux, int lo, int hi) {
if (lo >= hi) {
return;
}
int mid = lo + (hi-lo)/2;
// 如果区间sort[lo, mid-1]则可能会出现下标越界!!
mergeSort(arr, aux, lo, mid);
mergeSort(arr, aux, mid+1, hi);
merge(arr, aux, lo, mid, hi); // 传入mid,需要第二个区间的起始下标
} - 堆排序(时间O(nlogn), 空间O(n))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39void heapify(vector<int>& arr, int n, int i) {
// largest记录最大值的下标
int largest = i; // 初始化最大值为根节点
int l = 2 * i + 1; // 左子节点
int r = 2 * i + 2; // 右子节点
// 如果左子节点比根节点大,则更新最大值
if (l < n && arr[l] > arr[largest]) {
largest = l;
}
// 如果右子节点比根节点大,则更新最大值
if (r < n && arr[r] > arr[largest]) {
largest = r;
}
// 如果最大值不是根节点,则交换根节点和最大值
if (largest != i) {
swap(arr[i], arr[largest]);
// 递归调用heapify函数,直到所有子树都满足最大堆的性质
heapify(arr, n, largest);
}
}
void heapSort(vector<int>& arr) {
int n = arr.size();
// 建立最大堆
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 从最后一个元素开始,依次将最大值交换到数组末尾
for (int i = n - 1; i >= 0; i--) {
swap(arr[0], arr[i]);
// 维护最大堆的性质
heapify(arr, i, 0);
}
} - 前缀和问题,比如560和为k的子数组。本质上就是这一个公式,若pre[j]-pre[i-1]=k,即表示[i..j]区间内的元素和为k。转化为pre[i-1] = pre[j]-k(满足说明出现了和为k的子数组),每次遍历时都要统计前缀和为n出现的次数mp[pre]++,然后哈希表可以统计前缀和出现的次数。初始条件为map[0]=1,想想这种情况[3,4] k=7