數組是軟件開發過程當中很是重要的一種數據結構,可是數組至少有兩個侷限:算法
一、單向鏈表數組
事實上,咱們更加關注的是基於數據結構的算法,鏈表是一種簡單的數據組織方式,適合中等數量的數據,咱們考察鏈表的添加、刪除、查找便可,更加複雜的操做需求最好使用更加高級的數據結構。數據結構
首先定義鏈表:函數
#ifndef INT_LINKED_LIST
#define INT_LINKED_LIST
class Node {
public:
//構造函數,建立一個節點
Node(int el = 0, Node* ptr = nullptr) {
info = el;
next = ptr;
}
//節點的值
int info;
//下一個節點地址
Node* next;
};
class NodeList {
public:
//構造函數,建立一個鏈表,用於管理節點
NodeList() {
head = tail = nullptr;
}
//節點插入到頭部
void addToHead(int);
//節點插入到尾部
void addToTail(int);
//刪除頭部節點
int deleteFromHead();
//刪除尾部節點
int deleteFromTail();
//刪除指定節點
void deleteNode(int);
private:
//頭指針、尾指針
Node *head, *tail;
};
#endif
複製代碼
這裏定義了兩個class
,分別用來表示節點以及管理節點的鏈表。其中,節點具備兩個成員變量,分別是當前節點的值以及指向下個節點的指針。鏈表也具備兩個成員變量,分別指向頭結點以及尾節點。鏈表class
具備5個成員方法,分別表明着節點的添加、刪除、查找,咱們來考察下這3種操做在鏈表中的表現。post
單向鏈表的操做比較簡單,這裏直接使用動圖來代替代碼,更加易於理解。學習
假設已有鏈表以下:優化
節點插入到頭部的邏輯比較簡單,算法複雜度能在固定時間O(1)
內完成,也就是說,不管鏈表中有多少個節點,該函數所執行操做的數目都不會超過某個常數c。注意,該操做的實現依賴head
指針,不然沒法肯定頭結點的地址,那麼算法的複雜度將會大大增長。spa
節點插入到尾部的邏輯和插入到頭部類似,算法複雜度也是O(1)
,區別在於該操做的實現依賴tail
指針,不然沒法肯定尾節點的地址,那麼算法的複雜度將會大大增長。設計
刪除頭部節點操做的算法複雜度也是O(1)
,該操做依賴head
指針,經過head
指針能夠直接獲取到下個節點的地址,因此複雜度很低。指針
注意這裏,刪除尾部節點的算法複雜度是O(n)
,相比於前面的O(1)
,提高了兩個量級。緣由在於咱們須要一個臨時指針p,從頭結點一直遍歷到倒數第二個節點。由於刪除尾節點以後,tail指針須要向頭結點方向移動一次,可是在鏈表中不能直接獲取到倒數第二個節點的地址,只能依靠遍歷的方式,這就致使算法複雜度上升爲O(n)
。在單向鏈表中沒有更好的解決方式了,在後面咱們須要改進鏈表結構避免這種狀況。
刪除指定節點的算法複雜度也是不盡人意,在最好的狀況下花費O(1)
的時間,在最壞和平均狀況下則是O(n)
。經過動態圖能夠發現,咱們定義P指針指向目標節點,定義Q節點指向目標節點的前驅節點。這兩個變量的存在乎義在於修正單向鏈表的指向,是不可或缺的。
基於單向鏈表的某些操做的算法複雜度沒法知足咱們的需求,這裏主要指刪除尾部節點以及刪除指定節點,它們的平均複雜度達到了O(n)
,相比於O(1)增長了兩個量級。爲了改進算法,咱們須要修改鏈表的結構。對於刪除尾部節點來講,瓶頸在於沒法直接獲取尾節點的前驅節點地址,咱們能夠爲節點加上一個指向前節點的指針來解決,這就是所謂的雙向鏈表。
二、雙向鏈表
雙向鏈表是這個樣子:
首先是定義:
#ifndef INT_LINKED_LIST
#define INT_LINKED_LIST
class Node {
public:
//構造函數,建立一個節點
Node(int el = 0, Node* p = nullptr, Node* q = nullptr) {
info = el;
pre = p;
next = q;
}
//節點的值
int info;
//前一個節點地址
Node* pre;
//下一個節點地址
Node* next;
};
class NodeList {
public:
//構造函數,建立一個鏈表,用於管理節點
NodeList() {
head = tail = nullptr;
}
//節點插入到頭部
void addToHead(int);
//節點插入到尾部
void addToTail(int);
//刪除頭部節點
int deleteFromHead();
//刪除尾部節點
int deleteFromTail();
//刪除指定節點
void deleteNode(int);
private:
//頭指針、尾指針
Node *head, *tail;
};
#endif複製代碼
基於雙向鏈表的操做和單向鏈表很是類似,咱們是從單向鏈表中擴展出雙向鏈表的,目的是改進刪除尾部節點的算法。
能夠看到刪除尾部節點的算法複雜度已經降至O(1)
,事實上pre
指針不只僅簡化了刪除尾節點操做,對於其餘O(1)
的操做也有簡化,由於有了pre
指針,有些臨時指針就不必定義了。
儘管如此,咱們仍是增長了空間的使用程度才下降了時間上的消耗,本質上是空間換取時間的作法。對於現代軟件開發來說,硬件已經不是主要瓶頸,一些空間上的代價是值得的。
也許有人瞭解過所謂的循環單向鏈表、循環雙向鏈表,它們究竟是什麼東西呢?
循環單向鏈表和單向鏈表的差異:
差異就在於尾節點的next指針循環指向了頭結點,這時候head指針就不必存在了,若是繼續定義head指針,只是更加方便一些,但它已經不是不可或缺的了。
循環雙向鏈表和雙向鏈表的差異:
一樣的道理,head
指針根據須要添加。循環鏈表和普通鏈表沒有本質的差異,能夠根據須要自行選擇。
到目前爲止,咱們還有一個問題沒有解決,那就是刪除指定節點。該操做本質上是查找問題,爲了優化查找算法,咱們須要繼續對鏈表結構進行改動。事實上,上述鏈表已經足夠知足需求了,由於咱們假設對象是中等數量的數據,O(n)
級別的操做能夠接受,對於更加複雜的數據,須要更加複雜的數據結構進行處理。出於學習的態度,能夠繼續研究,畢竟有句話叫作-厚積薄發。