jjzjj

【数据结构】链表其实并不难 —— 手把手带你实现单链表

安 度 因 2023-12-31 原文

👑作者主页:@进击的安度因
🏠学习社区:进击的安度因(个人社区)
📖专栏链接:数据结构

文章目录

如果无聊的话,就来逛逛 我的博客栈 吧! 🌹

前两篇博客,我们学习了顺序表,而学习完后,我们仔细想想发现顺序表有些不足,不过不用担心。我们今天的学习内容 链表 就可以针对顺序表的不足做出改良。在本篇博客中,我会概述顺序表的缺陷、讲解链表的概念和结构分类、以及使用C语言实现单链表。话不多说,我们这就开始

1. 顺序表的缺陷

我们学习过顺序表之后,我们发现顺序表其实有 三大缺陷

  1. 空间不够了需要增容,增容是要付出代价的。因为 realloc 有原地扩容和异地扩容。原地扩容时代价很低。但是异地扩容不仅需要拷贝数据,还要释放旧空间。
  2. 为避免频繁扩容,空间满了基本都是扩2倍,可能就会导致一定的空间浪费。因为倍数为2时,扩容的幅度会越来越大。
  3. 顺序表要求数据从开始位置连续存储,那么我们在头部或者中间位置插入删除数据就需要挪动数据,效率不高。

那么我们是否能改善这些缺陷?比如:

  1. 按需申请释放。
  2. 不要挪动数据。

我们今天学习的链表就能改进上面两个缺陷,那么接下来我们就去学习它~

2. 链表的概念及结构

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

链表相较于顺序表的优点:顺序表要求 逻辑结构和物理结构上连续(通过下标访问) ;而链表只要求在逻辑结构连续物理结构上可以不连续(通过指针串联,它们之间并不连续)。

而逻辑结构就是指数据在逻辑上是如何存储的,就是一种形象但并不存在的易理解的结构。就比如链表的每个节点串联在一起,就像火车一样,比如这样:

其实这就非常形象了,车厢之间是无序的,因为每次不能保证链接到同样的车厢,车厢之间的位置可以改变。

链表 既然是数据结构,那么一定就要存储数据,并且需要有能力找到链表的下一个位置。那么它的结构到底长什么样?

我们可以先看一下某个链表的结构示意图:

我们把每两个小方块组成的大方块称为一个 节点 。我们的 n1、n2、n3、n4 则是每个节点的地址。它们指向 节点节点 之间使用箭头串联。

但是链表的 节点 之间真的有箭头吗?其实并没有。

它是我们的 逻辑结构 示意图,为了让我们更加容易理解,实际上通过箭头我们也可以知道,这可能和指针有关系。链表的 节点 分为两部分。一部分为 数据 ,一部分为 指针 ,而这个指针就是我们 下一个节点的地址 。所以从实际上来说,链表节点的第二部分是地址,它们之间没有箭头,包括 n1、n2、n3、n4 四个指针变量。

我们也可以画出它们的 物理结构 示意图:

(注:这里存储的地址数据我们不需要在意,我们只要观察它们之间的关系即可。)

逻辑结构中每个节点相互链接,像用箭头串联,和链条一样;物理结构每个节点的 指针域 存放下一个节点的地址,最后一个节点的 指针域 存放空指针代表链表结束,通过指针之间建立起链接关系。

从理解层面来说,逻辑结构更加容易让我们进行学习~

链表就是通过指针的链接次序来依次找到每一个节点,已对数据进行管理的。这就是链表结构为什么物理结构上非连续,但是能找到数据的原因。

3. 链表的分类

但是链表可不止一种,上面只是举个例子,实际上链表的结构非常多样,以下为链表的各种结构:

  1. 单向或双向

  1. 带头或不带头

  1. 循环或非循环

但是我们常用的链表主要是单链表带头双向循环链表,一个结构最简单,一个结构最复杂

下面简单介绍一下两个链表的特性:

  1. 无头单向非循环链表:也就是我们俗称的单链表。其结构简单,一般不会单独用来存储数据。实际上更多是作为其他数据结构的子结构,如哈希桶、栈的链式结构等。因为单链表本身有缺陷,所以为常见的考核点之一。
  2. 带头双向循环链表:结构最复杂,一般用来单独存储数据。实际使用的链表,大多都是带头双向循环链表。虽然这种链表结构最复杂,但是其实有很多优势,并且在一定程度上对单链表的缺陷做出了一定的纠正。

