link list 單向鏈表及常用方法實現 | cpp

鏈表一般有三種,一種是單向鏈表,每個結點只知道下家是誰;一種是雙向鏈表,每個結點知道自己的上下家;一種是循環鏈表,類似魔比斯環,可以從頭走到頭(還有一種是雙向+循環)。
差異在於增刪操作的便捷性,以及增刪後需要修改的內容(越方便操作的,需要修改的內容越多)。

這裏暫時只介紹單向鏈表

node

用來描述鏈表中數據的基本單元

Data Structure

初始化設置 nextnullptr

// struct all public
struct node
{
    int data;
    node *next;

    node()
    {
        next = nullptr;
    }
    node(int _data)
    {
        data = _data;
        next = nullptr;
    }
};

link list

採用帶頭結點的鏈表
頭結點僅作佔位,更能統一插入和刪除的函數寫法

設置一個 tail 維護最有一個結點

class linked_list
{
private:
    node *head, *tail;
public:
    linked_list()
    {
        head = new node();
        tail = head;
    }
};

Methods

四個主要方法

  • insert
    • append 隊尾
    • insert_head 頭插
    • insert_after
  • delete
    • pop_front 掐頭
    • pop_back 去尾
    • delete_node
  • reverse
  • traverse
  • empty

insert_afterdelete_node 需要判斷是否越界,所以返回值設置爲 bool

DS

class linked_list
{
private:
    node *head, *tail;
public:
    linked_list()
    {
        head = new node();
        tail = head;
    }

    void insert_head(int _data);
    bool insert_after(int index, int _data);
    void append_node(int _data);

    void pop_front();
    bool pop_back();
    bool delete_node(int index);

    void reverse();
    // have no impact on the linklist
    bool empty();
    void traverse();
};

insert


insert_head

先從最簡單的頭插入手

head->A->..

如果需要在 head 後面插入結點 tmp,那麼需要做兩個操作

  • head->next = tmp
  • tmp->next = head->next

問題是順序如何?需要額外空間嗎?

可以將鏈表想象爲一列行駛的火車,head 相當於火車頭,一旦後面有一部分脫軌了,那麼就再也找不回來了。

所以,如果火車頭 head 不再將當前的下一個指針作爲下一列車廂,而是掛載 tmp,這樣就會丟失數據

所以順序應該是

tmp->next = head->next

head->next = tmp

需要注意,如果維護尾指針 tail,那麼當鏈表初始化後沒有元素時調用頭插,需要修改 tail


c++

void linked_list::insert_head(int _data){
    node *tmp = new node(_data);
    tmp->next = head->next;
    head->next = tmp;
    
    // if tail equals head, change tail to point to tmp
	if(tail == head)
        tail = tmp;
}

這裏調整下順序,先來寫鏈表的遍歷 traverse,這樣可以檢驗寫過的結果

traverse

遍歷鏈表,可以想象成一個人從車頭走到車尾,每次走到一個車廂都要記下來,接着走到下一個,走到頭了說明遍歷完畢

  • 使用一個額外的指針 cur 記錄當前的車廂

  • cur->next 表示走向下一個車廂

  • cur->next == nullptr 表示遍歷完畢


c++

void linked_list::traverse(){
    node *cur = head->next;
    while(cur != nullptr)
    {
        cout << cur->data << endl;
        cur = cur->next;
    }
}

現在檢驗一下 insert_head 是否正確

test

int main()
{
    linked_list a;
    for(int i = 0; i < 10; ++i)
        a.insert_head(i);

    a.traverse();
}

/*
9
8
7
6
5
4
3
2
1
0
*/

append_node

在鏈表尾部插入結點,直接使用 tail 即可(tail 版本的 insert_head)

c++

void linked_list::append_node(int _data){
    node *tmp = new node{_data};
    tail->next = tmp;
    tail = tail->next;
}

insert_after(index, _data)

插入需要插入到對應位置結點之後,默認下標從 0 開始

如果插入位置爲第一個,那麼只要調用 insert_head即可

