Java中常見的數據結構--圖文結合


0、前言

以本人學習以及面試心得所知,數據結構和算法可以說是程序員必學必會的基本技能,要想進大廠,必須學會這項基本功,而且要扎實。今天主要說說java開發中常用常見的九種數據結構。

數據結構:計算機存儲、組織數據的方式。相同特點的數據元素集合,不同數據結構在不同場景下有着不同的數據處理效率。

根據數據訪問的特點,可分為線性數據結構和非線性數據結構。

線性結構:數組、鏈表、棧、隊列等。

非線性結構:散列表、樹、堆、圖等。

 

 

 

 

 

1、數組

      數組可以說是最基本最常見的數據結構。數組一般用來存儲相同類型的數據,可通過數組名和下標進行數據的訪問和更新。

      數組中元素的存儲是按照先后順序進行的,同時在內存中也是按照這個順序進行連續存放。

      數組相鄰元素之間的內存地址的間隔一般就是數組數據類型的大小。

 

 

 

 

 

 2、鏈表

       鏈表是一種物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。

       鏈表由一系列結點(鏈表中每一個元素稱為結點)組成,結點可以在運行時動態生成。

       每個結點包括兩個部分:一個是存儲數據元素的數據域,另一個是存儲下一個結點地址的指針域。 相比與線性數據表結構,操作復雜。

       由於不必須按順序存儲,鏈表在插入的時候可以達到O(1)的復雜度,比另一種線性表順序表快得多,但是查找一個節點或者訪問特定編號的節點則需要O(n)的時間,而線性表和順序表相應的時間復雜度分別是O(logn)和O(1)。

       鏈表相較於數組,除了數據域,還增加了指針域用於構建鏈式的存儲數據。鏈表中每一個節點都包含此節點的數據和指向下一節點地址的指針。由於是通過指針進行下一個數據元素的查找和訪問,使得鏈表的自由度更高。

這表現在對節點進行增加和刪除時,只需要對上一節點的指針地址進行修改,而無需變動其它的節點。不過事物皆有兩極,指針帶來高自由度的同時,自然會犧牲數據查找的效率和多余空間的使用。

一般常見的是有頭有尾的單鏈表,對指針域進行反向鏈接,還可以形成雙向鏈表或者循環鏈表。

 

 

 

鏈表和數組對比

鏈表和數組在實際的使用過程中需要根據自身的優劣勢進行選擇。鏈表和數組的異同點也是面試中高頻的考察點之一。這里對單鏈表和數組的區別進行了對比和總結。

 

 

 

 

 

 

 3、跳表

跳表也叫跳躍表,是一種動態的數據結構。如果我們需要在有序鏈表中進行查找某個值,需要遍歷整個鏈表,二分查找對鏈表不支持,二分查找的底層要求為數組,遍歷整個鏈表的時間復雜度為O(n)。我們可以把鏈表改造成B樹、紅黑樹、AVL樹等數據結構來提升查詢效率,但是B樹、紅黑樹、AVL樹這些數據結構實現起來非常復雜,里面的細節也比較多。跳表就是為了提升有序鏈表的查詢速度產生的一種動態數據結構,跳表相對B樹、紅黑樹、AVL樹這些數據結構實現起來比較簡單,但時間復雜度與B樹、紅黑樹、AVL樹這些數據結構不相上下,時間復雜度能夠達到O(logn)。

      跳表一般使用單鏈表來實現,這樣比較節約空間。我使用雙向鏈表來實現跳表,因為雙向鏈表相對單向鏈表來說比較容易理解跳表的實現。

      跳表的性質:

  • 由很多層結構組成
  • 每一層都是一個有序的鏈表
  • 最底層(Level 1)的鏈表包含所有元素
  • 如果一個元素出現在 Level i 的鏈表中,則它在 Level i 之下的鏈表也都會出現。

從上面的對比中可以看出,鏈表雖然通過增加指針域提升了自由度,但是卻導致數據的查詢效率惡化。特別是當鏈表長度很長的時候,對數據的查詢還得從頭依次查詢,這樣的效率會更低。跳表的產生就是為了解決鏈表過長的問題,通過增加鏈表的多級索引來加快原始鏈表的查詢效率。這樣的方式可以讓查詢的時間復雜度從O(n)提升至O(logn)。

 

 

 4、棧

      棧是一種比較簡單的數據結構,常用一句話描述其特性,后進先出。棧本身是一個線性表,但是在這個表中只有一個口子允許數據的進出。這種模式可以參考腔腸動物...即進食和排泄都用一個口...

