紧接上文,依然为个人刷题记录。
动态规划
322.零钱兑换(中等)
给你一个整数数组coins,表示不同面额的硬币,以及一个整数amount,表示总金额。
计算并返回可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回-1。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
分析思路:子问题,状态转移方程,DP数组
子问题:用DP[N]表示凑成金额N所需的最少的硬币个数。则用coin遍历coins数组,找到最小的DP[N-coin]。
用DP(N+1)大小的数组存储amount金额的子问题结果。子问题的求解由其他子问题解出。
转移方程:DP[N] = 1+min DP[N-coin],DP[0]=0。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX); //需要大量的存储空间
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
//求解子问题
for (int coin : coins) {
if (i - coin >= 0 && dp[i - coin] != INT_MAX) {
dp[i] = min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
};
时间复杂度:O(N*M),76ms,60.14%
空间复杂度:O(N),14.25MB,25.43%
139.单词拆分(中等)
给你一个字符串S和一个字符串列表wordDict作为字典。请你判断是否可以利用字典中出现的单词拼接处S。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以由 “leet” 和 “code” 拼接成。
示例 2:
输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以由 “apple” “pen” “apple” 拼接成。
注意,你可以重复使用字典中的单词。
示例 3:
输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false
分析思路:
子问题:将字符串S划分为子问题。用f(k)表示字符串S的前k个子串能否利用字典中出现的单词拼接处。子问题的求解可以由
状态转移方程:F(k)=F(k-word.lenth)&& word在字典中
DP数组:存储子问题的结果
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
int n = s.size();
vector<bool> DP(n+1,false);
DP[0] = true;
for(int i=1;i<=n;i++){
//查询字典的单词
for(string temp : wordDict){
int lenth = temp.size();
if(i>=lenth && DP[i-lenth] && s.substr(i-lenth,lenth)==temp) {
DP[i] = true;
break; //一旦找到匹配就不需要继续在字典中查找
}
}
}
return DP[n];
}
};
时间复杂度:O(N*M),0ms,100.00%
空间复杂度:O(N),7.49MB,89.53%
300.最长递增子序列(中等)(不会)
给你一个整数数组nums,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]是数组[0,3,1,6,2,2,7]的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
思路:
子问题:设DP[i]为考虑前i个元素,以第i个数子结尾的最长上升子序列的长度,注意nums[i]必须被选取。我们从小到大计算DP数组的值,则
状态转移方程:DP[i] = max(DP[j]+1),其中j在0到i之间,并且nums[j]<nums[i]。返回DP数组中的最大值。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
if (n == 0) return 0;
vector<int> DP(n, 1); // 初始化DP数组,每个元素至少可以单独成为一个长度为1的子序列
for (int i = 1; i < n; i++) {
//修改DP数组值
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
DP[i] = max(DP[i], DP[j] + 1);
}
}
}
return *max_element(DP.begin(), DP.end()); // 返回DP数组中的最大值
}
};
时间复杂度:O(N*N),292ms,28.42%
空间复杂度:O(N),10.31MB,43.79%
152.乘积最大子数组(中等)(不会)
给你一个整数数组nums,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。测试用例的答案是一个32位整数。子数组是数组的连续子序列。
示例 1:
输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: nums = [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
分析思路:
子问题:sub_max[k]为包含nums[k]的最大连续乘积。sb_min[k] 为包含nums[k]的最小的连续乘积.
连续子数组。记录最大前缀乘积和最小前缀乘积。
有转移方程:
返回最大数组中的值即可关键在于子问题和转移方程的建立。
class Solution {
public:
int maxProduct(vector<int>& nums) {
vector<int> sub_max(nums),sub_min(nums);
for(int i=1;i<nums.size();++i){
sub_max[i] = max(sub_max[i-1] * nums[i],max(sub_min[i-1]*nums[i],nums[i]));
sub_min[i] = min(sub_max[i-1]*nums[i],min(sub_min[i-1]*nums[i],nums[i]));
}
return *max_element(sub_max.begin(),sub_max.end());
}
};
时间复杂度:O(N),遍历一次
空间复杂度:O(N),建立两个额外数组。
416.分割等和子集(中等)(没看懂)
给你一个只包含正整数的非空数组nums,请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
分析思路:
子问题:
多维动态规划
62.不同路径(中等)
一个机器人位于一个MXN网格的左上角(起始点在下图中标记为“start”)。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
- 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右
- 向下 -> 向右 -> 向下
分析思路:
思路一:排列组合,一共需要走m+n-2步,其中选m-1步向下即可。
思路二:动态规划:
子问题:记DP【i】【j】为到达i,j位置最多的路径
转移方程:DP[i][j] = DP[i-1][j] + DP[i][j-1];
DP[0][j],DP[i][0]在边界上,所以只能为1。
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> DP(m,vector<int>(n,1));
for(int i=1;i<m;++i){
for(int j=1;j<n;++j){
DP[i][j] = DP[i-1][j]+DP[i][j-1];
}
}
return DP[m-1][n-1];
}
};
时间复杂度:O(MN)
空间复杂度:O(MN)
64.最小路径和(中等)
给定一个包含非负整数的m*n网格grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。说明:每次只能向下或者向右移动一步。
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12
分析思路:贪心算法局部最优不一定是全局最优。动态规划,常规。
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<vector<int>> DP(m,vector<int>(n)) ;
//初始化
DP[0][0] = grid[0][0];
for(int i=1;i<m;++i){
DP[i][0] = DP[i-1][0] + grid[i][0];
}
for(int j=1;j<n;++j){
DP[0][j] = DP[0][j-1] + grid[0][j];
}
//遍历
DP[0][0] = grid[0][0];
for(int i=1;i<m;++i){
for(int j=1;j<n;++j){
DP[i][j] = min(DP[i-1][j],DP[i][j-1]) + grid[i][j];
}
}
return DP[m-1][n-1];
}
};
时间复杂度:O(MN)
空间复杂度:O(MN)
优化思路:直接将grid数组作为DP数组,但是不好。
5.最长回文子串(中等)(没看懂)
给你一个字符串S,找到S中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
示例 1:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2:
输入:s = “cbbd”
输出:“bb”
思路:使用中心扩展法
class Solution {
public:
string longestPalindrome(string s) {
if (s.empty()) return ""; //特判
int start = 0, end = 0; //指向区间边界
for (int i = 0; i < s.size(); ++i) {
int len1 = expandAroundCenter(s, i, i); // 以单个字符为中心扩展
int len2 = expandAroundCenter(s, i, i + 1); // 以相邻字符间隙为中心扩展
int len = max(len1, len2);
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substr(start, end - start + 1);
}
int expandAroundCenter(string s, int left, int right) {
//中心扩展法
while (left >= 0 && right < s.size() && s[left] == s[right]) {
--left;
++right;
}
return right - left - 1;
}
};
时间复杂度:O(N*N)
空间复杂度:O(1)
思路二:动态规划:
1143.最长公共子序列(中等)
给定两个字符串text1和text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,返回0。一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,“ace”是“abcde”的子序列,但“aec”不是“abcde”的子序列。
- 两个字符串的公共子序列是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。
示例 2:
输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc” ,它的长度为 3 。
示例 3:
输入:text1 = “abc”, text2 = “def”
输出:0
解释:两个字符串没有公共子序列,返回 0 。
思路:多维动态规划
子问题:设DP[i][j],表示长度为i的字符串text1和长度为j的字符串text2的最长公共子序列的长度。
边界条件当iorj等于0时,DP[i][j]=0,
递推关系:
D
P
[
i
]
[
j
]
=
{
D
P
[
i
−
1
]
[
j
−
1
]
+
1
,
t
e
x
t
1
[
i
−
1
]
=
t
e
x
t
2
[
j
−
1
]
m
a
x
(
D
P
[
i
−
1
]
[
j
]
,
D
P
[
i
]
[
j
−
1
]
)
,
t
e
x
t
1
[
i
−
1
]
≠
t
e
x
t
2
[
j
−
1
]
DP[i][j]=\begin{cases}DP[i-1][j-1]+1,text1[i-1]=text2[j-1] \\max(DP[i-1][j],DP[i][j-1]),text1[i-1]≠text2[j-1] \end{cases}
DP[i][j]={DP[i−1][j−1]+1,text1[i−1]=text2[j−1]max(DP[i−1][j],DP[i][j−1]),text1[i−1]=text2[j−1]
实现:
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.size();
int n = text2.size();
vector<vector<int>> DP(m+1,vector<int>(n+1,0)); //初始化DP数组
for(int i=1;i<m+1;++i){
for(int j=1;j<n+1;++j){
//遍历修改DP数组
if(text1[i-1]==text2[j-1]) DP[i][j] = DP[i-1][j-1]+1 ;
else DP[i][j] = max(DP[i-1][j],DP[i][j-1]);
}
}
return DP[m][n];
}
};
时间复杂度:O(MN),遍历一遍DP数组
空间复杂度:O(MN),开辟DP数组的存储空间
72.编辑距离(中等)
给你两个单词word1和word2,请返回将word1转换成word2所使用的最少操作数。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
示例 2:
输入:word1 = “intention”, word2 = “execution”
输出:5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)
思路:多维动态规划
子问题:DP[i][j]表示长度为i的单词word1和长度为j的单词word2的编辑距离。
边界条件,iorj等于0时,DP数组值为另一个单词的长度。
递推关系:
代码:
class Solution {
public:
int minDistance(string word1, string word2) {
int m=word1.size();
int n=word2.size();
//DP数组初始化
vector<vector<int>> DP(m+1,vector<int>(n+1,0));
for(int j=0;j<n+1;++j){
DP[0][j] = j;
}
for(int i=1;i<m+1;++i){
DP[i][0] = i;
}
//遍历修改DP数组
for(int i=1;i<m+1;++i){
for(int j=1;j<n+1;++j){
if(word1[i-1] == word2[j-1]) DP[i][j] = 1+min(DP[i][j-1],min(DP[i-1][j],DP[i-1][j-1]-1));
else DP[i][j] = 1+min(DP[i][j-1],min(DP[i-1][j],DP[i-1][j-1]));
}
}
return DP[m][n];
}
};
时间复杂度:O(MN),遍历一遍DP数组
空间复杂度:O(MN),开辟DP数组的存储空间
技巧
136.只出现一次的数字(简单)
给你一个非空整数数组nums,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素、
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1 :
输入:nums = [2,2,1]
输出:1
示例 2 :
输入:nums = [4,1,2,1,2]
输出:4
示例 3 :
输入:nums = [1]
输出:1
分析思路:利用位运算异或
- 任何数和0做异或结果仍然是原来的数,
- 任何数和自己做异或结果是0
- 异或满足交换律和结合律
总共有2M+1个数,所有数异或的结果一定为出现一次的数
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;
for(auto e:nums) ret = ret ^ e;
return ret;
}
};
169.多数元素(简单)
给定一个大小为n的数组nums,返回其中的多数元素。多数元素是指在数组中出现次数大于n/2的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:nums = [3,2,3]
输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2]
输出:2
分析思路:用哈希表,关键字为元素,值为元素出现的次数。遍历一遍数组之后,返回哈希表中值最大的元素即可。
class Solution {
public:
int majorityElement(vector<int>& nums) {
unordered_map<int ,int> hash_Map;
int n = nums.size();
int cnt = 0;
//遍历nums,创建哈希表
for(int i=0;i<n;i++){
hash_Map[nums[i]]++;
if(hash_Map[nums[i]]>=(n/2+1)) {
cnt = nums[i];
break;
}
}
return cnt;
}
};
时间复杂度:O(N)
空间复杂度:O(N),最多有n/2个不同的数字
75.颜色分类(中等)
给定一个包含红色、白色和蓝色、共n个元素的数组nums,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数0、1和2分别表示红色、白色和蓝色。
必须在不适用库内置的sort函数的情况下解决这个问题。
示例 1:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
示例 2:
输入:nums = [2,0,1]
输出:[0,1,2]
分析思路:双指针,遍历,时间复杂度O(N*N),暴力解法
思路二:哈希表,统计0、1、2的个数,再给数组重新赋值。遍历两次nums数组。时间复杂度O(N)
思路三:排序,排序算法时间复杂度O(NlogN)。
采用思路二解决:
class Solution {
public:
void sortColors(vector<int>& nums) {
unordered_map<int ,int> hash_map;
int n=nums.size();
for(int i=0;i<n;++i){
hash_map[nums[i]]++;
}
for(int i=0;i<n;++i){
//修改nums数组
if(hash_map[0]!=0) {
nums[i] = 0;
hash_map[0]--;
}
else if(hash_map[1]!=0) {
nums[i] =1;
hash_map[1]--;
}
else nums[i] =2;
}
return ;
}
};
时间复杂度:O(N),遍历了两遍nums数组
空间复杂度:O(1),建立了哈希表,只有三个键值对。
31.下一个排列(中等)(没看懂)
整数数组的一个排列 就是将其所有成员以序列或线性顺序排列。
整数数组的下一个排列是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的下一个排列就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
例如,arr =[1,2,3]的下一个排列是[1,3,2]
类似的,arr = [2,3,1]的下一个排列是[3,1,2]
而arr = [3,2,1]的下个排列是[1,2,3],因为[3,2,1]不存在一个字典序更大的排列。
给你一个整数数组nums,找出nums的下一个排列。
必须原地修改,只允许使用额外常数空间。
示例 1:
输入:nums = [1,2,3]
输出:[1,3,2]
示例 2:
输入:nums = [3,2,1]
输出:[1,2,3]
示例 3:
输入:nums = [1,1,5]
输出:[1,5,1]
分析思路:
287.寻找重复数(中等)(没懂)
给定一个包含n+1个整数的数组nums,其数字都在[1,n]范围内(包括1和n),可知至少存在一个重复的整数。
假设nums只有一个重复的整数,返回这个重复的数。
你设计的解决方案必须不修改数组nums且只用常量级O(1)的额外空间。
示例 1:
输入:nums = [1,3,4,2,2]
输出:2
示例 2:
输入:nums = [3,1,3,4,2]
输出:3
分析思路:将所有的整数进行求和-n(n-1)/2即为答案。有一个整数可能出现两次以上。错误
思路二:建立下标和值的映射,如果没重复元素则不存在环,每重复一个元素则存在一个环,该问题就转化为求解环的入口,用快慢指针即可。