而我们本篇博客就是对结构最简单的 单链表 进行讲解并实现。

4. 单链表的实现

4.1 结构设计

单链表和顺序表的结构略有不同,单链表的主结构其实为一个节点

一个节点由两部分组成:datanextdata为我们存储数据的地方。而next则为下一个节点的地址。

为了之后我们写数据结构更加方便,于是将结构typedef一下:

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

上面我们设计的是不带哨兵位的单链表。这种结构设计相对于带哨兵位的链表的缺点就是设计接口函数时需要考虑链表是否为空的情况。

可能有小伙伴不了解什么是哨兵位,下面就介绍一下。

哨兵位也叫哨兵节点,哑节点。该节点并不存储任何数据,只是为了方便操作而引入这个节点。起一个站岗放哨的作用。所以形象的叫它哨兵位。如果一个链表有哨兵位,那么链表的第一个元素应该是链表第二个节点对应的元素。

那么这时就说明链表永不为空,这就可以避免边界问题的处理,简化代码与减少代码出错的可能性。

由于笔试面试考察中,不带头的单链表考察的最多,且实现不带头单向非循环链表需要把控的细节更多,以此增加我们的代码能力。

4.2 接口总览

实现一个单向无头非循环链表,总共需要以下接口:

// 创建新节点
SLTNode* BuyListNode(SLTDateType x);
// 打印
void SListPrint(SLTNode* phead);
// 尾插
void SListPushBack(SLTNode** pphead, SLTDateType x);
// 头插
void SListPushFront(SLTNode** pphead, SLTDateType x);
// 尾删
void SListPopBack(SLTNode** pphead);
// 头删
void SListPopFront(SLTNode** pphead);
// 查找
SLTNode* SListFind(SLTNode* phead, SLTDateType x);
// 在pos位置之前插入一个节点 
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x);
// 在pos位置之后插入一个节点
void SListInsertAfter(SLTNode* pos, SLTDateType x);
// 在指定位置删除一个节点
void SListErase(SLTNode** pphead, SLTNode* pos);
// 删除指定pos位置后的一个节点
void SListEraseAfter(SLTNode** pphead, SLTNode* pos);
// 销毁
void SListDestory(SLTNode** pphead);

我们发现绝大多数接口的参数都为二级指针,这是为什么?

我们先了解一下,我们平时单链表的测试用例:

void TestList()
{
	SLTNode* plist = NULL;
}

链表的一开始是空的。所以我们插入数据时,需要让plist指向我们新节点。就相当于改变plist的指向。plist是一级指针,那么要改变plist就要传它的地址&plist,为二级指针,所以也需要用二级指针来接收该参数。

(注:虽然链表不为空后,只需要改变结构即可,这时一级指针是没问题的。但是在C语言中,对于同一个接口来说,参数还能不一样吗?难道再封装一个函数?那时不可能的,所以我们索性都用二级指针。)

就好比当我们要改变一个int类型的变量时a,我们需要传它的地址&a,那么函数的形参就应该用int* 接收;对于指针也是这样,一个int* 的指针变量p,它也是变量,我们需要改变这个值,就应该传它的地址&p,那么函数参数就应该那int* * 接收。

而对于一些接口就不需要传二级指针,就拿 打印 来说吧,因为我并不需要改变plist,我只需要通过结构体指针访问结构内的next成员,并迭代到下一个节点,然后打印出数据就可以,所以不需要传二级指针。

补充:

当然,对于带哨兵位的单链表,直接传plist是没问题的。因为此时链表带头,我们只需要改变哨兵位和节点之间的链接关系就可以,说白了就是改变结构、

而且对于我们上面的单链表也可以传一级指针plist,然后用返回值接收,比如尾插就是:SLTNode* plist = SListPushBack(plist, 1); 但是这样使用,当调用次数很多是不是很怪?一大排的返回值接收,会不会显得代码冗余不简洁?

所以对于我们上面的结构,还是传二级指针比较好~

可能大家现在对这些操作还不是很理解,没关系,我们慢慢来,我们只要搞清楚,这里为什么这么传参就ok~

4.3 创建新节点

我们插入数据,都需要创建节点。因为链表是按需分配的,创建即用。如果使用局部变量的话,那么在函数调用结束后节点就销毁了,那么肯定就需要使用动态内存开辟新的节点:

