本文采用Java語(yǔ)言來(lái)進(jìn)行描述,幫大家好好梳理一下數(shù)據(jù)結(jié)構(gòu)與算法,在工作和面試中用的上。亦即總結(jié)常見(jiàn)的的數(shù)據(jù)結(jié)構(gòu),以及在Java中相應(yīng)的實(shí)現(xiàn)方法,務(wù)求理論與實(shí)踐一步總結(jié)到位。
常用數(shù)據(jù)結(jié)構(gòu)
數(shù)組
數(shù)組是相同數(shù)據(jù)類型的元素按一定順序排列的集合,是一塊連續(xù)的內(nèi)存空間。數(shù)組的優(yōu)點(diǎn)是:get和set操作時(shí)間上都是O(1)的;缺點(diǎn)是:add和remove操作時(shí)間上都是O(N)的。
Java中,Array就是數(shù)組,此外,?ArrayList使用了數(shù)組Array?作為其實(shí)現(xiàn)基礎(chǔ),它和一般的Array相比,最大的好處是,我們?cè)谔砑釉貢r(shí)不必考慮越界,元素超出數(shù)組容量時(shí),它會(huì)自動(dòng)擴(kuò)張保證容量。
Vector和ArrayList相比,主要差別就在于多了一個(gè)線程安全性,但是效率比較低下。如今java.util.concurrent包提供了許多線程安全的集合類(比如 LinkedBlockingQueue),所以不必再使用Vector了。
進(jìn)群:697699179可以獲取Java各類入門學(xué)習(xí)資料!
這是我的微信公眾號(hào)【編程study】各位大佬有空可以關(guān)注下,每天更新Java學(xué)習(xí)方法,感謝!
學(xué)習(xí)中遇到問(wèn)題有不明白的地方,推薦加小編Java學(xué)習(xí)群:697699179內(nèi)有視頻教程 ,直播課程 ,等學(xué)習(xí)資料,期待你的加入
鏈表
鏈表是一種非連續(xù)、非順序的結(jié)構(gòu),數(shù)據(jù)元素的邏輯順序是通過(guò)鏈表中的指針鏈接次序?qū)崿F(xiàn)的,鏈表由一系列結(jié)點(diǎn)組成。鏈表的優(yōu)點(diǎn)是:add和remove操作時(shí)間上都是O(1)的;缺點(diǎn)是:get和set操作時(shí)間上都是O(N)的,而且需要額外的空間存儲(chǔ)指向其他數(shù)據(jù)地址的項(xiàng)。
查找操作對(duì)于未排序的數(shù)組和鏈表時(shí)間上都是O(N)。
Java中,LinkedList 使用鏈表作為其基礎(chǔ)實(shí)現(xiàn)。
//以上方法也適用于ArrayList
隊(duì)列
隊(duì)列是一種特殊的線性表,特殊之處在于它只允許在表的前端進(jìn)行刪除操作,而在表的后端進(jìn)行插入操作,亦即所謂的先進(jìn)先出(FIFO)。
Java中,LinkedList實(shí)現(xiàn)了Deque,可以做為雙向隊(duì)列(自然也可以用作單向隊(duì)列)。另外PriorityQueue實(shí)現(xiàn)了帶優(yōu)先級(jí)的隊(duì)列,亦即隊(duì)列的每一個(gè)元素都有優(yōu)先級(jí),且元素按照優(yōu)先級(jí)排序。
棧
棧(stack)又名堆棧,它是一種運(yùn)算受限的線性表。其限制是僅允許在表的一端進(jìn)行插入和刪除運(yùn)算。這一端被稱為棧頂,相對(duì)地,把另一端稱為棧底。它體現(xiàn)了后進(jìn)先出(LIFO)的特點(diǎn)。
Java中,Stack實(shí)現(xiàn)了這種特性,但是Stack也繼承了Vector,所以具有線程安全線和效率低下兩個(gè)特性,最新的JDK8中,推薦用Deque來(lái)實(shí)現(xiàn)棧,比如:
集合
集合是指具有某種特定性質(zhì)的具體的或抽象的對(duì)象匯總成的集體,這些對(duì)象稱為該集合的元素,其主要特性是元素不可重復(fù)。
在Java中,HashSet體現(xiàn)了這種數(shù)據(jù)結(jié)構(gòu),而HashSet是在MashMap的基礎(chǔ)上構(gòu)建的。LinkedHashSet繼承了HashSet,使用HashCode確定在集合中的位置,使用鏈表的方式確定位置,所以有順序。TreeSet實(shí)現(xiàn)了SortedSet 接口,是排好序的集合(在TreeMap 基礎(chǔ)之上構(gòu)建),因此查找操作比普通的Hashset要快(log(N));插入操作要慢(log(N)),因?yàn)橐S護(hù)有序。
散列表
散列表也叫哈希表,是根據(jù)關(guān)鍵鍵值(Keyvalue)進(jìn)行訪問(wèn)的數(shù)據(jù)結(jié)構(gòu),它通過(guò)把關(guān)鍵碼值映射到表中一個(gè)位置來(lái)訪問(wèn)記錄,以加快查找的速度,這個(gè)映射函數(shù)叫做散列函數(shù)。
Java中HashMap實(shí)現(xiàn)了散列表,而Hashtable比它多了一個(gè)線程安全性,但是由于使用了全局鎖導(dǎo)致其性能較低,所以現(xiàn)在一般用?ConcurrentHashMap來(lái)實(shí)現(xiàn)?線程安全的HashMap(類似的,以上的數(shù)據(jù)結(jié)構(gòu)在最新的java.util.concurrent的包中幾乎都有對(duì)應(yīng)的高性能的線程安全的類)。TreeMap實(shí)現(xiàn)SortMap接口,能夠把它保存的記錄按照鍵排序。LinkedHashMap保留了元素插入的順序。WeakHashMap是一種改進(jìn)的HashMap,它對(duì)key實(shí)行“弱引用”,如果一個(gè)key不再被外部所引用,那么該key可以被GC回收,而不需要我們手動(dòng)刪除。
樹
樹(tree)是包含n(n>0)個(gè)節(jié)點(diǎn)的有窮集合,其中:
每個(gè)元素稱為節(jié)點(diǎn)(node)
有一個(gè)特定的節(jié)點(diǎn)被稱為根節(jié)點(diǎn)或樹根(root)
除根節(jié)點(diǎn)之外的其余數(shù)據(jù)元素被分為m(m≥0)個(gè)互不相交的結(jié)合T1,T2,……Tm-1,其中每一個(gè)集合Ti(1<=i<=m)本身也是一棵樹,被稱作原樹的子樹(subtree)
樹這種數(shù)據(jù)結(jié)構(gòu)在計(jì)算機(jī)世界中有廣泛的應(yīng)用,比如操作系統(tǒng)中用到了紅黑樹,數(shù)據(jù)庫(kù)用到了B+樹,編譯器中的語(yǔ)法樹,內(nèi)存管理用到了堆(本質(zhì)上也是樹),信息論中的哈夫曼編碼等等等等,在Java中TreeSet和TreeMap用到了樹來(lái)排序(二分查找提高檢索速度),不過(guò)一般都需要程序員自己去定義一個(gè)樹的類,并實(shí)現(xiàn)相關(guān)性質(zhì),而沒(méi)有現(xiàn)成的API。
下面用Java來(lái)實(shí)現(xiàn)各種常見(jiàn)的樹。
二叉樹
二叉樹是一種基礎(chǔ)而且重要的數(shù)據(jù)結(jié)構(gòu),其每個(gè)結(jié)點(diǎn)至多只有二棵子樹,二叉樹有左右子樹之分,第i層至多有2^(i-1)個(gè)結(jié)點(diǎn)(i從1開(kāi)始);深度為k的二叉樹至多有2^(k)-1)個(gè)結(jié)點(diǎn),對(duì)任何一棵二叉樹,如果其終端結(jié)點(diǎn)數(shù)為n0,度為2的結(jié)點(diǎn)數(shù)為n2,則n0=n2+1。
二叉樹的性質(zhì):
在非空二叉樹中,第i層的結(jié)點(diǎn)總數(shù)不超過(guò)2^(i-1), i>=1;
深度為h的二叉樹最多有2^h-1個(gè)結(jié)點(diǎn)(h>=1),最少有h個(gè)結(jié)點(diǎn);
對(duì)于任意一棵二叉樹,如果其葉結(jié)點(diǎn)數(shù)為N0,而度數(shù)為2的結(jié)點(diǎn)總數(shù)為N2,則N0=N2+1;
具有n個(gè)結(jié)點(diǎn)的完全二叉樹的深度為log2(n+1);
有N個(gè)結(jié)點(diǎn)的完全二叉樹各結(jié)點(diǎn)如果用順序方式存儲(chǔ),則結(jié)點(diǎn)之間有如下關(guān)系: 若I為結(jié)點(diǎn)編號(hào)則 如果I>1,則其父結(jié)點(diǎn)的編號(hào)為I/2; 如果2I<=N,則其左兒子(即左子樹的根結(jié)點(diǎn))的編號(hào)為2I;若2I>N,則無(wú)左兒子; 如果2I+1<=N,則其右兒子的結(jié)點(diǎn)編號(hào)為2I+1;若2I+1>N,則無(wú)右兒子。
給定N個(gè)節(jié)點(diǎn),能構(gòu)成h(N)種不同的二叉樹,其中h(N)為卡特蘭數(shù)的第N項(xiàng),h(n)=C(2*n, n)/(n+1)。
設(shè)有i個(gè)枝點(diǎn),I為所有枝點(diǎn)的道路長(zhǎng)度總和,J為葉的道路長(zhǎng)度總和J=I+2i。
滿二叉樹、完全二叉樹
滿二叉樹:除最后一層無(wú)任何子節(jié)點(diǎn)外,每一層上的所有結(jié)點(diǎn)都有兩個(gè)子結(jié)點(diǎn);
完全二叉樹:若設(shè)二叉樹的深度為h,除第 h 層外,其它各層 (1~(h-1)層) 的結(jié)點(diǎn)數(shù)都達(dá)到最大個(gè)數(shù),第h層所有的結(jié)點(diǎn)都連續(xù)集中在最左邊,這就是完全二叉樹;
滿二叉樹是完全二叉樹的一個(gè)特例。
二叉查找樹
二叉查找樹,又稱為是二叉排序樹(Binary Sort Tree)或二叉搜索樹。二叉排序樹或者是一棵空樹,或者是具有下列性質(zhì)的二叉樹:
若左子樹不空,則左子樹上所有結(jié)點(diǎn)的值均小于它的根結(jié)點(diǎn)的值;
若右子樹不空,則右子樹上所有結(jié)點(diǎn)的值均大于或等于它的根結(jié)點(diǎn)的值;
左、右子樹也分別為二叉排序樹;
沒(méi)有鍵值相等的節(jié)點(diǎn)。
二叉查找樹的性質(zhì):對(duì)二叉查找樹進(jìn)行中序遍歷,即可得到有序的數(shù)列。
二叉查找樹的時(shí)間復(fù)雜度:它和二分查找一樣,插入和查找的時(shí)間復(fù)雜度均為O(logn),但是在最壞的情況下仍然會(huì)有O(n)的時(shí)間復(fù)雜度。原因在于插入和刪除元素的時(shí)候,樹沒(méi)有保持平衡。我們追求的是在最壞的情況下仍然有較好的時(shí)間復(fù)雜度,這就是平衡二叉樹設(shè)計(jì)的初衷。
二叉查找樹可以這樣表示:
查找:
插入:
刪除:
平衡二叉樹
平衡二叉樹又被稱為AVL樹,具有以下性質(zhì):它是一棵空樹或它的左右兩個(gè)子樹的高度差的絕對(duì)值不超過(guò)1,并且左右兩個(gè)子樹都是一棵平衡二叉樹。它的出現(xiàn)就是解決二叉查找樹不平衡導(dǎo)致查找效率退化為線性的問(wèn)題,因?yàn)樵趧h除和插入之時(shí)會(huì)維護(hù)樹的平衡,使得查找時(shí)間保持在O(logn),比二叉查找樹更穩(wěn)定。
ALLTree 的 Node 由 BST 的 Node 加上 private int height; 節(jié)點(diǎn)高度屬性即可,這是為了便于判斷樹是否平衡。
維護(hù)樹的平衡關(guān)鍵就在于旋轉(zhuǎn)。對(duì)于一個(gè)平衡的節(jié)點(diǎn),由于任意節(jié)點(diǎn)最多有兩個(gè)兒子,因此高度不平衡時(shí),此節(jié)點(diǎn)的兩顆子樹的高度差2,容易看出,這種不平衡出現(xiàn)在下面四種情況:
6節(jié)點(diǎn)的左子樹3節(jié)點(diǎn)高度比右子樹7節(jié)點(diǎn)大2,左子樹3節(jié)點(diǎn)的左子樹1節(jié)點(diǎn)高度大于右子樹4節(jié)點(diǎn),這種情況成為左左。
6節(jié)點(diǎn)的左子樹2節(jié)點(diǎn)高度比右子樹7節(jié)點(diǎn)大2,左子樹2節(jié)點(diǎn)的左子樹1節(jié)點(diǎn)高度小于右子樹4節(jié)點(diǎn),這種情況成為左右。
2節(jié)點(diǎn)的左子樹1節(jié)點(diǎn)高度比右子樹5節(jié)點(diǎn)小2,右子樹5節(jié)點(diǎn)的左子樹3節(jié)點(diǎn)高度大于右子樹6節(jié)點(diǎn),這種情況成為右左。
2節(jié)點(diǎn)的左子樹1節(jié)點(diǎn)高度比右子樹4節(jié)點(diǎn)小2,右子樹4節(jié)點(diǎn)的左子樹3節(jié)點(diǎn)高度小于右子樹6節(jié)點(diǎn),這種情況成為右右。
從圖2中可以可以看出,1和4兩種情況是對(duì)稱的,這兩種情況的旋轉(zhuǎn)算法是一致的,只需要經(jīng)過(guò)一次旋轉(zhuǎn)就可以達(dá)到目標(biāo),我們稱之為單旋轉(zhuǎn)。2和3兩種情況也是對(duì)稱的,這兩種情況的旋轉(zhuǎn)算法也是一致的,需要進(jìn)行兩次旋轉(zhuǎn),我們稱之為雙旋轉(zhuǎn)。
單旋轉(zhuǎn)是針對(duì)于左左和右右這兩種情況,這兩種情況是對(duì)稱的,只要解決了左左這種情況,右右就很好辦了。圖3是左左情況的解決方案,節(jié)點(diǎn)k2不滿足平衡特性,因?yàn)樗淖笞訕鋕1比右子樹Z深2層,而且k1子樹中,更深的一層的是k1的左子樹X子樹,所以屬于左左情況。
為使樹恢復(fù)平衡,我們把k1變成這棵樹的根節(jié)點(diǎn),因?yàn)閗2大于k1,把k2置于k1的右子樹上,而原本在k1右子樹的Y大于k1,小于k2,就把Y置于k2的左子樹上,這樣既滿足了二叉查找樹的性質(zhì),又滿足了平衡二叉樹的性質(zhì)。
這樣的操作只需要一部分指針改變,結(jié)果我們得到另外一顆二叉查找樹,它是一棵AVL樹,因?yàn)閄向上一移動(dòng)了一層,Y還停留在原來(lái)的層面上,Z向下移動(dòng)了一層。整棵樹的新高度和之前沒(méi)有在左子樹上插入的高度相同,插入操作使得X高度長(zhǎng)高了。因此,由于這顆子樹高度沒(méi)有變化,所以通往根節(jié)點(diǎn)的路徑就不需要繼續(xù)旋轉(zhuǎn)了。
代碼:
雙旋轉(zhuǎn)是針對(duì)于左右和右左這兩種情況,單旋轉(zhuǎn)不能使它達(dá)到一個(gè)平衡狀態(tài),要經(jīng)過(guò)兩次旋轉(zhuǎn)。同樣的,這樣兩種情況也是對(duì)稱的,只要解決了左右這種情況,右左就很好辦了。圖4是左右情況的解決方案,節(jié)點(diǎn)k3不滿足平衡特性,因?yàn)樗淖笞訕鋕1比右子樹Z深2層,而且k1子樹中,更深的一層的是k1的右子樹k2子樹,所以屬于左右情況。
為使樹恢復(fù)平衡,我們需要進(jìn)行兩步,第一步,把k1作為根,進(jìn)行一次右右旋轉(zhuǎn),旋轉(zhuǎn)之后就變成了左左情況,所以第二步再進(jìn)行一次左左旋轉(zhuǎn),最后得到了一棵以k2為根的平衡二叉樹樹。
代碼:
AVL查找操作與BST相同,AVL的刪除與插入操作在BST基礎(chǔ)之上需要檢查是否平衡,如果不平衡就要使用旋轉(zhuǎn)操作來(lái)維持平衡:
堆
堆是一顆完全二叉樹,在這棵樹中,所有父節(jié)點(diǎn)都滿足大于等于其子節(jié)點(diǎn)的堆叫大根堆,所有父節(jié)點(diǎn)都滿足小于等于其子節(jié)點(diǎn)的堆叫小根堆。堆雖然是一顆樹,但是通常存放在一個(gè)數(shù)組中,父節(jié)點(diǎn)和孩子節(jié)點(diǎn)的父子關(guān)系通過(guò)數(shù)組下標(biāo)來(lái)確定。如下面的小根堆及存儲(chǔ)它的數(shù)組:
值:7,8,9,12,13,11
數(shù)組索引:0,1,2,3, 4, 5
通過(guò)一個(gè)節(jié)點(diǎn)在數(shù)組中的索引怎么計(jì)算出它的父節(jié)點(diǎn)及左右孩子節(jié)點(diǎn)的索引:
維護(hù)大根堆的性質(zhì):
構(gòu)造堆:
堆的用途:堆排序,優(yōu)先級(jí)隊(duì)列。此外由于調(diào)整代價(jià)較小,也適合實(shí)時(shí)類型的排序與變更。
最后
寫著寫著就發(fā)現(xiàn)要想總結(jié)到位是一項(xiàng)非常龐大的工程,路漫漫其修遠(yuǎn)兮,吾將上下而求索。