数据结构(初阶)笔记归纳4:单链表的实现

单链表的实现

目录

单链表的实现

一、链表的概念及结构

1.1.链表的概念

1.2.链表的特性

1.3.链表的结构

1.3.1.链表的组成

1.3.2.节点的组成

1.4.单链表结构的定义

二、单链表的实现

2.1.单链表文件结构

2.2.头文件编写

2.2.1.头文件包含

2.2.2.节点数据重命名

2.2.3.节点结构定义

2.2.4.单链表的打印

2.2.5.单链表的尾插

2.2.6.单链表的头插

2.2.7.单链表的尾删

2.2.8.单链表的头删

2.2.9.单链表的查找

2.2.10.指定位置前插入数据

2.2.11.指定位置后插入数据

2.2.12.删除pos节点

2.2.13.删除pos位后节点

2.2.14.单链表的销毁

2.3.源文件编写

2.3.1.头文件包含

2.3.2.单链表的打印

2.3.3.节点申请函数

2.3.4.单链表的尾插

2.3.5.单链表的头插

2.3.6.单链表的尾删

2.3.6.单链表的头删

2.3.7.单链表的查找

2.3.8.指定位置前插入数据

2.3.9.指定位置后插入数据

2.3.10.删除pos节点

2.3.11.删除pos位后节点

2.3.12.单链表的销毁

2.4.测试文件编写

2.4.1.头文件包含

2.4.2.测试方法01

2.4.3.测试方法02

四、总结


一、链表的概念及结构

1.1.链表的概念

链表是一种线性表的实现方式

1.2.链表的特性

**物理结构:**非线性的(各节点存储空间不连续)

**逻辑结构:**线性的

1.3.链表的结构

1.3.1.链表的组成

**火车:**由火车头与车厢组成

**链表:**由头节点与节点组成

**注:**链表的每个节点都可以单独申请,解决了顺序表增容造成的运行效率低下问题

顺序表 VS 链表:

1.3.2.节点的组成

**数据域:**用于存储该节点的数据

**指针域:**用于存储下一个节点的地址

1.4.单链表结构的定义

cpp 复制代码
struct SListNode
{
	int data;//节点数据
	struct SListNode* next;//指向下一个节点的指针变量
};

**注:**SListNode名称解析:

  • S:Single(单)
  • List:列表
  • Node:节点

二、单链表的实现

2.1.单链表文件结构

  • 头文件(SList.h):单链表的结构创建,单链表的方法声明
  • 源文件(SList.c):单链表的方法实现
  • 测试文件(test.c):测试数据结构的方法

2.2.头文件编写

2.2.1.头文件包含
cpp 复制代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
2.2.2.节点数据重命名
cpp 复制代码
typedef int SLTDataType;
2.2.3.节点结构定义
cpp 复制代码
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;
2.2.4.单链表的打印
cpp 复制代码
void SLPrint(SLTNode* phead);
2.2.5.单链表的尾插
cpp 复制代码
void SLTPushBack(SLTNode** pphead,SLTDataType x);
2.2.6.单链表的头插
cpp 复制代码
void SLTPushFront(SLTNode** pphead,SLTDataType x);
2.2.7.单链表的尾删
cpp 复制代码
void SLTPopBack(SLTNode** pphead);
2.2.8.单链表的头删
cpp 复制代码
void SLTPopFront(SLTNode** pphead);
2.2.9.单链表的查找
cpp 复制代码
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
2.2.10.指定位置前插入数据
cpp 复制代码
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
2.2.11.指定位置后插入数据
cpp 复制代码
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
2.2.12.删除pos节点
cpp 复制代码
void SLTErase(SLTNode** pphead, SLTNode* pos);
2.2.13.删除pos位后节点
cpp 复制代码
void SLTEraseAfter(SLTNode* pos);
2.2.14.单链表的销毁
cpp 复制代码
void SListDestroy(SLNode** pphead);

2.3.源文件编写

2.3.1.头文件包含
cpp 复制代码
#include "SList.h"
2.3.2.单链表的打印
cpp 复制代码
void SLPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;

	while (pcur)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}

	printf("NULL\n");
}

解析:

  • 创建临时变量pcur存放头节点
  • while循环遍历链表,打印每个节点的数据
2.3.3.节点申请函数
cpp 复制代码
SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);//0:正常退出 非0:异常退出
	}

	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

解析:

  • 创建临时变量newode接收malloc动态申请内存空间
  • 将节点数据赋给新节点,新节点的指针域为NULL
  • 返回新创建的节点地址
2.3.4.单链表的尾插
cpp 复制代码
void SLTPushBack(SLTNode** pphead,SLTDataType x)
{
    assert(pphead);

	SLTNode* newnode = SLTBuyNode(x);

	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* ptail = *pphead;

		while (ptail->next)
		{
			ptail = ptail->next;
		}

		ptail->next = newnode;
	}
}

解析:

  • 断言判断传参是否为NULL
  • 创建临时变量newnode接收新节点的地址
  • 判断单链表是否为空链表

如果为空:

  • 将新节点的地址直接赋给头指针

