单链表详细解析,详细图解加代码实现,轻松拿捏。

目录

1. 链表的概念和结构

2.单链表的实现

1.创建结构体变量

2. 实现头插(在链表开始的位置插入数据)

2.1 动态申请节点

3.实现尾插(在链表的末尾插入数据)

4.实现头删(删除链表第一个元素)

5. 实现尾删

6.在链表中查找

7.在pos 位置后插入一个 新节点。

8. 在pos位置前,插入一个节点

9. 打印链表

10. 销毁链表

3. 总结 ,及单链表相关笔试题详解

4. 单链表功能实现源码分享

1. 链表的概念和结构

链表是一种物理存储结构上非连续,非顺序的储存结构。数据元素的逻辑顺序是通过链表中的指针链接次序来实现的。

在数据结构中,链表的结构非常多样,

1.单向,双向

2.带头,不带头

3.循环,非循环

以上的情况组合起来就有8种之多。

本篇博客主要介绍无头单向链表的分析及其功能的模拟实现。这种结构在笔试面试中考察的次数很多。

 

实现单链表,需要掌握的知识主要有,结构体的使用,指针的使用,动态内存开辟,这些在我前面的博客都有分析,需要复习一下的小伙伴可以自行观看。

2.单链表的实现

单链表的的实现需要使用的接口有点多,所以我们为了代码的书写和调试更方便的角度考虑,创造3个部分,包括头文件的包含和函数的声明,函数的具体实现,主测试函数。三个部分。

1.创建结构体变量

在头文件SList.h 中创建结构变量,由于链表的数据元素逻辑顺序是通过指针链接顺序来实现的,因此结构体中的指针变量较为重要。

#include<stdio.h>    
#include<assert.h>   // 断言,判断指针合法性
#include<stdlib.h>  // 动态内存开辟

typedef int SLTDateType;    // 重定义,后续需要更改链表数据类型较为方便

typedef struct SListNode
{
	SLTDateType data;

	struct SListNode* next;  //储存下一个元素的地址

} SListNode;

2. 实现头插(在链表开始的位置插入数据)

先在主函数中,先创建一个空链表。

#include"SList.h"

int main()
{
	SListNode* plist = NULL;

	
	return 0;
}

在插入数据前,我们需要先动态申请一个节点。

2.1 动态申请节点

//  增加数据
SListNode* BuySListNode(SLTDateType x)
{
    SListNode* node = (SListNode*)malloc(sizeof(SListNode));

    if (node != NULL)  //判断是否申请成功
    {
        node->data = x;  
        node->next = NULL;
        return node;
    }
    else
    {
        perror("buySListNode");
    }
}

在申请节点后,我们只知道它数据的大小,并不清楚它后续的节点位置,所以我们选择将每一个申请的节点数据中的指针置空,具体使用时,再赋值。

 我们可以通过这张图,了解单链表在内存中的存储,因为美观,上图看着好像单链表在内存中连续存储,但实际可能是

 理解了这些以后,我们可以尝试实现头插功能。

 

 将原链表的第一个数据的地址赋值给 需要插入的数据存储的地址,再将需要插入的数据的地址赋值给plist。

代码实现

SList.h  (函数申明)

// 头插
void SListPushFront(SListNode** pp, SLTDateType x);

test.c (测试)

#include"SList.h"

int main()
{
	SListNode* plist = NULL;

	SListPushFront(&plist, 1);

	return 0;
}

SList.c  (函数实现)

void SListPushFront(SListNode** pp, SLTDateType x)
{
    SListNode* node = BuySListNode(x);
    if (*pp == NULL)
    {
        *pp = node;
    }
    else
    {
        node->next = *pp;
        *pp = node;
    }
   
}

由于,我们需要对 plist 更改,所以我们选择传址调用,由于plist 为一级指针,所以在函数中,我们使用二级指针来接收。(如果我们传值调用,形参只是实参的一份临时拷贝,对形参的修改不会影响实参)。

如果是一个空链表,我们只需要将创建的数据的地址直接赋给plist。

3.实现尾插(在链表的末尾插入数据)

 我们需要遍历整个链表,找到链表结束的位置,并且将最后一个元素储存的地址(结尾,此时地址为NULL)改为创建的新元素的地址。

void SListPushBack(SListNode** pp, SLTDateType x)
{
    if (*pp == NULL)
    {
        *pp = BuySListNode(x);
    }
    else
    {

        SListNode* tail = *pp;
        while (tail->next != NULL)
        {
            tail = tail->next;
        }
        tail->next = BuySListNode(x);
    }
}

由于我们需要对地址进行解引用,因此,如果是个空链表,对空指针解引用会出现错误,我们需要先判空。

 当tail->next  为NULL 时,代表了,tail这是链表的最后一个元素,我们将新元素的地址赋给tail->next ,即可完成尾插。

4.实现头删(删除链表第一个元素)

 

//头删

