【長篇讀後】跟著兩隻倉鼠學演算法 漫畫算法系列
感謝微信公眾號“演算法愛好者”,以及該漫畫系列的出處“程式設計師小灰”
這裡會長期小灰每一期的學習感悟總結。
算法系列
漫畫演算法:什麼是動態規劃?(整合版)
漫畫演算法:什麼是跳躍表?
漫畫演算法:什麼是 B 樹?
漫畫演算法:什麼是 B+ 樹?
漫畫演算法:什麼是一致性雜湊?
漫畫演算法:無序陣列排序後的最大相鄰差值
漫畫演算法:什麼是布隆演算法?
漫畫演算法:什麼是 A* 尋路演算法?
漫畫演算法:什麼是 Base64 演算法?
漫畫演算法:什麼是 MD5 演算法?
漫畫演算法:如何破解 MD5 演算法?
漫畫演算法:什麼是 SHA 系列演算法?
漫畫演算法:什麼是 AES 演算法?
漫畫演算法:AES 演算法的底層原理
漫畫演算法:什麼是紅黑樹?
最小棧實現
題目: 實現一個棧,帶有出棧(pop),入棧(push),取最小元素(getMin)三個方法。要保證這三個方法的時間複雜度都是O(1)。
方法一: 設定兩個Stack,一個Stack正常出棧入棧。另一個Stack用來輔助儲存最小元素stackMin。 stackMin第一個元素正常入棧,之後入棧的元素和stackMin棧頂的元素相比,若小於等於(等於這個條件非常關鍵,想想為什麼)則入stackMin。getMin直接返回stackMin棧頂元素(peek)。pop的時候如果Stack棧頂的值等於最小值,則雙Stack同步pop。
這個方法也是該文章中的思想(雖然文章中輔助棧存的是下標,畢竟用陣列實現的)
方法二: 與方法一略有不同,區別在於每次入棧的時候stackMin都會判斷入棧元素是否小於等於(這裡的等於就可有可無)棧頂元素,如果符合入棧,不符合的話將stackMin棧頂已有的元素重複入自身棧頂。 這樣做的好處在於,出棧的時候非常方便,直接同步雙棧pop就行了。除了非空,不需要做任何多餘的判斷。
這裡為了省事,就用Stack來實現了。
要看陣列實現棧的話,單獨另寫
陣列實現棧的基本操作
方法一: 入棧方便,出棧複雜
public class StackPro{
private Stack<Integer> storeData;
private Stack<Integer> storeMin;
public StackPro(){
storeData = new Stack<Integer>();
storeMin = new Stack<Integer>();
}
public void push(Integer item){
if(storeMin.isEmpty()){
storeMin.push(item);
}else if(item<=storeMin.peek()){
storeMin.push(item);
}
storeData.push(item);
}
public Integer pop(){
if(storeData.isEmpty())
throw new RuntimeException("Your stack is Empty!");
if(storeData.peek()==getMin()){ //minStack的判斷在getMin()裡進行
storeMin.pop();
}
return storeData.pop();
}
public Integer getMin(){
if(storeMin.isEmpty())
throw new RuntimeException("Your stack is Empty");
return storeMin.peek();
}
}
方法二: 入棧複雜,出棧方便。需要的空間較大
/**
只需要相應改寫push和pop方法就行了,其他的省略
**/
public void push(Integer item){
if(storeMin.isEmpty()){
storeMin.push(item);
}else{
if(item<storeMin.peek()){
storeMin.push(item);
}else{
storeMin.push(storeMin.peek());
}
}
storeData.push(item);
}
public Integer pop(){
if(storeData.isEmpty())
throw new RuntimeException("Your stack is Empty!");
storeMin.pop();
return storeData.pop();
}
這道題的啟發就是: 巧妙利用輔助空間完成功能。
判斷2的乘方
題目:實現一個方法,判斷一個正整數是否是2的乘方(比如16是2的4次方,返回True;18不是2的乘方,返回False)。要求效能儘可能高。
最容易想到的就是設定一箇中間量,然後讓該值從1開始不斷乘以2和target進行比較,如果小於就繼續乘以2,如果==返回true。如果大於則直接返回false。
public boolean is2Fang(int target){
int tmp = 1;
while(tmp<=target){
if(tmp==target)
return true;
else
tmp<<=1;
}
return false;
}
雖然這樣的方法的時間複雜度也能夠達到O(lgn)級別,但仍然不是最快的。
改進: 利用2的整數次冪數的特性,以及位運算。
public boolean is2FangPro(int target){
return 0==(target&target-1); //由於運算子優先順序關係,target-1不需要括號
}
推導過程略,因為太簡單了..
找出缺失的整數
題目: 一個無序數組裡有99個不重複正整數,範圍從1到100,唯獨缺少一個整數。如何找出這個缺失的整數?
這道題目的解法真的是非常非常多啊。。。
先說一個不咋樣的是我條件反射想到的。
方法零:
利用bitMap的思想,建立一個100長度的陣列初始為0,如果來一個95就將下標為94的格子塗黑(賦值1)..
public int getLackNum(int [] arr){
int [] bitMap = new int[100]; //100個0
for(int j=0;j<arr.length;j++){
bitMap[arr[j]-1]=1; //存在100則把下標為99的格子染黑
}
for(int x=0;x<100;x++){
if(1!=bitMap[x])
return x+1;
}
return -1; //找不到返回-1
}
再談談文章中的方法。
方法一:
建立一個HashMap,以1到100為鍵,值都是0 。然後遍歷整個陣列,每讀到一個整數,就找到HashMap當中對應的鍵,讓其值加一。
由於陣列中缺少一個整數,最終一定有99個鍵對應的值等於1, 剩下一個鍵對應的值等於0。遍歷修改後的HashMap,找到這個值為0的鍵。
我個人不是很青睞這種方法,設定hashMap還行,最後遍歷hashMap太蠢了。。 不過我還是程式碼實現以下…
public int getLackNum(int [] arr){
Map<Integer,Integer> map = new HashMap<Integer,Integer>();
for(int i=1;i<=100;i++){
map.put(i, 0);
} //初始化map
for(int x : arr){ //塗黑
map.put(x,1);
}
Iterator<Entry<Integer,Integer>> it = map.entrySet().iterator();
while(it.hasNext()){
Map.Entry<Integer, Integer> entry = it.next();
Integer key = entry.getKey();
Integer value = entry.getValue();
if(value==0) return key;
}
return -1;
}
方法二:
將無序陣列排序,然後看相鄰元素是否連續,找到首個不連續的位置返回座標 (依賴於題目的100個數只差一個)
public int getLackNum(int [] arr){
Arrays.sort(arr);
for(int i=1;i<arr.length;i++){
if((arr[i]-arr[i-1])!=1)
return arr[i-1]+1;
}
return -1;
}
方法三: 也是該問題的最優解
將無序陣列相加減去1-100的和… 大道至簡
有些無聊… 就不討論了
public int getLackNum(int [] arr){
int sum = 5050;
for(int i=0;i<arr.length;i++){
sum-=arr[i];
}
return sum;
}
題目擴充套件1:
一個無序數組裡有若干個正整數,範圍從1到100,其中99個整數都出現了偶數次,只有一個整數出現了奇數次(比如1,1,2,2,3,3,4,5,5),如何找到這個出現奇數次的整數?
思路:
我條件反射的想到的是利用bitMap,出現的塗黑,再出現塗白。這樣最後奇數次的就是黑的。。
不過有更高階更省事的方法。
依次對陣列中所有的數進行亦或
public int getLackNum(int [] arr){
int result = 0; //根據亦或的規律
for(int i=0;i<arr.length;i++)
result = result^arr[i];
return result;
}
題目擴充套件2:
一個無序數組裡有若干個正整數,範圍從1到100,其中98個整數都出現了偶數次,只有兩個整數出現了奇數次(比如1,1,2,2,3,4,5,5),如何找到這個出現奇數次的整數?
思路:
這裡用bitMap相對更簡單一些,反而是文章中的分治法我覺得較為麻煩
public int[] getLackNum(int[] arr) {
int [] result = new int[2]; //結果集
int [] bitMap = new int[100]; //無序陣列全量表長度
for (int n : arr) {
bitMap[n - 1] = bitMap[n - 1] == 1 ? 0 : 1;
}
int index = 0;
for(int i=0;i<bitMap.length;i++){
if(bitMap[i]==1) result[index++] = i+1;
if(index == 2) return result;
}
return result;
}
輾轉相除法是什麼鬼?
這條演算法基於一個定理:兩個正整數a和b(a>b),它們的最大公約數等於a除以b的餘數c和b之間的最大公約數。
如何求兩個數的最大公約數
輾轉相除法很經典,畢竟歐幾里得演算法。需記憶,相反我倒是覺得暴力列舉很需要動腦子(捂臉)
輾轉相除法:
public int gcb(int a, int b) {
if(a==b)
return a;
if(a<b)
return gcd(b,a);
return gcd(b,a%b);
}
輾轉相除法的好處在於,如果出現1001和1000這樣的,只需兩次就能發現這倆的公約數為1。 第一次1001%1000 = 1, 1000%1 = 0。 return 1。 於是1001和1000的最大公約數為1。
更相減損術:
思路: 當兩個整形數較大的時候,取模效率較低。
更相減損術, 出自於中國古代的《九章算術》,也是一種求最大公約數的演算法。
他的原理更加簡單:兩個正整數a和b(a>b),它們的最大公約數等於a-b的差值c和較小數b的最大公約數。比如10和25,25減去10的差是15,那麼10和25的最大公約數,等同於10和15的最大公約數。
public static int gcb(int a,int b){
if(a==b)
return a;
if(a<b)
return gcd(b,a);
return gcd(b,a-b);
}
但這種演算法不適用於類似 (10000,1)這樣差距很大的組合。明顯公約數是1,但卻需要遞減999次。
眾所周知,移位運算的效能非常快。對於給定的正整數a和b,不難得到如下的結論。其中gcb(a,b)的意思是a,b的最大公約數函式:
當a和b均為偶數,gcb(a,b) = 2*gcb(a/2, b/2) = 2*gcb(a>>1, b>>1)
當a為偶數,b為奇數,gcb(a,b) = gcb(a/2, b) = gcb(a>>1, b)
當a為奇數,b為偶數,gcb(a,b) = gcb(a, b/2) = gcb(a, b>>1)
當a和b均為奇數,利用更相減損術運算一次,gcb(a,b) = gcb(b, a-b), 此時a-b必然是偶數,又可以繼續進行移位運算。
利用上面的規律,對更相減損術進行優化。
更相減損術優化:
/**
同時利用 &1運算來判斷奇數偶數。
*/
public static int gcd(int a, int b) {
if(a==b)
return b;
if(a<b){
return gcd(b,a);
}else{
if((a&1)==0&&(b&1)==0)
return gcd(a>>1,b>>1)<<1; //關鍵點 不然結果會小2的N次冪
else if((a&1)==0&&(b&1)==1)
return gcd(a>>1,b);
else if((a&1)==1&&(b&1)==0)
return gcd(a,b>>1);
else
return gcd(b,a-b);
}
}
結論:
1. 暴力列舉法:時間複雜度是O(min(a, b)))
2. 輾轉相除法:時間複雜度不太好計算,可以近似為O(log(max(a, b))),但是取模運算效能較差。
3. 更相減損術:避免了取模運算,但是演算法效能不穩定,最壞時間複雜度為O(max(a, b)))
4. 更相減損術與移位結合:不但避免了取模運算,而且演算法效能穩定,時間複雜度O(log(max(a, b)))
Bitmap 演算法
公眾號裡關於這個講了不少。我個人覺得bitMap不是一個具體的演算法,而是一種思想,一個模型。
你可以理解為有一個長度為N的陣列,數組裡的每一個值只有兩種情況0或1,或者是true和false。為了簡化,我們這裡就假設這個陣列只有0或1的情況,初始都是0。然後我們根據情況將對應的位置染黑。 這就是bitMap,每一個位置大小都是一個bit.
應用這個思想可以解決很多問題,比如著名的布隆過濾器。
EWAHCompressedBitmap