数据结构:详解【链表】的实现(单向链表+双向链表)

目录

  • 一,前言
  • 二 ,有关链表的概念,结构和分类
  • 三,无头单向非循环链表(单链表)
    • 1.单链表的功能
    • 2.单链表功能的实现
    • 3.完整代码
  • 四,带头双向循环链表(双链表)
    • 1.单链表与双链表的结构区别
    • 2.双链表的功能
    • 3.双链表功能的实现
    • 4. 完整代码

一,前言

1.顺序表的问题和思考

问题:

  1. 中间/头部的插入删除,时间复杂度为O(N)。
  2. 增容需要申请新空间,拷贝数据,释放旧空间,会有不小的消耗。
  3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

思考:
如何解决以上问题呢?下面给出了链表的结构来看看。

二 ,有关链表的概念,结构和分类

1. 链表的概念和结构

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

1.1 逻辑结构(就是我们想象的,数据元素依次链接):
在这里插入图片描述

1.2 物理结构(实实在在的在内存中真实存储的结构,数据元素不一定是连续存储的):
在这里插入图片描述

1. 每个结点(一块空间)都是随机在内存中动态开辟(malloc)出来的,都有它们的地址(第一个字节的地址),这些地址也是随机的。
2. 每个新结点都包含两个区域——数据域和指针域。数据域存放我们要操作的数据,指针域存放着下一个结点的地址。我们就是通过这个地址和下一个结点建立联系。
3. 最后一个结点的指针域中存放的是NULL。

2. 链表的分类

实际中的链表种类非常多,以下情况组合起来就有8种链表构:
在这里插入图片描述
比如:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构,下面也只对这两种结构进行代码实现:
在这里插入图片描述

  1. 无头单向非循环链表: 结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
  2. 带头双向循环链表: 结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

三,无头单向非循环链表(单链表)

1.单链表的功能

单链表的功能一般有如下几个:

  • 打印数据
  • 动态申请新结点
  • 尾部插入数据
  • 头部插入数据
  • 尾部删除数据
  • 头部删除数据
  • 查找指定数据的位置
  • 在pos位置前插入数据
  • 删除pos处的数据

2.单链表功能的实现

2.1 建立单链表

typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;//要操作的数据
	struct SListNode * next;//指向下一个结点的指针
}SLNode;

2.2 打印数据

首先定义一个指针cur指向第一个结点(头指针phead),使用循环进行遍历,当cur != NULL时,打印数据,再让cur指向下一个结点,直至跳出循环。

在这里插入图片描述

代码实现如下:

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

2.3 动态申请新结点
由于链表在内存中不是一块连续的空间,所以每次进行插入(增加)数据的操作时,都要在内存中创建新结点,即动态开辟(malloc)一块空间。

//这个函数是每次在增加数据时进行调用的

SLNode* BuySListNode(SLTDataType x)
{
     //随机开辟一块空间(结点)
	SLNode* newnode =(SLNode*)malloc(sizeof(SLNode));
	if (newnode == NULL)
	{
		printf("malloc fail!\n");
		return;
	}
	newnode->data = x;//存入要插入的数据
	newnode->next = NULL;//把最后一个结点指向空

	return newnode;
}

2.4 尾部插入数据
尾插分为两种情况:
1.如果链表中没有结点时,直接让新结点指向头指针;
2.链表中至少有一个结点时,需要循环找到链表的尾结点的指针,再链接上新结点。
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/7893ddd6c9f941a2a741c18a5911ad15.png
代码实现如下:

void SListPushBack(SLNode** pphead, SLTDataType x)
{
	SLNode* newnode = BuySListNode(x);
	
	//1.如果表中没有一个节点
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//2.有两个节点以上,找尾,
		SLNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		//链接新节点
		tail->next = newnode;
	}
}

2.5 头部插入数据
头插,首先在内存中开辟一个结点,把链表内原来第一个结点的地址存入新结点中,使两者建立联系,再把新结点的地址存入头指针即可。
在这里插入图片描述

代码实现如下:

