数据结构与算法—单链表

目录

一、链表

1、链表的概念及结构

2、分类

二、实现单向链表

1、声明链表结构体

2、输出

3、头插&尾插

4、头删尾删

5、查找

6、指定位置插入

7、删除指定节点

8、删除指定节点的后一个节点

9、单链表的销毁

完整版

LList.h

LList.c

text.c


一、链表

1、链表的概念及结构

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

这张图生动形象地呈现了链表的结构。

如同高铁一般,从头到尾一个连着一个。

2、分类

主要有两种类型的链表:单向链表和双向链表。在 单向链表 中,每个节点包含一个数据元素和一个指向下一个节点的引用。而在 双向链表中,每个节点有两个引用,一个指向前一个节点,另一个指向后一个节点。

  • 从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续。
  • 现实中的结点一般都是从堆上申请出来的。
  • 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

本次讲解基础的单向链表。

二、实现单向链表

我们创建三个文件:

  • 头文件LList.h用于调用库函数、声明结构体和函数。
  • 源文件LList.c存储函数。
  • 源文件text.c进行测试。

每个源文件都必须包含LList.h。

1、声明链表结构体

cpp 复制代码
#include <stdio.h>

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;
  • 将链表的数据类型用SLTDatatype这个别名代TT替int,以后程序中使用到元素数据类型时都替换成SLTDatatype,方便日后修改顺序表数据类型。
  • 将结构体struct SListNode定义别名为SLTNode。
  • 结构体成员data为链表节点数据,数据类型是 SLTDataType。
  • next表示一个指向同类型结构体的指针,它指向单向链表下一个结点。

2、输出

cpp 复制代码
void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL) {
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}
  • 接收传入参数为结构体的地址
  • 结构体指针cur指向头结点phead
  • 循环遍历链表,当cur不指向尾节点,则打印输出当前节点数据,cur指向下一个节点。
  • cur指向尾节点打印NULL。

3、头插&尾插

头插

cpp 复制代码
void SLPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuyLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}
  • 插入数据会修改头结点,所以传入头结点指针的地址,使用二级指针接收。
  • assert判断传入头节点指针的地址是否合法,为空则报错。(需要包含头文件<assert.h>)
  • *pphead 不需要断言,如果传入的链表为空,也可以进行插入数据。
  • 为新节点newnode开辟空间并将x储存其中,因为后续经常用到开辟空间,所以将这部分操作放入函数中。
  • 新节点newnode的next指针指向头结点*pphead
  • 头结点更新为newnode。

接下来讲解为新节点开辟空间的函数 BuyLTNode

新节点开辟空间

cpp 复制代码
SLTNode* BuyLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL) {
		perror("malloc fall");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
  • 为新节点开辟空间,返回值为新节点的地址,所以函数类型为 SLTNode* 结构体指针类型。
  • malloc函数为newnode开辟结构体大小个字节。
  • 判断是否开辟成功,失败则打印错误信息,结束函数运行。
  • 将新节点的数据data赋值为传入参数 x。
  • next赋值为空。

尾插

cpp 复制代码
void SLPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead); 
	SLTNode* newnode = BuyLTNode(x);
	if (*pphead == NULL){
		*pphead = newnode;
    }
	else {
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
			tail = tail->next;
		
		tail->next = newnode;
	}		
}
  • assert判断传入头节点指针的地址是否合法,为空则报错。
  • 为新节点newnode开辟空间并将x储存其中。
  • 插入时分两种情况:空链表 非空链表
  • 如果链表为空则直接将*pphead 指向新节点 newnode,使其成为新的链表的头节点。
  • 如果链表不为空,则创建变量tail指向头结点,循环遍历链表使tail指向尾节点,将新节点地址赋值给tail的next,成功将新节点添加到链表尾部。

4、头删尾删

头删