// 创建新节点
SLTNode* BuyListNode(SLTDateType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1); // 内存申请失败,说明几乎没有空间了,直接退出程序
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

创建一个节点,节点的next默认为NULL,这样对于尾插时,也就不需要将新节点置空了~

此接口大多被实现插入的接口函数所复用,而我们只需要创建节点将节点地址返回即可。

4.4 尾插

从尾部插入数据,需要分两种情况考虑:链表为空、链表有节点。

如果链表为空,那么就需要创建节点,并且将创建的节点赋给原链表;如果链表不为空,则需要找到链表的尾部,然后将新节点到尾部的后面链接就可以了,这时也不需要刻意的将新节点的 next 置空,因为我们创建的节点的 next 本身就为空。

至于如何创建新节点,就调用之前的BuyListNode就可以了。

// 尾删
void SListPushBack(SLTNode** pphead, SLTDateType x)
{
	// 建立新节点
	SLTNode* newnode = BuyListNode(x);

	// 链表没有节点,给新节点
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		// 找到尾结点
		SLTNode* tail = *pphead;
		while (tail->next != NULL) // tail->next为空,说明此时为尾结点
		{
			tail = tail->next;
		}
		// 尾结点链接新节点
		tail->next = newnode;
	}	
}

4.5 头插

对于头插来说,就不需要像尾插一样考虑链表是否为空。

我们直接创建新节点,并且将新节点的 next 链接为原来的头,然后将头变为新节点,就可以了。因为就头插来说,如果链表为空,那么头部也为空,所以为什么可以直接链接就是这个原因。

// 头删
void SListPushFront(SLTNode** pphead, SLTDateType x)
{
	SLTNode* newnode = BuyListNode(x);

	newnode->next = *pphead; // 新节点链接之前plist的地址
	*pphead = newnode; // 头指针更改为newnode
}

看接口的实现,我们也能看出就单链表来说,对于头部的操作还是十分便捷的,包括下面的头删~

4.6 尾删

对于单链表来说,尾删也是比较麻烦的一部分。

尾删需要判断链表是否为空,并且需要将链表中 节点数单个节点多个节点的情况分开讨论。

如果是单个节点删除,那么只需要释放节点,将节点置空

如果是两个及以上节点删除,尾删需要将删除元素的前一个链接为空指针,需要删除尾结点,而完成这两个步骤的方法就是遍历单链表,通过条件来求出这两个位置。找到这两个位置后,释放尾结点将尾结点的前一个位置的next置空

// 尾删
void SListPopBack(SLTNode** pphead)
{
	// 暴力处理 一个节点
	assert(*pphead);

	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else// 两个及以上节点
	{
		SLTNode* prev = NULL; // 记录上一个节点的地址
		SLTNode* tail = *pphead; // 尾结点

		while (tail->next)// 当值为空指针,被隐式转换为0,结束循环
		{
			prev = tail;
			tail = tail->next;
		}

		free(tail);// 释放尾结点空间
		tail = NULL; // 其实这里置空也没有意义,tail是局部变量,函数结束就自动销毁了,反正也找不到~也不会使用到
		// 将前一个节点的next置空
		prev->next = NULL;
	}
}

4.7 头删

头删,相对于尾删就简单不少了。

对于空链表,直接断言判断就可以。

对于1个及以上节点,那么就是释放头结点,并且头结点的next设定为头

一定要注意处理的方式,千万不要将头结点释放了,还在使用头结点的next。我们可以先拷贝newpphead为头结点的next,然后释放pphead,再将给上新的头。

// 头删
void SListPopFront(SLTNode** pphead)
{
	// 处理空链表
	assert(*pphead);
	
	// 处理1个及以上节点
	SLTNode* newpphead = (*pphead)->next;
	free(*pphead);
	*pphead = newpphead;
}

4.8 查找

对于查找来说,相对比较简单。查找链表中某个元素是否存在,只需要遍历链表,查看里面是否有这个值。如果找到了,返回这个节点的地址;没找到,返回空指针。

当然对于空链表也可以查找,只不过就是找不到而返回空指针罢了,千万不要直接断言哦。

// 查找
SLTNode* SListFind(SLTNode* phead, SLTDateType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL; // 没找到
}

通过这个 查找函数 我们也可以推测出,为什么定义链表时只需要定义一个指针,而对于顺序表却要定义一个结构的原因:

因为对于链表来说,我只需要一个指针就可以遍历链表,因为我的节点之间互相串联链接。

但是对于顺序表得定义结构,否则我们不知道它有多少个元素,容量有多大。

4.9 在pos位置之前插入节点

要在pos位置之前插入节点,那么肯定就要找到 pos 之前的位置,于是,遍历链表肯定少不了。

其他的就是链表链接的基本操作,但是这里和之前的头插、尾插不同,因为需要考虑前后节点的链接

所以我们需要将pos之前的位置也就是 posPrevnext 链接为新节点,将新节点next 链接为 pos 位置。

你以为这就完了?这里需要另外考虑一个问题

如果我插入的位置是头部,那么该如何插入?

那么肯定是用头插的方式解决啦,这个问题相对简单,但是这个情况容易被忽略。

// 在pos位置之前插入节点
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
	assert(pos);
	SLTNode* newnode = BuyListNode(x);
	// 头插情况特殊处理
	if (*pphead == pos)
	{
		newnode->next = *pphead; // newnode链接头
		*pphead = newnode; // 头变为newnode
	}
	// 其他位置插入
	else
	{
		// 找到pos前一个位置
		SLTNode* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		posPrev->next = newnode; // 将pos前一个位置链接到newnode
		newnode->next = pos; // newnode连接到pos
	}
}

4.10 在pos位置之后插入节点

我们实现了在pos位置之前插入节点之后,是不是觉得实现的很难受,这么麻烦,还要实现,这明显不合理啊。

在pos位置之后插入更加合适吧?这样我根本不需要遍历链表,我只需要让 新节点newnodepos 位置的下一个节点链接,然后将pos位置的next变为我的新节点即可。

// 在pos位置之后插入节点
void SListInsertAfter(SLTNode* pos, SLTDateType x)
{
	assert(pos); // 避免pos传参传错

	SLTNode* newnode = BuyListNode(x);
	newnode->next = pos->next;
	pos->next = newnode; 
}

4.11 删除pos位置的节点

删除pos位置的节点,需要考虑此时链表是否为空,但这只是一部分,只需要用断言处理一下就好。

而我们着重处理的为,头删和多个节点删除的情况。

如果是头删,那么删除的就是第一个节点,那么此时就需要将头变为pos->next,并且释放pos位置。头删情况也完美处理了单个节点删除时的情况。

如果是多个节点,那么就需要找到pos的前一个位置,将 pos 前一个位置(prev)的 next 链接为我当前 posnext。然后释放我的pos节点。

// 删除pos位置的节点
void SListErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pos);

	// 处理头删
	if (*pphead == pos)
	{
		*pphead = pos->next;
		free(pos);
	}
	// 处理其他情况
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

4.12 删除pos位置之后的节点

和在pos位置之前插入一样,我们发现删除pos位置的节点,也很麻烦。因为需要找pos的前一个位置,所以这种设计也比较麻烦。

所以有时我们会删除pos位置之后的节点,这样就很方便了。

既然要删除pos位置之后的节点,那么我就需要将 pos 位置的节点和pos位置的 nextnext 链接起来。那么我们首先用 next 拷贝记录一下 pos->next ,然后,再将pos->next赋值为next->next,也就是pos往后的两个节点。最后释放next位置的节点即可。

注意:我pos位置的下一个节点不能是尾结点后面的节点!

// 删除指定pos位置后的一个节点
void SListEraseAfter(SLTNode** pphead, SLTNode* pos)
{
	assert(pos);

	assert(pos->next);// 删除的不能是尾结点后面的位置
	SLTNode* next = pos->next;// 拷贝pos的下一个节点
	pos->next = next->next;// 将pos的next变为下一个节点的next
	free(next);// 释放之前pos的下一个节点
	next = NULL;
}

4.13 打印

打印整个链表,只需要遍历链表,控制好循环的停止条件:

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

4.14 销毁

销毁链表,如果仅仅释放我的*pphead,可行吗?这当然不可行。

因为单链表是由不同地址的节点串联起来的。它不像数组,存储连续,只要释放起始位置。

如果我仅仅释放*pphead,那么只释放了我的第一个节点,后面的节点没释放,函数调用结束后,这些节点也找不到了。所以我们需要逐个销毁

首先,使用 cur 拷贝头部位置。然后使用循环迭代。迭代过程中,需要记住我的 cur->next ,否则 cur 被释放后,无法找到下一个节点,然后逐个释放就可以了。

