单链表(无头单项非循环)

简介: 链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。链表的形式有很多,本篇文章主要介绍的是单链表且无头结点。在严版数据结构(C语言 第2版)中,单链表采用的是有头节点,这两种形式,各有利弊。含头节点的单链表在学习时,可能会容易些,但是在实践中或者在力扣中做题时,很少会有带头节点。但是有时候做题,使用带头节点的单链表会简单许多,不常见。

前言

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。链表的形式有很多,本篇文章主要介绍的是单链表且无头结点。在严版数据结构(C语言 第2版)中,单链表采用的是有头节点,这两种形式,各有利弊。含头节点的单链表在学习时,可能会容易些,但是在实践中或者在力扣中做题时,很少会有带头节点。但是有时候做题,使用带头节点的单链表会简单许多,不常见。

概述

在这里插入图片描述

一个结点有两个部分组成,数据域(val),指针域(next)。

链表的实现

初始化

在无头单项非循环链表中,需要声明一个数据域和指针域,指针域指向的是下一个节点的地址,数据域是当前节点的数据。这里需要注意的是,在声明指针域是,因为是结构体指针类型,所以一定要写全了。

typedef int SLNDataType;

//初始化
typedef struct SListNode
{
   
   
    SLNDataType val;    //指针域
    struct SListNode* next; //指针域
}SLNode;

遍历单链表

遍历单链表,就是从单链表的第一个节点一直访问到单链表的最后一个节点。
形参接收到的是单链表的第一节点,然后需要用指针去维护节点。通过改变指针的指向,从而实现访问不同的节点。
当cue不为空指针时,继续访问下一个节点,操作为:==cur=cur->next==
当cur为空时,已经访问结束,无需继续遍历。
需要注意的是,这里的判断条件是cur为不为空,而不是cur->next为不为空,如果去判断cur->next,那么当cur->next==NULL时,访问的是最后一个节点为空,此时还没有遍历结束。

void SLTprintf(SLNode* phead)
{
   
   
    SLNode* cur = phead;
    while (cur)
    {
   
   
        printf("%d->", cur->val);
        cur = cur->next;
    }
    printf("NULL\n");
}

在这里插入图片描述

创建新节点

用malloc函数,在堆上申请一片空间来存储数据,当newcode为空时,表示开辟失败。

//创建新节点
SLNode* CreateNode(SLNDataType x)
{
   
   
    SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
    if (newnode == NULL)
    {
   
   
        perror("malloc fail");
        exit(-1);
    }

    newnode->val = x;
    newnode->next = NULL;
    return newnode;
}

尾插

尾插又称后插法,顾名思义,是将新节点逐个插入链表的尾部来创建链表。每次申请一个新节点,读入相应的数据元素值,同时需要一个尾指针指针链表的尾节点(tail)。
需要注意的是,当链表为空时,直接将新创建的节点当作第一个节点。尾插是封装在函数里实现的,对于函数来说,形参的改变不会改变形参,因此需要传递地址,即址传递,因此需要一个二级指针接收。
pphead是头指针(plist)的地址,*pphead是对它进行解引用。

void SLTPushBack(SLNode** pphead, SLNDataType x)
{
   
   
    SLNode* newnode = CreateNode(x);

    if (*pphead == NULL)
    {
   
   
        *pphead = newnode;
    }
    else
    {
   
   
        // 找尾
        SLNode* tail = *pphead;
        while (tail->next != NULL)
        {
   
   
            tail = tail->next;
        }

        tail->next = newnode;
    }
}

空链表和链表不存在的区别:

  • 空链表: 头指针为空,也就是plist==NULL表示是一个空链表
    在遍历、尾插、头插时允许空链表存在,在头删、尾删中不允许出现
  • 链表不存在: ppead==NULL,表示链表不存在,这种情况不允许存在的,因此,每次都需检查一下链表存不存在。

头插

头插法即前插法,逐个将新节点插入到链表的头部来创建,每次申请一个新节点,读入相应的数据元素值。传递的也是二级指针,将新节点的头节点给newnode->next,将newhead变成头节点。

//头插
void SLTPushFront(SLNode** pphead, SLNDataType x)
{
   
   
    SLNode* newnode = CreateNode(x);
    newnode->next = *pphead;
    *pphead = newnode;
}

尾删

尾删是将最后一个节点给释放掉,同时还需要保存倒数第二个节点,将它的指针域存放的地址改成NULL。需要两个指针,一个找尾,一个找倒数第二个节点,同时遍历。
空链表不能删,链表中只有一个节点的链表删除后会变成一个空链表,改变头指针需要存放地址,形参也是一个二级指针。

void SLTPopBack(SLNode** pphead)
{
   
   
    assert(*pphead);
    assert(pphead);
    if ((*pphead)->next == NULL)
    {
   
   
        free(*pphead);
        *pphead = NULL;
    }
    else
    {
   
   
        SLNode* tail = *pphead;
        while (tail->next->next != NULL)
        {
   
   
            tail = tail->next;
        }

        free(tail->next);
        tail->next = NULL;
    }
}

头删

头删需要改变的是头指针的地址,因此传的也是地址。tous需要考虑链表是否为空,如果是空链表就不能操作了,因此需要先断言。在删除头节点的时候,需要先保存一下头节点,否则释放了头节点,就找不到原来的头节点了。

//头删
void SLTPopFront(SLNode** pphead)
{
   
   
    assert(pphead);
    assert(*pphead);

    SLNode* tmp = *pphead;
    *pphead = (*pphead)->next;
    free(tmp);
}

单链表的查找

单链表的查找实际上就是遍历单链表,遍历过程中,找到你所需要的数值,如果是的,就返回当前节点,不是就继续往下遍历,直到链表为空。如果遍历完了还没有找到,那就说明你想查找的数值不在链表里面,返回空指针。

