络腮胡菲菲 发表于 2025-3-8 11:10:25

次序表与链表·续

引言

本文承接上文(次序表与链表-CSDN博客),开始对链表的要点提炼。前文提到次序表得当需要频仍随机访问且数据量固定的场景,而链表得当需要频仍插入和删除且数据量动态厘革的场景。链表的引入弥补了次序表在动态性和操纵服从上的不敷,是线性表的告急实现方式之一。
链表

概念

   链表是一种物理存储结构上非连续、非次序的存储结构,数据元素的逻辑次序是通过链表   中的指针链接实现的 。            https://i-blog.csdnimg.cn/direct/b20b1cc2410e4078a916528dc2a60f1a.png         链表结构            容易发现链表结构在逻辑上连续,但物理上不一定连续,一样平常链表的节点从堆上申请的,每次申请的空间是否连续是不确定的。   分类

链表的分类可以根据以下维度进行:

[*] 单向或双向:决定节点的指针数量和遍历方向。
[*] 带头或不带头:决定是否有额外的头节点简化操纵。
[*] 循环或不循环:决定链表是否形成环。
通过组合这些维度,可以设计出得当不同场景的链表结构。例如:


[*] 带头单向循环链表:得当实现队列。
[*] 带头双向循环链表:得当需要双向遍历且操纵简化的场景。
  而我们常遇到的主要是不带头单向非循环链表和带头双向循环链表(以下图例分别对应这两种链表)
      https://i-blog.csdnimg.cn/direct/ab4a45e66a6547febdd8356b12095615.png       不带头单向非循环链表            https://i-blog.csdnimg.cn/direct/eea15150a7d94fafbb8d2ac7b25077d4.png       带头双向循环链表        

实现

无头单向非循环链表的简单实现
//"slist.h"
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int SLTDateType;

typedef struct SListNode
{
        SLTDateType data;
        struct SListNode* next;
}SListNode;

// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x)
{
        SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
        if (newnode == NULL)
        {
                perror("malloc fail");
                exit(-1);
        }
        newnode->data = x;
        newnode->next = NULL;
        return newnode;
}

// 单链表打印
void SListPrint(SListNode* plist)
{
        SListNode* cur = plist;
        while (cur != NULL)
        {
                printf("%d->", cur->data);
                cur = cur->next;
        }
        printf("NULL\n");
}

// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
        SListNode* newnode = BuySListNode(x);

        if (*pplist == NULL) {
                *pplist = newnode;
        }
        else
        {
                SListNode* tail = *pplist;
                while (tail->next!=NULL)
                {
                        tail = tail->next;
                }
                tail->next = newnode;
               
        }

}
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
        SListNode* newnode = BuySListNode(x);
        newnode->next = *pplist;
        *pplist = newnode;

}


// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
        assert(pplist);
        assert(*pplist);

        //一个节点
        if ((*pplist)->next == NULL) {
                free(*pplist);
                *pplist = NULL;
        }

        //多个节点
        SListNode* tail = *pplist;
        while (tail->next->next!=NULL)
        {
                tail = tail->next;
        }
        free(tail->next);
        tail->next = NULL;

}
void SListPopFront(SListNode** pplist) {
        // 防御性检查:拦截非法输入
        if (pplist == NULL) {
                fprintf(stderr, "Error: Invalid pointer in SListPopFront\n");
                return;
        }

        // 业务逻辑检查:空链表直接返回
        if (*pplist == NULL) {
                return;
        }

        SListNode* tmp = *pplist;
        *pplist = tmp->next;
        free(tmp);
}

// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
        SListNode* cur = plist;
        while (cur) {
                if (cur->data == x) {
                        return cur;
                }
                cur = cur->next;
        }
        return NULL;
}
// 单链表在pos位置之后插入x
// 分析思考为什么不在pos位置之前插入?

// 因为没有前置指针
// 若要在 pos 之前插入,必须从头节点开始遍历链表找到 pos 的前驱节点,时间复杂度为 O(n)
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
        if (pos == NULL) return;
        SListNode* newNode = BuySListNode(x);
        newNode->next = pos->next;
        pos->next = newNode;
}
// 单链表删除pos位置之后的值
// 分析思考为什么不删除pos位置?

//删除 pos 节点需要知道其前驱节点,而单链表无法直接获取前驱节点
// 必须从头遍历链表,时间复杂度为O(n),删除 pos 之后的节点只需修改 pos->next,时间复杂度为O(1)。

void SListEraseAfter(SListNode* pos)
{
        if (pos == NULL || pos->next == NULL) return;
        SListNode* tmp = pos->next;
        pos->next = tmp->next;
        free(tmp);
}

// 在pos的前面插入
void SLTInsert(SListNode** pphead, SListNode* pos, SLTDateType x)
{
        assert(pphead);
        assert(pos);
        assert(*pphead);
        if (*pphead == pos) SListPushFront(pphead,x);
        else
        {
                SListNode* prev = *pphead;
                while (prev->next!=pos)
                {
                        prev = prev->next;
                }
                SListNode* newnode=BuySListNode(x);
                prev->next = newnode;
                newnode->next = pos;

        }
}

// 删除pos位置
void SLTErase(SListNode** pphead, SListNode* pos)
{
        assert(pphead);
        assert(*pphead);
        assert(pos);

        if (*pphead == pos)
        {
                // 头删
                SListPopFront(pphead);
        }
        else
        {
                SListNode* prev = *pphead;
                while (prev->next != pos)
                {
                        prev = prev->next;
                }
                prev->next = pos->next;
                free(pos);
                pos = NULL;
        }
}

void SLTDestroy(SListNode** pphead)
{
        assert(pphead);
        SListNode* cur = *pphead;
        while (cur) {
                SListNode* tmp = cur;
                cur = cur->next;
                free(tmp);
        }
        *pphead = NULL;
} 关键点说明


[*] 二级指针的利用:

[*] 修改头指针时(如插入/删除头节点),需通过二级指针 pplist 修改一级指针 *pplist。
[*] 示例:SListPushFront 和 SListPopFront 直接修改头指针。

[*] 边界条件处理惩罚:

[*] 空链表、单节点链表、尾节点/头节点操纵需特殊处理惩罚。
[*] 例如 SListPopBack 中需处理惩罚链表只有一个节点的环境。

[*] 时间复杂度:

[*] 头插/头删:O(1)/O(1)
[*] 尾插/尾删:O(n)/O(n)
[*] 插入/删除指定位置:平均 O(n)/O(n)(需遍历找到位置)

[*] 内存管理:

[*] 每次 malloc 后需检查内存分配是否成功。
[*] 删除节点后需及时 free 防止内存泄漏。

  若需要频仍在恣意位置前插入或删除,最好利用双向链表,它可以通过前驱指针直接操纵,时间复杂度为 O(1)/O(1)。

https://i-blog.csdnimg.cn/direct/1de05e5091784857b6eff2827202573a.png

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 次序表与链表·续