注意:当销毁后,记得把*pphead 置空,防止销毁链表后对链表误操作而导致的野指针问题。

// 销毁
void SListDestory(SLTNode** pphead)
{
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL; // 置空
}

5. 完整代码

SList.h

#pragma once

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

typedef int SLTDateType;

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

// 打印
void SListPrint(SLTNode* phead);
// 尾插
void SListPushBack(SLTNode** pphead, SLTDateType x);
// 头插
void SListPushFront(SLTNode** pphead, SLTDateType x);
// 尾删
void SListPopBack(SLTNode** pphead);
// 头删
void SListPopFront(SLTNode** pphead);
// 查找
SLTNode* SListFind(SLTNode* phead, SLTDateType x);
// 在pos位置之前插入一个节点 
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x);
// 在pos位置之后插入一个节点
void SListInsertAfter(SLTNode* pos, SLTDateType x);
// 在指定位置删除一个节点
void SListErase(SLTNode** pphead, SLTNode* pos);
// 删除指定pos位置后的一个节点
void SListEraseAfter(SLTNode** pphead, SLTNode* pos);
// 销毁
void SListDestory(SLTNode** pphead);

SList.c

#define _CRT_SECURE_NO_WARNINGS 1 

#include "SList.h"

// 创建新节点
SLTNode* BuyListNode(SLTDateType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);// 内存申请失败,说明几乎没有空间了,直接退出程序
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

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

// 尾插
void SListPushBack(SLTNode** pphead, SLTDateType x)
{	// 建立新节点
	SLTNode* newnode = BuyListNode(x);
	// 链表没有节点,给新节点
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		// 找到尾结点
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		// 尾结点链接新节点
		tail->next = newnode;
	}	
}

// 头插
void SListPushFront(SLTNode** pphead, SLTDateType x)
{
	assert(pphead);
	SLTNode* newnode = BuyListNode(x);
	newnode->next = *pphead;// 新节点链接之前plist的地址
	*pphead = newnode;// 头指针更改为newnode
}

// 尾删
void SListPopBack(SLTNode** pphead)
{
	// 温柔处理
	/*if (*pphead == NULL)
	{
		return;
	}*/
	// 暴力处理 一个节点
	assert(*pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else// 两个及以上节点
	{
		// plan1
		SLTNode* prev = NULL;// 记录上一个节点的地址
		SLTNode* tail = *pphead;
		//while (tail->next != NULL)// 计算逻辑表达式的值为真或假决定循环是否继续执行
		while (tail->next)// 当值为空指针,被隐式转换为0,结束循环
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);// 释放尾结点空间
		tail = NULL;
		// 将前一个节点的next置空
		prev->next = NULL;
	}
	// plan2:
	//SLTNode* tail = *pphead;
	//while (tail->next->next)// 如果当前tail的next的地址访问下一个next为空,则停止
	//{
	//	tail = tail->next;// 将尾结点赋值为最后一个节点的next
	//}
	//free(tail->next);// 释放最后一个节点
	//tail->next = NULL;// 最后一个节点置空
}

// 头删
void SListPopFront(SLTNode** pphead)
{
	// 处理空链表
	assert(*pphead);	
	// 处理1个及以上节点
	SLTNode* newpphead = (*pphead)->next;
	free(*pphead);
	*pphead = newpphead;
}

// 查找
SLTNode* SListFind(SLTNode* phead, SLTDateType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;// 没找到
}

// 在pos位置之前插入节点
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
	assert(pos);
	SLTNode* newnode = BuyListNode(x);
	// 头插情况特殊处理
	if (*pphead == pos)
	{
		newnode->next = *pphead;// newnode链接头
		*pphead = newnode;// 头变为newnode
	}
	// 其他位置插入
	else
	{
		// 找到pos前一个位置
		SLTNode* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		posPrev->next = newnode;// 将pos前一个位置链接到newnode
		newnode->next = pos;// newnode连接到pos
	}
}

// pos不太适合在节点前插入,适合在节点后插入
// 在pos后面 插入节点

void SListInsertAfter(SLTNode* pos, SLTDateType x)
{
	assert(pos);
	SLTNode* newnode = BuyListNode(x);
	newnode->next = pos->next;
	pos->next = newnode; 
}

