单链表详解

目录

预备知识

单链表的概念以及结构

单链表的实现

单链表的创建

手动创建节点

链表的打印

链表的尾插

正确代码

链表的头插

链表的尾删

链表的头删

链表元素的查找

在指定位置之前插入数据

在指定位置之后插入数据

在指定位置删除数据

删除pos之后的一个节点

销毁链表



预备知识

C语言常见概念,循环,数组,函数,指针,二级指针,结构体指针,动态内存管理

单链表的概念以及结构

在上期顺序表的详解后,解决了一部分数组无法解决的问题,但即使是动态顺序表,仍存在空间浪费的情况,为了使得空间利用最大化,就出现了链表,链表可以实现有多少数据就开辟多少空间,理论上不存在浪费的情况。链表由一个个节点构成,一个节点的数据包含该节点的数据和下一个节点的地址组成,当下个节点的地址为NULL时,则链表走到了尾部。这样,所有节点连接起来就形成了链表。区别于线性表,链表的逻辑结构上是线性的,但是在物理结构上并不是线性的。

单链表的实现

接下来将实现一下功能

//链表的打印
void SLTPrint(SLTNode* phead);

//链表的尾插
void SLTPushBack(SLTNode** pphead, SLDataType x);

//链表的头插
void SLTPushFront(SLTNode** pphead, SLDataType x);

//链表的尾删
void SLTPopBack(SLTNode** pphead);

//链表的头删
void SLTPopFront(SLTNode** pphead);

//链表元素的查找
SLTNode* SLTFind(SLTNode** pphead, SLDataType x);

//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x);

//在指定位置之后插入数据
void SLTAfter(SLTNode* pos, SLDataType x);

//在指定位置删除数据
void SLTErase(SLTNode** pphead, SLTNode* pos);

//删除pos之后的一个节点
void SLTEraseAfter(SLTNode* pos);

//销毁链表
void SListDesTroy(SLTNode** pphead);

单链表的创建

链表的基本组成单位为节点,节点由两部分组成

一个是存储的数据,另一个为下一个节点的地址

typedef int SLDataType;

typedef struct SListNode
{
    SLDataType data;
    struct SListNode* next;//指向下一个节点
}SLTNode;

手动创建节点

我们创建一个test.c的测试文件

我们先手动创建几个节点

#include"SList.h"

void test01()
{
    //创建四个节点
    SLTNode* node1 = (SLTNode*)malloc(sizeof(SLDataType));
    assert(node1);//预防malloc失败
    node1->data = 1;

    SLTNode* node2 = (SLTNode*)malloc(sizeof(SLDataType));
    assert(node2);
    node2->data = 2;

    SLTNode* node3 = (SLTNode*)malloc(sizeof(SLDataType));
    assert(node3);
    node3->data = 3;

    SLTNode* node4 = (SLTNode*)malloc(sizeof(SLDataType));
    assert(node4);
    node4->data = 4;
    
    //首尾相连
    node1->next = node2;
    node2->next = node3;
    node3->next = node4;
    node4->next = NULL;

    SLTNode* plist = node1;//创建一个链表使第一个节点为node1
	SLTPrint(plist);
    
    //销毁链表
    SListDesTroy(&plist);

}

int main()
{
    test01();
    return 0;
}

在调试窗口,我们看见节点已经创建成功

我们手动创建了四个节点,并将节点首尾相接,那么我们先进行链表的打印

链表的打印

//链表的打印
void SLTPrint(SLTNode* phead)
{
    assert(phead);
    SLTNode* prev = phead;
    while(prev)
    {
        printf("%d->", prev->data);
        prev = prev->next;
    }
    printf("NULL\n");
}
  • 首先对指针进行assert断言
  • 然后创建一个临时的指针指向phead,一般不对phead直接操作,避免后期出现一些问题
  • 使用while循环,判断prev是否为空指针,如果为空指针就说明遍历了链表,就会跳出while循环。
  • 最后调用printf进行打印

我们运行test.c

链表中的元素就被打印出来了

链表的尾插

//创建新节点
SLTNode* SLTCreNode(SLDataType x)
{
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    //预防malloc失败
    if (newnode == NULL)
    {
        perror("malloc failed");
        exit(1);
    }
    newnode->data = x;
    newnode->next = NULL;

    return newnode;
}
//链表的尾插
void SLTPushBack(SLTNode* phead, SLDataType x)
{
	assert(phead);
	SLTNode* newnode = SLTCreNode(x);
	if (phead == NULL)
	{
		phead = newnode;
	}
	else
	{
		//先找到尾部
		SLTNode* prev = phead;

		while (prev->next)
		{
			prev = prev->next;
		}
		prev->next = newnode;
	}
}
  • 链表尾插的第一步是创建一个新的节点,我们可以将创建节点单独写成一个函数,方便之后调用。
  • 如果链表是空链表,直接将创建的节点赋给phead即可,如果不是空链表,我们首先要找到链表的尾部,如果prev->next为空指针,就说明prev已经是最后一个节点了,就找到尾部了
  • 然后再将最后一个节点接上创建好的新节点newnode

