C语言实现单链表

前言

本人的技术笔记会优先发布在个人站点longyxblg.cn

顺序表的问题及思考

  • 中间\头部的插入和删除,时间复杂度为 O(N)

  • 空间扩容时,需要生成新空间,拷贝就空间中的数据,释放就空间。有不小的空间消耗。

  • 空间扩容后的空间不一定会全部使用,会造成空间上的浪费

为解决上述问题,可以用到一种新的数据结构------链表

链表(Linked List)

链表也是一种线性表,但是链表的物理结构上是非顺序、非连续的,数据的逻辑顺序是通过链表中的指针链接次序实现的

单链表(Single linked list)

单链表是链表的一种分支,如图所示

可以接住生活中的火车来帮助理解,车厢是链接的

每节车厢都是独立存在的,增加删除不会影响其他车厢。

单链表的每个节点都是独立存在的,增加删除也不会影响其他节点(或结点)。

单链表节点的结构

单链表节点的结构可以通过一个结构体来实现,结构体中存放的是记录的数据和指向下个节点的指针。本文演示的手写的不带头单链表存放的数据类型为 int 。每个节点都是动态开辟的空间。

c 复制代码
// 重命名方便修改
typedef int SLDataType;

typedef struct Node
{
	// 存放的数据
	SLDataType data;
	// 指向下个节点的指针
	struct Node* next;
}Node;

单链表的函数

头文件包含

c 复制代码
#include<stdio.h> // 标准库函数
#include<stdlib.h> // 主要使用动态申请内存
#include<assert.h> // 主要使用断言

申请节点

每个节点都是动态申请的空间,将这个过程封装为一个函数,动态申请的内存空间存在于堆区。

函数结束后,这块空间不会被系统收回。

c 复制代码
Node* BuyNode(SLDataType data)
{
	//申请一个节点
	//动态申请的内存空间存在于堆区。
	//函数结束后, 这块空间不会被系统收回
	Node* ps = (Node*)malloc(sizeof(Node));
	if (!ps)
	{
		// 判断是否申请成功
		perror("malloc error");
		exit(1);
	}
	// 申请成功插入数值
	ps->data = data;
	ps->next = NULL;
	return ps;
}

测试

c 复制代码
int main()
{
	// 测试申请节点
    Node* n = BuyNode(1);
	return 0;
}

插入节点

单链表的打印

从头节点开始遍历,遍历一个打印一个节点的数据,直到尾节点打印结束

c 复制代码
void Print_Node(const Node** pphead)
{
	// 打印整个链表
	// 只是打印,用个 const 关键字
	// 判空 传进来的这个 pphead 不能为 NULL
	assert(pphead);	
	if (!(*pphead))
	{
		printf("当前链表没有数据\n");
		return;
	}
	Node* pcur = *pphead; // 用 pcur 这个指针去遍历
	while (pcur)
	{
		// 用 pcur->next 作为判断条件的话,就不会打印尾节点的数据了
		printf("%d ", pcur->data);
		pcur = pcur->next;
	}
}

单链表头插

插入的节点将成为这个链表的头节点,这个节点指向原链表的头节点

如果这个链表是空的,那么插入的这个节点直接成为该链表的头节点,没有任何其他操作

c 复制代码
void PushFront(Node** pphead, int data)
{
	// 单链表的头插
	// pphead 表示原链表的头节点
	// 由于希望在函数内的操作影响到实参
	// 传入的是 Node* 这个一级指针的地址
	// 用 Node** 二级指针来接收

	// 调用 BuyNode 函数申请一个节点
	Node* newnode = BuyNode(data);
	if (!pphead)
	{
		// pphead 是空说明链表是空的
		// 那直接让这个新节点成为这个链表的头节点,不需要做其他操作
		pphead = newnode;
		return;
	}
	// 到这儿说明这不是一个空链表
	// 插入的节点将成为这个链表的头节点
	// 这个节点指向原链表的头节点
	// -> 优先级大于 *
	newnode->next = *pphead;
	// 让这个新节点成为新的头节点
	*pphead = newnode;
}

测试

c 复制代码
int main()
{
	// 测试头插节点
	Node* n1 = BuyNode(1);
	PushFront(&n1,2);
	PushFront(&n1,3);
	PushFront(&n1,4);
	Print_Node(&n1);
	return 0;
}

单链表尾插

遍历链表找到尾节点,让尾节点指向这个新插入的节点,新插入的节点成为尾节点

c 复制代码
void PushBack(Node** pphead, int data)
{
	// 单链表的尾插
	// 需要先找到尾节点
	// 尾节点的 next 没有指向,为 NULL
	// 传进来的 newnode 不能是空
	// 调用 BuyNode 函数申请一个节点
	Node* newnode = BuyNode(data);

	if (!pphead)
	{
		// pphead 是空说明链表是空的
		// 那直接让这个新节点成为这个链表的头节点,不需要做其他操作
		pphead = newnode;
		return;
	}
	Node* ptail = *pphead; // 用新指针去便利链表
	while (ptail->next)
	{
		// 继续向后遍历
		ptail = ptail->next;
	}
	// 出循环说明找到了 next 为 NULL 的节点
	// 就是尾节点
	// 让尾节点的 next 指向这个新节点
	ptail->next = newnode;
}

