今天我很郁悶,在實驗室湊合睡了一晚,准備白天大干一場,結果一整天就只做出了一道算法題。看來還是經驗不足呀,同志仍需努力呀。
算法題目要求是這樣的:
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,
aabshould return 2 because it has 6 total permutations (aab,aab,aba,aba,baa,baa), but only 2 of them (abaandaba) don't have the same letter (in this casea) repeating.
看到這個題目的第一反應就是趕緊扣扣大腦里關於排列組合的各種基礎知識和公式,首先就是從上面的語句中抽象出一個數學模型:
n個隊伍排成一排,每個隊伍ai個人,每個人互不相同,相同隊伍的人不能相鄰,求可能排列數.
對於“不相鄰”問題,我們的一般會采用“插空法”,舉個簡單的例子:
這里有三個笑臉

和兩個哭臉
,每張臉互不相同,現在要將它們排成一排,要求相同臉型的小伙伴不能相鄰。排列的詳細步驟如下:
將三個笑臉排成一排,有3*2*1種排法,並且形成了4個間隔 —
—
—
—將兩個哭臉插入到四個間隔中,但是由於要去相同臉型兩兩不相鄰,所以這兩個哭臉只能插入到中間兩個間隔,此時有兩種方案
所以總的方案數為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++) {
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技巧_腳本之家



