单链表删除所有值为x的元素_二、单链表

本文介绍了链表的基本概念,对比了顺序表和链表的优缺点,并详细讲解了单链表的结构及其实现,包括插入、删除和清空操作。通过实例展示了如何在不同位置插入和删除节点,以及如何有效地清空链表。文章还总结了何时选择链表作为数据结构的依据。
摘要由CSDN通过智能技术生成

经过了上篇顺序表的学习,我们大概清楚了顺序表的特性,这篇我们来学习线性表的另一种结构:链表。

1. 链表概述

还记得在顺序表(数组)中,我们插入、删除一个元素,都需要移动大量的元素,来达到顺序排列的目的,所以计算量较大,是否有一种不需要移动元素即能插入或者删除元素的线性表结构呢,答案是有的,那就是链表结构。

链表分为:单链表、循环链表、双向链表、静态链表等。我们先看单链表,结构大致如下图所示。一个链表结点包含有数据域和指针域两部分,前一个结点的指针域存有下一个结点的指针地址,所以就形成了链式数据结构,而最后一个结点的指针域是指向NULL的。注意:一般链表头部都会有一个结点,它不包含数据域,只有指向第一个结点的指针,这个结点叫做头结点,指针叫做头指针。

36c880da5b524a28069d2d71e74e7e0e.png

所以链表的插入和删除是不需要移动元素的,只需要将插入位置的前一个结点的指针指向插入结点,插入结点的指针指向插入位置的下一个结点,说起来很拗口,其实理解起来并不费劲。插入过程如下图所示。

1686da97e203ecb46337b531fb52a07c.png

删除也同理,只需将删除结点的前一个结点直接指向下一个结点即可,然后释放删除结点的内存。过程如下图所示。

16f63845035e09eff29b1a6344ca5e60.png

2. 实现单链表

我们依次来实现单链表的插入、删除、清空。

我们还是沿用顺序表的数据元素ElementType结构体。

typedef struct {
    int id;
    char* name;
}ElementType;

接着定义单链表的“结点”,结点包含一个数据域和一个指针域。

/**定义链表的结点,包含数据域和指针域*/
typedef struct Node {
    ElementType date;   ///数据域
    struct Node* next;  ///指针域
}Node;

然后定义头结点,包含一个头指针和链表的元素个数(length)。可以看到我们将头结点就命名为LinkList,是因为单链表的初始化都是从头结点开始,然后根据指针一个一个地遍历下去的。

/**头结点*/
typedef struct LinkList {
    Node* next; ///头指针(如果链表有头结点,next就指向头结点,没有就指向第一个结点)
    int length; ///链表的长度,初始值为0
}LinkList;

然后我们开始实现插入。插入位置分第一个结点位置、中间位置、尾结点位置。第一个结点不需要遍历链表,直接头结点指向即可。如果插入的不是第一个结点位置,那我们必须遍历链表,找到插入位置的前后结点,然后插入结点,重新设置前后结点的指针域。最后一个结点位置,将插入结点的指针域置为空即可。记得插入后链表长度加一。

/**在指定位置pos处,插入元素element*/
void InsertLinkList(LinkList* linklist, int pos, ElementType element)
{
    ///1、创建空结点并为数据域赋值
    Node* node = (Node*)malloc(sizeof(Node));
    node->date = element;
    node->next = NULL;

    ///2、找到要插入位置的结点
    if (pos == 1)///如果插入的是第一个元素
    {
        linklist->next = node;
        linklist->length++;
        return;
    }
    ///通过循环找到要插入的结点位置
    Node* currNode = linklist->next;///第一个结点
    for (int i = 1; currNode && i < pos - 1; i++)
    {
        ///将下一个结点的地址赋值给当前结点(相当于移动到下一个结点)
        currNode = currNode->next;
    }

    ///3、将结点插入并对接前面的结点
    if (currNode)
    {
        ///必须先后再前,不然currNode->next值被改了
        node->next = currNode->next;///node和后面结点对接
        currNode->next = node;///node和前面结点对接
        linklist->length++;
    }
}

接着实现下删除操作,也同样分删除的是第一个结点位置、中间结点位置、尾结点位置,删除结点后,重新设置前后结点的指针,最后链表长度减一。注意:必须释放删除结点内存。

