带头双向循环链表

一、带头双向链表的基本实现

1.前期准备

带头双向循环链表

在这里插入图片描述

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

了解带头双向循环链表后,我们开始实现这个链表的基本操作了,首先我们准备好三个文件:
1.List.h :用来包含头文件和函数的声明
2.List.c :用来对函数的定义
3.test.c :用来实现函数,整体的逻辑

在List.h文件中,定义一个双向带头循环链表的结构:

typedef int LTDataType;


typedef struct ListNode
{
	LTDataType data; //数据
	struct ListNode* next; //指向下一个结点
	struct ListNode* prev; //指向前一个结点
}ListNode;
2.初始化链表
ListNode* ListInit()
{
	//定义了一个带哨兵卫的头节点,哨兵卫的结点不存储有效数据
	ListNode* phead = (ListNode*)malloc(sizeof(ListNode));
	//头节点的next指向自己,头节点的前一个也指向自己
	phead->next = phead;
	phead->prev = phead;

	return phead;
}
3.打印链表
void ListPrint(ListNode* phead)
{
	assert(phead);

	ListNode* cur = phead->next;
	//cur指向phead时就结束,如果不结束会导致死循环,因为这个链表是循环链表,头节点指向尾结点,尾结点指向头节点
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}
4.销毁链表
void ListDestroy(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		//把cur指向的下一个结点保存起来,然后销毁cur,再让cur指向next,直到cur指向phead结束
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL;
}
5.插入新结点

每次插入数据都需要开辟新节点,为了方便使用,我们定义了一个BuyListNode函数来进行封装。

ListNode* BuyListNode(LTDataType x)
{
	//开辟新节点newnode
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	//让newnode的值为x,然后让newnode的next和prev都指向空
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

6.头插

开辟newnode新结点,先把head的next存到posnext,然后让newnode的next指向posnext,posnext的prev指向newnode,然后让newnode的prev指向head,让head的next指向newnode;这样头插就完成了,这里没有传二级指针的原因是不需要改变结点的内容,我们只需要改变结点的结构就行了。
在这里插入图片描述

void ListPushFront(ListNode* phead, LTDataType x)
{
	//1.方法:没有定义指针(定义指着后更理解)
	//assert(phead); 

	//ListNode* newnode = BuyListNode(x);

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


	//2.方法:定义指针存放结点
	//assert(phead);
	//ListNode* newnode = BuyListNode(x);
	//ListNode* next = phead->next;

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

	//3.复用ListInsert
	ListInsert(phead->next, x);

}
7.头删

先保存head的next到next指针里面(为方便后面的销毁),然后再把next的next的结点保存到nextNext结点中,然后就可以修改指针的结构了,head的next指向nextNext,nextNext的prev指向head; 最后销毁next结点。
在这里插入图片描述

void ListPopFront(ListNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	//ListNode* next = phead->next;
	//ListNode* nextNext = next->next;
	//phead->next = nextNext;
	//nextNext->prev = phead;
	//free(next);	

	//3.对ListErase的复用
	ListErase(phead->next);
}
8.尾插

方法1的逻辑:
先用tail指针把head的prev保存起来(就是尾结点),然后开辟新节点,让tail的next指向newnode,newnode的prev指向tail,然后让newnode的next指向head,head的prev指向newnode;

void ListPushBack(ListNode* phead, LTDataType x)
{
	//1.方法
	//assert(phead);

	//ListNode* tail = phead->prev;
	//ListNode* newnode = BuyListNode(x);

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

	//2.为啥尾插是phead,而尾删是phead->prev?首先我们要理解进行尾插时,我们写的插入是在pos的前一个位置插入(画图理解)
	ListInsert(phead, x);

}

复用ListInsert的逻辑:
在这里插入图片描述

9.尾删

先把head的prev保存到tail指针中(方便后面释放),然后把tail的prev保存到tailprev中,再改变结点的结构,让tailprev的next指向head,head的prev指向tailprev;最后释放tail。
在这里插入图片描述

void ListPopBack(ListNode* phead)
{
	assert(phead);
	//表示链表为空,不能删除
	assert(phead->next != phead);

	//1.方法
	//ListNode* tail = phead->prev;
	//ListNode* tailprev = tail->prev;
	//tailprev->next = phead;
	//phead->prev = tailprev;
	//free(tail);

	//2.复用ListErase
	ListErase(phead->prev);

}
10.任意位置前插入

先把pos的prev保存到posprev中,然后让posprev的next指向newnode,newnode的prev指向posprev,在把newnode的next指向pos,pos的prev指向newnode;

在这里插入图片描述

//pos位置之前插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* posprev = pos->prev;
	ListNode* newnode = BuyListNode(x);

	posprev->next = newnode;
	newnode->prev = posprev;
	newnode->next = pos;
	pos->prev = newnode;
}
11.任意位置删除