此时我们对尾插进行测试

#include"SList.h"

void test02()
{
    SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
    SLTPushBack(&plist, 4);
	SLTPrint(plist);
}

int main()
{
    test02();
    return 0;
}

此时我们运行程序,会发现程序报错,那么问题出现在哪里?

我们仔细观察所创建的节点,首先我们创建了第一个节点,并赋值了NULL

但是plist已经是第一个节点的地址了,我们传参数时又取了plist的地址

所以传入的数据时地址的地址,也就是指针的指针,即二级指针

那么我们就需要用二级指针来接收。

正确代码

//链表的尾插
void SLTPushBack(SLTNode** pphead, SLDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTCreNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//找尾
		SLTNode* prev = *pphead;

		while (prev->next)
		{
			prev = prev->next;
		}
		prev->next = newnode;
	}
}

在使用指针进行操作时,最重要的是要搞清楚指针的层级关系,要一级对应一级。

链表的头插

//链表的头插
void SLTPushFront(SLTNode** pphead, SLDataType x)
{
    assert(pphead);
    SLTNode* newnode = SLTCreNode(x);
    newnode->next = *pphead;
    *pphead = newnode;
}
  • 首先先创建一个节点,再将创建的节点连接*pphead
  • 将首节点置为newnode

链表的尾删

//链表的尾删
void SLTPopBack(SLTNode** pphead)	
{
    assert(pphead && *pphead);
    if ((*pphead)->next == NULL)//注意这里的*pphead的括号,因为->的优先级高于*,所以需要括号
    {
        free(*pphead);
        *pphead = NULL;
    }
    else
    {
        SLTNode* prev = *pphead;//尾节点的前一个节点
        SLTNode* ptail = *pphead;//尾节点
        //找尾
        while (ptail->next)
        {
            prev = ptail;
            ptail = ptail->next;
        }
        free(ptail);
        ptail = NULL;
        prev->next = NULL;
    }
}

如果想要删除尾节点,需要找出两个节点,由于链表的不可逆操作,所以需要找出尾节点和尾节点之前的一个节点

  • pphead和*pphead都不能为空指针,删除的数据不能为空
  • 释放尾节点并置为空指针
  • 找到尾节点的前一个节点使其指向NULL 

链表的头删

//链表的头删
void SLTPopFront(SLTNode** pphead)
{
    assert(pphead && *pphead);
    SLTNode* prev = *pphead;
    *pphead = prev->next;
    free(prev);
    prev = NULL;
}
  • pphead和*pphead都不能为空指针
  • 先让*pphead指向下一个节点
  • 释放原节点并置为空指针

链表元素的查找

SLTNode* SLTFind(SLTNode** pphead, SLDataType x)
{
    assert(pphead && *pphead);
    
    SLTNode* prev = *pphead;
    if (prev->next == NULL)
    {
        if (prev->data == x)
        {
            return prev;
        }
        return NULL;
    }
    else
    {
        while (prev->next)
        {
            if (prev->data == x)
            {
                return prev;
            }
            prev = prev->next;
        }
        return NULL;
    }
}
  • pphead和*pphead都不能为空指针
  • 如果只有一个元素,则直接比较
  • 如果又多个元素,则进行遍历
  • 找到了返回节点的地址,没找到返回空指针

在指定位置之前插入数据

//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
    assert(pphead && *pphead);
    assert(pos);
    //判断是否为头插
    if (*pphead == pos)
    {
        SLTPushFront(pphead, x);//直接调用头插
    }    
    else
    {
        SLTNode* newnode = SLTCreNode(x);//创建新节点
        SLTNode* prev = *pphead;
        while (prev->next != pos)
        {
            prev = prev->next;
        }
        newnode->next = pos;
        prev->next = newnode;
    }
}
  • pphead和*pphead都不能为空指针,位置不能为第0位
  • 如果位置是第一位,则直接调用头插
  • 通过循环找到pos的位置
  • 将新节点newnode指向pos
  • pos前一个节点prev指向newnode

在指定位置之后插入数据

//在指定位置之后插入数据
void SLTAfter(SLTNode* pos, SLDataType x)
{
    assert(pos);
    SLTNode* newnode = SLTCreNode(x);
    //插入前pos - pos->next
    //插入后pos - newnode - pos->next
    newnode->next = pos->next;
    pos->next = newnode;
}
  • 创建一个新节点
  • 让newnode->next指向原来pos指向的数
  • 再让pos指向newnode

在指定位置删除数据

