数据结构初阶--带头双向循环链表

目录

一.带头双向循环链表的定义

二.带头双向循环链表的功能实现

2.1.带头双向循环链表的定义

2.2.带头双向循环链表的结点创建

2.3.带头双向循环链表的初始化

2.4.带头双向循环链表的打印

2.5.带头双向循环链表的判空

2.6.带头双向循环链表的尾插

2.7.带头双向循环链表的头插

2.8.带头双向循环链表的尾删

2.9.带头双向循环链表的头删

2.10.带头双向循环链表的在pos位置之前插入

2.11.带头双向循环链表的删除pos位置的结点

2.12.带头双向循环链表的求链表长度

2.13.带头双向循环链表的销毁

2.14.完整程序

List.h

List.c

test.c

三. 顺序表和链表的比较

逻辑结构

存储结构

基本操作

创建

销毁

增加与删除

查找


一.带头双向循环链表的定义

循环单链表虽然能够实现从任一结点出发沿着链能找到其前驱结点,但时间耗费是O(n)。如果希望从表中快速确定某一个结点的前驱,另一个解决方法就是在单链表的每个结点里再增加一个指向其前驱的指针域prior。这样形成的链表中就有两条方向不同的链,称之为双(向)链表。

与单链表类似,双链表也可增加头结点使双链表的某些运算变得方便。同时双向链表也可以有循环表,称为双向循环链表。

由于在双向链表中既有前向链又有后向链,寻找任一结点的直接前驱结点与直接后继结点都变得非常方便了。

二.带头双向循环链表的功能实现

2.1.带头双向循环链表的定义

//定义
typedef int LTDataType;

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

 与单链表的定义不同,带头双向循环链表要定义两个指针:前驱指针pre和后继指针next。前驱指针pre用于指向当前结点的上一个结点,后继指针next用于指向当前结点的下一个结点。

2.2.带头双向循环链表的结点创建

LTNode* BuyListNode(LTDataType x)
{
	//动态开辟一个结点node
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));

	//判空
	if (node == NULL)
	{
		perror("malloc fail!");
		exit(-1);
	}

	//前驱与后继结点均置为空
	node->data = x;
	node->next = NULL;
	node->pre = NULL;

	return node;
}

结点的创建主要是通过调用malloc函数来实现,初始化时要将前驱指针和后继指针都置为NULL。

2.3.带头双向循环链表的初始化

版本一:

void ListInit(LTNode** phead)
{
	//这里需要传入二级指针,即传地址,才能实现对链表的修改

	//判空
	assert(phead);
	
	//创建头结点
	*phead = BuyListNode(-1);

	//将头结点的前驱指针和后继指针均指向自身
	(*phead)->next = *phead;
	(*phead)->pre = *phead;
}

版本二:

LTNode* ListInit()
{
	//创建头结点
	LTNode* phead = BuyListNode(-1);

	//将头结点的前驱指针和后继指针均指向自身
	phead->next = phead;
	phead->pre = phead;

	//返回头结点
	return phead;
}

链表的初始化采用了两种方式:传二级指针和设置返回值。

总结:

如果要改变头指针,就要传二级指针。不需要改变头指针的话,则传入一级指针。

在使用带头结点的单链表时:

  1. 初始化链表头指针需要传二级指针;
  2. 销毁链表需要传二级指针;
  3. 插入、删除、遍历、清空结点用一级指针即可。

不带头结点的单链表,除了初始化和销毁,插入、删除和清空结点也需要二级指针。

调试分析:

2.4.带头双向循环链表的打印

void ListPrint(LTNode* phead)
{
	//判空
	assert(phead);

	//cur指向链表的第一个结点
	LTNode* cur = phead->next;

	//cur依次向后遍历,直到cur重新回到头结点
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}

	printf("\n");
}

设置一个临时变量cur,指向当前链表的第一个结点(非头结点),然后依次向后遍历该链表,直到cur重新回到头结点phead的位置。

2.5.带头双向循环链表的判空

bool ListEmpty(LTNode* phead)
{
	//判空
	assert(phead);

	//如果phead->next等于phead,则链表为空,返回true
	//如果phead->next不等于phead,则链表不为空,返回false
	return phead->next == phead;
}

如果phead->next等于phead,则链表为空,返回true;如果phead->next不等于phead,则链表不为空,返回false。

