数据结构——单向链表和双向链表的实现(C语言版)

news2025/1/20 3:40:45

目录

前言

1. 链表

1.1 链表的概念及结构

1.2 链表的分类

2. 单链表接口实现

2.1 数据结构设计与接口函数声明

2.2 创建结点,打印,查找

2.3 尾插,头插,尾删,头删

2.4 插入或删除

2.4.1在指定位置后

2.4.2在指定位置前

2.5 销毁链表

3. 双向带头循环链表

3.1 数据结构设计与接口函数声明

3.2 初始化,销毁,打印,动态创建结点

3.3 尾插,头插,尾删,头删

3.4 查找,插入和删除

4.链表和顺序表的区别

5. 源代码

5.1 单链表

(1)SList.h

(2)SList.c

(3)SLtest.c

5.2 双向链表

(1)Linked LIst.h

(2)Linked List.c

(3)Ltest.c

总结


前言

这篇文章关于链表的介绍,还有单向链表和双向链表的C语言实现,内容干货满满,建议边看边上手敲代码!


1. 链表

1.1 链表的概念及结构

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

 如下图所示:

注意:

  1. 从上图可看出,链式结构逻辑上是连续的,但在物理上不一定连续的,地址存放分布不均。
  2. 现实中的节点一般都是从堆上申请出来的。
  3. 从堆上申请的空间,是按照一定策略来分配的,两次申请的空间可能连续,也可能不连续。

1.2 链表的分类

实际中链表有这三种分法,单向或者双向,带头或不带头,循环或者非循环。

  1. 单向或双向
  2. 带头或者不带头
  3. 循环或者非循环

虽然有这么多的链表结构,但是我们实际中最常用还是两种结构

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

2. 单链表接口实现

在实现接口时,需要创建三个文件,分别是SList.hSList.cSLtest.c这三个文件,第一个是写入单链表数据结构设计和接口函数声明来串联三个文件,第二个是完成各个接口函数内部代码实现,第三个是来测试各个接口功能情况。

2.1 数据结构设计与接口函数声明

单链表结构体中有存储一个数据的变量,但与顺序表的不同之处,是使用指针联系着下一个结点,所以在创建个相同结构体的指针next。单链表只需要知道头结点的结构体指针就可以进行各种接口的实现,所以不用创建一个初始化接口,创建一个结构体指针就可以。

typedef int SLTDataType;

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

//动态申请一个结点
SLTNode* BuyListNode(SLTDataType x);
//单链表打印
void SListPrint(SLTNode* phead);
//单链表尾插
void SListPushBack(SLTNode** pphead, SLTDataType x);
//单链表头插
void SListPushFront(SLTNode** pphead, SLTDataType x);
//单链表尾删
void SListPopBack(SLTNode** pphead);
//单链表头删
void SListPopFront(SLTNode** pphead);
//单链表查找
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
//在pos位置之前去插入一个节点
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
// 在pos位置后面插入
void SListInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos位置的节点
void SListErase(SLTNode** pphead, SLTNode* pos);
//删除pos位置后一个节点
void SListEraseAfter(SLTNode** pphead, SLTNode* pos);
//单链表销毁
void SListDestory(SLTNode** pphead);

2.2 创建结点,打印,查找

 创建新结点是为了后面接口实现做准备。

SLTNode* BuyListNode(SLTDataType x)
{   
    //动态申请新结点
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{   //可能开辟内存失败,加上判断,增强代码的健壮性
		printf("malloc fail\n");
		exit(-1);
	}

	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

打印链表中的内容,用的是while循环,判断条件是cur指针不为空。

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

assert是断言,判断phead是否为空结点,用while循环遍历整个链表。

SListNode* SListFind(SLTNode* phead, SLTDataType x)
{
	assert(phead);
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;
}

2.3 尾插,头插,尾删,头删

因为只需要知道指向头结点的指针变量,就可以进行操作,但是第一次我们创建的头结点为空指针,尾插的时候如果传入一级指针在尾插函数内部修改头节点的值,无法影响头结点,因为尾插函数内的头结点的一份临时拷贝,即形参,改变形参的值是无法影响实参的值,所以要传入二级指针,通过指针变量的指针修改一级指针的值。故之后关于头结点的删除或者插入,都需要传二级指针,这是链表实现中较难理解的点。

void SListPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuyListNode(x);
    //头结点为空时,直接赋值
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//找到尾节点
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

我们写一个测试函数,测试一下尾插函数的功能。之后的测试函数,只写全局函数Test,不展示main函数的部分。

#include "SLinked list.h"

void TestSlist1()
{
	SLTNode* plist = NULL;

	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SListPrint(plist);
}

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

输出结果:

头插较为简单,只需要创建一个新结点,新结点的next指针指向头结点,再把头结点指向新结点。

void SListPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuyListNode(x);

	newnode->next = *pphead;
	*pphead = newnode;
}