//在指定位置删除数据
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
    assert(pphead && *pphead);
    if (*pphead->next == pos)
    {
        //如果指向第一个元素就是头删
        SLTPopFront(pphead, x);
    }
    else
    {
        SLTNode* prev = *pphead;
        //找出pos的前一个节点prev
        while (prev->next != pos)
        {
            prev = prev->next;
        }
        //prev连接pos后的节点
        prev->next = pos->next;

        free(pos);
        pos = NULL;
    }
}
  • pphead和*pphead都不能为空指针
  • 如果要删除的是第一位数据,则调用头删
  • 先找出pos的前一个节点prev
  • 再将prev连接pos后的节点
  • 释放pos并置为空指针

删除pos之后的一个节点

//删除pos之后的一个节点
void SLTEraseAfter(SLTNode* pos)
{
    assert(pos);
    //prev指向需要删除的节点
    SLTNode* prev = pos->next;
    //pos指向prev之后的节点
    pos->next = prev->next;
    free(prev);
    prev = NULL;
}
  • pos不能是空指针
  • prev指向需要删除的节点
  • 使pos指向prev之后的节点
  • 释放prev并置为空指针

销毁链表

//销毁链表
void SListDesTroy(SLTNode** pphead)
{
    assert(pphead && *pphead);
    SLTNode* prev = *pphead;\
    while (prev->next)
    {
        SLTNode* pcur = prev;
        prev = prev->next;
        free(pcur);
        pcur = NULL;
    }
    prev = NULL;
}
  • pphead和*pphead不能为空指针
  • 循环遍历链表,创建变量复制prev的值
  • prev快pcur一个节点,prev往前走,pcur在后面释放
  • 最后把prev置为空指针

源代码

SList.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int SLDataType;

typedef struct SListNode
{
	SLDataType data;
	struct SListNode* next;
}SLTNode;

//链表的打印
void SLTPrint(SLTNode* phead);

//链表的尾插
void SLTPushBack(SLTNode** pphead, SLDataType x);

//链表的头插
void SLTPushFront(SLTNode** pphead, SLDataType x);

//链表的尾删
void SLTPopBack(SLTNode** pphead);

//链表的头删
void SLTPopFront(SLTNode** pphead);

//链表元素的查找
SLTNode* SLTFind(SLTNode** pphead, SLDataType x);

//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x);

//在指定位置之后插入数据
void SLTAfter(SLTNode* pos, SLDataType x);

//在指定位置删除数据
void SLTErase(SLTNode** pphead, SLTNode* pos);

//删除pos之后的一个节点
void SLTEraseAfter(SLTNode* pos);

//销毁链表
void SListDesTroy(SLTNode** pphead);

SList.c

#include"SList.h"

//链表的打印
void SLTPrint(SLTNode* phead)
{
    assert(phead);
    SLTNode* prev = phead;
    while (prev)
    {
        printf("%d->", prev->data);
        prev = prev->next;
    }
    printf("NULL\n");
}

//销毁链表
void SListDesTroy(SLTNode** pphead)
{
    assert(pphead && *pphead);
    SLTNode* prev = *pphead; \
        while (prev->next)
        {
            SLTNode* pcur = prev;
            prev = prev->next;
            free(pcur);
            pcur = NULL;
        }
    prev = NULL;
}

//创建新节点
SLTNode* SLTCreNode(SLDataType x)
{
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    //预防malloc失败
    if (newnode == NULL)
    {
        perror("malloc failed");
        exit(1);
    }
    newnode->data = x;
    newnode->next = NULL;

    return newnode;
}

//ERROR
链表的尾插
//void SLTPushBack(SLTNode* phead, SLDataType x)
//{
//    assert(phead);
//    SLTNode* newnode = SLTCreNode(x);
//    if (phead == NULL)
//    {
//        phead = newnode;
//    }
//    else
//    {
//        //先找到尾部
//        SLTNode* prev = phead;
//
//        while (prev->next)
//        {
//            prev = prev->next;
//        }
//        prev->next = newnode;
//    }
//}

//链表的尾插
void SLTPushBack(SLTNode** pphead, SLDataType x)
{
    assert(pphead);
    SLTNode* newnode = SLTCreNode(x);
    if (*pphead == NULL)
    {
        *pphead = newnode;
    }
    else
    {
        //找尾
        SLTNode* prev = *pphead;

        while (prev->next)
        {
            prev = prev->next;
        }
        prev->next = newnode;
    }
}

//链表的头插
void SLTPushFront(SLTNode** pphead, SLDataType x)
{
    assert(pphead);
    SLTNode* newnode = SLTCreNode(x);
    newnode->next = *pphead;
    *pphead = newnode;
}

