CodeSky 代码之空

随手记录自己的学习过程

JavaScript 排列与组合

2016-05-13 23:33分类: JavaScript评论: 0

排列与组合在我们日常生活中其实也是挺常用的,包括在算法中,也算是比较实用的东西,但是排列组合也是一个难点,这里我们先从简单的出发。

最简单的是全排列和全组合,其中组合的思路更加简单易行,因此我们先从全组合说起。

在说之前,我们先来确定一下定义(数学渣只能从自己下定义开始以免误导读者),我们的全排列,指的是PNN,也就是有n个数字,坑也有n个,求全部的排列情况;全组合,则是除了空集外,n个字母能组成多少种可能性(不一定要全部使用)。

由此,我们开始了我们的全组合之旅:

首先,我们考虑,假设有n个字母,我们将其使用标记为1,没有使用过标记为0.则可以得出[a, b, c] [0, 0, 1]=> [c]。那么同理,我们很快就能够列出所有的情况,就是把1和0全部尝试一遍,那么当abc时,数组为全1,此时,其实我们就能够想到,跟二进制非常像,全1时为7,全0为0。

那么问题就简单了,就是把全组合转换为到n的二进制,将二进制对应的结果输出:

简单列表来说明问题:

c b a
0 0 1 => a
0 1 0 => b
0 1 1 => ab
1 0 0 => c
1 0 1 => ca
1 1 0 => cb
1 1 1 => abc

程序也比较简单:

1var combinate = function(listArr, callback) {
2    var result,
3        temp,
4        left,
5        max = Math.pow(2, listArr.length),
6        i;
7
8    for (i = 1; i < max; i++) {
9        temp = i;
10        result = listArr.filter(function() {
11            var result = false;
12            left = temp % 2;
13            if (left !== 0 && temp !== 0) {
14                result = true;
15            }
16            temp = parseInt(temp / 2);
17
18            return result;
19        });
20
21        callback(result);
22    }
23};
24

filter过滤数组结果,只有为true时才进入结果数组。

当然,这个明显没有考虑去重,没关系,接下来我们会在之后的程序中考虑去重的,先进入简单的排列,排列的思想是递归的交换前一项和后一项,算法比较神奇,但如果我们在草稿纸上仔细写下来,会发现真的是。之后就好办多了:

1/**
2 * 交换一个数组中的第i项和第j项(起始项为0)
3 * @param arr
4 * @param i
5 * @param j
6 */
7var swap = function(arr, i, j) {
8    var length = arr.length,
9        temp;
10
11    temp = arr[i];
12    arr[i] = arr[j];
13    arr[j] = temp;
14};
15
16/**
17 * 全排列使用的是递归交换数组中的数字
18 * @param listArr
19 * @param start 开始的值,调用时输入0
20 * @param callback
21 */
22var permutate = function(listArr, start, callback) {
23    var i,
24        length = listArr.length;
25
26    if (start === listArr.length - 1) {
27        callback(listArr);
28    } else {
29        for (i = start; i < length; i++) {
30            swap(listArr, start, i);
31            permutate(listArr, start + 1, callback);
32            swap(listArr, start, i);
33        }
34    }
35};
36

循环中第二次交换是为了还原现场。

之后我们考虑除了[a, b, c]之外,还有可能是[a, b, b]的情况,因此我们需要对过程或者结果中进行去重,这里先从过程去重开始考虑。之后我们会说结果去重。

过程去重的算法主要考虑在交换过程中选中了相同内容,不同位置的字符,因此只要只选中他们中的一个,忽略其他即可,通常来说我们都选择特殊位置,第一个或者最后一个,那么只要判断是否是第一个或者最后一个即可。

1/**
2 * 检测是否可以交换
3 * 查看交换的值在之后是否有重复出现,需求是只和特定的value仅交换一次,如果有重复则不交换
4 * 此时有两种思路,一种是首次交换,之后抛弃所有同value的值(此时我们可以提供向前检测的方法)
5 * 第二种是最后交换,在此之前所有值都不进行交换(这时我们使用向后检测的方法)
6 * 这里我们使用向后检测,保证这是最后一个
7 * @param arr
8 * @param i
9 * @param j
10 */
11var canSwap = function(arr, i, j) {
12    return arr.slice(i, j).indexOf(arr[j]) === -1;
13};
14

之后,在全排列中我们加入判断,即可达到去重的效果:

1/**
2 * 去重的全排列
3 * @param listArr
4 * @param start
5 * @param callback
6 */
7var permutateWithoutTheSame = function(listArr, start, callback) {
8    var i,
9        length = listArr.length;
10
11    if (start === listArr.length - 1) {
12        callback(listArr);
13    } else {
14        for (i = start; i < length; i++) {
15            // 检测是否需要交换
16            if (canSwap(listArr, start, i)) {
17                swap(listArr, start, i);
18                arguments.callee(listArr, start + 1, callback);
19                swap(listArr, start, i);
20            }
21        }
22    }
23};
24

这样挺直观,缺点是效率很低,毕竟每次都要找index,相当于循环了一遍,相当耗时,在结果去重中,我们将选择更快的方式。

