Open sisterAn opened 4 years ago
本题是回溯算法的经典应用场景
回溯算法是一种搜索法,试探法,它会在每一步做出选择,一旦发现这个选择无法得到期望结果,就回溯回去,重新做出选择。深度优先搜索利用的就是回溯算法思想。
回溯算法很简单,它就是不断的尝试,直到拿到解。它的这种算法思想,使它通常用于解决广度的搜索问题,即从一组可能的解中,选择一个满足要求的解。
我们可以写一下,数组 [1, 2, 3] 的全排列有:
即回溯的处理思想,有点类似枚举搜索。我们枚举所有的解,找到满足期望的解。为了有规律地枚举所有可能的解,避免遗漏和重复,我们把问题求解的过程分为多个阶段。每个阶段,我们都会面对一个岔路口,我们先随意选一条路走,当发现这条路走不通的时候(不符合期望的解),就回退到上一个岔路口,另选一种走法继续走。
这显然是一个 递归 结构;
depth
,或者命名为 index
,表示当前要确定的是某个全排列中下标为 index
的那个数是多少;used[num] = true
,这样在考虑下一个位置的时候,就能够以 O(1)的时间复杂度判断这个数是否被选择过,这是一种「以空间换时间」的思想。let permute = function(nums) {
// 使用一个数组保存所有可能的全排列
let res = []
if (nums.length === 0) {
return res
}
let used = {}, path = []
dfs(nums, nums.length, 0, path, used, res)
return res
}
let dfs = function(nums, len, depth, path, used, res) {
// 所有数都填完了
if (depth === len) {
res.push([...path])
return
}
for (let i = 0; i < len; i++) {
if (!used[i]) {
// 动态维护数组
path.push(nums[i])
used[i] = true
// 继续递归填下一个数
dfs(nums, len, depth + 1, path, used, res)
// 撤销操作
used[i] = false
path.pop()
}
}
}
时间复杂度: O(n∗n!),其中 n 为序列的长度
这是一个排列组合,每层的排列组合数为:Amn=n!/(n−m)! ,故而所有的排列有 :
A1n + A2n + … + An-1n = n!/(n−1)! + n!/(n−2)! + … + n! = n! (1/(n−1)! + 1/(n−2)! + … + 1) <= n! (1 + 1/2 + 1/4 + … + 1/2n-1) < 2 * n!
并且每个内部结点循环 n 次,故非叶子结点的时间复杂度为 O(n∗n!)
空间复杂度:O(n)
const permMutation = function (arr) {
let res = [];
if (arr.length <= 1) {
return arr;
}
for (let i = 0; i < arr.length; i++) {
let indexStr = arr[i];
let otherStr = [...arr.slice(0, i), ...arr.slice(i + 1)];
let tmp = permMutation(otherStr);
for (let j = 0; j < tmp.length; j++) {
res.push(Array.prototype.flat.call([indexStr, tmp[j]], Infinity));
}
}
return res;
}
var permute = function(nums) {
let len = nums.length;
if(len === 0) return [[]];
let res = [];
let perm = function(arr, p, q, res){
if(p === q){
res.push([...arr])
}
for(let i = p; i <= q; i++){
swap(arr, i, p);
perm(arr, p+1, q, res);
swap(arr, i, p);
}
}
let swap = function(arr, left, right){
let temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
perm(nums, 0, len-1, res)
return res;
};
题解 1.分析示列,不难发现全排列,首先将每个元素排到第一位。 2.剩余元素再重复第一步操作,依次处理各个元素。 3.通过1,2两步操作,可以利用递归实现操作,但是有个地方需要特别处理,就是对于移动的元素, 在递归操作之前,和之后该如何操作。 4.由此产生两种解决办法,一种是利用回溯,定义回溯状态标识,在递归操作完成后,重置状态 一种是利用swap函数(交换元素位置);相对来说,swap函数更好理解一些; 5.递归函数实现: 递归终止条件: 1.swap 子全排列的数组长度等于nums的长度时,递归终止。 2.dfs 子全排列所在层数(depth)等于nums的长度时,递归终止。
循环数组内部元素: 1.swap 通过swap,将nums[i] 与 p(从0开始)交换 调用递归函数prem,传入 nums, p+1, nums.lenght, res(结果集) 递归内部可以理解为,例如1,2,3 先将1取出,求解剩余2,3 的全排列,依次类推 当递归终止后,再次调用swap函数,将数组重置成原始状态;
2.dfs dfs函数中传入nums,len,depth(当前所在层数),path(存储当前排列结果的动态数组), used(元素是否已存在排列中的布尔标识), res(结果集) 调用递归dfs,判断当前元素是否在排列中used[i] === true ?,存在则跳过,不存在则 used[i] = true, 将当前nums[i] 存入path中,然后调用dfs dfs终止后,***重点来了,回溯状态需要重置,used[i] = false,path.pop() 将当前元素从已有排列中删除
全排列属于回溯入门问题,回溯还是需要多学多练
swap解法
var permute = function(nums) {
let len = nums.length;
if(len === 0) return [[]];
let res = [];
let perm = function(arr, p, q, res){
if(p === q){
res.push([...arr])
}
for(let i = p; i < q; i++){
swap(arr, i, p);
perm(arr, p+1, q, res);
swap(arr, i, p);
}
}
let swap = function(arr, left, right){
let temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
perm(nums, 0, len, res)
return res;
};
dfs解法
var permute = function(nums) {
let len = nums.length;
if(len === 0) return [[]];
let res = [];
let path = []; //维护动态数组
let used = {}; //保存已存在的元素
let dfs = function(arr, len, depth, path, used, res){
if(len === depth){
res.push([...path]);
return
}
for(let i = 0; i < len; i++){
if(!used[i]){
path.push(arr[i]);
used[i] = true;
dfs(arr, len, depth+1, path, used, res)
//状态回溯
used[i] = false;
path.pop();
}
}
}
dfs(nums, len, 0, path, used, res);
return res;
}
const permute = nums => { const res = []; const len = nums.length; const curr = []; const use = {}; if (curr.length === len) { res.push(curr); }
function dfs (nth) { for (let i = 0; i < len; i++) { if (!use[i]) { use[i] = 1; curr.push(nums[i]); dfs(i); curr.pop(); use[i] = 0; } } }
dfs(0); return res; };
感觉递归函数不需参数也可以
function fn(arr) {
const path = [], used = {}
const res = []
const loopFn = () => {
if (path.length === arr.length) {
res.push([...path])
return
}
for (let i = 0, len = arr.length; i < len; i++){
if (!used[i]) {
path.push(arr[i])
used[i] = true
loopFn()
path.pop()
used[i] = false
}
}
}
loopFn()
return res
}
function bar(list) {
const res = [];
function dfs(arr = []) {
if (arr.length === list.length) {
res.push([...arr]);
return;
}
for (const item of list) {
if (arr.indexOf(item) === -1) {
arr.push(item);
dfs(arr);
arr.pop();
}
}
}
dfs([]);
return res;
}
const res = bar([1, 2, 3]);
console.log(res);
/**
* 回溯算法
* @param {number[]} nums
*/
function permute(nums) {
let res = []
function dfs(track) {
// 到达决策树底部
if (track.length === nums.length) {
res.push([...track])
return
}
for (let num of nums) {
// 剪枝操作,避免重复遍历
if (track.includes(num)) {
continue
}
track.push(num)
dfs(track)
track.pop()
}
}
dfs([])
return res
}
console.log(permute([1,2,3]));
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
附赠leetcode地址:leetcode