1. 程式人生 > >資料結構與演算法—常用資料結構及其Java實現

資料結構與演算法—常用資料結構及其Java實現



陣列

陣列是相同資料型別的元素按一定順序排列的集合,是一塊連續的記憶體空間。陣列的優點是:get和set操作時間上都是O(1)的;缺點是:add和remove操作時間上都是O(N)的。

Java中,Array就是陣列,此外,ArrayList使用了陣列Array作為其實現基礎,它和一般的Array相比,最大的好處是,我們在新增元素時不必考慮越界,元素超出陣列容量時,它會自動擴張保證容量。

Vector和ArrayList相比,主要差別就在於多了一個執行緒安全性,但是效率比較低下。如今java.util.concurrent包提供了許多執行緒安全的集合類(比如 LinkedBlockingQueue),所以不必再使用Vector了。

  

連結串列

連結串列是一種非連續、非順序的結構,資料元素的邏輯順序是通過連結串列中的指標連結次序實現的,連結串列由一系列結點組成。

連結串列的優點是:add和remove操作時間上都是O(1)的;缺點是:get和set操作時間上都是O(N)的,而且需要額外的空間儲存指向其他資料地址的項。

查詢操作對於未排序的陣列和連結串列時間上都是O(N)。

Java中,LinkedList 使用連結串列作為其基礎實現。

  

佇列

佇列是一種特殊的線性表,特殊之處在於它只允許在表的前端進行刪除操作,而在表的後端進行插入操作,亦即所謂的先進先出(FIFO)。

Java中,LinkedList實現了Deque,可以做為雙向佇列(自然也可以用作單向佇列)。另外PriorityQueue實現了帶優先順序的佇列,亦即佇列的每一個元素都有優先順序,且元素按照優先順序排序。

  

棧(stack)又名堆疊,它是一種運算受限的線性表。其限制是僅允許在表的一端進行插入和刪除運算。這一端被稱為棧頂,相對地,把另一端稱為棧底。它體現了後進先出(LIFO)

的特點。

Java中,Stack實現了這種特性,但是Stack也繼承了Vector,所以具有執行緒安全線和效率低下兩個特性,最新的JDK8中,推薦用Deque來實現棧,比如:

  

集合

集合是指具有某種特定性質的具體的或抽象的物件彙總成的集體,這些物件稱為該集合的元素,其主要特性是元素不可重複。

在Java中,HashSet 體現了這種資料結構,而HashSet是在MashMap的基礎上構建的。LinkedHashSet繼承了HashSet,使用HashCode確定在集合中的位置,使用連結串列的方式確定位置,所以有順序。

TreeSet實現了SortedSet 介面,是排好序的集合(在TreeMap 基礎之上構建),因此查詢操作比普通的Hashset要快(log(N));插入操作要慢(log(N)),因為要維護有序。

  

散列表

散列表也叫雜湊表,是根據關鍵鍵值(Keyvalue)進行訪問的資料結構,它通過把關鍵碼值對映到表中一個位置來訪問記錄,以加快查詢的速度,這個對映函式叫做雜湊函式。

Java中HashMap實現了散列表,而Hashtable比它多了一個執行緒安全性,但是由於使用了全域性鎖導致其效能較低,所以現在一般用ConcurrentHashMap來實現執行緒安全的HashMap(類似的,以上的資料結構在最新的java.util.concurrent的包中幾乎都有對應的高效能的執行緒安全的類)。

TreeMap實現SortMap介面,能夠把它儲存的記錄按照鍵排序。LinkedHashMap保留了元素插入的順序。WeakHashMap是一種改進的HashMap,它對key實行“弱引用”,如果一個key不再被外部所引用,那麼該key可以被GC回收,而不需要我們手動刪除。

  

樹(tree)是包含n(n>0)個節點的有窮集合,其中:

每個元素稱為節點(node);

有一個特定的節點被稱為根節點或樹根(root)。

