带头双向循环链表
特点:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
了解带头双向循环链表后,我们开始实现这个链表的基本操作了,首先我们准备好三个文件:
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;
ListNode* ListInit()
{
//定义了一个带哨兵卫的头节点,哨兵卫的结点不存储有效数据
ListNode* phead = (ListNode*)malloc(sizeof(ListNode));
//头节点的next指向自己,头节点的前一个也指向自己
phead->next = phead;
phead->prev = phead;
return phead;
}
void ListPrint(ListNode* phead)
{
assert(phead);
ListNode* cur = phead->next;
//cur指向phead时就结束,如果不结束会导致死循环,因为这个链表是循环链表,头节点指向尾结点,尾结点指向头节点
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
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;
}
每次插入数据都需要开辟新节点,为了方便使用,我们定义了一个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;
}
开辟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);
}
先保存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);
}
方法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);
}
先把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);
}
先把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;
}
先把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;
}
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;
}
List.h文件
#pragma once
#include
#include
#include
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.支持随机访问
(用下标访问),需要随机访问结构支持算法可以很好的适用;
2.cpu高速缓存利用率更高;
(下面有简单的讲解)
1.头部和中部插入删除时间效率低,时间复杂度:O(N);
2.连续的物理空间,空间不够需要扩容
a.增容有一定的程度消耗
b.为了避免频繁增容,一般我们都按倍数去增,如果用不完则存在一定的空间浪费
1.任意位置插入删除效率高 O(1);
2.按需申请释放空间;
1.不支持随机访问(不支持使用下标访问),意味着一些排序,二分查找在这种结构上不适用;
2.链表存储一个值,同时要存储链接指针,也有一定的消耗;
3.cpu高速缓存命中率更低;
在内存中,数组是一块连续的物理空间,而指针是一块非连续的物理空间;当CPU执行指令,分别遍历顺序表和链表时,对于CPU来说,它是不会一个字节一个字节的加载的,因为这非常没有效率,一般来说都是要一块一块的加载的CPU的周围有三个高速缓冲存储器和一个寄存器,缓存基本上来说就是把后面的数据加载到离自己近的地方。
假设CPU一次取100个字节,那么在遍历顺序表时,第一次不命中,第二次就可以把顺序表中的数据全部拿到,也就是第二次全部命中。
在访问链表中的数据时,链表中的地址不是一块连续的空间,第一次不命中,所以在访问第2,3,4,5次时很可能全部都不命中,而且还会带来缓存的污染。
缓存污染:在访问链表中的数据时,如果一直没有访问到指定数据,会有数据一直被加载到缓存中去,缓存中的大小是有限的,假设你一次加载了100个字节进去,但是实际上只有20个字节是你要的数据,剩下的80个字节是谁的也不知道,也不会被访问,还会把缓存的空间给占用了,这就叫缓存污染。