《程序员面试金典(第6版)》面试题 08.08. 有重复字符串的排列组合(回溯算法,全排列问题)C++
Posted 阿宋同学
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《程序员面试金典(第6版)》面试题 08.08. 有重复字符串的排列组合(回溯算法,全排列问题)C++相关的知识,希望对你有一定的参考价值。
题目描述
有重复字符串的排列组合。编写一种方法,计算某字符串的所有排列组合。
示例1:
- 输入:S = “qqe”
输出:[“eqq”,“qeq”,“qqe”]
示例2:
- 输入:S = “ab”
输出:[“ab”, “ba”]
提示:
- 字符都是英文字母。
字符串长度在[1, 9]之间。
解题思路与代码
这道题一看还是一道关于排列的问题。只要有关排列的问题,我们都可以通过回溯法去解决。
方法一: 回溯法 + 使用unordered_set数据结构进行去重
如果没有做过《程序员面试金典(第6版)》面试题 08.07. 无重复字符串的排列组合(回溯算法,全排列问题)C++
这道题的小伙伴,先去做一下这道题。
这道题与上面链接的那道题非常像,只不过,这里字符串中的字符开始出现有重复的字符了。所以我们做这道题的时候就需要去重。我们直接用unordered_set这种数据结构去去一下重好了。代码与上道题的代码没什么区别,这里给出这道题的代码:
class Solution
public:
vector<string> permutation(string S)
unordered_set<string> result;
backtracking(S,result,0);
vector<string> vec;
for(auto a : result)
vec.push_back(a);
return vec;
void backtracking(string& S,unordered_set<string>& result,int begin)
if(begin == S.size())
result.insert(S);
return;
for(int i = begin;i < S.size(); ++i)
swap(S[i],S[begin]);
backtracking(S,result,begin+1);
swap(S[i],S[begin]);
;
复杂度分析
时间复杂度:
-
这段代码的时间复杂度主要取决于两个部分:backtracking 函数的执行次数以及将结果从 unordered_set 转移到 vector 的时间。
-
backtracking 函数的执行次数:对于长度为 n 的字符串,我们需要对每个字符进行排列组合,这会产生 n! 个排列。在回溯算法中,我们会遍历整个排列空间。因此,backtracking 函数的执行次数为 O(n!)。
-
将结果从 unordered_set 转移到 vector:result 中最多有 n! 个元素。遍历 result 并将其中的元素插入 vector 的时间复杂度为 O(n!)。
-
综合这两部分,总的时间复杂度为 O(n!)。
空间复杂度:
-
空间复杂度主要取决于三个方面:递归调用栈的深度、结果存储在 unordered_set 中所占用的空间,以及结果向量 vec 所占用的空间。
-
递归调用栈的深度:在回溯算法中,递归调用栈的深度等于字符串的长度 n。因此,递归调用栈的空间复杂度为 O(n)。
-
结果存储在 unordered_set 中所占用的空间:result 中最多有 n! 个元素,每个元素是一个长度为 n 的字符串。因此,结果存储在 unordered_set 中所占用的空间复杂度为 O(n * n!)。
-
结果向量 vec 所占用的空间:vec 中有 n! 个元素,每个元素是一个长度为 n 的字符串。因此,结果向量所占用的空间复杂度为 O(n * n!)。
-
由于这三部分空间是算法使用的空间,因此总的空间复杂度为这三者之和,即 O(n) + O(n * n!) + O(n * n!) = O(2 * n * n!) = O(n * n!)。
-
所以,这段代码的时间复杂度为 O(n!),空间复杂度为 O(n * n!)。
方法二 :对代码一的方法进行优化。
在这道题里,因为可能有重复的字符,方法一是直接用unordered_set在结果处进行去重,重复的答案不会被存进集合中,但这种方法不会减少递归的此时。那有没有一种方法,可以在做交换的时候就进行剪枝操作而进行去重呢,而去减少递归的次数呢?
答案当然是有,我们可以通过一个unordered_set去记住在当前的这一层循环里出现过哪些字符,如果出现了重复的字符,那我们就跳过这次交换,直接进入下一次交换。这样也同样达到了去重的目的,也减少了递归的次数。但是不好的一点是增加了内存的存储空间,因为每一层递归都要创建一个unordered_set去存储出现过的字符。
具体代码如下:
class Solution
public:
vector<string> permutation(string S)
vector<string> result;
backtracking(S, result, 0);
return result;
void backtracking(string &S, vector<string> &result, int begin)
if (begin == S.size())
result.push_back(S);
return;
unordered_set<char> used_chars; // 用于存储已经在当前位置出现过的字符
for (int i = begin; i < S.size(); ++i)
if (used_chars.find(S[i]) != used_chars.end())
continue; // 如果当前字符已经在当前位置出现过,则跳过这次交换
used_chars.insert(S[i]); // 记录当前字符
swap(S[i], S[begin]);
backtracking(S, result, begin + 1);
swap(S[i], S[begin]);
;
复杂度分析
通过这种剪枝策略,我们避免了搜索重复的路径,从而降低了时间复杂度。然而,在最坏情况下(如所有字符都不同),算法的时间复杂度仍然是 O(n!)。空间复杂度与之前的分析相同,为 O(n * n!)。虽然这种剪枝策略不能在理论上改进时间复杂度,但在有重复字符的情况下,实际运行效率会有所提升,但是同样每一层都会多创建出一个unordered_set去存储至多n个字符,会多消耗一部分的内存空间。
方法三,对代码二的再次优化!
这一次我们写了一个hasDuplicate函数来检查有没有重复出现的字符。这样就不会使用额外的内存空间去存储字符了。
因为begin的值肯定是要比i小的,因为i会递增,而begin不会,从而我们在这个函数中,去递增begin,看看有没有会出现与i相当的字符,如果出现了,就说明有重复,就要跳过这个循环!
class Solution
public:
vector<string> permutation(string S)
vector<string> result;
backtracking(S,result,0);
return result;
void backtracking(string& S,vector<string>& result,int begin)
if(begin == S.size())
result.push_back(S);
return;
for(int i = begin; i < S.size(); ++i)
if(hasDuplicate(S,begin,i)) continue;
swap(S[i],S[begin]);
backtracking(S,result,begin+1);
swap(S[i],S[begin]);
bool hasDuplicate(string& S, int begin,int end)
for(int i = begin; i < end; ++i)
if(S[i] == S[end]) return true;
return false;
;
复杂度分析
时间复杂度:
- permutation 函数的时间复杂度主要取决于 backtracking 函数。在最坏情况下,回溯算法将尝试所有可能的排列组合,即 n!。
- hasDuplicate 函数的时间复杂度为 O(n)(在 for 循环内部进行比较)。
- backtracking 函数中调用了 hasDuplicate 函数,所以在最坏情况下,总时间复杂度为 O(n! * n)。
空间复杂度:
- 结果向量 result 的空间复杂度为 O(n!),因为它需要存储所有排列组合。
- 递归栈的空间复杂度为 O(n),因为最深的递归调用次数等于字符串的长度。
- 总的空间复杂度为 O(n! + n)。
综上所述,该算法的时间复杂度为 O(n! * n),空间复杂度为 O(n! + n)。
虽然说,代码1,2,3的时间复杂度都是O(n!),但是在代码三的实际时间复杂度要比1,2快了不少。
总结
这道题不算一道特别难的题。但是呢,剪枝和去重,才是这道题的重中之重。写出简洁并且高效的回溯算法并不容易。我们还得去多学习多总结!
《程序员面试金典(第6版)》面试题 08.01. 三步问题(动态规划,c++)
题目描述
三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。
示例1:
- 输入:n = 3
输出:4
说明: 有四种走法
示例2:
- 输入:n = 5
输出:13
提示:
- n范围在[1, 1000000]之间
解题思路与代码
这道题,我说实话对我而言没有什么太大的思路。然后我去网上看了看题解,主流的解法就是使用动态规划
去解题,还有用矩阵快速幂
去解题的,博主我大学数学学的不好,后悔大学没有好好学习高数,线代了,所以我这里先介绍动态规划的解法,到未来,如果我有一天数学基础又好了,我会回来将矩阵快速幂这个解法来补上的。
方法一,动态规划
这道题我们直接去套用Carl哥的动态规划五部曲
,去解题分析。
第一步,确定dp数组以及下标的含义:
- dp[i] : 爬到第i层楼,有dp[i]种方法。
第二步,确定递推(推导)公式
- 从dp[i] 的定义我们可以推导出,有三种方式可以得到dp[i],分别是:
- dp[i-1]代表的是,到达i-1层楼,一共有dp[i-1]种方法,那我再上一层楼,是不是就得到dp[i]了呢?
- dp[i-2]代表的是,到达i-2层楼,一共有dp[i-2]种方法,那我再上两层楼,是不是就得到dp[i]了呢?
- dp[i-3]代表的是,到达i-3层楼,一共有dp[i-3]种方法,那我再上三层楼,是不是就得到dp[i]了呢?
- 所以dp[i]就是dp[i-1],dp[i-2],dp[i-3]它们的和,
dp[i] = dp[i-1] + dp[i-2] + dp[i-3];
第三步,dp数组如何初始化?
- 首先,我认为所谓初始化,就是如何从最底层向结果去推演的一个过程,因为我们之前知道,一共有三种方式可以得到dp[i],所以我们等下就要初始化3个值。
- 那么由题意可值,n是一个正整数,最小值为1,这里讨论dp[0],就没有意义。
- 所以我们要从dp[1]开始初始化,所以dp[1] = 1,dp[2] = 1,dp[2] = 2,dp[3] = 4
第四步,确定遍历顺序
- 由递推公式:dp[i] = dp[i-1] + dp[i-2] + dp[i-3]; 我们可以看出,遍历顺序,一定是从前向后去遍历的。
第五步,举例推导dp数组
- 当 n 为 6 时,
- dp[1] = 1, dp[2] = 2, dp[3] = 4,
- dp[4] = dp[1] + dp [2] + dp[3] = 7,
- dp[5] = dp[2] + dp [3] + dp[4] = 13,
- dp[6] = dp[3] + dp [4] + dp[5] = 24.
当这五部曲分析完成后,我们就可以去写代码啦~
代码如下:
class Solution
public:
int waysToStep(int n)
if(n <= 2) return n;
if(n == 3) return 4;
vector<int> dp (n+1,0);
dp[1] = 1; dp[2] = 2; dp[3] = 4;
for(int i = 4; i < n+1; ++i)
dp[i] = ((dp[i-1] + dp[i-2])%1000000007 + dp[i-3])%1000000007; //这个公式是由下面公式合并同类项而来的
//dp[i] = ((dp[i-3]%1000000007 + dp[i-2])%1000000007 + dp[i-1]%1000000007)%1000000007;
return dp[n];
;
复杂度分析
时间复杂度分析:
- 初始化一个长度为 n+1 的 dp 数组,时间复杂度为 O(n)。
遍历 dp 数组,计算 dp[i] 的值,时间复杂度为 O(n)。
整个算法的时间复杂度为 O(n)。
空间复杂度分析:
- 开辟一个长度为 n+1 的 dp 数组,空间复杂度为 O(n)。
综上所述,该算法的时间复杂度为 O(n),空间复杂度为 O(n)。需要注意的是,由于取模操作的存在,实际运行时间可能会略微慢一些,但不会改变时间复杂度的量级。
方法二,使用滚动遍历,优化动态规划
和上题的思想一样,只不过用4个int变量,去替代了dp数组。
int one = 1,代替了原来的dp[1], int two = 2,代替了原来的dp[2], int three = 4,代替了原来的dp[3],
int result = ((three + two)%1000000007 + one)%1000000007,代替了原来的dp[n]
然后用one = two ,two = three,three = result,去不断更新dp[i-1],dp[i-2],dp[i-3]的值。
具体的代码实现如下:
class Solution
public:
int waysToStep(int n)
if(n <= 2) return n;
if(n == 3) return 4;
int one = 1; int two = 2; int three = 4; int result = 0;
for(int i = 4; i < n+1; ++i)
result = ((three + two)%1000000007 + one)%1000000007;
one = two;
two = three;
three = result;
return result;
;
复杂度分析
时间复杂度分析:
初始化三个变量 one, two, three,时间复杂度为 O(1)。
遍历 n 次,计算 result 的值,时间复杂度为 O(n)。
整个算法的时间复杂度为 O(n)。
空间复杂度分析:
只开辟了三个变量 one, two, three,空间复杂度为 O(1)。
综上所述,该算法的时间复杂度为 O(n),空间复杂度为 O(1)。
总结
这道题,是一道动态规划非常好的练手题。我们可以拿它来做动态规划的入门。很好很不错!
以上是关于《程序员面试金典(第6版)》面试题 08.08. 有重复字符串的排列组合(回溯算法,全排列问题)C++的主要内容,如果未能解决你的问题,请参考以下文章
《程序员面试金典(第6版)》面试题 08.04. 幂集(回溯算法,位运算,C++)不断更新
《程序员面试金典(第6版)》 面试题 08.13. 堆箱子(动态规划,与最长上升子序列问题相关的组合问题,C++)