Java進階專題(十六) 數據結構與算法的應用(上)


前言

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

​ 本文將回顧數據結構與算法的基礎知識,學習日常所接觸場景中的一些算法和策略,以及這些算法的原理和他背后的思想,最后會動手寫代碼,用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步,就是創建這些桶,並確定每一個桶的區間范圍。

小結


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM