漫畫算法-小灰的算法之旅
魏夢舒(@程序員小灰)著
小灰用漫畫(可愛的手繪小倉鼠)的形式,給算法這顆“炮彈”包上了“糖衣”,讓算法的為力潛藏於內,外表不再嚇人,變得萌萌噠,Q彈可愛。
本書通過主人公小灰,用漫畫的形式講述了算法與數據結構的基礎知識、復雜多變的算法面試及算法的實際應用。
- 第一章:講述什么是算法、數據結構,有什么用。如何計算時間復雜度、空間復雜度。
- 第二章:講述基本的數據結構:數組、鏈表、棧、隊列、哈希。
- 第三章:講述了樹、二叉樹相關知識。
- 第四章:講述了經典的排序算法:冒泡、快速、堆、計數、桶排序。
- 第五章:講述了10多道職場上流行的算法面試題及詳細的解題思路。
- 第六章:講述了算法在職場上的實際應用。
“學習算法,我們不需要死記硬背那些冗長復雜的背景知識、底層原理、指令語法......需要做的是領悟算法思想、理解算法對內存空間和性能的影響,以及開動腦筋去尋求解決問題的最佳方案。相比編程領域的其他技術,算法更純粹,更接近數學,也更具有趣味性。” -- 作者說
1. 算法和數據結構
算法
algorithm。始於計算出1+2+3+4...+10000的結果。首先把從1到10000這10000個數字兩兩分組相加,如下:
1 + 10000 = 10001
2 + 9999 = 10001
3 + 9998 = 10001
......
一共有多少組這樣結果相同的和呢?有10000/2即5000組,即結果:(1 + 10000) * 10000 / 2 = 50005000
。
在數學上稱這種等差數列求和的方法為:高斯算法。
在數學領域:算法是用於解決一類問題的公式和思想。
在計算機領域:算法本質是一序列程序指令,用於解決特定的運算和邏輯問題。衡量算法好壞的重要標准有兩個:
- 空間復雜度:占用內存空間的大小
- 時間復雜度:運行時間的長短
應用:運算、查找、排序、尋找最優路線、面試等。
數據結構
數據結構是算法的基石。如果把算法比喻成美麗靈動的舞者,那么數據結構就是舞者腳下廣闊而堅實的舞台。
data structure,是數據的組織、管理和存儲格式,其使用的目的是為了高效地訪問和修改數據。
數據結構組成方式:
- 線性結構:最簡單的數據結構。包括數組、鏈表,及衍生的棧、隊列、哈希表。
- 樹:相對復雜的數據結構,有代表性的是二叉樹。
- 圖:更為復雜的數據結構,因為在圖中會呈現出多對多的關聯關系。
時間復雜度
是對一個算法運行時間長短的量度,用大O表示,記作T(n) = O(f(n))
直白地講,時間復雜度就是把程序的相對執行時間函數T(n)簡化為一個數量級,這個數量級可以是n、n²、n³等。
推導出時間復雜度,有如下幾個原則:
- 如果運行時間是常數量級,則用常數1表示
- 只保留時間函數中的最高階項
- 如果最高階項存在,則省去最高階項前面的系數
比如:
執行次數是T(n) = 3n,最高階項為3n,省去系數3,轉化的時間復雜度為T(n) = O(n)
執行次數是T(n) = 5logn,最高階項為5logn,省去系數5,轉化的時間復雜度為T(n) = O(logn)。5logn,數學中的對數。
執行次數是T(n) = 2,只有常數量級,轉化的時間復雜度為T(n)=O(1)
執行次數是T(n) = 0.5n² + 0.5n,最高階項為0.5n²,省去系數0.5,轉化的時間復雜度為T(n) = O(n²)
誰更節省時間呢?結論如下:
O(1) < O(logn) < O(n) < O(n²)
空間復雜度
space complexity。是對一個算法在運行過程中臨時占用存儲空間大小的度量,用大O表示,記作S(n) = O(f(n))
- 常量空間:O(1),存儲空間大小固定,和輸入規模沒有直接關系
- 線性空間:O(n),空間是一個線性的集合(比如數組),並且集合大小和輸入規模n成正比
- 二維空間:O(n²)空間是一個二維數組集合,並且集合的長度和寬度都與輸入規模n成正比
- 遞歸空間:執行遞歸操作所需的內存空間和遞歸的深度成正比。純粹的遞歸操作的空間復雜度也是線性的,如果遞歸的深度是n,那么空間復雜度就是O(n)
2. 數據結構基礎
數組
有限個,在內存中順序存儲。
內存是由一個個連續的內存單元組成的,每一個內存單元都有自己的地址。
數組中的每一個元素,都存儲在小小的內存單元中,並且元素直接緊密排列,既不能打亂元素的存儲順序,也不能跳過某個存儲單元進行存儲。
操作:
- 讀取、更新:利用索引,比較容易,直接操作
- 插入:末尾插入簡單,直接在最后插入即可。非末尾插入,由於數組的每一個元素都有其固定下標,所以需要先把插入位置及后面的元素向后移動,挪出地方,再把要插入的元素放到對應的數組位置上。
- 刪除:和插入過程相反,若刪除的元素不是位於末尾,其后的元素都要向前移動。
插入、刪除的時間復雜度都是O(n)
優勢:非常高效的訪問能力,只要給出下標,就能找到對應元素。
劣勢:插入、刪除效率低下。數組元素連續緊密地存儲在內存中,插入、刪除元素都會導致大量元素被迫移動,影響效率。
數組適合讀操作多、寫操作少的場景。鏈表和數組正好相反。
鏈表
一種在物理上非連續、非順序的數據結構,由若干節點(node)所組成。
單向鏈表的每一個節點包含兩部分,存放數據的變量data,指向下一個節點的指針next。
鏈表的第一個節點被稱為頭結點,最后一個節點被稱為尾節點。尾結點的下一個節點指向空。
雙向鏈表,每一個節點除了擁有 data 和 next,還擁有指向前置節點的 prev 指針。
數組在內存中的存儲方式是順序存儲,鏈表 在內存中的存儲順序是隨機存儲。
數組在內存中占用了連續完整的存儲空間,而鏈表采用了見縫插針的方式,鏈表的沒一個節點分布在內存的不同位置,依靠next指針關聯起來。這樣可以靈活有效地利用零散的碎片空間。
鏈表操作:
- 查找節點:只能從頭結點開始向后一個一個節點逐一查找。
- 更新:查找到了,替換數據就好
- 插入、刪除:分三種情況:尾部、頭部、中間,需要處理對應的next指針。具體看書中的圖會更直觀呢,文字描述太晦澀了。
;鏈表的插入和刪除的時間復雜度:如果不考慮插入、刪除操作之前查找元素的過程,只考慮純粹的插入、刪除操作,時間復雜度為O(1)
數組 vs 鏈表:數組能夠利用下標快速定位元素,對於讀操作多、寫操作少的場景非常使用。而鏈表的有事在於能靈活的進行插入和刪除操作,適用於讀操作少、寫操作多的場景。
棧和隊列
數據存儲的物理結構和邏輯結構:
- 物理結構:物質層面,實實在在的,看得見,摸得着。
- 順序存儲結構:數組
- 鏈式存儲結構:鏈表
- 邏輯結構:精神層面,抽象的概念,依賴於物理結構而存在。
- 線性結構:順序表、棧、隊列
- 非線性結構:樹、圖
棧:stack。一種線性數據結構,棧中的元素只能先進后出(FILO,First In Last Out)。最早進入的元素存放的位置叫作棧底,最后進入的元素存放的位置叫作棧頂。用數組、鏈表均可以實現。
棧的基本操作:
- 入棧:只能從棧頂放入元素,新元素成為新棧頂。
- 出棧:只能從棧頂彈出元素,出棧元素的前一個元素成為新棧頂。
入棧、出棧的時間復雜度:O(1),因為只會影響到最后一個元素。
隊列:queue。一種線性數據結構,隊列中的元素只能先進先出(FIFO,First In First Out)。隊列的出口端叫作隊頭(front),隊列的入口端叫作隊尾(rear)。用數組、鏈表均可以實現。
隊列的基本操作:
- 入隊:enqueue,只能在隊尾的位置放入元素,新元素成為新隊尾。
- 出隊:dequeue,只能在隊頭的位置移出元素,出隊元素的后一個元素成為新隊頭。
額外:循環隊列、雙端隊列、優先隊列。
散列表
散列表:也稱哈希表,hash table。是存儲 key-value 映射的集合,時間復雜度接近於O(1)。本質上也是一個數組。
哈希函數:通過某種方式,把 key 和數組下標進行轉換的函數。
散列表的讀寫操作:
- 寫操作(put):在散列表中插入新的鍵值對。
- 通過哈希函數,把 key 轉換成數組下標。如果數組下標的位置沒有元素,就放到該位置,如果該位置有值,這種情況為哈希沖突。
- 解決哈希沖突:開放尋址法和鏈表法。
- 讀操作(get):通過給定的key,在散列表中查找對應的value。
- 通過哈希函數,把 key 轉換成數組下標,然后根據下標找value.
3.樹
樹
tree,是n個節點的有限集。有如下特點:
- 有且僅有一個特定的稱為根的節點。
- 當n>1時,其余節點可分為m(m>0)個互不相交的有限集,每一個集合本身又是一個樹,並稱為根的子樹。
樹的最大層級樹,被稱為樹的高度或深度。
相關節點
- 根節點(root)
- 葉子節點(leaf):沒有孩子的節點
- 父節點
- 孩子節點
- 兄弟節點
二叉樹
樹的一種特殊形式。樹的每個節點最多有2個孩子節點。
二叉樹的兩個孩子節點,一個被稱為左孩子,一個被稱為右節點。
二叉樹有兩種特殊形式:滿二叉樹、完全二叉樹。
滿二叉樹:一個二叉樹的所有非葉子節點都存在左右孩子,並且所有葉子節點都在同一層接上。簡言之,滿二叉樹的每一個分支都是滿的。
完全二叉樹:對一個有n個節點的二叉樹,按層級順序編號,則所有節點的編號為從1到n。如果這個樹所有節點和同樣深度的滿二叉樹的編號為從1到n的節點位置相同,則這個二叉樹為滿二叉樹。
一棵樹,若為滿二叉樹,那么一定是完全二叉樹。反之,不一定。
在內存中存儲:
- 鏈式存儲結構:一個節點含數據data,指向左孩子的節點,指向右孩子的節點。
- 數組:按照層級順序把二叉樹的節點放到數組中對應的位置上。如果某一個節點的孩子為空,則數組的相應位置也空出來。
- 為什么這么設計?可以更方便的定位孩子節點、父節點。
- 若父節點的下標是parent,那么左孩子節點下標是2parent+1,右孩子節點下標是2parent+2。
- 反之,若左孩子節點下標是leftChild,那么父節點下標是(leftChild - 1)/2。
- 稀疏二叉樹,用數組表示會很浪費空間。
二叉樹的應用:查找操作、維持相對順序。
- 查找:二叉查找樹。左子樹上所有節點都小於根節點,右子樹上所有節點都大於根節點。左右子樹也都是二叉查找樹。
- 維持相對順序:二叉排序樹
二叉樹的遍歷:
從節點之間位置關系的角度:
- 前序遍歷:輸出順序:根節點、左子樹、右子樹
- 中序遍歷:輸出順序:左子樹、根節點、右子樹
- 后序遍歷:輸出順序:左子樹、右子樹、根節點
- 層序遍歷:按照從根節點到葉子節點的層級關系,一層一層橫向遍歷各個節點。
從更宏觀的角度:
- 深度優先遍歷(前、中、后序遍歷,前中后是相對根節點)
- 廣度優先遍歷(層序遍歷)
二叉堆:本質上是一種完全二叉樹。
- 最大堆:任何一個父節點的值,都大於或等於它左、右孩子節點的值。
- 最小堆:任何一個父節點的值,都小於或等於它左、右孩子節點的值。
二叉堆的根節點,叫作堆頂。最大堆的堆頂是整個堆中最大元素,最小堆的堆頂是整個堆中最小元素。
優先隊列:基於二叉堆實現:
- 最大優先隊列:無論入隊順序如何,當前最大元素都會優先出隊,基於最大堆實現
- 最小優先隊列:無論出隊順序如何,當前最小元素都會優先出隊,基於最小堆實現
觀后感:回想起了很多大學學習時的場景與概念(專業:軟件工程)