链表界的“扛把子”—双向带头循环链表

目录

一:链表的分类

二:链表的实现

2-1:创建链表

2-2:创建新的结点

2-3:初始化链表 

2-4:打印链表  

2-5:销毁链表  

 三:链表的核心功能

3-1:尾插

3-2:尾删

3-3:链表查找

3-4:在pos结点前插入数据

3-5:头插

3-6:删除pos位置结点

3-7:头删

四:源码 


一:链表的分类

链表的分类分为以下几种:

 

在实际中链表的结构非常多样,以上情况组合起来就有8种链表结构。我们在前面已经讲过最简单的结构:单向无头非循环链表。并且在我们所刷的OJ题里也涉及到了有无头结点情况,今天我们讲解最复杂结构:双向带头循环链表。在我们讲解之前我们先了解一下这两种结构在链表界的重要性。

1:无头单向非循环链表:

结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

2:带头双向循环链表:

结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。

                                 正文开始

二:链表的实现

2-1:创建链表

思路:

因为链表为双向链表,所以在我们创建的结构体中需要创建两个结构体指针prev和next,其中prev指向上一个结点,next指向下一个结点。

Lish.h文件 


typedef int LTDataType; //重定义方便数据类型的转换,这里以int为主
typedef struct ListNode
{
	LTDataType data;       //存储数据
	struct ListNode* next; //存储下一个结点的地址
	struct ListNode* prev; //存储上一个结点的地址
}LTNode;

 2-2:创建新的结点

Lish.h文件 

//创建一个结点
LTNode* BuyLTNode(LTDataType x);

List.c文件 

//创建一个结点
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL) //检查内存是否开辟成功
	{
		printf("malloc fail\n");
		exit(-1);
	}

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

 2-3:初始化链表 

思路1:

我们所创建的链表为带头链表,所以我们在初始化链表的时候就需要把哨兵位头结点创建好,哨兵位头结点不存储数据,但却有着至关重要的作用,当链表为空链表时哨兵位头结点仍存在。同时形参的改变并不会影响实参,所以我们在传参的时候需要传二级指针,因为保存哨兵位头结点地址的地址不可能为空,所以我们需要用assert进行断言,同时因为是双向链表,所以哨兵位结点的prev和next都指向自己。

⭐:对于不可能出现空指针的情况,我们需要断言。

Lish.h文件 

//初始化链表(二级指针)
void ListInit(LTNode** pphead);

List.c文件 

//初始化链表(二级指针)
void ListInit(LTNode** pphead)
{
	assert(pphead); //pphead不可能为空指针
	*pphead = BuyLTNode(0);//创建一个结点给哨兵位头结点
	//哨兵位头结点初始化next和prev不能为空,需都指向自己
	(*pphead)->next = *pphead;//注意优先级问题,需要加括号。
	(*pphead)->prev = *pphead;
}

思路2:

如果我们想传一级指针也可以,只不过函数的返回值为结构体指针。

Lish.h文件 

//初始化链表(一级指针)
LTNode* ListInit();

List.c文件 

//初始化链表(一级指针)
LTNode* ListInit()
{
	LTNode*phead = BuyLTNode(0);
	phead->prev = phead;
	phead->next = phead;
	return phead;
}

 2-4:打印链表  

思路:

首先需要明确的是哨兵位结点不存储数据,所以我们要打印链表数据就需要创建一个指针变量cur从phead->next开始依次遍历打印链表,因为链表是循环的,所以当cur回到phead时停止。

Lish.h文件 

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

List.c文件 

//打印数据
void ListPrint(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;//cur从phead->next开始
	while (cur != phead)//cur回到phead时停止
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

  2-5:销毁链表  

思路:

销毁链表我们可以创建指针变量cur遍历链表,先把有效数据的结点free掉,最后再把哨兵位结点free掉即可。

Lish.h文件 

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

List.c文件 

//销毁链表
void ListDestory(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)//从第一个结点开始依次销毁
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	//销毁哨兵结点
	free(phead);
	phead = NULL;
}

看到这里是不是觉得双链表也就那样,并没有想象中的那么难,前面已经理解的话我们继续往下走,不理解的话多看几遍多敲敲代码,说不定过一会就理解了~~

 三:链表的核心功能

3-1:尾插

思路:

尾插就体现了双向链表的结构优势,因为哨兵位头节点的phead的prev指向的就是链表尾结点的地址,所以我们找到了尾结点后就需要再创建一个结点存储插入数据。

 Lish.h文件 


