单链表SList

本文介绍了单链表的基本结构和操作,包括创建节点、尾部插入、头部插入和删除、查找节点等功能。讨论了单链表在内存管理和操作效率上的特点,并提供了相应的C语言实现代码。同时,文章强调了在处理链表时对空指针的检查以及在特定位置插入和删除节点时的时间复杂度问题。
摘要由CSDN通过智能技术生成

链表是针对顺序表的缺陷而设计的。

1.顺序表扩容存在一定的空间浪费——链表按需扩容

2.头部和中间的删除和插入都需要挪动数据,时间复杂度O(N)——链表物理空间不连续,不用挪动


单链表SList(Single List)


通过下面对单链表的实现,可以发现单链表的缺陷也有很多,但学单链表还是有很大意义的:

链表笔试面试题,基本都是单链表结构;
单链表以后会作为其他数据结构的子结构,如哈希桶、图的邻接表。


基本结构

链表是由一个个结点链接而成,每个结点包含:1.存储的数据;2.指向下一个结点的指针。
微信图片_20220707151438.png

//.h头文件中
typedef int SLTDataType;

typedef struct SListNode{ //Node:结点
    SLTDataType data;
    struct SListNode* next;
}SLTNode;
void Test1(){ //简单写几个结点并链接起来成为一条链表
    SLTNode* n1 = (SLTNode*)malloc(sizeof(SLTNode));
    assert(n1);
    SLTNode* n2 = (SLTNode*)malloc(sizeof(SLTNode));
    assert(n2);
    SLTNode* n3 = (SLTNode*)malloc(sizeof(SLTNode));
    assert(n3);
    SLTNode* n4 = (SLTNode*)malloc(sizeof(SLTNode));
    assert(n4);

    n1->data = 1;
    n2->data = 2;
    n3->data = 3;
    n4->data = 4;

    n1->next = n2;
    n2->next = n3;
    n3->next = n4;
    n4->next = NULL;

    SListPrint(n1);//打印
}


打印链表数据

微信图片_20220707151752.png

void SListPrint(SLTNode* phead){//phead:第一个结点的地址
    //assert(phead != NULL);

    SLTNode* cur = phead;//保留头部指针,只让cur指针移动。cur:current,表示当前指针。

    while (cur != NULL){
        printf("%d->", cur->data);
        cur = cur->next;
    }
    printf("NULL\n");
}


新建结点


由于多个接口都需要新建结点,故将此逻辑封装为另外的接口,方便代码复用。

SLTNode* CreatSListNode(SLTDataType x){
    SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
    assert(newNode != NULL);
    newNode->data = x;
    newNode->next = NULL;
    return newNode;
}


尾插数据


尾插数据接口有两种情况:

链表尚未有结点;——我们直接给它一个结点即可
链表已有结点。——让尾结点的next指向新结点即可
在第一种情况下,由于改变了指针变量,因此接口接收的参数应该为二级指针,否则,形参的改变不影响实参。

第二种情况的思路:
微信图片_20220707151921.png

void SListPushBack(SLTNode** pphead,SLTDataType x){
    //因为需要对指向结点的指针进行改变,所以要用到二级指针。
    assert(pphead != NULL);

    SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
    assert(newNode != NULL);
    newNode->data = x;
    newNode->next = NULL;

    if (*pphead == NULL){ //当传入空指针时
        *pphead = newNode;
    }
    else{
        SLTNode* tail = *pphead;
        while (tail->next != NULL){
            tail = tail->next;
        }
        tail->next = newNode;
    }
}

void test(){
    SLTNode* n1 = NULL;
    SLTNode* n2 = (SLTNode*)malloc(sizeof(SLTNode));
    assert(n2);
    SListPushBack(&n1,666); //要想改变n1,就得传n1的地址
    SListPushBack(&n2,666);
}


头插数据


头插也是两种情况。

微信图片_20220707152130.png

void SListPushFront(SLTNode** pphead, SLTDataType x){
    assert(pphead != NULL);

    SLTNode* newNode = CreatSListNode(x);

    newNode->next = *pphead;
    *pphead = newNode;
}


头删数据

void SListPopFront(SLTNode** pphead){
    assert(pphead != NULL);
    assert(*pphead != NULL);

    SLTNode* next = (*pphead)->next;
    free(*pphead);
    *pphead = next;
}


尾删数据


不能简单粗暴地直接free掉尾结点,否则造成倒数第二个结点的next指向一个不存在的空间(野指针)。

可以用到双指针的思想。

但同时要注意双指针容易造成空指针的问题,比如下面的代码,如果只考虑多个结点的情况,当链表只剩下1个结点时,preTail->next = NULL会报空指针的错误,因为preTail此时是空指针,对空指针解引用->会出错。
微信图片_20220707152308.png

