数据结构:单链表

单链表

单链表概念与结构

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

用生活实例比喻就好比我们日常做的高铁火车:

每个列车有n节车厢,每个车厢在组装前都是独立的,当我们需要时,会用一个一个的钩锁将每节车厢链接,每个钩锁就好比指针链接,他将两个不相干的车厢链接。

那链表的车厢(结构)时怎样的?

本质上就是通过结构体来存放数据与下一个结构体的地址,每个结构体被称为节点。

节点

图中plist存放的时第一个节点的地址(就是列车的车头),我们可以通过地址来找寻下一个节点,通过下一个节点里存放的地址就可以找到第二个节点,这就是节点的作用。

链表的性质

  1. 链式机构在逻辑上是连续的,在物理结构上不⼀定连续
  2. 结点⼀般是从堆上申请的
  3. 从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续
    节点(结构体)的编写:
c 复制代码
typedef int SLTDataType//如果要修改数据类型就只用在这里改一下,
//不用在整个程序里面替换数据类型,方面后续修改数据类型的需要
typedef struct SLTNode
{
 SLTDataType data; //结点数据
 struct SListNode* next; //指针保存下⼀个结点的地址
}SLTNode;

单链表的打印

前面我们说了可以通过每个节点存放的地址来找到下一个节点,我们打印链表就可以借助这个特点来打印。

c 复制代码
void SLTPrint(SLTNode* phead)
{
assert(phead);
SLTNode* pcur=phead;//保存链表地址,利用这个临时变量来读取内存
while(pcur)
{
	printf("%d->",pcur->data);
	pcur=pcur->next;
}
printf("NULL\n");
}

while循环判定条件的选择:

前面的结构让我们了解到最后一个节点里存放的指针是NULL;

当我们pcur为NULL时,代表我们通过循环访问了每一个节点,每访问一个节点就打印了它的数据,这样我们就打印出了整个链表的数据内容。

实现单链表

创建一个头文件SList.h

c 复制代码
#ifndef __SList_H_
#define __SList_H_

#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 SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//删除
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插⼊数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDestroy(SLTNode** pphead);
#endif

接下来在SList.c文件内一个一个实现头文件里面的函数。

创建新的节点

因为要插入数据,就必须创建节点,为了代码的可读性与简洁,我们将创建节点封装为一个函数,写在SList.c文件内部。

c 复制代码
SL_BuyNode(SLTDataType x)//x为要写入的数据
{
SLTNode* newnode=(SLTNode*)malloc(sizeof(SLTNode));//采用动态内存管理,方便后续的删除数据等操作
assert(newnode);//防止空间开辟失败
//初始化
newnode->data=x;//保存数据
newnode->next=NULL;//让它指向的下一个节点暂时为空
retuen newnode;//返回新节点的地址
}

在末尾插入数据

思路:

观察结构图,我们要在尾部插入节点,首先要创建一个新的节点,将新节点的地址赋给原来的末尾节点内的指针;

分析一下执行此操作需要的数据:

原末尾节点的地址

新节点的地址

代码实现:

c 复制代码
void SLTPushBack(SLTNode** pphead, SLTDataType x)//这里注意下我们的形参时二级指针
//原因:因为我们尾插时可能会遇到首地址phead为空,也就是一个节点都没有的情况
//所以我们会将节点的地址存放的首地址内部,这样我们就需要改变首地址的内容
//因为形参是实参的临时拷贝,形参的改变不会影响实参,想要形参改变的同时影响到实参,就要使用地址传参,这里接收数据就要使用二级指针
{
assert(pphead);
SLTNode* newnode=SL_BuyNode(x);//创建新节点
if(*pphead==NULL)//链表里面一个节点都没有
{
	*pphead=newnode;
}
else
{
	SLTNode* pcur=*pphead;//这里链表里面以及有节点了,防止改变首地址,我们借用临时变量来操作
	while(pcur->next)//当节点内的next指针指向空,我们就找到了原末尾节点地址
	{
		pcur=pcur->next;
	}
	pcur->next=newnode;
}
}

在头部插入数据

思路:

  1. 创建一个新节点
  2. 将原首节点的地址存放的新节点内部的指针里
  3. 将首地址plist内存放的地址改为新头节点的地址
    需要的数据:新节点地址,原头节点地址(plist存放的地址)
    代码实现:
c 复制代码
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode=SL_BuyNode(x);
	newnode->next=*pphead;
	*pphead=newnode;
}

删除尾部数据

思路:

  1. 找到倒数第二个节点
  2. 利用free函数释放最后一个节点的空间(最后一个节点的空间在倒数第二个节点内部存放)
  3. 让倒数第二节点内的指针指向NULL;
    需要的数据:倒数第二个节点地址
    代码实现:
c 复制代码
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	if((*pphead)->next==NULL)
	{
		free(*pphead);
		*pphead=NULL;
	}
	else
	{
		SLTNode* prv=*NULL;
		SLTNode* pcur=*pphead;
		while(pcur->next==NULL)
		{
			prv=pcur;
			pcur=pcur->next;
		}
		free(pcur);
		prv->next=NULL;
	}
}

删除第一个节点

