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 非常重要。

关于双向链表,由于知道了结点上下家,从头和尾寻找结点都比较方便,只是修改时比较麻烦,之后应该会更新。

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