【数据结构】单向链表实现 超详细

目录


前言:(受篇幅限制,为了划清知识模块,进行分章节讲解)

若想了解清楚 链表的概念和分类 

可以点击跳转 这一章节—-> :【数据结构】链表的概念 及 分类 (使用比喻解释概念)

一. 单链表的实现

1.准备工作及其注意事项
1.1 先创建三个文件

 解释这三个文件的作用
 1、头文件SList.h  是来声明接口函数,定义链表,将几个公共用到的库函数集合起来
 2、源文件SList.c  是用来具体实现接口
 3、源文件test.c  用于接口的测试工作 ,即具体的使用场景

1.2 注意事项:帮助高效记忆和理解

1. 但凡是删除,必须 断言 链表不能为 空,避免传过来的是NULL指针 assert(*pphead);
2. 传递二级指针,要断言 不能为 NULL ,指针不能为空:assert(pphead);
3. 指定位置 pos 时,要确保pos存在:assert(pos);
4. (pphead)->next;  解引用前一定要加 括号,* 号 优先级 < 箭头操作符
5. 链表的所有插入接口:链表为空: 没有节点 就直接插入
6.要找 前一个节点 prev 或 后一个节点 next 时, 一定要确保 其本身存在!!!!
7.当指定位置 pos 刚好是 头节点时 就没有 prev 了

2.链表的基本功能接口
2.0 创建一个 链表
// 创建链表节点结构体
// 和顺序表创建原理相同,可以看我的 【顺序表】章节
typedef  int  SLDataType;
typedef struct SListNode
{
	SLDataType data;
	struct SListNode* next;
}SLTNode;
2.1 链表的打印
// 打印函数
//注意这里的 phead 仅表示单向链表传递过来的 第一个节点地址,不是 双向链表的带头节点 
void SLTPrint(SLTNode* phead)
{
	SLTNode* ptemp = phead;
	while (ptemp) // ptemp != NULL 说明为是有效的节点,则循环打印
	{
		printf("%d -> ", ptemp->data);
		ptemp = ptemp->next; // 更新 ptemp :保存下个节点的地址:遍历节点的关键
	}
	printf("NULL\n");
}
 3.链表的创建新节点接口
// 创建新节点函数:所有的插入接口,都需要 创建新节点
SLTNode* STLCreatNode(SLDataType x)
{
	// 先创建 一个 新节点
	SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
	newNode->data = x;
	newNode->next = NULL;
	return newNode;
}
4.链表的节点插入功能接口
4.1 尾插接口

链表的尾插法
几种情况
1、链表非空:先找到尾节点,然后直接将尾节点 的 next 指向 新节点,(形成链接), 新节点 则 指向 NULL 变成 新的尾节点
2、链表为空:将头指针phead更新 成 新插入节点的地址,然后新节点 next 指向 NULL

// 注意:plist 头指针是 时刻更新为了指向头节点,当创建新的头节点后,要更新 plist ,而若要真正改变 plist 的值,需要传地址
// 传递过来的 头指针 &plist 是 一级指针 plist 的地址,要用 二级指针 pphead 来接收 ; 而 *pphead == plist 
// void SLTPushBack(SLTNode* phead, SLDataType x) // 错误写法:传值

void SLTPushBack(SLTNode** pphead, SLDataType x)
{
	assert(pphead); // 传递过来的 一级指针的地址 二级指针 不能为 NULL
	// 先创建 一个 新节点
	SLTNode* newNode = STLCreatNode(x);

	// 1、链表为空
	if (*pphead == NULL)
	{
		*pphead = newNode;
		return;
	}

	// 2、链表非空
	SLTNode* ptail = *pphead; // 因为要找 尾节点:干脆命名为 tail
	//本质是:找到尾节点的地址,而不是 尾节点的 next
	while (ptail->next) 
	{
		ptail = ptail->next;
	}
	ptail->next = newNode;
}

4.2 头插接口  

void SLTPushFront(SLTNode** pphead, SLDataType x)
{
	assert(pphead); // 传递过来的 一级指针的地址 二级指针 不能为 NULL
	// 先创建 一个 新节点
	SLTNode* newNode = STLCreatNode(x);

	newNode->next = *pphead; // 先将原本的 头指针放在 新节点的next
	*pphead = newNode; // 更新 头指针
}
  4.3 指定位置 pos 之前 插入接口

