不会全排列算法(Javascript实现),我教你呀!

今天我很郁闷,在实验室凑合睡了一晚,准备白天大干一场,结果一整天就只做出了一道算法题。看来还是经验不足呀,同志仍需努力呀。

算法题目要求是这样的:

Return the number of total permutations of the provided string that don‘t have repeated consecutive letters. Assume that all characters in the provided string are each unique.For example, aab should return 2 because it has 6 total permutations (aabaababaababaa,baa), but only 2 of them (aba and aba) don‘t have the same letter (in this case a) repeating.

看到这个题目的第一反应就是赶紧扣扣大脑里关于排列组合的各种基础知识和公式,首先就是从上面的语句中抽象出一个数学模型:

n个队伍排成一排,每个队伍ai个人,每个人互不相同,相同队伍的人不能相邻,求可能排列数.

对于“不相邻”问题,我们的一般会采用“插空法”,举个简单的例子:

这里有三个笑脸和两个哭脸,每张脸互不相同,现在要将它们排成一排,要求相同脸型的小伙伴不能相邻。排列的详细步骤如下:

  1. 将三个笑脸排成一排,有3*2*1种排法,并且形成了4个间隔 —
  2. 将两个哭脸插入到四个间隔中,但是由于要去相同脸型两两不相邻,所以这两个哭脸只能插入到中间两个间隔,此时有两种方案
  3. 所以总的方案数为12种。

按照这个思路,我先统计了字符串中每个字符出现的个数,然后试图通过“插空法”求出排列方案总数,却发现对于已知有限组的排列这个方案还是可枚举的,但是对于未知组时,计算相当混乱。就这样折腾了一上午也没有求出来。

解决不了问题,我的心总是放不下,下午再战。我看到题目下方有一个提示:

这个提示让我转变了思路,我们是在编程解决问题,不是在解算数学题,首先我们可以将所有的排列组合求出来,不管存不存在相不相邻的情况,然后使用正则表达式过滤掉相邻的情况不就解决问题了吗。现在问题就转变成了求一组给定字符的全排列问题,这就引出了这篇博客的重点。首先介绍一种普通的递归方法。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