2.6.带头双向循环链表的尾插

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

	//创建新结点
	LTNode* newnode = BuyListNode(x);

	//查找尾结点
	LTNode* tail = phead->pre;

	//尾插
	//原尾和新尾相互链接
	tail->next = newnode;
	newnode->pre = tail;
	//头结点和新尾相互链接
	newnode->next = phead;
	phead->pre = newnode;
}

相较于单链表的尾插,带头双向循环链表的尾插不需要从头结点开始依次向后遍历,因为头结点的前驱结点便指向尾结点tail。在找到尾结点tail之后,便可将新结点newnode插入到尾结点tail的后面。此时newnode变为新的尾结点。

调试分析:

运行结果:

2.7.带头双向循环链表的头插

void ListPushFront(LTNode* phead, LTDataType x)
{
	//判空
	assert(phead);

	//创建新结点
	LTNode* newnode = BuyListNode(x);

	//头插
	//phead newnode next:三者不分先后顺序
	//法一:
	LTNode* next = phead->next;
	phead->next = newnode;
	newnode->pre = phead;
	newnode->next = next;
	next->pre = newnode;

	//phead newnode phead->next:先处理后两个,再处理前两个
    //法二:
	//phead->next->pre = newnode;
	//newnode->next = phead->next;
	//phead->next = newnode;
	//newnode->pre = phead;
}

在进行头插时,要注意结点之间插入的先后顺序,这里主要介绍两种方式。方式一:创建一个临时变量next,然后将头结点的下一个结点保存在next当中。首先调用BuyListNode(x)创建一个新结点newnode,然后将phead,newnode和next三个结点进行链接。三个结点不分先后顺序,直接进行链接即可。该方式最为简单,也最不容易出错;方式二:不创建临时变量next。首先调用BuyListNode(x)创建一个新结点newnode,然后将phead,newnode和phead->next三个结点进行链接。链接是关键:要先将后两个结点进行链接,然后再将前两个结点进行链接。三个结点一定要注意先后顺序,不可随意链接。

调试分析:

运行结果:

2.8.带头双向循环链表的尾删

void ListPopBack(LTNode* phead)
{
	//判空
	assert(phead);

	//判断链表是否为空
	assert(phead->next != phead);
	//assert(!ListEmpty(phead));

	//找尾结点
	LTNode* tail = phead->pre;

	//找尾结点的前一结点
	LTNode* tailPre = tail->pre;

	//释放尾结点
	free(tail);

	tailPre->next = phead;
	phead->pre = tailPre;
}

在进行尾删之前,首先要判断链表是否为空,可以通过phead->next != phead进行判断,也可以调用ListEmpty(phead)函数进行判断;然后找到链表的尾结点tail,以及链表尾结点的前一个结点tailPre;接着调用free函数释放尾结点tail,并将tailPre作为新的尾结点;最后再将新的尾结点与头结点phead进行相连即可。

调试分析:

运行结果:

2.9.带头双向循环链表的头删

void ListPopFront(LTNode* phead)
{
	//判空
	assert(phead);

	//判断链表是否为空
	assert(phead->next != phead);
	//assert(!ListEmpty(phead));

	//tail记录第一个结点之后的下一个结点
	LTNode* tail = phead->next->next;
	
	//释放第一个结点
	free(phead->next);

	//将头结点和tail相链接
	phead->next = tail;
	tail->pre = phead;
}

在进行头删之前,首先要判断链表是否为空,可以通过phead->next != phead进行判断,也可以调用ListEmpty(phead)函数进行判断;然后找到链表的第二个有效结点tail;接着调用free函数释放掉第一个有效结点,并将tail作为新的第一个有效结点;最后再将新的第一个结点tail与头结点phead进行相连即可。

调试分析:

运行结果:

2.10.带头双向循环链表的在pos位置之前插入

void ListInsert(LTNode* pos, LTDataType x)
{
	//判空
	assert(pos);

	//查找pos的前一个结点
	LTNode* pre = pos->pre;

	//创建新结点
	LTNode* newnode = BuyListNode(x);

	//pre newnode pos
	pre->next = newnode;
	newnode->pre = pre;
	newnode->next = pos;
	pos->pre = newnode;
}

给定一个结点pos,如果是带头双向循环链表,那么pos之前的结点和pos之后的结点都是可知的。要在pos位置之前插入,首先要找到pos的前一结点pre,然后调用BuyListNode(x)创建一个新结点newnode,接着将pre,newnode和pos三个结点进行链接即可。此时pos位置的结点将由pos变为newnode。

调试分析:

运行结果:

2.11.带头双向循环链表的删除pos位置的结点

void ListErase(LTNode* pos)
{
	//判空
	assert(pos);

	//查找pos的前一个结点
	LTNode* pre = pos->pre;
	
	//查找pos的后一个结点
	LTNode* next = pos->next;

	//将前一个结点pre与后一个结点next相链接
	pre->next = next;
	next->pre = pre;

	//释放pos结点
	free(pos);
}

在删除pos位置的结点之前,首先要找到pos位置的前一个结点pre,然后找到pos位置的后一个结点next,接着将结点pre与next相链接,最后再调用free函数释放掉pos结点即可。

调试分析:

运行结果:

2.12.带头双向循环链表的求链表长度

int ListSize(LTNode* phead)
{
	//判空
	assert(phead);

	//cur指向当前链表的第一个结点
	LTNode* cur = phead->next;
	
	//用于记录遍历过的结点数
	int size = 0;

	//从第一个结点开始依次向后遍历,直到遍历到头结点
	while (cur != phead)
	{
		++size;
		cur = cur->next;
	}

	return size;
}

调试分析:

运行结果:

2.13.带头双向循环链表的销毁

void ListDestory(LTNode* phead)
{
	//判空
	assert(phead);

	//cur指向当前第一个结点
	LTNode* cur = phead->next;

	while (cur != phead)
	{
		//保存cur的下一个结点
		LTNode* next = cur->next;

		//删除cur
		ListErase(cur);

		//更新cur
		cur = next;
	}

	//释放头结点
	free(phead);
}

调试分析:

运行结果:

2.14.完整程序

List.h

#pragma once

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

//带头双向循环链表

//定义
typedef int LTDataType;

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

//创建结点
LTNode* BuyListNode(LTDataType x);

//初始化:版本一
//void ListInit(LTNode** phead);

//初始化:版本二
LTNode* ListInit();

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

//判空
bool ListEmpty(LTNode* phead);

//尾插
//不用二级指针的原因:尾插时不会改变phead,因为它带哨兵位,尾插时不会对哨兵位进行修改
void ListPushBack(LTNode* phead, LTDataType x);

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

//尾删
void ListPopBack(LTNode* phead);

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

//在pos位置之前插入
void ListInsert(LTNode* pos, LTDataType x);

//删除pos位置的结点
void ListErase(LTNode* pos);

//链表长度
int ListSize(LTNode* phead);

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

List.c

#define _CRT_SECURE_NO_WARNINGS 1


#include"List.h"


//创建结点
LTNode* BuyListNode(LTDataType x)
{
	//动态开辟一个结点node
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));

	//判空
	if (node == NULL)
	{
		perror("malloc fail!");
		exit(-1);
	}

	//前驱与后继结点均置为空
	node->data = x;
	node->next = NULL;
	node->pre = NULL;

	return node;
}


//初始化
/*
void ListInit(LTNode** phead)
{
	//这里需要传入二级指针,即传地址,才能实现对链表的修改

	//判空
	assert(phead);
	
	//创建头结点
	*phead = BuyListNode(-1);

	//将头结点的前驱指针和后继指针均指向自身
	(*phead)->next = *phead;
	(*phead)->pre = *phead;
}
*/


//初始化
LTNode* ListInit()
{
	//创建头结点
	LTNode* phead = BuyListNode(-1);

	//将头结点的前驱指针和后继指针均指向自身
	phead->next = phead;
	phead->pre = phead;

	//返回头结点
	return phead;
}


//打印
void ListPrint(LTNode* phead)
{
	//判空
	assert(phead);

	//cur指向链表的第一个结点
	LTNode* cur = phead->next;

	//cur依次向后遍历,直到cur重新回到头结点
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}

	printf("\n");
}


//判空
bool ListEmpty(LTNode* phead)
{
	//判空
	assert(phead);

	//如果phead->next等于phead,则链表为空,返回true
	//如果phead->next不等于phead,则链表不为空,返回false
	return phead->next == phead;
}


//尾插
void ListPushBack(LTNode* phead, LTDataType x)
{
	//判空
	assert(phead);

	/*
	//创建新结点
	LTNode* newnode = BuyListNode(x);

	//查找尾结点
	LTNode* tail = phead->pre;

	//尾插
	//原尾和新尾相互链接
	tail->next = newnode;
	newnode->pre = tail;
	//头结点和新尾相互链接
	newnode->next = phead;
	phead->pre = newnode;
	*/

	//尾插
	ListInsert(phead, x);//是phead而不是phead->pre
}