// 在指定位置节点pos 之前插入数据 
// 思路:找三个节点 :prev     newNode      next 
//                  前      本身新节点     后         为了找到 地址 才能将三者链接起来
// 注意:要找 前一个节点 prev 时, 一定要确保 其本身存在!!! 
// 当 pos 刚好是 头节点时 就没有 prev 了,特殊情况, 否则会因为找不到prev而 程序奔溃
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
	assert(pphead); // 指针不能为空
	assert(pos); // pos 不能为空
	assert(*pphead); // 链表不能为空: pos 是链表中 的一个有效节点,pos 不为空,链表也不能为 空

	// 创建新节点
	SLTNode* newNode = STLCreatNode(x);
	if (*pphead == pos)
	{
		// 当 pos 刚好是 头节点时 就用 头插法:调用之前写的函数接口
		SLTPushFront(pphead, x);
		return;
	}
	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	// 三者关联起来:  prev -> newNode -> pos
	newNode->next = pos;
	prev->next = newNode;
}
 4.4 指定位置pos 之后 插入接口(推荐)

// 在指定位置节点pos 之后插入数据: 注意一下错误写法: 和正确写法刚好相反
// 特点:有了 pos 就不需要 头节点了!!
void SLTInsertAfter(SLTNode*pos, SLDataType x)
{
	assert(pos); // pos 不能为空
	// 创建新节点
	SLTNode* newNode = STLCreatNode(x);
	newNode->next = pos->next;
	pos->next = newNode;
}
5.链表表的删除功能接口
5.1 尾删接口

// 链表的尾删法
// 思路:删除尾节点 以及  前一个节点 pre 的next 要置为 NULL
// 要注意 只有一个节点时,没有前置节点 pre!!!!
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead); // 一级指针为plist 指向头节点,传递过来的 一级指针的地址二级指针不能为 NULL
	assert(*pphead); // 一级指针为plist 指向头节点, *pphead == plist 这个指针也不能为 NULL 不能为空链表

	// 链表非空
	// 链表只有一个节点时
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	// 链表有多个节点
	SLTNode* ptail = *pphead;
	SLTNode* prev = NULL; // 时刻更新保存 
	while(ptail->next)
	{ 
		prev = ptail;
		ptail = ptail->next;
	}
	prev->next = NULL;
	free(ptail);
	ptail = NULL;
}
5.2头删接口

// 链表的头删法
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead); // 指针不能为空
	assert(*pphead); // 链表不能为空
	// 让第二个节点成为新的头节点
	// 将 旧的节点释放掉: 注意 要先将 第二个节点的地址  (*pphead) -> next  临时储存起来!!
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}
5.3 删除 指定位置 pos 节点 接口
// 删除pos 节点
// 删除 pos 节点后,还要 将 prev 和 next (pos的前后两个节点) 链接上!!: 比较麻烦
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead); // 指针不能为空
	assert(pos); // pos 不能为空
	assert(*pphead); // 链表不能为空: pos 是链表中 的一个有效节点,pos 不为空,链表也不能为 空

	//先 让 prev 链接上 next,后 free 释放掉 pos
	// 遇到 需要找 prev 的,一定要 检查是否存在 prev
	if (*pphead == pos)//检查是否存在 prev
	{
		free(pos);
		*pphead = NULL;
		return;
	}
	SLTNode* prev = *pphead;
	while (prev->next != pos) // 找 前面节点 prev
	{
		prev = prev->next;
	}
	prev->next = pos->next; // 先赋值后 再free释放
	free(pos);
	pos = NULL;
}
 5.4 删除  指定位置 pos ==之后== 的一个 节点 接口

// 删除pos 之后的节点
// 直接 删除 pos 之后的 节点 pos->next  
// 先让 pos 和 pos -> next -> next 链接起来,后 删除 节点 pos -> next 
// 注意:像这类找 上一个节点 prev 或 找下一个节点 next :都需要 检查 是否存在 该节点
void SLTEraseBeind(SLTNode* pos)
{
	assert(pos); // pos 不能为空
	assert(pos->next); // 这个节点必须存在,否则该函数无意义! 链表至少要有两个节点
	 // 三者的关联关系:pos    pos->next    pos->next->next
	 // 注意:下面 先 pos->next = pos->next->next, 后 free(pos->next) 是不对的  
     // pos->next 已经更新,不是你所认为的原来的那个 中间节点了,因此要先 临时保存起来!!!
	SLTNode* temp = pos->next;
	pos->next = pos->next->next;
	free(temp);
	temp = NULL;
}
 6.链表的  查找  接口
