鏈表 知識點整理


鏈表(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被作者們用來開發幾種早期的人工智能程序,包括邏輯推理機,通用問題解算器和一個計算機象棋程序。
 
結構
單向鏈表
鏈表中最簡單的一種是單向鏈表,它包含兩個域,一個信息域和一個指針域。這個鏈接指向列表中的下一個節點,而最后一個節點則指向一個空值。
一個單向鏈表的節點被分成兩個部分。第一個部分保存或者顯示關於節點的信息,第二個部分存儲下一個節點的地址。單向鏈表只可向一個方向遍歷。
鏈表最基本的結構是在每個節點保存數據和到下一個節點的地址,在最后一個節點保存一個特殊的結束標記,另外在一個固定的位置保存指向第一個節點的指針,有的時候也會同時儲存指向最后一個節點的指針。一般查找一個節點的時候需要從第一個節點開始每次訪問下一個節點,一直訪問到需要的位置。但是也可以提前把一個節點的位置另外保存起來,然后直接訪問。當然如果只是訪問數據就沒必要了,不如在鏈表上儲存指向實際數據的指針。這樣一般是為了訪問鏈表中的下一個或者前一個(需要儲存反向的指針,見下面的雙向鏈表)節點。
相對於下面的雙向鏈表,這種普通的,每個節點只有一個指針的鏈表也叫單向鏈表,或者單鏈表,通常用在每次都只會按順序遍歷這個鏈表的時候(例如圖的鄰接表,通常都是按固定順序訪問的)。
鏈表也有很多種不同的變化:

 單向鏈表的代碼實現:

#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個(如果編譯器支持)節點固定的表示這個虛擬節點。

 雙向鏈表的代碼實現:

#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)甚至是一些小的數據集合。不管怎樣,一些時候一個鏈表在這樣一個樹中創建一個節點子集,並且以此來更有效率地轉換這個集合。
 
參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM