鏈表 知識點整理

鏈表(Linked list)是一種常見的基礎數據結構,是一種線性表,可是並不會按線性的順序存儲數據,而是在每個節點裏存到下一個節點的指針(Pointer)。因爲沒必要須按順序存儲,鏈表在插入的時候能夠達到O(1)的複雜度,比另外一種線性表順序錶快得多,可是查找一個節點或者訪問特定編號的節點則須要O(n)的時間,而順序表相應的時間複雜度分別是O(logn)和O(1)。
使用鏈表結構能夠克服數組鏈表須要預先知道數據大小的缺點,鏈表結構能夠充分利用計算機內存空間,實現靈活的內存動態管理。可是鏈表失去了數組隨機讀取的優勢,同時鏈表因爲增長告終點的指針域,空間開銷比較大。
在計算機科學中,鏈表做爲一種基礎的數據結構能夠用來生成其它類型的數據結構。鏈表一般由一連串節點組成,每一個節點包含任意的實例數據(data fields)和一或兩個用來指向上一個/或下一個節點的位置的連接("links")。鏈表最明顯的好處就是,常規數組排列關聯項目的方式可能不一樣於這些數據項目在記憶體或磁盤上順序,數據的訪問每每要在不一樣的排列順序中轉換。而鏈表是一種自我指示數據類型,由於它包含指向另外一個相同類型的數據的指針(連接)。鏈表容許插入和移除表上任意位置上的節點,可是不容許隨機存取。鏈表有不少種不一樣的類型:單向鏈表,雙向鏈表以及循環鏈表。
 
歷史
鏈表開發於1955-56,由當時所屬於蘭德公司(英語:RAND Corporation)的艾倫紐維爾(Allen Newell),克里夫肖(Cliff Shaw)和赫伯特西蒙(Herbert Simon)在他們編寫的信息處理語言(IPL)中作爲原始數據類型所編寫。IPL被做者們用來開發幾種早期的人工智能程序,包括邏輯推理機,通用問題解算器和一個計算機象棋程序。
 
結構
單向鏈表
鏈表中最簡單的一種是單向鏈表,它包含兩個域,一個信息域和一個指針域。這個連接指向列表中的下一個節點,而最後一個節點則指向一個空值。
一個單向鏈表的節點被分紅兩個部分。第一個部分保存或者顯示關於節點的信息,第二個部分存儲下一個節點的地址。單向鏈表只可向一個方向遍歷。
鏈表最基本的結構是在每一個節點保存數據和到下一個節點的地址,在最後一個節點保存一個特殊的結束標記,另外在一個固定的位置保存指向第一個節點的指針,有的時候也會同時儲存指向最後一個節點的指針。通常查找一個節點的時候須要從第一個節點開始每次訪問下一個節點,一直訪問到須要的位置。可是也能夠提早把一個節點的位置另外保存起來,而後直接訪問。固然若是隻是訪問數據就不必了,不如在鏈表上儲存指向實際數據的指針。這樣通常是爲了訪問鏈表中的下一個或者前一個(須要儲存反向的指針,見下面的雙向鏈表)節點。
相對於下面的雙向鏈表,這種普通的,每一個節點只有一個指針的鏈表也叫單向鏈表,或者單鏈表,一般用在每次都只會按順序遍歷這個鏈表的時候(例如圖的鄰接表,一般都是按固定順序訪問的)。
鏈表也有不少種不一樣的變化:

 單向鏈表的代碼實現:node

#include <bits/stdc++.h>
using namespace std;
/**
 * 單向鏈表的實現
*/
struct Node {
    int val;
    Node* next;
};

Node* head;
int sz;

void init() {
    head = new Node;
    head->next = NULL;
    sz = 0;
}

