和演算法渣一起練習--利用位運算,輕輕鬆鬆就能解決數學裡的子集問題

三國夢迴發表於2020-09-27

前言

為什麼要說演算法?老實說,演算法的重要性其實是毋庸置疑的,當然了,平時做CURD工作的時候,其實資料結構其實更重要一些,比如表的設計,以及部分場景下,比如秒殺這類,一般是需要在redis等記憶體中去維護一些資料結構,方便我們提升效能。

但基本來說,部分場景下,定下了資料結構,也就間接地定下了對應的演算法;對於一個陣列,要寫一個演算法,支援隨機讀取,就很簡單;對於一個連結串列,要寫一個演算法,支援隨機讀取,那就得遍歷。

個人覺得,演算法和資料結構是一個並重的關係,資料結構的學習還是相對容易的,演算法嘛,就,啊哈,你懂的。

畢業多年來,也曾經嘗試過多次演算法方面的學習,每次都是不了了之,我覺得,有可能是學習的方式不太對。以前看演算法,要麼拿本書在那看;要麼弄不出來,就把答案找出來看一遍,看了就算完了,有時候呢,會把答案的程式碼debug一遍。

總的來說,學習效果真的太差了。

寫這個系列(我也不知道能不能堅持,至少先開一個頭),就是這麼想的,學一個東西,不管是啥,不僅要有輸入,也要有輸出,輸出是什麼呢,就是把我覺得已經會了的東西,講給大家聽,大家能學到一點,我也高興;學不到,那就傾聽並改進。

也鼓勵大家去輸出,不管是筆記,腦圖,還是部落格,都是一個沉澱的方式,大家可以自由選擇。

好了,大家就跟著我這個演算法渣一起,學一點演算法吧。

題目描述及解析

原題連結:https://leetcode-cn.com/problems/subsets/

這個題目,大家看上面的示例部分可以知道,就是給你一個整數集合,返回它的全部子集,比如給你一個集合[1,2],那它的子集就有:

[],[1],[2],[1,2]

我們再來看看,最終的子集的數量,和集合中元素的個數,似乎有關係,比如,集合[1,2]中包含2個元素,最終結果是4個子集,正好是2的2次方;集合[1,2,3]中包含3個元素,最終結果是8個子集,則是2的3次方。

總結下來就是,集合中有n個元素,則最終子集個數為2的n次方。

這難道是巧合嗎,我覺得不是,大家可以思考下,這個題,像不像是從一堆球中,選擇其中的一部分球。對於每一個球來說,只有兩種結果:被選中或者沒被選中。

針對每個球,有2種選法,那n個球,是不是就是2的n次方種選法呢?

如果用true,false來表達被選中和沒被選中,那麼,針對[1,2,3]這個集合來說,可能的子集,可以如下表示:

1被選中 2被選中  3被選中
false  false   false
false  false   true
false  true    false
false  true    true
    
true   false   false
true   false   true
true   true    false
true   true    true   

但是,我發現,用程式來生成這樣一個集合,並不容易,具體怎麼不容易,可以自行試試哈。

我們轉換下思路,採用0表示沒被選中,1表示選中。

1被選中 2被選中 3被選中 子集 二進位制數 十進位制數
0 0 0 [] 000 0
0 0 1 [3] 001 1
0 1 0 [2] 010 2
0 1 1 [2,3] 011 3
1 0 0 [1] 100 4
1 0 1 [1,3] 101 5
1 1 0 [1,2] 110 6
1 1 1 [1,2,3] 111 7

上面的表格中,我們羅列了全部的組合,注意倒數第二列,我們使用二進位制數來表達這個選項,比如001,表示只選中了3.

那麼,我們應該就可以遍歷000到111,就得到了所有的子集,而轉換為十進位制,就是從000遍歷到(2的n次方-1)。

大體的邏輯就是這樣:

int resultLength = 1 << 3; (即2的3次方)
for(int i = 0; i < resultLength; i++){
    // 使用java.lang.Integer#toBinaryString,可直觀看到該int對應的二進位制位
    System.out.println(Integer.toBinaryString(i));
    
    // todo 根據當前二進位制,來選擇對應的元素
    
}

下邊的程式碼比較直觀:

int resultLength = 1 << 3;
for (int i = 0; i < resultLength; i++) {
    System.out.println(Integer.toBinaryString(i));
}

輸出如下:

0
1
10
11
100
101
110
111

通過位運算,來選擇出對應的元素

現在的問題變成了,假設當前遍歷到6,即110這個選項時,我們人類,可以知道該選項對應了[1,2].

但是計算機要怎麼來對應呢?我們可以遍歷我們的陣列,來依次檢查每個元素對應的二進位制bit是否為1,如果為1,說明該元素被選中了,加到結果集中,比如遍歷到1這個元素的時候,它在陣列中的下標為0(陣列下標從0開始別忘)。

那麼,那麼它在110這個二進位制中,它是儲存在哪個bit呢?是儲存在最高位的bit的。

所以,對於陣列[1,2,3]中的第一個值1,其對應了二進位制中的高位,需要右移n-1(n為陣列長度,這裡為3),即右移2位後進行計算;

對於陣列[1,2,3]中的第二個值2,其對應了二進位制中的第1位,需要右移3-2位,即右移1位。

對於陣列中第三個值3,其對應了二進位制中的第0位,需要右移3-3位,即不需移位。

好了,接下來,我們轉換為程式碼:

class Solution {
    public static void main(String[] args) {
        List<List<Integer>> set = subsets(new int[]{1, 2, 3});
        for (List<Integer> integers : set) {
//            System.out.println(integers);
        }
    }
    
    public List<List<Integer>> subsets(int[] nums) {
        if (nums == null || nums.length == 0){
            List<Integer> list = new ArrayList<Integer>();
            List<List<Integer>> result = new ArrayList<List<Integer>>();
            result.add(list);
            return result;
        }

        int arraySize = 1 << nums.length;
        
        List<List<Integer>> result = new ArrayList<List<Integer>>(arraySize);
        for (int i = 0; i < arraySize; i++){
            // leetcode提交時,記得註釋掉,列印比較費時間
            System.out.println(Integer.toBinaryString(i));
            
            // 用來存放當前選項下的結果集
            List<Integer> list = new ArrayList<Integer>();
            // 遍歷陣列
            for (int j = 0; j < nums.length; j++){
                // 找到要右移的位數; 假設為第一個數,則需要右移2位
                int moveStep = nums.length - j - 1;
                // 判斷該bit是否為1,為1表示該數被選中
                if (((i >> moveStep) & 1) == 1){
                    list.add(nums[j]);
                }
            }
            // 列印本選項下的結果,leetcode提交時,記得註釋掉,列印比較費時間
            System.out.println("選中的子集為:" + list);
            result.add(list);
        }

        return result;
    }

}

執行結果如下:

0
選中的子集為:[]
1
選中的子集為:[3]
10
選中的子集為:[2]
11
選中的子集為:[2, 3]
100
選中的子集為:[1]
101
選中的子集為:[1, 3]
110
選中的子集為:[1, 2]
111
選中的子集為:[1, 2, 3]

小結

這個題還有很多其他思路,什麼回溯之類的,鑑於我也還沒搞懂,就先不寫了(寫不出來啊。。。)。

演算法渣渣的演算法之旅,希望能堅持下去!

相關文章