cpp 复制代码
void SLPopFront(SLTNode** pphead)
{
	assert(pphead); 
	assert(*pphead);
	SLTNode* del = *pphead;
	*pphead = (*pphead)->next;
	free(del);
}
  • 第一个assert判断传入头节点指针的地址是否合法,为空则报错。
  • 第二个assert判断链表头节点是否为空,为空无法删除,则报错。
  • 定义变量del指向头节点,以便稍后释放该节点的内存。
  • 头节点指向头节点的next,也就是指向后一个节点。
  • 使用free函数释放del指向的已删除头节点空间。(需要包含头文件<stdlib.h>)

尾删

cpp 复制代码
void SLPopBack(SLTNode** pphead)
{
    assert(pphead);
    assert(*pphead);

    if ((*pphead)->next == NULL) {
        free(*pphead);
        *pphead = NULL;
    }
    else {
        SLTNode* tail = *pphead;
        while (tail->next->next) {
            tail = tail->next;
        }
        free(tail->next);
        tail->next = NULL;
    }
}
  • 第一个assert判断传入头节点指针的地址是否合法,为空则报错。
  • 第二个assert判断链表头节点是否为空,为空则报错。
  • 链表只有一个节点时,直接释放头节点空间,然后置空。
  • 链表有多个节点使,通过循环使变量 tail->next 找到尾节点,然后释放tail后一个节点的空间,也就是尾节点的空间,同时将其置空。

5、查找

cpp 复制代码
SLTNode* STFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	while (cur) {
		if (cur->data == x)
			return cur;
		
		cur = cur->next;
	}
	return NULL;
}
  • 函数在单链表中查找包含特定数据值 x 的节点。
  • 变量cur通过循环找到数据data等于x的节点。
  • 找到则返回指向当前节点的指针 cur,否则返回值为空。

6、指定位置插入

指定位置之前

cpp 复制代码
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);
	if (*pphead == pos){
		SLPushFront(pphead, x);
	}
	else {
		SLTNode* prev = *pphead;
		while (prev->next != pos){
			prev = prev->next;
		}

		SLTNode* newnode = BuyLTNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}
  • 第一个assert判断传入头节点指针的地址是否合法,为空则报错。

  • 第二个assert判断传入指向链表中某个节点的指针pos是否合法,不存在则报错。

  • 如果在头节点位置之前插入,则调用头插解决。

  • 如果不是头节点位置,则创建一个指向链表头节点的指针 prev,然后使用循环找到要插入位置 pos 前面的节点。

  • 创建一个新的节点 newnode 并将数据值 x 存储在其中。

  • 修改 prev 节点的 next 指针,使其指向新节点 newnode,从而将新节点插入到 pos 前面。

指定位置之后

cpp 复制代码
void SLInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuyLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
  • assert判断传入指向链表中某个节点的指针pos是否合法,不存在则报错。
  • 创建一个新的节点 newnode 并将数据值 x 存储在其中。
  • newnode的next指针指向pos的后一项。
  • pos的next指向新节点newnode。

7、删除指定节点

cpp 复制代码
void SLErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(*pphead);
	if (pos = *pphead){
		SLPopFront(pphead);
	}
	else {
		SLTNode* prev = *pphead;
		while (prev->next != pos) {
			prev = prev -> next;
		}
		prev->next = pos->next;
		free(pos);
	}
}
  • 第一个assert判断传入头节点指针的地址是否合法,为空则报错。
  • 第二个assert判断链表头节点是否为空,为空则报错。
  • pos节点为头节点,则调用头删解决。
  • pos不为头节点,则创建变量prev指向头节点,通过循环找到pos节点的前一个节点。
  • 将prev的next指向要删除的pos节点的下一个节点。
  • 释放pos空间

8、删除指定节点的后一个节点

cpp 复制代码
void SLEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);
	SLTNode* next = pos->next;
	pos->next = next->next;
	free(next);
}
  • 创建一个指向要删除的节点 pos 后面节点的指针 next,以便稍后释放该节点的内存。

  • 修改 pos 节点的 next 指针,将其指向 next 的下一个节点,从而绕过要删除的节点,使链表不再包含它。

  • 最后,使用 free 函数释放 next 指向的节点的内存,完成删除操作。