void SListPushFront(SLNode** pphead, SLTDataType x)
{
	SLNode* newnode = BuySListNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

2.6 尾部删除数据
尾删分为三种情况:
1.如果链表中没有数据(链表为空),则不需要删除,略作提示终止程序即可;
2.如果表中只有一个结点,直接free释放,再置空即可;
3.如果表中多于一个结点,不能直接循环找到尾结点删除,这样前一个结点会形成野指针,而是要先保存倒数第二个结点的地址,再把尾结点释放置空。

![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/675e1b58c8ee47aa96ec789bdf037d70.png

代码实现如下:

void SListPopBack(SLNode** pphead)
{
	//1.链表内无数据
	if (*pphead == NULL)
	{
		printf("链表为空!\n");
		return;
	}
	//2.如果只有一个结点
	else if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLNode* prev = NULL;
		SLNode* tail = *pphead;
		//3.记住前一个位置
		while (tail->next != NULL)
		{
			prev= tail;
			tail = tail->next;
		}
		
		free(tail);
		prev->next = NULL;
	}
}

2.7 头部删除数据
头删,相同的道理,不能直接把第一个结点free释放,这样就找不到后面的数据了,而是先要定义一个指针变量保存第二个结点的地址,再把第一个结点free释放,置空。最后让头指针指向这个变量。
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/1a80381988754e858dad4ca8ec3412b4.png

代码实现如下:

void SListPopFront(SLNode** pphead)
{
		SLNode* cur = NULL;
		cur = (*pphead)->next;
		free(*pphead);
		*pphead = cur;
}

2.8 查找指定数据的位置
循环遍历链表,查找出指定数字的位置,用指针pos保存,这个函数一般配合下面两个函数一起使用。

代码实现如下:

SLNode* SListFind(SLNode* phead, SLTDataType x)
{
	SLNode* cur = phead;
	while (cur!=NULL)
	{
		if (cur->data == x)
		{
		   //找到了发回地址
			return cur;
		}
		cur = cur->next;
	}
   //没有找到,返回空
	return NULL;
}

2.9 在pos位置前插入数据
这个函数包含两种情况:
1.当在第一个结点前插入时(pos指向第一个结点,pos是通过查找函数找到的),相当于头插;
2.当在其余结点前插入时,需要一个变量prev保存pos的前一个结点的地址,再把prev,pos,newnode这三个结点链接起来。
在这里插入图片描述
代码实现如下:

void SListInsert(SLNode** pphead, SLNode* pos, SLTDataType x)
{
	SLNode* newnode = BuySListNode(x);

	//当在第一个结点前插入时,相当于头插
	if ((*pphead)->next == NULL)
	{
		SListPushFront(pphead, x);
	}
	else
	{
		SLNode* prev = NULL;
		SLNode* cur = *pphead;
		while (cur->next != pos)
		{
			cur = cur->next;
		}
		prev = cur;//保存到了pos前一个结点的地址
		prev->next = newnode;
		newnode->next = pos;
	}
}

2.10 删除pos处的数据
相同的,这个函数也包含两种情况:
1.当要删除第一个结点时(此时pos指向第一个结点),相当于头删;
2.当pos指向其他位置时,不能直接free释放掉pos,而是要先找到pos前一个结点的位置,把它与pos的后一个结点进行链接,最后free释放掉pos。

在这里插入图片描述
代码实现如下:

void SListErase(SLNode** pphead, SLNode* pos)
{
	//当要删除第一个结点时,相当于头删
	if (pos == *pphead)
	{
		SListPopFront(pphead);
	}
	else
	{
		SLNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}

}

3.完整代码

SList.h

#define _CRT_SECURE_NO_WARNINGS 

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

typedef int SLTDataType;

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

//尾插
void SListPushBack(SLTNode** pphead, SLTDataType x);

//头插
void SListPushFront(SLTNode** pphead, SLTDataType x);

//打印数据
void SListPrint(SLTNode* phead);

//尾删
void SListPopBack(SLTNode** pphead);

//头删
void SListPopFront(SLTNode** pphead);

//查找
SLTNode* SListFind(SLTNode* pphead, SLTDataType x);

//在pos位置前插入数据
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);

//在pos位置处删除数据
void SListEarse(SLTNode** pphead, SLTNode* pos);

SList.c

#define _CRT_SECURE_NO_WARNINGS

#include "SList.h"

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

SLTNode* BuySListNode(SLTDataType x)
{
	//开辟新节点
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		printf("malloc fail!\n");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
}

