【数据结构】链表

前言

本篇博客我们继续来总结一个与顺序表一样也属于线性表的一种数据结构------链表,以及单链表如何用代码实现

个人主页:小张同学zkf

若有问题 评论区见

感兴趣就关注一下吧

目录

1.链表的结构与概念

2.单链表的代码实现

以下是单链表全部代码


1.链表的结构与概念

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

我们可以把链表比作一个火车,每节车厢不是连续在一起的,是单独存在的,增或减几节车厢,就在对应车厢处,直接加上或删减,这个过程不影响其他车厢,车厢是独立存在的,且每节车厢都有车门。想象一下这样的场景,假设每节车厢的车门都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带一把钥匙的情况下如何从车头走到车尾?


最简单的做法: 每节车厢里都放一把下一节车厢的钥匙。
在链表里,每节"车厢"是什么样的呢?

与顺序表不同的是,链表里的每节"车厢"都是独立申请下来的空间,我们称之为" 结点/节点 "
节点的组成主要有两个部分: 当前节点要保存的数据保存下一个节点的地址(指针变量)
图中指针变量 plist保存的是第一个节点的地址,我们称plist此时"指向"第一个节点,如果我们希
望plist"指向"第二个节点时,只需要修改plist保存的内容为0x0012FFA0。
为什么还需要指针变量来保存下一个节点的位置?
链表中每个节点都是独立申请的(即需要插入数据时才去申请一块节点的空间),我们需要通过指针变量来保存下一个节点位置才能从当前节点找到下一个节点。
结合前面总结的结构体知识,我们可以给出每个节点对应的结构体代码:
假设当前保存的节点为整型:

struct SListNode
{
int data; // 节点数据
struct SListNode * next ; // 指针变量⽤保存下⼀个节点的地址
};
当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要保存整型数
据,也需要保存下一个节点的地址(当下一个节点为空时保存的地址为空)。
当我们想要从第一个节点走到最后一个节点时,只需要在前一个节点拿上下一个节点的地址(下一个节点的钥匙)就可以了。
给定的链表结构中,如何实现节点从头到尾的打印?

由图我们可以用pcur变量赋值头节点地址,然后给一个while循环,打印一次数据,再使pcur赋值成下一个节点的地址 ,直到指向尾节点的空指针,退出循环

补充

1、链式机构在逻辑上是连续的,在物理结构上不一定连续
2、节点一般是从堆上申请的
3、从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能连续,可能不连续


2.单链表的代码实现

跟顺序表一样,也需要三个文件,一个是测试我们写的链表的.c文件,一个是函数声明以及结点结构体定义的.h文件,最后一个是函数定义的.c文件

首先,我们还是要把单链表的功能函数全部声明一下,以及节点结构体的定义和头文件的包含都在.h里打完

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int leixing;
typedef struct SListNode
{
	leixing data;
	struct SListNode* next;
}SLN;//定义单链表节点
void SLNprint(SLN* pphead);//打印
void SLNpushback(SLN** pphead,leixing x);//尾插
void SLNpushfront(SLN** pphead, leixing x);//头插
void SLNpopback(SLN** pphead);//尾删
void SLNpopfront(SLN** pphead);//头删
SLN* SLNnewnoad(leixing x);//申请新节点
SLN* SLNfind(SLN* phead, leixing x);//查找
void SLNinsrt(SLN** pphead, SLN* pos, leixing x);//指定位置前插
void SLNhoucha( SLN* pos, leixing x);//指定位置后插
void shanchu(SLN** pphead,SLN* pos);//将指定节点删除
void chanchuhou(SLN* pos);//将指定位置之后的节点删除
void xiaohui(SLN** pphead);//销毁链表

注意这里的类型,我们在链表里的数据是随机类型的,所以我们可以把类型名更改一下,这样有利于转换链表里的类型,所以要添上一行代码------------typedef int leixing; 这里我们暂定int类型,要是想更改链表里的数据类型,直接在这里把int改成你想要的类型就可以了


单链表打印我们上面总结过了,这里我们直接开始插入,像顺序表一样分为头插入和尾插入,但是插入肯定需要新节点的生成,所以我们先来定义一下申请新节点的函数

我们分析一下,申请新节点,我们肯定要返回这个新节点的地址,在堆区上申请用malloc函数,申请完,有可能申请失败,若失败,提前返回并报错,若没有问题,让新的节点数据初始化,以及指针初始化,传进来的参数就是对应的节点里的数据,代码如下


新节点函数定义完,就可以进行插入了,我们先来看尾插,我们可以画图分析一下