9、单链表的销毁

cpp 复制代码
void SListDestroy(SLTNode* pphead)
{
	SLTNode* cur = pphead;
	SLTNode* tmp = NULL;
	while (cur != NULL) {
		tmp = cur;
		cur = cur->next;
		free(tmp);
	}
}
  • 定义了两个指针,curtmp,用于遍历链表并释放内存。开始时,cur 被初始化为链表的头节点指针 pphead

  • 这是一个循环,它会一直执行,直到 cur 变为 NULL,也就是遍历到链表的末尾。

  • 在循环中,首先将 cur 赋值给 tmp,以便稍后释放 cur 指向的节点的内存。

  • 然后,将 cur 移动到下一个节点,即 cur = cur->next;

  • 最后,使用 free 函数释放 tmp 指向的节点的内存,即释放链表中的一个节点,接着进行循环依次释放节点直到链表最后。

完整版

LList.h

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

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

//头插尾插
void SLPushFront(SLTNode** pphead, SLTDataType x);
void SLPushBack(SLTNode** pphead, SLTDataType x);

//头删尾删
void SLPopFront(SLTNode** pphead);
void SLPopBack(SLTNode** pphead);

// 单链表查找
SLTNode * STFind(SLTNode * phead, SLTDataType x);

// 在pos之前插入
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
void SLInsertAfter(SLTNode* pos, SLTDataType x);

// 删除pos位置的值
void SLErase(SLTNode** pphead, SLTNode* pos);

// 删除pos位置后面的值
void SLEraseAfter(SLTNode* pos);

// 单链表的销毁
void SListDestroy(SLTNode* plist);

LList.c

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "LList.h"

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

SLTNode* BuyLTNode(SLTDataType x)//为新元素开辟空间
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL) {
		perror("malloc fall");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

void SLPushFront(SLTNode** pphead, SLTDataType x)//头插
{
	assert(pphead);  // 链表为空,pphead也不为空,因为他是头指针plist的地址
	//assert(*pphead); // 不能断言,链表为空,也需要能插入
	SLTNode* newnode = BuyLTNode(x);//newnode是局部变量
	newnode->next = *pphead;//头插后首节点next指向原有的首节点
	*pphead = newnode;//将链表的头指针 *pphead 指向新插入的节点
}

void SLPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead); // 链表为空,pphead也不为空,因为他是头指针plist的地址
	SLTNode* newnode = BuyLTNode(x);
	//两种情况
	//空链表  非空链表
	if (*pphead == NULL)//链表为空改变结构体指针
		*pphead = newnode;

	else {//不为空,则改变结构体的节点
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
			tail = tail->next;
		
		tail->next = newnode;
	}		
}

void SLPopFront(SLTNode** pphead)
{
	assert(pphead); // 链表为空,pphead也不为空,因为他是头指针plist的地址
	assert(*pphead); // 链表为空,不能头删。(当然你还可以用温柔的检查)
	SLTNode* del = *pphead;//指针del用于释放节点空间
	*pphead = (*pphead)->next;
	free(del);
}

void SLPopBack(SLTNode** pphead)
{
	assert(pphead); // 链表为空,pphead也不为空,因为他是头指针plist的地址
	assert(*pphead); // 链表为空,不能头删。(当然你还可以用温柔的检查)

	//只有一个节点
	if ((*pphead)->next == NULL) {
		free(*pphead);
		*pphead = NULL;//修改头节点为空
	}
	else {
		//第一种增加前项变量
		//SLTNode* prev = NULL;
		//SLTNode* tail = *pphead;
		//while (tail->next) {
		//	prev = tail;
		//	tail = tail->next;
		//}
		//free(tail);
		//prev->next = NULL;

		//第二种不新增变量
		//改变结构体的节点
		SLTNode* tail = *pphead;
		while (tail->next->next) {
			tail = tail->next;
		}
		free(tail->next);//将指向的最后一个节点释放
		tail->next = NULL;
	}
}


