基础数据结构:链表

链表是相对顺序表来说复杂一些些的数据结构,但其与顺序表在性质上是存在一定的互补的,我们一起来随着这篇文章来看看。

首先来了解基本的链表概念

  • 链表在逻辑结构上是连续的,物理结构上则为非连续的 链表也分为静态与动态链表
    (只是静态链表如今已经不常用)
  • 链表本质就是数据元素之间通过指针相互链接了起来(故分为了数据域和指针域)
  • 其次是链表的分类,链表根据是否带“哨兵卫”的头结点,是否可循环,是双向还是单向大体分为三大类,这三大类互相排列组合共能组合出八种链表结构。

本篇就带大家来看看“最简单”和“最复杂”的链表结构是如何实现的
最简单的即是:无头单向不循环的链表(也就是常见的单链表,类似于STL中的forward_list)
最复杂的即是:带头双向循环链表(也就是STL中的list容器)

八种链表结构中最常用的即这两种,这两种懂了之后其它形式的链表结构也是一通百通的

如果单链表您已理解,不妨可以再去看看双向带头循环链表->双向带头循环链表


单链表(singleList)

单链表的每个结点形式

typedef int SLTDataType;

typedef struct SLNode
{
    SLTDataType val;
    struct SLNode *next;
}SLNode;

该结点内仅包含了一个数据(数据域),以及一个指针用于存储下一个结点的地址(指针域)tips:该单链表也是以存储整型数据来进行实现

在单链表处,我们不进行具体的链表初始化操作

我们仅在调用时创建一个“头指针”,用于存储头结点的地址(以后操作这个单链表时都可以通过头指针来操作)。如下:

//存放头结点的地址
    SLNode *plist = nullptr;

需要注意的是我们创建的是一个指针,在进行函数传参时需要传递该指针的地址(也就是二级指针)才能真正的操控我们这里创建的这个头指针。(因为我们都知道,传参时,形参只是实参的一份临时拷贝,那在函数内部操作的仅是一个拷贝指针,出了作用域即被回收)

介绍完单链表的结点以及基本的起始阶段,接下来就是对单链表的增删查改功能的实现啦

 在进行增加结点时,因为链表典型特征之一即何时用何时申请,故添加数据时都需要进行下述buy_newnode()的操作来获取新结点才可对链表进行数据插入

SLNode* buy_newnode(SLTDataType x)
{
    SLNode *newnode = (SLNode*)malloc(sizeof(SLNode));
    newnode->val = x;
    newnode->next = NULL;
    return newnode;
}

紧接着便是添加数据了:push_back()

void push_back(SLNode** pplist,SLTDataType x)
{
    assert(pplist);
    SLNode *newnode = buy_newnode(x);
    if(*pplist == NULL)
        *pplist = newnode;
    else 
    {
        SLNode *tail = *pplist;
        while(tail->next)
            tail = tail->next;
        tail->next = newnode;
    }
}

尾插数据时,需要先找到尾部,故此每次尾插的效率都为O(N),比较低下

tips:尾插数据时即要注意了,在链表内没有数据时,头结点还并未存在(nullptr的状态),此时要将要插入的结点作为头结点进行赋值,否则则会造成对空指针进行引用(紧接着程序崩溃~)

除了尾插数据,自然还有头插数据:push_front()

void push_front(SLNode** pplist,SLTDataType x)
{
    assert(pplist);
    SLNode *newnode = buy_newnode(x);
    newnode->next = *pplist;
    *pplist = newnode;
}

头插数据就比较容易了,效率也较高,因为我们时刻都把握着头结点,直接进行插入操作即可,然后记得将头结点更新即可。

插入了数据之后,即是应该实现删除数据的功能了

尾删数据:pop_back()

