字符串和数组面试总结

一. 排序算法

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. 快速排序

  1. 原始版本以及解决几乎有序数组情况下快排将退化成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;
}
  1. 三路快排:解决数组中存在大量重复元素的问题
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. 空间复杂度
    在这里插入图片描述
  2. 稳定性
    在这里插入图片描述

二. 查找算法

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;
	}
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值