//尾插一个结点
void ListPushBack(LTNode* phead, LTDataType x);

List.c文件 

//尾插一个结点
void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* tail = phead->prev;//保存尾结点地址
	LTNode* newnode = BuyLTNode(x);//创建一个新的结点

	//循环链表
	tail->next = newnode;
	newnode->prev = tail;

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

Test.c文件

void TestList1()
{
	LTNode* pList = NULL;
	ListInit(&pList);
	ListPushBack(pList, 1);
	ListPushBack(pList, 2);
	ListPushBack(pList, 3);
	ListPushBack(pList, 4);
	ListPrint(pList);
}

3-2:尾删

思路:

当我们谈到尾删时,双向链表的结构优势再次体现出来。对于传统的单向链表如果想要实现尾删就必须遍历链表,找到尾结点将其free掉,遍历链表时间复杂度为O(N)。而对于双向链表来说因为链表是循环的,所以链表尾结点的地址时刻存在哨兵位结点的prev里面,时间复杂度为O(1),我们可以创建指针变量tail保存尾结点地址。同时删除尾结点的同时我们也要保证尾结点的上一个结点地址被保存,所以我们创建指针变量tailPrev指向tail的prev,然后把tailPrev的next指向哨兵位结点phead,把哨兵位phead的prev置成tailPrev。

 Lish.h文件 

//尾删一个结点
void ListPopBack(LTNode* phead);

List.c文件 

//尾删一个结点
void ListPopBack(LTNode* phead)
{
	assert(phead);//哨兵位不能为空
	
	assert(phead->next != phead);//链表不能为空,防止删除哨兵位结点

	LTNode* tail = phead->prev;//尾结点放入tail
	LTNode* tailPrev = tail->prev;//新的尾结点tailPrev

	free(tail);
	tail = NULL;
	//循环链表
	tailPrev->next = phead;
	phead->prev = tailPrev;
}

Test.c文件

void TestList2()
{
	LTNode* pList = NULL;
	ListInit(&pList);
	ListPushBack(pList, 1);
	ListPushBack(pList, 2);
	ListPushBack(pList, 3);
	ListPushBack(pList, 4);
	ListPrint(pList);
	ListPopBack(pList);
	ListPopBack(pList);
	ListPopBack(pList);
    ListPrint(pList);
}

 3-3:链表查找

思路:

创建一个指针变量cur指向哨兵位phead的next,然后开始遍历链表,判断cur->data是否为查找的数据,如果是就返回cur,不是继续遍历(cur=->cur->next),终止条件是cur指向哨兵位结点。

 Lish.h文件 

//链表查找
LTNode* ListFind(LTNode* phead, LTDataType x);

List.c文件 

//链表查找
LTNode* ListFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

 3-4:在pos结点前插入数据

思路:

用上一个查找数据函数查找pos,然后在pos之前插入数据:创建一个新的结点newnode,pos上一个结点的next指向新的结点newnode,同时把newnode的prev指向pos上一个结点,newnode的next指向pos,pos的prev指向newnode。

Lish.h文件 

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

List.c文件 

//在pos前插入数据
void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	//创建插入结点
	LTNode* newnode = BuyLTNode(x);
	//存储pos上一个结点地址
	LTNode* posPrev = pos->prev;

	//循环链表
	newnode->next = pos;
	pos->prev = newnode;
	posPrev->next = newnode;
	newnode->prev = posPrev;
}

Test.c文件

void TestList3()
{
	LTNode* pList = NULL;
	ListInit(&pList);
	ListPushBack(pList, 1);
	ListPushBack(pList, 2);
	ListPushBack(pList, 3);
	ListPushBack(pList, 4);
	ListPrint(pList);

	LTNode* pos = ListFind(pList, 3);
	if (pos)//判断链表中是否有要查找的元素
	{
		ListInsert(pos, 30);//在数字3前插入30
	}
	ListPrint(pList);
}

⭐:有了这个函数那么尾插也就可以更改一下,直接调用这个函数。

//尾插一个结点
void ListPushBack(LTNode* phead, LTDataType x)
{
      assert(phead);
	ListInsert(phead, x);
}

 3-5:头插

思路:

同样的,头插我们也可以引用ListInsert函数,当pos=phead->next时,我们实现的就是头插。

Lish.h文件 

//头插
void ListPushFront(LTNode* phead, LTDataType x);

List.c文件 

//头插
void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	ListInsert(phead->next, x);
}

Test.c文件