测试

c 复制代码
int main()
{
	// 测试尾插节点
	Node* n1 = BuyNode(1);
	PushBack(&n1,2);
	PushBack(&n1,3);
	PushBack(&n1,4);
	Print_Node(&n1);
	return 0;
}

链表查找

遍历这个链表查找

c 复制代码
Node* Find(Node** pphead, int data)
{
	// 单链表查找
	// 判空,链表不为空才能进行查找
	assert(pphead);
	Node* pcur = *pphead; // 用 pcur 这个指针去遍历链表
	while (pcur)
	{
		// 如果用 pcur->next 就不会查找最后一个节点
		if (pcur->data == data)
		{
			// 返回这个节点的指针
			return pcur;
		}
	}
	// 出循环说明遍历完了链表
	return NULL;
}

测试

c 复制代码
int main()
{
	// 测试查找函数
	Node* n1 = BuyNode(1);
	PushFront(&n1, 2);
	Node* test1 = Find(&n1,2);
	Node* test2 = Find(&n1,3);
	return 0;
}

指定位置前插入

找到指定位置,让指定位置前的节点

c 复制代码
void Push_Pos_Before(Node** pphead, Node* pos, SLDataType data)
{
	// 指定位置前插入
	// 判空,pos 和 pphead 都不能传空
	assert(pos && pphead);
	if (*pphead == pos)
	{
		// 这实际上就是头插
		// 调用头插函数
		PushFront(pphead, data);
		return;
	}
	Node* pcur = *pphead; // 用 pcur 去遍历链表
	Node* prev = *pphead; // 用 prev 来记录 pcur 前的一个节点

	while (pcur)
	{	
		if (pcur->data == pos->data)
		{
			// 找到了指定位置
			// 申请一个节点
			Node* newnode =  BuyNode(data);
			// 让指定位置前的这个节点指向这个新节点
			prev->next = newnode;
			// 让这个新节点指向指定位置节点
			newnode->next = pcur;
			break;
		}
		prev = pcur;// 记录 pcur 前的一个节点
		pcur = pcur->next;
	}
}

测试

c 复制代码
int main()
{
	// 测试指定位置前插入
	Node* n1 = BuyNode(1);
	PushFront(&n1, 2);
	PushFront(&n1, 3);
	PushFront(&n1, 4);
	Print_Node(&n1);
	printf("\n");
	Node* test1 = Find(&n1, 2);
	Push_Pos_Before(&n1, test1, 5);
	Print_Node(&n1);
	return 0;
}

指定位置后插入

新节点指向指定位置的节点指向的节点,指定位置节点指向这个新节点

c 复制代码
void Push_Pos_After(Node* pos, SLDataType data)
{
	// 指定位置后插入数据
	// 传进来的 pos 不能为空
	assert(pos);
	// 申请新节点
	Node* newnode = BuyNode(data);
	// 先让这个节点指向指定位置后的那个节点
	newnode->next = pos->next;
	// 再让指定位置节点指向这个新节点
	pos->next = newnode;
}

测试

c 复制代码
int main()
{
	// 测试指定位置后插入
	Node* n1 = BuyNode(1);
	Push_Pos_After(n1,2);
	Push_Pos_After(n1,3);
	Print_Node(&n1);
	return 0;
}

删除节点

链表的每个节点都是一块动态申请的内存空间,删除节点要用到 free

头删

让链表的头节点指向的下一个节点成为这个链表的新头节点,释放原头节点

c 复制代码
void PopFront(Node** pphead)
{
	// 单链表的头删
	// 判空,链表不为空才能进行删除,传进来的 pphead 也不能为 NULL
	assert(pphead && (*pphead));
	// 记录原头节点指向的下一个节点
	Node* newhead = (*pphead)->next;
	// 释放原头节点
	free((*pphead));
	// 让原头节点指向的下一个节点成为新的头节点
	(*pphead) = newhead;
}

测试

c 复制代码
int main()
{
	// 测试头删
	Node* n1 = BuyNode(1);
	PushBack(&n1, 2);
	PushBack(&n1, 3);
	PushBack(&n1, 4);
	Print_Node(&n1);
	printf("\n");

	PopFront(&n1);
	Print_Node(&n1);
	printf("\n");

	PopFront(&n1);
	Print_Node(&n1);
	printf("\n");

	PopFront(&n1);
	Print_Node(&n1);
	printf("\n");

	PopFront(&n1);
	Print_Node(&n1);
	printf("\n");

	PopFront(&n1);
	Print_Node(&n1);
	printf("\n");


	return 0;
}

尾删

