【数据结构】第五站:带头双向循环链表

简介: 【数据结构】第五站:带头双向循环链表



一、链表的八种结构

我们已经知道链表可以有以下三种分法

而这三种结构又可以排列组合,形成八种结构

其中我们最常见的就是无头单向非循环链表和带头双向循环链表

1.无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。

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

二、带头双向循环链表的实现

1.链表的定义

对于这个链表,我们需要使用两个指针来控制,一个是前驱指针,一个是后继指针

typedef int LTDateType;
typedef struct ListNode
{
  struct ListNode* prev;
  struct ListNode* next;
  LTDateType data;
}ListNode;

2.链表的接口定义

如下代码所示,是我们需要实现的接口

//链表的初始化
ListNode* ListCreat();
//链表的打印
void ListPrint(ListNode* phead);
//链表的尾插
void ListPushBack(ListNode* phead, LTDateType x);
//链表的头插
void ListPushFront(ListNode* phead, LTDateType x);
//链表是否为空
bool ListEmpty(ListNode* phead);
//链表的尾删
void ListPopBack(ListNode* phead);
//链表的头删
void ListPopFront(ListNode* phead);
//链表的查找
ListNode* ListFind(ListNode* phead, LTDateType x);
//链表在pos前面的插入
void ListInsert(ListNode* pos, LTDateType x);
//链表在pos位置处的删除
void ListErase(ListNode* pos);
//链表的销毁
void ListDestroy(ListNode* phead);

3.接口的具体实现

1.链表的初始化

ListNode* BuyListNode(LTDateType x)
{
  ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
  if (newnode == NULL)
  {
    perror("malloc");
    return NULL;
  }
  newnode->data = x;
  newnode->next = NULL;
  newnode->prev = NULL;
  return newnode;
}
//链表的初始化
ListNode* ListCreat()
{
  ListNode* phead = BuyListNode(-1);
  phead->next = phead;
  phead->prev = phead;
  return phead;
}

如上代码所示,对于这个初始化,他与单链表不同,单链表其实是没有必要去专门做一个初始化函数的。但同时我们需要传二级指针来解决一些问题

对于双向带头循环链表,我们就有必要设置一个初始化函数了。

我们有两种思路去构建这个函数

一种是在主函数中直接定义一个指针,然后通过传这个指针的地址去构建。但是这样我们就需要传二级指针了。

另外一种是我们可以利用返回值,这样我们就直接避免了传参数了。

2.链表的打印

//链表的打印
void ListPrint(ListNode* phead)
{
  assert(phead);
  ListNode* cur = phead->next;
  printf("<=head=>");
  while (cur != phead)
  {
    printf("%d<=>", cur->data);
    cur = cur->next;
  }
  printf("\n");
}

如上代码所示,链表的打印与单链表的打印是一样的,没有什么太大的区别,我们可以利用printf来更加形象的表达双向循环的意思

3.链表的尾插

//链表的尾插
void ListPushBack(ListNode* phead, LTDateType x)
{
  assert(phead);
  ListNode* newnode = BuyListNode(x);
  ListNode* tail = phead->prev;
  tail->next = newnode;
  newnode->prev = tail;
  newnode->next = phead;
  phead->prev = newnode;
}

对于尾插就比单链表简单多了,因为有了头节点,我们就不需要处理是否为空的状态了

4.链表的头插

//链表的头插
void ListPushFront(ListNode* phead, LTDateType x)
{
  assert(phead);
  ListNode* newnode = BuyListNode(x);
  ListNode* first = phead->next;
  phead->next = newnode;
  newnode->prev = phead;
  newnode->next = first;
  first->prev = newnode;
}

头插和尾插基本上是非常类似的,这也得益于这种链表结构基本是无死角的

5.链表的头删尾删以及是否为空

//链表是否为空
bool ListEmpty(ListNode* phead)
{
  assert(phead);
  return phead->next == phead;
}
//链表的尾删
void ListPopBack(ListNode* phead)
{
  assert(phead);
  assert(!ListEmpty(phead));
  ListNode* tail = phead->prev;
  ListNode* tailPrev = tail->prev;
  phead->prev = tailPrev;
  tailPrev->next = phead;
  free(tail);
  tail = NULL;
}
//链表的头删
void ListPopFront(ListNode* phead)
{
  assert(phead);
  assert(!ListEmpty(phead));
  ListNode* first = phead->next;
  ListNode* firstNext = first->next;
  phead->next = firstNext;
  firstNext->prev = phead;
  free(first);
  first = NULL;
}

对于头删和尾删其实也是一致的思路,我们只需要改变结点即可,值得注意的是,我们需要判断链表是否为空, 如果为空肯定不可以删除

6.链表的查找