// 链表的查找
SLTNode* SLTFind(SLTNode** pphead, SLDataType x)
{
	// 遍历链表
	assert(pphead); // 指针不能为空
	SLTNode* ptemp = *pphead;
	while (ptemp)
	{
		if (ptemp->data == x) return ptemp; // 找到了就返回节点
		ptemp = ptemp->next;
	}
	// 没找到
	return NULL;
}
7.链表的  销毁  接口
// 销毁链表
// 一旦涉及到 动态内存申请,不要忘记销毁
void SListDestory(SLTNode** pphead)
{
	assert(pphead); // 指针不能为空
	assert(*pphead); // 链表不能为空,空的没必要销毁了
	// 顺序表是一段连续的空间,可以执行一次free全部销毁,而链表节点是独立的,需要遍历节点一个一个销毁
	 // 不能直接 一个一个 free 下去,需要 保存 next 节点,才能找到下个节点!!!!
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	// 当所有节点销毁后,需要 将头指针 销毁
	*pphead = NULL;
}

二、总代码

SList.h

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

// 零、创建链表节点
typedef int SLDataType;
typedef struct SListNode
{
	SLDataType data;
	struct SListNode* next;
}SLTNode;

// 一、打印函数: 这里用传址
void SLTPrint(SLTNode* ps);

// 二、尾插法
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);

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

// 八、指定位置 pos 之后 插入
void SLTInsertAfter(SLTNode* pos, SLDataType x);

// 九、删除 指定 pos 节点
void SLTErase(SLTNode** pphead, SLTNode* pos);

// 十、删除指定 pos 之后 的节点
void SLTEraseAfter(SLTNode* pos);

// 十一、销毁链表
void SLTDestory(SLTNode** pphead);

SList.c

#include"SList.h"

// 一、打印函数: 这里用传址
void SLTPrint(SLTNode* phead)
{
	SLTNode* ptemp = phead;
	while (ptemp) //  ptemp != NULL 表示为有效节点 
	{
		printf("%d -> ", ptemp->data);
		ptemp = ptemp->next;
	}
	printf("NULL\n");
}

// 创建节点函数
SLTNode* SLTCreatNode(SLDataType x)
{
	SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
	newNode->data = x;
	newNode->next = NULL;
	return newNode;
}
// 二、尾插法
void SLTPushBack(SLTNode** pphead, SLDataType x)
{
	assert(pphead);
	// 创建新节点
	SLTNode* newNode = SLTCreatNode(x);
	// 链表为空: 没有节点 就直接插入
	if (*pphead == NULL)
	{
		*pphead = newNode;
		return;
	}
	// 链表非空:找尾节点
	SLTNode* ptail = *pphead;
	while (ptail->next)
	{
		ptail = ptail->next;
	}
	ptail->next = newNode;
}

// 三、头插法
void SLTPushFront(SLTNode** pphead, SLDataType x)
{
	assert(pphead);
	// 创建新节点
	SLTNode* newNode = SLTCreatNode(x);
	// 先 链接 第一个节点,  没有第一个节点就是 NULL,也可以直接给 newNode->next,后更新 *pphead
	newNode->next = *pphead;
	*pphead = newNode;
}

// 四、尾删法
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	// 先找尾节点 和 前置节点 prev,后删除+链接
	// 当只有一个 节点时
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	// 当有 两个 节点 及以上
	SLTNode* ptail = *pphead;
	SLTNode* prev = NULL;
	while (ptail->next)
	{
		prev = ptail;
		ptail = ptail->next;
	}
	prev->next = NULL;
	free(ptail);
	ptail = NULL;
}

// 五、头删法
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	// 让 *pphead 指向第二个节点,free 掉第一个
	SLTNode* next = (*pphead)->next; // 一定要加 括号,* 号 优先级 < 箭头操作符 
	free(*pphead);
	*pphead = next;
}

// 六、链表的查找
SLTNode* SLTFind(SLTNode** pphead, SLDataType x)
{
	assert(pphead);
	// 遍历
	SLTNode* ptemp = *pphead;
	while (ptemp)
	{
		if (ptemp->data == x) return ptemp;
		ptemp = ptemp->next;
	}
	return NULL;
}