除根節點之外的其餘資料元素被分為m(m≥0)個互不相交的結合T1,T2,……Tm-1,其中每一個集合Ti(1<=i<=m)本身也是一棵樹,被稱作原樹的子樹(subtree)。

樹這種資料結構在計算機世界中有廣泛的應用,比如作業系統中用到了紅黑樹,資料庫用到了B+樹,編譯器中的語法樹,記憶體管理用到了堆(本質上也是樹),資訊理論中的哈夫曼編碼等等等等。

在Java中TreeSet和TreeMap用到了樹來排序(二分查詢提高檢索速度),不過一般都需要程式設計師自己去定義一個樹的類,並實現相關性質,而沒有現成的API。下面就用Java來實現各種常見的樹。

二叉樹

二叉樹是一種基礎而且重要的資料結構,其每個結點至多隻有二棵子樹,二叉樹有左右子樹之分,第i層至多有2^(i-1)個結點(i從1開始);深度為k的二叉樹至多有2^(k)-1)個結點,對任何一棵二叉樹,如果其終端結點數為n0,度為2的結點數為n2,則n0=n2+1。

二叉樹的性質:

1) 在非空二叉樹中,第i層的結點總數不超過2^(i-1), i>=1;

2) 深度為h的二叉樹最多有2^h-1個結點(h>=1),最少有h個結點;

3) 對於任意一棵二叉樹,如果其葉結點數為N0,而度數為2的結點總數為N2,則N0=N2+1;

4) 具有n個結點的完全二叉樹的深度為log2(n+1);

5)有N個結點的完全二叉樹各結點如果用順序方式儲存,則結點之間有如下關係:

若I為結點編號則 如果I>1,則其父結點的編號為I/2;

如果2I<=N,則其左兒子(即左子樹的根結點)的編號為2I;若2I>N,則無左兒子;

如果2I+1<=N,則其右兒子的結點編號為2I+1;若2I+1>N,則無右兒子。

6)給定N個節點,能構成h(N)種不同的二叉樹,其中h(N)為卡特蘭數的第N項,h(n)=C(2*n, n)/(n+1)。

7)設有i個枝點,I為所有枝點的道路長度總和,J為葉的道路長度總和J=I+2i。

滿二叉樹、完全二叉樹

滿二叉樹:除最後一層無任何子節點外,每一層上的所有結點都有兩個子結點;

完全二叉樹:若設二叉樹的深度為h,除第 h 層外,其它各層 (1~(h-1)層) 的結點數都達到最大個數,第h層所有的結點都連續集中在最左邊,這就是完全二叉樹;

滿二叉樹是完全二叉樹的一個特例。

二叉查詢樹

二叉查詢樹,又稱為是二叉排序樹(Binary Sort Tree)或二叉搜尋樹。二叉排序樹或者是一棵空樹,或者是具有下列性質的二叉樹:

1) 若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;

2) 若右子樹不空,則右子樹上所有結點的值均大於或等於它的根結點的值;

3) 左、右子樹也分別為二叉排序樹;

4) 沒有鍵值相等的節點。

二叉查詢樹的性質:對二叉查詢樹進行中序遍歷,即可得到有序的數列。

二叉查詢樹的時間複雜度:它和二分查詢一樣,插入和查詢的時間複雜度均為O(logn),但是在最壞的情況下仍然會有O(n)的時間複雜度。

原因在於插入和刪除元素的時候,樹沒有保持平衡。我們追求的是在最壞的情況下仍然有較好的時間複雜度,這就是平衡二叉樹設計的初衷。

二叉查詢樹可以這樣表示:

  

  

查詢:

  

插入:

  

刪除:

  

  

平衡二叉樹

平衡二叉樹又被稱為AVL樹,具有以下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。它的出現就是解決二叉查詢樹不平衡導致查詢效率退化為線性的問題,因為在刪除和插入之時會維護樹的平衡,使得查詢時間保持在O(logn),比二叉查詢樹更穩定。

ALLTree 的 Node 由 BST 的 Node 加上 private int height; 節點高度屬性即可,這是為了便於判斷樹是否平衡。

