带头双向循环链表的实现

带头双向循环链表的实现

在这里插入图片描述

一、模型构建

在这里插入图片描述
每一个结点都有一个前置指针,前置指针存上一个结点的地址,一个后置指针存下一个结点的地址,还有一个存储数据的变量。尾的下一个就是头,头的上一个就是尾。
并且首尾相连还带有哨兵位头结点。
充分体现出了双向带头循环。
如果要是链表为空,那应该是什么样子的呢?
在这里插入图片描述
phead->next = phead, phead->prev = phead.

二、代码实现(接口函数以及测试用例)

①初始化 + Create函数

因为双向带头循环链表无论如何都是有一个哨兵位phead的结点,所以我们初始化的时候不能像创建顺序表和链表一样把指针设置为NULL之后就OK啦,需要用malloc函数去开辟一块空间。

LT* CreateNode(LTDataType x)
{
	LT* newnode = (LT*)malloc(sizeof(LT));
	if (newnode == NULL)
	{
		printf("malloc");
		exit(-1);
	}
	newnode->x = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}
LT* LTIni()
{
	LT* newnode = CreateNode(-1);
	LT* phead = newnode;
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

②尾插

void LTPushBack(LT* phead, LTDataType x)
{
	assert(phead);
	LT* tail = phead->prev;
	LT* newnode = CreateNode(x);
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

找尾很方便,直接就是phead->prev

③尾删

void LTPopBack(LT* phead)
{
	assert(phead);
	assert(phead->next != phead);
	LT* tail = phead->prev;
	LT* newtail = tail->prev;
	newtail->next = phead;
	phead->prev = newtail;
	free(tail);
	tail = NULL;
}

④头插

void LTPushFront(LT* phead, LTDataType x)
{
	assert(phead);
	LT* newnode = CreateNode(x);
	LT* tmp = phead->next;
	phead->next = newnode;
	newnode->prev = phead;
	newnode->next = tmp;
	tmp->prev = newnode;
}

⑤头删

void LTPopFront(LT* phead)
{
	assert(phead);
	assert(phead->next != phead);
	LT* node = phead->next;
	phead->next = node->next;
	node->next->prev = phead;
	free(node);
	node = NULL;
}

⑥查找

LT* LTFind(LT* phead, LTDataType x)
{
	assert(phead);
	LT* cur = phead->next;
	while (cur != phead)
	{
		if (cur->x == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

⑦在pos位置前插入

void LTInsert(LTDataType x, LT* pos)//在pos位置前插入一个结点
{
	assert(pos);
	LT* tmp = pos->prev;
	LT* newnode = CreateNode(x);
	tmp->next = newnode;
	newnode->prev = tmp;
	newnode->next = pos;
	pos->prev = newnode;
}

大家仔细观察一下如果pos = phead是一个什么情况呢?
我画图为大家解释一下:
在这里插入图片描述

由此可见,如果要是pos = phead,那插入pos位之前就相当于是尾插,
而如果想实现头插,我们也只需把pos位设置成phead->next就可以啦。

新尾插
void LTPushBack(LT* phead, LTDataType x)
{
	assert(phead);
	LTInsert(x, phead);
}
新头插
void LTPopBack(LT* phead LTDataType x)
{
	assert(phead);
	LTInsert(x, phead->next);	
}

⑧删除pos位

void LTErase1(LT* pos)//删除pos位置前的结点
{
	assert(pos);
	LT* tmp = pos->prev;
	tmp->prev->next = pos;
	pos->prev = tmp->prev;
	free(tmp);
	tmp = NULL;
}

void LTErase2(LT* pos)
{
	assert(pos);
	LT* tmp1 = pos->next;
	LT* tmp2 = pos->prev;
	tmp2->next = tmp1;
	tmp1->prev = tmp2;
	free(pos);
	//pos = NULL;
}

LTErase1函数其实并不是很重要,让我们一起来看LTErase2函数,最后这步pos是否设置NULL,其实影响都不是很大,因为形参的改变是不会影响实参的,所以这个时候其实实际的pos已经成为了一个野指针了,我们就需要在工程里使用他的地方后面自己设置成NULL。
例如:
在这里插入图片描述
但是在此处个人认为在这个函数里是不是加上这句断言会更好一些呀

assert(pos != phead)
新尾删
void LTPopBack(LT* phead)
{
	assert(phead);
	assert(phead->next != phead);
	LTErase2(phead->prev);
}
新头删
void PopFront(LT* phead)
{
	assert(phead);
	assert(phead->next);
	LTErase2(phead->next);
}

⑨销毁链表

void LTDestroy(LT* phead)
{
	assert(phead);
	LT* cur = phead->next;
	while (cur != phead)
	{
		LT* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	//phead = NULL;//外面自己置空,形参的改变不影响实参。
}

销毁和删除一样,动态开辟的内存的指针都需要在外面重新置空

⑩打印链表

void LTPrint(LT* phead)
{
	assert(phead);
	LT* cur = phead->next;
	printf("<=>哨兵位<=>");
	while (cur != phead)
	{
		printf("%d<=>", cur->x);
		cur = cur->next;
	}
	printf("\n");
}

⑪测试用例

#define _CRT_SECURE_NO_WARNINGS
#include "List.h"

void test1()
{
	LT* listnode = LTIni();
	LTPushBack(listnode, 1);
	LTPushBack(listnode, 2);
	LTPushBack(listnode, 3);
	LTPushBack(listnode, 4);
	LTPushBack(listnode, 5);
	LTPrint(listnode);
	LTPopBack(listnode);
	LTPopBack(listnode);
	LTPopBack(listnode);
	LTPopBack(listnode);
	LTPopBack(listnode);
	LTPrint(listnode);
	LTPushFront(listnode, 1);
	LTPushFront(listnode, 2);
	LTPushFront(listnode, 3);
	LTPushFront(listnode, 4);
	LTPushFront(listnode, 5);
	LTPrint(listnode);
	LTPopFront(listnode);
	LTPopFront(listnode);
	LTPopFront(listnode);
	LTPopFront(listnode);
	LTPopFront(listnode);
	LTPrint(listnode);
}

void test2()
{
	LT* listnode = LTIni();
	LTPushBack(listnode, 1);
	LTPushBack(listnode, 2);
	LTPushBack(listnode, 3);
	LTPushBack(listnode, 4);
	LTPushBack(listnode, 5);
	LTPrint(listnode);
	LT* findnode = LTFind(listnode, 5);
	LTInsert(100, findnode);
	LTInsert(100, listnode);
	LTPrint(listnode);
	LTErase1(listnode);
	listnode = NULL;
	LTErase1(findnode);
	findnode = NULL;
	LTPrint(listnode);
	LTErase2(findnode);
	findnode = NULL;
	LTPrint(listnode);
}

void test3()
{
	LT* listnode = LTIni();
	LTPushBack(listnode, 1);
	LTPushBack(listnode, 2);
	LTPushBack(listnode, 3);
	LTPushBack(listnode, 4);
	LTPushBack(listnode, 5);
	//LTErase2(listnode);
	//listnode = NULL;//这样已经避免了不能传入哨兵位。
	LTPrint(listnode);
	LTDestroy(listnode);
	listnode = NULL;
}

int main()
{
	//test1();
	//test2();
	test3();
	return 0;
}

三、链表和顺序表的对比

顺序表链表(主要是双向带头循环链表)
支持下标随机访问, 便于排序下标随机访问不方便,不便于随机排序
空间连续,便于缓存命中
头部,尾部插删效率低,需要挪动数据在知道结点地址的情况下,任意位置的插删都是O(1)
空间不够需要扩容,扩容有一定的消耗且有一定的空间浪费按需申请释放,合理利用空间,不存在浪费。
以下是Java实现带头双向循环链表的完整源码,供参考: ``` public class DoublyCircularLinkedList<T> { private Node<T> head; // 头节点 // 节点类 private static class Node<T> { T data; Node<T> prev; Node<T> next; Node(T data) { this.data = data; this.prev = null; this.next = null; } } // 构造函数 public DoublyCircularLinkedList() { head = new Node<>(null); head.prev = head; head.next = head; } // 在链表末尾添加元素 public void add(T data) { Node<T> node = new Node<>(data); node.prev = head.prev; node.next = head; head.prev.next = node; head.prev = node; } // 在指定位置插入元素 public void insert(int index, T data) { Node<T> node = new Node<>(data); Node<T> p = head.next; int i = 0; while (p != head && i < index) { p = p.next; i++; } if (p == head || i > index) { throw new IndexOutOfBoundsException(); } node.prev = p.prev; node.next = p; p.prev.next = node; p.prev = node; } // 删除指定位置的元素 public void remove(int index) { Node<T> p = head.next; int i = 0; while (p != head && i < index) { p = p.next; i++; } if (p == head || i > index) { throw new IndexOutOfBoundsException(); } p.prev.next = p.next; p.next.prev = p.prev; p.prev = null; p.next = null; } // 获取指定位置的元素 public T get(int index) { Node<T> p = head.next; int i = 0; while (p != head && i < index) { p = p.next; i++; } if (p == head || i > index) { throw new IndexOutOfBoundsException(); } return p.data; } // 获取链表长度 public int size() { Node<T> p = head.next; int size = 0; while (p != head) { size++; p = p.next; } return size; } } ``` 该代码实现带头双向循环链表数据结构,支持在链表末尾添加元素、在指定位置插入元素、删除指定位置的元素、获取指定位置的元素、获取链表长度等操作。在算法实现中,通过一个Node类来表示链表中的节点,包含数据域、前驱指针和后继指针。同时,链表的头节点也是一个Node对象,通过头节点来连接链表的首尾,形成双向循环链表
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值