棧的常用操作包括入棧push和出棧pop,對應於數據的壓入和壓出。還有訪問棧頂數據、判斷棧是否為空和判斷棧的大小等。由於棧后進先出的特性,常可以作為數據操作的臨時容器,對數據的順序進行調控,與其它數據結構相結合可獲得許多靈活的處理。

 

 

 5、隊列

      隊列是一種特殊的線性表,特殊之處在於它只允許在表的前端(front)進行刪除操作,而在表的后端(rear)進行插入操作,和棧一樣,隊列是一種操作受限制的線性表。進行插入操作的端稱為隊尾,進行刪除操作的端稱為隊頭。隊列是棧的兄弟結構,與棧的后進先出相對應,隊列是一種先進先出的數據結構。顧名思義,隊列的數據存儲是如同排隊一般,先存入的數據先被壓出。常與棧一同配合,可發揮最大的實力。  

 

 

 6、樹

     樹(英語:tree)是一種抽象數據類型或是實現這種抽象數據類型的數據結構,用來模擬具有樹狀結構性質的數據集合。它是由n(n>0)個有限節點組成一個具有層次關系的集合。把它叫做“樹”是因為它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。它具有以下的特點:

    ①每個節點有零個或多個子節點;
    ②沒有父節點的節點稱為根節點;
    ③每一個非根節點有且只有一個父節點;
    ④除了根節點外,每個子節點可以分為多個不相交的子樹;

     樹作為一種樹狀的數據結構,其數據節點之間的關系也如大樹一樣,將有限個節點根據不同層次關系進行排列,從而形成數據與數據之間的父子關系。常見的數的表示形式更接近“倒掛的樹”,因為它將根朝上,葉朝下。

     樹的數據存儲在結點中,每個結點有零個或者多個子結點。沒有父結點的結點在最頂端,成為根節點;沒有非根結點有且只有一個父節點;每個非根節點又可以分為多個不相交的子樹。

     這意味着樹是具備層次關系的,父子關系清晰,家庭血緣關系明朗;這也是樹與圖之間最主要的區別。

 

 

 

二叉樹
二叉樹:每個節點最多含有兩個子樹的樹稱為二叉樹。(我們一般在書中試題中見到的樹是二叉樹,但並不意味着所有的樹都是二叉樹。)

 

別看樹好像很高級,其實可看作是鏈表的高配版。樹的實現就是對鏈表的指針域進行了擴充,增加了多個地址指向子結點。同時將“鏈表”豎起來,從而凸顯了結點之間的層次關系,更便於分析和理解。

樹可以衍生出許多的結構,若將指針域設置為雙指針,那么即可形成最常見的二叉樹,即每個結點最多有兩個子樹的樹結構。二叉樹根據結點的排列和數量還可進一度划分為完全二叉樹、滿二叉樹、平衡二叉樹、紅黑樹等。

 

 

 

在二叉樹的概念下又衍生出滿二叉樹和完全二叉樹的概念