先来说说一般的排列和组合,我们发现,排列或者组合,这个程度的还是不足以满足我们的需求,毕竟平时组合我们可能是固定个数的,排列也不一定要求全排列。

还是先从组合开始,我们依旧从二进制下手:

可以来看一下这一段:

  1   2   3   4   5
  1   1   1   0   0   //1,2,3     
  1   1   0   1   0   //1,2,4     
  1   0   1   1   0   //1,3,4     
  0   1   1   1   0   //2,3,4     
  1   1   0   0   1   //1,2,5     
  1   0   1   0   1   //1,3,5     
  0   1   1   0   1   //2,3,5     
  1   0   0   1   1   //1,4,5     
  0   1   0   1   1   //2,4,5     
  0   0   1   1   1   //3,4,5  

以上是他的全组合,我们可以看到,找到1前面的0,与其交换,把其他1归于从0开始的最左端,实现一次循环。

看懂了这个,接下来就能做了:

1/**
2 * 部分组合
3 * 我们不需要全部的组合,只需要指定selectNum个空去进行组合
4 * @param listArr
5 * @param selectNum
6 * @param callback
7 */
8var combinateWithoutAll = function(listArr ,selectNum, callback) {
9    var listCount = [], // 计数器数组
10        result = [],
11        length = listArr.length,
12        oneCount = 0,   // 统计出现过的1
13        i,
14        canPrint = true;
15
16    init(listCount, selectNum, length - 1);
17
18    while (checkEnd(listCount, selectNum)) {
19        store(listArr, listCount, result);
20
21        if (canPrint) {
22            callback(result);
23        }
24
25        canPrint = true;
26
27        for (i = 0; i < length; i++) {
28
29            if (listCount[i] === 0 && oneCount !== 0) {
30
31                listCount[i - 1] = 0;
32                listCount[i] = 1;
33
34                // 去除重复的无用项
35                if (!canSwap(listArr, 0, i)) {
36                    canPrint = false;
37                }
38
39                init(listCount, oneCount - 1, i - 2);
40                oneCount = 0;
41                break;
42
43            } else if (listCount[i] === 1) {
44                oneCount++;
45            }
46
47        }
48    }
49
50
51    store(listArr, listCount, result);
52    callback(result);
53
54    /**
55     * 对 0 / 1结构进行初始化
56     * 前num个为 1
57     * 直到end为止为0
58     * @param listCount
59     * @param num
60     * @param end
61     */
62    function init (listCount, num, end) {
63        var i = 0;
64        for (i = 0; i < num; i++) {
65            listCount[i] = 1;
66        }
67
68        for (; i <= end; i++) {
69            listCount[i] = 0;
70        }
71    }
72
73    /**
74     * 存储结果
75     * @param listArr
76     * @param listCount
77     * @param result
78     */
79    function store(listArr, listCount, result) {
80        var length = listCount.length,
81            i,
82            j = 0;
83
84        for (i = 0; i < length; i++) {
85            if (listCount[i] === 1) {
86                result[j++] = listArr[i];
87            }
88        }
89    }
90
91    /**
92     * 检查是否是最后一个组合情况
93     * @param listCount
94     * @param selectNum
95     * @returns {boolean}
96     */
97    function checkEnd(listCount, selectNum) {
98        var length = listCount.length,
99            i = length - 1,
100            end = length - selectNum;
101        for (i = length - 1; i >= end; i--) {
102            if (listCount[i] !== 1) {
103                return true;
104            }
105        }
106
107        return false;
108    }
109};
110

这里我们顺便实现了一个去重,如果不需要的话把去重的几行删掉即可(同时效率也提高了)。

对于排列,我们想到了一个办法:把组合进行全排列,将全排列函数作为回调传入即可。

另一种方法,对于全排列,选出每个排列中的前n项截断,之后进行去重。

这里我们说了半天的结果去重终于要登场了,我们使用一个利器:哈希表。在JavaScript也就是对象,我们把数组转换为字符串,作为index,然后把index取出即可。

1/**
2 * 利用全排列的思路,先截断后去重
3 * @param listArr
4 * @param selectorNum
5 * @param callback
6 */
7var permutateWithoutAll = function(listArr, selectorNum, callback) {
8    var resultHash = {},
9        result = [],
10        i = 0;
11
12    permutateWithoutTheSame(listArr, 0, function(arr) {
13        var index = arr.slice(0, selectorNum).join(',');
14        resultHash[index] = '';
15    });
16
17    for (var index in resultHash) {
18        if (resultHash.hasOwnProperty(index)) {
19            result[i++] = index.split(',');
20            callback(result[i - 1]);
21        }
22    }
23};
24

效率比起原来肯定是高了不少,原来我们的去重也可以通过这个思路来处理。

之后我们用系统的time来统计了一下运行时间,发现大量时间都花在了去重上,确实在过程中去重还是很坑的,另外一点,由于是递归调用,容易stackoverflow,只能适合少量的排列,大量就不适合了,其他排列的方法以后有空再进行学习。

另外,如果只是需要一组随机字串,可以使用洗牌程序。

扩展阅读:

全排列和全组合实现

高效率的排列组合算法--《编程珠矶》--python实现

全排列的六种算法

评论 (0)