void SListPushBack(SLTNode** pphead, SLTDataType x)
{
	//判断刚开始是否有节点,若没有,则直接开辟
	SLTNode* newnode = BuySListNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//若已经存在节点 则找到尾结点,链接新节点
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

void SListPushFront(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySListNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

void SListPopFront(SLTNode** pphead)
{
	if (*pphead == NULL)
	{
		printf("链表无数据!\n");
	}
	//注意不能直接free,要先记住第二个节点的地址,再释放第一个节点
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

void SListPopBack(SLTNode** pphead)
{
	//1.当链表为空时
	if (*pphead == NULL)
	{
		printf("链表无数据!\n");
		return;
	}
	//2.只有一个节点时,直接释放掉,再置空
	else if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
		//让prev记住倒数第二个节点,此时tail指向最后一个节点
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		//释放最后一个空间
		free(tail);
		//这时prev是最后一个节点,要置空,避免野指针
		prev->next = NULL;
	}
	
}

SLTNode* SListFind(SLTNode* pphead, SLTDataType x)
{
	SLTNode* cur = pphead;
	//遍历链表,找数
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;

	}
	return NULL;
}

void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	//要在第一个节点前插入数据时,相当于头插
	if (pos == *pphead)
	{
		SListPushFront(pphead, x);
	}
	else
	{
		SLTNode* newnode = BuySListNode(x);
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = newnode;
		newnode->next = pos;
	}
}

void SListEarse(SLTNode** pphead, SLTNode* pos)
{
	//1.当pos为第一个节点时,没有前一个节点,删除时相当于头删
	if (pos == *pphead)
	{
		SListPopFront(pphead);
	 }
	else
	{
		//2.Pos不是第一个节点,找到pos的前一个位置,再释放空间
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}

}

test.c

#define _CRT_SECURE_NO_WARNINGS 

#include "SList.h"

 //在这里对那些函数接口进行测试,例如:
void SListTest()
{
   //定义一个头指针,置空
	SLTNode* plist = NULL;
	
	//这里传地址的原因是我们要对链表中的数据进行修改,
	//由于形参是实参的一份临时拷贝,改变形参不影响实参
	//所以只有传地址,对内存进行修改
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SListPrint(plist);

	//在3前面插入有一个30,先找到3的前一个的位置,再插入
	SLTNode* pos = SListFind(plist, 3);
	if (pos)
	{
		SListInsert(&plist,pos,30);
	}
	SListPrint(plist);

}

int main()
{
	SListTest();

	return 0;
}

四,带头双向循环链表(双链表)

虽然单链表是我们要掌握基础链表之一,但是它并不完美,比如它不能从后往前操作,不容易找到前驱,每次尾插,尾删都要进行遍历,使得时间复杂度为O(N)等,使用起来其实并不是那么方便。

下面介绍的双链表虽然结构更复杂,但是它完美解决了单链表的缺陷,使用起来十分方便,丝滑。

1.单链表与双链表的结构区别