//头插
void ListPushFront(LTNode* phead, LTDataType x)
{
	//判空
	assert(phead);

	/*
	//创建新结点
	LTNode* newnode = BuyListNode(x);

	//头插
	//phead newnode next:三者不分先后顺序
	//法一:
	LTNode* next = phead->next;
	phead->next = newnode;
	newnode->pre = phead;
	newnode->next = next;
	next->pre = newnode;

	//phead newnode phead->next:先处理后两个,再处理前两个
    //法二:
	//phead->next->pre = newnode;
	//newnode->next = phead->next;
	//phead->next = newnode;
	//newnode->pre = phead;
	*/

	//头插
	ListInsert(phead->next, x);
}


//尾删
void ListPopBack(LTNode* phead)
{
	//判空
	assert(phead);

	//判断链表是否为空
	assert(phead->next != phead);
	//assert(!ListEmpty(phead));

	/*
	//找尾结点
	LTNode* tail = phead->pre;

	//找尾结点的前一结点
	LTNode* tailPre = tail->pre;

	//释放尾结点
	free(tail);

	tailPre->next = phead;
	phead->pre = tailPre;
	*/

	//尾删
	ListErase(phead->pre);
}


//头删
void ListPopFront(LTNode* phead)
{
	//判空
	assert(phead);

	//判断链表是否为空
	assert(phead->next != phead);
	//assert(!ListEmpty(phead));

	/*
	//tail记录第一个结点之后的下一个结点
	LTNode* tail = phead->next->next;
	
	//释放第一个结点
	free(phead->next);

	//将头结点和tail相链接
	phead->next = tail;
	tail->pre = phead;
	*/

	//头删
	ListErase(phead->next);
}


//在pos位置之前插入x
void ListInsert(LTNode* pos, LTDataType x)
{
	//判空
	assert(pos);

	//查找pos的前一个结点
	LTNode* pre = pos->pre;

	//创建新结点
	LTNode* newnode = BuyListNode(x);

	//pre newnode pos
	pre->next = newnode;
	newnode->pre = pre;
	newnode->next = pos;
	pos->pre = newnode;
}


//删除pos位置的结点
void ListErase(LTNode* pos)
{
	//判空
	assert(pos);

	//查找pos的前一个结点
	LTNode* pre = pos->pre;
	
	//查找pos的后一个结点
	LTNode* next = pos->next;

	//将前一个结点pre与后一个结点next相链接
	pre->next = next;
	next->pre = pre;

	//释放pos结点
	free(pos);
}


//链表长度
int ListSize(LTNode* phead)
{
	//判空
	assert(phead);

	//cur指向当前链表的第一个结点
	LTNode* cur = phead->next;
	
	//用于记录遍历过的结点数
	int size = 0;

	//从第一个结点开始依次向后遍历,直到遍历到头结点
	while (cur != phead)
	{
		++size;
		cur = cur->next;
	}

	return size;
}


//销毁
void ListDestory(LTNode* phead)
{
	//判空
	assert(phead);

	//cur指向当前第一个结点
	LTNode* cur = phead->next;

	while (cur != phead)
	{
		//保存cur的下一个结点
		LTNode* next = cur->next;

		//法一:删除cur
		//ListErase(cur);
		
		//法二:删除cur
		free(cur);

		//更新cur
		cur = next;
	}

	//释放头结点
	free(phead);
}

test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include"List.h"

void test()
{
	LTNode* plist = NULL;

	//初始化
	plist = ListInit();

	//头插
	ListPushFront(plist, 1);
	ListPushFront(plist, 2);
	ListPushFront(plist, 3);
	ListPushFront(plist, 4);
	ListPushFront(plist, 5);
	ListPrint(plist);

	ListDestory(plist);
	ListPrint(plist);
}

int main()
{
	test();

	return 0;
}

三. 顺序表和链表的比较

下面分别从逻辑结构,存储结构,基本操作的角度对顺序表和链表进行比较。

逻辑结构

都属于线性表,都是线性结构。

存储结构

基本操作

对于任何一个数据结构,基本操作基本都能归纳为创销,增删改查。其中改建立在查的基础上。

创建

销毁

增加与删除

查找

用链表还是顺序表

          顺序表          链表
弹性(可扩容)             ×           √
     增,删             ×           √
        改             √           ×

表长难以预估,经常需要增加/删除元素--链表

表长可预估,查询(搜索)操作较多--顺序表

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值