再写一个测试函数:

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

	SListPrint(plist);
}

输出的结果:

尾删要分情况,分为链表内有一个节点还是两个结点及以上。如果链表内只有一个结点直接释放头结点,并将其置为空指针,如果是两个节点及以上,需要找到尾结点的位置,用while循环遍历链表,新建一个tail指针,当tail的next指针为空时,便找到尾结点,然后进行释放操作。

void SListPopBack(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead != NULL);

	if ((*pphead)->next == NULL)
	{
		//1. 一个节点
		free(*pphead);
		*pphead = NULL;
	}
	else
	{	
		//2. 两个及以上的节点
		SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

在之前Test1函数上稍加改动:

void TestSlist1()
{
	SLTNode* plist = NULL;

	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SListPrint(plist);

	SListPopBack(&plist);
	SListPopBack(&plist);
	SListPopBack(&plist);
	SListPrint(plist);
}

输出的结果:

但是如果你调用尾删次数超过链表存储数据个数,就会报错。所以调用尾删函数需注意。

头删函数只需在创建一个next指针,并赋值为头结点的下一个结点,释放头结点,再赋值。

void SListPopFront(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead != NULL);

	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

同理,这次在Test2函数上进行改动即可。

void TestSlist2()
{
	SLTNode* plist = NULL;
	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPrint(plist);

	SListPopFront(&plist);
	SListPopFront(&plist);
	SListPopFront(&plist);
	SListPopFront(&plist);
	SListPrint(plist);
}

输出的结果:

2.4 插入或删除

一般是在指定位置后插入或删除,这是因为单链表的结点只有下一个结点的地址,如果想要在指定位置之前插入,需要从头结点开始遍历,消耗时间。

2.4.1在指定位置后

插入操作:

// 在pos位置后面插入,这个更适合单链表
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuyListNode(x);
	if (newnode == NULL)
	{
		return;
	}
	newnode->next = pos->next;
	pos->next = newnode;
}

删除操作:

void SListEraseAfter(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos->next);
	SLTNode* next = pos->next;
	pos->next = next->next;
	free(next);
}

写个测试函数,先通过查找函数,获得想要结点的地址,再修改。

void TestSlist3()
{
	SLTNode* plist = NULL;
	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPrint(plist);

	SLTNode* pos = SListFind(plist, 2);
	if (pos)
	{
		SListInsertAfter(pos, 30);
	}
	SListPrint(plist);

	pos = SListFind(plist, 30);
	if (pos)
	{
		SListEraseAfter(&plist, pos);
	}
	SListPrint(plist);
}

输出的结果:

2.4.2在指定位置前

在进行插入操作之前,应该先判断该位置是否为头结点,如果是头结点,直接头插;如果不是,需要遍历链表找到该位置结点的前一个节点,再插入。

void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuyListNode(x);
	if (*pphead == pos)
	{
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else
	{
		// 找到pos的前一个位置
		SLTNode* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}

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

删除也是一样,需要区分该位置是否为头结点,不是的话需要先找到该位置前一个节点,再删除。

void SListErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);

	//删除头节点
	if (*pphead == pos)
	{
		*pphead = pos->next;
		free(pos);//要不要把pos置为空指针呢
		pos = NULL;
	}
	else
	{	//找前一个节点
		SLTNode* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		//开始删除
		posPrev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

再写一个测试函数,与前一个测试函数类似。

void TestSlist4()
{
	SLTNode* plist = NULL;
	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPrint(plist);

	SLTNode* pos = SListFind(plist, 2);
	if (pos)
	{
		SListInsert(&plist, pos, 30);
	}
	SListPrint(plist);

	pos = SListFind(plist, 1);
	if (pos)
	{
		SListErase(&plist, pos);
	}
	SListPrint(plist);
}

 输出的结果:

2.5 销毁链表

销毁链表需要遍历整个链表,因为链表上的每一个结点都是动态开辟出来的。

void SListDestory(SLTNode** pphead)
{
	assert(pphead);

	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur); 
		cur = next;
	}
	*pphead = NULL;
}