//链表的尾删
void SLTPopBack(SLTNode** pphead)
{
    assert(pphead && *pphead);
    if ((*pphead)->next == NULL)
    {
        free(*pphead);
        *pphead = NULL;
    }
    else
    {
        SLTNode* prev = *pphead;//尾节点的前一个节点
        SLTNode* ptail = *pphead;//尾节点
        //找尾
        while (ptail->next)
        {
            prev = ptail;
            ptail = ptail->next;
        }
        free(ptail);
        ptail = NULL;
        prev->next = NULL;
    }
}

//链表的头删
void SLTPopFront(SLTNode** pphead)
{
    assert(pphead && *pphead);
    SLTNode* prev = *pphead;
    *pphead = prev->next;
    free(prev);
    prev = NULL;
}

SLTNode* SLTFind(SLTNode** pphead, SLDataType x)
{
    assert(pphead && *pphead);

    SLTNode* prev = *pphead;
    if (prev->next == NULL)
    {
        if (prev->data == x)
        {
            return prev;
        }
        return NULL;
    }
    else
    {
        while (prev->next)
        {
            if (prev->data == x)
            {
                return prev;
            }
            prev = prev->next;
        }
        return NULL;
    }
}

//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
    assert(pphead && *pphead);
    assert(pos);
    //判断是否为头插
    if (*pphead == pos)
    {
        SLTPushFront(pphead, x);//直接调用头插
    }
    else
    {
        SLTNode* newnode = SLTCreNode(x);//创建新节点
        SLTNode* prev = *pphead;
        while (prev->next != pos)
        {
            prev = prev->next;
        }
        newnode->next = pos;
        prev->next = newnode;
    }
}

//在指定位置之后插入数据
void SLTAfter(SLTNode* pos, SLDataType x)
{
    assert(pos);
    SLTNode* newnode = SLTCreNode(x);
    //插入前pos - pos->next
    //插入后pos - newnode - pos->next
    newnode->next = pos->next;
    pos->next = newnode;
}

//在指定位置删除数据
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
    assert(pphead && *pphead);
    if ((*pphead)->next == pos)
    {
        //如果指向第一个元素就是头删
        SLTPopFront(pphead);
    }
    else
    {
        SLTNode* prev = *pphead;
        //找出pos的前一个节点prev
        while (prev->next != pos)
        {
            prev = prev->next;
        }
        //prev连接pos后的节点
        prev->next = pos->next;

        free(pos);
        pos = NULL;
    }
}

//删除pos之后的一个节点
void SLTEraseAfter(SLTNode* pos)
{
    assert(pos);
    //prev指向需要删除的节点
    SLTNode* prev = pos->next;
    //pos指向prev之后的节点
    pos->next = prev->next;
    free(prev);
    prev = NULL;
}

test.c

#include"SList.h"

void test01()
{
    printf("test01\n");
    //创建四个节点
    SLTNode* node1 = (SLTNode*)malloc(sizeof(SLDataType));
    assert(node1);//预防malloc失败
    node1->data = 1;

    SLTNode* node2 = (SLTNode*)malloc(sizeof(SLDataType));
    assert(node2);
    node2->data = 2;

    SLTNode* node3 = (SLTNode*)malloc(sizeof(SLDataType));
    assert(node3);
    node3->data = 3;

    SLTNode* node4 = (SLTNode*)malloc(sizeof(SLDataType));
    assert(node4);
    node4->data = 4;

    //首尾相连
    node1->next = node2;
    node2->next = node3;
    node3->next = node4;
    node4->next = NULL;

    SLTNode* plist = node1;//创建一个链表使第一个节点为node1
    SLTPrint(plist);

    SListDesTroy(&plist);
    printf("\n");
}

void test02()
{
    printf("test02\n");
    SLTNode* plist = NULL;
    
    //链表的尾插
    SLTPushBack(&plist, 1);
    SLTPushBack(&plist, 2);
    SLTPushBack(&plist, 3);
    SLTPushBack(&plist, 4);
    SLTPrint(plist);

    //链表的头插
    SLTPushFront(&plist, 0);
    SLTPrint(plist);

    //链表的尾删
    SLTPopBack(&plist);
    SLTPrint(plist);

    //链表的头删
    SLTPopFront(&plist);
    SLTPrint(plist);
    
    //链表元素的查找
    SLTNode* Find = SLTFind(&plist, 1);
    if (Find == NULL)
        printf("没找到!\n");
    else
        printf("找到了!\n");

    //在指定位置之前插入数据
    SLTInsert(&plist, Find, 0);
    SLTPrint(plist);

    //在指定位置之后插入数据
    SLTAfter(Find, 1);
    SLTPrint(plist);

    //在指定位置删除数据
    SLTErase(&plist, Find);
    SLTPrint(plist);

    //删除pos之后的一个节点
    SLTEraseAfter(Find);
    SLTPrint(plist);

    //销毁链表
    SListDesTroy(&plist);
}

int main()
{
    test01();
    test02();
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值