演算法學習總結1
基礎概念
核心指標
時間複雜度(流程決定)
額外空間複雜度(流程決定)
常數項時間(實現細節決定)
時間複雜度
常數時間的操作
確定演算法流程的總運算元量與樣本數量之間的表示式關係
只看表示式最高階項的部分
何為常數時間的操作?
如果一個操作的執行時間不以具體樣本量為轉移,每次執行時間都是固定時間。稱這樣的操作為常數時間的操作。
常見的常數時間的操作
常見的算術運算(+、-、*、/、% 等)
常見的位運算(>>、>>>、<<、|、&、^等)
賦值、比較、自增、自減操作等
陣列定址操作
總之,執行時間固定的操作都是常數時間的操作。
反之,執行時間不固定的操作,都不是常數時間的操作。
如何確定演算法流程的總運算元量與樣本數量之間的表示式關係?
- 想象該演算法流程所處理的資料狀況,要按照最差情況來。
- 把整個流程徹底拆分為一個個基本動作,保證每個動作都是常數時間的操作。
- 如果資料量為N,看看基本動作的數量和N是什麼關係。
如何確定演算法流程的時間複雜度?
當完成了表示式的建立,只要把最高階項留下即可。低階項都去掉,高階項的係數也去掉。
記為:O(忽略掉係數的高階項)
選擇排序
過程: arr[0~N-1]範圍上,找到最小值所在的位置,然後把最小值交換到0位置。 arr[1~N-1]範圍上,找到最小值所在的位置,然後把最小值交換到1位置。 arr[2~N-1]範圍上,找到最小值所在的位置,然後把最小值交換到2位置。 … arr[N-1~N-1]範圍上,找到最小值位置,然後把最小值交換到N-1位置。 估算: 很明顯,如果arr長度為N,每一步常數操作的數量,如等差數列一般 所以,總的常數運算元量 = a*(N^2) + b*N + c (a、b、c都是常數) 所以選擇排序的時間複雜度為O(N^2)。
參見程式碼:com.hellozjf.algorithm.basic.service.SortService#chooseSort
氣泡排序
過程: 在arr[0~N-1]範圍上: arr[0]和arr[1],誰大誰來到1位置;arr[1]和arr[2],誰大誰來到2位置…arr[N-2]和arr[N-1],誰大誰來到N-1位置 在arr[0~N-2]範圍上,重複上面的過程,但最後一步是arr[N-3]和arr[N-2],誰大誰來到N-2位置 在arr[0~N-3]範圍上,重複上面的過程,但最後一步是arr[N-4]和arr[N-3],誰大誰來到N-3位置 … 最後在arr[0~1]範圍上,重複上面的過程,但最後一步是arr[0]和arr[1],誰大誰來到1位置 估算: 很明顯,如果arr長度為N,每一步常數操作的數量,依然如等差數列一般 所以,總的常數運算元量 = a*(N^2) + b*N + c (a、b、c都是常數) 所以氣泡排序的時間複雜度為O(N^2)。
參見程式碼:com.hellozjf.algorithm.basic.service.SortService#bubbleSort
插入排序
過程:
想讓arr[0~0]上有序,這個範圍只有一個數,當然是有序的。
想讓arr[0~1]上有序,所以從arr[1]開始往前看,如果arr[1]<arr[0],就交換。否則什麼也不做。
…
想讓arr[0~i]上有序,所以從arr[i]開始往前看,arr[i]這個數不停向左移動,一直移動到左邊的數字不再比自己大,停止移動。
最後一步,想讓arr[0~N-1]上有序, arr[N-1]這個數不停向左移動,一直移動到左邊的數字不再比自己大,停止移動。
估算時發現這個演算法流程的複雜程度,會因為資料狀況的不同而不同。
你發現了嗎?
如果某個演算法流程的複雜程度會根據資料狀況的不同而不同,那麼你必須要按照最差情況來估計。
很明顯,在最差情況下,如果arr長度為N,插入排序的每一步常數操作的數量,還是如等差數列一般
所以,總的常數運算元量 = a*(N^2) + b*N + c (a、b、c都是常數)
所以插入排序排序的時間複雜度為O(N^2)。
參見程式碼:com.hellozjf.algorithm.basic.service.SortService#insertSort
時間複雜度的意義
抹掉了好多東西,只剩下了一個最高階項啊…
那這個東西有什麼意義呢?
時間複雜度的意義在於:
當我們要處理的樣本量很大很大時,我們會發現低階項是什麼不是最重要的;每一項的係數是什麼,不是最重要的。真正重要的就是最高階項是什麼。
這就是時間複雜度的意義,它是衡量演算法流程的複雜程度的一種指標,該指標只與資料量有關,與過程之外的優化無關。
額外空間複雜度
你要實現一個演算法流程,在實現演算法流程的過程中,你需要開闢一些空間來支援你的演算法流程。
作為輸入引數的空間,不算額外空間。
作為輸出結果的空間,也不算額外空間。
因為這些都是必要的、和現實目標有關的。所以都不算。
但除此之外,你的流程如果還需要開闢空間才能讓你的流程繼續下去。這部分空間就是額外空間。
如果你的流程只需要開闢有限幾個變數,額外空間複雜度就是O(1)。
演算法流程的常數項
我們會發現,時間複雜度這個指標,是忽略低階項和所有常數係數的。
難道同樣時間複雜度的流程,在實際執行時候就一樣的好嗎?
當然不是。
時間複雜度只是一個很重要的指標而已。如果兩個時間複雜度一樣的演算法,你還要去在時間上拼優劣,就進入到拼常數時間的階段,簡稱拼常數項。
演算法流程的常數項的比拼方式
放棄理論分析,生成隨機資料直接測。
為什麼不去理論分析?
不是不能純分析,而是沒必要。因為不同常數時間的操作,雖然都是固定時間,但還是有快慢之分的。
比如,位運算的常數時間原小於算術運算的常數時間,這兩個運算的常數時間又遠小於陣列定址的時間。
所以如果純理論分析,往往會需要非常多的分析過程。都已經到了具體細節的程度,莫不如交給實驗資料好了。
一個問題的最優解是什麼意思
一般情況下,認為解決一個問題的演算法流程,在時間複雜度的指標上,一定要儘可能的低,先滿足了時間複雜度最低這個指標之後,使用最少的空間的演算法流程,叫這個問題的最優解。
一般說起最優解都是忽略掉常數項這個因素的,因為這個因素只決定了實現層次的優化和考慮,而和怎麼解決整個問題的思想無關。
常見的時間複雜度
排名從好到差:
O(1)
O(logN)
O(N)
O(N*logN)
O(N^2) O(N^3) … O(N^K)
O(2^N) O(3^N) … O(K^N)
O(N!)
對數器
1,你想要測的方法a
2,實現複雜度不好但是容易實現的方法b
3,實現一個隨機樣本產生器
4,把方法a和方法b跑相同的隨機樣本,看看得到的結果是否一樣
5,如果有一個隨機樣本使得比對結果不一致,列印樣本進行人工干預,改對方法a和方法b
6,當樣本數量很多時比對測試依然正確,可以確定方法a已經正確。
求中點的方法
mid = L + ((R - L) >> 1)
二分法
經常見到的型別是在一個有序陣列上,開展二分搜尋
但有序真的是所有問題求解時使用二分的必要條件嗎?
不
只要能正確構建左右兩側的淘汰邏輯,你就可以二分。
例如:
1) 在一個有序陣列中,找某個數是否存在,參見:com.hellozjf.algorithm.basic.service.FindService#bsExist(int[], int)
2) 在一個有序陣列中,找>=某個數最左側的位置,參見:com.hellozjf.algorithm.basic.service.FindService#bsNearLeft(int[], int)
3) 在一個有序陣列中,找<=某個數最右側的位置,參見:com.hellozjf.algorithm.basic.service.FindService#bsNearRight(int[], int)
4) 區域性最小值問題
一個數組,0 ~ n-1
比較num[0]和num[1],判斷是上升還是下降趨勢
比較num[n-2]和num[n-1],判斷是上升還是下降趨勢
如果左邊是下降,右邊是上升,那麼中間必然有區域性最小值,用二分的方式去求就好了
異或運算
異或運算就記成無進位相加!
比方說二進位制數
0110
0101
0011 -- 異或結果 = 無進位加法
異或運算的性質
1) 0^N == N N^N == 0
2) 異或運算滿足交換律和結合率 (說人話就是,a^b^c = c^b^a = a^c^b = ...
)
題目一
如何不用額外變數交換兩個數
前提,兩塊記憶體是不一樣的
a = a ^ b
b = a ^ b
a = a ^ b
題目二
一個數組中有一種數出現了奇數次,其他數都出現了偶數次,怎麼找到並列印這種數
leetcode [136]只出現一次的數字
題目三
怎麼把一個int型別的數,提取出最右側的1來
說人話
0000001101010000
變成
0000000000010000
N & ((~N) + 1) // ~是取反
題目四
一個數組中有兩種數出現了奇數次,其他數都出現了偶數次,怎麼找到並列印這兩種數
leetcode [260]只出現一次的數III
連結串列
單向連結串列
單向連結串列節點結構(可以實現成範型)
public class Node {
public int value;
public Node next;
public Node(int data) {
value = data;
}
}
雙向連結串列
雙向連結串列節點結構
public class DoubleNode {
public int value;
public DoubleNode last;
public DoubleNode next;
public DoubleNode(int data) {
value = data;
}
}
單向連結串列和雙向連結串列最簡單的練習
連結串列相關的問題幾乎都是coding問題
1) 單鏈表和雙鏈表如何反轉
2) 把給定值都刪除
這裡就是熟悉結構。連結串列還有哪些常見面試題,後續有專門一節來系統學習。
leetcode [206]反轉連結串列
棧和佇列
邏輯概念
棧:資料先進後出,猶如彈匣
佇列:資料先進先出,好似排隊
實際實現
雙向連結串列實現
陣列實現
既然語言都有這些結構和api,為什麼還需要手擼練習?
1)演算法問題無關語言
2)語言提供的api是有限的,當有新的功能是api不提供的,就需要改寫
3)任何軟體工具的底層都是最基本的演算法和資料結構,這是繞不過去的
常見面試題
題目1
怎麼用陣列實現不超過固定大小的佇列和棧?
棧:正常使用
佇列:環形陣列
題目2
實現一個特殊的棧,在基本功能的基礎上,再實現返回棧中最小元素的功能
1)pop、push、getMin操作的時間複雜度都是 O(1)。
2)設計的棧型別可以使用現成的棧結構。
leetcode [155]最小棧
題目3
1)如何用棧結構實現佇列結構,leetcode [232]用棧實現佇列
2)如何用佇列結構實現棧結構,leetcode [225]用佇列實現棧
這兩種結構的應用實在是太多了,在刷題時我們會大量見到
遞迴
怎麼從思想上理解遞迴
怎麼從實際實現的角度出發理解遞迴
例子
求陣列arr[L..R]中的最大值,怎麼用遞迴方法實現。
1)將[L..R]範圍分成左右兩半。左:[L..Mid] 右[Mid+1..R]
2)左部分求最大值,右部分求最大值
3) [L..R]範圍上的最大值,是max{左部分最大值,右部分最大值}
注意:2)是個遞迴過程,當範圍上只有一個數,就可以不用再遞迴了
遞迴的腦圖和實際實現
對於新手來說,把呼叫的過程畫出結構圖是必須的,這有利於分析遞迴
遞歸併不是玄學,遞迴底層是利用系統棧來實現的
任何遞迴函式都一定可以改成非遞迴
Master公式
形如
T(N) = a * T(N/b) + O(N^d)(其中的a、b、d都是常數)
的遞迴函式,可以直接通過Master公式來確定時間複雜度
如果 logb a < d,複雜度為O(N^d)
如果 logb a > d,複雜度為O(N^logb a)
如果 logb a == d,複雜度為O(N^d * logN)
雜湊表
1)雜湊表在使用層面上可以理解為一種集合結構
2)如果只有key,沒有伴隨資料value,可以使用HashSet結構
3)如果既有key,又有伴隨資料value,可以使用HashMap結構
4)有無伴隨資料,是HashMap和HashSet唯一的區別,實際結構是一回事
5)使用雜湊表增(put)、刪(remove)、改(put)和查(get)的操作,可以認為時間複雜度為 O(1),但是常數時間比較大
6)放入雜湊表的東西,如果是基礎型別,內部按值傳遞,記憶體佔用是這個東西的大小
7)放入雜湊表的東西,如果不是基礎型別,內部按引用傳遞,記憶體佔用是8位元組
Integer
Integer:-128 ~ 127 之間是按值比較,以外是按物件比較
hash表中不存在Integer 1000000 != Integer 1000000的問題!!!請放心使用
有序表
1)有序表在使用層面上可以理解為一種集合結構
2)如果只有key,沒有伴隨資料value,可以使用TreeSet結構
3)如果既有key,又有伴隨資料value,可以使用TreeMap結構
4)有無伴隨資料,是TreeSet和TreeMap唯一的區別,底層的實際結構是一回事
5)有序表把key按照順序組織起來,而雜湊表完全不組織
6)紅黑樹、AVL樹、size-balance-tree和跳錶等都屬於有序表結構,只是底層具體實現不同
7)放入如果是基礎型別,內部按值傳遞,記憶體佔用就是這個東西的大小
8)放入如果不是基礎型別,內部按引用傳遞,記憶體佔用是8位元組
9)不管是什麼底層具體實現,只要是有序表,都有以下固定的基本功能和固定的時間複雜度
1)void put(K key, V value)
將一個(key,value)記錄加入到表中,或者將key的記錄 更新成value。
2)V get(K key)
根據給定的key,查詢value並返回。
3)void remove(K key)
移除key的記錄。
4)boolean containsKey(K key)
詢問是否有關於key的記錄。
5)K firstKey()
返回所有鍵值的排序結果中,最小的那個。
6)K lastKey()
返回所有鍵值的排序結果中,最大的那個。
7)K floorKey(K key)
返回<= key 離key最近的那個
8)K ceilingKey(K key)
返回>= key 離key最近的那個
雜湊表和有序表的原理
以後講!現在的你可能會聽不懂,只需要記住:
雜湊表在使用時,增刪改查時間複雜度都是O(1)
有序表在使用時複雜度都是logN