3. 双向带头循环链表

开始之前,需要先创建三个文件List.h ,List.c和Ltest.c这三个文件。

  • Linked List.h文件包含所有用到的头文件,还有数据结构的设计和各种接口函数声明。
  • Linked List.c文件完成所有接口函数的实现。
  • Ltest.c主要代码是测试函数,来测试接口函数功能是否达标。

3.1 数据结构设计与接口函数声明

双向链表的数据结构中,不只有指向下一个结点的指针,还有指向上一个结点的指针。

typedef int LTDataType;

typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;

//创建返回链表的头结点
LTNode* ListInit();
//双向链表的销毁
void ListDestroy(LTNode* phead);
//双向链表打印
void ListPrint(LTNode* phead);
//双向链表尾插
void ListPushBack(LTNode* phead, LTDataType x);
//双向链表头插
void ListPopBack(LTNode* phead);
//双向链表尾删
void ListPushFront(LTNode* phead, LTDataType x);
//双向链表头删
void ListPopFront(LTNode* phead);
//双向链表查找
LTNode* ListFind(LTNode* phead, LTDataType x);
//双向链表在pos的前面进行插入
void ListInsert(LTNode* pos, LTDataType x);
//双向链表删除pos位置的结点
void ListErase(LTNode* pos);

3.2 初始化,销毁,打印,动态创建结点

初始化的时候不是都只为空,需要创建一个哨兵位结点,不存储有效数据,并且next和prev都需要指向头结点,函数返回类型是LTNode*这样子就不需要传入二级指针。

LTNode* ListInit()
{
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	if (phead == NULL)
	{
		exit(1);
	}
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

销毁操作跟单链表相同,需要逐个释放,最后头结点也得释放,但是不需要将头结点置为空指针,因为传入的是一级指针,此时销毁函数内的是形参,改变形参无法影响实参。

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

	free(phead);
}

打印函数使用while循环遍历链表即可。

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

动态创建一个新结点用处很大。注意next和prev都要置为空指针。

LTNode* BuyListNode(LTDataType x)
{
	LTNode* ptr = (LTNode*)malloc(sizeof(LTNode));
	if (ptr != NULL)
	{
		LTNode* newnode = ptr;
		newnode->data = x;
		newnode->next = NULL;
		newnode->prev = NULL;
		return newnode;
	}
	exit(1);
}

3.3 尾插,头插,尾删,头删

尾插不需要查找,只需要通过prev指针就能定位到。

void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* tail = phead->prev;
	LTNode* newnode = BuyListNode(x);
	newnode->data = x;

	//改变结点连接关系
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

头插操作如下,改变结点之间的指向问题。

void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyListNode(x);
	LTNode* next = phead->next;

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

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

尾删需要注意不能删除到哨兵位,通过断言头结点的下一个结点不能指向本身。

void ListPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
	
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	free(tail);

	tailPrev->next = phead;
	phead->prev = tailPrev;
}

头删的断言跟尾删一样,接下来就是释放并改变结点的指向。

void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* next = phead->next;
	LTNode* nextNext = next->next;
	
	phead->next = nextNext;
	nextNext->prev = phead;
	free(next);
}

3.4 查找,插入和删除

查找函数跟单链表查找函数类似,遍历链表。

LTNode* ListFind(LTNode* phead, LTDataType x)
{
	assert(phead);

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

		cur = cur->next;
	}
	printf("\n");
}

插入函数因为双链表结构的复杂性,反而变得十分简单,不需要遍历链表。

