數據結構與算法-鏈表(上)

數組是軟件開發過程當中很是重要的一種數據結構,可是數組至少有兩個侷限:算法

  • 編譯期須要肯定元素大小
  • 數組在內存中是連續的,插入或者刪除須要移動數組中其餘數據
數組適合處理肯定長度的,對於插入或者刪除不敏感的數據。若是數據是頻繁變化的,就須要選擇其餘數據結構了。鏈表是一種邏輯簡單的、實用的數據結構,幾乎被全部程序設計語言支持。咱們從最簡單的鏈式結構開始,根據需求的變化一步步改進,知足產品需求。


一、單向鏈表數組

單向鏈表是由一個個節點組成的,每一個節點是一種信息集合,包含元素自己以及下一個節點的地址。節點在內存中是非連續分佈的,在程序運行期間,根據須要能夠動態的建立節點,這就使得鏈表的長度沒有邏輯上的限制,有限制的是堆的大小。在單向鏈表中,每一個節點中存儲着下一個節點的地址,就像這樣:


事實上,咱們更加關注的是基於數據結構的算法,鏈表是一種簡單的數據組織方式,適合中等數量的數據,咱們考察鏈表的添加、刪除、查找便可,更加複雜的操做需求最好使用更加高級的數據結構。數據結構

首先定義鏈表:函數

#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)級別的操做能夠接受,對於更加複雜的數據,須要更加複雜的數據結構進行處理。出於學習的態度,能夠繼續研究,畢竟有句話叫作-厚積薄發。

數據結構與算法-鏈表(下)

相關文章
相關標籤/搜索