c 复制代码
void PopBack(Node** pphead)
{
	// 单链表尾删
	// 判空,链表不为空才能进行删除,传进来的 pphead 也不能为 NULL
	assert(pphead && (*pphead));
	//找到当前链表的尾节点
	
	Node* pcur = *pphead; // 用 pcur 去遍历链表
	Node* ptail = pcur->next; // 用 ptail 去记录 pcur-next 指向的节点
	// 处理只有一个节点的情况
	if (!ptail)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	while (ptail->next)
	{
		pcur = pcur->next;
		ptail = ptail->next;
	}
	// 出来说明 ptail->next = NULL ,这是尾节点
	// 释放尾节点
	free(ptail);
	ptail = NULL;
	//指向这个尾节点的节点的 next 置空
	pcur->next = NULL;
}

测试

c 复制代码
int main()
{
	//测试尾删
	Node* n1 = BuyNode(1);
	PushBack(&n1, 2);
	PushBack(&n1, 3);
	PushBack(&n1, 4);
	Print_Node(&n1);
	printf("\n");

	PopBack(&n1);
	Print_Node(&n1);
	printf("\n");


	PopBack(&n1);
	Print_Node(&n1);
	printf("\n");

	PopBack(&n1);
	Print_Node(&n1);
	printf("\n");

  	PopBack(&n1);
	Print_Node(&n1);
	printf("\n");

	return 0;
}

指定位置删除

让指定位置前的节点指向指定位置后的节点,释放指点位置的节点

c 复制代码
void Pop_Pos(Node** pphead, Node* pos)
{
	// 指定位置删除
	//判空, pos 和 pphead 都不能传空
	assert(pphead && pos);
	
	Node* pcur = *pphead; // 用 pcur 遍历链表
	Node* prev = NULL; // 记录 pcur 前的节点
	if (*pphead == pos)
	{
		// 如果删除的是头节点就直接调头删函数
		PopFront(pphead);
		return;
	}
	while (pcur)
	{
		if (pcur == pos)
		{
			// 让指定位置前的节点指向指定位置后的节点
			prev->next = pcur->next;
			// 释放指点位置的节点
			free(pcur);
			return;	
		}
		prev = pcur;
		pcur = pcur->next;
	}
}

测试

c 复制代码
int main()
{
	Node* n1 = BuyNode(1);
	PushBack(&n1, 2);
	PushBack(&n1, 3);
	PushBack(&n1, 4);
	Print_Node(&n1);
	printf("\n");

	// 测试一直删头部节点的情况
	printf("测试一直删头部节点\n");
	Pop_Pos(&n1, n1);
	Print_Node(&n1);
	printf("\n");

	Pop_Pos(&n1, n1);
	Print_Node(&n1);
	printf("\n");

	Pop_Pos(&n1, n1);
	Print_Node(&n1);
	printf("\n");

	
	Node* n2 = BuyNode(1);
	PushBack(&n2, 2);
	PushBack(&n2, 3);
	PushBack(&n2, 4);
	Print_Node(&n2);
	printf("\n");

	// 测试删除中间数据的情况
	Node* find = Find(&n2, 2);
	printf("测试删除中间节点\n");
	Pop_Pos(&n2, find);
	Print_Node(&n2);
	printf("\n");

	// 测试删除尾节点的情况
	find = Find(&n2, 4);
	printf("测试删除尾节点\n");
	Pop_Pos(&n2, find);
	Print_Node(&n2);
	printf("\n");
	return 0;
}

单链表的销毁

使用完成后将每一个动态申请的节点释放

c 复制代码
void Destory(Node** pphead)
{
	// 单链表的销毁
	if (!(pphead && *pphead))
	{
		printf("当前已是空链表!\n");
		return;
	}
	Node* pcur = *pphead; // 用 pcur 去遍历链表
	Node* ptail = pcur->next; // 记录 pcur 的下一个节点
	while (pcur)
	{
		// pcur 为空的时候说明尾节点也已经完成释放
		// 记录下一节点
		ptail = pcur->next;
		// 释放当前节点
		free(pcur);
		pcur = ptail;
	}
	// 完成释放后将头节点指控
	*pphead = NULL;
}

测试

c 复制代码
int main()
{
	Node* n1 = BuyNode(1);
	PushBack(&n1, 2);
	PushBack(&n1, 3);
	PushBack(&n1, 4);
	Print_Node(&n1);
	printf("\n");

	Destory(&n1);
	Print_Node(&n1);

	Destory(&n1);
	return 0;
}
相关推荐
萌动的小火苗19 分钟前
Linux进程线程面试题【无答案】
linux·运维·服务器·c语言·开发语言
春风解人意1 小时前
从零开始学习嵌入式P29----线程间的通信
c语言·嵌入式硬件·学习
这是程序猿2 小时前
深入剖析Java HashMap:底层原理、数据结构与核心机制全解析
java·开发语言·数据结构
从零开始的嵌入式之旅2 小时前
day32
linux·c语言·经验分享·笔记·嵌入式硬件
一条破秋裤3 小时前
C-数据类型
c语言·开发语言·chrome
xx~t3 小时前
嵌入式学习——进程与线程2
linux·c语言·学习·进程与线程
HanhahnaH3 小时前
各数据结构操作的时间复杂度汇总
数据结构·算法
_wxd6663 小时前
排序-堆排序(Heap Sort)
数据结构