如果插入位置爲最後一個,那麼只要調用 append 即可

如果是中間的結點
那麼設置一個指針 pre 來找到前面的結點,cnt 記錄已經走過的結點數,需要走 index - 1 個結點

bool linked_list::insert_after(int index, int _data){
    // call insert_head()
    if(index == 0){
        insert_head(_data);
        return true;
    }

    int cnt = 0;
    node *pre = head->next;// get the previous node of target node
    while(cnt < index - 1){
        pre = pre->next;
        cnt++;
    }

    // if insert after the last node
    if(pre == tail){
        append_node(_data);
        return true;
    }
    
    node *tmp = new node(_data);
    tmp->next = pre->next;
    pre->next = tmp;
    return true;
}

test

int main()
{
    linked_list a;
    for(int i = 0; i < 10; ++i)
        a.insert_head(i);

    a.insert_after(10, -1);
    a.insert_after(2, -2);
    a.traverse();
}

/*
9
8
-2
7
6
5
4
3
2
1
0
-1
*/

no problem


delete_node (index)

刪除固定下標的元素

同 insert 相同,如果要從一列形式火車中去除一個車廂(假設可以這樣做),那麼我們需要有前一個車廂掛鉤,以及後一個車廂的位置

設置一個指針 pre 來找到前面的結點,cur 找到要刪的結點,cnt 記錄已經走過的結點,需要走 index 個車廂(同 insert 實質一致,pre 走的結點數仍然是 index - 1)

爲什麼要設置兩個指針

cur 代表當前的車廂
pre->next 代表前一節車廂

在刪除時,如果修改了前一節車廂的掛鉤 pre->next,將它掛在了後面的車廂上,我們會失去需要刪除的車廂的位置,這是爲什麼需要兩個指針的原因(其實也可以僅使用 cur 一個指針,但是函數格式難以統一)

尾指針維護

另外,爲了維護尾指針 tail,如果刪除的是最後一個結點,那麼需要重新修改 tail 位置

*注意,刪除尾結點後,還需要修改新的尾結點的 next 指針,可以想象成這是一位列車員,你走的每一步路都是由她指引的,當後面沒有車廂時,你要讓它知道這是最後一節車廂了,不能再往後找了

c++

bool linked_list::delete_node(int index){
    if(index == 0){
        pop_front();// 這裏是刪除第一個元素,函數在下文給出
        return true;
    }

    int cnt = 0;
    node *pre = head, *cur = head->next;// get the previous node of target node
    while(cnt < index){
        pre = pre->next;
        cur = cur->next;
        cnt++;
    }

    // if delete the tail
    if(cur == tail){
        tail = pre;
        tail->next = nullptr;// * notice to change the next ptr
        delete(cur);
        return true;
    }

    pre->next = cur->next;
    delete(cur);
    return true;
}

跟 insert 相同,delete 同樣選擇對頭部和尾部單獨處理,如果考慮到刪除尾結點需要一個獨立邏輯寫一個函數,所以 delete_node 中只複用了 pop_frontpop_back 單獨寫一個函數(不能複用的原因顯而易見)

這裏直接給出代碼,不再贅述,邏輯在 delete_node 中寫出,可以先往下看

c++

void linked_list::pop_front(){
    node *tmp = head->next;
    head->next = head->next->next;
    delete(tmp);
}

bool linked_list::pop_back(){
    if(empty()){
        cerr << "the linklist is empty" << endl;
        return false;
    }

    node *pre = head, *cur = head->next;// get the previous node of target node
    while(pre->next != tail){
        pre = pre->next;
        cur = cur->next;
    }

    tail = pre;
    tail->next = nullptr;
    delete(cur);
    return true;
}

reverse

將鏈表順序逆置

這裏的做法是,左手拿着頭結點(火車頭),右手拿着後面的結點們(車廂們),每次把 head 當做新的空鏈表頭結點,用左手的新結點做頭插,知道沒有結點可用。