先把pos的next位置保存到posnext中,把pos的prev保存到posprev中,然后让posprev的next指向posnext,posnext的prev指向posprev;最后释放pos位置。
在这里插入图片描述

void ListErase(ListNode* pos)
{
	assert(pos);
	
	ListNode* posnext = pos->next;
	ListNode* posprev = pos->prev;

	posprev->next = posnext;
	posnext->prev = posprev;
	free(pos);
	pos = NULL;
}
12.查找元素
ListNode* ListFind(ListNode* phead, LTDataType x)
{
	assert(phead);

	ListNode* cur = phead->next;
	//cur指向phead时就结束,如果不结束会导致死循环,因为这个链表是循环链表,头节点指向尾结点,尾结点指向头节点
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}
13.整体代码的实现

List.h文件

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

typedef int LTDataType;


typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

ListNode* ListInit();

void ListPushBack(ListNode* phead, LTDataType x);

void ListPopBack(ListNode* phead);

void ListPushFront(ListNode* phead, LTDataType x);

void ListPopFront(ListNode* phead);

void ListPrint(ListNode* phead);

ListNode* BuyListNode(LTDataType x);

ListNode* ListFind(ListNode* phead, LTDataType x);

void ListInsert(ListNode* pos, LTDataType x);

void ListErase(ListNode* pos);

void ListDestroy(ListNode* phead);

List.c文件

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"


ListNode* ListInit()
{
	ListNode* phead = (ListNode*)malloc(sizeof(ListNode));
	phead->next = phead;
	phead->prev = phead;

	return phead;
}


ListNode* BuyListNode(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

void ListPushBack(ListNode* phead, LTDataType x)
{
	//1.
	//assert(phead);

	//ListNode* tail = phead->prev;
	//ListNode* newnode = BuyListNode(x);

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

	//2.为啥尾插是phead,而尾删是phead->prev?首先我们要理解进行尾插时,我们写的插入是在pos的前一个位置插入,(画图理解)
	ListInsert(phead, x);

}

void ListPopBack(ListNode* phead)
{
	assert(phead);
	表示链表为空,不能删除
	assert(phead->next != phead);

	//1.
	//ListNode* tail = phead->prev;
	//ListNode* tailprev = tail->prev;
	//tailprev->next = phead;
	//phead->prev = tailprev;
	//free(tail);

	//2.
	ListErase(phead->prev);

}

void ListPushFront(ListNode* phead, LTDataType x)
{
	//1.
	//assert(phead); 

	//ListNode* newnode = BuyListNode(x);

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


	//2.
	//assert(phead);
	//ListNode* newnode = BuyListNode(x);
	//ListNode* next = phead->next;

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

	//3.
	ListInsert(phead->next, x);

}
void ListPopFront(ListNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	//ListNode* next = phead->next;
	//ListNode* nextNext = next->next;
	//phead->next = nextNext;
	//nextNext->prev = phead;
	//free(next);	

	//3.
	ListErase(phead->next);
}
 
void ListPrint(ListNode* phead)
{
	assert(phead);

	ListNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}


ListNode* ListFind(ListNode* phead, LTDataType x)
{
	assert(phead);

	ListNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}


//pos位置之前插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* posprev = pos->prev;
	ListNode* newnode = BuyListNode(x);

	posprev->next = newnode;
	newnode->prev = posprev;
	newnode->next = pos;
	pos->prev = newnode;
}


//删除pos位置
void ListErase(ListNode* pos)
{
	assert(pos);
	
	ListNode* posnext = pos->next;
	ListNode* posprev = pos->prev;

	posprev->next = posnext;
	posnext->prev = posprev;
	free(pos);
	pos = NULL;
}


void ListDestroy(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL;
}

test.c文件

#include"List.h"

void menu()
{
	printf("*******************************************\n");
	printf("****** 1.头插          2.头删         ******\n");
	printf("****** 3.尾插          4.尾删         ******\n");
	printf("****** 5.任意位置前插入 6.任意位置删除   ******\n");
	printf("****** 7.打印          8.修改         ******\n");
	printf("****** 0.退出                        ******\n");
	printf("*******************************************\n");
}


