圖解!24張圖完全弄懂九大常見數據結構!

數據結構想必你們都不會陌生,對於一個成熟的程序員而言,熟悉和掌握數據結構和算法也是基本功之一。數據結構自己其實不過是數據按照特色關係進行存儲或者組織的集合,特殊的結構在不一樣的應用場景中每每會帶來不同的處理效率。程序員

經常使用的數據結構可根據數據訪問的特色分爲線性結構和非線性結構。線性結構包括常見的鏈表、棧、隊列等,非線性結構包括樹、圖等。數據結構種類繁多,本文將經過圖解的方式對經常使用的數據結構進行理論上的介紹和講解,以方便你們掌握經常使用數據結構的基本知識。web

本文提綱面試

 1  數組

數組能夠說是最基本最多見的數據結構。數組通常用來存儲相同類型的數據,可經過數組名和下標進行數據的訪問和更新。數組中元素的存儲是按照前後順序進行的,同時在內存中也是按照這個順序進行連續存放。數組相鄰元素之間的內存地址的間隔通常就是數組數據類型的大小。redis

 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的插入和刪除效率並不高。

爲了解決這樣的問題,能不能找一種結構可以兼顧搜索和插入刪除的效率呢?這時候紅黑樹便申請出戰了。

紅黑樹具備五個特性:

  1. 每一個結點要麼是紅的要麼是黑的。
  2. 根結點是黑的。
  3. 每一個葉結點(葉結點即指樹尾端NIL指針或NULL結點)都是黑的。
  4. 若是一個結點是紅的,那麼它的兩個兒子都是黑的。
  5. 對於任意結點而言,其到葉結點樹尾端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,頂點Afirstout針須要指向邊04tailvex同時,從B出發可以到達A,因此在逆鄰接表中頂點A要經過AB即邊10)指向B,頂點Afirstin針須要指向邊10的弧頭,headlink針。依次類推。

十字鏈表採用了一種看起來比較繁亂的方式對邊的方向性進行了表示,可以在儘量下降存儲空間的狀況下增長指針保留頂點之間的方向性。具體的操做可能一時間很差弄懂,建議多看幾回上圖,弄清指針指向的意義,明白正向和逆向鄰接表的表示。

 10  總結

數據結構博大精深,沒有高等數學的諱莫如深,也沒有量子力學的玄乎其神,可是其在計算機科學的各個領域都具備強大的力量。本文試圖採用圖解的方式對九種數據結構進行理論上的介紹,可是其實這都是不夠的。

即使是簡單的數組、棧、隊列等結構,在實際使用以及底層實現上都會有許多優化設計以及使用技巧,這意味着還須要真正把它們靈活的用起來,纔可以算是真正意義上的熟悉和精通。可是本文能夠做爲常見數據結構的一個總結,當你對某些結構有些淡忘的時候,不妨從新回來看看。

本文分享自微信公衆號 - 業餘碼農(Amateur_coder)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索