數據結構和算法之美

01.爲什麼學習數據結構和算法?

爲了突破編程的瓶頸,不再只寫功能性代碼
爲了體驗編程的魅力,打開新世界的大門
爲了不被淘汰,掌握別人認爲難以學會的,才更有價值

02.如何抓住重點,系統高效的學習數據結構和算法?

1)理解概念
什麼是數據結構?
廣義上指一組數據的存儲結構
狹義上是指隊列,堆棧等
什麼是算法?
廣義上是一組操作數據的方法
狹義上是指二分查找,排序等
2)數據結構和算法的關係
相輔相成
數據結構是爲算法服務的,算法作用在特定的數據結構之上
例如,常用的二分查找要用數組來存儲數據才能正常工作
3)學習的重點是什麼?
複雜度分析的方法
是精髓,半壁江山,心法,考量效率和資源消耗的方法	
20個常用的數據結構和算法
10個數據結構:數組、鏈表、棧、隊列、散列表、二叉樹、堆、跳錶、圖、Trie 樹;
10個算法:遞歸、排序、二分查找、搜索、哈希算法、貪心算法、分治算法、回溯算法、動態規劃、字符串匹配算法。
要學習它的“來歷”“自身的特點”“適合解決的問題”以及“實際的應用場景”
不要爲了記憶知識而學習,訓練思維邏輯,要多辯證思考,多問爲什麼
4)事半功倍的學習技巧
邊學邊練,適度刷題
多問,多思考,多互動
打怪升級學習法:精彩留言,學習筆記
知識需要沉澱,不要試圖一下子掌握所有

03.複雜度分析(上):如何分析,統計算法的執行效率和資源消耗?

數據結構和算法是解決更快和更省的問題,即讓代碼運行的更快,讓代碼更省空間
1)爲什麼需要複雜度分析法?
不用具體的測試數據,粗略的估算算法執行效率的方法
2)大O複雜度表示法
所有代碼的執行總時間T(n)與每行代碼的執行次數n成正比

T(n)=O(f(n))
T(n)代表代碼執行的總時間
n表示數據的規模大小
f(n)表達式表示每行代碼執行的次數總和
O表示T(n)和f(n)成正比

時間複雜度並不是表示代碼真正執行的時間,而是表示代碼執行時間隨數據規模增長的變化趨勢
3)時間複雜度分析

時間複雜度表示算法執行時間和數據規模的之間的增長關係

只關注循環執行次數最多的一段代碼
時間複雜度是表示一個算法執行效率和數據規模增長的的變化趨勢,忽略常量,係數,低階只記錄最大階的量級
加法法則:總複雜度等於量級最大的那段代碼的複雜度

如果T1(n)=O(f(n)),T2(n)=O(f(n));
那麼T(n)=T1(n)+T2(n)=max(O(f(n),g(n)))=O(max(f(n),g(n))).

乘法法則:嵌套代碼的複雜度等於嵌套內外代碼複雜度的乘積

如果T1(n)=O(f(n)),T2(n)=O(f(n));
那麼T(n)=T1(n)*T2(n)=O(f(n))*O(g(n))=O(f(n)*g(n))

4)幾種常見的時間複雜度實例分析

常見的複雜度量級,按量級遞增

多項式量級

常量階O(1),對數階O(logn),線性階O(n),線性對數階O(nlongn),平方階O(n2),立方階O(n3),K次方階O(n*k)

非多項式量級

指數階O(2*n),階乘階O(n!)

常見的多項式時間複雜度

O(1)
代碼的執行時間不隨n的增大而增長
一般代碼中不存在循環語句,遞歸語句即使成千上萬行代碼,
其時間複雜度也是O(1)
O(logn),O(nlogn)
該時間複雜度的常見算法:歸併排序,快速排序
O(m+n),O(m*n)
代碼複雜度由m,n兩個數據規模來決定

5)空間複雜度分析

空間複雜度表示算法的存儲空間與數據規模之間的增長關係
常見的空間複雜度就是 O(1)、O(n)、O(n2)
像 O(logn)、O(nlogn) 這樣的對數階複雜度平時都用不到

04.複雜度分析啊(下):淺析最好、最壞、平均、均攤時間複雜度

來歷:同一段代碼,在不同輸入情況下,複雜度量級有可能不一樣。

最好、最快情況時間複雜度
最好情況時間複雜度就是,在最理想的情況下,執行代碼的時間複雜度
比如數組中查找一個元素,正好在第一個位置

最好情況時間複雜度就是,在最糟糕的情況下,執行代碼的時間複雜度
比如數組中查找一個元素,數組中不存在這個元素
平均情況時間複雜度
每種情況需要加權計算,全稱加權平均時間複雜度
只有同一塊代碼在不同的情況下有量級的差別,我們纔會使用最好,最壞,平均三種						複雜度表示法來區分。大多數情況我們使用一個複雜度就能滿足需求。
均攤時間複雜度
使用攤還分析法

對一個數據結構進行一組操作,大部分情況時間複雜度很低,個別情況時間複雜度比較高,而且這些操作存在前後連貫的時序關係,看是否能夠將複雜度高的耗時,均攤到那些複雜度低的操作上。一般情況,均攤時間複雜度等於最好情況時間複雜度