維護樹的平衡關鍵就在於旋轉。對於一個平衡的節點,由於任意節點最多有兩個兒子,因此高度不平衡時,此節點的兩顆子樹的高度差2.容易看出,這種不平衡出現在下面四種情況:

  

1、6節點的左子樹3節點高度比右子樹7節點大2,左子樹3節點的左子樹1節點高度大於右子樹4節點,這種情況成為左左。

2、6節點的左子樹2節點高度比右子樹7節點大2,左子樹2節點的左子樹1節點高度小於右子樹4節點,這種情況成為左右。

3、2節點的左子樹1節點高度比右子樹5節點小2,右子樹5節點的左子樹3節點高度大於右子樹6節點,這種情況成為右左。

4、2節點的左子樹1節點高度比右子樹4節點小2,右子樹4節點的左子樹3節點高度小於右子樹6節點,這種情況成為右右。

從圖2中可以可以看出,1和4兩種情況是對稱的,這兩種情況的旋轉演算法是一致的,只需要經過一次旋轉就可以達到目標,我們稱之為單旋轉。2和3兩種情況也是對稱的,這兩種情況的旋轉演算法也是一致的,需要進行兩次旋轉,我們稱之為雙旋轉。

單旋轉是針對於左左和右右這兩種情況,這兩種情況是對稱的,只要解決了左左這種情況,右右就很好辦了。圖3是左左情況的解決方案,節點k2不滿足平衡特性,因為它的左子樹k1比右子樹Z深2層,而且k1子樹中,更深的一層的是k1的左子樹X子樹,所以屬於左左情況。

  

為使樹恢復平衡,我們把k1變成這棵樹的根節點,因為k2大於k1,把k2置於k1的右子樹上,而原本在k1右子樹的Y大於k1,小於k2,就把Y置於k2的左子樹上,這樣既滿足了二叉查詢樹的性質,又滿足了平衡二叉樹的性質。

這樣的操作只需要一部分指標改變,結果我們得到另外一顆二叉查詢樹,它是一棵AVL樹,因為X向上一移動了一層,Y還停留在原來的層面上,Z向下移動了一層。整棵樹的新高度和之前沒有在左子樹上插入的高度相同,插入操作使得X高度長高了。

因此,由於這顆子樹高度沒有變化,所以通往根節點的路徑就不需要繼續旋轉了。

程式碼:

private int height(Node t){

return t == null ? -1 : t.height;

}

  

雙旋轉是針對於左右和右左這兩種情況,單旋轉不能使它達到一個平衡狀態,要經過兩次旋轉。同樣的,這樣兩種情況也是對稱的,只要解決了左右這種情況,右左就很好辦了。

圖4是左右情況的解決方案,節點k3不滿足平衡特性,因為它的左子樹k1比右子樹Z深2層,而且k1子樹中,更深的一層的是k1的右子樹k2子樹,所以屬於左右情況。

  

為使樹恢復平衡,我們需要進行兩步,第一步,把k1作為根,進行一次右右旋轉,旋轉之後就變成了左左情況,所以第二步再進行一次左左旋轉,最後得到了一棵以k2為根的平衡二叉樹樹。

程式碼:

  

AVL查詢操作與BST相同,AVL的刪除與插入操作在BST基礎之上需要檢查是否平衡,如果不平衡就要使用旋轉操作來維持平衡。

堆是一顆完全二叉樹,在這棵樹中,所有父節點都滿足大於等於其子節點的堆叫大根堆,所有父節點都滿足小於等於其子節點的堆叫小根堆。堆雖然是一顆樹,但是通常存放在一個數組中,父節點和孩子節點的父子關係通過陣列下標來確定。如下圖的小根堆及儲存它的陣列:

  

值: 7,8,9,12,13,11

陣列索引: 0,1,2,3, 4, 5

通過一個節點在陣列中的索引怎麼計算出它的父節點及左右孩子節點的索引:

  

維護大根堆的性質:

  

  

