? 數據結構
數據結構是計算機存儲和組織數據的的方式
1. 數組
在Java中,數組是用來存放同一種數據類型的集合,注意只能存放同一種數據類型。
數組的局限性分析:
①、插入慢,對于無序數組,上面我們實現的數組就是無序的,即元素沒有按照從大到小或者某個特定的順序排列,只是按照插入的順序排列。無序數組增加一個元素很簡單,只需要在數組末尾添加元素即可,但是有序數組卻不一定了,它需要在指定的位置插入。
②、查找快,當然如果根據下標來查找是很快的。但是通常我們都是根據元素值來查找,給定一個元素值,對于無序數組,我們需要從數組第一個元素開始遍歷,知道找到那個元素。有序數組通過特定的算法查找的速度會比無需數組快,后面我們會講各種排序算法。
③、刪除慢,根據元素值刪除,我們要先找到該元素所處的位置,然后將元素后面的值整體向前面移動一個位置。也需要比較多的時間。
④、數組一旦創建后,大小就固定了,不能動態擴展數組的元素個數。如果初始化你給一個很大的數組大小,那會白白浪費內存空間,如果給小了,后面數據個數增加了又添加不進去了。
很顯然,數組雖然查找快,但是插入和刪除都比較慢,所以我們不會用數組來存儲所有的數據,那有沒有什么數據結構插入、查找、刪除都很快,而且還能動態擴展存儲個數大小
一維數組
如何定義數組
數據類型[] 數組名=new 數據類型[數組長度];
? 數組類型 數組名[]=new 數組類型[數組長度];
二維數組
定義
數組類型 [][] 數組名;
數組類型 數組 [][];
數組是可以再內存中連續存儲多個元素的結構。數組中的所有元素必須屬于相同的數據類型。
數組中的元素通過數組下標進行訪問,數組下標從0開始。
二維數組實際上是一個一維數組,它的每個元素又是一個一維數組。
使用Array類提供的方法可以方便地對數組中的元素進行排序、查詢等操作。
JDK1.5之后提供了增強for循環,可以用來實現對數組和集合中數據的訪問。
1. 棧
又稱為堆棧或堆疊,棧作為一種數據結構,是一種只能在一端進行插入和刪除操作的特殊線性表。它按照先進后出的原則存儲數據,先進入的數據被壓入棧底,最后的數據在棧頂,需要讀數據的時候從棧頂開始彈出數據(最后一個數據被第一個讀出來)。棧具有記憶作用,對棧的插入與刪除操作中,不需要改變棧底指針。
棧是允許在同一端進行插入和刪除操作的特殊線性表。允許進行插入和刪除操作的一端稱為棧頂(top),另一端為棧底(bottom);棧底固定,而棧頂浮動;棧中元素個數為零時稱為空棧。插入一般稱為進棧(PUSH),刪除則稱為退棧(POP)。
由于堆疊數據結構只允許在一端進行操作,因而按照后進先出(LIFO, Last In First Out)的原理運作。棧也稱為后進先出表。
這里以羽毛球筒為例,羽毛球筒就是一個棧,剛開始羽毛球筒是空的,也就是空棧,然后我們一個一個放入羽毛球,也就是一個一個push進棧,當我們需要使用羽毛球的時候,從筒里面拿,也就是pop出棧,但是第一個拿到的羽毛球是我們最后放進去的。
產生的問題:
①、上面棧的實現初始化容量之后,后面是不能進行擴容的(雖然棧不是用來存儲大量數據的),如果說后期數據量超過初始容量之后怎么辦?(自動擴容)
②、我們是用數組實現棧,在定義數組類型的時候,也就規定了存儲在棧中的數據類型,那么同一個棧能不能存儲不同類型的數據呢?(聲明為Object)
③、棧需要初始化容量,而且數組實現的棧元素都是連續存儲的,那么能不能不初始化容量呢?(改為由鏈表實現)
1. 隊列
隊列(queue)是一種特殊的線性表,特殊之處在于它只允許在表的前端(front)進行刪除操作,而在表的后端(rear)進行插入操作,和棧一樣,隊列是一種操作受限制的線性表。進行插入操作的端稱為隊尾,進行刪除操作的端稱為隊頭。隊列中沒有元素時,稱為空隊列。
隊列的數據元素又稱為隊列元素。在隊列中插入一個隊列元素稱為入隊,從隊列中刪除一個隊列元素稱為出隊。因為隊列只允許在一端插入,在另一端刪除,所以只有最早進入隊列的元素才能最先從隊列中刪除,故隊列又稱為先進先出(FIFO—first in first out)線性表。
隊列分為:
①、單向隊列(Queue):只能在一端插入數據,另一端刪除數據。
②、雙向隊列(Deque):每一端都可以進行插入數據和刪除數據操作。
單向隊列實現
①、與棧不同的是,隊列中的數據不總是從數組的0下標開始的,移除一些隊頭front的數據后,隊頭指針會指向一個較高的下標位置,如下圖:
②、我們再設計時,隊列中新增一個數據時,隊尾的指針rear 會向上移動,也就是向下標大的方向。移除數據項時,隊頭指針 front 也會向下移動。那么這樣設計好像和現實情況相反,比如排隊買電影票,隊頭的買完票就離開了,然后隊伍整體向前移動。在計算機中也可以在隊列中刪除一個數之后,隊列整體向前移動,但是這樣做效率很差。我們選擇的做法是移動隊頭和隊尾的指針。
③、如果向第②步這樣移動指針,相信隊尾指針很快就移動到數據的最末端了,這時候可能移除過數據,那么隊頭會有空著的位置,然后新來了一個數據項,由于隊尾不能再向上移動了,那該怎么辦呢?如下圖:
為了避免隊列不滿卻不能插入新的數據,我們可以讓隊尾指針繞回到數組開始的位置,這也稱為“循環隊列”。
雙向隊列
雙端隊列就是一個兩端都是結尾或者開頭的隊列,?隊列的每一端都可以進行插入數據項和移除數據項
優先級隊列(priority queue)
是比棧和隊列更專用的數據結構,在優先級隊列中,數據項按照關鍵字進行排序,關鍵字最小(或者最大)的數據項往往在隊列的最前面,而數據項在插入的時候都會插入到合適的位置以確保隊列的有序。
優先級隊列 是0個或多個元素的集合,每個元素都有一個優先權,對優先級隊列執行的操作有:
(1)查找
(2)插入一個新元素
(3)刪除
一般情況下,查找操作用來搜索優先權最大的元素,刪除操作用來刪除該元素 。對于優先權相同的元素,可按先進先出次序處理或按任意優先權進行。
這里我們用數組實現優先級隊列,這種方法插入比較慢,但是它比較簡單,適用于數據量比較小并且不是特別注重插入速度的情況。
后面我們會講解堆,用堆的數據結構來實現優先級隊列,可以相當快的插入數據。
數組實現優先級隊列,聲明為int類型的數組,關鍵字是數組里面的元素,在插入的時候按照從大到小的順序排列,也就是越小的元素優先級越高。
通過前面講的棧以及本篇講的隊列這兩種數據結構,我們稍微總結一下:
①、棧、隊列(單向隊列)、優先級隊列通常是用來簡化某些程序操作的數據結構,而不是主要作為存儲數據的。
②、在這些數據結構中,只有一個數據項可以被訪問。
③、棧允許在棧頂壓入(插入)數據,在棧頂彈出(移除)數據,但是只能訪問最后一個插入的數據項,也就是棧頂元素。
④、隊列(單向隊列)只能在隊尾插入數據,對頭刪除數據,并且只能訪問對頭的數據。而且隊列還可以實現循環隊列,它基于數組,數組下標可以從數組末端繞回到數組的開始位置。
⑤、優先級隊列是有序的插入數據,并且只能訪問當前元素中優先級別最大(或最小)的元素。
⑥、這些數據結構都能由數組實現,但是可以用別的機制(后面講的鏈表、堆等數據結構)實現。
1. 鏈表
是一種常見的基礎數據結構,是一種線性表,但是并不會按線性的順序存儲數據,而是在每一個(或兩個)節點里存到下一個(或上一個)節點的指針(Pointer)。
使用鏈表結構可以克服數組鏈表需要預先知道數據大小的缺點,鏈表結構可以充分利用計算機內存空間,實現靈活的內存動態管理。但是鏈表失去了數組隨機讀取的優點,同時鏈表由于增加了結點的指針域,空間開銷比較大。
單鏈表是鏈表中結構最簡單的。一個單鏈表的節點(Node)分為兩個部分,第一個部分(data)保存或者顯示關于節點的信息,另一個部分存儲下一個節點的地址。最后一個節點存儲地址的部分指向空值。
單向鏈表
只可向一個方向遍歷,一般查找一個節點的時候需要從第一個節點開始每次訪問下一個節點,一直訪問到需要的位置。而插入一個節點,對于單向鏈表,我們只提供在鏈表頭插入,只需要將當前插入的節點設置為頭節點,next指向原頭節點即可。刪除一個節點,我們將該節點的上一個節點的next指向該節點的下一個節點。
雙端鏈表
1. 二叉樹
樹的每個節點最多只能有兩個子節點
查找某個節點,我們必須從根節點開始遍歷。
①、查找值比當前節點值大,則搜索右子樹;
②、查找值等于當前節點值,停止搜索(終止條件);
③、查找值小于當前節點值,則搜索左子樹;
要插入節點,必須先找到插入的位置。與查找操作相似,由于二叉搜索樹的特殊性,待插入的節點也需要從根節點開始進行比較,小于根節點則與根節點左子樹比較,反之則與右子樹比較,直到左子樹為空或右子樹為空,則插入到相應為空的位置,在比較的過程中要注意保存父節點的信息 及 待插入的位置是父節點的左子樹還是右子樹,才能插入到正確的位置。
刪除節點是二叉搜索樹中最復雜的操作,刪除的節點有三種情況,前兩種比較簡單,但是第三種卻很復雜。
1、該節點是葉節點(沒有子節點)
2、該節點有一個子節點
3、該節點有兩個子節點
下面我們分別對這三種情況進行講解。
①、刪除沒有子節點的節點
要刪除葉節點,只需要改變該節點的父節點引用該節點的值,即將其引用改為 null 即可。要刪除的節點依然存在,但是它已經不是樹的一部分了,由于Java語言的垃圾回收機制,我們不需要非得把節點本身刪掉,一旦Java意識到程序不在與該節點有關聯,就會自動把它清理出存儲器。
刪除節點,我們要先找到該節點,并記錄該節點的父節點。在檢查該節點是否有子節點。如果沒有子節點,接著檢查其是否是根節點,如果是根節點,只需要將其設置為null即可。如果不是根節點,是葉節點,那么斷開父節點和其的關系即可。
②、刪除有一個子節點的節點
刪除有一個子節點的節點,我們只需要將其父節點原本指向該節點的引用,改為指向該節點的子節點即可。
③、刪除有兩個子節點的節點
當刪除的節點存在兩個子節點,那么刪除之后,兩個子節點的位置我們就沒辦法處理了。既然處理不了,我們就想到一種辦法,用另一個節點來代替被刪除的節點,那么用哪一個節點來代替呢?
我們知道二叉搜索樹中的節點是按照關鍵字來進行排列的,某個節點的關鍵字次高節點是它的中序遍歷后繼節點。用后繼節點來代替刪除的節點,顯然該二叉搜索樹還是有序的。(這里用后繼節點代替,如果該后繼節點自己也有子節點,我們后面討論。)
那么如何找到刪除節點的中序后繼節點呢?其實我們稍微分析,這實際上就是要找比刪除節點關鍵值大的節點集合中最小的一個節點,只有這樣代替刪除節點后才能滿足二叉搜索樹的特性。
后繼節點也就是:比刪除節點大的最小節點。
6.紅黑樹
有如下兩個特征:
①、節點都有顏色;
②、在插入和刪除的過程中,要遵循保持這些顏色的不同排列規則。
第一個很好理解,在紅-黑樹中,每個節點的顏色或者是黑色或者是紅色的。當然也可以是任意別的兩種顏色,這里的顏色用于標記,我們可以在節點類Node中增加一個boolean型變量isRed,以此來表示顏色的信息。
第二點,在插入或者刪除一個節點時,必須要遵守的規則稱為紅-黑規則:
1.每個節點不是紅色就是黑色的;
2.根節點總是黑色的;
3.如果節點是紅色的,則它的子節點必須是黑色的(反之不一定),(也就是從每個葉子到根的所有路徑上不能有兩個連續的紅色節點);
4.從根節點到葉節點或空子節點的每條路徑,必須包含相同數目的黑色節點(即相同的黑色高度)。
從根節點到葉節點的路徑上的黑色節點的數目稱為黑色高度,規則 4 另一種表示就是從根到葉節點路徑上的黑色高度必須相同。
注意:新插入的節點顏色總是紅色的,這是因為插入一個紅色節點比插入一個黑色節點違背紅-黑規則的可能性更小,原因是插入黑色節點總會改變黑色高度(違背規則4),但是插入紅色節點只有一半的機會會違背規則3(因為父節點是黑色的沒事,父節點是紅色的就違背規則3)。另外違背規則3比違背規則4要更容易修正。
紅黑樹和二叉樹的區別 :
紅黑樹和之前所講的AVL樹類似,都是在進行插入和刪除操作時通過特定操作保持二叉查找樹的平衡,從而獲得較高的查找性能。自從紅黑樹出來后,AVL樹就被放到了博物館里,據說是紅黑樹有更好的效率,更高的統計性能。 紅黑樹和AVL樹的區別在于它使用顏色來標識結點的高度,它所追求的是局部平衡而不是AVL樹中的非常嚴格的平衡。AVL樹的復雜比起紅黑樹來說簡直是小巫見大巫。紅黑樹是真正的變態級數據結構。
(AVL樹,平衡二叉樹)
7.哈希表
Hash表也稱散列表,也有直接譯作哈希表,Hash表是一種根據關鍵字值(key - value)而直接進行訪問的數據結構。它基于數組,通過把關鍵字映射到數組的某個下標來加快查找速度,但是又和數組、鏈表、樹等數據結構不同,在這些數據結構中查找某個關鍵字,通常要遍歷整個數據結構
哈希表(Hash table,也叫散列表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。
?而當使用哈希表進行查詢的時候,就是再次使用哈希函數將key轉換為對應的數組下標,并定位到該空間獲取value,如此一來,就可以充分利用到數組的定位性能進行數據定位
優缺點
優點:不論哈希表中有多少數據,查找、插入、刪除(有時包括刪除)只需要接近常量的時間即0(1)的時間級。實際上,這只需要幾條機器指令。
哈希表運算得非常快,在計算機程序中,如果需要在一秒種內查找上千條記錄通常使用哈希表(例如拼寫檢查器)哈希表的速度明顯比樹快,樹的操作通常需要O(N)的時間級。哈希表不僅速度快,編程實現也相對容易。
如果不需要有序遍歷數據,并且可以提前預測數據量的大小。那么哈希表在速度和易用性方面是無與倫比的。
缺點:它是基于數組的,數組創建后難于擴展,某些哈希表被基本填滿時,性能下降得非常嚴重,所以程序員必須要清楚表中將要存儲多少數據(或者準備好定期地把數據轉移到更大的哈希表中,這是個費時的過程)。
(有個不錯的例子,點擊)
8. 堆
堆是一顆完全二叉樹,在這棵樹中,所有父節點都滿足大于等于其子節點的堆叫大根堆,所有父節點都滿足小于等于其子節點的堆叫小根堆。堆雖然是一顆樹,但是通常存放在一個數組中,父節點和孩子節點的父子關系通過數組下標來確定。
堆是弱序的,所以想要遍歷堆是很困難的,基本上,堆是不支持遍歷的。
對于查找,由于堆的特性,在查找的過程中,沒有足夠的信息來決定選擇通過節點的兩個子節點中的哪一個來選擇走向下一層,所以也很難在堆中查找到某個關鍵字。
因此,堆這種組織似乎非常接近無序,不過,對于快速的移除最大(或最小)節點,也就是根節點,以及能快速插入新的節點,這兩個操作就足夠了。