数据结构--单链表

1. 基本概念

  • 顺序表:顺序存储的线性表。
  • 链式表:链式存储的线性表,简称链表。

既然顺序存储中的数据因为挤在一起而导致需要成片移动,那很容易想到的解决方案是将数据离散地存储在不同内存块中,然后在用来指针将它们串起来。这种朴素的思路所形成的链式线性表,就是所谓的链表。

顺序表和链表在内存在的基本样态如下图所示:


 

2. 链表的分类

根据链表中各个节点之间使用指针的个数,以及首尾节点是否相连,可以将链表细分为如下种类:

  1. 单向链表
  2. 单向循环链表
  3. 双向循环链表

这些不同链表的操作都是差不多的,只是指针数目的异同。以最简单的单向链表为例,其基本示意图如下所示:

上图中,所有的节点均保存一个指针,指向其逻辑上相邻的下一个节点(末尾节点指向空)。另外注意到,整条链表用一个所谓的头指针 head 来指向,由 head 开始可以找到链表中的任意一个节点。head 通常被称为头指针

链表的基本操作,一般包括:

  1. 节点设计
  2. 初始化空链表
  3. 增删节点
  4. 链表遍历
  5. 销毁链表

下面着重针对这几项常见操作,讲解单向链表的处理。

3. 单链表节点设计

单向链表的节点非常简单,节点中除了要保存用户数据之外(这里以整型数据为例),只需要增加一个指向本类节点的指针即可,如下所示:

typedef struct node
{
    // 以整型数据为例
    int data; 

    // 指向相邻的下一个节点的指针
    struct node * next;
}node;

4. 单链表初始化

首先,空链表有两种常见的形式。一种是带所谓的头结点的,一种是不带头结点的。所谓的头结点是不存放有效数据的节点,仅仅用来方便操作,如下:

而不带头结点的空链表如下所示:

注意:

  • 头指针 head 是必须的,是链表的入口
  • 头节点是可选的,为了方便某些操作

由于头结点是不存放有效数据的,因此如果空链表中带有头结点,那么头指针 head 将永远不变,这会给以后的链表操作带来些许便捷。

下面以带头结点的链表为例,展示单向链表的初始化的示例代码:

node * initList()
{
    node * head = malloc(sizeof(node));

    // 将头结点的 next 指针置空
    // 但不需要对 data 做任何操作
    if(head != NULL)
        head->next = NULL;

    return head;
}

5. 单链表增删节点

相对于顺序表需要整片移动数据,链表增删节点只需要修改几个相关指针的指向,动作非常快速。

与顺序表类似,可以对一条链表中的任意节点进行增删操作,假设以增删链表首节点为例,示例代码是:

// 将新节点new插入到链表的首部
void insert(node *head, node *new)
{
    new->next  = head->next;
    head->next = new;
}

// 判断链表是否为空
bool isEmpty(node *head)
{
    return head->next == NULL;
}

// 将链表首部节点剔除
node * remove(node *head)
{
    if(isEmpty(head))
        return NULL;

    node * tmp = head->next;

    // 将原链表首节点绕过
    head->next = tmp->next;;
    tmp->next  = NULL;

    return tmp;
}

注意:
删除链表的节点并不意味着释放其内存,而是将其剔除出链表

6. 单链表的遍历

遍历的意思就是逐个访问每一个节点,对于线性表而言,由于路径唯一的选择就是从头走到尾。因此相当而言比较简单。

下面是单向链表的遍历示例代码,假设遍历每个节点并将其整数数据输出:

void listForEach(node * head)
{
    if(isEmpty(head))
        return;

    for(node * tmp=head->next; tmp!=NULL; tmp=tmp->next)
    {
        printf("%d\n", tmp->data);
    }
}

7. 单链表的销毁

由于链表中的各个节点被离散地分布在各个随机的内存空间,因此销毁链表必须遍历每一个节点,释放每一个节点。

注意:
销毁链表时,遍历节点要注意不能弄丢相邻节点的指针

示例代码如下:

void destroy(node * head)
{
    if(isEmpty(head))
        return;

    node *n;
    for(node *tmp = head->next; tmp!=NULL; tmp=n)
    {
        n = tmp->next;
        free(tmp);
    }
}

8. 链表优缺点

链式存储中,所有节点的存储位置是随机的,他们之间的逻辑关系用指针来确定,跟物理存储位置无关,因此从上述示例代码可以很清楚看到,增删数据都非常迅速,不需要移动任何数据。另外,又由于位置与逻辑关系无关,因此也无法直接访问某一个指定的节点,只能从头到尾按遍历的方式一个个找到想要的节点。简单讲,链式存储的优缺点跟顺序存储几乎是相对的。

总结其特点如下:

  • 优点
    1. 插入、删除时只需要调整几个指针,无需移动任何数据
    2. 当数据节点数量较多时,无需一整片较大的连续内存空间,可以灵活利用离散的内存
    3. 当数据节点数量变化剧烈时,内存的释放和分配灵活,速度快
  • 缺点
    1. 在节点中,需要多余的指针来记录节点之间的关联。
    2. 所有数据都是随机存储的,不支持立即访问任意一个随机数据。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

我不吃辣。

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

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

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

打赏作者

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

抵扣说明:

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

余额充值