//pos位置之前插入
void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* posPrev = pos->prev;
	LTNode* newnode = BuyListNode(x);

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

删除函数也是,注意可通过新创建几个变量区分pos位置的前一个结点和后一个结点,方便操作。

//删除pos位置
void ListErase(LTNode* pos)
{
	assert(pos);

	// posPrev  pos  posNext
	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;

	posPrev->next = posNext;
	posNext->prev = posPrev;
	free(pos);
	pos = NULL;
}

写个测试函数:

void TestList2()
{
	LTNode* plist = ListInit();
	ListPushFront(plist, 1);
	ListPushFront(plist, 2);
	ListPushFront(plist, 3);
	ListPushFront(plist, 4);
	ListPrint(plist);

	LTNode* pos = ListFind(plist, 3);
	if (pos)
	{
		ListInsert(pos, 30);
	}
	ListPrint(plist);

	pos = ListFind(plist, 2);
	if (pos)
	{
		ListErase(pos);
	}
	ListPrint(plist);

	ListDestroy(plist);
	plist = NULL;
}

输出的结果:

4.链表和顺序表的区别

         不同点                顺序表                  链表
存储空间上  物理上一定连续逻辑上连续,物理上不一定连续
随机访问  支持O(1)  不支持 :O(N)
任意位置插入或删除元素可能需要搬移元素,效率低O(N)只需要修改指针指向
插入动态顺序表,空间不够需要扩容没有容量的概念
应用场景 元素高效存储+频繁访问任意位置插入和删除频繁
缓存利用率

5. 源代码

5.1 单链表

(1)SList.h

#pragma once

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

typedef int SLTDataType;

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

//动态申请一个结点
SLTNode* BuyListNode(SLTDataType x);
//单链表打印
void SListPrint(SLTNode* phead);
//单链表尾插
void SListPushBack(SLTNode** pphead, SLTDataType x);
//单链表头插
void SListPushFront(SLTNode** pphead, SLTDataType x);
//单链表尾删
void SListPopBack(SLTNode** pphead);
//单链表头删
void SListPopFront(SLTNode** pphead);
//单链表查找
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
//在pos位置之前去插入一个节点
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
// 在pos位置后面插入
void SListInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos位置的节点
void SListErase(SLTNode** pphead, SLTNode* pos);
//删除pos位置后一个节点
void SListEraseAfter(SLTNode** pphead, SLTNode* pos);
//单链表销毁
void SListDestory(SLTNode** pphead);

(2)SList.c

#include "SLinked list.h"

SLTNode* BuyListNode(SLTDataType 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");
}

SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
	assert(phead);
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;
}

void SListPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	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, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuyListNode(x);

	newnode->next = *pphead;
	*pphead = newnode;
}


void SListPopBack(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead != NULL);

	if ((*pphead)->next == NULL)
	{
		//1. 一个节点
		free(*pphead);
		*pphead = NULL;
	}
	else
	{	
		//2. 两个及以上的节点
		SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

void SListPopFront(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead != NULL);

	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

// 在pos位置后面插入,这个更适合单链表
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuyListNode(x);
	if (newnode == NULL)
	{
		return;
	}
	newnode->next = pos->next;
	pos->next = newnode;
}

void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuyListNode(x);
	if (*pphead == pos)
	{
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else
	{
		// 找到pos的前一个位置
		SLTNode* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}

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


void SListErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);

	//删除头节点
	if (*pphead == pos)
	{
		*pphead = pos->next;
		free(pos);//要不要把pos置为空指针呢
		pos = NULL;
	}
	else
	{	//找前一个节点
		SLTNode* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		//开始删除
		posPrev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

void SListEraseAfter(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos->next);

	SLTNode* next = pos->next;
	pos->next = next->next;
	free(next);
}

(3)SLtest.c

#include "SLinked list.h"

void TestSlist1()
{
	SLTNode* plist = NULL;

	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SListPrint(plist);

	SListPopBack(&plist);
	SListPopBack(&plist);
	SListPopBack(&plist);
	SListPrint(plist);
}