int main()
{
	ListNode* plist = ListInit();
	int option = -1;
	while (option)
	{
		menu();
		printf("请输入你要执行的选项: ");
		scanf("%d", &option);
		
		if (option == 1)
		{
			int data = 0;
			printf("请输入你要头插的数字:");
			scanf("%d", &data);
			ListPushFront(plist, data);
			ListPrint(plist);
		}
		else if (option == 2)
		{
			int data = 0;
			printf("请输入你要头删的数字:");
			scanf("%d", &data);
			ListPopFront(plist);
			ListPrint(plist);
		}
		else if (option == 3)
		{
			int data = 0;
			printf("请输入你要尾插的数字:");
			scanf("%d", &data);
			ListPushBack(plist, data);
			ListPrint(plist);
		}
		else if (option == 4)
		{
			int data = 0;
			printf("请输入你要尾删的数字:");
			scanf("%d", &data);
			ListPopBack(plist);
			ListPrint(plist);
		}
		else if (option == 5)
		{
			int data = 0;
			int n = 0;
			printf("请输入你要插入的位置:");
			scanf("%d", &n);
			ListNode* pos = ListFind(plist, n);
			if (pos)
			{
				printf("请输入你要在指定位置插入的数字:");
				scanf("%d", &data);
				ListInsert(pos, data);
				ListPrint(plist);
			}

		}
		else if (option == 6)
		{
			int data = 0;
			printf("请输入你要在pos位置删除的数字:");
			scanf("%d", &data);
			ListNode* pos = ListFind(plist, data);
			if (pos)
			{
				ListErase(pos);
				ListPrint(plist);
			}
			
		}
		else if (option == 7)
		{
			ListPrint(plist);
		}
		else if (option == 8)
		{
			printf("请输入你要修改的元素:");
			int data = 0;
			scanf("%d", &data);
			ListNode* pos = ListFind(plist, data);
			if (pos)
			{
				printf("请输入你要修改的值:");
				int val = 0;
				scanf("%d", &val);
				pos->data = val;
				ListPrint(plist);
			}
			else
			{
				printf("元素不存在\n");
			}
		}
		else if (option == 0)
		{
			printf("退出链表");
			ListDestroy(plist);
		}
		else
		{
			printf("无此选项\n");
		}
	}

	return 0;
}

实现结果:
在这里插入图片描述

二、顺序表和链表的区别

(1)顺序表
1.顺序表的优点:

1.支持随机访问(用下标访问),需要随机访问结构支持算法可以很好的适用;

2.cpu高速缓存利用率更高;(下面有简单的讲解)

2.顺序表的缺点:

1.头部和中部插入删除时间效率低,时间复杂度:O(N);

2.连续的物理空间,空间不够需要扩容
a.增容有一定的程度消耗
b.为了避免频繁增容,一般我们都按倍数去增,如果用不完则存在一定的空间浪费

(2)链表(双向带头循环链表)
1.链表的优点:

1.任意位置插入删除效率高 O(1);

2.按需申请释放空间;

2.链表的缺点:

1.不支持随机访问(不支持使用下标访问),意味着一些排序,二分查找在这种结构上不适用;

2.链表存储一个值,同时要存储链接指针,也有一定的消耗;

3.cpu高速缓存命中率更低;

三、缓存命中率

在这里插入图片描述

在内存中,数组是一块连续的物理空间,而指针是一块非连续的物理空间;当CPU执行指令,分别遍历顺序表和链表时,对于CPU来说,它是不会一个字节一个字节的加载的,因为这非常没有效率,一般来说都是要一块一块的加载的CPU的周围有三个高速缓冲存储器和一个寄存器,缓存基本上来说就是把后面的数据加载到离自己近的地方。

假设CPU一次取100个字节,那么在遍历顺序表时,第一次不命中,第二次就可以把顺序表中的数据全部拿到,也就是第二次全部命中。

在访问链表中的数据时,链表中的地址不是一块连续的空间,第一次不命中,所以在访问第2,3,4,5次时很可能全部都不命中,而且还会带来缓存的污染。

缓存污染:在访问链表中的数据时,如果一直没有访问到指定数据,会有数据一直被加载到缓存中去,缓存中的大小是有限的,假设你一次加载了100个字节进去,但是实际上只有20个字节是你要的数据,剩下的80个字节是谁的也不知道,也不会被访问,还会把缓存的空间给占用了,这就叫缓存污染。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小卜~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值