带头双向循环链表的实现

  • 带头双向链表在单链表的基础上,每个结点增加了指向上一个结点的指针,这样在任意位置插入删除时,不需要去遍历链表,找上一个结点,增加了效率
  • 同时由于它有一个带哨兵位的头节点,不需要为头指针的改变而单独判断

带头双向链表的结构:

结构定义:

typedef int DListDataType;

typedef struct DListNode
{
	DListDataType val;
	struct DListNode* next;//下一个结点的指针
	struct DListNode* prev;//上一个结点的指针
}DListNode;

大致接口:

//打印
void DListPrint(DListNode* phead);

//初始化
DListNode* DListInit();

//销毁
void DListDestroy(DListNode* phead);

//头插
void DListPushFront(DListNode* phead, DListDataType x);

//尾插
void DListPushBack(DListNode* phead, DListDataType x);

//头删
void DListPopFront(DListNode* phead);

//尾删
void DListPopBack(DListNode* phead);

//查找
DListNode* DListFind(DListNode* phead, DListDataType x);

//pos位置前插入
void DListInsert(DListNode* pos, DListDataType x);

//pos位置删除
void DListErase(DListNode* pos);

  • 在初始化中,我们需要建立一个不存储有效数据的头节点,该结点的next和prev指针都指向自己,并返回头节点

后面的插入数据都需要建立新结点,那么干脆将建立新结点分装成一个函数,返回新结点的地址

DListNode* CreateNewNode(DListDataType x)
{
	DListNode* newNode = (DListNode*)malloc(sizeof(DListNode));
	if (newNode == NULL)
	{
		perroe("CreateNewNode:malloc fail");
		exit(-1);
	}

	newNode->val = x;
	newNode->next = NULL;
	newNode->prev = NULL;

	return newNode;
}
//初始化
DListNode* DListInit()
{
	DListNode* phead = CreateNewNode(-1);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

头插:

  • 哨兵位节点的下一个结点才是头结点
//头插
void DListPushFront(DListNode* phead, DListDataType x)
{
	assert(phead);

	DListNode* newNode = CreateNewNode(x);

	DListNode* head = phead->next;
	newNode->next = head;
	head->prev = newNode;
	phead->next = newNode;
	newNode->prev = phead;
}

可以看到,该代码跟单链表的头插相比,简洁了很多,并且该代码适用于空结点的情况

打印函数:

//打印
void DListPrint(DListNode* phead)
{
	assert(phead);

	DListNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<=>", cur->val);
		cur = cur->next;
	}
}
  • 注意:此时cur停止的条件不再是空了,而是等于哨兵位结点


尾插:

//尾插
void DListPushBack(DListNode* phead, DListDataType x)
{
	assert(phead);

	DListNode* newNode = CreateNewNode(x);

	DListNode* tail = phead->prev;
	tail->next = newNode;
	newNode->prev = tail;
	newNode->next = phead;
	phead->prev = newNode;
}

该代码同样适用于空结点的情况


头删:

  • 空结点时,不能进行删除,哨兵位结点的next指向自己时表示空结点
//头删
void DListPopFront(DListNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	DListNode* cur = phead->next;
	phead->next = cur->next;
	cur->next->prev = phead;
	free(cur);
}

尾删:

  • 为空结点时不能删除
//尾删
void DListPopBack(DListNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	DListNode* tail = phead->prev;
	tail->prev->next = phead;
	phead->prev = tail->prev;
	free(tail);
}

查找:

  • 如果找到了返回结点的地址,找不到返回空
  • cur结束的条件是等于哨兵位结点
//查找
DListNode* DListFind(DListNode* phead, DListDataType x)
{
	assert(phead);

	DListNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->val == x)
			return cur;
		cur = cur->next;
	}

	return NULL;
}

在pos前插入:

//pos位置前插入
void DListInsert(DListNode* pos, DListDataType x)
{
	assert(pos);

	DListNode* newNode = CreateNewNode(x);

	pos->prev->next = newNode;
	newNode->prev = pos->prev;
	newNode->next = pos;
	pos->prev = newNode;
}
  • 如果pos等于哨兵位结点,相当于尾插
  • 如果pos等于哨兵位结点的下一个结点,相当于头插

因此,前面的头插和尾插函数可以用该函数替换

//头插
void DListPushFront(DListNode* phead, DListDataType x)
{
	assert(phead);

	DListInsert(phead->next, x);
}

//尾插
void DListPushBack(DListNode* phead, DListDataType x)
{
	assert(phead);

	DListInsert(phead, x);
}

删除pos位置:

//pos位置删除
void DListErase(DListNode* pos)
{
	assert(pos);

	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
}
  • 如果pos为尾指针,相当于尾删
  • 如果pos为哨兵位结点的下一个结点,相当于头删

因此,前面的头删和尾删可以用该函数替换

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

	DListErase(phead->next);
}

//尾删
void DListPopBack(DListNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	DListErase(phead->prev);

}

未来面试官可以让你在10分钟内写出一个双向循环链表,你会想:怎么可能在10分钟内写出一个双向循环链表出来呢?

实际上,我们只要实现pos位置前插入,pos位置删除两个接口,其他的接口就游刃而解了


还有最后的销毁:

  • 先释放掉链表结点,最后释放掉哨兵位结点
  • 由于哨兵位结点已经释放掉,在销毁函数中应当将指针置空,但因为函数的参数是一级指针,即使置空也不会影响到外面的指针,所以销毁后应当手动置空,避免野指针
//销毁
void DListDestroy(DListNode* phead)
{
	assert(phead);

	DListNode* cur = phead->next;
	while (cur != phead)
	{
		DListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

//测试函数
void TestDList()
{
	DListNode* dl = DListInit();
	DListPushFront(dl, 1);
	DListPushFront(dl, 2);
	DListPushFront(dl, 3);
	DListPrint(dl);

	DListPushBack(dl, 6);
	DListPushBack(dl, 7);
	DListPrint(dl);

	DListPopFront(dl);
	DListPrint(dl);

	DListPopBack(dl);
	DListPrint(dl);

	DListNode* pos = DListFind(dl, 2);
	if (pos != NULL)
		printf("找到了\n");
	else
		printf("找不到\n");

	pos = DListFind(dl, 6);
	DListInsert(pos, 11);
	DListPrint(dl);

	pos = DListFind(dl, 2);
	DListErase(pos);
	DListPrint(dl);

	DListDestroy(dl);
	dl = NULL;
}

顺序表和链表的对比:

顺序表的优势:

  1. 支持下标随机访问,便于排序数据
  2. 内存命中率高(额外的知识)

顺序表的劣势:

  1. 只适合头插和尾插;在其他位置插入删除都要挪动数据,效率低
  2. 每次都是2倍扩容,而实际上我们不确定要存多少数据,可能会存在空间的浪费

链表(双向循环)的优势:

  1. 在任意位置插入删除时间复杂度都是O(1),效率高
  2. 按需开辟空间,不会造成空间的浪费

链表的劣势:

  1. 不支持下标随机访问,不方便排序数据
  2. 容易产生内存碎片

总的来说,顺序表和链表是互补的关系,没有哪一种数据结构能满足我们所有的需要,只有不同的场景应用不同的数据结构!


最后,有需要本篇文章源码的小伙伴可以去我的Gitee自行查看!DList/DList · baiyahua/LeetCode - 码云 - 开源中国 (gitee.com)

以下是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对象,通过头节点来连接链表的首尾,形成双向循环链表
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值