void TestSlist2()
{
	SLTNode* plist = NULL;
	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPrint(plist);

	SListPopFront(&plist);
	SListPopFront(&plist);
	SListPopFront(&plist);
	SListPopFront(&plist);
	SListPrint(plist);
}

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

	SListPrint(plist);

	SLTNode* pos = SListFind(plist, 2);
	if (pos)
	{
		SListInsertAfter(pos, 30);
	}
	SListPrint(plist);

	pos = SListFind(plist, 30);
	if (pos)
	{
		SListEraseAfter(&plist, pos);
	}
	SListPrint(plist);
}

void TestSlist4()
{
	SLTNode* plist = NULL;
	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPrint(plist);

	SLTNode* pos = SListFind(plist, 2);
	if (pos)
	{
		SListInsert(&plist, pos, 30);
	}
	SListPrint(plist);

	pos = SListFind(plist, 1);
	if (pos)
	{
		SListErase(&plist, pos);
	}
	SListPrint(plist);
}

int main()
{
	//TestSlist1();
	//TestSlist2();
	//TestSlist3();
	TestSlist4();

	return 0;
}

5.2 双向链表

(1)Linked LIst.h

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

typedef int LTDataType;

typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;

//创建返回链表的头结点
LTNode* ListInit();
//双向链表的销毁
void ListDestroy(LTNode* phead);
//双向链表打印
void ListPrint(LTNode* phead);
//双向链表尾插
void ListPushBack(LTNode* phead, LTDataType x);
//双向链表头插
void ListPopBack(LTNode* phead);
//双向链表尾删
void ListPushFront(LTNode* phead, LTDataType x);
//双向链表头删
void ListPopFront(LTNode* phead);
//双向链表查找
LTNode* ListFind(LTNode* phead, LTDataType x);
//双向链表在pos的前面进行插入
void ListInsert(LTNode* pos, LTDataType x);
//双向链表删除pos位置的结点
void ListErase(LTNode* pos);

(2)Linked List.c

#include "Linked List.h"

LTNode* ListInit()
{
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	if (phead == NULL)
	{
		exit(1);
	}
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

LTNode* BuyListNode(LTDataType x)
{
	LTNode* ptr = (LTNode*)malloc(sizeof(LTNode));
	if (ptr != NULL)
	{
		LTNode* newnode = ptr;
		newnode->data = x;
		newnode->next = NULL;
		newnode->prev = NULL;
		return newnode;
	}
	exit(1);
}

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

void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* tail = phead->prev;
	LTNode* newnode = BuyListNode(x);
	newnode->data = x;

	//改变结点连接关系
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}


void ListPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
	
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	free(tail);

	tailPrev->next = phead;
	phead->prev = tailPrev;
}

void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyListNode(x);
	LTNode* next = phead->next;

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

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

void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* next = phead->next;
	LTNode* nextNext = next->next;
	
	phead->next = nextNext;
	nextNext->prev = phead;
	free(next);
}

LTNode* ListFind(LTNode* phead, LTDataType x)
{
	assert(phead);

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

		cur = cur->next;
	}
	printf("\n");
}


//pos位置之前插入
void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* posPrev = pos->prev;
	LTNode* newnode = BuyListNode(x);

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


//删除pos位置
void ListErase(LTNode* pos)
{
	assert(pos);

	// posPrev  pos  posNext
	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;

	posPrev->next = posNext;
	posNext->prev = posPrev;
	free(pos);
	pos = NULL;
}


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

	free(phead);
}

(3)Ltest.c

测试函数也可以自行封装几个,参照单链表的测试函数。

#include "Linked List.h"

void TestList1()
{
	LTNode* plist = ListInit();
	
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPrint(plist);

	ListPopBack(plist);
	ListPopBack(plist);
	ListPrint(plist);

}

void TestList2()
{
	LTNode* plist = ListInit();
	ListPushFront(plist, 1);
	ListPushFront(plist, 2);
	ListPushFront(plist, 3);
	ListPushFront(plist, 4);
	ListPrint(plist);

	LTNode* pos = ListFind(plist, 3);
	if (pos)
	{
		ListInsert(pos, 30);
	}
	ListPrint(plist);

	pos = ListFind(plist, 2);
	if (pos)
	{
		ListErase(pos);
	}
	ListPrint(plist);

	ListDestroy(plist);
	plist = NULL;
}

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

	return 0;
}