![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/723a6c3b744648d2b956ee6594b89ae7.png

  • 双链表的"带头"是指需要在链表的最前端动态申请一个特殊的结点,这第一个结点不存储有效数据,这种结点也称为带哨兵位的头结点 它的好处是在进行增删查改等操作时,不需要改变传过来的指针了,也就意味着不需要传二级指针了。
    而且,与单链表相比,它不仅有存放下一个结点地址的next指针(也可叫后驱指针),还会增加一个前驱指针prev,用来存放上一个节点的地址。这样就使得每个节点都能快速找到自己的前一个结点,也能找到自己的下一个结点。 最终会形成"循环"。
    双链表的这两点结构对我们增删查改的实现有极大的方便,使它能够完美解决单链表的缺陷。
  • 单链表的结构就更简单了,它没有带头,也没有循环,只能从头到尾前一个结点指向后一个结点。

2.双链表的功能

双链表的功能一般有如下几个:

  • 初始化链表
  • 打印数据
  • 动态申请新结点
  • 尾部插入数据
  • 头部插入数据
  • 尾部删除数据
  • 头部删除数据
  • 查找指定数据的位置
  • 在pos位置前插入数据
  • 删除pos处的数据
  • 销毁链表

3.双链表功能的实现

3.1 建立双链表

typedef int SLTDataType;

typedef struct ListNode
{
	SLTDataType data;//要操作的数据
	struct ListNode* next;//后驱指针
	struct ListNode* prev;//前驱指针

}ListNode;

3.2 初始化链表
首先要申请一个带哨兵位的头结点,并且让它的前驱和后驱都指向自己。
在这里插入图片描述
代码实现如下:

//注意:这个函数并没有进行传参,而是通过返回值的方式
//把申请空间的地址返回。这就避免了使用二级指针。
ListNode* ListInit()
{
	ListNode* phead = BuyListNode(0);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

3.3 动态申请新结点
在初始化链表和插入数据时,都需要在内存中动态申请新结点。

ListNode* BuyListNode(SLTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		printf("malloc fail!\n");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	return newnode;
}

3.4 打印数据
首先定义一个指针cur指向第二个结点(头节点不存放数据),由于是循环链表,所以通过循环,当cur != phead时,打印出数据即可。

在这里插入图片描述
代码实现如下:

void ListPrint(ListNode* phead)
{
	ListNode* cur = phead->next;

	if (cur == phead)
	{
		printf("链表为空!\n");
		return;
	}

	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

3.5 尾部插入数据
尾插,申请新结点newnode,定义变量tail指向最后一个结点(其实就是phead的前驱,这就避免了要向单链表那样通过遍历找到尾节点,体现出了双链表的优越性。),再链接起phead,newnode,tail

在这里插入图片描述
代码实现如下:

void ListPushBcck(ListNode* phead, SLTDataType x)
{
	assert(phead);//断言
	ListNode* newnode = BuyListNode(x);

	ListNode* tail = phead->prev;
	tail->next = newnode;
	newnode->prev = tail;
	
	phead->prev = newnode;
	newnode->next = phead;
	
}

3.6 头部插入数据
头插,就是在头结点phead和和第二个结点之间插入,申请新结点newnode,再定义变量first指向第二个结点,再链接起三者即可。
在这里插入图片描述
代码实现如下:

void ListPushFront(ListNode* phead, SLTDataType x)
{
	assert(phead);

	ListNode* first = phead->next;
	ListNode* newnode = BuyListNode(x);
	
	phead->next = newnode;
	newnode->prev = phead;
	
	newnode->next = first;
	first->prev = newnode;

}

3.7 尾部删除数据
尾删,与单链表类似,不能直接把最后一个结点free释放。定义一个变量tail指向尾结点(其实就是phead->prev),再定义一个变量prev保存倒数第二个结点的地址(其实就是tail->prev),再进行三者链接,最后释放,置空。

在这里插入图片描述
代码实现如下:

void ListPopBcck(ListNode* phead)
{
	assert(phead);
	
	//此时链表一个结点都没有
	assert(phead->next != phead);

	ListNode* tail = phead->prev;
	ListNode* prev = tail->prev;

	phead->prev = prev;
	prev->next = phead;

	free(tail);
	tail = NULL;

}

3.8 头部删除数据
头删,是删除第二个结点,不能直接释放第二个结点。定义变量first指向第二个结点(就是phead->next),再定义变量second指向第三个结点(就是first->next),再进行三者链接,最后释放,置空。

在这里插入图片描述
代码实现如下:

void ListPopFront(ListNode* phead)
{
	assert(phead);
	
	//此时链表一个结点都没有
	assert(phead->next != phead);

	ListNode* first = phead->next;
	ListNode* second = first->next;
	
	phead->next = second;
	second->prev = phead;

	free(first);
	first = NULL;

}

3.9 查找指定数据的位置
定义变量cur指向第二个结点,直接循环遍历,直至cur指向phead。若找到了,则直接返回该结点的地址,用pos接收,否则返回NULL。这个函数一般与下面两个函数配合使用。

ListNode* ListFind(ListNode* phead, SLTDataType x)
{
	assert(phead);

	ListNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

3.10 在pos位置前插入数据
pos是查找函数找到的位置,申请新结点newnode,定义变量prev指向pos的前一个结点,再链接三者即可。

在这里插入图片描述
代码实现如下:

void ListInsert(ListNode* pos, SLTDataType x)
{
	assert(pos);//断言,pos不能为空

	ListNode* prev = pos->prev;
	ListNode* newnode = BuyListNode(x);
	
	prev->next = newnode;
	newnode->prev = prev;

	newnode->next = pos;
	pos->prev = newnode;
}

3.11 删除pos处的数据
与前面类似,不能直接free释放pos处的空间。定义变量prev指向(保存)pos前一个结点的(地址),定义变量next指向(保存)pos后一个结点(地址),再把两者进行链接,最后释放,置空。
在这里插入图片描述
代码实现如下:

void ListEarse(ListNode* pos)
{
	assert(pos);

	ListNode* prev = pos->prev;
	ListNode* next = pos->next;

	prev->next = next;
	next->prev = prev;

	free(pos);//释放
	pos = NULL;
}

3.12 销毁链表
销毁链表是从第二个结点开始,循环依次释放。定义变量cur指向第二个结点,与前面类似,不能直接释放cur处的空间,而是要先保存到下一个结点的空间,再把当前节点空间释放。最后再释放头结点(phead),置空。

在这里插入图片描述

代码实现如下:

void ListDestory(ListNode* phead)
{
	assert(phead);
	
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		ListNode* next = cur->next;//保存下一个结点的地址
		free(cur);//先释放有数据的结点
		cur = next;
	}

	free(phead);//释放哨兵位头结点
	phead = NULL;
}

4. 完整代码

List.h

#define _CRT_SECURE_NO_WARNINGS 

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

typedef int SLTDataType;

typedef struct ListNode
{
	SLTDataType data;
	struct ListNode* next;
	struct ListNode* prev;

}ListNode;


//初始化链表
ListNode* ListInit();

//销毁链表
void ListDestory(ListNode* phead);

//打印数据
void ListPrint(ListNode* phead);

//尾插
void ListPushBcck(ListNode* phead, SLTDataType x);

//头插
void ListPushFront(ListNode* phead, SLTDataType x);

//尾删
void ListPopBcck(ListNode* phead);

//头删
void ListPopFront(ListNode* phead);

//查找
ListNode*  ListFind(ListNode* phead, SLTDataType x);

//在pos前插入数据
void ListInsert( ListNode* pos, SLTDataType x);

//删除pos处的数据
void ListEarse( ListNode* pos);

List.c

#define _CRT_SECURE_NO_WARNINGS 

#include "List.h"

ListNode* BuyListNode(SLTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		printf("malloc fail!\n");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	return newnode;
}

ListNode* ListInit()
{
	ListNode* phead = BuyListNode(0);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

void ListPrint(ListNode* phead)
{
	ListNode* cur = phead->next;

	if (cur == phead)
	{
		printf("链表为空!\n");
		return;
	}

	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

void ListDestory(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}

	free(phead);
	phead = NULL;
}

void ListPushBcck(ListNode* phead, SLTDataType x)
{
	assert(phead);
	ListNode* newnode = BuyListNode(x);

	ListNode* tail = phead->prev;
	tail->next = newnode;
	newnode->prev = tail;
	
	phead->prev = newnode;
	newnode->next = phead;

}

void ListPushFront(ListNode* phead, SLTDataType x)
{
	assert(phead);

	ListNode* first = phead->next;
	ListNode* newnode = BuyListNode(x);
	
	phead->next = newnode;
	newnode->prev = phead;
	
	newnode->next = first;
	first->prev = newnode;

}

void ListPopFront(ListNode* phead)
{
	assert(phead);
	
	//此时链表一个结点都没有
	assert(phead->next != phead);

	ListNode* first = phead->next;
	ListNode* second = first->next;
	
	phead->next = second;
	second->prev = phead;

	free(first);
	first = NULL;

}

void ListPopBcck(ListNode* phead)
{
	assert(phead);
	
	//此时链表一个结点都没有
	assert(phead->next != phead);

	ListNode* tail = phead->prev;
	ListNode* prev = tail->prev;

	phead->prev = prev;
	prev->next = phead;

	free(tail);
	tail = NULL;

}

ListNode* ListFind(ListNode* phead, SLTDataType x)
{
	assert(phead);

	ListNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

void ListInsert(ListNode* pos, SLTDataType x)
{
	assert(pos);

	ListNode* prev = pos->prev;
	ListNode* newnode = BuyListNode(x);
	
	prev->next = newnode;
	newnode->prev = prev;

	newnode->next = pos;
	pos->prev = newnode;
}

void ListEarse(ListNode* pos)
{
	assert(pos);

	ListNode* prev = pos->prev;
	ListNode* next = pos->next;

	prev->next = next;
	next->prev = prev;

	free(pos);
	pos = NULL;
}

test.c

#define _CRT_SECURE_NO_WARNINGS 

#include "List.h"

//在这个函数中进行各函数接口的测试:
ListNode* ListTest()
{
	ListNode* plist = ListInit();

	ListPushBcck(plist, 1);
	ListPushBcck(plist, 2);
	ListPushBcck(plist, 3);
	ListPushBcck(plist, 4);
	ListPrint(plist);


	ListDestory(plist);

	return 0;
}

int main()
{
	ListTest();

	return;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/463464.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

YOLOv9改进策略:注意力机制 | 归一化的注意力模块(NAM)

&#x1f4a1;&#x1f4a1;&#x1f4a1;本文改进内容&#xff1a; NAM作为一种高效且轻量级的注意力机制。采用了CBAM的模块集成并重新设计了通道和空间注意子模块。 yolov9-c-NAMAttention summary: 965 layers, 51000614 parameters, 51000582 gradients, 238.9 GFLOPs 改…

Java基础 - 9 - 集合进阶(二)

一. Collection的其他相关知识 1.1 可变参数 可变参数就是一种特殊形参&#xff0c;定义在方法、构造器的形参列表里&#xff0c;格式是&#xff1a;数据类型…参数名称; 可变参数的特点和好处 特点&#xff1a;可以不传数据给它&#xff1b;可以传一个或者同时传多个数据给…

html中如何让网页禁用右键禁止查看源代码

在网页中&#xff0c;辛辛苦苦写的文章&#xff0c;被别人复制粘贴给盗用去另很多站长感到非常无奈&#xff0c;通常大家复制都会使用选取右键复制&#xff0c;或CTRLC等方式&#xff0c;下面介绍几种禁止鼠标右键代码&#xff0c;可减少网页上文章被抄袭的几率&#xff0c;当然…

Day38:安全开发-JavaEE应用SpringBoot框架MyBatis注入Thymeleaf模版注入

目录 SpringBoot-Web应用-路由响应 SpringBoot-数据库应用-Mybatis SpringBoot-模版引擎-Thymeleaf 思维导图 Java知识点 功能&#xff1a;数据库操作&#xff0c;文件操作&#xff0c;序列化数据&#xff0c;身份验证&#xff0c;框架开发&#xff0c;第三方库使用等. 框架…

PyTorch学习笔记之激活函数篇(二)

文章目录 2、Tanh函数2.1 公式2.2 对应的图像2.3 对应生成图像代码2.4 优点与不足2.5 torch.tanh()函数 2、Tanh函数 2.1 公式 Tanh函数的公式&#xff1a; f ( x ) e x − e − x e x e − x f(x)\frac{e^x-e^{-x}}{e^xe^{-x}} f(x)exe−xex−e−x​ Tanh函数的导函数&am…

idea找不到或无法加载主类

前言 今天在运行项目的时候突然出了这样一个错误&#xff1a;IDEA 错误 找不到或无法加载主类,相信只要是用过IDEA的朋友都 遇到过它吧&#xff0c;但是每次遇到都是一顿焦头烂额、抓耳挠腮、急赤白咧&#xff01;咋整呢&#xff1f;听我给你吹~ 瞧我这张嘴~ 问题报错 找不…

C++之类(持续更新)

1、类的基础知识点 1.1、类和对象 和C中的结构体不同&#xff0c;在C类中不仅可以定义变量&#xff0c;也可以定义函数。【在C结构体中也可以定义变量和函数&#xff0c;但是一般情况下都使用类】。 类的成员属性默认都是private&#xff1b;结构体的成员属性默认都是public。…

利用express从0到1搭建后端服务

目录 步骤一&#xff1a;安装开发工具步骤二&#xff1a;安装插件步骤三&#xff1a;安装nodejs步骤四&#xff1a;搭建启动入口文件步骤五&#xff1a;启动服务器总结 在日常工作中&#xff0c;有很多重复和繁琐的事务是可以利用软件进行提效的。但每个行业又有自己的特点&…

特殊文件——属性文件、XML文件

目录 特殊文件 ——属性文件、XML文件 特殊文件的作用 需要掌握的知识点 Properties文件 ​编辑 构造器与方法​编辑 使用Properties 把键值对数据写出到属性文件中 ​编辑 XML文件​编辑 XML文件的作用和应用场景 解析XML文件 使用Dom4J框架解析出XML文件——下载…

EXCEL+PYTHON学习3

1&#xff09; 遍历一个SHEET&#xff0c;无非就是两个循环&#xff0c;rows属性是取得所有行。 fn data3_16.xlsx wb openpyxl.load_workbook(fn) ws wb.active for row in ws.rows:for cell in row:print(cell.value, end )print() 2&#xff09; 返回工作表的最小行数…

TCP/IP协议栈

TCP/IP协议栈&#xff08;Transmission Control Protocol/Internet Protocol Suite&#xff09;是互联网上进行数据通信的一系列网络协议的集合&#xff0c;它是现代计算机网络通信的基础架构。 它由多个不同的协议层构成&#xff0c;每层负责不同层面的数据处理和传输工作&…

PyQt5使用

安装Pyqt5信号与槽使用可视化界面编辑UI (Pyside2)ui生成之后的使用(两种方法)1 ui转化为py文件 进行import2 动态调用UI文件 安装Pyqt5 pip install pyqt5-tools这时候我们使用纯代码实现一个简单的界面 from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButto…

LabVIEW湍流等离子体束热效率优化

LabVIEW湍流等离子体束热效率优化 利用LabVIEW虚拟仪器技术&#xff0c;对湍流等离子体束的热效率进行了实时监测与优化&#xff0c;提高其在材料处理领域的应用效率和精度。通过双进气湍流等离子体发生器&#xff0c;实现了在不同工作参数下对热效率的实时在线监测&#xff0…

openssl3.2 - note - Writing OpenSSL Provider Skeleton

文章目录 openssl3.2 - note - Writing OpenSSL Provider Skeleton概述笔记测试工程的建立复现的provider工程总结Provider包含的头文件openssl/core.h中的数据结构实现 OSSL_provider_init()看一下openssl自带的提供者provider的openssl命令行测试provider的本质是hook了opens…

RabbitMQ高级-高级特性

1.消息可靠性传递 在使用RabbitMQ的时候&#xff0c;作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ为我们提供了两种方式来控制消息的投递可靠性模式 1.confirm 确认模式 确认模式是由exchange决定的 2.return 退回模式 回退模式是由routing…

数据结构-红黑树

1.容器 容器用于容纳元素集合&#xff0c;并对元素集合进行管理和维护&#xff0e; 传统意义上的管理和维护就是&#xff1a;增&#xff0c;删&#xff0c;改&#xff0c;查&#xff0e; 我们分析每种类型容器时&#xff0c;主要分析其增&#xff0c;删&#xff0c;改&#xff…

NFTScan 正式上线 Blast NFTScan 浏览器和 NFT API 数据服务

2024 年 3 月 15 号&#xff0c;NFTScan 团队正式对外发布了 Blast NFTScan 浏览器&#xff0c;将为 Blast 生态的 NFT 开发者和用户提供简洁高效的 NFT 数据搜索查询服务。NFTScan 作为全球领先的 NFT 数据基础设施服务商&#xff0c;Blast 是继 Bitcoin、Ethereum、BNBChain、…

wifi7有哪些新特性?看这份文档

更多内容在 这是H3C发布的关于wifi7的介绍文档。 回顾802.11协议发展历程&#xff0c;初版802.11协议速率仅为2Mbps。802.11b使用新的编码形式&#xff0c;将速 率提升到11Mbps。802.11a利用新的5GHz频段&#xff0c;引入OFDM技术并采用64-QAM调制将无线速 率提升到54Mbps。80…

sqllab第二十六关通关笔记

知识点&#xff1a; 空格替换 %09 %0a %0b %0c %0d %a0 (%2b)or替换&#xff1a;|| ||是不需要空格区分的and替换&#xff1a;&& &&同样不需要空格区分的双写绕过&#xff0c;但是绕过后需要和内容进行空格区分的&#xff0c;要不然不发挥作用&#xff1b;这关…

免费阅读篇 | 芒果YOLOv8改进113:注意力机制ShuffleAttention:深度卷积神经网络的随机注意力

&#x1f4a1;&#x1f680;&#x1f680;&#x1f680;本博客 改进源代码改进 适用于 YOLOv8 按步骤操作运行改进后的代码即可 该专栏完整目录链接&#xff1a; 芒果YOLOv8深度改进教程 该篇博客为免费阅读内容&#xff0c;YOLOv8ShuffleAttention改进内容&#x1f680;&…
最新文章