void add(int pos, int val) {    // 用於向單向鏈表的 pos 位置插入一個元素
    if (pos > sz) {
        puts("illegal");
        return;
    }
    sz ++;
    Node* tmp = head;
    for (int i = 0; i < pos; i ++) tmp = tmp->next;
    Node* new_node = new Node;
    new_node->val = val;
    new_node->next = tmp->next;
    tmp->next = new_node;
}

void del(int pos) {     // 用於刪除單向鏈表的 pos 位置的元素
    if (pos >= sz) {
        puts("illegal");
        return;
    }
    sz --;
    Node* tmp = head;
    for (int i = 0; i < pos; i ++) tmp = tmp->next;
    tmp->next = tmp->next->next;
}

void output() { // 輸出單向鏈表中的全部元素
    Node* tmp = head;
    for (int i = 0; i < sz; i ++) {
        if (i) cout << " ";
        tmp = tmp->next;
        cout << tmp->val;
    }
    cout << endl;
}

string s;
int pos, val;

int main() {
    init();
    while (cin >> s) {
        if (s == "add") {
            cin >> pos >> val;
            add(pos, val);
        }
        else if (s == "del") {
            cin >> pos;
            del(pos);
        }
        else if (s == "output") {
            output();
        }
        else if (s == "size") {
            cout << sz << endl;
        }
    }
    return 0;
}

  

雙向鏈表
一種更復雜的鏈表是「雙向鏈表」或「雙面鏈表」。每一個節點有兩個鏈接:一個指向前一個節點,(當此「鏈接」爲第一個「鏈接」時,指向空值或者空列表);而另外一個指向下一個節點,(當此「鏈接」爲最後一個「鏈接」時,指向空值或者空列表)
雙向鏈表也叫雙鏈表。雙向鏈表中不只有指向後一個節點的指針,還有指向前一個節點的指針。這樣能夠從任何一個節點訪問前一個節點,固然也能夠訪問後一個節點,以致整個鏈表。通常是在須要大批量的另外儲存數據在鏈表中的位置的時候用。雙向鏈表也能夠配合下面的其餘鏈表的擴展使用。
因爲另外儲存了指向鏈表內容的指針,而且可能會修改相鄰的節點,有的時候第一個節點可能會被刪除或者在以前添加一個新的節點。這時候就要修改指向首個節點的指針。有一種方便的能夠消除這種特殊狀況的方法是在最後一個節點以後、第一個節點以前儲存一個永遠不會被刪除或者移動的虛擬節點,造成一個下面說的循環鏈表。這個虛擬節點以後的節點就是真正的第一個節點。這種狀況一般能夠用這個虛擬節點直接表示這個鏈表,對於把鏈表單獨的存在數組裏的狀況,也能夠直接用這個數組表示鏈表並用第0個或者第-1個(若是編譯器支持)節點固定的表示這個虛擬節點。

 雙向鏈表的代碼實現:c++

#include <bits/stdc++.h>
using namespace std;
/**
 * 雙向鏈表的實現
*/
struct DNode {
    int val;
    DNode* pre;
    DNode* next;
};

DNode* head;
int sz;

void init() {
    head = new DNode;
    head->pre = head->next = NULL;
    sz = 0;
}

void add(int pos, int val) {    // 用於向單向鏈表的 pos 位置插入一個元素
    if (pos > sz) {
        puts("illegal");
        return;
    }
    sz ++;
    DNode* tmp = head;
    for (int i = 0; i < pos; i ++) tmp = tmp->next;
    DNode* new_node = new DNode;
    new_node->val = val;
    new_node->next = tmp->next;
    new_node->pre = tmp;
    if (tmp->next != NULL) tmp->next->pre = new_node;
    tmp->next = new_node;
}

void del(int pos) {     // 用於刪除單向鏈表的 pos 位置的元素
    if (pos >= sz) {
        puts("illegal");
        return;
    }
    sz --;
    DNode* tmp = head;
    for (int i = 0; i < pos; i ++) tmp = tmp->next;
    if (tmp->next->next != NULL) tmp->next->next->pre = tmp;
    tmp->next = tmp->next->next;
}