如图,要是在尾部进行插入,那么原来的尾结点的空指针,就需要指向新申请的节点地址,这样就OK了

这里需要注意一点,我们传进来的得是头节点指针的地址,如果我们传进来是头结点指针,改变头结点指针的指向,不影响实参变化,相当于传了值,但我们要影响实参,所以我们要传进去的是头节点的地址,所以对应的形参就要用二级指针来接受,这就是所谓的"传址调用"。既然要对头结点指针的地址解引用,那确保传进来的不是空指针,所以,我们要assert断言,我们要再想一下极端情况,若传进来的节点是空节点那,如果是空节点,那么现在的头结点是不是就是我们刚申请的节点,这样考虑就完全了,若不为空节点,那就是正常找尾结点,用while循环找尾结点,最后将尾结点里的空指针指向新节点的地址

把指针当做参数传过去,不是我们说的传址调用,你要看这个指针指向的地址里的数据是否发生改变,若只是指针的指向的地址发生改变,相当于是指针的值发生改变,那这只是传值调用,真正的传址调用是将地址传过去,里面储存的数据发生改变,从而影响实参,这才是所谓的"传址调用",下面的大部分函数都需要传址调用,所以在这里一定要想清楚!!!!!!!!!!!!!!


尾插函数定义完就是头插函数定义,我们还是画图分析

这个函数依旧需要二级指针,那对这个指针需要解引用,防止它传来空指针,所以要assert断言,申请完新节点后,由图可知,新节点里的指针需要指向原头节点的地址,那现头结点的地址就变为了新节点的地址


有插入肯定有删除,那么接下来,我们就来定义尾删与头删,先来定义一下尾删

还是画图分析

头删相当于是把头结点空间释放,让下一个节点成为头结点

我们要注意一点,这回assert断言不仅是二级指针pphead,还要断言*pphead,因为在删时,节点不能为空,空节点怎么删那,创建个临时变量记录头结点的下一个节点,然后释放掉头结点,最后将临时变量赋值给头结点


头删完,我们来看尾删,我们依旧画图分析。

由图分析将尾结点释放掉,再将尾结点的上一个节点里的指针变为空,我们得考虑一个特殊情况,

当链表里只有一个节点时,那是不是直接释放掉这个节点就行了,再把头结点置空,看下代码

一共两种情况,当不只有一个节点时,我们要需要两个临时变量,从头节点开始,一个变量通过while循环找到尾节点,另一个变量则找到尾结点的上一个节点,都找到之后,再free掉尾结点,让那个变量为空,再让另一个记录原尾结点上一个节点里的指针置空成为新的尾结点


接下来我们定义一个查找函数,方便我们可以在链表里查找数据,那此时我们是不是不需要再传二级指针了,它要的是节点里的数据,那我们只要while循环遍历一下链表里每个节点里的数据就行了,找到了返回这个有这个数据的节点,没找到就返回空指针,代码如下


接下来我们来定义在指定节点前后插入新节点,我们先来看在指定位置前插入新节点

我们还得要画图

由声明可知,函数里有个节点类型,这个就是要在这个节点处之前插入新的节点,我们还是想要考虑断言,二级指针解引用需断言判断是否为空,既然里面都有指定节点,那链表里不可能是空节点,所以还要断言这个,而且这个规定的节点也不能为空吧,所以也得断言这个。接下来我们排除这些情况后,再考虑一下,假如这个指定节点是头结点那,那是不是就是头插了, 这个是一种特殊情况我们得考虑,接下来我们就正式由图分析一下,这个pos就是指定的节点,在这个节点前面要插新节点,我们既然需要新节点,所以肯定要调用申请新节点的函数,让新节点的下一地址指向pos,让原来在pos前的节点指向新节点。代码如下

我们要用while循环找到pos前一个节点,先让新节点指向pos,再让我们找到的前一个节点指向新的节点。


我们再来看在指定节点后插入节点

这个其实就简单,我们用不到头结点,所以只有两个参数,我们画图可以得知,只需要把pos节点指向新节点,新节点指向pos下一个节点,就可以,但这里我们要考率一个问题,就如图中是先红色,还是先绿色,答案是第一种得先让新节点指向pos的下一个节点,再让POS节点指向新节点,为什么第二种不可以那

第二种如果先让POS指向新节点,那新节点如何让他指向POS原来的下一个节点呀,现在POS下一个节点是已经是新节点了,找不到原来的下一个节点了,除非给个临时变量将POS原来的下一个节点给记录,否则是错误的。