思路:

  1. 将第二个节点的地址存放到plist
  2. 释放第一个节点空间
    需要的数据:第二个节点的地址
    代码实现:
c 复制代码
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* div = (*pphead)->next;//暂存第二个节点的地址
	free(*pphead);
	*pphead = div;
}

在链表中寻找目标数据

思路:

  1. 通过循环遍历每个节点
  2. 每个节点的数据与目标数据对比
  3. 找到了就返回该节点地址,没找到就返回NULL
    代码实现:
c 复制代码
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;;
}

在指定位置之前插入数据

思路:

  1. 创建新的节点
  2. 找到指定位置之前的节点地址
  3. 将新节点的地址存放的指定位置之前的节点中
  4. 将指定位置的节点地址存放到新节点里
    数据需求:指定位置,新的节点,指定位置前的节点地址
    指定位置由函数SLTFind提供。
c 复制代码
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);
	SLTNode* newnode=SL_BuyNode(x);
	if (pos == NULL)//当指定位置为NULL,说明该链表没有节点或者没有我们我要的数据,就将新节点作为头节点
	{
		SLTPushFront(pphead,x);
	}
	else
	{
		SLTNode* prv = *pphead;
		while (prv->next!=pos)
		{
			prv =prv->next;
		}
		newnode->next = pos;
		prv->next = newnode;
	}
}

在指定位置之后插⼊数据

思路:

  1. 创建新节点
  2. 找到指定位置
  3. 将指定位置后节点地址放到newnode里面
  4. 将newnode的地址放到指定位置节点里
c 复制代码
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode=SL_BuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

删除pos结点

思路:

  1. 找到该位置下一个节点地址并赋值给该位置前一个节点
  2. 释放该位置空间
    代码:
c 复制代码
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead&&*pphead);//当pphead为NULL,说明没有节点,防止free释放NULL
	assert(pos);
	if(pos==*pphead)//只有一个节点
	{
		SLTPopFront(pphead);
	}
	else//两个及两个以上
	{
		SLTNode* prv = *pphead;
		while (prv->next != pos)
		{
			prv = prv->next;
		}
		prv->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

删除pos之后的结点

思路:

  1. 找到pos的下下一个节点地址赋值给pos
  2. 释放pos下一个节点
    代码:
c 复制代码
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	SLTNode* div = pos->next;
	pos->next = div->next;//div->next等价于pos->next->next
	free(div);
	div = NULL;
}

销毁链表

思路:

  1. 将plist(*pphead)重新赋值为NULL
  2. 利用free释放后续节点
c 复制代码
void SListDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* div = pcur->next;
		free(pcur);
		pcur = div;
	}
	*pphead = NULL;
}

单链表测试

c 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void test()
{
	SLTNode* plist = NULL;
	//尾插1,2,3,4
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	//结果:1->2->3->4->NULL
	SLTPrint(plist);
	//头插
	SLTPushFront(&plist,0);
	//结果:0->1->2->3->4->NULL
	SLTPrint(plist);
	//尾删
	SLTPopBack(&plist);
	//结果:0->1->2->3->NULL
	SLTPrint(plist);
	//头删
	SLTPopFront(&plist);
	//结果:1->2->3->NULL
	SLTPrint(plist);
	//查找数据
	SLTNode* div=SLTFind(plist, 3);
	if (div)
	{
		printf("YES\n");
	}
	else
	{
		printf("NO!\n");
	}
	//在指定位置前插入
	SLTInsert(&plist,div,11);
	//结果:1->2->->11->3->NULL
	SLTPrint(plist);
	//在指定位置后插入
	SLTInsertAfter(div,12);
	//结果:1->2->->11->3->12->NULL
	SLTPrint(plist);
	//删除指定位置
	SLTErase(&plist, div);
	//结果:1->2->->11->12->NULL
	SLTPrint(plist);

	SListDestroy(&plist);
	SLTPrint(plist);

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

源码地址如下

https://gitee.com/xiao-li-learning-c-language/c-language-exercisehomework/commit/4556d2aa9e0f2506be4ae0f2e52b430650f7b2ec

-------------------------------------------------分隔符

单链表的基本结构与性质就介绍完了,出了我列举的几个函数外还有其他功能的函数,有兴趣的可以自行尝试,原理与上述函数差不多。

有错请在评论区指正,谢谢。

相关推荐
Source.Liu10 分钟前
【用Rust写CAD】第二章 第四节 函数
开发语言·rust
monkey_meng11 分钟前
【Rust中的迭代器】
开发语言·后端·rust
余衫马13 分钟前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng17 分钟前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
Jacob程序员19 分钟前
java导出word文件(手绘)
java·开发语言·word
懒大王就是我25 分钟前
C语言网络编程 -- TCP/iP协议
c语言·网络·tcp/ip
小白学大数据26 分钟前
正则表达式在Kotlin中的应用:提取图片链接
开发语言·python·selenium·正则表达式·kotlin
VBA633728 分钟前
VBA之Word应用第三章第三节:打开文档,并将文档分配给变量
开发语言
半盏茶香28 分钟前
【C语言】分支和循环详解(下)猜数字游戏
c语言·开发语言·c++·算法·游戏
小堇不是码农34 分钟前
在VScode中配置C_C++环境
c语言·c++·vscode