// 七、指定位置 pos 之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
	assert(pphead);
	assert(pos);
	assert(*pphead);
	// 涉及到三个节点: prev    newNode   pos
	// 创建新节点
	SLTNode* newNode = SLTCreatNode(x);
	if (*pphead == pos)// 无 prev 的情况
	{
		SLTPushFront(pphead, x);
		return;
	}
	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	newNode->next = pos;
	prev->next = newNode;
}

// 八、指定位置 pos 之后 插入(推荐!)
void SLTInsertAfter(SLTNode* pos, SLDataType x)
{
	assert(pos);
	// 创建新节点
	SLTNode* newNode = SLTCreatNode(x);
	newNode->next = pos->next;
	pos->next = newNode;
}

// 九、删除 指定 pos 节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	assert(*pphead);
	// 关系三个节点:prev    pos    next
	// 先找 prev
	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
	pos = NULL;
}

// 十、删除指定 pos 之后 的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next); // 注意  pos->next 这个必须存在
	// 关系三个节点: pos      pos->next     pos->next->next
	// 另一种可能:     pos      pos->next     NULL
	if (pos->next->next == NULL)
	{
		free(pos->next);
		pos->next = NULL;
		return;
	}
	// 直接销毁  pos->next  会找不到 pos->next->next,先保存
	SLTNode* ptemp = pos->next->next;
	free(pos->next);
	pos->next = NULL;
	pos->next = ptemp;
}

// 十一、销毁链表
void SLTDestory(SLTNode** pphead)
{
	assert(pphead);
	// 遍历销毁
	if (*pphead == NULL)return;
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	// 别忘了将 头指针销毁!!!
	*pphead = NULL;
}

test.c

#include"SList.h"

void SLTest1()
{
	// 零、创建 链表
	SLTNode* plist = NULL;

	// 二、尾插法
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4); // 1 -> 2 -> 3 -> 4 -> NULL
	printf("测试尾插:");
	SLTPrint(plist);

	// 三、头插法
	SLTPushFront(&plist, 5);
	SLTPushFront(&plist, 6); // 6 -> 5 -> 1 -> 2 -> 3 -> 4 -> NULL
	printf("测试头插:");
	SLTPrint(plist);

	// 四、尾删法
	SLTPopBack(&plist);
	SLTPopBack(&plist); // 6 -> 5 -> 1 -> 2 -> NULL
	printf("测试尾删:");
	SLTPrint(plist);

	// 五、头删法
	SLTPopFront(&plist);
	SLTPopFront(&plist); // 1 -> 2 -> NULL
	printf("测试头删:");
	SLTPrint(plist);

	// 六、链表的查找
	SLTNode* FindRet = SLTFind(&plist, 2);
	printf("测试查找:");
	if (FindRet) printf("找到了\n");
	else printf("没找到\n");

	// 七、指定位置 pos 之前插入
	SLTNode* FindRet1 = SLTFind(&plist, 2);
	SLTNode* FindRet2 = SLTFind(&plist, 1);
	SLTInsert(&plist, FindRet1, 200);
	SLTInsert(&plist, FindRet2, 100);// 100 -> 1 -> 200 -> 2 -> NULL
	printf("测试指定位置之前插入:");
	SLTPrint(plist);

	// 八、指定位置 pos 之后 插入(推荐!)
	SLTNode* FindRet3 = SLTFind(&plist, 2);
	SLTInsertAfter(FindRet3, 200); // 100 -> 1 -> 200 -> 2 -> 200 -> NULL
	printf("测试指定位置之后插入:");
	SLTPrint(plist);

	// 九、删除 指定 pos 节点
	SLTNode* FindRet4 = SLTFind(&plist, 1);
	SLTErase(&plist, FindRet4);// 100 -> 200 -> 2 -> 200 -> NULL
	printf("测试删除指定节点:");  
	SLTPrint(plist);

	// 十、删除指定 pos 之后 的节点
	SLTNode* FindRet5 = SLTFind(&plist, 200); // 注意如果 通过 Find 函数 找 节点,节点中有重复数据,返回 第一个遇到的
	SLTEraseAfter(FindRet5);// 100 -> 200 -> 200 -> NULL
	printf("测试删除指定节点:"); 
	SLTPrint(plist);

	//
}
int main()
{
	SLTest1();
	return 0;
}

完。

若上述文章有什么错误,欢迎各位大佬及时指出,我们共同进步!

版权声明:本文为博主作者:时差freebright原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/2301_79499548/article/details/135959835

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2024年2月19日
下一篇 2024年2月19日

相关推荐