一. 排序算法
1. 选择排序
void selectionSort(int* arr, int n){
for(int i = 0; i < n; i++){
int minIndex = i;
for(int j = i + 1; j < n; j++)
if(arr[minIndex] > arr[j])
minIndex = j;
swap(arr[minIndex], arr[i]);
}
}
2. 插入排序
void insertionSort(int* arr, int n){
for(int i = 1; i < n; i++){
int e = arr[i];
int j;
for(j = i; j > 0; j--) {
if(arr[j - 1] > e)
arr[j] = arr[j - 1];
else:
break;
}
arr[j] = e;
}
}
3. 冒泡排序
void bubbleSort(int* arr, int n){
bool swapped;
do{
swapped = false;
for(int i = 1; i < n; i++){
if(arr[i - 1] > arr[i]){
swap(arr[i - 1], arr[i]);
swapped = true;
}
}
n--;
}while(swapped)
}
3. 归并排序
1). 自顶向下的归并排序(递归)
void mergeSort(int* arr, int n){
__mergeSort(arr, 0, n - 1);
}
void __mergeSort(int* arr, int l, int r){
//if(l >= r) return; //orig version
if(r - l <= 15){ // 优化1: 对于小规模数组, 使用插入排序
insertionSort(arr, l ,r);
return;
}
int mid = l + (r - l) / 2;
__mergeSort(arr, l, mid);
__mergeSort(arr, mid + 1, r);
// 优化2: 对于arr[mid] <= arr[mid+1]的情况,不进行merge
// 对于近乎有序的数组非常有效,但是对于一般情况,有一定的性能损失
if(arr[mid] > arr[mid + 1])
__merge(arr, l, mid, r);
}
void __merge(int* arr, int l, int mid, int r){
int aux[r - l + 1];
for(int i = l; i <= r; i++)
aux[i - l] = arr[i];
int i = l, j = mid + 1;
for(int k = l; k <= r; k++){
if(i > mid){
arr[k] = aux[j - l];
j++;
}
else if(j > r){
arr[k] = aux[i - l];
i++;
}
else if(aux[i - l] < aux[j - l]){
arr[k] = aux[i - l];
i++;
}
else{
arr[k] = aux[j - l];
j++
}
}
}
2). 自底向上的归并排序(迭代)
void mergeSortBU(int* arr, int n){
for(int sz = 1; sz < n; sz += sz)
for(int i = 0; i < n - sz; i += sz + sz)
//对arr[i...i-sz+1]和arr[i+sz+sz-1...n-1]进行merge
__merge(arr, i, i-sz+1, min(i+sz+sz-1, n-1));
}
3. 快速排序
- 原始版本以及解决几乎有序数组情况下快排将退化成O(n^2)复杂度
void quickSort(int* arr, int n){
srand(time(NULL));
__quickSort(arr, 0, n - 1);
}
void __quickSort(int* arr, int l, int r){
// 对于小规模数组, 使用插入排序进行优化
if( r - l <= 15 ){
insertionSort(arr,l,r);
return;
}
int p = __partition(arr, l ,r);
__quickSort(arr, l, p - 1);
__quickSort(arr, p + 1, r);
}
//对arr[l...r]进行partition,使得arr[l+1...j] < v, arr[j+1...i) > v
int __partition(int* arr, int l, int r){
// 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
swap(arr[l], arr[rand()%(r-l+1)+l]);
int v = arr[l];
int j = l;
for(int i = l + 1; i <= r; i++)
if(arr[i] < v)
swap(arr[++j], arr[i]);
swap(arr[l], arr[j]);
return j;
}
- 三路快排:解决数组中存在大量重复元素的问题
void quickSort(int* arr, int n){
srand(time(NULL));
__quickSort3Ways(arr, 0, n - 1);
}
void __quickSort3Ways(int* arr, int l, int r){
if(r - l <= 15){
insertionSort(arr, l, r);
return;
}
//partition: arr[l+1...lt]<v, arr[lt+1...i)==v, arr[gt...r]>v
swap(arr[l], arr[rand()%(r-l+1)+l]);
int v = arr[l];
int lt = l, gt = r + 1, i = l + 1;
while(i < gt){
if(arr[i] < v)
swap(arr[++lt], arr[i++]);
else if(arr[i] > v)
swap(arr[--gt], arr[i]);
else // arr[i] == v
i++;
}
swap(arr[l], arr[lt]);
__quickSort3Ways(arr, l, lt - 1);
__quickSort3Ways(arr, gt, r);
}
4. 堆排序(原地堆排序)
void shiftDown(int* arr, int n, int k){
int e = arr[k];
while(2*k+1 < n){
int j = 2 * k + 1;
if(j + 1 < n && arr[j] < arr[j + 1])
j += 1;
if(e >= arr[j])
break;
arr[k] = arr[j];
k = j;
}
arr[k] = e;
}
void heapSort(int* arr, int n){
// heapify
for(int i = (n - 1) / 2; i >= 0; i--)
shiftDown(arr, n, i);
// sort
for(int i = n - 1; i > 0; i--){
swap(arr[0], arr[i]);
shiftDown(arr, i, 0);
}
}
4. 排序算法的总结
- 空间复杂度
- 稳定性
二. 查找算法
1. 二分查找法(数组必须有序)
int binarySearch(int arr[], int n, int target){
int l = 0, r = n - 1; //在arr[l...r]内寻找target
while(l <= r){
int mid = l + (r - l) / 2;
if(target == arr[mid])
return mid;
if(target < arr[mid]) //在arr[l...mid - 1]内寻找target
r = mid - 1;
else // target > arr[mid] //在arr[mid + 1...r]内寻找target
l = mid + 1;
}
return -1;
}
2. 双索引(对撞指针和滑动窗口)
2.1 Leetcode 167:Two Sum II
给定一个有序整型数组和一个整数target,在其中寻找两个元素,使其和为target。返回这两个数的索引。
例如:numbers=[2,7,11,15],target=9
答案:返回数字2,7的索引1,2(索引从1开始计算)
解法一(暴力解法):遍历两遍数组,时间复杂度O(n^2),空间复杂度O(1).
解法二(利用二分查找法):遍历一遍数组,当下标指到索引i时,在 numbers[i+1…n-1] 利用二分查找法寻找target-numbers[i]. 时间复杂度O(nlog(n)),空间复杂度O(1).
解法三(对撞指针):遍历一遍数组,设置两个索引指针i, j,若numbers[i] + numbers[j] > target,那么j- -;若numbers[i] + numbers[j] < target,那么i++. 时间复杂度O(n),空间复杂度O(1).
class Solution{
public:
vector<int> twoSum(vector<int>& numbers, int target){
assert(numbers.size() >= 2);
int l = 0, r = numbers.size() - 1; //在numbers[l...r]寻找寻找两个元素,使其和为target
while(l < r){ //为啥不是l<=r?因为要寻找两个元素,如果l==r,就只剩一个元素
if(numbers[l] + numbers[r] == target){
int res[2] = {l + 1, r + 1} //索引从1开始计算
return vector<int>(res, res + 2);
}
else if(numbers[l] + numbers[r] < target)
l++;
else
r--;
}
throw invalid_argument("The input has no solution.");
}
}
2.2 Leetcode 125:Valid Palindrome
给定一个字符串,只看其中的数字和字母,忽略大小写,判断这个字符串是否为回文串。
例如:
“A man, a plan, a canal: Panama”,返回true.
“race a car”,返回false.
解法(对撞指针):遍历一遍字符串,判断索引指针i与s.length()-1-i对应的字符是否相等,若不等,则返回false;否则返回true。时间复杂度O(n),空间复杂度O(1).
class Solution
{
public:
bool isPalindrome(string s)
{
s.erase(remove_if(s.begin(), s.end(), static_cast<int(*)(int)>(&ispunct)), s.end()); //去掉特殊字符
s.erase(remove_if(s.begin(), s.end(), static_cast<int(*)(int)>(&isspace)), s.end()); //去掉空格
transform(s.begin(), s.end(), s.begin(), ::toupper); //将所有字符转化为大写字符
for (int i = 0; i < (s.length() / 2); i++)
{
if (s[i] != s[s.length() - i - 1])
return false;
}
return true;
}
};
2.3 Leetcode 11:container-with-most-water
给出一个非负整数a1,a2,a3,…,an;每一个整数表示一个竖立在坐标轴x位置的一堵高度为ai的“墙”,选择两堵墙,和x轴构成的容器可以容纳最多的水。
例如:
输入[1,8,6,2,5,4,8,3,7],则返回49
解法一(暴力解法):遍历两遍数组,计算可以承载的水量。然后不断更新最大值,最后返回最大值即可。时间复杂度O(n^2),空间复杂度O(1).
class Solution {
public:
int maxArea(vector<int>& height) {
int res = 0;
for(int i = 0; i < height.size(); i++)
for(int j = i + 1; j < height.size(); j++)
res = max(res, abs(i - j) * min(height[i], height[j]));
return res;
}
};
解法二(对撞指针):遍历一遍数组,设立两个索引指针i,j,并计算当前面积;然后移动height[i]与height[j]较小的索引。时间复杂度O(n),空间复杂度O(1).
class Solution {
public:
int maxArea(vector<int>& height) {
int res = 0;
int i = 0, j = height.size() - 1; //计算height[i...j]的面积
while(i < j){ //为什么不是i<=r?因为面积至少是由两个边组成的
res = max(res, abs(i - j) * min(height[i], height[j]));
if(height[i] < height[j])
++i;
else
--j;
}
return res;
}
};
2.4 Leetcode 209:Minimum Size Subarray Sum
给定一个整形数组和一个数字s,找到数组中最短的一个连续子数组,使得连续子数组的数字和sum>=s,返回这个最短连续子数组的长度值。
例如:s = 7, nums = [2,3,1,2,4,3],则返回2
解法一(暴力解法):遍历所有的连续子数组[i…j],计算其和sum。验证sum>=s。时间复杂度O(n^2),空间复杂度O(1).
class Solution
{
public:
int minSubArrayLen(int s, vector<int>& nums){
int res = nums.size() + 1;
for(int i = 0; i < nums.size(); i++){
int sum = 0;
for(int j = i; j < nums.size(); j++){
sum += nums[j];
if(sum >= s){
res = min(res, j - i + 1);
break;
}
}
}
return res != nums.size() + 1 ? res : 0;
}
};
解法二(滑动窗口):设置两个索引指针l,r,计算连续子数组[l…r]的sum,若sum < s,sum += nums[++r];;否则sum -= nums[l++]。如果sum>=s,则计算该连续子数组的长度。时间复杂度O(n),空间复杂度O(1).
class Solution{
public:
int minSubArrayLen(int s, vector<int>& nums){
int res = nums.size() + 1;
int l = 0, r = -1; // sliding window: nums[l...r]
int sum = 0;
while(l < nums.size()){
if(r + 1 < num.size() && sum < s)
sum += nums[++r];
else
sum -= nums[l++];
if(sum >= s)
res = min(res, r - l + 1);
}
return res != nums.size() + 1 ? res : 0;
}
};
2.4 Leetcode 3:Longest Substring Without Repeating Characters
在一个字符串中寻找没有重复字母的最长子串。
例如:
输入"abcabcbb",子串是"abc",那么返回3
输入"“bbbbb”“,子串是"b”,那么返回1
输入"pwwkew",子串是"wke",那么返回3
解法(滑动窗口):设置两个索引l,r,假定s[l…r]是当前含有不重复字母的最长子串,并用数组char_freq来记录每个字符是否已经出现过。如果s[l…r+1]不重复,那么将r指针右移,并在char_freq标定该字符已经出现;否则将l指针右移,并在char_freq标定该字符已经移除。时间复杂度O(n),空间复杂度O(1).
class Solution{
public:
int lengthOfLongestSubstring(string s) {
int char_freq[256] = { 0 }; // record the frequency of each character appears
int l = 0, r = -1; //slide window: s[l...r]
int res = 0;
while (l < s.length()){
if (r + 1 < s.length() && char_freq[s[r + 1]] == 0)
char_freq[s[++r]]++;
else
char_freq[s[l++]]--;
res = max(res , r - l + 1);
}
return res;
}
};
3. 设置查找表(set和map)
3.1 Leetcode 349:Intersection of Two Arrays
给定两个数组,实现它们的交集。
例如:
nums1 = [1,2,2,1], nums2 = [2,2],返回[2]
nums1 = [4,9,5], nums2 = [9,4,9,8,4], 返回[9,4]
解法(设置set):先将nums1中的元素记录到一个recordSet中,然后遍历nums2中的元素,若该元素存在于recordSet中,则记录到另一个resultSet中。最后将resultSet中元素放入到vector中并返回。时间复杂度O(n),空间复杂度O(n).
class Solution{
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> record(nums1.begin(), nums1.end());
unordered_set<int> result_set;
for (int i = 0; i < nums2.size(); i++)
if (record.find(nums2[i]) != record.end())
result_set.insert(nums2[i]);
return vector<int>(result_set.begin(), result_set.end());
}
};
3.2 Leetcode 350:Intersection of Two Arrays
给定两个数组,实现它们的交集。
例如:
nums1 = [1,2,2,1], nums2 = [2,2],返回[2,2]
解法(设置set):先将nums1中每个元素出现的频次记录到一个recordMap中,然后遍历nums2中的元素,若该元素在recordMap的频次大于0,则记录到resultVector中,最终返回。时间复杂度O(n),空间复杂度O(n).
class Solution{
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2){
unordered_map<int, int> recordMap;
for (int i = 0; i < nums1.size(); i++)
recordMap[nums1[i]]++;
vector<int> resultVector;
for (int i = 0; i < nums2.size(); i++){
if (recordMap[nums2[i]] > 0){
resultVector.push_back(nums2[i]);
recordMap[nums2[i]]--;
}
}
return resultVector;
}
};
3.2 Leetcode 1:Two Sum
给定一个整型数组和一个整数target,在其中寻找两个元素,使其和为target。返回这两个数的索引。
例如:numbers=[2,7,11,15],target=9
答案:返回数字2,7的索引1,2(索引从1开始计算)
解法一(暴力解法):遍历两遍数组,时间复杂度O(n^2),空间复杂度O(1).
解法二(利用二分查找法):先进行排序,当下标指到索引i时,在 numbers[i+1…n-1] 利用二分查找法寻找target-numbers[i]. 时间复杂度O(nlog(n)),空间复杂度O(1).
解法三(对撞指针):先进行排序,然后设置两个索引指针i, j,若numbers[i] + numbers[j] > target,那么j- -;若numbers[i] + numbers[j] < target,那么i++. 时间复杂度O(nlog(n)),空间复杂度O(1).
解法四(设置查找表map):对于每个元素a,在查找表中查找target-a是否存在。若存在,则返回;不存在,则将a之前所有的元素放入查找表。时间复杂度O(n),空间复杂度O(n).
class Solution{
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> record;
for (int i = 0; i < nums.size(); i++){
int res_num = target - nums[i];
if (record.find(res_num) != record.end())
return vector < int > {record[res_num], i};
record[nums[i]] = i;
}
throw invalid_argument("The input has no solution");
}
};
3.3 Leetcode 454:Four Sum
给出四个整型数组A,B,C,D,寻找有多少个i,j,k,l的组合,使得A[i]+B[j]+C[k]+D[l]==0。其中。A,B,C,D均含有相同的元素个数N,且0<=N<=500.
解法一(暴力解法):遍历四遍数组,时间复杂度O(n^4),空间复杂度O(1).
解法二(将D中的元素放入查找表):遍历三遍数组,时间复杂度O(n^3),空间复杂度O(n).
解法三(将C+D中的每一种可能放入查找表map):分别遍历两遍数组,时间复杂度O(n^2),空间复杂度O(n ^ 2).
class Solution{
public:
int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
unordered_map<int, int> record;
for (int i = 0; i < A.size(); i++)
for (int j = 0; j < B.size(); j++)
record[A[i] + B[j]]++;
int res = 0;
for (int i = 0; i < C.size(); i++)
for (int j = 0; j < D.size(); j++)
if (record.find(-C[i]-D[j]) != record.end())
res += record[-C[i] - D[j]];
return res;
}
};
3.4 Leetcode 447:Number of Boomeranges
给出一个平面上的n个点,寻找存在多少个由这些点构成的三元组(i,j,k),使得i,j两点的距离等于i,k两点的距离。其中n最大为500,且所有点的坐标范围在[-10000,10000]之间。
解法一(暴力解法):枚举所有可能的三元组,查看其是否满足题目要求。时间复杂度O(n^3),空间复杂度O(1).
解法二(设置查找表map):观察到i是一个枢纽,对于每个点i,遍历其余点到i的距离。时间复杂度O(n^2),空间复杂度O(n).
class Solution {
public:
int numberOfBoomerangs(vector<pair<int, int>>& points){
unordered_map<int, int> record;
int res = 0;
for (int i = 0; i < points.size(); i++){
record.clear();
for (int j = 0; j < points.size(); j++){
if (j != i)
record[dis(points[i], points[j])]++;
}
for (auto ite = record.begin(); ite != record.end(); ite++)
res += ite->second * (ite->second - 1);
}
return res;
}
private:
int dis(const pair<int, int>& p1, const pair<int, int>& p2){
return (p1.first - p2.first) * (p1.first - p2.first) +
(p1.second - p2.second) * (p1.second - p2.second);
}
};
3.5 Leetcode 219:Contains Duplicate II
给出一个整形数组nums和一个整数k,是否存在索引i和j,使得nums[i]==nums[j]且i和j之间的差不超过k。
解法一(暴力解法):枚举所有可能i和j,查看其是否满足题目要求。时间复杂度O(n^2),空间复杂度O(1).
解法二(滑动窗口+查找表set):对于任意一个元素索引l+k+1,在l+1和l+k区间内查找是否包含有重复元素。时间复杂度O(n),空间复杂度O(k).
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k){
unordered_set<int> record;
for (int i = 0; i < nums.size(); i++){
if (record.find(nums[i]) != record.end())
return true;
record.insert(nums[i]);
if (record.size() == k + 1)
record.erase(nums[i - k]);
}
return false;
}
};
解法三(设置查找表map):记录每个nums[i]的索引i,在查找的同时判断索引差是否满足题意。时间复杂度O(n),空间复杂度O(n).
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k){
unordered_map<int, int> record;
for (int i = 0; i < nums.size(); i++){
if (record.find(nums[i]) != record.end() && i - record[nums[i]] <= k)
return true;
record[nums[i]] = i;
}
return false;
}
};
3.6 Leetcode 220:Contains Duplicate III
给出一个整形数组nums,是否存在索引i和j,使得nums[i]和nums[j]之间的差不超过给定的整数t,且i和j之间的差不超过给定的整数k。
解法一(暴力解法):枚举所有可能的i和j,查看其是否满足题目要求。时间复杂度O(n^2),空间复杂度O(1).
解法二(滑动窗口+查找表set(有序)):对于任意一个元素索引l+k+1,在l+1和l+k区间内查找某个索引v是否满足abs(nums[v]-nums[l+k+1])<=t。时间复杂度O(n),空间复杂度O(k).
解析:对于|x-a|<=t,可转化成a-t<=v<=a+t,进一步只要满足ceil(a-t)<=a+t或者a-t<=floor(a+t)即可。
class Solution{
public:
bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t){
set<long long> record;
for (int i = 0; i < nums.size(); i++){
if (record.lower_bound((long long)nums[i] - (long long)t) != record.end() &&
*record.lower_bound((long long)nums[i] - (long long)t) <= (long long)nums[i] + (long long)t)
return true;
record.insert(nums[i]);
if (record.size() == k + 1)
record.erase(nums[i - k]);
}
return false;
}
};
4. 栈和队列
4.1 Leetcode 20:Valid Parentheses
给定一个字符串,只包含(,[,{,),],},判定字符串中的括号匹配是否合法。
例如:
“()”,“()[]{}“是合法的;”(]”,"([)]"是非法的。
解法(设置stack):如果当前字符为左半边括号时,则将其压入栈中。如果遇到右半边括号时,分类讨论:1)如栈不为空且为对应的左半边括号,则取出栈顶元素,继续循环;2)若此时栈为空,则直接返回false;3)若不为对应的左半边括号,反之返回false
class Solution{
public:
bool isValid(string s) {
stack<char> record;
for (int i = 0; i < s.size(); i++){
if (s[i] == '(' || s[i] == '[' || s[i] == '{')
record.push(s[i]);
else{
if (record.size() == 0)
return false;
char left = record.top();
record.pop();
char right;
switch (s[i]){
case ')':
right = '(';
break;
case ']':
right = '[';
break;
default:
assert(s[i] == '}');
right = '{';
break;
}
if (left != right)
return false;
}
}
if (record.size() != 0)
return false;
return true;
}
};
4.2 Leetcode 150:Evaluate Reverse Polish Notation
逆波兰表达式求值。给定一个数组,表示一个逆波兰表达式,求其值。
例如:
输入[“2”, “1”, “+”, “3”, “*”],输出9,分析:((2 + 1) * 3) = 9
输入[“4”, “13”, “5”, “/”, “+”],输出6,分析:(4 + (13 / 5)) = 6
解法(设置stack):逆波兰表达式又叫做后缀表达式。在通常的表达式中,二元运算符总是置于与之相关的两个运算对象之间,这种表示法也称为中缀表示。波兰逻辑学家J.Lukasiewicz于1929年提出了另一种表示表达式的方法,按此方法,每一运算符都置于其运算对象之后,故称为后缀表示
class Solution{
public:
int evalRPN(vector<string>& tokens) {
stack<string> record;
for (int i = 0; i < tokens.size(); i++){
if (tokens[i] == "+" || tokens[i] == "-" || tokens[i] == "*" || tokens[i] == "/"){
int a = atoi(record.top().c_str());
record.pop();
int b = atoi(record.top().c_str());
record.pop();
int result;
char p = tokens[i].c_str()[0];
switch (p)
{
case '+':
result = b + a;
break;
case '-':
result = b - a;
break;
case '*':
result = b * a;
break;
default:
assert(p == '/');
result = b / a;
break;
}
record.push(to_string(result));
}
else
record.push(tokens[i]);
}
return atoi(record.top().c_str());
}
};
5. 图
5.1 Leetcode 279:Perfect Squares
给出一个正整数n,寻找最少的完全平方数,使他们的和为n。
完全平方数:1,4,9,16,…
例如:
12=4+4+4;13=4+9
解法一(图的广度优先遍历):对问题建模:整个问题转化为一个图论问题。从n到0,每个数字代表一个结点;如果两个数字x到y相差一个完全平方数,则连接一条边。那么我们得到了一个无权图。原问题就转化成,求这个无权图中从n到0的最短路径。
class Solution {
public:
// 解决方法: 用了图的广度优先遍历
int numSquares(int n) {
queue<pair<int, int>> q;
q.push(make_pair(n, 0));
//解决大量的重复结点
vector<bool> visited(n + 1, false);
visited[n] = true;
while (!q.empty()){
int num = q.front().first;
int step = q.front().second;
q.pop();
for (int i = 1; ; i++){
int result = num - i * i;
if (result < 0)
break;
if (result == 0)
return step + 1;
if (!visited[result]){
q.push(make_pair(result, step + 1));
visited[result] = true;
}
}
}
throw invalid_argument("No Solution.");
}
};
解法二(递归回溯):见后面章节。
解法三(动态规划):见后面章节。
5.2 Leetcode 127. Word Ladder
给出两个单词(beginWord和endWord),以及一个单词列表,寻找一条从beginWord到endWord的最短变换路径。每次变换只能修改单词的一个字母。
例如:
解法(图的广度优先遍历):对问题建模:整个问题转化为一个图论问题。单词列表中每个字符串代表一个结点;如果两个字符串x和y只有一个字母不同,则连接一条边。那么我们得到了一个无权图。原问题就转化成,求这个无权图中从beginWord到endWord的最短路径。
class Solution{
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
//判断单词列表中是否有endWord
bool has_endWords = false;
for (int i = 0; i < wordList.size(); i++)
if (wordList[i] == endWord)
has_endWords = true;
if (!has_endWords)
return 0;
//运用图的广度优先遍历
queue<pair<string, int>> q;
q.push(make_pair(beginWord, 1));
vector<bool> visited(wordList.size(), false);
while (!q.empty()){
string changeWord = q.front().first;
int step = q.front().second;
q.pop();
if (changeWord == endWord)
return step;
for (int i = 0; i < wordList.size(); i++){
bool canTransform = isTransform(changeWord, wordList[i]);
if (canTransform && !visited[i]){
q.push(make_pair(wordList[i], step + 1));
visited[i] = true;
}
}
}
return 0;
}
private:
//判断两个字符串是否只有一个字母不同
bool isTransform(string& str1, string& str2){
assert(str1.size() == str2.size());
int num = 0;
for (int i = 0; i < str1.size(); i++)
if (str1[i] != str2[i])
num++;
if (num == 1)
return true;
return false;
}
};
5.3 Leetcode 127. Word Ladder II
给出两个单词(beginWord和endWord),以及一个单词列表,返回所有从beginWord到endWord的变换路径。每次变换只能修改单词的一个字母。
例如:
解法(图的广度优先遍历):对问题建模:整个问题转化为一个图论问题。单词列表中每个字符串代表一个结点;如果两个字符串x和y只有一个字母不同,则连接一条边。那么我们得到了一个无权图。原问题就转化成,求这个无权图中从beginWord到endWord的所有路径。
class Solution {
private:
unordered_set<string> wordList;
vector<vector<string>> ans;
unordered_set<string> visited;
int level = 1;
int minLevel = INT_MAX;
public:
vector<vector<string>> findLadders(string beginWord, string endWord, vector<string> &words) {
//Putting all words in a set
for (auto word : words)
wordList.insert(word);
//Queue of Paths
queue<vector<string>> q;
q.push({ beginWord });
while (!q.empty()){
vector<string> path = q.front();
q.pop();
if (path.size() > level){
//reach a new level
for (string w : visited)
wordList.erase(w);
if (path.size() > minLevel)
break;
else
level = path.size();
}
string lastWord = path.back();
addNeighboursToQ(lastWord, path, q, endWord);
}
return ans;
}
private:
void addNeighboursToQ(string curr, vector<string> path, queue<vector<string>> &q, const string &endWord){
for (int i = 0; i < curr.size(); i++){
char originalChar = curr[i];
for (int j = 0; j < 26; j++){
curr[i] = j + 'a';
if (wordList.find(curr) != wordList.end()){
vector<string> newpath = path;
newpath.push_back(curr);
visited.insert(curr);
if (curr == endWord) {
minLevel = level;
ans.push_back(newpath);
}
else
q.push(newpath);
}
}
curr[i] = originalChar;
}
}
};
6. 优先队列(C++默认最大堆)
6.1 Leetcode 347:Top K Frequent Elements
给定一个非空数组,返回前k个出现频率最高的元素。
例如:
给定nums = [1,1,1,2,2,3], k = 2,返回[1,2]
解法一(map + sort):先使用map统计每个元素出现的频次,然后对频次进行排序,并选择前k个出现频次最高的元素。时间复杂度:O(nlogn),空间复杂度O(n).
解法二(map + 优先队列):维护一个含有k个元素的优先队列。如果遍历到的元素比队列中最小频率元素的频率还要高,则取出队列中最小频率的元素,将新元素入队。最终,队列中剩下的就是前k个出现频率最高的元素。时间复杂度:O(nlogk),空间复杂度O(k).
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
assert(k > 0);
//统计每个元素出现的频率
unordered_map<int, int> freq;
for (int i = 0; i < nums.size(); i++)
freq[nums[i]]++;
assert(k <= nums.size());
// 设置一个最小优先队列,等push满k个元素后,进入一个新的元素
// 判断新元素的频率与优先队列最小频率相比,若大于最小频率,则
// pop掉最小频率的元素,push进新元素和它的频率
// 优先队列是以 (频率,元素) 的形式声明的
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
for (auto ite = freq.begin(); ite != freq.end(); ite++){
if (pq.size() == k){
if (ite->second > pq.top().first){
pq.pop();
pq.push(make_pair(ite->second, ite->first));
}
}
else
pq.push(make_pair(ite->second, ite->first));
}
vector<int> res;
while (!pq.empty()){
res.push_back(pq.top().second);
pq.pop();
}
return res;
}
};
6.2 Leetcode 23:Merge k Sorted Lists
给定k个有序链表,将他们归并为一个有序链表。
例如:
解法(优先队列):首先将vector中每个链表的头结点push进一个最小堆中,然后依次取出优先队列中val值最小的那个结点,并将它加入要返回的有序链表中。然后判断该结点的next指针是否为空,若不为空,则继续push进队列中。
bool myCmp(ListNode* node1, ListNode* node2){
return node1->val > node2->val;
}
class Solution{
public:
ListNode* mergeKLists(vector<ListNode*>& lists)
{
priority_queue<ListNode*, vector<ListNode*>, function<bool(ListNode*, ListNode*)>> pq(myCmp);
ListNode* dummyHead= new ListNode(0);
ListNode* curNode = dummyHead;
for (int i = 0; i < lists.size(); i++){
if (lists[i])
pq.push(lists[i]);
}
while (!pq.empty()){
auto insertNode = pq.top();
pq.pop();
curNode->next = insertNode;
curNode = curNode->next;
if (insertNode->next)
pq.push(insertNode->next);
}
return dummyHead->next;
}
};
7. 递归与回溯
7.1 一维平面上的递归回溯
7.1.1 Leetcode 17:Letter Combinations of a Phone Number
给出一个数字字符串,返回这个数字字符串所能表示的所有字母组合。
例如:
输入:“23”, 输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”]
解法(暴力解法):该问题其实是一个树形结构问题,如下图所示:
因此可以用递归求解,时间复杂度O(n^2),思路如下:
class Solution {
private:
static const string letter_map[10];
vector<string> res;
//s中保存了此时从digits[0...index-1]翻译得到的一个字母字符串
//寻找和digits[index]匹配的字母,获得digits[0...index]翻译得到的解
void findCombinations(const string& digits, int index, const string& s){
if (index == digits.size()){
res.push_back(s);
return;
}
char c = digits[index];
assert(c >= '0' && c <= '9' && c != '1');
string letters = letter_map[c - '0'];
for (int i = 0; i < letters.size(); i++)
findCombinations(digits, index + 1, s + letters[i]);
return;
}
public:
vector<string> letterCombinations(string digits){
if (digits == "")
return res;
findCombinations(digits, 0, "");
return res;
}
};
const string Solution::letter_map[10] = { " ", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz" // 9
};
7.1.2 Leetcode 46:Permutations
给定一个整形数组,其中每一个元素各不相同,返回这些元素所有排列的可能。
例如:
输入[1,2,3],返回[[1,2,3], [1,3,2],[2,1,3], [2,3,1], [3,1,2], [3,2,1]]
解法:和上题一样要求出所有结果,因此该问题也是一个递归回溯问题,思路如下:
用公式表示如下:
class Solution {
private:
vector<vector<int>> res;
vector<bool> used;
//p中保存了一个有index个元素的排列
//向这个排列的末尾添加第index+1个元素,获得一个有index+1个元素的排列
void generatePermutation(const vector<int>& nums, int index, vector<int>& p){
if (index == nums.size()){
res.push_back(p);
return;
}
for (int i = 0; i < nums.size(); i++){
if (!used[i]){
p.push_back(nums[i]);
used[i] = true;
generatePermutation(nums, index + 1, p);
p.pop_back(); //回溯的本质(这两行代码特别重要)
used[i] = false;
}
}
return;
}
public:
vector<vector<int>> permute(vector<int>& nums) {
if (nums.size() == 0)
return res;
used = vector<bool>(nums.size(), false);
vector<int> p;
generatePermutation(nums, 0, p);
return res;
}
};
7.1.3 Leetcode 47:Permutations II
给定一个整形数组,其中可能有相同的元素,返回这些元素所有排列的可能。
例如:
输入[1,1,2],返回[[1,1,2],[1,2,1],[2,1,1]]
解法:思路和上题一样,只不过要处理重复元素的问题,因此判断复杂了一点。
class Solution{
private:
vector<vector<int>> res;
vector<bool> used;
void generatePermutation(const vector<int>& num, int index, vector<int>& p){
if (index == num.size()){
res.push_back(p);
return;
}
for (int i = 0; i < num.size(); i++){
if (used[i] || (i > 0 && num[i] == num[i - 1] && !used[i - 1]))
continue;
p.push_back(num[i]);
used[i] = true;
generatePermutation(num, index + 1, p);
p.pop_back();
used[i] = false;
}
}
public:
vector<vector<int> > permuteUnique(vector<int> &num){
if (num.size() == 0)
return res;
sort(num.begin(), num.end()); //需要先排序
used = vector<bool>(num.size(), false);
vector<int> p;
generatePermutation(num, 0, p);
return res;
}
};
7.1.4 Leetcode 77:Combinations
给定两个整数n和k,求在1…n这n个数字中选出k个数字的所有组合。
例如:
n=4,k=2,结果为[[2,4],[3,4],[2,3],[1,2],[1,3],[1,4],]
解法:组合问题仍然可以视为一个树形结构,所以可以用递归回溯解决。思路如下:
class Solution {
private:
vector<vector<int>> res;
//求解C(n,k),当前已经找到的组合存储在c中,需要从start开始搜索新的元素
void generateCombination(int n, int k, int start, vector<int>& c){
if (c.size() == k){
res.push_back(c);
return;
}
//最初版写的是i<=n,但是这样会遍历完上图中所有的分支
//优化:递归回溯的剪枝:下面的写法其实减掉了上图“取4”的分支。
//对c来说,还有k-c.size()个空位,所以[i...n]中至少有k-c.size()个元素
//所以有不等式n-i+1>=k-c.size(),即i<=n-(k-c.size())+1
for (int i = start; i <= n - (k - c.size()) + 1; i++){
c.push_back(i);
generateCombination(n, k, i + 1, c);
c.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k){
if (n < 0 || k < 0 || k > n)
return res;
vector<int> c;
generateCombination(n, k, 1, c);
return res;
}
};
7.1.5 Leetcode 39:Combination Sum
给出一个集合,其中所有的元素各不相同,以及一个数字T。寻找所有该集合中的元素组合,使得组合中所有的元素和为T。(注意: 集合中每一个元素可以使用多次。)
例如:
给定集合nums=[2,3,6,7],T=7,结果为[[7],[2,2,3]]
解法 递归回溯。
class Solution{
private:
vector<vector<int>> res;
void computeCombinationSum(vector<int>& candidates, int target, int index, vector<int>& c){
if (target == 0){
res.push_back(c);
return;
}
for (int i = index; i < candidates.size(); i++){
if(target < candidates[i])
continue;
c.push_back(candidates[i]);
computeCombinationSum(candidates, target - candidates[i], i, c);
c.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target){
if (candidates.size() == 0)
return res;
sort(candidates.begin(), candidates.end());
vector<int> c;
computeCombinationSum(candidates, target, 0, c);
return res;
}
};
7.1.6 Leetcode 40:Combination Sum II
给出一个集合,其中元素可能相同,以及一个数字T。寻找所有该集合中的元素组合,使得组合中所有的元素和为T。(注意: 集合中每一个元素只可以使用一次。)
例如:
给定集合nums=[10,1,2,7,6,1,5],T=8,结果为[ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6]]
解法 递归回溯。
class Solution{
private:
vector<vector<int>> res;
void computeCombinationSum(vector<int>& candidates, int target, int index, vector<int>& c){
if (target == 0){
res.push_back(c);
return;
}
for (int i = index; i < candidates.size(); i++){
if (target < candidates[i] || (i > index && candidates[i] == candidates[i - 1]))
continue;
c.push_back(candidates[i]);
computeCombinationSum(candidates, target - candidates[i], i + 1, c);
c.pop_back();
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target){
if (candidates.size() == 0)
return res;
sort(candidates.begin(), candidates.end());
vector<int> c;
computeCombinationSum(candidates, target, 0, c);
return res;
}
};
7.1.7 Leetcode 78:Subsets
给出一个集合,其中所有元素各不相同,求出该集合的所有子集。
例如:
给定集合nums=[1,2,3],结果为[ [3],[1],[2],[1,2,3],[1,3],[2,3],[1,2],[]]
解法 递归回溯。
class Solution {
private:
vector<vector<int>> res;
void findSubsets(vector<int>& nums, int index, vector<int>& s){
if (s.size() > 0 && s.size() <= nums.size())
res.push_back(s);
if (res.size() == pow(2, nums.size()) - 1) // 一个集合的子集个数等于2^n,减去1是为了除掉空集
return;
for (int i = index; i < nums.size(); i++){
s.push_back(nums[i]);
findSubsets(nums, i + 1, s);
s.pop_back();
}
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
if (nums.empty())
return res;
vector<int> s;
findSubsets(nums, 0, s);
res.push_back(vector<int>());
return res;
}
};
7.1.8 Leetcode 90:Subsets II
给出一个集合,其中所有元素可能相同,求出该集合的所有子集。
例如:
给定集合nums=[1,2,2],结果为[ [1],[2],[1,2],[2,2],[1,2,2],[]]
解法 递归回溯。
class Solution{
private:
vector<vector<int>> res;
void findSubsets(vector<int>& nums, int index, vector<int>& s){
if (s.size() > 0 && s.size() <= nums.size())
res.push_back(s);
if (res.size() == pow(2, nums.size()) - 1) // 一个集合的子集个数等于2^n,减去1是为了除掉空集
return;
for (int i = index; i < nums.size(); i++){
if (i > index && nums[i] == nums[i - 1])
continue;
s.push_back(nums[i]);
findSubsets(nums, i + 1, s);
s.pop_back();
}
}
public:
//套路和Permutations II一样,使用排序来剔除相同子集
vector<vector<int>> subsetsWithDup(vector<int>& nums){
if (nums.empty())
return res;
sort(nums.begin(), nums.end());
vector<int> s;
findSubsets(nums, 0, s);
res.push_back(vector<int>());
return res;
}
};
7.2 二维平面上的递归回溯
7.2.1 Leetcode 79:Word Search
给定一个二维平面上的字母和一个单词,看是否可以在这个二维平面上找到该单词。其中找到这个单词的规则是,从一个字母出发,可以横向或者纵向连接二维平面上的其它字母。同一个位置上的字母只能使用一次。
例如:
解法: 该问题其实是一个二维平面的上递归回溯。对于每一个元素,从上下左右四个方向分别匹配,如果匹配,则寻找下一个元素,不匹配,则进行回溯。
递归树如下:
class Solution{
private:
int h, w;
vector<vector<int>> d;
vector<vector<bool>> visited;
bool inArea(int x, int y){
return (x >= 0 && x < h) && (y >= 0 && y < w);
}
//从board[start_x,start_y]开始,寻找word[index...word.size()-1]
bool searchWord(const vector<vector<char>>& board, const string word, int index, int start_x, int start_y){
if (index == word.size() - 1)
return board[start_x][start_y] == word[index];
if (board[start_x][start_y] == word[index]){
visited[start_x][start_y] = true;
// loop for four directions(up, right, down, left), respectively
for (int i = 0; i < 4; i++){
int new_x = start_x + d[i][0];
int new_y = start_y + d[i][1];
// it must dertermine this point is not out of range, visited and can search it in board in each search time.
if (inArea(new_x, new_y) && !visited[new_x][new_y] && searchWord(board, word, index + 1, new_x, new_y))
return true;
}
visited[start_x][start_y] = false;
}
return false;
}
public:
bool exist(vector<vector<char>>& board, string word) {
h = board.size();
assert(h > 0);
w = board[0].size();
d = vector < vector<int> > {{-1, 0}, { 0, 1 }, { 1, 0 }, { 0, -1 }};
visited = vector<vector<bool>>(h, vector<bool>(w, false));
for (int i = 0; i < h; i++)
for (int j = 0; j < w; j++)
if (searchWord(board, word, 0, i, j))
return true;
return false;
}
};
7.2.2 Leetcode 200:Numbers of Islands
给定一个二维数组,只含有0和1两个字符。其中1代表陆地。0代表水域。横向和纵向的陆地连接成岛屿,被水域分隔开。问给出的地图中有多少岛屿?
例如:
解法: floodfill算法:从初始点做深度优先遍历,标记所有陆地点。
class Solution {
private:
int h, w;
int res;
vector<vector<int>> d;
vector<vector<bool>> visited;
bool inArea(const int x, const int y){
return (x >= 0 && x < h) && (y >= 0 && y < w);
}
// 从grid[start_x][start_y]的位置开始,进行floodfill
//保证(x,y)合法,且grid[x][y]是没有被访问过的陆地
void dfs(const vector<vector<char>>& grid, int start_x, int start_y){
visited[start_x][start_y] = true;
for (int i = 0; i < 4; i++){
int new_x = start_x + d[i][0];
int new_y = start_y + d[i][1];
if (inArea(new_x, new_y) && !visited[new_x][new_y] && grid[new_x][new_y] == '1')
dfs(grid, new_x, new_y);
}
}
public:
int numIslands(vector<vector<char>>& grid){
res = 0;
h = grid.size();
if (h == 0)
return res;
w = grid[0].size();
d = vector < vector<int> > {{-1, 0}, { 0, 1 }, { 1, 0 }, { 0, -1 }};
visited = vector<vector<bool>>(h, vector<bool>(w, false));
for (int i = 0; i < h; i++)
for (int j = 0; j < w; j++)
if (grid[i][j] == '1' && !visited[i][j]){
res++;
dfs(grid, i, j);
}
return res;
}
};
7.2.3 Leetcode 51:N-Queens
例如:
解法: 递归回溯。思路如下:
然后判断位置是否合法,若不合法,则进行剪枝:
判断不合法的思路:
class Solution
{
private:
vector<vector<string>> res;
vector<bool> cols, diag1, diag2;
//尝试在一个n皇后问题中,摆放第index行的皇后位置
void putQueen(const int n, int index, vector<int>& row){
if (index == n){
res.push_back(generateBoard(n, row));
return;
}
for (int i = 0; i < n; i++){
if (!cols[i] && !diag1[index + i] && !diag2[index - i + n - 1]){
row.push_back(i);
cols[i] = true;
diag1[index + i] = true; //对角线1:i+j
diag2[index - i + n - 1] = true; //对角线2:i-j+n-1
putQueen(n, index + 1, row);
cols[i] = false;
diag1[index + i] = false;
diag2[index - i + n - 1] = false;
row.pop_back();
}
}
return;
}
vector<string> generateBoard(const int n, const vector<int>& row){
assert(row.size() == n);
vector<string> board(n, string(n, '.'));
for (int i = 0; i < n; i++)
board[i][row[i]] = 'Q';
return board;
}
public:
vector<vector<string>> solveNQueens(int n) {
cols = vector<bool>(n, false);
diag1 = vector<bool>(2 * n - 1, false);
diag2 = vector<bool>(2 * n - 1, false);
vector<int> row;
putQueen(n, 0, row);
return res;
}
};
8. 动态规划
8.1 从一个简单问题开始(斐波那契数列)
斐波那契数列:0,1,1,2,3,5…
解法一(递归解法): 缺点是包含有大量的重复计算,因此速度非常慢,时间复杂度为O(2^n)。递归树如下:
int fib(int n){
if(n == 0 || n == 1)
return n;
return fib(n - 1) + fib(n - 2);
}
解法二(递归+记忆化搜索): 创建一个memo数组,记录每次计算的结果。时间复杂度为O(n)。
vector<int> memo(n + 1, -1);
int fib(int n){
if(n == 0 || n == 1)
return n;
if(memo[n] == -1)
memo[n] = fib(n - 1) + fib(n - 2);
return memo[n];
}
解法三(动态规划): 动态规划和记忆化搜索本质上差不多,都是创建一个memo数组记录相应结果。唯一区别是思考问题方式不同:记忆化搜索是自上而下的思考方式,一般更容易些,动态规划是自下而上的思考方式,相比稍微难一点。但时间复杂度均为O(n)。
int fib(int n){
vector<int> memo(n + 1, -1);
memo[0] = 0;
memo[1] = 1;
for(int i = 2; i <= n; i++)
memo[i] = memo[i - 1] + memo[i - 2];
return memo[n];
}
总结:
- 动态规划的定义:将原问题拆解成若干个子问题,同时保存子问题的答案,使得每个子问题只求解一次,最终获得原问题的答案。
8.2 Leetcode 70:Climbing Stairs
有一个楼梯,总共有n阶台阶。每一次,可以上一个台阶,也可以上两个台阶。问,爬上这样的一个楼梯,一共有多少种不同的方法?
例如:
n=3,可以爬上这个楼梯的方法有:[1,1,1],[1,2],[2,1],所以答案为3
解法一(递归): 递归树如下:
class Solution {
public:
int climbStairs(int n) {
return calWays(n);
}
private:
int calWays(int n){
if (n == 0 || n == 1)
return 1;
return calWays(n - 1) + calWays(n - 2);
}
};
解法二(递归+记忆化搜索): 创建一个memo数组,记录每次计算的结果。时间复杂度为O(n)。
class Solution {
private:
vector<int> memo;
public:
int climbStairs(int n) {
memo = vector<int> (n + 1, -1);
return calWays(n);
}
private:
int calWays(int n){
if (n == 0 || n == 1)
return 1;
if (memo[n] == -1)
memo[n] = calWays(n - 1) + calWays(n - 2);
return memo[n];
}
};
解法三(动态规划)
class Solution {
public:
int climbStairs(int n){
vector<int> memo (n + 1, -1);
memo[0] = 1;
memo[1] = 1;
for (int i = 2; i <= n; i++)
memo[i] = memo[i - 1] + memo[i - 2];
return memo[n];
}
};
8.3 Leetcode 343:Integer Break
给定一个正整数n,可以将其分割成多个数字的和,若让这些数字的乘积最大,求分割的方法(至少要分成两个数)。算法返回这个最大的乘积。
例如:
解法一(暴力解法): 回溯遍历将一个数做分割的所有可能性,时间复杂度O(2^n)。递归树如下:
解法二(递归+记忆化搜索)
class Solution {
private:
vector<int> memo;
//将n进行分割(至少分割两部分),可以获得的最大乘积
int breakInteger(int n){
if (n == 1)
return 1;
if (memo[n] == -1)
for (int i = 1; i < n; i++)
memo[n] = max(memo[n], max(i * (n - i), i * breakInteger(n - i)));
return memo[n];
}
public:
int integerBreak(int n) {
if (n == 1)
return 1;
memo = vector<int>(n + 1, -1);
return breakInteger(n);
}
};
解法三(动态规划)
class Solution {
public:
int integerBreak(int n) {
if (n == 1)
return 1;
//memo[i]表示将数字i进行分割(至少分割两部分),可以获得的最大乘积
vector<int> memo(n + 1, -1);
memo[1] = 1;
for (int i = 2; i <= n; i++)
//求解memo[i]
for (int j = 1; j <= i - 1; j++)
// j + (i - j)
memo[i] = max(memo[i], max(j * (i - j), j * memo[i - j]));
return memo[n];
}
};
8.4 Leetcode 279:Perfect Squares
给出一个正整数n,寻找最少的完全平方数,使他们的和为n。
完全平方数:1,4,9,16,…
例如:
12=4+4+4;13=4+9
解法一(图的广度优先遍历):对问题建模:整个问题转化为一个图论问题。从n到0,每个数字代表一个结点;如果两个数字x到y相差一个完全平方数,则连接一条边。那么我们得到了一个无权图。原问题就转化成,求这个无权图中从n到0的最短路径。如第5章节所示。
解法二(递归+记忆化搜索)
解法三(动态规划)
class Solution{
public:
int numSquares(int n){
if (n == 1)
return 1;
//memo[i]表示将数字i表示为最少的完全平方数,使他们的和为i。
vector<int> memo(n + 1, INT_MAX);
memo[1] = 1;
for (int i = 2; i <= n; i++){
for (int j = 1; j * j <= i; j++){
if (j * j == i)
memo[i] = 1;
else
memo[i] = min(memo[i], 1 + memo[i - j * j]);
}
}
return memo[n];
}
};
8.5 Leetcode 91:Decode Ways
解法(动态规划)
class Solution {
public:
int numDecodings(string s){
if (s[0] == '0')
return 0;
int len = s.size();
vector<int> memo(len + 1, -1);
memo[0] = 1;
memo[1] = 1;
for (int i = 2; i <= len; i++){
int last = s[i - 1] - '0';
int sec_last = s[i - 2] - '0';
int num = sec_last * 10 + last;
memo[i] = (last ? memo[i - 1] : 0) + (sec_last && num >= 1 && num <= 26 ? memo[i - 2] : 0);
}
return memo[len];
}
};
8.6 Leetcode 62:Unique Paths
解法(动态规划)
class Solution{
public:
int uniquePaths(int m, int n){
if (m == 1 || n == 1)
return 1;
vector<vector<int>> memo(m, vector<int>(n, 1));
for (int i = 1; i < m; i++)
for (int j = 1; j < n; j++)
memo[i][j] = memo[i - 1][j] + memo[i][j - 1];
return memo[m - 1][n - 1];
}
};
8.6 Leetcode 62:Unique Paths II
解法(动态规划)
class Solution{
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid){
if (obstacleGrid[0][0] == 1)
return 0;
int m = obstacleGrid.size();
assert(m > 0);
int n = obstacleGrid[0].size();
vector<vector<int>> memo(m, vector<int>(n, 1));
//判断第一列是否有障碍物,有的话则路径数为0
for (int i = 1; i < m; i++)
if (obstacleGrid[i][0] == 1 || memo[i - 1][0] == 0)
memo[i][0] = 0;
//判断第一行是否有障碍物,有的话则路径数为0
for (int j = 1; j < n; j++)
if (obstacleGrid[0][j] == 1 || memo[0][j - 1] == 0)
memo[0][j] = 0;
for (int i = 1; i < m; i++){
for (int j = 1; j < n; j++){
//判断第i行j列是否有障碍物,有的话则路径数为0
if (obstacleGrid[i][j] == 1){
memo[i][j] = 0;
continue;
}
memo[i][j] = memo[i - 1][j] + memo[i][j - 1];
}
}
return memo[m - 1][n - 1];
}
};
8.7 Leetcode 198:House Robber
解法一(暴力解法): 检查所有房子的组合,对每一个组合,检查是否有相邻的房子。如果没有,记录其价值,最后找出最大值。时间复杂度O(n*(2^n))
由该问题可以得到动态规划里面的状态和状态转移方程:
- 状态的定义:考虑偷取[x…n-1]范围里的房子 (函数的定义)
- 根据对状态的定义,决定状态转移方程:
解法二(记忆化搜索)
class Solution {
private:
//memo[i]表示抢劫nums[i...nums.size())这个范围内所能获得的最大收益
vector<int> memo;
//考虑抢劫nums[index...nums.size())这个范围内的房子
int tryRob(vector<int>& nums, int index){
if (index >= nums.size())
return 0;
if (memo[index] != -1)
return memo[index];
int res = 0;
for (int i = index; i < nums.size(); i++)
res = max(res, nums[i] + tryRob(nums, i + 2));
memo[index] = res;
return res;
}
public:
int rob(vector<int>& nums) {
memo = vector<int>(nums.size(), -1);
return tryRob(nums, 0);
}
};
解法三(动态规划)
class Solution{
public:
int rob(vector<int>& nums){
int n = nums.size();
if (n == 0)
return 0;
//memo[i]表示抢劫nums[i...n)这个范围内所能获得的最大收益
vector<int> memo(n, -1);
memo[n - 1] = nums[n - 1];
for (int i = n - 2; i >= 0; i--)
for (int j = i; j < n; j++)
memo[i] = max(memo[i], nums[j] + (j + 2 < n ? memo[j + 2] : 0));
return memo[0];
}
};
8.8 经典问题:0-1背包问题
解法一(暴力解法): 本质上也可以视为组合问题,每一件物品可以放进背包,也可以不放进背包。时间复杂度O(n*(2^n)).
背包问题状态和状态转移如下:
解法二(记忆化搜索)
class KnapSack01{
private:
vector<vector<int>> memo;
//用[0...index]的物品,填充容量为C的背包的最大价值
int bestValue(const vector<int>& w, const vector<int>&v, int index, int c){
if (index < 0 || c <= 0)
return 0;
if (memo[index][c] != -1)
return memo[index][c];
int res = bestValue(w, v, index - 1, c);
if (c >= w[index])
res = max(res, v[index] + bestValue(w, v, index - 1, c - w[index]));
memo[index][c] = res;
return res;
}
public:
int knapscak(const vector<int>& w, const vector<int>&v, int C){
assert(w.size() == v.size());
if (w.empty())
return 0;
int n = w.size();
memo = vector<vector<int>>(n, vector<int>(C + 1, -1));
return bestValue(w, v, n - 1, C);
}
};
解法三(第一个版本的动态规划): 时间复杂度为O(nC),空间复杂度为O(nC).
class KnapSack01{
public:
int knapscak(const vector<int>& w, const vector<int>&v, int C){
assert(w.size() == v.size());
int n = w.size();
if (n == 0 || C == 0)
return 0;
vector<vector<int>> memo(n, vector<int>(C + 1, -1));
for (int j = 0; j <= C; j++)
memo[0][j] = (j >= w[0] ? v[0] : 0);
//其中i表示物品id,j表示容量
for (int i = 1; i < n; i++){
for (int j = 0; j <= C; j++){
memo[i][j] = memo[i - 1][j];
if (j >= w[i])
memo[i][j] = max(memo[i][j], v[i] + memo[i - 1][j - w[i]]);
}
}
return memo[n - 1][C];
}
};
解法四(第二个版本的动态规划): 时间复杂度为O(nC),空间复杂度为O(2*C)。思路如下:
class KnapSack01{
public:
int knapscak(const vector<int>& w, const vector<int>&v, int C){
assert(w.size() == v.size());
int n = w.size();
if (n == 0 || C == 0)
return 0;
vector<vector<int>> memo(2, vector<int>(C + 1, -1));
for (int j = 0; j <= C; j++)
memo[0][j] = (j >= w[0] ? v[0] : 0);
//其中i表示物品id,j表示容量
for (int i = 1; i < n; i++){
for (int j = 0; j <= C; j++){
memo[i%2][j] = memo[(i - 1)%2][j];
if (j >= w[i])
memo[i%2][j] = max(memo[i%2][j], v[i] + memo[(i - 1)%2][j - w[i]]);
}
}
return memo[(n - 1)%2][C];
}
};
解法五(第三个版本的动态规划): 时间复杂度为O(nC),空间复杂度为O©。思路如下:
class KnapSack01{
public:
int knapscak(const vector<int>& w, const vector<int>&v, int C){
assert(w.size() == v.size());
int n = w.size();
if (n == 0 || C == 0)
return 0;
vector<int> memo(C + 1, -1);
for (int j = 0; j <= C; j++)
memo[j] = (j >= w[0] ? v[0] : 0);
//其中i表示物品id,j表示容量
for (int i = 1; i < n; i++)
for (int j = C; j >= w[i]; j--)
memo[j] = max(memo[j], v[i] + memo[j - w[i]]);
return memo[C];
}
};
8.9 Leetcode 416:Partition Equal Subset Sum
给定一个非空数组,其中所有的数字都是正整数。问是否可以将这个数组的元素分成两部分,使得每部分的数字和相等?
- 最多有200个数字
- 每个数字最大为100
例如:
对于[1,5,11,5],可以分成[1,5,5]和[11]两部分,返回true
对于[1,2,3,5],无法分成元素和相等的两部分,返回false.
分析:该问题是一个典型的背包问题,思路如下:
解法一(递归+记忆化搜索)
class Solution {
private:
//memo[i][c]表示使用索引为[0...i]的这些元素,是否可以完全填充一个容量为c的背包
// -1表示未计算;0表示不可以填充;1表示可以填充
vector<vector<int>> memo;
//使用nums[0...index]是否可以完全填充一个容量为sum的背包
bool tryPartition(const vector<int>& nums, int index, int sum){
if (sum == 0)
return true;
if (index < 0 || sum < 0)
return false;
if (memo[index][sum] != -1)
return memo[index][sum] == 1;
memo[index][sum] = (tryPartition(nums, index - 1, sum) || tryPartition(nums, index - 1, sum - nums[index]) == true ? 1 : 0);
return memo[index][sum] == 1;
}
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for (int i = 0; i < nums.size(); i++)
sum += nums[i];
if (sum % 2 || sum == 0)
return false;
memo = vector<vector<int>>(nums.size(), vector<int>(sum / 2 + 1, -1));
return tryPartition(nums, nums.size() - 1, sum / 2);
}
};
解法二(动态规划)
class Solution{
public:
bool canPartition(vector<int>& nums){
int sum = 0;
for (int i = 0; i < nums.size(); i++)
sum += nums[i];
if (sum % 2 || sum == 0)
return false;
int C = sum / 2;
vector<bool> memo(C + 1, false);
for (int i = 0; i <= C; i++)
memo[i] = (nums[0] == i);
for (int i = 1; i < nums.size(); i++)
for (int j = C; j >= nums[i]; j--)
memo[j] = memo[j] || memo[j - nums[i]];
return memo[C];
}
};
8.10 Leetcode 300:Longest Increasing Subsequence (LIS)
给定一个无序的整数数组,找到其中最长上升子序列的长度。
例如:
输入: [10,9,2,5,3,7,101,18],输出: 4,解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
解法一(暴力解法): 本质上也是一个组合问题,选择所有的子序列进行判断。时间复杂度O(n*(2^n)).
LIS问题状态和状态转移如下:
解法二(递归回溯)
class Solution{
private:
vector<int> LIS;
//LIS[i]表示以nums[i]结尾的最长上升子序列的长度
int getLISLength(const vector<int>& nums, int i, const int len){
if (i > len)
return 0;
for (int j = 0; j < i; j++)
if (nums[i] > nums[j])
LIS[i] = max(LIS[i], 1 + LIS[j]);
return max(LIS[i], getLISLength(nums, i + 1, len));
}
public:
int lengthOfLIS(vector<int>& nums){
int n = nums.size();
if (n == 0 || n == 1)
return n;
LIS = vector<int>(n, 1);
return getLISLength(nums, 0, n - 1);
}
};
解法三(动态规划)
class Solution {
private:
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
if (n == 0 || n == 1)
return n;
vector<int> LIS(n, 1);
for(int i = 1; i < nums.size(); i++)
for(int j = 0; j < i; j++)
if(nums[i] > nums[j])
LIS[i] = max(LIS[i - 1], 1 + LIS[j]);
int res = 1;
for(int i = 0; i < nums.size(); i++)
res = max(res, LIS[i]);
return res;
}
};
8.11 Longest Common Subsequence (LCS)
给出两个字符串S1和S2,求出这两个字符串的最长公共子序列。
例如:
解法一(暴力解法): 本质上也是一个组合问题,选择所有的子序列进行判断。时间复杂度O(n*(2^n)).
LCS的状态和状态转移如下:
递归树如下:
解法二(递归回溯)
class Solution{
private:
vector<vector<int>> memo;
int getLCSLength(string s1, string s2, int m, int n){
if (m < 0 || n < 0)
return 0;
if (memo[m][n] != -1)
return memo[m][n];
int res = 0;
if (s1[m] == s2[n])
res = 1 + getLCSLength(s1, s2, m - 1, n - 1);
else
res = max(getLCSLength(s1, s2, m - 1, n), getLCSLength(s1, s2, m, n - 1));
memo[m][n] = res;
return res;
}
public:
int LCS(string s1, string s2){
if (s1.empty() || s2.empty())
return 0;
int m = s1.size() - 1;
int n = s2.size() - 1;
memo = vector<vector<int>>(m + 1, vector<int>(n + 1, -1));
return getLCSLength(s1, s2, m, n);
}
};
解法三(动态规划)
class Solution{
public:
int LCS(string s1, string s2){
if (s1.empty() || s2.empty())
return 0;
int m = s1.size();
int n = s2.size();
vector<vector<int>> memo(m + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= m; i++){
for (int j = 1; j <= n; j++){
if (s1[i - 1] == s2[j - 1])
memo[i][j] = 1 + memo[i - 1][j - 1];
else
memo[i][j] = max(memo[i][j - 1], memo[i - 1][j]);
}
}
return memo[m][n];
}
};
8.11 Longest Common Substring
给出两个字符串S1和S2,求出这两个字符串的最长公共子串。
- 和最长公共子序列的区别:子串要求是连续的。
分析: 和求解LCS特别的相似,唯一不同的地方是当S1[m] != S2[n] 时,LCS[m][n] = 0 。
解法(动态规划)
class Solution{
public:
int LCS(string s1, string s2){
if (s1.empty() || s2.empty())
return 0;
int m = s1.size();
int n = s2.size();
vector<vector<int>> memo(m + 1, vector<int>(n + 1, 0));
int res = 0;
for (int i = 1; i <= m; i++){
for (int j = 1; j <= n; j++){
if (s1[i - 1] == s2[j - 1]){
memo[i][j] = 1 + memo[i - 1][j - 1];
res = max(res, memo[i][j]);
}
else
memo[i][j] = 0;
}
}
return res;
}
};
9. 贪心算法
9.1 Leecode 455: Assign Cookies
解法(贪心算法) : 将两个数组逆序排列,每次将最大的饼干分给最贪心的小朋友。如下图所示:
class Solution{
public:
int findContentChildren(vector<int>& g, vector<int>& s){
sort(g.begin(), g.end(), greater<int>());
sort(s.begin(), s.end(), greater<int>());
int gi = 0, si = 0;
int res = 0;
while (gi < g.size() && si < s.size()){
if (s[si] >= g[gi]){
res++;
si++;
gi++;
}
else
gi++;
}
return res;
}
};
9.1 Leecode 435: Non-overlapping Intervals
分析: 该问题可以转化成,给定一组区间,问最多保留多少个区间,可以让这些区间之间互相不重叠。
解法一(暴力解法): 找出所有子区间的组合,之后判断它能不重叠。时间复杂度O(n*(2^n)).
解法二(动态规划): 类似于最长上升子序列。
bool compare(const Interval& a, const Interval& b){
if (a.start != b.start)
return a.start < b.start;
return a.end < b.end;
}
class Solution {
public:
int eraseOverlapIntervals(vector<Interval>& intervals) {
if (intervals.size() == 0 || intervals.size() == 1)
return 0;
sort(intervals.begin(), intervals.end(), compare);
//memo[i]表示在interval[0...i]能够保留最长的不重叠的区间个数
vector<int> memo(intervals.size(), 1);
int res = 0;
for (int i = 1; i < intervals.size(); i++){
for (int j = 0; j < i; j++)
if (intervals[i].start >= intervals[j].end)
memo[i] = max(memo[i], 1 + memo[j]);
res = max(res, memo[i]);
}
return intervals.size() - res;
}
};
解法三(贪心算法): 按照区间的结尾排序,每次选择结尾最早的,且和前一个区间不重叠的区间。
bool compare(const Interval& a, const Interval& b){
if (a.end != b.end)
return a.end < b.end;
return a.start < b.start;
}
class Solution{
public:
int eraseOverlapIntervals(vector<Interval>& intervals){
if (intervals.size() == 0 || intervals.size() == 1)
return 0;
sort(intervals.begin(), intervals.end(), compare);
int res = 1;
int pre = 0;
for (int i = 1; i < intervals.size(); i++){
if (intervals[i].start >= intervals[pre].end){
res++;
pre = i;
}
}
return intervals.size() - res;
}
};