代码如下:

哦对了别忘了pos断言


我们再来定义一个删除指定节点的函数

由图可知,删除掉指定节点, 我们需要知道指定节点的前后节点,让指定删除节点的前节点指向这个删除节点的后节点,再释放掉这个删除节点就OK了

我们还是先把该断言的断言,用变量从头节点遍历,找到删除节点的前节点,然后前节点指向后节店,最后free掉这个删除的节点

但是我们是不是没有考虑到特殊情况,假如这个删除节点是头结点那,以上推论都只能适用于删除节点不是头节点,但如果是头结点那,那不就是头删吗,调用一下头删函数就行了

完整代码如下


我们来继续定义一个函数用来删除指定节点之后的节点

由图可知,直接将POS指向POS下一个节点的下一个节点,再free掉POS的下一个节点,但是我们不能直接像图上那样这么直接,图上乍一看没有什么毛病,但仔细瞅瞅,会发现free掉的不是POS->next,而是POS->next->next,所以我们需要一个临时变量记录pos->next,代码如下

注意这个函数要求pos和pos->next都不能为空,所以要断言


最后就是链表销毁函数的定义了

这个就简单了,遍历一个节点,free掉一个,直到遍历完,再把头结点置空就销毁了


所有的函数定义完,别忘了在test.c文件中检测一下,写的链表对不对


以下是单链表全部代码

单链表.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int leixing;
typedef struct SListNode
{
	leixing data;
	struct SListNode* next;
}SLN;//定义单链表节点
void SLNprint(SLN* pphead);//打印
void SLNpushback(SLN** pphead,leixing x);//尾插
void SLNpushfront(SLN** pphead, leixing x);//头插
void SLNpopback(SLN** pphead);//尾删
void SLNpopfront(SLN** pphead);//头删
SLN* SLNnewnoad(leixing x);//申请新节点
SLN* SLNfind(SLN* phead, leixing x);//查找
void SLNinsrt(SLN** pphead, SLN* pos, leixing x);//指定位置前插
void SLNhoucha( SLN* pos, leixing x);//指定位置后插
void shanchu(SLN** pphead,SLN* pos);//将指定节点删除
void chanchuhou(SLN* pos);//将指定位置之后的节点删除
void xiaohui(SLN** pphead);//销毁链表

单链表.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "单链表.h"
void SLNchushihua(SLN* phead)
{
	phead->data = 0;
	phead->next = NULL;
}
void SLNprint(SLN* phead)//打印
{
	SLN* cur = phead;
	while (cur)
	{
		printf("%d->",cur->data);
		cur =cur ->next;
	}
	printf("NULL\n");
}
SLN* SLNnewnoad(leixing x)//申请新节点
{
	SLN* slnnewnoad = (SLN*)malloc(sizeof(SLN));
	if (slnnewnoad == NULL)
	{
		perror("malloc");
		exit(1);
	}
	slnnewnoad->data = x;
	slnnewnoad->next = NULL;
	return slnnewnoad;
}
void SLNpushback(SLN** pphead,leixing x)//尾插
{
	assert(pphead);    
	SLN* new = SLNnewnoad(x);
	if (*pphead== NULL)//要是传过来的节点是空节点,此刻头节点就是新节点
	{
		*pphead= new;
	}
	else {
		SLN* cur = *pphead;
		while (cur->next)//找尾
		{
			cur = cur->next;
		}
		cur->next = new;
	}
}
void SLNpushfront(SLN** pphead, leixing x)//头插
{
	assert(pphead);
	SLN* new = SLNnewnoad(x);
	new->next = *pphead;
	*pphead = new;
}
void SLNpopback(SLN** pphead)//尾删
{
	assert(pphead);
	assert(*pphead);
	//当链表只有一个节点时
	if ((*pphead)->next == NULL)//->优先级高于*
	{
		free(*pphead);
		(*pphead)=NULL;
	}
	//不止有一个节点时
	else
	{
		SLN* b = *pphead;
		SLN* c = *pphead;
		while (c->next)
		{
			b = c;
			c = c->next;
		}
		free(c);
		c = NULL;
		b->next = NULL;
	}
}
void SLNpopfront(SLN** pphead)//头删
{
	assert(*pphead);
	assert(pphead);
	SLN* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}