完全二叉樹:除最后一層無任何子節點外,每一層上的所有結點都有兩個子結點。也可以這樣理解,除葉子結點外的所有結點均有兩個子結點。節點數達到最大值,所有葉子結點必須在同一層上(除了最后一層結點,其它層的結點數都達到了最大值;同時最后一層的結點都是按照從左到右依次排布。

滿二叉樹:若設二叉樹的深度為h,除第 h 層外,其它各層 (1~(h-1)層) 的結點數都達到最大個數,第h層所有的結點都連續集中在最左邊(除了最后一層,其它層的結點都有兩個子結點)。

平衡二叉樹

平衡二叉樹又被稱為AVL樹,它是一棵二叉排序樹,且具有以下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹

二叉排序樹:是一棵空樹,或者:若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;它的左、右子樹也分別為二叉排序樹。

       樹的高度:結點層次的最大值。

       平衡因子:左子樹高度 - 右子樹高度。

二叉排序樹意味着二叉樹中的數據是排好序的,順序為左結點<根節點<右結點,這表明二叉排序樹的中序遍歷結果是有序的。

二叉樹的遍歷方式:

先序遍歷:先根節點->遍歷左子樹->遍歷右子樹

中序遍歷:遍歷左子樹->根節點->遍歷右子樹

后序遍歷:遍歷左子樹->遍歷右子樹->根節點

 

 

深度優先搜索(DFS)與廣度優先搜索(BFS)
實現:bfs=隊列,入隊列,出隊列 一次訪問一條路徑;dfs=棧,壓棧,出棧 一次訪問多條路徑(來自知乎)

關系:用DFS解決的問題都可以用BFS解決。DFS易於編寫(遞歸),時間消耗較少但是容易發生爆棧,而BFS可以控制隊列的長度。

 

 平衡二叉樹的產生是為了解決二叉排序樹在插入時發生線性排列的現象。由於二叉排序樹本身為有序,當插入一個有序程度十分高的序列時,生成的二叉排序樹會持續在某個方向的字數上插入數據,導致最終的二叉排序樹會退化為鏈表,從而使得二叉樹的查詢和插入效率惡化。

 

 平衡二叉樹的出現能夠解決上述問題,但是在構造平衡二叉樹時,卻需要采用不同的調整方式,使得二叉樹在插入數據后保持平衡。

 主要的四種調整方式有LL(左旋)、RR(右旋)、LR(先左旋再右旋)、RL(先右旋再左旋)。這里先給大家介紹下簡單的單旋轉操作,左旋和右旋。LR和RL本質上只是LL和RR的組合。

在插入一個結點后應該沿搜索路徑將路徑上的結點平衡因子進行修改,當平衡因子大於1時,就需要進行平衡化處理。從發生不平衡的結點起,沿剛才回溯的路徑取直接下兩層的結點,如果這三個結點在一條直線上,則采用單旋轉進行平衡化,如果這三個結點位於一條折線上,則采用雙旋轉進行平衡化。

左旋:S為當前需要左旋的結點,E為當前結點的父節點。

 

 左旋的操作可以用一句話簡單表示:將當前結點S的左孩子旋轉為當前結點父結點E的右孩子,同時將父結點E旋轉為當前結點S的左孩子。

 右旋:S為當前需要左旋的結點,E為當前結點的父節點。右單旋是左單旋的鏡像旋轉。

 

 

 

 右旋的操作同樣可以用一句話簡單表示:將當前結點S的左孩子E的右孩子旋轉為當前結點S的左孩子,同時將當前結點S旋轉為左孩子E的右孩子。

 

紅黑樹

平衡二叉樹(AVL)為了追求高度平衡,需要通過平衡處理使得左右子樹的高度差必須小於等於1。高度平衡帶來的好處是能夠提供更高的搜索效率,其最壞的查找時間復雜度都是O(logN)。但是由於需要維持這份高度平衡,所付出的代價就是當對樹種結點進行插入和刪除時,需要經過多次旋轉實現復衡。這導致AVL的插入和刪除效率並不高。

為了解決這樣的問題,能不能找一種結構能夠兼顧搜索和插入刪除的效率呢?紅黑樹可以解決。

紅黑樹具有五個特性:   

  1. 每個結點要么是紅的要么是黑的。
  2. 根結點是黑的。
  3. 每個葉結點(葉結點即指樹尾端NIL指針或NULL結點)都是黑的。
  4. 如果一個結點是紅的,那么它的兩個兒子都是黑的。
  5. 對於任意結點而言,其到葉結點樹尾端NIL指針的每條路徑都包含相同數目的黑結點。

 

 紅黑樹通過將結點進行紅黑着色,使得原本高度平衡的樹結構被稍微打亂,平衡程度降低。紅黑樹不追求完全平衡,只要求達到部分平衡。這是一種折中的方案,大大提高了結點刪除和插入的效率。C++中的STL就常用到紅黑樹作為底層的數據結構。

 

紅黑樹VS平衡二叉樹

 

 除了上面所提及的樹結構,還有許多廣泛應用在數據庫、磁盤存儲等場景下的樹結構。比如B樹、B+樹等。

7、堆

堆就是用數組實現的二叉樹,所有它沒有使用父指針或者子指針。堆根據“堆屬性”來排序,“堆屬性”決定了樹中節點的位置。

堆的常用方法:

  • 構建優先隊列
  • 支持堆排序
  • 快速找出一個集合中的最小值(或者最大值)

堆分為兩種:最大堆最小堆,兩者的差別在於節點的排序方式。

在最大堆中,父節點的值比每一個子節點的值都要大。在最小堆中,父節點的值比每一個子節點的值都要小。這就是所謂的“堆屬性”,並且這個屬性對堆中的每一個節點都成立。

了解完二叉樹,再來理解堆就不是什么難事了。堆通常是一個可以被看做一棵樹的數組對象。堆的具體實現一般不通過指針域,而是通過構建一個一維數組與二叉樹的父子結點進行對應,因此堆總是一顆完全二叉樹。

對於任意一個父節點的序號n來說(這里n從0算),它的子節點的序號一定是2n+1,2n+2,因此可以直接用數組來表示一個堆。

不僅如此,堆還有一個性質:堆中某個節點的值總是不大於或不小於其父節點的值。將根節點最大的堆叫做最大堆或大根堆,根節點最小的堆叫做最小堆或小根堆。

 

 堆常用來實現優先隊列,在面試中經常考的問題都是與排序有關,比如堆排序、topK問題等。由於堆的根節點是序列中最大或者最小值,因而可以在建堆以及重建堆的過程中,篩選出數據序列中的極值,從而達到排序或者挑選topK值的目的。

 

8、散列表(hash表)

散列表也叫哈希表,是一種通過鍵值對直接訪問數據的機構。在初中,我們就學過一種能夠將一個x值通過一個函數獲得對應的一個y值的操作,叫做映射。散列表的實現原理正是映射的原理,通過設定的一個關鍵字和一個映射函數,就可以直接獲得訪問數據的地址,實現O(1)的數據訪問效率。在映射的過程中,事先設定的函數就是一個映射表,也可以稱作散列函數或者哈希函數。

 

 

 

 散列表的實現最關鍵的就是散列函數的定義和選擇。一般常用的有以下幾種散列函數:

直接尋址法:取關鍵字或關鍵字的某個線性函數值為散列地址。

數字分析法:通過對數據的分析,發現數據中沖突較少的部分,並構造散列地址。例如同學們的學號,通常同一屆學生的學號,其中前面的部分差別不太大,所以用后面的部分來構造散列地址。

平方取中:當無法確定關鍵字里哪幾位的分布相對比較均勻時,可以先求出關鍵字的平方值,然后按需要取平方值的中間幾位作為散列地址。這是因為:計算平方之后的中間幾位和關鍵字中的每一位都相關,所以不同的關鍵字會以較高的概率產生不同的散列地址。

取隨機數法:使用一個隨機函數,取關鍵字的隨機值作為散列地址,這種方式通常用於關鍵字長度不同的場合。

除留取余法:取關鍵字被某個不大於散列表的表長 n 的數 m 除后所得的余數 p 為散列地址。這種方式也可以在用過其他方法后再使用。該函數對 m 的選擇很重要,一般取素數或者直接用 n。

確定好散列函數之后,通過某個key值的確會得到一個唯一的value地址。但是卻會出現一些特殊情況。即通過不同的key值可能會訪問到同一個地址,這個現象稱之為沖突。

沖突在發生之后,當在對不同的key值進行操作時會使得造成相同地址的數據發生覆蓋或者丟失,是非常危險的。所以在設計散列表往往還需要采用沖突解決的辦法。

常用的沖突處理方式有很多,常用的包括以下幾種:

開放地址法(也叫開放尋址法):實際上就是當需要存儲值時,對Key哈希之后,發現這個地址已經有值了,這時該怎么辦?不能放在這個地址,不然之前的映射會被覆蓋。這時對計算出來的地址進行一個探測再哈希,比如往后移動一個地址,如果沒人占用,就用這個地址。如果超過最大長度,則可以對總長度取余。這里移動的地址是產生沖突時的增列序量。

再哈希法:在產生沖突之后,使用關鍵字的其他部分繼續計算地址,如果還是有沖突,則繼續使用其他部分再計算地址。這種方式的缺點是時間增加了。

鏈地址法:鏈地址法其實就是對Key通過哈希之后落在同一個地址上的值,做一個鏈表。其實在很多高級語言的實現當中,也是使用這種方式處理沖突的。

公共溢出區:這種方式是建立一個公共溢出區,當地址存在沖突時,把新的地址放在公共溢出區里。

目前比較常用的沖突解決方法是鏈地址法,一般可以通過數組和鏈表的結合達到沖突數據緩存的目的。

 

 左側數組的每個成員包括一個指針,指向一個鏈表的頭。每發生一個沖突的數據,就將該數據作為鏈表的節點鏈接到鏈表尾部。這樣一來,就可以保證沖突的數據能夠區分並順利訪問。

考慮到鏈表過長造成的問題,還可以使用紅黑樹替換鏈表進行沖突數據的處理操作,來提高散列表的查詢穩定性。

9、圖

圖相較於上文的幾個結構可能接觸的不多,但是在實際的應用場景中卻經常出現。比方說交通中的線路圖,常見的思維導圖都可以看作是圖的具體表現形式。

圖結構一般包括頂點和邊,頂點通常用圓圈來表示,邊就是這些圓圈之間的連線。邊還可以根據頂點之間的關系設置不同的權重,默認權重相同皆為1。此外根據邊的方向性,還可將圖分為有向圖和無向圖。

 

 圖結構用抽象的圖線來表示十分簡單,頂點和邊之間的關系非常清晰明了。但是在具體的代碼實現中,為了將各個頂點和邊的關系存儲下來,卻不是一件易事。

 


免責聲明!

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



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