【算法】【回溯】两道经典排列问题OJ详解【力扣46 力扣47】【超详细的回溯算法教程】让我们牢牢把握回溯的精髓
【算法】【回溯】两道经典排列问题OJ详解【力扣46 力扣47】【超详细的回溯算法教程】让我们牢牢把握回溯的精髓
作者: @小小Programmer
这是我的主页:@小小Programmer
在食用这篇博客之前,博主在这里介绍一下其它高质量的编程学习栏目:
数据结构专栏:数据结构 这里包含了博主很多的数据结构学习上的总结,每一篇都是超级用心编写的,有兴趣的伙伴们都支持一下吧!
算法专栏:算法 这里可以说是博主的刷题历程,里面总结了一些经典的力扣上的题目,和算法实现的总结,对考试和竞赛都是很有帮助的!
力扣刷题专栏:Leetcode想要冲击ACM、蓝桥杯或者大学生程序设计竞赛的伙伴,这里面都是博主的刷题记录,希望对你们有帮助!
先赞后看好习惯 打字不容易,这都是很用心做的,希望得到支持你 大家的点赞和支持对于我来说是一种非常重要的动力。看完之后别忘记关注我哦!️️️
本篇建议收藏后食用~
题目:
OJ46:46. 全排列
OJ47:47. 全排列 II
OJ46 全排列
题目描述
实现思路
首先,这两道题我们肯定是要用回溯实现的,我们要通过回溯,来找到每一种排列方式。
因此,在这里,博主先带着大家复习一下回溯算法的精髓
回溯算法的三部曲:
- 确定递归函数参数和返回值
- 确定递归终止条件
- 确定单层递归逻辑
一:确定递归函数的参数和返回值
这里和组合问题是不一样的,因为排列问题是有顺序可言的,比如例题一:我们要找[1,2,3]
的排列,第二次从2开始找的时候,前面的1也是要进去的,即[1,2..]
和[2,1...]
是不同的排列,因此,我们的递归函数除了原来的数组之外,还需要一个数组来记录,哪一个数字已经用过了。 比如第一个数字挑了2,即[2,...]
,此时,第二个数字的选择只能是1或者3,因为2已经用过了。
所以我们的递归函数:
void backtracking(vector<int>& nums, vector<bool>& used)
当然,我们还需要创建全局的两个数组,一个用来做最后结果的收集,一个用于递归回溯。
vector<vector<int>>ret;
vector<int>path;
二:确定递归终止条件
很明显,当递归数组(path)满了的时候,就终止递归。
比如:求[1,2,3]
的排列,当一个数组path[]
收集到3个数的时候,就要返回了,比如path
里面是[2,1,3]
的时候,如果只有两个或者一个数,递归就要继续。
if (path.size() == nums.size()) {
ret.push_back(path);
return;
}
三. 确定单层递归逻辑
当然,单层搜索就是递归了
- 递归回溯永不分离
- 一次递归伴随着一次回溯
所以我们每次递归完一定要记得撤销,回溯。
完整代码
//OJ46 全排列
class Solution {
private:
vector<vector<int>>ret;
vector<int>path;
void backtracking(vector<int>& nums, vector<bool>& used) {
//此时说明找到了一组结果
//2.终止条件
if (path.size() == nums.size()) {
ret.push_back(path);
return;
}
//3.单层搜索
for (int i = 0; i < nums.size(); i++) {//每次都要从头开始搜索
if (used[i] == true)continue;//如果这个数字用过了,直接跳过本次循环
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
//撤销,回溯
path.pop_back();
used[i] = false;
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<bool>used(nums.size(), false);
backtracking(nums, used);
return ret;
}
};
/*
* 和组合问题的区别
* 每层都是从0开始搜索,而不是从startIndex开始搜索---[1,2]中用过1了,但是[2,1]中还得用1
* 需要used数组记录path中都存放了哪些元素
*/
//used是用来防止同一个数选两次的
OJ47 全排列ll
题目描述
实现思路
这其实就是一个经典的去重问题,方法同样是用used数组来完成去重
要注意的点:
- 去重之前要对数组进行排序,要将一样的数字放在一起
- 去重的判断条件,当当前下标所对应的数和前一个下标所对应的数一致,并且这个数并不是在同一个树枝上时去重
一:确定递归函数的返回值和参数
void backtracking(vector<int>& nums, vector<bool>& used)
二:确定递归的终止条件,这些都和前面一样的。
if (path.size() == nums.size()) {
ret.push_back(path);
return;
}
三:确定单层搜索逻辑:
- 具体的去重实现,请看完整代码的注释,若还有不明白的,可以私信博主噢! 其实这个去重思路是非常经典的思路,是必须要掌握的!
完整代码
class Solution {
private:
vector<vector<int>>ret;
vector<int>path;
void backtracking(vector<int>& nums, vector<bool>& used) {
if (path.size() == nums.size()) {
ret.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++) {
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false)continue;//首先一定要i>0,保证这不是这棵树的根节点
//nums[i]==nums[i-1]说明可能会出现重复,但是此时不确定是这两个相同的数出现在同一树层上还是同一树枝上
//如果实在同一树枝上,是可以的,比如在[1,1,2]是没问题的
//但是如果是在同一树层上是不行的,可能会出现重复
//所以used[i-1]=false 保证这是在同一树层上的,而不是同一树枝上的
if (used[i] == false) {
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
used[i] = false;
}
}
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<bool>used(nums.size(), false);
backtracking(nums, used);
return ret;
}
};
//为什么去重的时候写第二个&&左边写
//used[i-1]==false;或used[i-1]=true;都可以呢?
//因为在树枝上去重和在树层上去重都可以的
//在树层上去重的效率很高,
//在树枝上去也可以,但是做了很多无用的搜索
尾声
看到这里相信你对排列问题的这两道OJ已经有一定的理解了,其实排列问题是一个非常经典的我们必须要掌握的问题。
在走之前,可以再看看博主的这些专栏内容对你是否有帮助: 有的话请不要吝啬你们的点赞收藏关注和转发噢!
数据结构专栏:数据结构 这里包含了博主很多的数据结构学习上的总结,每一篇都是超级用心编写的,有兴趣的伙伴们都支持一下吧!
算法专栏:算法 这里可以说是博主的刷题历程,里面总结了一些经典的力扣上的题目,和算法实现的总结,对考试和竞赛都是很有帮助的!
力扣刷题专栏:Leetcode想要冲击ACM、蓝桥杯或者大学生程序设计竞赛的伙伴,这里面都是博主的刷题记录,希望对你们有帮助!