void SListPopFront(SListNode** pp)
{
    if (*pp == NULL)  // 空链表
        return;
    else
    {
        SListNode* cur = *pp;  
        (*pp) = (*pp)->next;
        free(cur);
        cur = NULL;
    }

}

如果是一个空链表,我们对一个空指针解引用操作就会报错,我们先要判断链表是否为空,为空则直接返回。

5. 实现尾删

 

创建变量tail ,进入循环,当tail->next 为NULL 时,tail 就是我们需要找到的最后一个节点。

同时,我们需要找到tail 前一个节点(prev),将其存储的地址置成空。

但是如果链表为空,或者链表只有一个节点,我们就找不到尾删最后一个节点的前一个节点。这种就需要分开讨论。

我们在创建变量名时,最好使用英文缩写,这样更有意义,不至于后期查看代码时,忘记变量的意义

//尾删
void SListPopBack(SListNode** pp)
{
    if (*pp == NULL)   //  空链表
    {
        return;
    }
    else if ((*pp)->next == NULL)    //   一个节点
    {
        free(*pp);
        *pp = NULL;
    }
    else
    {                               //     多个节点
        SListNode* prev = NULL;    
        SListNode* tail = *pp;

        while (tail->next != NULL)
        {
            prev = tail;
            tail = tail->next;
        }
      
        free(tail);
        tail = NULL;
        prev->next = NULL;
    }
    
}

6.在链表中查找

这个函数较简单,遍历一遍链表,判断值是否相等,返回地址,注意其返回值的类型

//单链表查找

SListNode* SListFind(SListNode* p, SLTDateType x)
{
    while (p != NULL)
    {
        if (p->data == x)
        {
            printf("找到了\n");
            return p;

            break;
        }
        else
        {
            p = p->next;
        }
    }
    printf("没找到\n");
    return;
}

查找函数对参数没有改变,我们可以直接传值。

7.在pos 位置后插入一个 新节点。

 基本思路:将pos->next 的值 赋给 newnode(新创建的节点)->next , 再将pos->next 赋值为newnode地址。

将pos指向的旧节点位置赋给新创建的节点,然后再将新节点的位置赋给pos ,这样就实现了 链式访问。

但是这只是最普通的情况,我们在设计程序时,还应该考虑到多种情况。

1. pos 地址不合法 (assert断言)

2. pos位置处于链表的尾端(相当于尾插)

//在pos后,增加数据

void SListInsertAfter(SListNode*pos, SLTDateType x)
{
    assert(pos);
    
    SListNode* newnode = BuySListNode(x);
    SListNode* tmp = pos->next;
    pos->next = newnode;
    newnode->next = tmp;

}

8. 在pos位置前,插入一个节点

基本思路,遍历链表,找到pos 位置 前的一个节点,再插入新节点。想对于pos位置后插入新节点,pos位置前更加复杂,需要遍历链表,需要做出的判断也更多。

 需要注意的点

1. pos的合法性

2. pos如果是第一个节点,那么就不存在前一个节点 prev 。


//  在pos 位置前,加入一个数据
void SListInsertBefore(SListNode** pp,SListNode *pos, SLTDateType x)
{
    assert(pos);

    SListNode* newnode = BuySListNode(x);

    if (pos == *pp)   // pos  是第一个节点
    {
        newnode->next = pos;

        *pp = newnode;
    }
    else
    {
        SListNode* cur = *pp;
        SListNode* prev = NULL;
        while (cur != pos)     // cur 为pos 位置时,循环结束
        {
            prev = cur;         // prev 为 pos的前一个节点
            cur = cur->next;
        }
        prev->next = newnode;
        newnode->next = pos;
    }

}

9. 打印链表

遍历链表,打印数值

void SlistPrintf(SListNode* p)
{
    SListNode* cur = p;

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

10. 销毁链表

由于链表的空间都是动态开辟的,我们在使用结束时,应该主动释放


//单链表的销毁

void SListDestroy(SListNode** pp)
{
    SListNode* tmp = NULL;
    while (*pp)
    {
        tmp = *pp;
       *pp = (*pp)->next;
       free(tmp);
    }
}

单链表的销毁同样,会对参数进行改变,我们选择传址调用,遍历链表,一步一步的释放,不同顺序表,只需要释放一次,链表的每一个节点都是单独的内存空间,释放起来相对复杂。

3. 总结 ,及单链表相关笔试题详解

在处理链表相关问题时,可以先构造一个简单的思路来适应大多数普通场景,然后根据这个思路来调整在特殊情境下的使用,要考虑好链表的头尾问题。很多的笔试题面试题,本质上其实都是在考察链表的基本功能,比如增删查改,只是他们的使用场景更加复杂,我们需要考虑的问题更多。

笔试题分析会在下一篇博客进行,敬请期待,先留个坑。

4. 单链表功能实现源码分享

Single linked list/Single linked list · 斯文/Date Stucture - 码云 - 开源中国 (gitee.com)

本篇博客到此结束,谢谢观看。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值