数据结构:链表及其C++实现

前言

链表是一种非常非常基础的数据结构,本文首先讲解链表的基础知识,然后使用C++的模板实现了一个链表类,并简单实现了常见的插入、删除、查找等算法。

阅读本文需要对C/C++的指针具有一定的了解。

基础知识

链表是一种逻辑上连续,内存上分散的线性表数据结构,其基本单位为结点,每个结点分数据区和指针区,数据区用于存放数据,指针区则用于指向其他结点,通过指针每个结点就被串接成了一条“链子”。

单链表

最基本的单链表结构如下图所示:

单链表图示

单链表每个结点包含一个指针,该指针指向下一个结点,最后一个结点的指针则为NULL,通常也会通过NULL判断是否到达链表的尾部。因此,单链表无法“回头”,只能向前遍历,不能向后遍历。假设需要访问结点D的数据,就需要将头指针head赋值给一个指针p,然后让p依次前移两次,此时p指向D结点,就可以通过指针p访问D结点的数据。所谓前移即如下操作:

p = p->next

插入操作

假如需要在C结点后面插入F结点,只需要使C结点的指针指向F,F结点的指针指向结点D即可,如图所示

需要注意的是,在具体实现的时候,需要先暂存C结点的指针,先将其赋值给F结点指针,然后再将F结点的地址赋值给C结点的指针,否则会丢失D结点的地址,链表就会在此处断开。

删除操作

假如需要删除D结点,则直接让C结点的指针指向E结点即可,如图所示

以上图片素材来源于《代码随想录:链表理论基础》,原文链接,侵删。

与数组的对比

相比于数组,链表存在如下优势:

  • 不要求连续的储存空间,且链表的长短随时可变,空间利用率高
  • 因为链表的线性特征是利用指针实现的,所以链表的插入和删除都非常方便,只需要修改节点的指针即可,不需要像数组一样需要挪动元素的位置。

当然,天下没有白吃的午餐,相比数组,链表也存在如下劣势:

  • 数据访问效率低,需要移动指针找到想要的数据,不像数组可以直接通过索引获取数据。
  • 实现更复杂,且需要多余的内存用于存放指针

因此,到底是使用链表还是数组,应该根据应用场景与需求选择

代码实现

本文利用C++面向对象的特性与模板实现了一个链表类,并实现了插入、删除、查找、拷贝构造、拷贝赋值等基本操作。出于篇幅考虑,完整代码放在了我的github,点击这里查看,本文仅讲解插入、删除、查找部分的代码,并阐述链表对象的析构思路,毕竟C++申请的内存还是需要手动回收的。

结点类实现

// 链表节点类
template<typename T>
class node
{
public:
    node() : next(nullptr){}
    node(T val) : data(val), next(nullptr) {}
private:
    T data;
    node* next;
    friend class list<T>;
};

链表的结点类node如上,非常简单,就是每个结点一个存放数据的成员data和一个指针next,同时在node类中将链表类list声明为友元,便于访问node的成员。

链表类声明

链表类list的声明如下,主要实现了以下操作。list类包含两个成员属性head_ptr和length,前者是链表的头指针,后者储存链表的长度。

template<typename T>
class list
{
public:
    list(); // 构造函数
    list(const list<T>& l); // 拷贝构造
    list<T>& operator= (const list<T>& l); // 拷贝赋值 
    void insert_node(int index, T val); // 在index处插入结点
    void del_node(int index); // 删除index处结点
    T get_node(int index); // 获取index处结点值
    int find(int value); // 查找值value,找到返回index,找不到返回-1
    int get_length(); // 获取链表长度
    void push_back(T val); // 在链表尾部插入数据
    ~list(); // 析构函数

private:
    node<T>* head_ptr; // 链表头指针
    int length; // 链表长度
};

插入实现

对于插入操作,本文将其分为了三种情况

  • 超出索引,抛出异常
  • 插在空链表的头
  • 一般情况
// 在index处插入结点
template<typename T>
void list<T>::insert_node(int index, T val)
{
    if((index > this->length)) // 超过索引,最多可以插到当前结点的下一个结点,否则就是超过索引
    {
        throw runtime_error("index out of this list`s range");
    }
    else if((this->head_ptr == nullptr) && (index == 0)) // 插在空链表的头
    {
        this->head_ptr = new node<T>;
        this->head_ptr->next = nullptr;
        this->head_ptr->data = val;
        this->length++;
    } 
    else // 一般情况
    {
        node<T>* p1 = this->head_ptr;
        node<T>* p2 = new node<T>;
        for(int i = 0; i < index - 1; i++)
        {
            p1 = p1->next;
        }
        p2->data = val;
        p2->next = p1->next;
        p1->next = p2;
        this->length++;
    }
}

删除实现

删除操作需要注意的是,每个结点都是通过new在堆区申请的内存,因此删除节点需要手动释放其内存。

// 删除index处结点
template<typename T>
void list<T>::del_node(int index)
{
    node<T>* p1 = this->head_ptr;
    node<T>* p2 = nullptr;
    for(int i = 0; i < index - 1; i++)
    {
        p1 = p1->next;
    }
    p2 = p1->next->next;
    delete p1->next;
    p1->next = p2;
    this->length--;
}

索引实现

// 获取index处结点值
template<typename T>
T list<T>::get_node(int index)
{
    if(index > this->length - 1) // 超过索引
    {
        throw runtime_error("index out of this list`s range");
    }
    
    node<T>* p1 = this->head_ptr;
    for(int i = 0; i < index; i++)
    {
        p1 = p1->next;
    }

    return p1->data;
}

查找实现

// 查找值value,找到返回index,找不到返回-1
template<typename T>
int list<T>::find(int value)
{
    node<T>* p1 = this->head_ptr;
    for(int i = 0; i < this->length; i++)
    {
        if(p1->data == value)
        {
            return i;
        }
        p1 = p1->next;
    }

    return -1;
}

析构函数

析构函数需要做的就是释放链表每个节点的内存。

// 析构函数
template<typename T>
list<T>::~list()
{
    // 清空链表
    node<T>* p1 = this->head_ptr;
    node<T>* p2 = p1->next;
    while(p2 != nullptr)
    {
        delete p1;
        p1 = p2;
        p2 = p2->next;
    }
    delete p1;
    this->length = 0;
    this->head_ptr = nullptr;

} 
posted @ 2022-05-26 15:19  菜鸡刘  阅读(805)  评论(0编辑  收藏  举报