Java進階專題(十六) 資料結構與演算法的應用(上)

有夢想的老王發表於2020-11-06

前言

​ 學習演算法,我們不需要死記硬背那些冗長複雜的背景知識、底層原理、指令語法……需要做的是領悟演算法思想、理解演算法對記憶體空間和效能的影響,以及開動腦筋去尋求解決問題的最佳方案。相比程式設計領域的其他技術,演算法更純粹,更接近數學,也更具有趣味性。

​ 本文將回顧資料結構與演算法的基礎知識,學習日常所接觸場景中的一些演算法和策略,以及這些演算法的原理和他背後的思想,最後會動手寫程式碼,用java裡的資料結構來實現這些演算法,如何去做?

​ 本文基本知識概念有借鑑《漫畫演算法-小灰的演算法之旅》相關篇幅與圖片。

基本概念回顧

什麼是資料結構

1)概述
資料結構是計算機儲存、組織資料的方式。資料結構是指相互之間存在一種或多種特定關係的資料元素的集合。通常情況下,精心選擇的資料結構可以帶來更高的執行或者儲存效率。
2)劃分
從關注的維度看,資料結構可以劃分為資料的邏輯結構和物理結構,同一邏輯結構可以對應不同的儲存結構。邏輯結構反映的是資料元素之間的邏輯關係,邏輯關係是指資料元素之間的前後間以什麼形式相互關聯,這與他們在計算機中的儲存位置無關。邏輯結構包括:
集合:只是扎堆湊在一起,沒有互相之間的關聯
線性結構:一對一關聯,隊形
樹形結構:一對多關聯,樹形
圖形結構:多對多關聯,網狀
資料物理結構指的是邏輯結構在計算機儲存空間中的存放形式(也稱為儲存結構)。一般來說,一種資料結構的邏輯結構根據需要可以表示成多種儲存結構,常用的儲存結構有順序儲存、鏈式儲存、索引儲存和雜湊儲存等。
順序儲存:用一組地址連續的儲存單元依次儲存集合的各個資料元素,可隨機存取,但增刪需要大批移動
鏈式儲存:不要求連續,每個節點都由資料域和指標域組成,佔據額外空間,增刪快,查詢慢需要遍歷
索引儲存:除建立儲存結點資訊外,還建立附加的索引表來標識結點的地址。檢索快,空間佔用大
雜湊儲存:將資料元素的儲存位置與關鍵碼之間建立確定對應關係,檢索快,存在對映函式碰撞問題
3)程式中常見的資料結構
陣列(Array):連續儲存,線性結構,可根據偏移量隨機讀取,擴容困難
( Stack):線性儲存,只允許一端操作,先進後出,類似水桶
佇列(Queue):類似棧,可以雙端操作。先進先出,類似水管
連結串列( LinkedList):鏈式儲存,配備前後節點的指標,可以是雙向的
( Tree):典型的非線性結構,從唯一的根節點開始,子節點單向執行前驅(父節點)
(Graph):另一種非線性結構,由節點和關係組成,沒有根的概念,互相之間存在關聯
(Heap):特殊的樹,特點是根結點的值是所有結點中最小的或者最大的,且子樹也是堆
雜湊表(Hash):源自於雜湊函式,將值做一個函式式對映,對映的輸出作為儲存的地址

什麼是演算法

​ 演算法指的是基於儲存結構下,對資料如何有效的操作,採用什麼方式可以更有效的處理資料,提高資料運算效率。資料的運算是定義在資料的邏輯結構上,但運算的具體實現要在儲存結構上進行。一般涉及的操作有以下幾種:
檢索:在資料結構裡查詢滿足一定條件的節點。
插入:往資料結構中增加新的節點,一般有一點位置上的要求。
刪除:把指定的結點從資料結構中去掉,本身可能隱含有檢索的需求。
更新:改變指定節點的一個或多個欄位的值,同樣隱含檢索。
排序:把節點裡的資料,按某種指定的順序重新排列,例如遞增或遞減。

資料結構基礎

陣列

​ 陣列對應的英文是array,是有限個相同型別的變數所組成的有序集合,陣列中的每一個變數被稱為元素。陣列是最為簡單、最為常用的資料結構。

陣列的另一個特點,是在記憶體中順序儲存,因此可以很好地實現邏輯上的順序表。