void d_solve(int val) { // 這個函數的做用是找到雙向鏈表中第一個值爲val的元素,輸出它左右兩個元素的值
    for (DNode* tmp = head->next; tmp != NULL; tmp = tmp->next) {
        if (tmp->val == val) {
            if (tmp->pre == head) cout << "no";
            else cout << tmp->pre->val;
            if (tmp->next == NULL) cout << " no" << endl;
            else cout << " " << tmp->next->val << endl;
            return;
        }
    }
    puts("none");
}

void output() { // 輸出單向鏈表中的全部元素
    DNode* tmp = head;
    for (int i = 0; i < sz; i ++) {
        if (i) cout << " ";
        tmp = tmp->next;
        cout << tmp->val;
    }
    cout << endl;
}

string s;
int pos, val;

int main() {
    init();
    while (cin >> s) {
        if (s == "add") {
            cin >> pos >> val;
            add(pos, val);
        }
        else if (s == "del") {
            cin >> pos;
            del(pos);
        }
        else if (s == "output") {
            output();
        }
        else if (s == "size") {
            cout << sz << endl;
        }
        else if (s == "find") {
            cin >> val;
            d_solve(val);
        }
    }
    return 0;
}

  

循環鏈表
在一個 循環鏈表中, 首節點和末節點被鏈接在一塊兒。這種方式在單向和雙向鏈表中皆可實現。要轉換一個循環鏈表,你開始於任意一個節點而後沿着列表的任一方向直到返回開始的節點。再來看另外一種方法,循環鏈表能夠被視爲「無頭無尾」。這種列表很利於節約數據存儲緩存, 假定你在一個列表中有一個對象而且但願全部其餘對象迭代在一個非特殊的排列下。
指向整個列表的指針能夠被稱做訪問指針。
循環鏈表中第一個節點以前就是最後一個節點,反之亦然。循環鏈表的無邊界使得在這樣的鏈表上設計算法會比普通鏈表更加容易。對於新加入的節點應該是在第一個節點以前仍是最後一個節點以後能夠根據實際要求靈活處理,區別不大(詳見下面實例代碼)。固然,若是隻會在最後插入數據(或者只會在以前),處理也是很容易的。
另外有一種模擬的循環鏈表,就是在訪問到最後一個節點以後的時候,手工的跳轉到第一個節點。訪問到第一個節點以前的時候也同樣。這樣也能夠實現循環鏈表的功能,在直接用循環鏈表比較麻煩或者可能會出現問題的時候能夠用。

 循環鏈表的代碼實現:算法

#include <bits/stdc++.h>
using namespace std;
/**
 * 循環鏈表的實現
*/
struct Node {
    int val;
    Node* next;
};

Node* head;
int sz;

void init() {
    head = new Node;
    head->next = head;
    sz = 0;
}

void add(int pos, int val) {    // 用於向循環鏈表的 pos 位置插入一個元素
    if (pos > sz) {
        puts("illegal");
        return;
    }
    sz ++;
    Node* tmp = head;
    for (int i = 0; i < pos; i ++) tmp = tmp->next;
    Node* new_node = new Node;
    new_node->val = val;
    new_node->next = tmp->next;
    tmp->next = new_node;
}

void del(int pos) {     // 用於刪除循環鏈表的 pos 位置的元素
    if (pos >= sz) {
        puts("illegal");
        return;
    }
    sz --;
    Node* tmp = head;
    for (int i = 0; i < pos; i ++) tmp = tmp->next;
    tmp->next = tmp->next->next;
}

void output() { // 輸出循環鏈表中的全部元素
    Node* tmp = head;
    for (int i = 0; i < sz; i ++) {
        if (i) cout << " ";
        tmp = tmp->next;
        cout << tmp->val;
    }
    cout << endl;
}