構造堆:

  

堆的用途:堆排序,優先順序佇列。

陣列

陣列是相同資料型別的元素按一定順序排列的集合,是一塊連續的記憶體空間。陣列的優點是:get和set操作時間上都是O(1)的;缺點是:add和remove操作時間上都是O(N)的。

Java中,Array就是陣列,此外,ArrayList使用了陣列Array作為其實現基礎,它和一般的Array相比,最大的好處是,我們在新增元素時不必考慮越界,元素超出陣列容量時,它會自動擴張保證容量。

Vector和ArrayList相比,主要差別就在於多了一個執行緒安全性,但是效率比較低下。如今java.util.concurrent包提供了許多執行緒安全的集合類(比如 LinkedBlockingQueue),所以不必再使用Vector了。

  

連結串列

連結串列是一種非連續、非順序的結構,資料元素的邏輯順序是通過連結串列中的指標連結次序實現的,連結串列由一系列結點組成。

連結串列的優點是:add和remove操作時間上都是O(1)的;缺點是:get和set操作時間上都是O(N)的,而且需要額外的空間儲存指向其他資料地址的項。

查詢操作對於未排序的陣列和連結串列時間上都是O(N)。

Java中,LinkedList 使用連結串列作為其基礎實現。

  

佇列

佇列是一種特殊的線性表,特殊之處在於它只允許在表的前端進行刪除操作,而在表的後端進行插入操作,亦即所謂的先進先出(FIFO)。

Java中,LinkedList實現了Deque,可以做為雙向佇列(自然也可以用作單向佇列)。另外PriorityQueue實現了帶優先順序的佇列,亦即佇列的每一個元素都有優先順序,且元素按照優先順序排序。

  

棧(stack)又名堆疊,它是一種運算受限的線性表。其限制是僅允許在表的一端進行插入和刪除運算。這一端被稱為棧頂,相對地,把另一端稱為棧底。它體現了後進先出(LIFO)

的特點。

Java中,Stack實現了這種特性,但是Stack也繼承了Vector,所以具有執行緒安全線和效率低下兩個特性,最新的JDK8中,推薦用Deque來實現棧,比如:

  

集合

集合是指具有某種特定性質的具體的或抽象的物件彙總成的集體,這些物件稱為該集合的元素,其主要特性是元素不可重複。

在Java中,HashSet 體現了這種資料結構,而HashSet是在MashMap的基礎上構建的。LinkedHashSet繼承了HashSet,使用HashCode確定在集合中的位置,使用連結串列的方式確定位置,所以有順序。

TreeSet實現了SortedSet 介面,是排好序的集合(在TreeMap 基礎之上構建),因此查詢操作比普通的Hashset要快(log(N));插入操作要慢(log(N)),因為要維護有序。

  

散列表

散列表也叫雜湊表,是根據關鍵鍵值(Keyvalue)進行訪問的資料結構,它通過把關鍵碼值對映到表中一個位置來訪問記錄,以加快查詢的速度,這個對映函式叫做雜湊函式。

Java中HashMap實現了散列表,而Hashtable比它多了一個執行緒安全性,但是由於使用了全域性鎖導致其效能較低,所以現在一般用ConcurrentHashMap來實現執行緒安全的HashMap(類似的,以上的資料結構在最新的java.util.concurrent的包中幾乎都有對應的高效能的執行緒安全的類)。

TreeMap實現SortMap介面,能夠把它儲存的記錄按照鍵排序。LinkedHashMap保留了元素插入的順序。WeakHashMap是一種改進的HashMap,它對key實行“弱引用”,如果一個key不再被外部所引用,那麼該key可以被GC回收,而不需要我們手動刪除。

  

樹(tree)是包含n(n>0)個節點的有窮集合,其中:

每個元素稱為節點(node);

有一個特定的節點被稱為根節點或樹根(root)。

除根節點之外的其餘資料元素被分為m(m≥0)個互不相交的結合T1,T2,……Tm-1,其中每一個集合Ti(1<=i<=m)本身也是一棵樹,被稱作原樹的子樹(subtree)。