ElementType DeleteLinkListElement(LinkList* linkList, int pos)
{
    Node* node = linkList->next;//初始化第一个结点(头结点指向第一个结点)
    Node* prevNode = NULL;//删除结点的前一个结点
    ElementType delElement = {};//初始化删除结点数据

    //删除结点超出范围
    if (pos > linkList->length)
    {
        printf("超出数组范围n");
        return delElement;
    }
    //删除第一个结点
    if (pos == 1)
    {
        if (node)
        {
            linkList->next = node->next;//头结点指向第二个结点
            delElement = node->date;//保存删除结点的数据
            free(node);//删除结点,释放内存
            linkList->length--;     //链表长度减一      
        } 
        return delElement;
    }

    //删除除第一个以外的结点
    //循环遍历到要删除的元素处
    for (int i = 1; node && i < pos; i++)
    {
        prevNode = node;//记录上一个结点
        node = node->next;
    }

    //删除元素并返回
    if (node)
    {
        prevNode->next = node->next;//前一个结点指向删除结点的下一个结点
        delElement = node->date;//保存删除结点的数据
        free(node);//删除结点,释放内存
        linkList->length--;//链表长度减一

        return delElement;
    }
    
}

最后实现清空链表。我们使用循环,按指针顺序一个一个的遍历并删除结点,最终删完所有结点,并将链表长度清零,头指针置为空。

/*清空链表*/
void ClearLinkList(LinkList* linkList)
{
    Node* node = linkList->next;//头指针指向第一个结点
    Node* nextNode = NULL;
    while (node)
    {
        nextNode = node->next;
        free(node);
        node = nextNode;
    }
    linkList->length = 0;
    linkList->next = NULL;
}

然后我们实现初始化函数,将结点挨个插入。

/**初始化链表*/
void InitLinkList(LinkList* linklist, ElementType* dataArray, int length)
{
    for (int i = 0; i < length; i++)
    {
        InsertLinkList(linklist, i + 1, dataArray[i]);
    }
}

最后为了测试,我们再实现一个遍历链表并打印的函数。链表的遍历:node = node->next;这是链表遍历中最常用到的代码,这在插入和删除函数中遍历链表时也同样用到。

void PrintLinkList(LinkList* linklist)
{
    Node* node = linklist->next;
    if (!node)
    {
        printf("LinkList is null!n");
        linklist->length = 0;
        return;
    }
    for (int i = 1; i <= linklist->length; i++)
    {
        printf("%dt%sn", node->date.id, node->date.name);
        node = node->next;
    }
}

我们开始测试,在主函数中执行链表测试函数。链表测试函数如下,首先初始化链表,然后删除3号结点,最后清空链表。

void TestLinkList()
{
    LinkList linklist;
    linklist.length = 0;
    InitLinkList(&linklist, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
    printf("初始化列表:n");
    PrintLinkList(&linklist);
    printf("删除后的列表:n");
    ElementType delData = DeleteLinkListElement(&linklist, 3);
    PrintLinkList(&linklist); 
    printf("删除的是:n%dt%sn", delData.id, delData.name); 
    ClearLinkList(&linklist);
    printf("链表清空后n");
    PrintLinkList(&linklist);
}

编译运行,3号结点“山治”被删除了,清空链表后,打出了“LinkList is null!”的字符串。链表插入、删除、清空成功。

625a3f420affc8685b62bf52a5079bca.png

3. 单链表总结

我们简单地对单链表结构和顺序表结构做对比:

存储分配方式:(1)顺序表结构用一段连续地存储单元依次存储线性表的数据元素。

(2)单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素。

时间性能:(1)顺序表查找直接使用下标,速度非常快,时间为0[1]。插入和删除需要平均移动表长一半的元素,时间为0[n]。

(2)单链表查找需要根据指针从头遍历,速度比顺序表慢(根据遍历的次数)时间为n[n]。链表插入和删除时,遍历出插入或删除位置的前后结点,再设置他们的指针,而不需要移动元素,所以时间为0[1]。

空间性能:(1)顺序表结构需要预分配存储空间,分大了,浪费,分小了,容易溢出。

(2)单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制。

所以我们总结:如果线性表需要频繁查找,很少进行插入和删除操作时,我们宜采用顺序表存储结构。如果需要频繁插入和删除时,宜采用单链表结构。当线性表中元素个数变化较大或者根本不知道有多大时,宜采用单链表结构。如果事先知道线性表的大致长度,宜采用顺序表结构。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值