function permutate(str) {

    var result=[];

    if(str.length==1){

        return [str]   

    }else{

    

            var preResult=permutate(str.slice(1));

            for (var j = 0; j < preResult.length; j++) {

                for (var k = 0; k < preResult[j].length+1; k++) {

                    var temp=preResult[j].slice(0,k)+str[0]+preResult[j].slice(k);             

                    result.push(temp);             

                }

            }

        return result;

    

}

console.log(permutate("abc"));

如果能直接看懂上面的代码,就可以忽略下面的解析过程,下面的解析过程参考了全排列算法的JS实现,这篇博客写的调理很清晰,但是有一点点错误导致结果不完全正确,我在下面改正了过来:

实现过程

首先明确函数的输入和输出,输入是一个字符串,输出是各种排列组合形成的字符串组成的数组,所以函数的大体框架应该是这样的:


1

2

3

4

5

function permutate(str) {

    var result = [];

 

    return result;

}

然后,确定是用递归的形式解决。递归的解法,倒过来想就是数学归纳法:第一步,给出基础值,比如输入为1的时候输出应该是成立的。第二步,假设对于输入n成立,证明输入n+1时也成立。好了,所以先来完成第一步。对这个问题而言,基础情况应该是输入字符串为单个字符时的情况。这个时候输出应该是什么呢。当然是输入本身。但是,不要忘了输出应该是数组形式,所以接下来的样子:


1

2

3

4

5

6

7

8

9

10

function permutate(str) {

    var result = [];

    if(str.length===1){

      return [str];

    }else{

      ...

      return result;

    }

    

}

接着进行第二步,假设我们已经知道了n-1的输出,要由这个输出得出n的输出。在这个问题里,n-1的输入,对应着长度比当前输入的字符串少1的输入字符串。也就是说,如果我已经知道了“abc”的全排列输出的集合,现在再给你一个“d”,要怎样得出新的全排列呢?

很简单,只要对于集合中每一个元素,把d插入到任意相邻字母之间(或者头部和尾部),就可以得到一个新的排列。例如对于元素“acb”,插入到第一个位置,即可得到“dacb”,插入其余位置,可得到“adcb”,“acdb”,“acbd”。这也就是上文提到的"插空法"的思想,这不过这里我们不用考虑是否相邻的问题,所以操作起来会比较方便。

在这里,对于每一个输入的str,我们把它分为两部分,第一部分为字符串的第一个字母str[0],(注意ES5之前是不能直接通过下标来访问字符的,需要使用codeAt()方法,这里没有考虑兼容性仅做演示用)第二部分为剩余的字符串str.slice(1),根据以上的假设,现在可以把 permutate(str.slice(1)) 作为一个已知量看待。


1

2

3

4

5

6

7

8

9

10

11

12

13

function permutate(str) {

    var result=[];

    if(str.length==1){

        return [str]   

    }else{  

        var preResult=permutate(str.slice(1));

        ...

....

        return result;

    

}

接着对permutate(str.slice(1))里的每一个排列进行处理,将str[0]插入到每一个位置中,每得到一个排列,便将它push到result里面去。


1

2

3

4

5

6

for (var j = 0; j < preResult.length; j++) {

     for (var k = 0; k < preResult[j].length+1; k++) {

             var temp=preResult[j].slice(0,k)+str[0]+preResult[j].slice(k);

              result.push(temp);             

     }

}

在读懂上述代码时,时刻不要忘了preResult是个什么样的数组,当递归到最后一个字符时,preResult为[ ‘c‘ ],再上一层的为[ ‘bc‘, ‘cb‘ ]。

上述代码比较难理解的是:


1

var temp=preResult[j].slice(0,k)+str[0]+preResult[j].slice(k);

这里是将str[0]插入到上一次的某个排列方案结果中,采用的是字符串拼接的方案,返回一个新字符串给temp,注意这里不能直接在preResult[j]上操作,否则会修改preResult[j]的长度导致内层的循环永远不接结束。

另外需要注意的是代码中高亮的部分preResult[j].length+1这里必须加上1,考虑到slice()方法的截取范围是“左闭右开”区间,这样当k取值为preResult[j].length时才能将str[0]添加到字符串尾部。

通过上面的过程,我们就能求出给定字符串形成的排列组合的所有情况:[ ‘abc‘, ‘bac‘, ‘bca‘, ‘acb‘, ‘cab‘, ‘cba‘ ]

不要忘了,这不是我们的最终目的,我们的最终目的是找出所有不相邻的情况。这个问题可以很方便的采用正则表达式来过滤:


1

var regex = /(.)\1+/g;

这个正则表达式使用了一个回溯操作匹配前面的字符出现一次否则多次。这样我们就能完整的解决问题了,完整的代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

//同一个字母不相邻的排列组合

/*先组合出所有的情况,再使用正则表达式过滤掉不符合的情况*/

function permAlone(str) {

    var regex = /(.)\1+/g;

    var permutate=function(str) {

        var result=[];

        if(str.length==1){

            return [str];  

        }else{

            

                var preResult=permutate(str.slice(1));

                for (var j = 0; j < preResult.length; j++) {

                    for (var k = 0; k < preResult[j].length+1; k++) {

                        var temp=preResult[j].slice(0,k)+str[0]+preResult[j].slice(k);                 

                        result.push(temp);

                    }

                }

            return result;

        

    };

    var permutations= permutate(str);

    var filtered = permutations.filter(function(string) {

        return !string.match(regex);

    });

    return filtered.length;

}

console.log(permAlone(‘aab‘));

参考:

全排列算法的JS实现 - 迷路的约翰 - 博客园


扩展阅读:

JS实现的数组全排列输出算法_javascript技巧_脚本之家

JavaScript全排列的六种算法 具体实现_javascript技巧_脚本之家


来自为知笔记(Wiz)

时间: 2024-10-04 14:27:32

不会全排列算法(Javascript实现),我教你呀!的相关文章

全排列算法 --javascript 实现

(function(){ var ret = new Array(); var A = function a(str){ if(str == undefined || str == null){return new Array();} if(str.length < 2) {return new Array(str);} if(str.length == 2) {return new Array(str[0]+str[1],str[1]+str[0]);} for(var k = 0;k <

全排列算法(转)

列出全排列的初始思想: 解决一个算法问题,我比较习惯于从基本的想法做起,我们先回顾一下我们自己是如何写一组数的全排列的:1,3,5,9(为了方便,下面我都用数进行全排列而不是字符). 1,3,5,9.(第一个) 首先保持第一个不变,对3,5,9进行全排列. 同样地,我们先保持3不变,对5,9进行全排列. 保持5不变,对9对进行全排列,由于9只有一个,它的排列只有一种:9.接下来5不能以5打头了,5,9相互交换,得到 1,3,9,5. 此时5,9的情况都写完了,不能以3打头了,得到 1,5,3,9

数据结构与算法javascript描述

<数据结构与算法javascript描述>--数组篇 导读: 这篇文章比较长,介绍了数组常见的操作方法以及一些注意事项,最后还有几道经典的练习题(面试题). 数组的定义: JavaScript 中的数组是一种特殊的对象,用来表示偏移量的索引是该对象的属性,索引可能是整数.然而,这些数字索引在内部被转换为字符串类型,这是因为 JavaScript 对象中的属性名必须是字符串.在内部被归类为数组.由于 Array 在 JavaScript 中被当作对象,因此它有许多属性和方法可以在编程时使用. 使

两种常用的全排列算法(java)

问题:给出一个字符串,输出所有可能的排列. 全排列有多种算法,此处仅介绍常用的两种:字典序法和递归法. 1.字典序法: 如何计算字符串的下一个排列了?来考虑"926520"这个字符串,我们从后向前找第一双相邻的递增数字,"20"."52"都是非递增的,"26 "即满足要求,称前一个数字2为替换数,替换数的下标称为替换点,再从后面找一个比替换数大的最小数(这个数必然存在),0.2都不行,5可以,将5和2交换得到"956

数据结构与算法javascript描述笔记--数组篇

数组的定义: JavaScript 中的数组是一种特殊的对象,用来表示偏移量的索引是该对象的属性,索引可能是整数.然而,这些数字索引在内部被转换为字符串类型,这是因为 JavaScript 对象中的属性名必须是字符串.在内部被归类为数组.由于 Array 在 JavaScript 中被当作对象,因此它有许多属性和方法可以在编程时使用. 使用数组: 1.创建数组 ① 使用 [] 操作符 ,var arr=[] ,该方法效率最高. ② 调用 Array 的构造函数创建数组,var myArr=new

翻阅《数据结构与算法javascript描述》--数组篇

导读: 这篇文章比较长,介绍了数组常见的操作方法以及一些注意事项,最后还有几道经典的练习题(面试题). 数组的定义: JavaScript 中的数组是一种特殊的对象,用来表示偏移量的索引是该对象的属性,索引可能是整数.然而,这些数字索引在内部被转换为字符串类型,这是因为 JavaScript 对象中的属性名必须是字符串.在内部被归类为数组.由于 Array 在 JavaScript 中被当作对象,因此它有许多属性和方法可以在编程时使用. 使用数组: 1.创建数组 使用 [] 操作符 ,var a

数据结构与算法JavaScript (三) 链表

数据结构与算法JavaScript (三) 链表 我们可以看到在javascript概念中的队列与栈都是一种特殊的线性表的结构,也是一种比较简单的基于数组的顺序存储结构.由于 javascript的解释器针对数组都做了直接的优化,不会存在在很多编程语言中数组固定长度的问题(当数组填满后再添加就比较困难了,包括添加删除, 都是需要把数组中所有的元素全部都变换位置的,javascript的的数组确实直接给优化好了,如 push,pop,shift,unshift,split方法等等…) 线性表的顺序

数据结构与算法JavaScript (四) 串(BF)

串是由零个或多个字符组成的有限序列,又叫做字符串 串的逻辑结构和线性表很相似的,不同的是串针对是是字符集,所以在操作上与线性表还是有很大区别的.线性表更关注的是单个元素的操作CURD,串则是关注查找子串的位置,替换等操作. 当然不同的高级语言对串的基本操作都有不同的定义方法,但是总的来说操作的本质都是相似的.比如javascrript查找就是indexOf, 去空白就是trim,转化大小写toLowerCase/toUpperCase等等 这里主要讨论下字符串模式匹配的几种经典的算法:BF.BM

数据结构与算法JavaScript (五) 串(经典KMP算法)

数据结构与算法JavaScript (五) 串(经典KMP算法) KMP算法和BM算法 KMP是前缀匹配和BM后缀匹配的经典算法,看得出来前缀匹配和后缀匹配的区别就仅仅在于比较的顺序不同 前缀匹配是指:模式串和母串的比较从左到右,模式串的移动也是从 左到右 后缀匹配是指:模式串和母串的的比较从右到左,模式串的移动从左到右. 通过上一章显而易见BF算法也是属于前缀的算法,不过就非常霸蛮的逐个匹配的效率自然不用提了O(mn),网上蛋疼的KMP是讲解很多,基本都是走的高大上路线看的你也是一头雾水,我试