总结

通过这篇文章,相信你已经对链表这个数据结构有了一定的了解,可以开始刷一些链表的OJ题目。如果只是看了一遍,建议上手敲敲代码,实践出真知。

创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连哦,你的支持的我最大的动力!!!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一文讲透Python函数中的形式参数和实际参数

函数参数包括形式参数和实际参数&#xff0c;简称形参和实参。其中形式参数即是在定义函数时函数后面括号中的参数列表&#xff08;parameterlist&#xff09;&#xff0c;比如上一个帖子的示例中的width, length&#xff1b;实际参数则是调用函数时函数后面括号中的参数值&…

【OrangePi Zero2的系统移植】OrangePi Zero2 SDK说明

一、使用环境要求 二、获取Linux SDK 三、首次编译完整SDK 基于OrangePi Zero2的系统移植 之前我们讲解香橙派的使用时&#xff0c; 都是直接在香橙派上进行代码编译&#xff0c; 但在实际的项目开发过程中&#xff0c;更多 的还是使用交叉编译环境进行代码的编译。再编译完成…

学习Android的第八天

目录 Android ImageView 图像视图 ImageView 的基本使用 src属性和background属性的区别 范例 解决 anndroid:blackground 属性拉伸导致图片变形的方法 设置透明度的问题 范例 android:src 和 android:background 结合 范例 Java 代码中设置 blackground 和 src 属性…

【C语言自定义类型详解进阶】结构体(补充结构体的对齐和位段,一口气看完系列,央妈都点赞的博文)

目录 1.结构体 1.1 结构的基础知识 1.2 结构的声明 1.2.1特殊的声明&#xff08;匿名结构体类型&#xff09; 1.3结构体变量的定义 1.4关于匿名结构体类型的补充 1.5结构体的自引用 1.6结构体变量的初始化 2.结构体内存对齐&#xff08;重点&#xff09; 2.1偏移量补…

B2080 计算多项式的值(洛谷)

题目描述 假定多项式的形式为 … x1&#xff0c;请计算给定单精度浮点数 x 和正整数 n 值的情况下这个多项式的值。多项式的值精确到小数点后两位&#xff0c;保证最终结果在 double 范围内。 输入格式 输入仅一行&#xff0c;包括 x 和 n&#xff0c;用单个空格隔开。 输…

数据结构 - 线索树

一、 为什么要用到线索二叉树&#xff1f; 我们先来看看普通的二叉树有什么缺点。下面是一个普通二叉树&#xff08;链式存储方式&#xff09;&#xff1a; 乍一看&#xff0c;会不会有一种违和感&#xff1f;整个结构一共有 7 个结点&#xff0c;总共 14 个指针域&#xff0c…

Web前端框架-Vue(初识)