如果不为空

  • 创建临时变量ptail存放尾节点
  • while循环找到尾节点
  • 尾节点的指针域存放新节点的地址

注:

  • 判断循环条件:ptail是指向尾节点的指针,所以当ptail指向尾节点后循环终止,由于尾节点的指针域为NULL,因此判断条件应为ptail指向节点的指针域是否为NULL,如果判断条件设为ptail不为NULL,会导致结束后ptail指向NULL,而非指向尾节点
  • 空指针解引用:初始情况下,单链表为空,指向头节点的指针变量*pphead存放的是NULL,此时对*pphead解引用,就是对空指针的解引用,程序一定会报错,因此需要先判断头指针是否为空指针
2.3.5.单链表的头插
cpp 复制代码
void SLTPushFront(SLTNode** pphead,SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);

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

解析:

  • 断言判断传参是否为NULL
  • 创建临时变量newnode接收新节点的地址
  • 将当前头节点的地址存放到新节点的指针域中
  • 将新节点的地址赋给头指针变量
2.3.6.单链表的尾删
cpp 复制代码
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;
	}
}

解析:

  • 断言判断传参是否为NULL,判断单链表是否为空链表
  • 判断头节点的指针域是否为NULL

存在一个节点:

  • 直接释放头节点,将头节点指针设为NULL

存在多个节点:

  • 创建临时变量prev和ptail存放头节点地址
  • while循环,让ptail指向尾节点,prev指向尾节点的前一个节点
  • 释放尾节点,将尾节点指针设为NULL,尾节点的前一个节点指针域的指针变量设为NULL

注:

  • 不能直接释放尾节点,否则上一个节点指针域的指针变量就会变成野指针,所以在释放之后,要将上一个节点指针域的指针变量设为NULL
  • 如果单链表中只有一个节点,则可以直接释放该节点
  • ->的优先级高于*,所以*pphead要加括号改变优先级
2.3.6.单链表的头删
cpp 复制代码
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);

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

	free(*pphead);
	*pphead = next;
}

解析:

  • 断言判断传参是否为NULL,判断单链表是否为空链表
  • 创建临时变量next接收头节点的下一个节点地址
  • 释放头节点,将更新头节点为next

**注:**不能先释放头节点,否则会找不到下一个节点,需要先创建临时变量next接收头节点的下一个节点地址,即头节点中指针域的指针变量值,再将头节点释放

2.3.7.单链表的查找
cpp 复制代码
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead;

	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}

	return NULL;
}

解析:

  • 创建临时变量pcur存储头指针
  • while循环遍历单链表
  • 如果pcur指向的节点数据等于查找数据,则返回该节点指针
  • 如果pcur指向的节点数据不为查找数据,则返回NULL

**注:**遍历单链表时通常需要创建临时变量存储头指针,以便保留起始节点的地址

2.3.8.指定位置前插入数据
cpp 复制代码
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);

	if (pos == *pphead)
	{
		SLTPushFront(pphead,x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

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

解析:

  • 断言判断传参是否为NULL,单链表是否为空链表
  • 创建临时变量newnode接收新节点的地址
  • 判断pos位是否为头节点

如果为头插:

  • 调用头插函数

如果不为头插:

  • 创建临时变量prev存储头指针
  • while循环找到pos的前一个节点
  • 当prev指向节点指针域的指针变量为pos时终止循环
  • 此时prev指向的节点就是pos的前一个节点
  • 将pos节点的地址存放在newnode的指针域中
  • 将newnode节点的地址存放在prev的指针域中

注:

当pos与头指针相同时,说明是头插,此时这种方法行不通,因为prev与pos相同,prev->next无法找到pos,直接遍历了单链表,可以直接调用头插的方法来解决

2.3.9.指定位置后插入数据
cpp 复制代码
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);

	SLTNode* newnode = SLTBuyNode(x);

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

解析:

  • 断言判断传参是否为NULL
  • 创建临时变量newnode接收新节点的地址
  • 将pos->next指向节点的地址存储到newnode的指针域
  • 将newnode指向节点的地址存储到pos节点的指针域

注:

  • 在指定位置后插入数据不需要传头指针,因为不需要从头遍历单链表来寻找pos之前的节点,而pos下一个节点可以通过pos的指针域找到
  • 不能先在pos节点的指针域存放newnode节点的地址,否则无法找到pos节点的下一个节点,应当先将newnode的指针域存放pos节点的下一个节点的地址,然后在pos节点的指针域存放newnode节点的地址
2.3.10.删除pos节点
cpp 复制代码
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);

    if(pos == *pphead)
    {
        SLTPopFront(pphead);
    }
    else
    {
	    SLTNode* prev = *pphead;
	    while (prev->next != pos)
	    {
		    prev = prev->next;
	    }

	    prev->next = pos->next;
	    free(pos);
	    pos = NULL;
    }
}

解析:

  • 断言判断传参是否为NULL,单链表是否为空链表
  • 判断pos是否为头指针

如果为头删:

  • 调用头删函数