樹這種資料結構在計算機世界中有廣泛的應用,比如作業系統中用到了紅黑樹,資料庫用到了B+樹,編譯器中的語法樹,記憶體管理用到了堆(本質上也是樹),資訊理論中的哈夫曼編碼等等等等。

在Java中TreeSet和TreeMap用到了樹來排序(二分查詢提高檢索速度),不過一般都需要程式設計師自己去定義一個樹的類,並實現相關性質,而沒有現成的API。下面就用Java來實現各種常見的樹。

二叉樹

二叉樹是一種基礎而且重要的資料結構,其每個結點至多隻有二棵子樹,二叉樹有左右子樹之分,第i層至多有2^(i-1)個結點(i從1開始);深度為k的二叉樹至多有2^(k)-1)個結點,對任何一棵二叉樹,如果其終端結點數為n0,度為2的結點數為n2,則n0=n2+1。

二叉樹的性質:

1) 在非空二叉樹中,第i層的結點總數不超過2^(i-1), i>=1;

2) 深度為h的二叉樹最多有2^h-1個結點(h>=1),最少有h個結點;

3) 對於任意一棵二叉樹,如果其葉結點數為N0,而度數為2的結點總數為N2,則N0=N2+1;

4) 具有n個結點的完全二叉樹的深度為log2(n+1);

5)有N個結點的完全二叉樹各結點如果用順序方式儲存,則結點之間有如下關係:

若I為結點編號則 如果I>1,則其父結點的編號為I/2;

如果2I<=N,則其左兒子(即左子樹的根結點)的編號為2I;若2I>N,則無左兒子;

如果2I+1<=N,則其右兒子的結點編號為2I+1;若2I+1>N,則無右兒子。

6)給定N個節點,能構成h(N)種不同的二叉樹,其中h(N)為卡特蘭數的第N項,h(n)=C(2*n, n)/(n+1)。

7)設有i個枝點,I為所有枝點的道路長度總和,J為葉的道路長度總和J=I+2i。

滿二叉樹、完全二叉樹

滿二叉樹:除最後一層無任何子節點外,每一層上的所有結點都有兩個子結點;

完全二叉樹:若設二叉樹的深度為h,除第 h 層外,其它各層 (1~(h-1)層) 的結點數都達到最大個數,第h層所有的結點都連續集中在最左邊,這就是完全二叉樹;

滿二叉樹是完全二叉樹的一個特例。

二叉查詢樹

二叉查詢樹,又稱為是二叉排序樹(Binary Sort Tree)或二叉搜尋樹。二叉排序樹或者是一棵空樹,或者是具有下列性質的二叉樹:

1) 若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;

2) 若右子樹不空,則右子樹上所有結點的值均大於或等於它的根結點的值;

3) 左、右子樹也分別為二叉排序樹;

4) 沒有鍵值相等的節點。

二叉查詢樹的性質:對二叉查詢樹進行中序遍歷,即可得到有序的數列。

二叉查詢樹的時間複雜度:它和二分查詢一樣,插入和查詢的時間複雜度均為O(logn),但是在最壞的情況下仍然會有O(n)的時間複雜度。

原因在於插入和刪除元素的時候,樹沒有保持平衡。我們追求的是在最壞的情況下仍然有較好的時間複雜度,這就是平衡二叉樹設計的初衷。

二叉查詢樹可以這樣表示:

  

  

查詢:

  

插入:

  

刪除:

  

  

平衡二叉樹

平衡二叉樹又被稱為AVL樹,具有以下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。它的出現就是解決二叉查詢樹不平衡導致查詢效率退化為線性的問題,因為在刪除和插入之時會維護樹的平衡,使得查詢時間保持在O(logn),比二叉查詢樹更穩定。

ALLTree 的 Node 由 BST 的 Node 加上 private int height; 節點高度屬性即可,這是為了便於判斷樹是否平衡。

