全排列演算法的JS實現

迷路的約翰發表於2015-11-08

問題描述:給定一個字串,輸出該字串所有排列的可能。如輸入“abc”,輸出“abc,acb,bca,bac,cab,cba”。

雖然原理很簡單,然而我還是折騰了好一會才實現這個演算法……這裡主要記錄的是解決問題中的思路。

我實現的是最普通的遞迴演算法,也沒有除重,嗯非遞迴及除重的演算法以後再補上吧。

 

實現過程

首先明確函式的輸入和輸出,輸入是一個字串,輸出麼對於JS而言用陣列來表示最恰當了,所以函式的雛形應該是這樣的:

function permutate(str) {
    var result = [];

    return result;
}

 然後,確定是用遞迴的形式解決。遞迴的解法麼,其實就是數學歸納法的尋找規律那一步。數學歸納法是什麼樣來著:第一步,給出基礎值,比如輸入為1的時候輸出應該是成立的。第二步,假設對於輸入n成立,證明輸入n+1時也成立。

好了,所以先來完成第一步。對這個問題而言,基礎情況應該是輸入字串為單個字元時的情況。這個時候輸出應該是什麼呢。當然是輸入本身。但是,不要忘了輸出應該是陣列形式,所以接下來的樣子:

function permutate(str) {
    var result = [];

    if(str.length > 1) {
} else if (str.length == 1) { return [str]; } return result; }

 中間用了else if 而沒有用else的原因是不清楚到最後是否要處理空字串的情況,所以先留著個else。邏輯上應該位於第一個if裡的return語句,放到了最後,比較清晰。

接著進行第二步,假設我們已經知道了n-1的輸出,要由這個輸出得出n的輸出。在這個問題裡,n-1的輸入,對應著長度比當前輸入的字串少1的輸入字串。也就是說,如果我已經知道了“abc”的全排列輸出的集合,現在再給你一個“d”,要怎樣得出新的全排列呢?

很簡單,只要對於集合中每一個元素,把d插入到任意相鄰字母之間(或者頭部和尾部),就可以得到一個新的排列。例如對於元素“acb”,插入到第一個位置,即可得到“dacb”,插入其餘位置,可得到“adcb”,“acdb”,“acbd”。容易證明這樣形成的新元素不會有重複。

在這裡,對於每一個輸入的str,我們把它分為兩部分,第一部分為字串的第一個字母,定義為left,第二部分為剩餘的字串,定義為rest,根據以上的假設,現在可以把 permutate(rest) 作為一個已知量看待。

function permutate(str) {
    var result = [];

    if(str.length > 1) {
        var left = str[0];
        var rest = str.slice(1, str.length);
        var preResult = permutate(rest);
        /*
          Do some operation
        */
    } else if (str.length == 1) {
        return [str];
    }

    return result;
}

接著對permutate(rest)裡的每一個排列進行處理,將left插入到每一個位置中,每得到一個排列,便將它push到result裡面去。

......
    for(var i=0; i<preResult.length; i++) {
        for(var j=0; j<preResult[i].length; j++) {
            var tmp = preResult[i],slice(0, j) + left + preResult[i].slice(j, preResult[i].length);
            result.push(tmp);
        }
    }
......

 有了字串自帶的slice方法省了不少事。一開始想到插入字串想到的是splice方法,然而這個方法會對原始字串進行修改,要是用它的話會出很無奈的bug……

到這裡就告一段落了,把上面的片段插入到前面的註釋的位置就是完整程式碼了。

然後這個函式對於空字串的輸入會輸出空字串,所以前面else if的if也可以去掉。

 

另一個問題

說起全排列我還想到了另一個問題:

給定一個數字字串,輸出組成這個字串的每個數字的所有組合中,比當前數字大的下一個數字。

舉例:對於輸入“113”,它的所有不重複的排列組合為“113”,“131”,“311”,那麼其中比當前數字大的下一個數字為“131”,所以輸出為“131”。

我想到的第一個解法,首先求出當前字串的所有排列組合,然後對返回的結果排序,再求出當前數字所在下標的下一個。

程式的樣子差不多應該是這樣:

function permutate() {
	//......
	return result;
}

function main(str) {
	var all = permutate().sort();
	var targetIndex = all.indexOf(str)+1;
	return result[targetIndex];
}

 思路非常直接,然而這個演算法的缺點也是很明顯的:複雜度太高了。對於輸入長度為 n 的字串,光是排列演算法就得計算 n!次。

所以這個想法可以pass。

實際上比較正確的演算法是可以在O(n)的複雜度內求出結果的。不過在這裡就不詳細說明了。各種解決途徑可以點選以下連結檢視:

連結 (注:註冊了codewars賬號並給出一種解法後方可檢視對應的solution)

之所以提到這個問題,是因為雖然不推薦使用全排列的方法解決這個問題,但是我們可以通過這個問題的解法,反過來給出全排列的一種非遞迴式解法。

例如我們要給出字串“aabccdde"的全排列,可以把對應字母替換為“11233445”,然後呼叫上面問題的解法,依次輸出每一個排列即可。因為本身這個演算法的複雜度很低,所以不會影響到最終全排列演算法的複雜度。

缺點麼,如果有十個以上的不同字元,那就沒有辦法了……

 

相關文章