資料結構和演算法-常見資料結構總結
參考:
https://www.21ic.com/article/807017.html
https://www.cnblogs.com/wanghuaijun/p/7302303.html
https://blog.csdn.net/yeyazhishang/article/details/82353846
https://www.cnblogs.com/xdecode/p/9321848.html
圖解!24張圖徹底弄懂九大常見資料結構!
[導讀]資料結構想必大家都不會陌生,對於一個成熟的程式設計師而言,熟悉和掌握資料結構和演算法也是基本功之一。資料結構本身其實不過是資料按照特點關係進行儲存或者組織的集合,特殊的結構在不同的應用場景中往往會帶來不一樣的處理效率。 常用的資料結構可根據資料訪資料結構想必大家都不會陌生,對於一個成熟的程式設計師而言,熟悉和掌握資料結構和演算法也是基本功之一。資料結構本身其實不過是資料按照特點關係進行儲存或者組織的集合,特殊的結構在不同的應用場景中往往會帶來不一樣的處理效率。
常用的資料結構可根據資料訪問的特點分為線性結構和非線性結構。線性結構包括常見的連結串列、棧、佇列等,非線性結構包括樹、圖等。資料結構種類繁多,本文將通過圖解的方式對常用的資料結構進行理論上的介紹和講解,以方便大家掌握常用資料結構的基本知識。
本文提綱
1陣列
陣列可以說是最基本最常見的資料結構。陣列一般用來儲存相同型別的資料,可通過陣列名和下標進行資料的訪問和更新。陣列中元素的儲存是按照先後順序進行的,同時在記憶體中也是按照這個順序進行連續存放。陣列相鄰元素之間的記憶體地址的間隔一般就是陣列資料型別的大小。
2連結串列
連結串列相較於陣列,除了資料域,還增加了指標域用於構建鏈式的儲存資料。連結串列中每一個節點都包含此節點的資料和指向下一節點地址的指標。由於是通過指標進行下一個資料元素的查詢和訪問,使得連結串列的自由度更高。
這表現在對節點進行增加和刪除時,只需要對上一節點的指標地址進行修改,而無需變動其它的節點。不過事物皆有兩極,指標帶來高自由度的同時,自然會犧牲資料查詢的效率和多餘空間的使用。
一般常見的是有頭有尾的單鏈表,對指標域進行反向連結,還可以形成雙向連結串列或者迴圈連結串列。
連結串列和陣列對比
連結串列和陣列在實際的使用過程中需要根據自身的優劣勢進行選擇。連結串列和陣列的異同點也是面試中高頻的考察點之一。這裡對單鏈表和陣列的區別進行了對比和總結。
3跳錶
從上面的對比中可以看出,連結串列雖然通過增加指標域提升了自由度,但是卻導致資料的查詢效率惡化。特別是當連結串列長度很長的時候,對資料的查詢還得從頭依次查詢,這樣的效率會更低。跳錶的產生就是為了解決連結串列過長的問題,通過增加連結串列的多級索引來加快原始連結串列的查詢效率。這樣的方式可以讓查詢的時間複雜度從O(n)提升至O(logn)。
跳錶通過增加的多級索引能夠實現高效的動態插入和刪除,其效率和紅黑樹和平衡二叉樹不相上下。目前redis和levelDB都有用到跳錶。
從上圖可以看出,索引級的指標域除了指向下一個索引位置的指標,還有一個down指標指向低一級的連結串列位置,這樣才能實現跳躍查詢的目的。
4棧
棧是一種比較簡單的資料結構,常用一句話描述其特性,後進先出。棧本身是一種線性結構,但是在這個結構中只有一個口子允許資料的進出。這種模式可以參考腔腸動物...即進食和排洩都用一個口...
棧的常用操作包括入棧push和出棧pop,對應於資料的壓入和壓出。還有訪問棧頂資料、判斷棧是否為空和判斷棧的大小等。由於棧後進先出的特性,常可以作為資料操作的臨時容器,對資料的順序進行調控,與其它資料結構相結合可獲得許多靈活的處理。
5佇列
佇列是棧的兄弟結構,與棧的後進先出相對應,佇列是一種先進先出的資料結構。顧名思義,佇列的資料儲存是如同排隊一般,先存入的資料先被壓出。常與棧一同配合,可發揮最大的實力。
6樹
樹作為一種樹狀的資料結構,其資料節點之間的關係也如大樹一樣,將有限個節點根據不同層次關係進行排列,從而形成資料與資料之間的父子關係。常見的數的表示形式更接近“倒掛的樹”,因為它將根朝上,葉朝下。
樹的資料儲存在結點中,每個結點有零個或者多個子結點。沒有父結點的結點在最頂端,成為根節點;沒有非根結點有且只有一個父節點;每個非根節點又可以分為多個不相交的子樹。
這意味著樹是具備層次關係的,父子關係清晰,家庭血緣關係明朗;這也是樹與圖之間最主要的區別。
別看樹好像很高階,其實可看作是連結串列的高配版。樹的實現就是對連結串列的指標域進行了擴充,增加了多個地址指向子結點。同時將“連結串列”豎起來,從而凸顯了結點之間的層次關係,更便於分析和理解。
樹可以衍生出許多的結構,若將指標域設定為雙指標,那麼即可形成最常見的二叉樹,即每個結點最多有兩個子樹的樹結構。二叉樹根據結點的排列和數量還可進一度劃分為完全二叉樹、滿二叉樹、平衡二叉樹、紅黑樹等。
完全二叉樹:除了最後一層結點,其它層的結點數都達到了最大值;同時最後一層的結點都是按照從左到右依次排布。
滿二叉樹:除了最後一層,其它層的結點都有兩個子結點。
平衡二叉樹
平衡二叉樹又被稱為AVL樹,它是一棵二叉排序樹,且具有以下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。
二叉排序樹:是一棵空樹,或者:若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;它的左、右子樹也分別為二叉排序樹。
樹的高度:結點層次的最大值
平衡因子:左子樹高度 - 右子樹高度
二叉排序樹意味著二叉樹中的資料是排好序的,順序為左結點<根節點<右結點,這表明二叉排序樹的中序遍歷結果是有序的。(還不懂二叉樹四種遍歷方式[前序遍歷、中序遍歷、後序遍歷、層序遍歷]的同學趕緊補習!)
平衡二叉樹的產生是為了解決二叉排序樹在插入時發生線性排列的現象。由於二叉排序樹本身為有序,當插入一個有序程度十分高的序列時,生成的二叉排序樹會持續在某個方向的字數上插入資料,導致最終的二叉排序樹會退化為連結串列,從而使得二叉樹的查詢和插入效率惡化。
平衡二叉樹的出現能夠解決上述問題,但是在構造平衡二叉樹時,卻需要採用不同的調整方式,使得二叉樹在插入資料後保持平衡。主要的四種調整方式有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的插入和刪除效率並不高。
為了解決這樣的問題,能不能找一種結構能夠兼顧搜尋和插入刪除的效率呢?這時候紅黑樹便申請出戰了。
紅黑樹具有五個特性:
- 每個結點要麼是紅的要麼是黑的。
- 根結點是黑的。
- 每個葉結點(葉結點即指樹尾端NIL指標或NULL結點)都是黑的。
- 如果一個結點是紅的,那麼它的兩個兒子都是黑的。
- 對於任意結點而言,其到葉結點樹尾端NIL指標的每條路徑都包含相同數目的黑結點。
紅黑樹通過將結點進行紅黑著色,使得原本高度平衡的樹結構被稍微打亂,平衡程度降低。紅黑樹不追求完全平衡,只要求達到部分平衡。這是一種折中的方案,大大提高了結點刪除和插入的效率。C++中的STL就常用到紅黑樹作為底層的資料結構。
紅黑樹VS平衡二叉樹
除了上面所提及的樹結構,還有許多廣泛應用在資料庫、磁碟儲存等場景下的樹結構。比如B樹、B+樹等。這裡就先不介紹了誒,下次在講述相關儲存原理的時候將會著重介紹。(其實是因為懶)
7堆
瞭解完二叉樹,再來理解堆就不是什麼難事了。堆通常是一個可以被看做一棵樹的陣列物件。堆的具體實現一般不通過指標域,而是通過構建一個一維陣列與二叉樹的父子結點進行對應,因此堆總是一顆完全二叉樹。
對於任意一個父節點的序號n來說(這裡n從0算),它的子節點的序號一定是2n+1,2n+2,因此可以直接用陣列來表示一個堆。
不僅如此,堆還有一個性質:堆中某個節點的值總是不大於或不小於其父節點的值。將根節點最大的堆叫做最大堆或大根堆,根節點最小的堆叫做最小堆或小根堆。
堆常用來實現優先佇列,在面試中經常考的問題都是與排序有關,比如堆排序、topK問題等。由於堆的根節點是序列中最大或者最小值,因而可以在建堆以及重建堆的過程中,篩選出資料序列中的極值,從而達到排序或者挑選topK值的目的。
8散列表
散列表也叫雜湊表,是一種通過鍵值對直接訪問資料的機構。在初中,我們就學過一種能夠將一個x值通過一個函式獲得對應的一個y值的操作,叫做對映。散列表的實現原理正是對映的原理,通過設定的一個關鍵字和一個對映函式,就可以直接獲得訪問資料的地址,實現O(1)的資料訪問效率。在對映的過程中,事先設定的函式就是一個對映表,也可以稱作雜湊函式或者雜湊函式。
散列表的實現最關鍵的就是雜湊函式的定義和選擇。一般常用的有以下幾種雜湊函式:
直接定址法:取關鍵字或關鍵字的某個線性函式值為雜湊地址。
數字分析法:通過對資料的分析,發現數據中衝突較少的部分,並構造雜湊地址。例如同學們的學號,通常同一屆學生的學號,其中前面的部分差別不太大,所以用後面的部分來構造雜湊地址。
平方取中法:當無法確定關鍵字裡哪幾位的分佈相對比較均勻時,可以先求出關鍵字的平方值,然後按需要取平方值的中間幾位作為雜湊地址。這是因為:計算平方之後的中間幾位和關鍵字中的每一位都相關,所以不同的關鍵字會以較高的概率產生不同的雜湊地址。
取隨機數法:使用一個隨機函式,取關鍵字的隨機值作為雜湊地址,這種方式通常用於關鍵字長度不同的場合。
除留取餘法:取關鍵字被某個不大於散列表的表長 n 的數 m 除後所得的餘數 p 為雜湊地址。這種方式也可以在用過其他方法後再使用。該函式對 m 的選擇很重要,一般取素數或者直接用 n。
確定好雜湊函式之後,通過某個key
值的確會得到一個唯一的value
地址。但是卻會出現一些特殊情況。即通過不同的key
值可能會訪問到同一個地址,這個現象稱之為衝突。
衝突在發生之後,當在對不同的key
值進行操作時會使得造成相同地址的資料發生覆蓋或者丟失,是非常危險的。所以在設計散列表往往還需要採用衝突解決的辦法。
常用的衝突處理方式有很多,常用的包括以下幾種:
開放地址法(也叫開放定址法):實際上就是當需要儲存值時,對Key雜湊之後,發現這個地址已經有值了,這時該怎麼辦?不能放在這個地址,不然之前的對映會被覆蓋。這時對計算出來的地址進行一個探測再雜湊,比如往後移動一個地址,如果沒人佔用,就用這個地址。如果超過最大長度,則可以對總長度取餘。這裡移動的地址是產生衝突時的增列序量。
再雜湊法:在產生衝突之後,使用關鍵字的其他部分繼續計算地址,如果還是有衝突,則繼續使用其他部分再計算地址。這種方式的缺點是時間增加了。
鏈地址法:鏈地址法其實就是對Key通過雜湊之後落在同一個地址上的值,做一個連結串列。其實在很多高階語言的實現當中,也是使用這種方式處理衝突的。
公共溢位區:這種方式是建立一個公共溢位區,當地址存在衝突時,把新的地址放在公共溢位區裡。
目前比較常用的衝突解決方法是鏈地址法,一般可以通過陣列和連結串列的結合達到衝突資料快取的目的。
左側陣列的每個成員包括一個指標,指向一個連結串列的頭。每發生一個衝突的資料,就將該資料作為連結串列的節點連結到連結串列尾部。這樣一來,就可以保證衝突的資料能夠區分並順利訪問。考慮到連結串列過長造成的問題,還可以使用紅黑樹替換連結串列進行衝突資料的處理操作,來提高散列表的查詢穩定性。
9圖
圖相較於上文的幾個結構可能接觸的不多,但是在實際的應用場景中卻經常出現。比方說交通中的線路圖,常見的思維導圖都可以看作是圖的具體表現形式。
圖結構一般包括頂點和邊,頂點通常用圓圈來表示,邊就是這些圓圈之間的連線。邊還可以根據頂點之間的關係設定不同的權重,預設權重相同皆為1。此外根據邊的方向性,還可將圖分為有向圖和無向圖。
圖結構用抽象的圖線來表示十分簡單,頂點和邊之間的關係非常清晰明瞭。但是在具體的程式碼實現中,為了將各個頂點和邊的關係儲存下來,卻不是一件易事。
鄰接矩陣
目前常用的圖儲存方式為鄰接矩陣,通過所有頂點的二維矩陣來儲存兩個頂點之間是否相連,或者儲存兩頂點間的邊權重。
無向圖的鄰接矩陣是一個對稱矩陣,是因為邊不具有方向性,若能從此頂點能夠到達彼頂點,那麼彼頂點自然也能夠達到此頂點。此外,由於頂點本身與本身相連沒有意義,所以在鄰接矩陣中對角線上皆為0。
有向圖由於邊具有方向性,因此彼此頂點之間並不能相互達到,所以其鄰接矩陣的對稱性不再。用鄰接矩陣可以直接從二維關係中獲得任意兩個頂點的關係,可直接判斷是否相連。但是在對矩陣進行儲存時,卻需要完整的一個二維陣列。若圖中頂點數過多,會導致二維陣列的大小劇增,從而佔用大量的記憶體空間。
而根據實際情況可以分析得,圖中的頂點並不是任意兩個頂點間都會相連,不是都需要對其邊上權重進行儲存。那麼儲存的鄰接矩陣實際上會存在大量的0。雖然可以通過稀疏表示等方式對稀疏性高的矩陣進行關鍵資訊的儲存,但是卻增加了圖儲存的複雜性。
因此,為了解決上述問題,一種可以只儲存相連頂點關係的鄰接表應運而生。
鄰接表
在鄰接表中,圖的每一個頂點都是一個連結串列的頭節點,其後連線著該頂點能夠直接達到的相鄰頂點。相較於無向圖,有向圖的情況更為複雜,因此這裡採用有向圖進行例項分析。
在鄰接表中,每一個頂點都對應著一條連結串列,連結串列中儲存的是頂點能夠達到的相鄰頂點。儲存的順序可以按照頂點的編號順序進行。比如上圖中對於頂點B來說,其通過有向邊可以到達頂點A和頂點E,那麼其對應的鄰接表中的順序即B->A->E,其它頂點亦如此。
通過鄰接表可以獲得從某個頂點出發能夠到達的頂點,從而省去了對不相連頂點的儲存空間。然而,這還不夠。對於有向圖而言,圖中有效資訊除了從頂點“指出去”的資訊,還包括從別的頂點“指進來”的資訊。這裡的“指出去”和“指進來”可以用出度和入度來表示。
入度:有向圖的某個頂點作為終點的次數和。
出度:有向圖的某個頂點作為起點的次數和。
由此看出,在對有向圖進行表示時,鄰接表只能求出圖的出度,而無法求出入度。這個問題很好解決,那就是增加一個表用來儲存能夠到達某個頂點的相鄰頂點。這個表稱作逆鄰接表。
逆鄰接表
逆鄰接表與鄰接表結構類似,只不過圖的頂點連結著能夠到達該頂點的相鄰頂點。也就是說,鄰接表時順著圖中的箭頭尋找相鄰頂點,而逆鄰接表時逆著圖中的箭頭尋找相鄰頂點。
鄰接表和逆鄰接表的共同使用下,就能夠把一個完整的有向圖結構進行表示。可以發現,鄰接表和逆鄰接表實際上有一部分資料時重合的,因此可以將兩個表合二為一,從而得到了所謂的十字連結串列。
十字連結串列
十字連結串列似乎很簡單,只需要通過相同的頂點分別鏈向以該頂點為終點和起點的相鄰頂點即可。
但這並不是最優的表示方式。雖然這樣的方式共用了中間的頂點儲存空間,但是鄰接表和逆鄰接表的連結串列節點中重複出現的頂點並沒有得到重複利用,反而是進行了再次儲存。因此,上圖的表示方式還可以進行進一步優化。
十字連結串列優化後,可通過擴充套件的頂點結構和邊結構來進行正逆鄰接表的儲存:(下面的弧頭可看作是邊的箭頭那端,弧尾可看作是邊的圓點那端)
data:用於儲存該頂點中的資料;
firstin指標:用於連線以當前頂點為弧頭的其他頂點構成的連結串列,即從別的頂點指進來的頂點;
firstout指標:用於連線以當前頂點為弧尾的其他頂點構成的連結串列,即從該頂點指出去的頂點;
邊結構通過儲存兩個頂點來確定一條邊,同時通過分別代表這兩個頂點的指標來與相鄰頂點進行連結:
tailvex:用於儲存作為弧尾的頂點的編號;
headvex:用於儲存作為弧頭的頂點的編號;
headlink指標:用於連結下一個儲存作為弧頭的頂點的節點;
taillink指標:用於連結下一個儲存作為弧尾的頂點的節點;
以上圖為例子,對於頂點A而言,其作為起點能夠到達頂點E。因此在鄰接表中頂點A要通過邊AE
(即邊04)指向頂點E,頂點A的firstout
指標需要指向邊04的tailvex
。同時,從B出發能夠到達A,所以在逆鄰接表中頂點A要通過邊AB
(即邊10)指向B,頂點A的firstin
指標需要指向邊10的弧頭,即headlink
指標。依次類推。
十字連結串列採用了一種看起來比較繁亂的方式對邊的方向性進行了表示,能夠在儘可能降低儲存空間的情況下增加指標保留頂點之間的方向性。具體的操作可能一時間不好弄懂,建議多看幾次上圖,弄清指標指向的意義,明白正向和逆向鄰接表的表示。
10總結
資料結構博大精深,沒有高等數學的諱莫如深,也沒有量子力學的玄乎其神,但是其在電腦科學的各個領域都具有強大的力量。本文試圖採用圖解的方式對九種資料結構進行理論上的介紹,但是其實這都是不夠的。
即便是簡單的陣列、棧、佇列等結構,在實際使用以及底層實現上都會有許多優化設計以及使用技巧,這意味著還需要真正把它們靈活的用起來,才能夠算是真正意義上的熟悉和精通。但是本文可以作為常見資料結構的一個總結,當你對某些結構有些淡忘的時候,不妨重新回來看看。
常見資料結構
資料結構是以某種形式將資料組織在一起的集合,它不僅儲存資料,還支援訪問和處理資料的操作。演算法是為求解一個問題需要遵循的、被清楚指定的簡單指令的集合。下面是自己整理的常用資料結構與演算法相關內容,如有錯誤,歡迎指出。
為了便於描述,文中涉及到的程式碼部分都是用Java語言編寫的,其實Java本身對常見的幾種資料結構,線性表、棧、佇列等都提供了較好的實現,就是我們經常用到的Java集合框架,有需要的可以閱讀這篇文章。Java - 集合框架完全解析
一、線性表
1.陣列實現
2.連結串列
二、棧與佇列
三、樹與二叉樹
1.樹
2.二叉樹基本概念
3.二叉查詢樹
4.平衡二叉樹
5.紅黑樹
四、圖
五、總結
一、線性表
線性表是最常用且最簡單的一種資料結構,它是n個數據元素的有限序列。
實現線性表的方式一般有兩種,一種是使用陣列儲存線性表的元素,即用一組連續的儲存單元依次儲存線性表的資料元素。另一種是使用連結串列儲存線性表的元素,即用一組任意的儲存單元儲存線性表的資料元素(儲存單元可以是連續的,也可以是不連續的)。
陣列實現
陣列是一種大小固定的資料結構,對線性表的所有操作都可以通過陣列來實現。雖然陣列一旦建立之後,它的大小就無法改變了,但是當陣列不能再儲存線性表中的新元素時,我們可以建立一個新的大的陣列來替換當前陣列。這樣就可以使用陣列實現動態的資料結構。
- 程式碼1 建立一個更大的陣列來替換當前陣列
int[] oldArray = new int[10];
int[] newArray = new int[20];
for (int i = 0; i < oldArray.length; i++) {
newArray[i] = oldArray[i];
}
// 也可以使用System.arraycopy方法來實現陣列間的複製
// System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
oldArray = newArray;
- 程式碼2 在陣列位置index上新增元素e
//oldArray 表示當前儲存元素的陣列
//size 表示當前元素個數
public void add(int index, int e) {
if (index > size || index < 0) {
System.out.println("位置不合法...");
}
//如果陣列已經滿了 就擴容
if (size >= oldArray.length) {
// 擴容函式可參考程式碼1
}
for (int i = size - 1; i >= index; i--) {
oldArray[i + 1] = oldArray[i];
}
//將陣列elementData從位置index的所有元素往後移一位
// System.arraycopy(oldArray, index, oldArray, index + 1,size - index);
oldArray[index] = e;
size++;
}
上面簡單寫出了陣列實現線性表的兩個典型函式,具體我們可以參考Java裡面的ArrayList集合類的原始碼。陣列實現的線性表優點在於可以通過下標來訪問或者修改元素,比較高效,主要缺點在於插入和刪除的花費開銷較大,比如當在第一個位置前插入一個元素,那麼首先要把所有的元素往後移動一個位置。為了提高在任意位置新增或者刪除元素的效率,可以採用鏈式結構來實現線性表。
連結串列
連結串列是一種物理儲存單元上非連續、非順序的儲存結構,資料元素的邏輯順序是通過連結串列中的指標連結次序實現的。連結串列由一系列節點組成,這些節點不必在記憶體中相連。每個節點由資料部分Data和鏈部分Next,Next指向下一個節點,這樣當新增或者刪除時,只需要改變相關節點的Next的指向,效率很高。
單鏈表的結構
下面主要用程式碼來展示連結串列的一些基本操作,需要注意的是,這裡主要是以單鏈表為例,暫時不考慮雙鏈表和迴圈連結串列。
- 程式碼3 連結串列的節點
class Node<E> {
E item;
Node<E> next;
//建構函式
Node(E element) {
this.item = element;
this.next = null;
}
}
- 程式碼4 定義好節點後,使用前一般是對頭節點和尾節點進行初始化
//頭節點和尾節點都為空 連結串列為空
Node<E> head = null;
Node<E> tail = null;
- 程式碼5 空連結串列建立一個新節點
//建立一個新的節點 並讓head指向此節點
head = new Node("nodedata1");
//讓尾節點也指向此節點
tail = head;
- 程式碼6 連結串列追加一個節點
//建立新節點 同時和最後一個節點連線起來
tail.next = new Node("node1data2");
//尾節點指向新的節點
tail = tail.next;
- 程式碼7 順序遍歷連結串列
Node<String> current = head;
while (current != null) {
System.out.println(current.item);
current = current.next;
}
- 程式碼8 倒序遍歷連結串列
static void printListRev(Node<String> head) {
//倒序遍歷連結串列主要用了遞迴的思想
if (head != null) {
printListRev(head.next);
System.out.println(head.item);
}
}
- 程式碼 單鏈表反轉
//單鏈表反轉 主要是逐一改變兩個節點間的連結關係來完成
static Node<String> revList(Node<String> head) {
if (head == null) {
return null;
}
Node<String> nodeResult = null;
Node<String> nodePre = null;
Node<String> current = head;
while (current != null) {
Node<String> nodeNext = current.next;
if (nodeNext == null) {
nodeResult = current;
}
current.next = nodePre;
nodePre = current;
current = nodeNext;
}
return nodeResult;
}
上面的幾段程式碼主要展示了連結串列的幾個基本操作,還有很多像獲取指定元素,移除元素等操作大家可以自己完成,寫這些程式碼的時候一定要理清節點之間關係,這樣才不容易出錯。
連結串列的實現還有其它的方式,常見的有迴圈單鏈表,雙向連結串列,迴圈雙向連結串列。迴圈單鏈表主要是連結串列的最後一個節點指向第一個節點,整體構成一個鏈環。雙向連結串列主要是節點中包含兩個指標部分,一個指向前驅元,一個指向後繼元,JDK中LinkedList集合類的實現就是雙向連結串列。迴圈雙向連結串列是最後一個節點指向第一個節點。
二、棧與佇列
棧和佇列也是比較常見的資料結構,它們是比較特殊的線性表,因為對於棧來說,訪問、插入和刪除元素只能在棧頂進行,對於佇列來說,元素只能從佇列尾插入,從佇列頭訪問和刪除。
棧
棧是限制插入和刪除只能在一個位置上進行的表,該位置是表的末端,叫作棧頂,對棧的基本操作有push(進棧)和pop(出棧),前者相當於插入,後者相當於刪除最後一個元素。棧有時又叫作LIFO(Last In First Out)表,即後進先出。
棧的模型
下面我們看一道經典題目,加深對棧的理解。
關於棧的一道經典題目
上圖中的答案是C,其中的原理可以好好想一想。
因為棧也是一個表,所以任何實現表的方法都能實現棧。我們開啟JDK中的類Stack的原始碼,可以看到它就是繼承類Vector的。當然,Stack是Java2前的容器類,現在我們可以使用LinkedList來進行棧的所有操作。
佇列
佇列是一種特殊的線性表,特殊之處在於它只允許在表的前端(front)進行刪除操作,而在表的後端(rear)進行插入操作,和棧一樣,佇列是一種操作受限制的線性表。進行插入操作的端稱為隊尾,進行刪除操作的端稱為隊頭。
佇列示意圖
我們可以使用連結串列來實現佇列,下面程式碼簡單展示了利用LinkedList來實現佇列類。
- 程式碼9 簡單實現佇列類
public class MyQueue<E> {
private LinkedList<E> list = new LinkedList<>();
// 入隊
public void enqueue(E e) {
list.addLast(e);
}
// 出隊
public E dequeue() {
return list.removeFirst();
}
}
普通的佇列是一種先進先出的資料結構,而優先佇列中,元素都被賦予優先順序。當訪問元素的時候,具有最高優先順序的元素最先被刪除。優先佇列在生活中的應用還是比較多的,比如醫院的急症室為病人賦予優先順序,具有最高優先順序的病人最先得到治療。在Java集合框架中,類PriorityQueue就是優先佇列的實現類,具體大家可以去閱讀原始碼。
三、樹與二叉樹
樹型結構是一類非常重要的非線性資料結構,其中以樹和二叉樹最為常用。在介紹二叉樹之前,我們先簡單瞭解一下樹的相關內容。
樹
樹是由n(n>=1)個有限節點組成一個具有層次關係的集合。它具有以下特點:每個節點有零個或多個子節點;沒有父節點的節點稱為根節點;每一個非根節點有且只有一個父節點;除了根節點外,每個子節點可以分為多個不相交的子樹。
樹的結構
二叉樹基本概念
- 定義
二叉樹是每個節點最多有兩棵子樹的樹結構。通常子樹被稱作“左子樹”和“右子樹”。二叉樹常被用於實現二叉查詢樹和二叉堆。
- 相關性質
二叉樹的每個結點至多隻有2棵子樹(不存在度大於2的結點),二叉樹的子樹有左右之分,次序不能顛倒。
二叉樹的第i層至多有2^(i-1)個結點;深度為k的二叉樹至多有2^k-1個結點。
一棵深度為k,且有2^k-1個節點的二叉樹稱之為滿二叉樹;
深度為k,有n個節點的二叉樹,當且僅當其每一個節點都與深度為k的滿二叉樹中,序號為1至n的節點對應時,稱之為完全二叉樹。
- 三種遍歷方法
在二叉樹的一些應用中,常常要求在樹中查詢具有某種特徵的節點,或者對樹中全部節點進行某種處理,這就涉及到二叉樹的遍歷。二叉樹主要是由3個基本單元組成,根節點、左子樹和右子樹。如果限定先左後右,那麼根據這三個部分遍歷的順序不同,可以分為先序遍歷、中序遍歷和後續遍歷三種。
(1)先序遍歷若二叉樹為空,則空操作,否則先訪問根節點,再先序遍歷左子樹,最後先序遍歷右子樹。 (2)中序遍歷若二叉樹為空,則空操作,否則先中序遍歷左子樹,再訪問根節點,最後中序遍歷右子樹。(3)後序遍歷若二叉樹為空,則空操作,否則先後序遍歷左子樹訪問根節點,再後序遍歷右子樹,最後訪問根節點。
給定二叉樹寫出三種遍歷結果
- 樹和二叉樹的區別
(1) 二叉樹每個節點最多有2個子節點,樹則無限制。 (2) 二叉樹中節點的子樹分為左子樹和右子樹,即使某節點只有一棵子樹,也要指明該子樹是左子樹還是右子樹,即二叉樹是有序的。 (3) 樹決不能為空,它至少有一個節點,而一棵二叉樹可以是空的。
上面我們主要對二叉樹的相關概念進行了介紹,下面我們將從二叉查詢樹開始,介紹二叉樹的幾種常見型別,同時將之前的理論部分用程式碼實現出來。
二叉查詢樹
- 定義
二叉查詢樹就是二叉排序樹,也叫二叉搜尋樹。二叉查詢樹或者是一棵空樹,或者是具有下列性質的二叉樹: (1) 若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;(2) 若右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;(3) 左、右子樹也分別為二叉排序樹;(4) 沒有鍵值相等的結點。
典型的二叉查詢樹的構建過程
- 效能分析
對於二叉查詢樹來說,當給定值相同但順序不同時,所構建的二叉查詢樹形態是不同的,下面看一個例子。
不同形態平衡二叉樹的ASL不同
可以看到,含有n個節點的二叉查詢樹的平均查詢長度和樹的形態有關。最壞情況下,當先後插入的關鍵字有序時,構成的二叉查詢樹蛻變為單支樹,樹的深度為n,其平均查詢長度(n+1)/2(和順序查詢相同),最好的情況是二叉查詢樹的形態和折半查詢的判定樹相同,其平均查詢長度和log2(n)成正比。平均情況下,二叉查詢樹的平均查詢長度和logn是等數量級的,所以為了獲得更好的效能,通常在二叉查詢樹的構建過程需要進行“平衡化處理”,之後我們將介紹平衡二叉樹和紅黑樹,這些均可以使查詢樹的高度為O(log(n))。
- 程式碼10 二叉樹的節點
class TreeNode<E> {
E element;
TreeNode<E> left;
TreeNode<E> right;
public TreeNode(E e) {
element = e;
}
}
二叉查詢樹的三種遍歷都可以直接用遞迴的方法來實現:
- 程式碼12 先序遍歷
protected void preorder(TreeNode<E> root) {
if (root == null)
return;
System.out.println(root.element + " ");
preorder(root.left);
preorder(root.right);
}
- 程式碼13 中序遍歷
protected void inorder(TreeNode<E> root) {
if (root == null)
return;
inorder(root.left);
System.out.println(root.element + " ");
inorder(root.right);
}
- 程式碼14 後序遍歷
protected void postorder(TreeNode<E> root) {
if (root == null)
return;
postorder(root.left);
postorder(root.right);
System.out.println(root.element + " ");
}
- 程式碼15 二叉查詢樹的簡單實現
/**
* @author JackalTsc
*/
public class MyBinSearchTree<E extends Comparable<E>> {
// 根
private TreeNode<E> root;
// 預設建構函式
public MyBinSearchTree() {
}
// 二叉查詢樹的搜尋
public boolean search(E e) {
TreeNode<E> current = root;
while (current != null) {
if (e.compareTo(current.element) < 0) {
current = current.left;
} else if (e.compareTo(current.element) > 0) {
current = current.right;
} else {
return true;
}
}
return false;
}
// 二叉查詢樹的插入
public boolean insert(E e) {
// 如果之前是空二叉樹 插入的元素就作為根節點
if (root == null) {
root = createNewNode(e);
} else {
// 否則就從根節點開始遍歷 直到找到合適的父節點
TreeNode<E> parent = null;
TreeNode<E> current = root;
while (current != null) {
if (e.compareTo(current.element) < 0) {
parent = current;
current = current.left;
} else if (e.compareTo(current.element) > 0) {
parent = current;
current = current.right;
} else {
return false;
}
}
// 插入
if (e.compareTo(parent.element) < 0) {
parent.left = createNewNode(e);
} else {
parent.right = createNewNode(e);
}
}
return true;
}
// 建立新的節點
protected TreeNode<E> createNewNode(E e) {
return new TreeNode(e);
}
}
// 二叉樹的節點
class TreeNode<E extends Comparable<E>> {
E element;
TreeNode<E> left;
TreeNode<E> right;
public TreeNode(E e) {
element = e;
}
}
上面的程式碼15主要展示了一個自己實現的簡單的二叉查詢樹,其中包括了幾個常見的操作,當然更多的操作還是需要大家自己去完成。因為在二叉查詢樹中刪除節點的操作比較複雜,所以下面我詳細介紹一下這裡。
- 二叉查詢樹中刪除節點分析
要在二叉查詢樹中刪除一個元素,首先需要定位包含該元素的節點,以及它的父節點。假設current指向二叉查詢樹中包含該元素的節點,而parent指向current節點的父節點,current節點可能是parent節點的左孩子,也可能是右孩子。這裡需要考慮兩種情況:
- current節點沒有左孩子,那麼只需要將patent節點和current節點的右孩子相連。
- current節點有一個左孩子,假設rightMost指向包含current節點的左子樹中最大元素的節點,而parentOfRightMost指向rightMost節點的父節點。那麼先使用rightMost節點中的元素值替換current節點中的元素值,將parentOfRightMost節點和rightMost節點的左孩子相連,然後刪除rightMost節點。
// 二叉搜尋樹刪除節點
public boolean delete(E e) {
TreeNode<E> parent = null;
TreeNode<E> current = root;
// 找到要刪除的節點的位置
while (current != null) {
if (e.compareTo(current.element) < 0) {
parent = current;
current = current.left;
} else if (e.compareTo(current.element) > 0) {
parent = current;
current = current.right;
} else {
break;
}
}
// 沒找到要刪除的節點
if (current == null) {
return false;
}
// 考慮第一種情況
if (current.left == null) {
if (parent == null) {
root = current.right;
} else {
if (e.compareTo(parent.element) < 0) {
parent.left = current.right;
} else {
parent.right = current.right;
}
}
} else { // 考慮第二種情況
TreeNode<E> parentOfRightMost = current;
TreeNode<E> rightMost = current.left;
// 找到左子樹中最大的元素節點
while (rightMost.right != null) {
parentOfRightMost = rightMost;
rightMost = rightMost.right;
}
// 替換
current.element = rightMost.element;
// parentOfRightMost和rightMost左孩子相連
if (parentOfRightMost.right == rightMost) {
parentOfRightMost.right = rightMost.left;
} else {
parentOfRightMost.left = rightMost.left;
}
}
return true;
}
平衡二叉樹
平衡二叉樹又稱AVL樹,它或者是一棵空樹,或者是具有下列性質的二叉樹:它的左子樹和右子樹都是平衡二叉樹,且左子樹和右子樹的深度之差的絕對值不超過1。
平衡二叉樹
AVL樹是最先發明的自平衡二叉查詢樹演算法。在AVL中任何節點的兩個兒子子樹的高度最大差別為1,所以它也被稱為高度平衡樹,n個結點的AVL樹最大深度約1.44log2n。查詢、插入和刪除在平均和最壞情況下都是O(log n)。增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹。
紅黑樹
紅黑樹是平衡二叉樹的一種,它保證在最壞情況下基本動態集合操作的事件複雜度為O(log n)。紅黑樹和平衡二叉樹區別如下:(1) 紅黑樹放棄了追求完全平衡,追求大致平衡,在與平衡二叉樹的時間複雜度相差不大的情況下,保證每次插入最多隻需要三次旋轉就能達到平衡,實現起來也更為簡單。(2) 平衡二叉樹追求絕對平衡,條件比較苛刻,實現起來比較麻煩,每次插入新節點之後需要旋轉的次數不能預知。點選檢視更多
四、圖
- 簡介
圖是一種較線性表和樹更為複雜的資料結構,線上性表中,資料元素之間僅有線性關係,在樹形結構中,資料元素之間有著明顯的層次關係,而在圖形結構中,節點之間的關係可以是任意的,圖中任意兩個資料元素之間都可能相關。圖的應用相當廣泛,特別是近年來的迅速發展,已經滲入到諸如語言學、邏輯學、物理、化學、電訊工程、電腦科學以及數學的其他分支中。
- 相關閱讀
因為圖這部分的內容還是比較多的,這裡就不詳細介紹了,有需要的可以自己搜尋相關資料。
(1)《百度百科對圖的介紹》
(2)《資料結構之圖(儲存結構、遍歷)》
五、總結
到這裡,關於常見的資料結構的整理就結束了,斷斷續續大概花了兩天時間寫完,在總結的過程中,通過查閱相關資料,結合書本內容,收穫還是很大的,在下一篇部落格中將會介紹常用資料結構與演算法整理總結(下)之演算法篇,歡迎大家關注。
作者:塵語凡心
連結:http://www.jianshu.com/p/230e6fde9c75
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
資料結構:八大資料結構分類
資料結構分類
資料結構是指相互之間存在著一種或多種關係的資料元素的集合和該集合中資料元素之間的關係組成 。
常用的資料結構有:陣列,棧,連結串列,佇列,樹,圖,堆,散列表等,如圖所示:
每一種資料結構都有著獨特的資料儲存方式,下面為大家介紹它們的結構和優缺點。
1、陣列
陣列是可以再記憶體中連續儲存多個元素的結構,在記憶體中的分配也是連續的,陣列中的元素通過陣列下標進行訪問,陣列下標從0開始。例如下面這段程式碼就是將陣列的第一個元素賦值為 1。
int[] data = new int[100];data[0] = 1;
- 1
- 2
優點:
1、按照索引查詢元素速度快
2、按照索引遍歷陣列方便
缺點:
1、陣列的大小固定後就無法擴容了
2、陣列只能儲存一種型別的資料
3、新增,刪除的操作慢,因為要移動其他的元素。
適用場景:
頻繁查詢,對儲存空間要求不大,很少增加和刪除的情況。
2、棧
棧是一種特殊的線性表,僅能線上性表的一端操作,棧頂允許操作,棧底不允許操作。 棧的特點是:先進後出,或者說是後進先出,從棧頂放入元素的操作叫入棧,取出元素叫出棧。
棧的結構就像一個集裝箱,越先放進去的東西越晚才能拿出來,所以,棧常應用於實現遞迴功能方面的場景,例如斐波那契數列。
3、佇列
佇列與棧一樣,也是一種線性表,不同的是,佇列可以在一端新增元素,在另一端取出元素,也就是:先進先出。從一端放入元素的操作稱為入隊,取出元素為出隊,示例圖如下:
使用場景:因為佇列先進先出的特點,在多執行緒阻塞佇列管理中非常適用。
4、連結串列
連結串列是物理儲存單元上非連續的、非順序的儲存結構,資料元素的邏輯順序是通過連結串列的指標地址實現,每個元素包含兩個結點,一個是儲存元素的資料域 (記憶體空間),另一個是指向下一個結點地址的指標域。根據指標的指向,連結串列能形成不同的結構,例如單鏈表,雙向連結串列,迴圈連結串列等。
連結串列的優點:
連結串列是很常用的一種資料結構,不需要初始化容量,可以任意加減元素;
新增或者刪除元素時只需要改變前後兩個元素結點的指標域指向地址即可,所以新增,刪除很快;
缺點:
因為含有大量的指標域,佔用空間較大;
查詢元素需要遍歷連結串列來查詢,非常耗時。
適用場景:
資料量較小,需要頻繁增加,刪除操作的場景
5、樹
樹是一種資料結構,它是由n(n>=1)個有限節點組成一個具有層次關係的集合。把它叫做 “樹” 是因為它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。它具有以下的特點:
- 每個節點有零個或多個子節點;
- 沒有父節點的節點稱為根節點;
- 每一個非根節點有且只有一個父節點;
- 除了根節點外,每個子節點可以分為多個不相交的子樹;
在日常的應用中,我們討論和用的更多的是樹的其中一種結構,就是二叉樹。
二叉樹是樹的特殊一種,具有如下特點:
1、每個結點最多有兩顆子樹,結點的度最大為2。
2、左子樹和右子樹是有順序的,次序不能顛倒。
3、即使某結點只有一個子樹,也要區分左右子樹。
二叉樹是一種比較有用的折中方案,它新增,刪除元素都很快,並且在查詢方面也有很多的演算法優化,所以,二叉樹既有連結串列的好處,也有陣列的好處,是兩者的優化方案,在處理大批量的動態資料方面非常有用。
擴充套件:
二叉樹有很多擴充套件的資料結構,包括平衡二叉樹、紅黑樹、B+樹等,這些資料結構二叉樹的基礎上衍生了很多的功能,在實際應用中廣泛用到,例如mysql的資料庫索引結構用的就是B+樹,還有HashMap的底層原始碼中用到了紅黑樹。這些二叉樹的功能強大,但演算法上比較複雜,想學習的話還是需要花時間去深入的。
6、散列表
散列表,也叫雜湊表,是根據關鍵碼和值 (key和value) 直接進行訪問的資料結構,通過key和value來對映到集合中的一個位置,這樣就可以很快找到集合中的對應元素。
記錄的儲存位置=f(key)
這裡的對應關係f成為雜湊函式,又稱為雜湊 (hash函式),而散列表就是把Key通過一個固定的演算法函式既所謂的雜湊函式轉換成一個整型數字,然後就將該數字對陣列長度進行取餘,取餘結果就當作陣列的下標,將value儲存在以該數字為下標的陣列空間裡,這種儲存空間可以充分利用陣列的查詢優勢來查詢元素,所以查詢的速度很快。
雜湊表在應用中也是比較常見的,就如Java中有些集合類就是借鑑了雜湊原理構造的,例如HashMap,HashTable等,利用hash表的優勢,對於集合的查詢元素時非常方便的,然而,因為雜湊表是基於陣列衍生的資料結構,在新增刪除元素方面是比較慢的,所以很多時候需要用到一種陣列連結串列來做,也就是拉鍊法。拉鍊法是陣列結合連結串列的一種結構,較早前的hashMap底層的儲存就是採用這種結構,直到jdk1.8之後才換成了陣列加紅黑樹的結構,其示例圖如下:
從圖中可以看出,左邊很明顯是個陣列,陣列的每個成員包括一個指標,指向一個連結串列的頭,當然這個連結串列可能為空,也可能元素很多。我們根據元素的一些特徵把元素分配到不同的連結串列中去,也是根據這些特徵,找到正確的連結串列,再從連結串列中找出這個元素。
雜湊表的應用場景很多,當然也有很多問題要考慮,比如雜湊衝突的問題,如果處理的不好會浪費大量的時間,導致應用崩潰。
7、堆
堆是一種比較特殊的資料結構,可以被看做一棵樹的陣列物件,具有以下的性質:
-
堆中某個節點的值總是不大於或不小於其父節點的值;
-
堆總是一棵完全二叉樹。
將根節點最大的堆叫做最大堆或大根堆,根節點最小的堆叫做最小堆或小根堆。常見的堆有二叉堆、斐波那契堆等。
堆的定義如下:n個元素的序列{k1,k2,ki,…,kn}當且僅當滿足下關係時,稱之為堆。
(ki <= k2i,ki <= k2i+1)或者(ki >= k2i,ki >= k2i+1), (i = 1,2,3,4…n/2),滿足前者的表示式的成為小頂堆,滿足後者表示式的為大頂堆,這兩者的結構圖可以用完全二叉樹排列出來,示例圖如下:
因為堆有序的特點,一般用來做陣列中的排序,稱為堆排序。
8、圖
圖是由結點的有窮集合V和邊的集合E組成。其中,為了與樹形結構加以區別,在圖結構中常常將結點稱為頂點,邊是頂點的有序偶對,若兩個頂點之間存在一條邊,就表示這兩個頂點具有相鄰關係。
按照頂點指向的方向可分為無向圖和有向圖:
圖是一種比較複雜的資料結構,在儲存資料上有著比較複雜和高效的演算法,分別有鄰接矩陣 、鄰接表、十字連結串列、鄰接多重表、邊集陣列等儲存結構,這裡不做展開,讀者有興趣可以自己學習深入。
圖解Java常用資料結構
最近在整理資料結構方面的知識, 系統化看了下Java中常用資料結構, 突發奇想用動畫來繪製資料流轉過程.
主要基於jdk8, 可能會有些特性與jdk7之前不相同, 例如LinkedList LinkedHashMap中的雙向列表不再是迴環的.
HashMap中的單鏈表是尾插, 而不是頭插入等等, 後文不再贅敘這些差異, 本文目錄結構如下:
LinkedList
經典的雙鏈表結構, 適用於亂序插入, 刪除. 指定序列操作則效能不如ArrayList, 這也是其資料結構決定的.
add(E) / addLast(E)
add(index, E)
這邊有個小的優化, 他會先判斷index是靠近隊頭還是隊尾, 來確定從哪個方向遍歷鏈入.
1 if (index < (size >> 1)) { 2 Node<E> x = first; 3 for (int i = 0; i < index; i++) 4 x = x.next; 5 return x; 6 } else { 7 Node<E> x = last; 8 for (int i = size - 1; i > index; i--) 9 x = x.prev; 10 return x; 11 }
靠隊尾
get(index)
也是會先判斷index, 不過效能依然不好, 這也是為什麼不推薦用for(int i = 0; i < lengh; i++)的方式遍歷linkedlist, 而是使用iterator的方式遍歷.
remove(E)
ArrayList
底層就是一個數組, 因此按序查詢快, 亂序插入, 刪除因為涉及到後面元素移位所以效能慢.
add(index, E)
擴容
一般預設容量是10, 擴容後, 會length*1.5.
remove(E)
迴圈遍歷陣列, 判斷E是否equals當前元素, 刪除效能不如LinkedList.
Stack
經典的資料結構, 底層也是陣列, 繼承自Vector, 先進後出FILO, 預設new Stack()容量為10, 超出自動擴容.
push(E)
pop()
字尾表示式
Stack的一個典型應用就是計算表示式如 9 + (3 - 1) * 3 + 10 / 2, 計算機將中綴表示式轉為字尾表示式, 再對字尾表示式進行計算.
中綴轉字尾
- 數字直接輸出
- 棧為空時,遇到運算子,直接入棧
- 遇到左括號, 將其入棧
- 遇到右括號, 執行出棧操作,並將出棧的元素輸出,直到彈出棧的是左括號,左括號不輸出。
- 遇到運算子(加減乘除):彈出所有優先順序大於或者等於該運算子的棧頂元素,然後將該運算子入棧
- 最終將棧中的元素依次出棧,輸出。
計算字尾表達
- 遇到數字時,將數字壓入堆疊
- 遇到運算子時,彈出棧頂的兩個數,用運算子對它們做相應的計算, 並將結果入棧
- 重複上述過程直到表示式最右端
- 運算得出的值即為表示式的結果
佇列
與Stack的區別在於, Stack的刪除與新增都在隊尾進行, 而Queue刪除在隊頭, 新增在隊尾.
ArrayBlockingQueue
生產消費者中常用的阻塞有界佇列, FIFO.
put(E)
put(E) 佇列滿了
1 final ReentrantLock lock = this.lock; 2 lock.lockInterruptibly(); 3 try { 4 while (count == items.length) 5 notFull.await(); 6 enqueue(e); 7 } finally { 8 lock.unlock(); 9 }
take()
當元素被取出後, 並沒有對陣列後面的元素位移, 而是更新takeIndex來指向下一個元素.
takeIndex是一個環形的增長, 當移動到佇列尾部時, 會指向0, 再次迴圈.
1 private E dequeue() { 2 // assert lock.getHoldCount() == 1; 3 // assert items[takeIndex] != null; 4 final Object[] items = this.items; 5 @SuppressWarnings("unchecked") 6 E x = (E) items[takeIndex]; 7 items[takeIndex] = null; 8 if (++takeIndex == items.length) 9 takeIndex = 0; 10 count--; 11 if (itrs != null) 12 itrs.elementDequeued(); 13 notFull.signal(); 14 return x; 15 }
HashMap
最常用的雜湊表, 面試的童鞋必備知識了, 內部通過陣列 + 單鏈表的方式實現. jdk8中引入了紅黑樹對長度 > 8的連結串列進行優化, 我們另外篇幅再講.
put(K, V)
put(K, V) 相同hash值
resize 動態擴容
當map中元素超出設定的閾值後, 會進行resize (length * 2)操作, 擴容過程中對元素一通操作, 並放置到新的位置.
具體操作如下:
- 在jdk7中對所有元素直接rehash, 並放到新的位置.
- 在jdk8中判斷元素原hash值新增的bit位是0還是1, 0則索引不變, 1則索引變成"原索引 + oldTable.length".
1 //定義兩條鏈 2 //原來的hash值新增的bit為0的鏈,頭部和尾部 3 Node<K,V> loHead = null, loTail = null; 4 //原來的hash值新增的bit為1的鏈,頭部和尾部 5 Node<K,V> hiHead = null, hiTail = null; 6 Node<K,V> next; 7 //迴圈遍歷出鏈條鏈 8 do { 9 next = e.next; 10 if ((e.hash & oldCap) == 0) { 11 if (loTail == null) 12 loHead = e; 13 else 14 loTail.next = e; 15 loTail = e; 16 } 17 else { 18 if (hiTail == null) 19 hiHead = e; 20 else 21 hiTail.next = e; 22 hiTail = e; 23 } 24 } while ((e = next) != null); 25 //擴容前後位置不變的鏈 26 if (loTail != null) { 27 loTail.next = null; 28 newTab[j] = loHead; 29 } 30 //擴容後位置加上原陣列長度的鏈 31 if (hiTail != null) { 32 hiTail.next = null; 33 newTab[j + oldCap] = hiHead; 34 }
LinkedHashMap
繼承自HashMap, 底層額外維護了一個雙向連結串列來維持資料有序. 可以通過設定accessOrder來實現FIFO(插入有序)或者LRU(訪問有序)快取.
put(K, V)
get(K)
accessOrder為false的時候, 直接返回元素就行了, 不需要調整位置.
accessOrder為true的時候, 需要將最近訪問的元素, 放置到隊尾.
removeEldestEntry 刪除最老的元素