带头双向循环链表

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

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个字节是谁的也不知道,也不会被访问,还会把缓存的空间给占用了,这就叫缓存污染。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

川子767

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

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

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

打赏作者

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

抵扣说明:

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

余额充值