// 删除pos位置的节点
void SListErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pos);
	// 处理头删
	if (*pphead == pos)
	{
		*pphead = pos->next;
		free(pos);
		// 直接调用头删
		// SListPopFront(pos);
	}
	// 处理其他情况
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

// 删除指定pos位置后的一个节点
void SListEraseAfter(SLTNode** pphead, SLTNode* pos)
{
	assert(pos);
	assert(pos->next);// 删除的不能是尾结点后面的位置
	SLTNode* next = pos->next;// 拷贝pos的下一个节点
	pos->next = next->next;// 将pos的next变为下一个节点的next
	free(next);// 释放之前pos的下一个节点
	next = NULL;
}

void SListDestory(SLTNode** pphead)
{
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
    // 这里一定要置空,放置销毁链表后被误使用
	*pphead = NULL;
}

test.c

#define _CRT_SECURE_NO_WARNINGS 1 

#include "SList.h"

// 测试尾插、带节点的头插
void TestList1()
{
	SLTNode* plist = NULL;
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SListPushBack(&plist, 5);
	SListPrint(plist);

	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPushFront(&plist, 5);
	SListPrint(plist);

}

// 测试头插、尾删
void TestList2()
{
	SLTNode* plist = NULL;

	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPushFront(&plist, 5);
	SListPrint(plist);

	SListPopBack(&plist);
	SListPopBack(&plist);
	SListPopBack(&plist);
	SListPopBack(&plist);
	SListPopBack(&plist);
	//SListPopBack(&plist);
	SListPrint(plist);

}

// 测试头删
void TestList3()
{
	SLTNode* plist = NULL;
    
	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPushFront(&plist, 5);
	SListPrint(plist);
	
	SListPopFront(&plist);
	SListPrint(plist);

	SListPopFront(&plist);
	SListPrint(plist);

	SListPopFront(&plist);
	SListPrint(plist);

	SListPopFront(&plist);
	SListPrint(plist);

	SListPopFront(&plist);
	SListPrint(plist);

}


//void TestList4()
//{
//	SLTNode* plist = NULL;
//
//	SListPushFront(&plist, 1);
//	SListPushFront(&plist, 2);
//	SListPushFront(&plist, 3);
//	SListPushFront(&plist, 2);
//	SListPushFront(&plist, 5);
//
//	SLTNode* pos = SListFind(plist, 2);
//	int i = 1;
//	while (pos)
//	{
//		printf("第%d个pos节点:%p->%d\n", i++, pos, pos->data); 
//		pos = SListFind(pos->next, 2);
//	}
//
//	// 修改 3->30
//	pos = SListFind(plist, 3);
//	if (pos)
//	{
//		pos->data = 30;
//	}
//	SListPrint(plist);
//}

void TestList5()
{
	SLTNode* plist = NULL;
    
	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 5);

	// 3前面插入一个30
	SLTNode* pos = SListFind(plist, 3);
	if (pos)
	{
		SListInsert(&plist, pos, 30);
	}
	SListPrint(plist);

	// 5前面插入一个10
	pos = SListFind(plist, 5);
	if (pos)
	{
		SListInsert(&plist, pos, 10);
	}
	SListPrint(plist);
}

int main()
{
	//TestList1();
	//TestList2();
	//TestList3();
	TestList5();

	return 0;
}

到这里本篇博客就到此结束了,仔细阅读这篇文章,你会发现,链表其实也没有那么难!
但是光看懂了还不够,链表是一个面试常考点,所以是十分重要的!于是在接下来几期,anduin 会带着大家一起刷链表的OJ题,多写多练,手撕链表~
更多精彩内容敬请期待~
如果觉得anduin写的还不错的话,还请一键三连!如有错误,还请指正!
我是anduin,一名C语言初学者,我们下期见!

有关【数据结构】链表其实并不难 —— 手把手带你实现单链表的更多相关文章

  1. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  2. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  3. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

  4. ruby - 我如何添加二进制数据来遏制 POST - 2

    我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_

  5. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  6. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  7. FOHEART H1数据手套驱动Optitrack光学动捕双手运动(Unity3D) - 2

    本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01  客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02  数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit

  8. 使用canal同步MySQL数据到ES - 2

    文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co

  9. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  10. ruby-on-rails - 创建 ruby​​ 数据库时惰性符号绑定(bind)失败 - 2

    我正在尝试在Rails上安装ruby​​,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf

随机推荐