SLTNode* STFind(SLTNode* phead, SLTDataType x)//找到返回链表地址
{
	SLTNode* cur = phead;
	while (cur) {
		if (cur->data == x)
			return cur;
		
		cur = cur->next;
	}
	return NULL;
}

// 在pos之前插入
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);
	if (*pphead == pos){//在头节点前插入等于头插
		SLPushFront(pphead, x);
	}
	else {
		SLTNode* prev = *pphead;//用于找到pos前的位置

		while (prev->next != pos){
			prev = prev->next;
		}

		SLTNode* newnode = BuyLTNode(x);
		prev->next = newnode;//pos前一个位置next指向新开辟节点
		newnode->next = pos;//新节点next指向pos
	}
}

// 在pos之后插入
void SLInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuyLTNode(x);
	//下面两行不能调换顺序,否则无法链接新节点后项节点
	newnode->next = pos->next;
	pos->next = newnode;
}

void SLErase(SLTNode** pphead, SLTNode* pos)// 删除pos位置的值
{
	assert(pphead);
	assert(*pphead);//链表为空则不能删除
	if (pos = *pphead){
		SLPopFront(pphead);
	}
	else {
		SLTNode* prev = *pphead;
		while (prev->next != pos) {//找到pos前一个节点
			prev = prev -> next;
		}
		prev->next = pos->next;//将pos前一个节点的next指向pos后一个节点
		free(pos);//释放pos空间
	}
}

void SLEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);//后项为空则不能删除
	SLTNode* next = pos->next;
	pos->next = next->next;
	free(next);
}

void SListDestroy(SLTNode* pphead)
{
	SLTNode* cur = pphead;
	SLTNode* tmp = NULL;
	while (cur != NULL) {
		tmp = cur;
		cur = cur->next;
		free(tmp);
	}
}

text.c

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

#include "LList.h"

void test1()
{
	SLTNode* plist = NULL;
	SLPushFront(&plist, 5);
	SLPushFront(&plist, 4); 
	SLPushFront(&plist, 3);
	SLPushBack(&plist, 6);

	//SLPopFront(&plist);

	SLTNode* pos = STFind(plist, 3);
	SLInsert(&plist, pos, 99);

	//pos = STFind(plist, 2);
	//if (pos)
	//{
	//	SLInsertAfter(pos, 20);
	//}
	
	//SLPopBack(&plist);
	//SLPopBack(&plist);
	//SLPopBack(&plist);
	//SLPopBack(&plist);
	//SLPopBack(&plist);

	SLTPrint(plist);
}

int main()
{
	test1();
	return 0;
}
相关推荐
时光の尘9 分钟前
C语言菜鸟入门·关键字·float以及double的用法
运维·服务器·c语言·开发语言·stm32·单片机·c
-一杯为品-18 分钟前
【51单片机】程序实验5&6.独立按键-矩阵按键
c语言·笔记·学习·51单片机·硬件工程
Lenyiin1 小时前
02.06、回文链表
数据结构·leetcode·链表
爪哇学长1 小时前
双指针算法详解:原理、应用场景及代码示例
java·数据结构·算法
爱摸鱼的孔乙己1 小时前
【数据结构】链表(leetcode)
c语言·数据结构·c++·链表·csdn
Dola_Pan1 小时前
C语言:数组转换指针的时机
c语言·开发语言·算法
烦躁的大鼻嘎2 小时前
模拟算法实例讲解:从理论到实践的编程之旅
数据结构·c++·算法·leetcode
IU宝2 小时前
C/C++内存管理
java·c语言·c++
qq_459730032 小时前
C 语言面向对象
c语言·开发语言
C++忠实粉丝2 小时前
计算机网络socket编程(4)_TCP socket API 详解
网络·数据结构·c++·网络协议·tcp/ip·计算机网络·算法