【数据结构】链表之单向链表

本文介绍了链表的基本概念,特别是单向链表的结构和操作,包括链表的初始化、添加节点(头插法和尾插法)、删除节点、遍历链表以及销毁链表的方法。此外,文章还提到了链表相对于顺序表的优点和缺点,例如在插入和删除操作上的高效性,以及无法立即访问任意节点的限制。
摘要由CSDN通过智能技术生成

一、基本概念

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

学习过顺序表的小伙伴都应该了解,顺序表的存储空间,数据是挤在一起的,对数据进行增删操作时需要将数据成片地移动。而存储空间为离散式的链表就提供了一种解决方案。顺序表和链表在内存上的基本形式如图所示:

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

  1. 单向链表

  1. 单向循环链表

  1. 双向链表

  1. 双向循环链表

这些链表的操作基本相同,区别只在于指针的数目不同,本文先以最简单的单向链表为例,展示链表的基本操作。

对单链表进行操作,首先要了解单链表的结构,单向链表结构示意图:

每一个节点都分为数据域和指针域两部分。其中,Head为单链表的头结点,头结点的数据域不存放数据,指针域指向下一节点。初始化时,头结点的指针域指向空(NULL)。

从头结点的下一个节点开始,才在节点的数据域(Data)存放数据,指针域存放下一节点的地址,通过该地址可以访问该节点的下一个节点,最后一个节点的指针域,指针指向空。

有的单链表头结点的数据域也存放数据,被称为无头结点或不带头结点链表,链栈就是一个不带头结点的单向链表,前面我就写过一篇关于链栈的文章,感兴趣的小伙伴可以去看一下,附上链接:https://blog.csdn.net/weixin_44358957/article/details/129337308

好了,言归正传,接下来我们继续讨论带头结点的单向链表。

二、带头结点的单向链表

一、单向链表的节点设计

单链表的节点包括数据域和指针域两部分,其节点设计如下:

typedef struct node{
    int data;//数据域,这里以整型数据为例,也可以是其他类型
    struct node *next;//指针域,用来存放下一个节点的地址,指向下一个节点
}Node;
Node *SL = NULL;//定义一个单链表,未真正初始化,需要进行初始化分配内存空间才能使用

二、单向链表的初始化

带头结点的单链表,其头结点是不存放数据的,单链表初始化时,只有头结点,所以在初始化时,让其指针域指向空。

bool ListInit(Node* &SL)
{
    //分配空间,如果申请分配内存失败,返回false
    if((SL = (Node *)malloc(sizeof(Node))) == NULL)
        return true;
    
    //内存申请成功
    SL->next = NULL;
    return true;
}

三、判断链表是否为空

判断单链表是否为空,只需要判断单链表的头结点的指针域是否指向空。

bool ListsEmpty(Node* SL)
{        
    //为空,返回true
    if(SL->next ==  NULL)
        return true;
    
    //不为空,返回false
    return false;

    //还有一种更简洁的写法,那就是直接返回
    //return SL->next == NULL;
    
}

四、增加节点

单向链表增加节点的常用方式有两种,分别是头插法、尾插法。

单链表的头插法是指,新的数据节点插入到头结点之后,头结点的下一个节点之前。

//头插法
bool ListHeadAdd(Node* &SL,int data)
{
    //如果链表未初始化,返回false
    if(NULL == SL)
        return false;
    
    //插入新节点需要申请新的节点空间
    Node *newnode = (Node *)malloc(sizeof(Node));
    if(newnode != NULL)//新节点内存申请成功
    {
        newnode->data = data;
        newnode->netx = SL->next;
        SL->next = newnode;
        return true;
    }
    
    //内存未申请成功,返回false
    return false;    
}

单链表的尾插法需要先遍历到链表的最后一个节点,将节点插入到最后一个节点后面。