void output_num(int num) { // 輸出循環鏈表中的前 n 個元素,由於是循環鏈表,因此若是鏈表中只有兩個元素:2,1;可是num==5的狀況下,會輸出2 1 2 1 2。
    if (sz == 0) {
        cout << "none" << endl;
        return;
    }
    Node* tmp = head;
    for (int i = 0; i < num; i ++) {
        tmp = tmp->next;
        if (tmp == head) {
            i --;
            continue;
        }
        if (i) cout << " ";
        cout << tmp->val;
    }
    cout << endl;
}

string s;
int pos, val;

int main() {
    init();
    while (cin >> s) {
        if (s == "add") {
            cin >> pos >> val;
            add(pos, val);
        }
        else if (s == "del") {
            cin >> pos;
            del(pos);
        }
        else if (s == "output") {
            output();
        }
        else if (s == "output_num") {
            int num;
            cin >> num;
            output_num(num);
        }
        else if (s == "size") {
            cout << sz << endl;
        }
    }
    return 0;
}

  

塊狀鏈表
塊狀鏈表自己是一個鏈表,可是鏈表儲存的並非通常的數據,而是由這些數據組成的順序表。每個塊狀鏈表的節點,也就是順序表,能夠被叫作一個塊。
塊狀鏈表經過使用可變的順序表的長度和特殊的插入、刪除方式,能夠在達到 O(sqrt(n)) 的複雜度。塊狀鏈表另外一個特色是相對於普通鏈表來講節省內存,由於不用保存指向每個數據節點的指針。

 

其它擴展
根據狀況,也能夠本身設計鏈表的其它擴展。可是通常不會在邊上附加數據,由於鏈表的點和邊基本上是一一對應的(除了第一個或者最後一個節點,可是也不會產生特殊狀況)。不過有一個特例是若是鏈表支持在鏈表的一段中把前和後指針反向,反向標記加在邊上可能會更方便。
對於非線性的鏈表,能夠參見相關的其餘數據結構,例如樹、圖。另外有一種基於多個線性鏈表的數據結構:跳錶,插入、刪除和查找等基本操做的速度能夠達到O(nlogn),和平衡樹同樣。

 

存儲結構
鏈表中的節點不須要以特定的方式存儲,可是集中存儲也是能夠的,主要分下面這幾種具體的存儲方法:
共享存儲空間
鏈表的節點和其它的數據共享存儲空間,優勢是能夠存儲無限多的內容(不過要處理器支持這個大小,而且存儲空間足夠的狀況下),不須要提早分配內存;缺點是因爲內容分散,有時候可能不方便調試。
獨立存儲空間
一個鏈表或者多個鏈表使用獨立的存儲空間,通常用數組或者相似結構實現,優勢是能夠自動得到一個附加數據:惟一的編號,而且方便調試;缺點是不能動態的分配內存。固然,另外的在上面加一層塊狀鏈表用來分配內存也是能夠的,這樣就解決了這個問題。這種方法有時候被叫作數組模擬鏈表,可是事實上只是用表示在數組中的位置的下標索引代替了指向內存地址的指針,這種下標索引其實也是邏輯上的指針,整個結構仍是鏈表,並不算是被模擬的(可是能夠說成是用數組實現的鏈表)。

 

鏈表的應用
鏈表用來構建許多其它數據結構,如堆棧,隊列和他們的派生。
節點的數據域也能夠成爲另外一個鏈表。經過這種手段,咱們能夠用列表來構建許多鏈性數據結構;這個實例產生於Lisp編程語言,在Lisp中鏈表是初級數據結構,而且如今成爲了常見的基礎編程模式。 有時候,鏈表用來生成聯合數組,在這種狀況下咱們稱之爲聯合數列。這種狀況下用鏈表會優於其它數據結構,如自平對分查找樹(self-balancing binary search trees)甚至是一些小的數據集合。無論怎樣,一些時候一個鏈表在這樣一個樹中建立一個節點子集,而且以此來更有效率地轉換這個集合。
 
參考資料
相關文章
相關標籤/搜索