均攤時間複雜度就是一種特殊的平均時間複雜度

使用什麼情況的時間複雜度不重要,初衷還是要更好的體現這個算法或者代碼的性能

05.爲什麼很多編程語言中數組都是從0開始編號?

來歷:

1)如何實現隨機訪問?

數組概念:是一種線性表數據結構,它用一組連續的內存空間,來存儲一組相同的數據。

計算機給每個內存單元分配一個內存地址,通過內存地址來訪問內存中的數據,當計算機需要隨機訪問數據中的某個元素時,就會通過下面的尋址公式計算出該元素的內存地址

a[i]_address=base_address + i * data_type_size
base_address:內存塊的首地址
data_type_size:存儲數據類型的佔用的內存大小

2)低效的“插入”和“刪除”

數組爲了保持內存數據的連續性,在插入或者刪除元素後,會造成後面數據的搬移,導致低效。

插入,刪除元素操作
最好情況時間複雜度:O(1)
最壞情況時間複雜度:O(n)
平均時間複雜度:O(n)

JVM標記垃圾清除算法:每次刪除操作並不真正搬移數據,只是記錄數據,等到空間不足,才真正執行一次刪除操作,這樣就大大減少刪除操作導致的數據搬移。

不要死記硬背數據結構和算法,學習其背後的思想和處理技巧,
這些東西纔是最有價值的

3)警惕數據訪問的越界問題
	訪問數據的本質就是訪問一段內存,只要數組通過偏移計算得到的內存地址是可用的,那麼程序就不會報任何錯誤。
	C中數組越界檢查由程序員做
	java本身會做越界檢查
4)解答開篇
	數組的下標爲什麼從0,而不是從1開始?
	效率優化角度:使用0,尋址操作會減少一次減法操作
	學習成本的角度:其他語言效仿C語言從0開始計數數組下標,減少學習成本,沿用了從0計數的習慣

06.鏈表(上):如何實現LRU緩存淘汰算法?

1)常見的緩存淘汰策略:

  • 先進先出策略FIFO
  • 最少使用策略LFU
  • 最近最少使用策略LRU

2)鏈表和數組底層存儲結構的區別:
數組是需要一塊連續的內存空間存儲

鏈表是並不需要一塊連續的內存空間,它是通過“指針”將零散的內存塊串聯在一起。

3)常見的鏈表結構:

  • 單鏈表
  • 雙向鏈表
  • 循環列表

4)單鏈表
特點:
每個內存塊是一個結點
每個結點指向下一個結點
結點記錄數據和鏈表上下一個結點的地址,這個地址叫後繼指針
尾結點指向null空地址

優點:
插入,刪除速度快,時間複雜度O(n),因爲不需要數據搬移,只需要改變相鄰結點的指針

缺點:
查找速度慢,時間複雜度O(n),不能像數組一樣通過基地址和下標計算使用尋址地址計算,需要通過結點地址依次遍歷

5)循環鏈表
特點:
一種特殊的單鏈表
尾結點指向頭結點
適合處理環形結構的數據:例如約瑟夫問題

6)雙向鏈表
特點:
雙向,包含後繼指針指向下一個結點和前驅指針指向前一個結點

缺點:
佔用更多的內存空間

優點:
操作靈活,O(1)時間複雜度找到前驅結點,
插入,刪除操作比單鏈表更高效

雙向鏈表在刪除操作中的優勢:
刪除給定指針指向的結點,單鏈表表需要遍歷找到該刪除結點
的前驅結點,複雜度O(n),而雙向鏈表不需要遍歷,可以直接找到,複雜度O(1)

插入操作也是同樣的優勢

7)鏈表 VS 數組性能大比拼

時間複雜度:
插入刪除:鏈表O(1),數組O(n)
查詢:鏈表O(n),數組O(1)

優缺點比較:
數組連續內存,利用CPU緩存,預讀數據,效率高;
鏈表是不連續內存,不能使用CPU緩存預讀

數組大小固定,申請內存太大,內存不足導致無法分配,申請內存太小,會,需要再次申請更大內存,數據拷貝費時
鏈表支持動態擴容

8)如何實現LRU緩存淘汰算法?
思路:維護一個有序的單鏈表,之前訪問的數據靠近鏈表的尾部,
當有一個新數據被訪問時,從鏈表頭結點開始遍歷:
1.如果此數據已經存在於緩存鏈表中,那麼遍歷找到此數據對應的結點,將它從鏈表中刪除,然後插入鏈表頭部
2.如果此數據不存在於緩存列表中,分兩種情況:
緩存未滿,直接插入到鏈表頭部結點
緩存已滿,將鏈表尾部結點刪除,數據插入頭部結點

這種鏈表的思路,緩存訪問的時間複雜度是O(n)

數組的實現思路:
維護一個有序的數組 ,之前訪問的數據靠近頭部,當訪問一個新數據時,
從後開始往前遍歷:
1.如果新數據存在於數組中,把此數據從數組中刪除,插入點之後的數據往前搬移,新數據添加到最後
2.如果新數據不存在於數組中,分爲兩種情況:
數組已滿,申請新的更大存儲空間,之前的數據拷貝到新空間,新數據插入到數組尾部
數組未滿,直接插入到數組尾部

這種數組的思路,緩存訪問的時間複雜度也是O(n)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章