記憶體是由一個個連續的記憶體單元組成的,每一個記憶體單元都有自己的地址。在這些記憶體單元中,有些被其他資料佔用了,有些是空閒的。
陣列中的每一個元素,都儲存在小小的記憶體單元中,並且元素之間緊密排列,既不能打亂元素的儲存順序,也不能跳過某個儲存單元進行儲存。

連結串列

連結串列(linked list)是一種在物理上非連續、非順序的資料結構,由若干節點(node)所組成。

單向連結串列的每一個節點又包含兩部分,一部分是存放資料的變數data,另一部分是指向下一個節點的指標next。

雙向連結串列比單向連結串列稍微複雜一些,它的每一個節點除了擁有data和next指標,還擁有指向前置節點的prev指標。

如果說陣列在記憶體中的儲存方式是順序儲存,那麼連結串列在記憶體中的儲存方式則是隨機儲存。

棧和佇列

(stack)是一種線性資料結構,它就像一個上圖所示的放入乒乓球的圓筒容器,棧中的元素只能先入後出(First In Last Out,簡稱FILO)。最早進入的元素存放的位置叫作棧底(bottom),最後進入的元素存放的位置叫作棧頂(top)。

棧這種資料結構既可以用陣列來實現,也可以用連結串列來實現。

佇列(queue)是一種線性資料結構,它的特徵和行駛車輛的單行隧道很相似。不同於棧的先入後出,佇列中的元素只能先入先出(First In First Out,簡稱FIFO)。佇列的出口端叫作隊頭(front),佇列的入口端叫作隊尾(rear)。

雜湊表

​ 雜湊表也叫作雜湊表(hash table),這種資料結構提供了鍵(Key)和值(Value)的對映關係。只要給出一個Key,就可以高效查詢到它所匹配的Value,時間複雜度接近於O(1)。

​ 由於陣列的長度是有限的,當插入的Entry越來越多時,不同的Key通過雜湊函式獲得的下標有可能是相同的。這種情況,就叫作雜湊衝突。

解決雜湊衝突的方法主要有兩種,一種是開放定址法,一種是連結串列法

開放定址法的原理很簡單,當一個Key通過雜湊函式獲得對應的陣列下標已被佔用時,我們可以“另謀高就”,尋找下一個空檔位置。

這就是開放定址法的基本思路。當然,在遇到雜湊衝突時,定址方式有很多種,並不一定只是簡單地尋找當前元素的後一個元素,這裡只是舉一個簡單的示例而已。在Java中,ThreadLocal所使用的就是開放定址法。

接下來,重點講一下解決雜湊衝突的另一種方法——連結串列法。這種方法被應用在了Java的集合類HashMap當中。

HashMap陣列的每一個元素不僅是一個Entry物件,還是一個連結串列的頭節點。每一個Entry物件通過next指標指向它的下一個Entry節點。當新來的Entry對映到與之衝突的陣列位置時,只需要插入到對應的連結串列中即可。

樹和圖就是典型的非線性資料結構,我們首先講一講樹的知識。

樹(tree)是n(n≥0)個節點的有限集。當n=0時,稱為空樹。在任意一個非空樹中,有如下特點。

  1. 有且僅有一個特定的稱為根的節點。
  2. 當n>1時,其餘節點可分為m(m>0)個互不相交的有限集,每一個集合本身又是一個樹,並稱為根的子樹。

二叉樹

二叉樹(binary tree)是樹的一種特殊形式。二叉,顧名思義,這種樹的每個節點最多有2個孩子節點。注意,這裡是最多有2個,也可能只有1個,或者沒有孩子節點。

​ 二叉樹節點的兩個孩子節點,一個被稱為左孩子(left chi ld),一個被稱為右孩子(right chi ld)。這兩個孩子節點的順序是固定的,就像人的左手就是左手,右手就是右手,不能夠顛倒或混淆。此外,二叉樹還有兩種特殊形式,一個叫作滿二叉樹,另一個叫作完全二叉樹

二叉樹儲存結構

  1. 鏈式儲存結構。
  2. 陣列。

小結

