特此說明,本文演算法改自於《從一個陣列中找出 N 個數,其和為 M 的所有可能--最 nice 的解法》一文。本文不同的是,採用二進位制正序表示法,這種實現思路更直觀、更簡單些。
問題
從一個陣列中找出 N 個數,其和為 M 的所有可能。
舉個例子,從陣列 [1, 2, 3, 4] 中選取 2 個元素,求和為 5 的所有可能。答案是兩組組合: 1,4 和 2,3。
假設封裝函式為 search
:
function search(arr, count, sum) {
...
return res
}
複製程式碼
則有,
search([1,2,3,4],2,5)
// => [[2,3],[1,4]]
複製程式碼
實現思路
這裡我們簡單說一下總體思路:根據陣列長度構建二進位制資料,再選擇其中滿足條件的資料。
我們用 1 和 0 來表示陣列中某位元素是否被選中。因此,可以用 0110 來表示陣列中第 1 位和第 2 位被選中了。
下面列一下長度為 4 的所有二進位制資料表示情況:
- 0000 表示沒有選擇陣列中的任何元素
- 0001 表示選擇了陣列中第 3 位元素
- 0010 表示選擇了陣列中第 2 位元素
- 0011 表示選擇了陣列中第 2、3位元素
- 0100 表示選擇了陣列中第 1 位元素
- 0101 表示選擇了陣列中第 1、3 位元素
- 0110 表示選擇了陣列中第 1、2 位元素
- 0111 表示選擇了陣列中第 1、2、3 位元素
- 1000 表示選擇了陣列中第 0 位元素
- 1001 表示選擇了陣列中第 0、3 位元素
- 1010 表示選擇了陣列中第 0、2 位元素
- 1011 表示選擇了陣列中第 0、2、3 位元素
- 1110 表示選擇了陣列中第 0、1、2 位元素
- 1111 表示選擇了陣列中所有位元素
那麼開篇的例子, 4 選 2,滿足條件的二進位制有 0011、0101、0110、1001、1010、1100 共 6 種可能。而符合對應元素之和為 5 的只有 0110 和 1001。
看到了嗎,思路是我們構建了所有長度為 4 的二進位制,再找到符合條件的二進位制。
這裡條件有兩個。
- 其一是,被選中的個數是 2。
- 其二是,被選中的和是 5。
我們的演算法思路逐漸清晰起來: 遍歷所有二進位制,判斷選中個數是否為 2,然後再求對應的元素之和,看其是否為 5。
第一個問題,如何遍歷所有二進位制資料呢?
這個難不到我們,陣列長度為 4,那麼所有二進位制資料是 0 - 15。
for (var i = 0; i < 16; i++) {
...
}
複製程式碼
陣列長度 4,對應16,即 1 << 4。
注意 1 << 31 為-2147483648,可以使用Math.pow(2, 31)來代替
第二個問題,如何求取被選中的元素個數呢?即求取二進位制字串中 1 的個數呢?
實現方式有多種,比如其中一種是:
function n(i) {
var count = 0;
while( i ) {
if(i & 1){
++count;
}
i >>= 1;
}
return count;
}
console.log(n(0b1010))
// => 2
複製程式碼
上述演算法的思路其實很簡單,將二進位制逐步右移 1 位,看看末尾為 1 的個數。比如 10 的二進位制是 1010,逐步右移的所有可能是 1010->101->10->1->0,其中有 2 次末尾是 1。因此結果是 2。
第三個問題,如何根據二進位制資料來求和呢?
比如 0110,我們應該求和 arr[1] + arr[2]。
問題轉化成了如何判斷陣列下標是否在 0110 中呢?
其實也很簡單,比如下標 1 在,而下標 3 不在。我們把 1 轉化成 0100,0110 & 0100 為 0100, 大於 0,因此下標 1 在。而 0110 & 0001 為 0,因此 下標 3 不在。
所以求和我們可以如下實現:
var arr = [1,2,3,4]
var s = 0, temp = [];
for (var i = 0, len = arr.length; i < len; i++) {
if ( 0b0110 & 1 << (len - 1 - i)) {
s += arr[i]
temp.push(arr[i])
}
}
console.log(temp)
// => [2,3]
複製程式碼
最終實現
有了以上鋪墊,這裡給出最終實現:
function search(arr, count, sum) {
var len = arr.length, res = [];
for (var i = 0; i < Math.pow(2, len); i++) {
if (n(i) == count) {
var s = 0, temp = [];
for (var j = 0; j < len; j++) {
if (i & 1 << (len - 1 -j)) {
s += arr[j]
temp.push(arr[j])
}
}
if (s == sum) {
res.push(temp)
}
}
}
return res;
}
function n(i) {
var count = 0;
while( i ) {
if(i & 1){
++count;
}
i >>= 1;
}
return count;
}
console.log(search([1,2,3,4],2,5))
// => [[2,3],[1,4]]
複製程式碼
最後,可以看出其實沒必要引入各種概念就可以把本演算法說清楚。
本文完。