維護樹的平衡關鍵就在於旋轉。對於一個平衡的節點,由於任意節點最多有兩個兒子,因此高度不平衡時,此節點的兩顆子樹的高度差2.容易看出,這種不平衡出現在下面四種情況:

  

1、6節點的左子樹3節點高度比右子樹7節點大2,左子樹3節點的左子樹1節點高度大於右子樹4節點,這種情況成為左左。

2、6節點的左子樹2節點高度比右子樹7節點大2,左子樹2節點的左子樹1節點高度小於右子樹4節點,這種情況成為左右。

3、2節點的左子樹1節點高度比右子樹5節點小2,右子樹5節點的左子樹3節點高度大於右子樹6節點,這種情況成為右左。

4、2節點的左子樹1節點高度比右子樹4節點小2,右子樹4節點的左子樹3節點高度小於右子樹6節點,這種情況成為右右。

從圖2中可以可以看出,1和4兩種情況是對稱的,這兩種情況的旋轉演算法是一致的,只需要經過一次旋轉就可以達到目標,我們稱之為單旋轉。2和3兩種情況也是對稱的,這兩種情況的旋轉演算法也是一致的,需要進行兩次旋轉,我們稱之為雙旋轉。

單旋轉是針對於左左和右右這兩種情況,這兩種情況是對稱的,只要解決了左左這種情況,右右就很好辦了。圖3是左左情況的解決方案,節點k2不滿足平衡特性,因為它的左子樹k1比右子樹Z深2層,而且k1子樹中,更深的一層的是k1的左子樹X子樹,所以屬於左左情況。

  

為使樹恢復平衡,我們把k1變成這棵樹的根節點,因為k2大於k1,把k2置於k1的右子樹上,而原本在k1右子樹的Y大於k1,小於k2,就把Y置於k2的左子樹上,這樣既滿足了二叉查詢樹的性質,又滿足了平衡二叉樹的性質。

這樣的操作只需要一部分指標改變,結果我們得到另外一顆二叉查詢樹,它是一棵AVL樹,因為X向上一移動了一層,Y還停留在原來的層面上,Z向下移動了一層。整棵樹的新高度和之前沒有在左子樹上插入的高度相同,插入操作使得X高度長高了。

因此,由於這顆子樹高度沒有變化,所以通往根節點的路徑就不需要繼續旋轉了。

程式碼:

private int height(Node t){

return t == null ? -1 : t.height;

}

  

雙旋轉是針對於左右和右左這兩種情況,單旋轉不能使它達到一個平衡狀態,要經過兩次旋轉。同樣的,這樣兩種情況也是對稱的,只要解決了左右這種情況,右左就很好辦了。

圖4是左右情況的解決方案,節點k3不滿足平衡特性,因為它的左子樹k1比右子樹Z深2層,而且k1子樹中,更深的一層的是k1的右子樹k2子樹,所以屬於左右情況。

  

為使樹恢復平衡,我們需要進行兩步,第一步,把k1作為根,進行一次右右旋轉,旋轉之後就變成了左左情況,所以第二步再進行一次左左旋轉,最後得到了一棵以k2為根的平衡二叉樹樹。

程式碼:

  

AVL查詢操作與BST相同,AVL的刪除與插入操作在BST基礎之上需要檢查是否平衡,如果不平衡就要使用旋轉操作來維持平衡。

堆是一顆完全二叉樹,在這棵樹中,所有父節點都滿足大於等於其子節點的堆叫大根堆,所有父節點都滿足小於等於其子節點的堆叫小根堆。堆雖然是一顆樹,但是通常存放在一個數組中,父節點和孩子節點的父子關係通過陣列下標來確定。如下圖的小根堆及儲存它的陣列:

  

值: 7,8,9,12,13,11

陣列索引: 0,1,2,3, 4, 5

通過一個節點在陣列中的索引怎麼計算出它的父節點及左右孩子節點的索引:

  

維護大根堆的性質:

  

  

構造堆:

  

堆的用途:堆排序,優先順序佇列。