什麼是陣列
陣列是由有限個相同型別的變數所組成的有序集合,它的物理儲存方式是順序儲存,訪問方式是隨機訪問。利用下標查詢陣列元素的時間複雜度是O(1),中間插入、刪除陣列元素的時間複雜度是O(n)。
什麼是連結串列
連結串列是一種鏈式資料結構,由若干節點組成,每個節點包含指向下一節點的指標。連結串列的物理儲存方式是隨機儲存,訪問方式是順序訪問。查詢連結串列節點的時間複雜度是O(n),中間插入、刪除節點的時間複雜度是O(1)。
什麼是棧
棧是一種線性邏輯結構,可以用陣列實現,也可以用連結串列實現。棧包含入棧和出棧操作,遵循先入後出的原則(FILO)。
什麼是佇列
佇列也是一種線性邏輯結構,可以用陣列實現,也可以用連結串列實現。佇列包含入隊和出隊操作,遵循先入先出的原則(FIFO)。
什麼是雜湊表
雜湊表也叫雜湊表,是儲存Key-Value對映的集合。對於某一個Key,雜湊表可以在接近O(1)的時間內進行讀寫操作。雜湊表通過雜湊函式實現Key和陣列下標的轉換,通過開放定址法和連結串列法來解決雜湊衝突。

什麼是樹
樹是n個節點的有限集,有且僅有一個特定的稱為根的節點。當n>1時,其餘節點可分為m個互不相交的有限集,每一個集合本身又是一個樹,並稱為根的子樹。
什麼是二叉樹
二叉樹是樹的一種特殊形式,每一個節點最多有兩個孩子節點。二叉樹包含完全二叉樹和滿二叉樹兩種特殊形式。
二叉樹的遍歷方式有幾種
根據遍歷節點之間的關係,可以分為前序遍歷、中序遍歷、後序遍歷、層序遍歷這4種方式;從更巨集觀的角度劃分,可以劃分為深度優先遍歷和廣度優先遍歷兩大類。
什麼是二叉堆
二叉堆是一種特殊的完全二叉樹,分為最大堆和最小堆。
在最大堆中,任何一個父節點的值,都大於或等於它左、右孩子節點的值。
在最小堆中,任何一個父節點的值,都小於或等於它左、右孩子節點的值。
什麼是優先佇列
優先佇列分為最大優先佇列和最小優先佇列。
在最大優先佇列中,無論入隊順序如何,當前最大的元素都會優先出隊,這是基於最大堆實現的。
在最小優先佇列中,無論入隊順序如何,當前最小的元素都會優先出隊,這是基於最小堆實現的。

排序演算法

氣泡排序

氣泡排序的英文是bubble sort,它是一種基礎的交換排序。

按照氣泡排序的思想,我們要把相鄰的元素兩兩比較,當一個元素大於右側相鄰元素時,交換它們的位置;當一個元素小於或等於右側相鄰元素時,位置不變。

排序過程如下

到此為止,所有元素都是有序的了,這就是氣泡排序的整體思路。
氣泡排序是一種穩定排序,值相等的元素並不會打亂原本的順序。由於該排序演算法的每一輪都要遍歷所有元素,總共遍歷(元素數量-1)輪,所以平均時間複雜度是O(n2)。

快速排序

同氣泡排序一樣,快速排序也屬於交換排序,通過元素之間的比較和交換位置來達到排序的目的。
不同的是,氣泡排序在每一輪中只把1個元素冒泡到數列的一端,而快速排序則在每一輪挑選一個基準元素,並讓其他比它大的元素移動到數列一邊,比它小的元素移動到數列的另一邊,從而把數列拆解成兩個部分。

在分治法的思想下,原數列在每一輪都被拆分成兩部分,每一部分在下一輪又分別被拆分成兩部分,直到不可再分為止。
每一輪的比較和交換,需要把陣列全部元素都遍歷一遍,時間複雜度是O(n)。這樣的遍歷一共需要多少輪呢?假如元素個數是n,那麼平均情況下需要logn輪,因此快速排序演算法總體的平均時間複雜度是O(nlogn)。

堆排序

堆排序演算法的步驟。

  1. 把無序陣列構建成二叉堆。
  2. 迴圈刪除堆頂元素,並將該元素移到集合尾部,調整堆產生新的堆頂。
    第1步,把無序陣列構建成二叉堆,這一步的時間複雜度是O(n)。
    第2步,需要進行n-1次迴圈。每次迴圈呼叫一次downAdjust方法,所以第2步的計算規模是 (n-1)×logn ,時間複雜度為O(nlogn)。兩個步驟是並列關係,所以整體的時間複雜度是O(nlogn)。

計數排序和桶排序

讓我們來看看桶排序的工作原理。
桶排序的第1步,就是建立這些桶,並確定每一個桶的區間範圍。

小結

相關文章