全排列算法

3,850 阅读3分钟

前言

全排列问题在笔试中出现过两三次。

递归求解

首先我们最容易想到递归算法。

将字符串的每一个字符交换到第一个,再将其他部分全排列,使用第一个字符连接上后面全排列的所有可能字符串。

时间复杂度分析

T(n)=\begin{cases}
O(1) &n=1 \\
n\times T(n-1) &n>1
\end{cases}

我们还是使用递推方程帮助我们思考并求解

\begin {aligned}
T(n)=n\times T(n-1)=n\times (n-1)\times ...\times 2\times O(1)=n!
\end {aligned}

代码实现

const getAllPermutations1 = (str) => {
  if (!str.length || str.length === 1) {
    return [str];
  }
  let strArr = Array.from(str);
  let resArr = [];
  strArr.forEach((v, i, arr) => {
    let temp = arr.slice();
    let header = temp.splice(i, 1);
    permutation(temp.join('')).forEach(v => {
      resArr.push([header, ...v].join(''));
    });
  });
  return resArr;
}

字典序求解

上网查找之后,还发现一种利用字典顺序的解法,先举一个例子,看这个解法的操作过程,假设求字符串123的全排列:

起点:123,终点:321,字典序的关键在于下一个排列基于前一个排列,且只比前一个排列大一点点。这个一点点的实现在于每次从倒数第二个元素开始,向后找比它刚好大一点的元素交换,交换之后,让该元素之后的部分元素升序排列,使得其排列刚好比前一个排列大一点点。如果向后找没找到,则从倒数第三个元素开始,以此类推。。。如果没懂,直接看下面一个例子。

  1. 1232向后找比它大一点点的元素3,交换为132,并将3之后部分元素升序排列,结果还是132
  2. 1323向后没找到比它小的,则从1开始向后找,找到比它刚好大一点点的元素2,交换为231,再将2后面的部分元素升序排列为213
  3. ...
  4. 最后到3213后面找不到比它更大的元素了,此时循环结束,算法完成

总结出算法的过程如下:

  1. 对输入字符串先进行升序排列
  2. 升序序列作为起点
  3. 输入当前排列,根据当前排列计算出下一轮排列
    1. 从当前排列的倒数第二个元素开始向后找一个刚好比它大的元素交换,交换后将它之后的部分元素升序排列,此时得到下一轮序列,返回即可
    2. 上面一步如果找不到则从倒数第三个元素开始,重复上面一步,以此类推
    3. 如果走到了第一个元素还找不到,说明该排列到达终点,算法结束
const getAllPermutations2 = (str) => {
  const strArr = Array.from(str);
  const resArr = [];
  let temp = strArr.sort();
  resArr.push(temp);
  temp = getNextPermutation(temp);
  while(temp) {
    resArr.push(temp);
    temp = getNextPermutation(temp);
  }
  console.log(resArr.map(item => item.join('')));
}

// 根据前一个排列获得下一个排列
const getNextPermutation = (perm) => {
  if(perm.length===0 || perm.length===1) {
    return;
  }
  let len = perm.length;
  let isFind = null;
  let nextPerm = null;
  while(len > 0) {
    let tempPerm = perm.slice();
    isFind = findAndSwap(tempPerm, len - 2); // 从倒数第二个元素开始,向后找刚好比它大的元素
    if (isFind) {
      nextPerm = sort(tempPerm, len-1); // 如果找到,交换后将该元素后面的元素升序排列
      return nextPerm;
    }
    len--; // 没找到则向前一个元素,继续找
  }
  return;
}

// 将index及其之后的元素进行升序排列
const sort = (arr, index) => {
  let sortPart = arr.splice(index, arr.length-index).sort();
  return [...arr, ...sortPart];
}

// 从当前数(index)后面找一个刚好比它大的数,并交换位置,找到返回true,找不到false
const findAndSwap = (arr, index) => {
  // 一个元素不需要找,直接false
  if(index < 0) {
    return false;
  }
  let cur = index + 1;
  let nearstIndex = null;
  while(cur!==arr.length) {
    if(arr[cur] > arr[index]) {
      // 第一次直接更新nearstIndex, 之后需要与之前的比较,更小则更新,否则不变
      nearstIndex = !nearstIndex ? cur : arr[cur] < arr[nearstIndex] ? cur : nearstIndex;
    }
    cur++;
  }
  if(nearstIndex) {
    let temp = arr[nearstIndex];
    arr[nearstIndex] = arr[index];
    arr[index] = temp;
    return true; 
  }
  return false;
}

allPermutations('cbda');