重溫四大基礎資料結構:陣列、連結串列、佇列和棧
阿新 • • 發佈:2020-08-05
![file](https://img2020.cnblogs.com/other/1648938/202008/1648938-20200805081813714-1550683399.jpg)
# 前言
> 本文收錄於專輯:[http://dwz.win/HjK](http://dwz.win/HjK),點選解鎖更多資料結構與演算法的知識。
你好,我是彤哥,一個每天爬二十六層樓還不忘讀原始碼的硬核男人。
陣列、連結串列、佇列、棧,是資料結構中最基礎的四大結構,陣列和連結串列更是基礎中的基礎,後續所有複雜的資料結構都是在它們的基礎上演變而來的。
本節,我們就來重溫這四大結構。
# 陣列
關於陣列,大家都比較熟悉了。
它是一種線性資料結構,使用一組連續的記憶體空間儲存一組具有相同型別的資料。
![file](https://img2020.cnblogs.com/other/1648938/202008/1648938-20200805081814116-225091817.jpg)
這個概念中有三個關鍵詞:線性、連續、相同型別。
線性,表示沒有分叉,任意元素的前後元素最多隻有一個,同樣是線性結構的還有連結串列、佇列等。
連續,它在記憶體空間中的儲存是連續的,不間斷的,前後兩個元素緊挨著,不存在間隙。
相同型別,陣列中儲存的元素的型別一定是相同的,當然,在Java中,你可以使用Object代表所有型別,本質上,它們依然是相同型別。
正是有了上面三個特性,才使得陣列具有了**隨機訪問**的特性,那麼,什麼是隨機訪問呢?
簡單點說,你可以通過下標快速定位到陣列中的元素,且時間複雜度是O(1),它是怎麼做到的呢?
我們知道,計算機中只有0和1,一切的一切都可以看作是0和1的各種組合,記憶體也是一樣。
當我們建立一個數組,比如`int[] array = new int[]{2, 5, 8, 7};`時,它其實返回的是這個陣列在記憶體中的位置(地址),我們知道,一個int型別佔用4個位元組,也就是32位的0或1,當我們訪問陣列下標為0的元素時,直接返回陣列地址處取32位轉成int即可,同樣地,當我們訪問陣列下標為1的元素時,返回陣列地址加上(32*1)的地址處取32位轉成int,依此類推。
![file](https://img2020.cnblogs.com/other/1648938/202008/1648938-20200805081814468-1070089458.jpg)
這也是大部分語言中陣列下標從0開始的原因,試想如果下標從1開始,那麼,計算記憶體地址的時候就變成了`address + 32 * (i - 1)`,這顯然會造成一定的效能損耗。
# 連結串列
連結串列,它也是一種執行緒資料結構,與陣列不同的是,它在記憶體空間中不一定是順序儲存的,為了保證連結串列中元素的連續性,一般使用一個指標來找到下一個元素。
![file](https://img2020.cnblogs.com/other/1648938/202008/1648938-20200805081814789-1939003508.jpg)
上圖是典型的單鏈表結構,在單鏈表中,只有一個指向下一個元素的指標。
如果要用Java類來表示單鏈表中的元素節點的話,大概看起來像這樣子:
```java
class Node {
int value;
Node next;
}
```
所以,連結串列不具有隨機訪問的特性,在連結串列中根據索引來查詢元素只能從頭開始(單鏈表),它的時間複雜度是O(n)。
上面我們說的是單鏈表,如果在單鏈表的基礎上再增加一個前驅指標(指向前一個元素的指標),就變成了雙向連結串列。
![file](https://img2020.cnblogs.com/other/1648938/202008/1648938-20200805081815160-522831973.jpg)
Java中的LinkedList就是典型的雙向連結串列結構,雙向連結串列既可以當作佇列使用,又可以當作棧來使用,非常方便。
如果在雙向連結串列的基礎上再增加HashMap的功能,就變成了LinkedHashMap了,咳咳,扯遠了。
> 希望學習LinkedList和LinkedHashMap原始碼解析的同學,可以關注我的公號主“彤哥讀原始碼”。
這裡提到了佇列,那麼,什麼是佇列呢?
# 佇列
所謂佇列,其實跟現實中的排隊是一樣的,其中的元素從一端進入,從另一端出去,英文叫做:First In,First Out,簡寫FIFO。
![file](https://img2020.cnblogs.com/other/1648938/202008/1648938-20200805081815475-1469091068.jpg)
從這張圖,也可以看出來,實現佇列最簡單的方式就是使用連結串列,把上圖中的箭頭倒過來即可。
![file](https://img2020.cnblogs.com/other/1648938/202008/1648938-20200805081815815-153399157.jpg)
入隊時,將元素加入到連結串列尾端,出隊時,將第一個元素刪除並將頭節點指向下一個節點即可。
讓我們來看看使用連結串列實現佇列的簡單程式碼實現:
```java
public class LinkedQueue {
Node head;
Node tail;
void offer(Integer value) {
if (value == null) {
throw new NullPointerException();
}
Node node = new Node(value);
if (head == null) {
head = tail = node;
} else {
tail.next = node;
tail = node;
}
}
Integer poll() {
Node first = head;
if (first != null) {
head = first.next;
first.next = null;
return first.value;
} else {
return null;
}
}
static class Node {
int value;
Node next;
public Node(int value) {
this.value = value;
}
}
}
```
是不是很簡單呢?
那麼,陣列能不能實現佇列呢?
答案是肯定的,使用陣列實現佇列有很多種方式,其中一種是使用兩個指標:入指標、出指標,它們分別指向下一個入佇列和下一個出佇列的位置。
入隊時,在入指標處放入元素,同時入指標後移。
出隊時,取出出指標處的元素返回,同時出指標後移。
當指標到達陣列末尾時,返回陣列開始的位置。
這樣就形成了一個可以迴圈使用的陣列,俗稱環形陣列。
![file](https://img2020.cnblogs.com/other/1648938/202008/1648938-20200805081816271-1815493816.jpg)
此時,我們考慮一個問題,佇列空和佇列滿時,兩個指標都是指向同一個位置,似乎不太好處理。
其實,很簡單,引入一個size變數標識佇列中有多少個元素即可。
所以,這玩意兒要怎麼實現呢?Show me the code!
```java
public class ArrayQueue {
int[] array;
int offerIndex;
int pollIndex;
int size;
public ArrayQueue(int capacity) {
this.array = new int[capacity];
this.offerIndex = this.pollIndex = 0;
this.size = 0;
}
boolean offer(Integer value) {
if (value == null) {
throw new NullPointerException();
}
if (size == array.length) {
return false;
}
array[offerIndex] = value;
offerIndex = (offerIndex + 1) % array.length;
size++;
return true;
}
Integer poll() {
if (size == 0) {
return null;
}
int value = array[pollIndex];
pollIndex = (pollIndex + 1) % array.length;
size--;
return value;
}
}
```
OK,以上就是使用陣列實現的佇列,可以看到,與連結串列實現的佇列相比,它需要指定容量,這叫做`有界佇列`,如果需要使用陣列實現無界佇列,則需要加入擴容的機制,有興趣的同學可以自己實現看看。
下面,我們再來看另一種基礎的資料結構——棧。
# 棧
棧,它是與隊列表現完全相反的資料結構,它的元素是先進的後出來,就像我們往一個杯子裡面放東西一樣,先放進去的放在最下面,只有把上面的東西拿出來後才能拿出下面壓著的東西,這種行為用英文叫做:First In,Last Out,簡稱FILO。
![file](https://img2020.cnblogs.com/other/1648938/202008/1648938-20200805081816660-2134127047.jpg)
棧,具有很多用處,計算機中很多處理都是通過棧這種資料結構來進行的,比如算術運算,準備兩個棧,一個棧儲存數字,一個棧儲存符號,從頭開始依次把字元壓入到這兩個棧中,當遇到符號優先順序比棧頂元素低時,則取出棧頂符號,並從數字棧中取出兩個數字進行運算,運算的結果再壓回數字棧中,繼續以此執行,當所有字元都放入棧之後,依次從數字棧中取出兩個元素,並從符號棧中取出一個元素,進行計算,結果壓回數字棧,繼續以此執行,直到符號棧為空,或者數字棧只剩下一個元素為止,彈出這個數字即為最後的結果。
以`3 + 2 * 4 -1`為例:
![file](https://img2020.cnblogs.com/other/1648938/202008/1648938-20200805081817094-1539083352.jpg)
好了,關於棧,我們就簡單介紹到這裡,後面,我們還會大量遇到這個資料結構。
# 後記
本節,我們一起重溫了陣列、連結串列、佇列、棧這四種最基礎的資料結構。
說起陣列,我們看到,記憶體本身就是一張大陣列,它裡面的元素就是0和1,那麼,我們能不能直接操作這些0和1呢?
答案是肯定的。
下一節,我們將介紹位運算,以及點陣圖這種資料結構,彼時,我們將詳細介紹如何使用`點陣圖來實現12306的搶票邏輯`,關注我,及時獲取最新推文。
> 關注公號主“彤哥讀原始碼”,解鎖更多原始碼、基礎、架構