如果不为头删:

  • while循环找到pos的前一个节点prev
  • 让prev的指针域存放pos->next节点的地址
  • 释放pos节点,将pos置为NULL

注:

  • 如果直接释放pos节点,就无法找到pos下一个节点,同时pos前一个节点指针域里的指针变量变成了野指针
  • 当pos为头指针时,说明时头删,此时这种方法行不通,因为prev与pos相同,prev->next无法找到pos,直接遍历了单链表,可以直接调用头删的方法来解决
2.3.11.删除pos位后节点
cpp 复制代码
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);
	SLTNode* del = pos->next;

	pos->next = del->next;
	free(del);
	del = NULL;
}

解析:

  • 断言判断传参是否为NULL,pos位后的节点是否存在
  • 创建临时变量del存放pos的下一个节点地址
  • 将pos的指针域存放pos位的下下个节点地址
  • 释放pos的下一个节点,将del置为NULL

注:

不能先将pos的指针域存放pos下下个节点的地址,然后释放pos的下一个节点,因为此时pos的下一个节点为pos下下个节点,而不是pos的下一个节点,可以先创建临时变量del存放pos的下一个节点,再将pos的指针域存放pos下下个节点的地址,然后释放del

2.3.12.单链表的销毁
cpp 复制代码
void SListDestroy(SLTNode** pphead)
{
	assert(pphead && *pphead);

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

	*pphead = NULL;
}

解析:

  • 判断传参是否为NULL,判断单链表是否为空链表
  • 创建临时变量pcur存放头指针
  • while循环遍历单链表
  • 创建临时变量next存放pcur下一个节点指针
  • 释放pcur节点的空间,将next节点地址赋给pcur
  • 直到pcur为NULL时结束循环
  • 将头节点置为NULL

2.4.测试文件编写

2.4.1.头文件包含
cpp 复制代码
#include "SList.h"
2.4.2.测试方法01
cpp 复制代码
void SListTest01()
{
	创建四个节点
	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
	node1->data = 1;

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

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

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

	连接四个节点
	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;

	单链表的打印
	SLTNode* plist = node1;
	SLTPrint(plist);
}

int main()
{
    SListTest01();
	return 0;
}
2.4.3.测试方法02
cpp 复制代码
void SListTest02()
{
    新建单链表头指针
	SLTNode* plist = NULL;

    单链表的尾插
	SLTPushBack(&plist, 1);
    SLTPushBack(&plist, 2);
    SLTPushBack(&plist, 3);
    SLTPushBack(&plist, 4);

    单链表的头插
    SLTPushFront(&plist, 6);
    SLTPushFront(&plist, 7); 
    SLTPushFront(&plist, 8); 
    
    单链表的尾删
    SLTPopBack(&plist);

    单链表的头删
    SLTPopFront(&plist);

    单链表的查找
    SLTNode* find = SLTFind(plist,3);
    if(find == NULL)
    {
        printf("没有找到!\n");
    }
    else
    {
        printf("找到了!\n");
    }

    指定位置前插入数据
    SLTInsert(&plist,find,11);

    删除pos节点
    SLTErase(&plist,find);

    删除pos位后节点
    SLTEraseAfter(find);

    单链表的打印
    SLPrint(plist);

    单链表的销毁
    SListDestroy(&plist);
}

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

**注:**当需要修改某个节点指针域中的指针变量值时,实际上是在修改一级指针变量,由于函数传参只有传地址才能修改变量值,所以要传送一级指针变量的地址,即二级指针

辨析:

  • *plist:第一个节点(**pphead)
  • plist:存放第一个节点地址的一级指针变量(*pphead)
  • &plist:存放第一个节点地址的指针变量的地址(pphead)

**思考:**为什么plist始终为头节点?

当我们在头插,头删操作时,会更新*pphead的值,从而保证了plist始终为单链表的头节点

四、总结

本篇博客是对于数据结构中单链表实现知识点的整理归纳,后续还会更新单链表的应用,双向链表等内容,如果对你有帮助,欢迎点赞+收藏+关注,让我们一起共同进步🌟~

相关推荐
wm10432 小时前
代码随想录第三天 链表
数据结构·链表
WHOVENLY2 小时前
揭秘正则表达式的基础语法与应用
开发语言·javascript·正则表达式
大雨淅淅2 小时前
【开发工具】Postman从入门到精通:环境搭建与使用全攻略
开发语言·lua
航Hang*2 小时前
第3章:复习篇——第4节:创建、管理视图与索引---题库
网络·数据库·笔记·sql·学习·mysql·期末
flysh052 小时前
.NET 基础 - StringBuilder 类
开发语言·c#·编程语言·c#10
JeffDingAI2 小时前
【Datawhale学习笔记】NLP 概述
笔记·学习·自然语言处理
BLSxiaopanlaile2 小时前
关于子集和问题的几种解法
数据结构·算法·剪枝·回溯·分解
甄心爱学习2 小时前
Python 中 combinations 的详细用法
开发语言·python
Arenaschi2 小时前
关于垃圾的CSDN
java·网络·chrome·笔记·其他·oracle·pdf