void pop_back(SLNode** pplist)
{
    assert(pplist);
    if(*pplist == NULL)
    {
        printf("当前链表无数据!\n");
        return ;
    }
    if((*pplist)->next == NULL)
    {
        free(*pplist);
        *pplist = NULL;
    }
    else 
    {
        SLNode *cur = *pplist;
        SLNode *prev = NULL;
        while(cur->next)
        {
            prev = cur;
            cur = cur->next;
        }
        free(cur);
        prev->next = NULL;
    }
}

 尾删相对考虑的地方会更多一些

  • 链表没有数据(头结点为空)
  • 链表只有一个结点(只剩下了头结点)
  • 普通情况(链表有一个以上的结点)

根据这三种情况我们要分别处理,如代码所示:

如果链表无结点则直接提示用户并返回即可;

如果链表只有头结点,我们直接free掉头指针并置空即可;

如果链表有两个及以上的结点时,则找到尾部数据的前一个再对尾部数据进行删除即可


头删数据:pop_front()

void pop_front(SLNode** pplist)
{
    assert(pplist);
    if(*pplist == NULL)
    {
        printf("当前链表无数据!\n");
        return ;
    }
    SLNode *next = (*pplist)->next;
    free(*pplist);
    *pplist = next;
}

头删数据就比较简单了,只用考虑链表是否有数据即可。


最后就是查找以及修改了

查找数据:find()

SLNode* Find(SLNode** const pplist,SLTDataType x)
{
    assert(pplist);
    SLNode *cur = *pplist;
    while(cur)
    {
        if(cur->val == x)
            return cur;
        cur = cur->next;
    }
    printf("未找到该数据!\n");
    retun nullptr;
}

 比较简单,直接遍历整个单链表即可,若没找到则直接提示即可

修改数据:modify()

修改数据比较简单,即find找到了该结点直接进行修改即可,故不做代码展示了

然后还有随机插入以及随机删除

随机插入:insert()

void insert(SLNode** pplist,SLNode *pos,SLTDataType x)
{
    assert(pplist);
    SLNode *newnode = buy_newnode(x);
    if(*pplist == pos)
        push_front(pplist,x);
    SLNode *cur = *pplist;
    SLNode *prev = NULL;
    while(cur != pos)
    {
        prev = cur;
        cur = cur->next;
    }
    prev->next = newnode;
    newnode->next = pos;
}

 插入也是属于添加数据,所以也是要先“buy_newnode()”获取新结点,然后找到要插入的位置,再将其连接上即可。

随机删除:erase()

void erase(SLNode** pplist,SLNode** pos)
{
    assert(pplist);
    if(*pplist == *pos)
        pop_front(pplist);
    if(*pos == NULL)
    {
        printf("该结点不存在!\n");
        return ;
    }
    SLNode *cur = *pplist;
    SLNode *prev = NULL;
    while(cur != *pos)
    {
        prev = cur;
        cur = cur->next;
    }
    prev->next = cur->next;
    free(*pos);
    *pos = NULL;
}

删除时也是注意特殊情况:当链表没有数据时要进行判断即可,然后即是遍历找到该结点及其前端结点,然后前端连接后续,再free掉删除结点即可。


销毁链表:destroy()

void destroy(SLNode **pplist)
{
    assert(pplist);
    if(*pplist == NULL)
    {
        printf("当前链表无数据!\n");
        return ;
    }
    SLNode *cur = *pplist;
    SLNode *next = (*pplist)->next;
    while(next)
    {
        free(cur);
        cur = next;
        next = next->next;
    }
    *pplist = NULL;
    printf("销毁成功!\n");
}

由于结点都是动态开辟的,所以销毁链表时需要遍历到每一个结点进行释放才行(还是相对来说比较麻烦的)


到这里,模拟实现无头单向不循环链表就告一段落了,双向带头循环链表将在下一篇中为大家展现。如果本篇文章对你有帮助的话,请顺手点个赞以及收藏吧~ 

链表的下篇:双向带头循环链表

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

c.Coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值