void TestList4()
{
	LTNode* pList = NULL;
	ListInit(&pList);
	ListPushBack(pList, 1);
	ListPushBack(pList, 2);
	ListPushBack(pList, 3);
	ListPushBack(pList, 4);
	ListPrint(pList);
	//头插10和20
	ListPushFront(pList, 10);
	ListPushFront(pList, 20);
	ListPrint(pList);
}

 3-6:删除pos位置结点

思路:

创建两个指针变量prev和next分别保存pos上一个结点和下一个结点的地址,然后将pos结点free掉,连接prev和next。

Lish.h文件 

//删除pos处结点
void ListErase(LTNode* pos);

List.c文件 

//删除pos处结点
void ListErase(LTNode* pos)
{
	assert(pos);
	//保存pos前一个结点
	LTNode* prev = pos->prev;
	//保存pos下一个结点
	LTNode* next = pos->next;
	free(pos);
	pos = NULL;
	//连接链表
	prev->next = next;
	next->prev->prev;
}

Test.c文件

void TestList5()
{
	LTNode* pList = NULL;
	ListInit(&pList);
	ListPushBack(pList, 1);
	ListPushBack(pList, 2);
	ListPushBack(pList, 3);
	ListPushBack(pList, 4);
	ListPrint(pList);

	LTNode* pos = ListFind(pList, 3);
	if (pos)//判断链表中是否有要查找的元素
	{
		ListErase(pos);//删除pos位结点
        pos=NULL//防止野指针
	}
	ListPrint(pList);
}

⭐:当我们有了ListErase函数后,直接调用此函数就能实现尾删。

void ListPopBack(LTNode* phead)
{
    assert(phead);
	assert(phead->next != phead);
	ListErase(phead->prev);
}

 3-7:头删

思路:

直接调用ListErase函数,删除head->next位置结点。

Lish.h文件 

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

List.c文件 

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

	ListErase(phead->next);
}

Test.c文件

void TestList6()
{
	LTNode* pList = NULL;
	ListInit(&pList);
	ListPushBack(pList, 1);
	ListPushBack(pList, 2);
	ListPushBack(pList, 3);
	ListPushBack(pList, 4);
	ListPushBack(pList, 5);
	ListPushBack(pList, 6);
	ListPrint(pList);
	//头插三个元素
	ListPushFront(pList, 0);
	ListPushFront(pList, -1);
	ListPushFront(pList, -2);
	ListPrint(pList);
	//尾删三个元素
	ListPopBack(pList);
	ListPopBack(pList);
	ListPopBack(pList);
	ListPrint(pList);
	//头删三个元素
	ListPopFront(pList);
	ListPopFront(pList);
	ListPopFront(pList);
	ListPrint(pList);
}

接口终于都实现完了~

 

四:源码 

List.h文件

#pragma once

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

typedef int LTDataType; //重定义方便数据类型的转换,这里以int为主
typedef struct ListNode
{
	LTDataType data;       //存储数据
	struct ListNode* next; //存储下一个结点的地址
	struct ListNode* prev; //存储上一个结点的地址
}LTNode;
//初始化链表(二级指针)
void ListInit(LTNode** pphead);
//初始化链表(一级指针)
//LTNode* ListInit();

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

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

//创建一个结点
LTNode* BuyLTNode(LTDataType x);

//尾插一个结点
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);

List.c文件

#include "List.h"

//初始化链表(二级指针)
void ListInit(LTNode** pphead)
{
	assert(pphead);
	*pphead = BuyLTNode(0);//创建一个结点给哨兵位头结点
	//哨兵位头结点初始化next和prev不能为空,需都指向自己
	(*pphead)->next = *pphead;//注意优先级问题,需要加括号。
	(*pphead)->prev = *pphead;
}

初始化链表(一级指针)
//LTNode* ListInit()
//{
//	LTNode*phead = BuyLTNode(0);
//	phead->prev = phead;
//	phead->next = phead;
//	return phead;
//}

//销毁链表
void ListDestory(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)//从第一个结点开始依次销毁
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	//销毁哨兵结点
	free(phead);
	phead = NULL;
}


//打印数据
void ListPrint(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;//cur从phead->next开始
	while (cur != phead)//cur回到phead时停止
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}


//创建一个结点
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

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

//尾插
void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* tail = phead->prev;//保存尾结点地址
	LTNode* newnode = BuyLTNode(x);//创建一个新的结点

	//循环链表
	tail->next = newnode;
	newnode->prev = tail;

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

	//法二
	/*assert(phead);
	ListInsert(phead, x);*/
}