//尾插法
bool ListTailAdd(Node* &SL,int data)
{
    //如果链表未初始化,返回false
    if(NULL == SL)
        return false;
    
    Node *newnode = (Node *)malloc(sizeof(Node));
    newnode->data = data;

    //遍历整个链表,找到最后一个节点
    Node *p = SL;
    while(p->next != NULL)//该循环结束后,遍历完链表,p指向最后一个节点,p->next为NULL
    {
        p = p->next;
    }
    //遍历到最后一个节点,插入新节点
    newnode->next = p->next;
    p->next = noewnode;
    return true;
}

五、删除节点

删除节点也通常有两种方法,分别是从头部删除节点和从尾部删除节点,删除节点其实只是将节点剔除出链表,对于被剔除的链表节点,只有将其空间释放,才算是彻底删除了节点。

从头部删除节点,即删除头结点的下一个节点

//从头部删除
bool ListHeadDele(Node* &SL)
{
    //未初始化或链表为空,返回false 
    if((SL == NULL) || (SL->next == NULL))
        return false;
    
    //删除节点需要释放空间,所以需要一个临时指针变量指向要被释放的空间
    Node *tmp = SL->next;
    //从链表中剔除tmp节点
    SL->next = tmp->next;
    
    //释放被删除的节点,即真正地删除被剔除的节点
    free(tmp);
    tmp = NULL;//防止tmp成为野指针
    return true;
}

从尾部删除,遍历到尾结点,将尾结点删除

bool ListTailDele(Node* &SL)
{
    //未初始化或链表为空,返回false 
    if((SL == NULL) || (SL->next == NULL))
        return false;
    
    Node *p = SL;
    while(p->next->next != NULL)//该循环结束后,p指向倒数第二个节点,尾结点为p->next
    {
        p = p->next;
    }
    //释放尾节点的空间
    free(p->next);
    
    //剔除尾节点,让倒数第二个节点的指针域指向空,变成新的尾结点,
    p->next = NULL;

    return true;
}

六、遍历链表

遍历链表就是从头结点开始读取链表数据,一直读取到尾结点,这里直接打印链表。

void ListTraverse(Node *SL)
{
    if((SL == NULL) || (SL->next == NULL))
    {
        printf("链表未初始化或链表为空!\n");
        return;
    }
    
    //链表不为空
    Node *tmp = SL;
    printf("链表元素: ");
    while(tmp->next != NULL)//循环结束,tmp指向最后一个元素
    {
        printf("%d ",tmp->data);
        tmp = tmp->next;
    }
    //打印最后一个元素
    printf("%d\n",tmp->data);
}

七、销毁链表

销毁链表,需要遍历链表的每一个节点,将每一个节点的空间释放。

void ListDestroy(Node* &SL)
{
    if(SL == NULL)//如果链表没有初始化,不用销毁
        return;
    
    Node *p = SL,*q = p->next;//q的作用是,在释放当前节点时,记录当前节点的下一个节点
    while(q != NULL)//从头结点开始释放节点空间,一直遍历到尾结点,循环结束,p指向尾结点
    {
        free(p);
        p = q;
        q = p->next;
    }
    //释放尾结点,此时 q = NULL,不需要释放
    free(p);
    p = NULL;//防止成为野指针
}

三、链表的优缺点

链式存储中,所有节点的存储位置是随机的,他们之间的逻辑关系用指针来确定,跟物理存储位置无关,因此从上述示例代码可以很清楚看到,增删数据都非常迅速,不需要移动任何数据。

另外,又由于位置与逻辑关系无关,因此也无法直接访问某一个指定的节点,只能从头到尾按遍历的方式一个个找到想要的节点。简单讲,链式存储的优缺点跟顺序存储几乎是相对的。其优缺点总结如下:

优点:

  1. 插入、删除时无需移动任何数据,只需要对指针进行操作。

  1. 当数据节点的数量较多时,无需一整片较大的连续内存空间,可以灵活利用离散的内存。

  1. 当数据节点数量变化剧烈时,内存的分配和释放灵活,速度快。

缺点:

  1. 在节点中,需要多余的指针来记录节点之间的关联。

  1. 所有数据都是随机存储的,不支持立即访问任意一个节点数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值