文章目录 web前端三大主流框架**1.Angular****2.React****3.Vue**什么是Vue.js 为什么要学习流行框架框架和库和插件的区别一.简介指令v-cloakv-textv-htmlv-pre**v-once**v-onv-on事件函数中传入参数事件修饰符双向数据绑定v-model 按键修饰符自定义按键修饰符别名v-bind(属性…

速过计算机二级python——第9讲 详解第 2 套真题

第9讲 详解第 2 套真题 基本编程题【15 分】简单应用题【25 分】综合应用题【20 分】**问题 1**【10 分】:**问题 2【10 分】:**基本编程题【15 分】 考生文件夹下存在一个文件 PY101.py,请写代码替换横线,不修改其他代码,实现以下功能:【5 分】题目: import __________ b…

【数据结构与算法】力扣刷题记之 稀疏数组

&#x1f389;&#x1f389;欢迎光临&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;特别推荐给大家我的最新专栏《数据结构与算法&#xff1a;初学者入门指南》&#x1f4d8;&am…

手把手教你开发Python桌面应用-PyQt6图书管理系统-图书添加模块UI设计实现

锋哥原创的PyQt6图书管理系统视频教程&#xff1a; PyQt6图书管理系统视频教程 Python桌面开发 Python入门级项目实战 (无废话版) 火爆连载更新中~_哔哩哔哩_bilibiliPyQt6图书管理系统视频教程 Python桌面开发 Python入门级项目实战 (无废话版) 火爆连载更新中~共计24条视频&…

ClickHouse的优缺点和应用场景

当业务场景需要一个大批量、快速的、可支持聚合运算的数据库&#xff0c;那么可选择ClickHouse。 选择ClickHouse 的原因&#xff1a; 记录类型类似于LOG&#xff0c;读取、运算远远大于写入操作选取有限列&#xff0c;对近千万条数据&#xff0c;快算的运算出结果。数据批量…

LiteFlow规则引擎框架

LiteFlow规则引擎框架 Hi&#xff0c;我是阿昌&#xff0c;今天介绍一个规则引擎框架&#xff0c;LiteFlow&#xff1b; 一、前言 那首先得知道什么是规则引擎&#xff1f;规则引擎是 一种用于自动化处理业务规则的软件组件。 在软件行业中&#xff0c;规则引擎通常用于解决…

HTML+CSS+JS网页设计

文章目录 作品介绍一、代码演示1.登录、注册&#xff0c;获取当前时间2.轮播图3.家乡简介4.热门景点5.特色美食6.页尾 二、效果图总结 作品介绍 HTML页面主要由&#xff1a;登录、注册跳转页面&#xff0c;轮播图&#xff0c;家乡简介&#xff0c;热门景点&#xff0c;特色美食…

js中new操作符详解

文章目录 一、是什么二、流程三、手写new操作符 一、是什么 在JavaScript中&#xff0c;new操作符用于创建一个给定构造函数的实例对象 例子 function Person(name, age){this.name name;this.age age; } Person.prototype.sayName function () {console.log(this.name) …

NOVATEK显示技术系列之CEDSCHPI Training差异简介

CEDS的数据封包格式&#xff1a;首先CEDS数据封包包括三个部分&#xff1a; Training Pattern即Phase1Control Data 即 Phase2RGB Data 即Phase3 Power on Timing&#xff1a; 工作原理&#xff1a; Power ON时&#xff0c;TCON会发Training Pattern&#xff0c;当COF接受Tr…

springboot-web服务迁移Kubernetes

1、搞定基础镜像 docker pull openjdk:8-jre-alpine docker tag openjdk:8-jre-alpine 10.204.82.15/kubernetes/openjdk:8-jre-alpine docker push 10.204.82.15/kubernetes/openjdk:8-jre-alpine 2、springboot-web应用服务打包 3、编写Dockerfile构建镜像 FROM 10.204.82.…

Vue源码系列讲解——虚拟DOM篇【二】(Vue中的DOM-Diff)

目录 1. 前言 2. patch 3. 创建节点 4. 删除节点 5. 更新节点 6. 总结 1. 前言 在上一篇文章介绍VNode的时候我们说了&#xff0c;VNode最大的用途就是在数据变化前后生成真实DOM对应的虚拟DOM节点&#xff0c;然后就可以对比新旧两份VNode&#xff0c;找出差异所在&…

uTools工具使用

之前发现一款非常有用的小工具&#xff0c;叫uTools&#xff0c;该软件集成了比如进制转换、json格式化、markdown、翻译、取色等等集插件大成&#xff0c;插件市场提供了很多开源插件工具。可以帮助开发人员节省了寻找各种处理工具的时间&#xff0c;非常推荐。 1、软件官方下…

横扫Spark之 - 22个常见的转换算子

水善利万物而不争&#xff0c;处众人之所恶&#xff0c;故几于道&#x1f4a6; 文章目录 1. map()2. flatMap()3. filter()4. mapPartitions()5. mapPartitionsWithIndex()6. groupBy()7. distinct()8. coalesce()9. repartition()10. sortBy()11. intersection()12.union()13.…

Python实现文本情感分析

前言 文本情感分析是一种重要的自然语言处理(NLP)任务&#xff0c;旨在从文本数据中推断出情感信息&#xff0c;例如正面、负面或中性情感。它在社交媒体分析、产品评论、市场调研等领域都有广泛的应用。本文将详细介绍如何使用Python进行文本情感分析&#xff0c;包括基础概念…