SLNode* SLTFind(SLNode* phead, SLNDataType x)
{
   
   
    SLNode* ptr = phead;
    while (ptr)
    {
   
   
        if (ptr->val == x)
        {
   
   
            return ptr;
        }
        else
        {
   
   
            ptr = ptr->next;
        }
    }
    return NULL;
}

在pos位置之前插入一个节点

遍历链表,找到pos之前的节点,然后按照尾插的方式插入即可。但是需要判断一种情况,如果pos就是第一个节点,那么在pos位置之前插入,那就相当于是头插了。有限制条件,pos一定是链表中的一个有效节点。

//在pos位置之前插入
SLNode* SLTInsert(SLNode** pphead, SLNode* pos, SLNDataType x)
{
   
   
    assert(pphead);
    assert(*pphead);
    assert(pos);

    //pos如果为头节点,就是头插
    if (*pphead == pos)
    {
   
   
        SLTPushFront(pphead, x);
    }
    //pos不是头节点
    else
    {
   
   
        SLNode* pre = *pphead;
        while (pre->next != pos)
        {
   
   
            pre = pre->next;
        }
        SLNode* newnode = CreateNode(x);
        pre->next = newnode;
        newnode->next = pos;
    }
}

在pos位置删除节点

pos需要是链表中存在的节点,链表不能为空。pos可能是头节点,因此需要二级指针,这种情况就相当于头删。删除pos节点时,需要一个指针保存pos前一个节点,让pos前一个结点的指针域直接指向pos下一个节点即可,释放pos,让pos=NULL。

//在pos位置删除节点
void SLTErase(SLNode** pphead, SLNode* pos)
{
   
   
    assert(pphead);
    assert(*pphead);
    assert(pos);

    if (*pphead == pos)
    {
   
   
        SLTPopFront(pphead);
    }
    else
    {
   
   
        SLNode* pre = *pphead;
        while (pre->next != pos)
        {
   
   
            pre=pre->next;
        }
        pre->next = pos->next;
        free(pos);
        pos = NULL;
    }
}

在pos位置后插入节点

需要保证pos是链表的一个有效节点,然后让新节点的指针域指向pos的下一个节点,然后让pos的指针域指向新节点。

在这里插入图片描述

//pos位置之后插入
void SLTInsertAfter(SLNode* pos, SLNDataType x)
{
   
   
    assert(pos);
    SLNode* newnode = CreateNode(x);

    newnode->next = pos->next;
    pos->next = newnode;
}

删除pos后一个节点

删除pos后一个节点,pos已经是一个节点,因此,链表不可能为空。
需要注意的是,这里不能直接是pos的指针域指向pos后的第二个节点,因为每一个节点都是通过malloc在堆上申请的,不使用的时候要主动的去释放掉,也就是free掉,把这块空间归还给操作系统,否则会导致内存泄漏。需要借助一个节点,保存待删节点,然后去释放借助的这一个节点并置为空指针即可。

//删除pos后一个节点
void SLTEraseAfter(SLNode* pos)
{
   
   
    assert(pos);
    assert(pos->next);

    SLNode* tmp = pos->next;
    pos->next = pos->next->next;

    free(tmp);
    tmp = NULL;
}

销毁

void SLTDestroy(SLNode** pphead){
   
   
    assert(pphead);

    SLNode* cur = *pphead;
    while (cur)
    {
   
   
        SLNode* next = cur->next;
        free(cur);
        cur = next;
    }

    *pphead = NULL;
}

结尾

单链表相对于顺序表会难很多,也会有很多的坑点,我们要去判断链表是否为空?(在遍历、尾插、头插时允许空链表存在),头节点是否存在?什么时候传二级指针?

目录
相关文章
|
2天前
【数据结构】单链表之--无头单向非循环链表
【数据结构】单链表之--无头单向非循环链表
|
2天前
|
缓存 算法 Java
6.单向链表正确实现方式
6.单向链表正确实现方式
41 1
|
2天前
【无头双向链表和链表练习题2】
【无头双向链表和链表练习题2】
25 0
不带头非循环的单向链表的增删查改的实现(代码版)
不带头非循环的单向链表的增删查改的实现(代码版)
LeetCode刷题Day04——链表(设计单/双链表、移除、翻转、交换链表节点)
迭代法:首先创建一个临时的节点p用于遍历链表,其开始可以指向头节点,也可以让其next节点指向头节点(如果p指向头节点则while循环的判断条件是p!=null,反之则是p.next!=null),随后p不断地向后移动,在这个过程中进行要求的操作。如果结果要返回头指针,可以实现创建一个节点让其next指向头指针。 如果是要删除元素,那么需要拥有前一个节点的指针,让其指向要删除的元素的下一个元素,所以此时则不能让p指向头节点,而应该是让next去指向,即判断的是下一个元素的值,这样才能够实现删除。 如果是要翻转链表,那么需要不断改变指针的方向,即让next等于之前的元素,所以需要一个变量prev
|
6月前
【单链表】无头单项不循环(1)
【单链表】无头单项不循环(1)
588 2
|
6月前
【单链表】无头单项不循环(2)
【单链表】无头单项不循环(2)
612 2
|
5月前
|
存储 算法
数据结构单链表之检测和删除链表中的循环 | 第十三套
数据结构单链表之检测和删除链表中的循环 | 第十三套
30 0
|
5月前
数据结构单向链表和循环链表的插入 | 第二套
数据结构单向链表和循环链表的插入 | 第二套
29 0
|
6月前
|
存储 C++
【双向链表】带头双向循环(1)
【双向链表】带头双向循环(1)
48 0