void SListPopBack(SLTNode** pphead){
    assert(pphead != NULL);
    assert(*pphead != NULL);

    //链表只有1个结点
    if ((*pphead)->next == NULL){
        free((*pphead)->next);
        *pphead = NULL;
    }
    else{//链表有多个结点
        SLTNode* tail = *pphead;
        SLTNode* preTail = NULL;
        while (tail->next != NULL){
            preTail = tail;
            tail = tail->next;
        }
        free(tail);    
        preTail->next = NULL;
    }
}
//另一种写法
void SListPopBack(SLTNode** pphead){
    assert(*pphead != NULL);
    //链表只有1个结点
    if ((*pphead)->next == NULL){
        free((*pphead)->next);
        *pphead = NULL;
    }
    else{//链表有多个结点
        SLTNode* tail = *pphead;
        while(tail->next->next != NULL){
            tail = tail->next;
        }
        free(tail->next);
        tail->next = NULL;
    }
}


查询数据


单链表中的查找,返回值设置成SLTNode的指针,这样就可以利用返回值进行:

修改找到的值;
配合Insert和Erase接口使用;

SLTNode* SListFind(SLTNode* phead, SLTDataType x){
    assert(phead != NULL);
    SLTNode* cur = phead;

    while (cur){
        if (cur->data == x)
            return cur;
        else
            cur = cur->next;
    }
    return NULL;
}

//test.c
void Test3(){
    SLTNode* n1 = NULL;
    SListPushFront(&n1, 3);
    SListPushFront(&n1, 2);
    SListPushFront(&n1, 3);
    SListPushFront(&n1, 3);
    SListPushFront(&n1, 1);
    SListPrint(n1);

    SLTNode* ret = SListFind(n1, 3);
    if (ret){
        printf("找到了,把它修改为6\n");
        ret->data = 6;
    }
    else{
        printf("没找到\n");
    }
    SListPrint(n1);
}


在定义接口时,如果指针一定不允许为空,但用户有可能传空值的时候,记得assert(ptr)断言一下,这样如果用户误传了个NULL的指针过来,程序能很快找到错误地点,方便排查错误。


在特定位置前插入新结点


配合查询SListFind( )使用。

微信图片_20220707152552.png

//任意位置插入(在pos结点前插入新结点,数据为x)
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x){
    assert(pphead != NULL);
    assert(pos != NULL); //检查了pos不为空,就相当于链表不是空的。

    if (pos == *pphead){ //头插
        SListPushFront(pphead, x);
    }
    else{
        //找到pos结点的前一个结点
        SLTNode* prev = *pphead;
        while (prev->next != pos){
            prev = prev->next;
        }

        SLTNode* newNode = CreatSListNode(x);
        prev->next = newNode;
        newNode->next = pos;
    }
}

void Test4(){
    SLTNode* n1 = NULL;
    SListPushFront(&n1, 5);
    SListPushFront(&n1, 4);
    SListPushFront(&n1, 3);
    SListPushFront(&n1, 2);
    SListPushFront(&n1, 1);
    SListPrint(n1);//1->2->3->4->5->NULL

    SLTNode* pos = SListFind(n1, 4);
    SListInsert(&n1,pos,666);
    SListPrint(n1);//1->2->3->666->4->5->NULL

}


删除特定位置结点

这个也是要配合查询SListFind( )使用。也是分为头删和非头删两种情况。

void SListErase(SLTNode** pphead, SLTNode* pos){
    assert(pphead);
    assert(pos); //检查了pos不为空,就相当于链表不是空的。

    if (pos == *pphead){
        SListPopFront(pphead);
    }
    else{
        //找到pos结点的前一个结点
        SLTNode* prev = *pphead;
        while (prev->next != pos){
            prev = prev->next;
        }
        prev->next = pos->next;
        free(pos);
    }
}


可以看到,在pos位置之前插入/删除数据,需要遍历一遍链表以找到pos结点的前一个结点,时间复杂度是O(N),没有解决顺序表挪动数据的问题。
因此在特定位置前插入/删除,不适合用单链表解决。
在C++官方实现的STL中的单链表forward_list中实现的插入删除,都是在pos位置之后插入/删除。



在特定位置后插入新结点


时间复杂度O(1)

void SListInsertAfter(SLTNode* pos, SLTDataType x){
    assert(pos);
    SLTNode* newNode = CreatSListNode(x);
    newNode->next = pos->next;//注意顺序
    pos->next = newNode;
}


删除特定位置后一个结点


时间复杂度O(1)

void SListEraseAfter(SLTNode* pos){
    assert(pos);
    if (pos->next == NULL){
        return;
    }
    else{
        SLTNode* tmp = pos->next->next;
        free(pos->next);
        pos->next = tmp;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

裙下的霸气

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

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

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

打赏作者

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

抵扣说明:

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

余额充值