我們先來看個簡單的問題。
假如給你 20 億個非負數的 int 型整數,然後再給你一個非負數的 int 型整數 t ,讓你判斷 t 是否存在於這 20 億數中,你會怎麼做呢?
有人可能會用一個 int 陣列,然後把 20 億個數給存進去,然後再迴圈遍歷一下就可以了。
想一下,這樣的話,時間複雜度是 O(n),所需要的記憶體空間
4 byte * 20 億,一共需要 80 億個位元組,
大概需要 8GB 的記憶體空間,顯然有些計算機的記憶體一次是載入不了這麼這麼多的資料的。
初步優化
按照上面的做法,時間複雜度是 O(n),記憶體是 8GB,實際上我們是可以把時間複雜度降低到 O(1) 的。
例如我們可以這樣來存資料,把一個 int 非負整數 n 作為陣列下標,如果 n 存在,則對應的值為 1,如果不存在,對應的值為 0。例如陣列 arr[n] = 1,表示n存在,arr[n] = 0表示 n 不存在。
那麼,我們就可以把 20 億個數作為下標來存,之後直接判斷 arr[t] 的值,如果 arr[t] = 1,則代表存在,如果 arr[t] = 0,則代表不存在。這樣,我們就可以把時間複雜度降低到 O(1)。不過空間複雜度我們並沒有降低。還稍微大了點。
由於 int 非負整數一共有 2^31 個,所以陣列的大小需要 2^32 這麼大。
這裡可能有人說也可以用HashSet來存啊,時間複雜度也是近似O(1)。不過這裡需要說明的是,HashSet裡面存的必須是物件,也就是說需要把int包裝成Integer,顯然一個物件的話是更花銷記憶體的,需要物件頭啊什麼的.....
再次優化
大家想一個問題,對於一個數,實際上我們只需要兩種狀態,就是這個數存在和不存在這兩種可能。上面我們用1代表存在,用0代表不存在。
也就是說,我們是可以不用int型的陣列來儲存的,一個int型佔用4個位元組,即32個二進位制位,一共可以表示40億多個狀態。用int型的來存兩個狀態,多浪費。
所以我們可以考慮用boolean型的來存的,boolean貌似就佔用一個位元組(java中的boolena貌似是佔用一個位元組)。而一個boolean有true和false兩種狀態,所以也是成立的。這樣子的話佔用的記憶體就是2GB的記憶體了。
這樣,就可以降低到之前的四分之1記憶體了。
最終優化:bitmap
大家再想一個問題,雖然boolean是表示兩種狀態,但是boolean實際上佔用了8bit啊,按道理8bit是可以表示128種狀態的。而被我們拿來表示兩個狀態,是否也有點浪費了呢?
我們都知道,一個二進位制位,有0和1兩種狀態,所以說,其實我們是可以用一個二進位制位來代表一個int型的數是否存在的。例如對於1,3,5,7這四個數,如果存在的話,則可以這樣表示:
1代表這個數存在,0代表不存在。例如表中01010101代表1,3,5,7存在,0,2,4,6不存在。
那如果8,10,14也存在怎麼存呢?如圖,8,10,14我們可以存在第二個位元組裡
以此類推。這樣子,我們又可以把記憶體降低到之前的8分之一了。
這種採用一個二進位制位來儲存資料的方法,我們也叫做bitmap演算法。
可能有人會問,假如我要新增一個數n,我知道它要存在第n個位那裡,把第n個二進位制改為1,可是我要怎麼操作呢?
這個對於bitmap演算法是如何儲存的,如何進行增刪操作的,我會在之後的文章裡講,這篇就大概介紹下bitmap演算法。
Java中有自帶的bitmap實現,今天我們就用Java中自帶的bitmap來做道題練練手。我們換道類似題目吧,不知道你一眼是否就能想到用bitmap演算法來做。
題目描述:
現在有五十億個int型別的正整數,要從中找出重複的數並返回。
判斷50億個數有哪些是重複和剛才上面那個判斷是否存在,其實是一樣的。我們採用bitmap演算法來做。不過這裡50億個數,別人肯定是以檔案流的形式給你的。這樣我們為了方便,我們就假設這些數是以存在int型陣列的形式給我們的。
程式碼如下:
public class Test {
//為了方便,假設資料是以陣列的形式給我們的
public static Set<Integer> test(int[] arr) {
int j = 0;
//用來把重複的數返回,存在Set裡,這樣避免返回重複的數。
Set<Integer> output = new HashSet<>();
BitSet bitSet = new BitSet(Integer.MAX_VALUE);
int i = 0;
while (i < arr.length) {
int value = arr[i];
//判斷該數是否存在bitSet裡
if (bitSet.get(value)) {
output.add(value);
} else {
bitSet.set(value, true);
}
i++;
}
return output;
}
//測試
public static void main(String[] args) {
int[] t = {1,2,3,4,5,6,7,8,3,4};
Set<Integer> t2 = test(t);
System.out.println(t2);
}
}
複製程式碼
列印結果:
[3, 4]
複製程式碼
當然,bitmap演算法的應用不僅僅是節省記憶體,它還有很多其他的優點。之後有機會就拿一些其他的應用來寫篇文章。
本次講解到此結束。如果喜歡,可以分享給更多的小夥伴哦。
bitmap的儲存會在之後的文章講哦
完
推薦閱讀:
獲取更多原創文章,可以關注下我的公眾號:苦逼的碼農,我會不定期分享一些資源和軟體等。。同時也感謝把文章介紹給更多需要的人。