陣列定義:
陣列(array)是一種線性表資料結構。它用一組連續的記憶體空間,來儲存一組具有相同型別的資料。
第一是線性表(linear list)。顧名思義,線性表就是資料排成像一條線一樣的結構。每個線性表上的資料最多只有前和後兩個方向。除了陣列,鍊錶、佇列、棧等也是線性表結構。
而與它相對立的概念是非線性表,比如二叉樹、堆、圖等。之所以叫非線性,是因為,在非線性表中,資料之間並不是簡單的前後關係。
第二個是連續的記憶體空間和相同型別的資料。正是因為這兩個限制,它才有了乙個堪稱「殺手鐗」的特性:「隨機訪問」。但有利就有弊,這兩個限制也讓陣列的很多操作變得非常低效,比如要想在陣列中刪除、插入乙個資料,為了保證連續性,就需要做大量的資料搬移工作。
在進行陣列的插入、刪除操作時,為了保持記憶體資料的連續性,需要做大量的資料搬移,所以時間複雜度是 o(n)。
鍊錶並不需要一塊連續的記憶體空間,它通過「指標」將一組零散的記憶體塊串聯起來使用。
三種最常見的鍊錶結構,它們分別是:單鏈表、雙向鍊錶和迴圈鍊錶。
首先來看最簡單、最常用的單鏈表。
把第乙個結點叫作頭結點,把最後乙個結點叫作尾結點。其中,頭結點用來記錄鍊錶的基位址。有了它,就可以遍歷得到整條鍊錶。而尾結點特殊的地方是:指標不是指向下乙個結點,而是指向乙個空位址 null,表示這是鍊錶上最後乙個結點。
針對鍊錶的插入和刪除操作,只需要考慮相鄰結點的指標改變,所以對應的時間複雜度是 o(1)。鍊錶隨機訪問的效能沒有陣列好,需要 o(n) 的時間複雜度。
迴圈鍊錶是一種特殊的單鏈表。實際上,迴圈鍊錶也很簡單。它跟單鏈表唯一的區別就在尾結點。
單鏈表的尾結點指標指向空位址,表示這就是最後的結點了。而迴圈鍊錶的尾結點指標是指向鍊錶的頭結點。
和單鏈表相比,迴圈鍊錶的優點是從鏈尾到鏈頭比較方便。當要處理的資料具有環型結構特點時,就特別適合採用迴圈鍊錶。
單向鍊錶只有乙個方向,結點只有乙個後繼指標 next 指向後面的結點。而雙向鍊錶,顧名思義,它支援兩個方向,每個結點不止有乙個後繼指標 next 指向後面的結點,還有乙個前驅指標 prev 指向前面的結點。
雙向鍊錶需要額外的兩個空間來儲存後繼結點和前驅結點的位址。所以,如果儲存同樣多的資料,雙向鍊錶要比單鏈表占用更多的記憶體空間。雖然兩個指標比較浪費儲存空間,但可以支援雙向遍歷,這樣也帶來了雙向鍊錶操作的靈活性。
相比單鏈表,雙向鍊錶適合解決哪種問題呢?
從結構上來看,雙向鍊錶可以支援 o(1) 時間複雜度的情況下找到前驅結點,正是這樣的特點,也使雙向鍊錶在某些情況下的插入、刪除等操作都要比單鏈表簡單、高效。
而單鏈表的插入、刪除操作的時間複雜度已經是 o(1) 了,雙向鍊錶還能再怎麼高效呢?
先來看刪除操作。
在實際的軟體開發中,從鍊錶中刪除乙個資料無外乎這兩種情況:
對於第一種情況,不管是單鏈表還是雙向鍊錶,為了查詢到值等於給定值的結點,都需要從頭結點開始乙個乙個依次遍歷對比,直到找到值等於給定值的結點,然後再通過指標操作將其刪除。
儘管單純的刪除操作時間複雜度是 o(1),但遍歷查詢的時間是主要的耗時點,對應的時間複雜度為 o(n)。根據時間複雜度分析中的加法法則,刪除值等於給定值的結點對應的鍊錶操作的總時間複雜度為 o(n)。
對於第二種情況,已經找到了要刪除的結點,但是刪除某個結點 q 需要知道其前驅結點,而單鏈表並不支援直接獲取前驅結點,所以,為了找到前驅結點,我們還是要從頭結點開始遍歷鍊錶,直到 p->next=q,說明 p 是 q 的前驅結點。
但是對於雙向鍊錶來說,這種情況就比較有優勢了。因為雙向鍊錶中的結點已經儲存了前驅結點的指標,不需要像單鏈表那樣遍歷。所以,針對第二種情況,單鏈表刪除操作需要 o(n) 的時間複雜度,而雙向鍊錶只需要在 o(1) 的時間複雜度內就搞定了!
同理,如果希望在鍊錶的某個指定結點前面插入乙個結點,雙向鍊錶比單鏈表有很大的優勢。雙向鍊錶可以在 o(1) 時間複雜度搞定,而單向鍊錶需要 o(n) 的時間複雜度。
除了插入、刪除操作有優勢之外,對於乙個有序鍊錶,雙向鍊錶的按值查詢的效率也要比單鏈表高一些。因為,我們可以記錄上次查詢的位置 p,每次查詢時,根據要查詢的值與 p 的大小關係,決定是往前還是往後查詢,所以平均只需要查詢一半的資料。
不過,陣列和鍊錶的對比,並不能侷限於時間複雜度。而且,在實際的軟體開發中,不能僅僅利用複雜度分析就決定使用哪個資料結構來儲存資料。
陣列簡單易用,在實現上使用的是連續的記憶體空間,可以借助 cpu 的快取機制,預讀陣列中的資料,所以訪問效率更高。而鍊錶在記憶體中並不是連續儲存,所以對 cpu 快取不友好,沒辦法有效預讀。
陣列的缺點是大小固定,一經宣告就要占用整塊連續記憶體空間。如果宣告的陣列過大,系統可能沒有足夠的連續記憶體空間分配給它,導致「記憶體不足(out of memory)」。如果宣告的陣列過小,則可能出現不夠用的情況。這時只能再申請乙個更大的記憶體空間,把原陣列拷貝進去,非常費時。鍊錶本身沒有大小的限制,天然地支援動態擴容。
和陣列相比,鍊錶更適合插入、刪除操作頻繁的場景,查詢的時間複雜度較高。不過,在具體軟體開發中,要對陣列和鍊錶的各種效能進行對比,綜合來選擇使用兩者中的哪乙個。
從棧的操作特性上來看,棧是一種「操作受限」的線性表,只允許在一端插入和刪除資料。
當某個資料集合只涉及在一端插入和刪除資料,並且滿足後進先出、先進後出的特性,應該首選「棧」這種資料結構。
棧既可以用陣列來實現,也可以用鍊錶來實現。用陣列實現的棧,我們叫作順序棧,用鍊錶實現的棧,我們叫作鏈式棧。
不管是順序棧還是鏈式棧,我們儲存資料只需要乙個大小為 n 的陣列就夠了。在入棧和出棧過程中,只需要一兩個臨時變數儲存空間,所以空間複雜度是 o(1)。
不管是順序棧還是鏈式棧,入棧、出棧只涉及棧頂個別資料的操作,所以時間複雜度都是 o(1)。
先進者先出,這就是典型的「佇列」。
棧只支援兩個基本操作:入棧 push()和出棧 pop()。佇列跟棧非常相似,支援的操作也很有限,最基本的操作也是兩個:入隊 enqueue(),放乙個資料到佇列尾部;出隊 dequeue(),從佇列頭部取乙個元素。
佇列跟棧一樣,也是一種操作受限的線性表資料結構。
跟棧一樣,佇列可以用陣列來實現,也可以用鍊錶來實現。用陣列實現的棧叫作順序棧,用鍊錶實現的棧叫作鏈式棧。同樣,用陣列實現的佇列叫作順序佇列,用鍊錶實現的佇列叫作鏈式佇列。
順序佇列:
佇列需要兩個指標:乙個是 head 指標,指向隊頭;乙個是 tail 指標,指向隊尾。
結合下面這幅圖來理解。當 a、b、c、d 依次入隊之後,佇列中的 head 指標指向下標為 0 的位置,tail 指標指向下標為 4 的位置。
當我們呼叫兩次出隊操作之後,佇列中 head 指標指向下標為 2 的位置,tail 指標仍然指向下標為 4 的位置。
隨著不停地進行入隊、出隊操作,head 和 tail 都會持續往後移動。當 tail 移動到最右邊,即使陣列中還有空閒空間,也無法繼續往佇列中新增資料了。
鏈式佇列:
同樣需要兩個指標:head 指標和 tail 指標。它們分別指向鍊錶的第乙個結點和最後乙個結點。如圖所示,入隊時,tail->next= new_node, tail = tail->next;出隊時,head = head->next。
剛才用陣列來實現佇列的時候,在 tail==n 時,會有資料搬移操作,這樣入隊操作效能就會受到影響。那有沒有辦法能夠避免資料搬移呢?我們來看看迴圈佇列的解決思路。
迴圈佇列,顧名思義,它長得像乙個環。原本陣列是有頭有尾的,是一條直線。現在我們把首尾相連,扳成了乙個環。
圖中這個佇列的大小為 8,當前 head=4,tail=7。當有乙個新的元素 a 入隊時,我們放入下標為 7 的位置。但這個時候,我們並不把 tail 更新為 8,而是將其在環中後移一位,到下標為 0 的位置。當再有乙個元素 b 入隊時,我們將 b 放入下標為 0 的位置,然後 tail 加 1 更新為 1。所以,在 a,b 依次入隊之後,迴圈佇列中的元素就變成了下面的樣子:
通過這樣的方法,我們成功避免了資料搬移操作。看起來不難理解,但是迴圈佇列的**實現難度要比前面講的非迴圈佇列難多了。要想寫出沒有 bug 的迴圈佇列的實現**,最關鍵的是,確定好隊空和隊滿的判定條件。
在用陣列實現的非迴圈佇列中,隊滿的判斷條件是 tail == n,隊空的判斷條件是 head == tail。那針對迴圈佇列,如何判斷隊空和隊滿呢?
隊列為空的判斷條件仍然是 head == tail。
但佇列滿的判斷條件就稍微有點複雜了。
圖中畫的隊滿的情況,tail=3,head=4,n=8,所以總結一下規律就是:(3+1)%8=4。
當隊滿時,(tail+1)%n=head。
圖中的 tail 指向的位置實際上是沒有儲存資料的。所以,迴圈佇列會浪費乙個陣列的儲存空間。
《資料結構與演算法之美》學習筆記之開篇
本系列是極客時間中前 google 工程師王爭 資料結構與演算法之美 專欄的學習筆記,想加強資料結構及演算法能力的同學可以直接購買此專欄,跳轉鏈結在此 從廣義上講,資料結構就是指一組資料的儲存結構。演算法就是運算元據的一組方法。資料結構和演算法是相輔相成的。資料結構是為演算法服務的,演算法要作用在特...
資料結構與演算法之美學習心得3
為什麼需要複雜度分析 事後統計法 把 跑一遍,通過統計 監控來得到演算法執行的時間和占用的記憶體大小。缺點 1.測試結果非常依賴測試環境 測試環境中的硬體不同會導致測試結果也不同。2.測試結果受資料規模的影響很大 資料規模太大,測試 需要花費的時間就太多了 資料規模太小,反映不出演算法的真實性能 對...
資料結構與演算法之美學習筆記 5 9章
陣列是一種線性表資料結構,他用一組連續的記憶體空間,來儲存相同型別的資料 這裡要注意不根據下標是不能隨機訪問的啊 假設乙個長度為10的int型陣列,會分配一塊連續記憶體空間 1000 1039,其中,記憶體塊首位址是1000 我們可以通過以下公式快速獲取到指定下標的元素 a i address ba...