带哨兵位的双向循环链表

一、前言

带头双向循环链表,是我们学过的链表中结构最复杂的一种,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。虽然他结构最复杂,但是他实现起来却又是很简单。至于怎样简单,请看下文。

二、带头双向循环链表的结构

1、哨兵位

哨兵位的优势:我们创建一个结点,里面存取随机数据,并不是有效数据,它永远不会为空,就可以减少空指针带来的麻烦,比如在尾插,头插时,都会存在空指针的问题,我们往往需要考虑这个方面,但是有了哨兵位,我们在敲代码的过程中就少了这方面的烦恼,也会减少我们的出错率。

2、带头双向链表的结构

双向链表在带头单链表的基础上,加了头结点,去存储上一个结点的地址,单链表的结点中只有一个指向其后继的指针,使得单链表要访问某个结点的前驱结点时,只能从头开始遍历。所以双向链表的出现,让我们更加方便去找到一个结点的前驱结点。

3、带头双向循环链表

我们循环无谓是将一条直线首尾相接成为圆,让哨兵位的头结点指向尾结点,让最后一个结点的尾结点指向哨兵位,这样的话,我们在尾插的时候就不用遍历去找最后一个结点了,因为我们哨兵位的头结点就是链表的最后一个结点。这样使尾插变的很简单。


三、带头双向循环链表的实现 

Fir、敲码三件套

Sec、函数实现前的准备

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int ElemType;
typedef struct LTListNode
{
	ElemType val;
	LTNode* next;
	LTNode* prev; 
}LTNode;
LTNode* CreatNewNode(ElemType x);
LTNode* LTNInit();
void LTNPrint(LTNode* phead);
void LTNPushBack(LTNode* phead,ElemType x);
void LTNPopBack(LTNode*phead).

Thr、函数实现

1、结点创造函数

LTNode* CreatNewNode(ElemType x)
{
	LTNode* newnode = (LTNode *)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->val = x;
	newnode->next = NULL; 
	newnode->prev = NULL;
	return newnode;
}

2、初始化函数

LTNode* LTNInit()
{ 
	LTNode* phead = CreatNewNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

这里初始化时,需要注意,当我们在初始化哨兵位时,我们如果将哨兵位传进这个函数,那么初始化就是对它进行了修改,而形参只是实参的一个临时拷贝,那么本应该用二级指针的,但是因为我们这个带哨兵位的链表在头插,尾插等操作根本不需要用二级指针,因为我们没有改变哨兵位,为了避免二级带来的不必要的麻烦,我们就采取这种return返回类型的方法,就避免使用了二级指针。

3、打印函数

void LTNPrint(LTNode* phead)
{
	assert("phead");
	printf("哨兵位");
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<=>", cur->val);
		cur = cur->next;
	}

}

这里断言的原因是,我们的哨兵位phead是不可能为空的,所以才会进行断言,严格要求它不能为空。先定义一个结点cur去存去链表第一个结点的值phead->next,这里需要注意while函数的判断条件,每打印完一个时,cur都会向后走,指向下一个结点,当cur=phead时,也就是打印完最后一个结点后cur会指向哨兵位,while函数就会停止循环。

4、尾插函数

void LTNPushBack(LTNode* phead,ElemType x)
{
	assert("phead");
	LTNode* tail = phead->prev;
	LTNode*newnode= CreatNewNode(x);
	phead->next = newnode;
	newnode->prev = phead;
	tail->next = newnode;
	newnode->prev = tail;
	
}

刚开始定义一个结构体指针tail去存上最后一个节点,然后创造一个新结点newnode,再将tail、newnode、phead链接起来。

5、尾删函数

void LTNPopBack(LTNode* phead)
{
	assert(phead);
	LTNode* tail = phead->prev;
	LTNode* tailprev = tail->prev;
	free(tail);
	tail = NULL;
	tailprev->next = phead;
	phead->prev = tailprev;
	
}

6、头插函数

void LTNPushFront(LTNode* phead, ElemType x)
{
	assert(phead);
	LTNode* tail = phead->next;
	LTNode*newnode= CreatNewNode(x);
	newnode->next = tail;
	tail->prev = newnode;
	phead->next = newnode;
	newnode->prev = phead;
}

7、头删函数

void LTNPopFront(LTNode* phead)
{
	assert(phead);
    assert(phead->next!=NULL);
	LTNode* tail = phead->next;
	LTNode* tailnext = tail->next;
	phead->next = tailnext;
	tailnext->prev = phead;
	free(tail);
	tail = NULL;

}

这里需要注意的是,我们哨兵位指向的第一个结点不能为空,如果为空,我们定义的指针都会指向哨兵位phead,最后就会把phead销毁掉,出现野指针的问题,所以此处第一个结点不能为空。

8、查找函数

LTNode* LTNFind(LTNode* phead, ElemType x)
{
	assert(phead);
	LTNode* tail = phead->next;
	while (tail != phead)
	{
		if (tail->val == x)
		{
			return tail;
		}
		tail = tail->next;
	}
	return NULL;
}

9、在pos位置前插入

void LTNInsert(LTNode* pos, ElemType x)
{
	assert(pos);
	LTNode* posprev = pos->prev;
	LTNode* newnode = CreatNewNode(x);
	pos->prev = newnode;
	newnode->next = pos;
	newnode->prev = newnode;
	posprev->next = newnode;

}

10、删除任意结点

void LTNErase(LTNode* pos)
{
	assert(pos);
	LTNode* posnext = pos->next;
	LTNode* posprev = pos->prev;
	posprev->next = posnext;
	posnext->prev = posprev;
	free(pos);

}

这里需要注意的是,我们这里free后并没有对pos进行置空,因为这里对其置空没什么意义,你只是对形参进行置空,外面的实参还在,所以你要用二级,但是为了保证一致性,我们可以在主函数用完之后手动对pos进行置空。

11、销毁函数

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

}

这里也是,我们最后哨兵位没有置空,如果要置空,也可以用二级指针,或者手动置空。

四、优化

1、尾插优化

我们在实现任意插入删除任意结点这两个函数时,如果我们想在哨兵位前(phead->prev)插入一个元素,其实就是在链表末尾插入一个元素,也就是尾插,所以尾插可以用LTNInsert函数来实现:

void LTNPushBack(LTNode* phead,ElemType x)
{
	assert("phead");
	LTNInsert(phead->prev, x);
	
}

2、头插优化

同样,在phead后(phead->next)插入一个元素,也就是头插,同样可以用LTNInsert函数来实现头插功能:

void LTNPushFront(LTNode* phead, ElemType x)
{
	assert(phead);
	LTNInsert(phead->next, x);
}

3、尾删优化

我们在任意删除的时候,删除phead前(phead->prev)的那个元素,这个元素就是链表末尾元素,删除它也就是尾删:

void LTNPopBack(LTNode* phead)
{
	assert(phead);
	LTNErase(phead->prev);
}

4、头删优化

同样,我们删除phead后(phead->next)一个元素,也就是头删:

void LTNPopFront(LTNode* phead)
{
	assert(phead);
	LTNErase(phead->next);
}

所以,当我们实现了LTNInsert与LTNErase后,就可以很快的实现出头插、头删、尾插、尾删函数了

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值