//尾删
void ListPopBack(LTNode* phead)
{
	assert(phead);//哨兵位不能为空
	
	assert(phead->next != phead);//链表不能为空,防止删除哨兵位结点

	LTNode* tail = phead->prev;//尾结点放入tail
	LTNode* tailPrev = tail->prev;//新的尾结点tailPrev

	free(tail);
	tail = NULL;
	//循环链表
	tailPrev->next = phead;
	phead->prev = tailPrev;

	/*assert(phead);
	assert(phead->next != phead);
	ListErase(phead->prev);*/
}

//链表查找
LTNode* ListFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

//在pos前插入数据
void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	//创建插入结点
	LTNode* newnode = BuyLTNode(x);
	//存储pos上一个结点地址
	LTNode* posPrev = pos->prev;

	//循环链表
	newnode->next = pos;
	pos->prev = newnode;
	posPrev->next = newnode;
	newnode->prev = posPrev;
}

//头插
void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	ListInsert(phead->next, x);
}

//删除pos处结点
void ListErase(LTNode* pos)
{
	assert(pos);
	//保存pos前一个结点
	LTNode* prev = pos->prev;
	//保存pos下一个结点
	LTNode* next = pos->next;
	free(pos);
	pos = NULL;
	//连接链表
	prev->next = next;
	next->prev=prev;
}

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

	ListErase(phead->next);
}

Test.c文件

#include "List.h"

void TestList1()
{
	LTNode* pList = NULL;
	ListInit(&pList);
	ListPushBack(pList, 1);
	ListPushBack(pList, 2);
	ListPushBack(pList, 3);
	ListPushBack(pList, 4);
	ListPrint(pList);
}

void TestList2()
{
	LTNode* pList = NULL;
	ListInit(&pList);
	ListPushBack(pList, 1);
	ListPushBack(pList, 2);
	ListPushBack(pList, 3);
	ListPushBack(pList, 4);
	ListPrint(pList);
	ListPopBack(pList);
	ListPopBack(pList);
	ListPopBack(pList);
    ListPrint(pList);
}

void TestList3()
{
	LTNode* pList = NULL;
	ListInit(&pList);
	ListPushBack(pList, 1);
	ListPushBack(pList, 2);
	ListPushBack(pList, 3);
	ListPushBack(pList, 4);
	ListPrint(pList);

	LTNode* pos = ListFind(pList, 3);
	if (pos)//判断链表中是否有要查找的元素
	{
		ListInsert(pos, 30);//在数字3前插入30
	}
	ListPrint(pList);
}

void TestList4()
{
	LTNode* pList = NULL;
	ListInit(&pList);
	ListPushBack(pList, 1);
	ListPushBack(pList, 2);
	ListPushBack(pList, 3);
	ListPushBack(pList, 4);
	ListPrint(pList);
	//头插10和20
	ListPushFront(pList, 10);
	ListPushFront(pList, 20);
	ListPrint(pList);
}

void TestList5()
{
	LTNode* pList = NULL;
	ListInit(&pList);
	ListPushBack(pList, 1);
	ListPushBack(pList, 2);
	ListPushBack(pList, 3);
	ListPushBack(pList, 4);
	ListPrint(pList);

	LTNode* pos = ListFind(pList, 3);
	if (pos)//判断链表中是否有要查找的元素
	{
		ListErase(pos);//删除pos位结点
		pos = NULL;
	}
	ListPrint(pList);
}

void TestList6()
{
	LTNode* pList = NULL;
	ListInit(&pList);
	ListPushBack(pList, 1);
	ListPushBack(pList, 2);
	ListPushBack(pList, 3);
	ListPushBack(pList, 4);
	ListPushBack(pList, 5);
	ListPushBack(pList, 6);
	ListPrint(pList);
	//头插三个元素
	ListPushFront(pList, 0);
	ListPushFront(pList, -1);
	ListPushFront(pList, -2);
	ListPrint(pList);
	//尾删三个元素
	ListPopBack(pList);
	ListPopBack(pList);
	ListPopBack(pList);
	ListPrint(pList);
	//头删三个元素
	ListPopFront(pList);
	ListPopFront(pList);
	ListPopFront(pList);
	ListPrint(pList);
}

int main()
{
	/*TestList1();*/
	/*TestList2();*/
	/*TestList3();*/
	/*TestList4();*/
	/*TestList5();*/
	TestList6();

	return 0;
}

看完觉得有收获的要记得点赞噢~ 

 

  • 77
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 85
    评论
评论 85
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值