//链表的查找
ListNode* ListFind(ListNode* phead,LTDateType x)
{
  assert(phead);
  assert(!ListEmpty(phead));
  ListNode* cur = phead->next;
  while (cur != phead)
  {
    if (cur->data == x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return NULL;
}

这个也是比较简单的,他的思路与单链表的是一样的

7.链表在pos位置之前插入

//链表在pos前面的插入
void ListInsert(ListNode* pos, LTDateType x)
{
  assert(pos);
  ListNode* newnode = BuyListNode(x);
  ListNode* prev = pos->prev;
  prev->next = newnode;
  newnode->prev = prev;
  newnode->next = pos;
  pos->prev = newnode;
}

这个的话,我们更能体会到他相对于单链表的优势了。我们只需要pos位置即可。然后思路和头插尾插是一致的

8.链表在pos位置的删除

//链表在pos位置处的删除
void ListErase(ListNode* pos)
{
  assert(pos);
  ListNode* prev = pos->prev;
  ListNode* next = pos->next;
  prev->next = next;
  next->prev = prev;
  free(pos);
  pos = NULL;
}

对于删除,也是与头删尾删思路一致的

9.链表的销毁

//链表的销毁
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;
}

对于链表的销毁思路与单链表也是一致的,只需要循环遍历即可

三、带头双向循环链表的完整代码

List.h

#pragma once
#include<stdio.h>
#include<malloc.h>
#include<assert.h>
#include<stdbool.h>
typedef int LTDateType;
typedef struct ListNode
{
  struct ListNode* prev;
  struct ListNode* next;
  LTDateType data;
}ListNode;
//链表的初始化
ListNode* ListCreat();
//链表的打印
void ListPrint(ListNode* phead);
//链表的尾插
void ListPushBack(ListNode* phead, LTDateType x);
//链表的头插
void ListPushFront(ListNode* phead, LTDateType x);
//链表是否为空
bool ListEmpty(ListNode* phead);
//链表的尾删
void ListPopBack(ListNode* phead);
//链表的头删
void ListPopFront(ListNode* phead);
//链表的查找
ListNode* ListFind(ListNode* phead, LTDateType x);
//链表在pos前面的插入
void ListInsert(ListNode* pos, LTDateType x);
//链表在pos位置处的删除
void ListErase(ListNode* pos);
//链表的销毁
void ListDestroy(ListNode* phead);

List.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
ListNode* BuyListNode(LTDateType x)
{
  ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
  if (newnode == NULL)
  {
    perror("malloc");
    return NULL;
  }
  newnode->data = x;
  newnode->next = NULL;
  newnode->prev = NULL;
  return newnode;
}
//链表的初始化
ListNode* ListCreat()
{
  ListNode* phead = BuyListNode(-1);
  phead->next = phead;
  phead->prev = phead;
  return phead;
}
//链表的打印
void ListPrint(ListNode* phead)
{
  assert(phead);
  ListNode* cur = phead->next;
  printf("<=head=>");
  while (cur != phead)
  {
    printf("%d<=>", cur->data);
    cur = cur->next;
  }
  printf("\n");
}
//链表的尾插
void ListPushBack(ListNode* phead, LTDateType x)
{
  assert(phead);
  ListNode* newnode = BuyListNode(x);
  ListNode* tail = phead->prev;
  tail->next = newnode;
  newnode->prev = tail;
  newnode->next = phead;
  phead->prev = newnode;
}
//链表的头插
void ListPushFront(ListNode* phead, LTDateType x)
{
  assert(phead);
  ListNode* newnode = BuyListNode(x);
  ListNode* first = phead->next;
  phead->next = newnode;
  newnode->prev = phead;
  newnode->next = first;
  first->prev = newnode;
}
//链表是否为空
bool ListEmpty(ListNode* phead)
{
  assert(phead);
  return phead->next == phead;
}
//链表的尾删
void ListPopBack(ListNode* phead)
{
  assert(phead);
  assert(!ListEmpty(phead));
  ListNode* tail = phead->prev;
  ListNode* tailPrev = tail->prev;
  phead->prev = tailPrev;
  tailPrev->next = phead;
  free(tail);
  tail = NULL;
}
//链表的头删
void ListPopFront(ListNode* phead)
{
  assert(phead);
  assert(!ListEmpty(phead));
  ListNode* first = phead->next;
  ListNode* firstNext = first->next;
  phead->next = firstNext;
  firstNext->prev = phead;
  free(first);
  first = NULL;
}
//链表的查找
ListNode* ListFind(ListNode* phead,LTDateType x)
{
  assert(phead);
  assert(!ListEmpty(phead));
  ListNode* cur = phead->next;
  while (cur != phead)
  {
    if (cur->data == x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return NULL;
}
//链表在pos前面的插入
void ListInsert(ListNode* pos, LTDateType x)
{
  assert(pos);
  ListNode* newnode = BuyListNode(x);
  ListNode* prev = pos->prev;
  prev->next = newnode;
  newnode->prev = prev;
  newnode->next = pos;
  pos->prev = newnode;
}
//链表在pos位置处的删除
void ListErase(ListNode* pos)
{
  assert(pos);
  ListNode* prev = pos->prev;
  ListNode* next = pos->next;
  prev->next = next;
  next->prev = prev;
  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

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
void TestList1()
{
  ListNode* plist = ListCreat();
  ListPushBack(plist, 1);
  ListPushBack(plist, 2);
  ListPushBack(plist, 3);
  ListPushBack(plist, 4);
  ListPushBack(plist, 5);
  ListPrint(plist);
  ListPushFront(plist, 6);
  ListPushFront(plist, 7);
  ListPushFront(plist, 8);
  ListPushFront(plist, 9);
  ListPushFront(plist, 10);
  ListPrint(plist);
  ListPopBack(plist);
  ListPopBack(plist);
  ListPopBack(plist);
  ListPopBack(plist);
  ListPrint(plist);
  ListPopFront(plist);
  ListPopFront(plist);
  ListPopFront(plist);
  ListPopFront(plist);
  ListPrint(plist);
}
void TestList2()
{
  ListNode* plist = ListCreat();
  ListPushBack(plist, 1);
  ListPushBack(plist, 2);
  ListPushBack(plist, 3);
  ListPushBack(plist, 4);
  ListPushBack(plist, 5);
  ListPrint(plist);
  ListNode* pos = ListFind(plist, 3);
  (pos->data) *= 10;
  ListPrint(plist);
  ListInsert(pos, 20);
  ListPrint(plist);
  ListErase(pos);
  ListPrint(plist);
}
int main()
{
  //TestList1();
  TestList2();
  return 0;
}

四、顺序表和链表的区别

不同点 顺序表 链表
存储空间上 物理上一定连续 逻辑上连续,但物理上不一定连续
随机访问 支持,O(1) 不支持,O(N)
任意位置插入或删除元素 可能需要搬移元素,效率低,O(N) 只需要改变指针指向
插入 动态顺序表,空间不够时需要扩容 没有容量的概念
应用场景 元素高效存储+频繁访问 任意位置插入和删除频繁
缓存利用率

如上表所示,是顺序表和链表的区别

在这里我们需要注意缓存利用率这个东西。

对于我们的顺序表/链表,想要实现他的数据访问、遍历、修改等

这些数据是由cpu来进行访问的。

也就是这些访问的指令都是cpu执行的。从而访问打印等操作。这时候cpu就需要去访问内存

而cpu其实是不能直接访问内存的,这是因为cpu太快了,内存太慢了

为了解决这个问题,就有了三级缓存,即cpu高速缓存这个东西。

也就是说cpu会先访问缓存,最高级的缓存再次缓存其实就是命中。

在计算机中有一个局部性原理,在访问某个位置的数据的时候,大概率要访问后面的数据。因此他就会将这个数据后面的数据也顺便加载进去。

这时候由于顺序表是连续的,后面的都直接被命中了。所以他的缓存利用率高

而链表他访问某个数据的时候也会加载一长段,但是后面的不一定会用。所以就会浪费,也会导致缓存污染。所以缓存利用率低。也就是他不仅没有起到正向作用,反而起到了反向作用。


本节内容到此位置

如果对你有帮助的话,不要忘记点赞加收藏哦!!!

相关文章
|
2月前
|
存储 算法 Perl
数据结构实验之链表
本实验旨在掌握线性表中元素的前驱、后续概念及链表的建立、插入、删除等算法,并分析时间复杂度,理解链表特点。实验内容包括循环链表应用(约瑟夫回环问题)、删除单链表中重复节点及双向循环链表的设计与实现。通过编程实践,加深对链表数据结构的理解和应用能力。
69 4
|
10天前
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】单链表的基本运算(头歌实践教学平台习题)【合集】
本内容介绍了单链表的基本运算任务,涵盖线性表的基本概念、初始化、销毁、判定是否为空表、求长度、输出、求元素值、按元素值查找、插入和删除数据元素等操作。通过C++代码示例详细解释了顺序表和链表的实现方法,并提供了测试说明、通 - **任务描述**:实现单链表的基本运算。 - **相关知识**:包括线性表的概念、初始化、销毁、判断空表、求长度、输出、求元素值、查找、插入和删除等操作。 - **测试说明**:平台会对你编写的代码进行测试,提供测试输入和预期输出。 - **通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了测试通过后的预期输出结果。 开始你的任务吧,祝你成功!
29 5
|
24天前
|
数据库
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
|
2月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
87 5
|
2月前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
137 4
|
2月前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
2月前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
2月前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
61 0
|
3月前
|
存储 Java
数据结构第三篇【链表的相关知识点一及在线OJ习题】
数据结构第三篇【链表的相关知识点一及在线OJ习题】
38 7
|
3月前
|
存储 安全 Java
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
31 3

热门文章

最新文章