SLN* SLNfind(SLN* phead, leixing x)//查找
{
	SLN* chenjiaxu = phead;
	while (chenjiaxu)
	{
		if (chenjiaxu->data == x)
		{
			return chenjiaxu;
		}
		chenjiaxu = chenjiaxu->next;
	}
	return NULL;
}
void SLNinsrt(SLN** pphead,SLN* pos, leixing x)//指定节点前插入新节点
{
	assert(pphead && pos);
	assert(*pphead);
	SLN* new = SLNnewnoad(x);
	if (pos == *pphead)//指定节点是头结点,是头插
	{
		SLNpushfront(pphead,x);
	}
	else//指定节点不是头结点
	{
		SLN* as = *pphead;
		while (as->next != pos)
		{
			as = as->next;
		}
		new->next = pos;
		as->next = new;
	}
}
void SLNhoucha( SLN* pos, leixing x)//指定节点后插入新节点
{
	assert(pos);
	/*if (pos->next == NULL)
	{
		SLNpushback(, x)
	}*/
	SLN* new = SLNnewnoad(x);
	new->next = pos->next;
	pos->next = new;
}
void shanchu(SLN** pphead, SLN* pos)//删除指定节点
{
	assert(pphead&&*pphead);
	assert(pos);
	//当pos是头结点时
	if (pos == *pphead)
	{
		SLNpopfront(pphead);
	}
	else//不是头结点时
	{
		SLN* cd = *pphead;
		while (cd->next != pos)
		{
			cd = cd->next;
		}
		cd->next = pos->next;
		free(pos);
		pos = NULL;
	}
}
void chanchuhou(SLN* pos)//删除指定节点之后的节点
{
	assert(pos&&pos->next);
	SLN* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}
void xiaohui(SLN** pphead)//销毁链表
{
	assert(pphead && *pphead);
	SLN* del = *pphead;
	while (del)
	{
		SLN* next = del->next;
		free(del);
		del = next;
	}
	*pphead = NULL;
}

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "单链表.h"

int main()
{
	//SLN* node1 = (SLN*)malloc(sizeof(SLN));//创建4个节点
	//node1->data = 1;
	//SLN* node2 = (SLN*)malloc(sizeof(SLN));
	//node2->data = 2;
	//SLN* node3 = (SLN*)malloc(sizeof(SLN));
	//node3->data = 3;
	//SLN* node4 = (SLN*)malloc(sizeof(SLN));
	//node4->data = 4;
	//node1->next = node2;
	//node2->next = node3;
	//node3->next = node4;
	//node4->next = NULL;
	//SLN* a = node1;
	//	SLNprint(a);
	SLN* a = NULL;
	SLNpushback(&a, 1);
	SLNpushback(&a, 2);
	SLNpushback(&a, 3);
	SLNpushfront(&a, 5);
	SLNpushfront(&a, 5);
	/*SLNpopback(&a);
	SLNpopback(&a);
	SLNpopback(&a);
	SLNpopback(&a);
	SLNpopback(&a);*/
	SLN* find = SLNfind(a,1);
	/*SLNpopfront(&a);
	SLNpopfront(&a);*/
	/*chanchuhou(find);*/
	/*shanchu(&a,find);*/
	SLNinsrt(&a,find,7);
	SLNhoucha(find,8);
	SLNprint(a);
	xiaohui(&a);
	return 0;
}

结束语

链表这一块东西很多,这个代码还是单链表的,还有双链表,那其他的链表有关知识就放在下一片博客说,这片博客中有什么疑问可以在评论区分享,要是有问题我也会及时修改,

感谢观看

OK,下片博客见!!!

相关推荐
懒惰才能让科技进步9 分钟前
从零学习大模型(十二)-----基于梯度的重要性剪枝(Gradient-based Pruning)
人工智能·深度学习·学习·算法·chatgpt·transformer·剪枝
DARLING Zero two♡13 分钟前
关于我、重生到500年前凭借C语言改变世界科技vlog.16——万字详解指针概念及技巧
c语言·开发语言·科技
Gu Gu Study16 分钟前
【用Java学习数据结构系列】泛型上界与通配符上界
java·开发语言
Ni-Guvara23 分钟前
函数对象笔记
c++·算法
芊寻(嵌入式)38 分钟前
C转C++学习笔记--基础知识摘录总结
开发语言·c++·笔记·学习
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
泉崎1 小时前
11.7比赛总结
数据结构·算法
有梦想的咸鱼_1 小时前
go实现并发安全hashtable 拉链法
开发语言·golang·哈希算法
你好helloworld1 小时前
滑动窗口最大值
数据结构·算法·leetcode
海阔天空_20131 小时前
Python pyautogui库:自动化操作的强大工具
运维·开发语言·python·青少年编程·自动化