可以想象成裝手槍子彈時,新到的子彈不斷把之前的子彈壓入彈匣。

尾結點維護

尾結點即當前的第一個結點 head->next

c++

void linked_list::reverse(){
    if(empty()){
        cerr << "the linklist is empty" << endl;
        return;
    }

    tail = head->next;
    
    node *cur = head->next, *next = cur->next;
    while(next != nullptr){
        cur->next = head->next;// the same as insert_head
        head->next = cur;

        cur = next;
        next = next->next;
    }
    
	// dont forget the last node ~
    cur->next = head->next;
    head->next = cur;
    
    tail->next = nullptr;
}

test

int main()
{
    linked_list a;
    for(int i = 0; i < 10; ++i)
        a.insert_head(i);

    a.reverse();
    a.traverse();
}
/*
0
1
2
3
4
5
6
7
8
9
*/

越界問題

insert 和 delete 都有可能導致越界,指針指向位置區域

分析越界的情況

  • index < 0
  • index > 鏈表大小

delete 需要額外注意鏈表是否爲空

重寫 insert_after 和 delete_node

insert_after

在找到前一個車廂的位置時,如果我們發現後面沒有車廂了,那麼暫停檢查(這時已經滿足 pre == tail

while(cnt < index - 1 && pre != tail)

我們停下來看自己數過的車廂是不是到了數

cnt == index - 1

如果到了,那麼說明剛好在車尾,這是唯一符合的情況
不然,因爲不存在數過了的情況,只剩下

cnt < index - 1

這樣說明我們要找的車廂越界了

bool linked_list::insert_after(int index, int _data){
    if(index == 0){
        insert_head(_data);
        return true;
    }

    int cnt = 0;
    node *pre = head->next;// get the previous node of target node
    while(cnt < index - 1 && pre != tail){
        pre = pre->next;
        cnt++;
    }

    // out of range
    if(cnt < index - 1){// CHECK
        cerr << "out of range" << endl;
        return false;
    }

    // if insert after the last node
    if(pre == tail){
        append_node(_data);
        return true;
    }

    node *tmp = new node(_data);
    tmp->next = pre->next;
    pre->next = tmp;
    return true;
}

test

int main()
{
    linked_list a;
    for(int i = 0; i < 10; ++i)
        a.insert_head(i);

    a.insert_after(11, -1);
    a.traverse();
}
/*
out of range
9
8
7
6
5
4
3
2
1
0
*/

delete_node

越界注意,如果我們要去掉的車廂的前一個車廂已經數到了最後一節,即 pre == tail,但是 cnt 仍然小於 index,那麼我們要去掉的車廂是不存在的

另外,加上一個 empty() 空鏈表不可刪的判定

bool linked_list::delete_node(int index){
    if(empty()){
        cerr << "the linklist is empty" << endl;
        return false;
    }

    if(index == 0){
        pop_front();
        return true;
    }

    int cnt = 0;
    node *pre = head, *cur = head->next;// get the previous node of target node
    while(cnt < index && cur != tail){
        pre = pre->next;
        cur = cur->next;
        cnt++;
    }

    // out of range
    if(cnt < index){
        cerr << "out of range" << endl;
        return false;
    }

    // if delete the tail
    if(cur == tail){
        tail = pre;
        tail->next = nullptr;// * notice to change the next ptr
        delete(cur);
        return true;
    }

    pre->next = cur->next;
    delete(cur);
    return true;
}

test

int main()
{
    linked_list a;
    for(int i = 0; i < 10; ++i)
        a.insert_head(i);

    a.delete_node(10);// exceed range
    a.traverse();
}

/*
out of range
9
8
7
6
5
4
3
2
1
0
*/

完整代碼

#include<iostream>

using namespace std;

struct node
{
    int data;
    node *next;

    node()
    {
        next = nullptr;
    }
    node(int _data)
    {
        data = _data;
        next = nullptr;
    }
};

// link list with head node
class linked_list
{
private:
    node *head, *tail;
public:
    linked_list()
    {
        head = new node();
        tail = head;
    }

    void insert_head(int _data);
    bool insert_after(int index, int _data);
    void append_node(int _data);

    void pop_front();
    bool pop_back();
    bool delete_node(int index);

    // have no impact on the linklist
    bool empty();
    void reverse();
    void traverse();
};

void linked_list::insert_head(int _data){
    node *tmp = new node(_data);
    tmp->next = head->next;
    head->next = tmp;

    if(tail == head)
        tail = tmp;
}

void linked_list::append_node(int _data){
    node *tmp = new node{_data};

    tail->next = tmp;
    tail = tail->next;
}

// index start from 0
bool linked_list::insert_after(int index, int _data){
    if(index == 0){
        insert_head(_data);
        return true;
    }

    int cnt = 0;
    node *pre = head->next;// get the previous node of target node
    while(cnt < index - 1 && pre != tail){
        pre = pre->next;
        cnt++;
    }

    // out of range
    if(cnt < index - 1){// && pre == tail
        cerr << "out of range" << endl;
        return false;
    }

    // if insert after the last node
    if(pre == tail){
        append_node(_data);
        return true;
    }

    node *tmp = new node(_data);
    tmp->next = pre->next;
    pre->next = tmp;
    return true;
}

void linked_list::pop_front(){
    node *tmp = head->next;
    head->next = head->next->next;
    delete(tmp);
}

bool linked_list::pop_back(){
    if(empty()){
        cerr << "the linklist is empty" << endl;
        return false;
    }

    node *pre = head, *cur = head->next;// get the previous node of target node
    while(pre->next != tail){
        pre = pre->next;
        cur = cur->next;
    }

    tail = pre;
    tail->next = nullptr;
    delete(cur);
    return true;
}

bool linked_list::delete_node(int index){
    if(empty()){
        cerr << "the linklist is empty" << endl;
        return false;
    }

    if(index == 0){
        pop_front();
        return true;
    }

    int cnt = 0;
    node *pre = head, *cur = head->next;// get the previous node of target node
    while(cnt < index && cur != tail){
        pre = pre->next;
        cur = cur->next;
        cnt++;
    }

    // out of range
    if(cnt < index){
        cerr << "out of range" << endl;
        return false;
    }

    // if delete the tail
    if(cur == tail){
        tail = pre;
        tail->next = nullptr;// * notice to change the next ptr
        delete(cur);
        return true;
    }

    pre->next = cur->next;
    delete(cur);
    return true;
}

void linked_list::reverse(){
    if(empty()){
        cerr << "the linklist is empty" << endl;
        return;
    }

    tail = head->next;

    node *cur = head->next, *next = cur->next;
    while(next != nullptr){
        cur->next = head->next;
        head->next = cur;

        cur = next;
        next = next->next;
    }

    cur->next = head->next;// the same as insert_head
    head->next = cur;

    tail->next = nullptr;
}

bool linked_list::empty(){
    if(head->next == nullptr)
        return true;
    return false;
}

void linked_list::traverse(){
    node *cur = head->next;
    while(cur != nullptr)
    {
        cout << cur->data << endl;
        cur = cur->next;
    }
}

小結

鏈表可以很形象地看做是一列行駛的列車,指針相當於每列車廂上的列車員,她來指引你走向的下一個車廂,如果你沒有及時更新,那麼你將會跳車或者在內存空間中迷路。

單向鏈表沒法找到上家,所以在刪除操作時,需要記錄上家的位置(類似歷史記錄),注意結點指針的修改順序,考慮是否要使用額外的內存空間來防止“脫鉤”的發生。

在刪除操作維護尾指針時,一個常見的錯誤是,修改了尾指針,卻沒有及時更新當前尾指針的 next 域,這會導致在遍歷時訪問已經刪除的結點,及在無垠的內存空間中迷路,tail->next = nullptr 非常重要。

關於雙向鏈表,由於知道了結點上下家,從頭和尾尋找結點都比較方便,只是修改時比較麻煩,之後應該會更新。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章