【数据结构与算法】双向循环链表的实现

简介: 【数据结构与算法】双向循环链表的实现

👉双向循环链表的引入👈


因为单向链表不能实现任意位置的插入和删除,所以今天我们来学习一个能够高效地在任意位置插入和删除数据的结构 —— 带哨兵位的双向循环链表。

1e54b437ef6b484aae544992783c3f53.png

什么是哨兵位?


哨兵位(DummyNode)也是一个节点,但是该节点不存储任何的有效数据。哨兵位的创建方便我我们进行头插数据。如果有了哨兵位的话,头插的时候,我们就不再需要改变头节点了。如果还是不是很理解的话,那么先看下面链表的实现。或许看完,你就能够理解了。


双向循环链表的实现👈


带哨兵位双向循环链表要实现的函数接口有:初始化链表、销毁链表、打印链表、申请节点、尾插数据、头插数据、判断链表是否为空链表、尾删数据、头插数据、链表节点的个数、查找数据、在pos位置之前插入数据和删除pos位置的数据。虽然带哨兵位双向循环链表的结构很复杂,但是其函数接口相当容易事项。来一起学习一下吧!


List.h


#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int LTDataType;
typedef struct ListNode
{
  struct ListNode* next;
  struct ListNode* prev;
  LTDataType data;
}LTNode;
//void ListInit(LTNode** pphead);
// 初始化链表
LTNode* ListInit();
// 销毁链表
void ListDestory(LTNode* phead);
// 打印链表
void ListPrint(LTNode* phead);
// 申请节点
LTNode* BuyListNode(LTDataType x);
// 尾插数据
void ListPushBack(LTNode* phead, LTDataType x);
// 头插数据
void ListPushFront(LTNode* phead, LTDataType x);
// 尾删数据
void ListPopBack(LTNode* phead);
// 头删数据
void ListPopFront(LTNode* phead);
// 判断链表是否为空链表
bool ListEmpty(LTNode* phead);
// 链表节点的个数
size_t ListSize(LTNode* phead);
// 查找数据
LTNode* ListFind(LTNode* phead, LTDataType x);
// 在pos位置之前插入数据
void ListInsert(LTNode* pos, LTDataType x);
// 删除pos位置的数据
void ListErase(LTNode* pos);

Test.c源文件里面是头文件的包含,类型的重命名、结构体的声明以及函数接口的声明。


List.c


List.c源文件负责实现函数接口。注意:因为phead是指向哨兵位的指针,phead不可能为NULL,所以都需要对phead进行断言assert(phead)。


#include "List.h"
// 链表初始化
//void ListInit(LTNode** pphead)
//{
//  assert(pphead);
//  *pphead = (LTNode*)malloc(sizeof(LTNode)); // 哨兵位
//  if(*pphead == NULL)
//  { 
//    perror("malloc fail");
//    exit(-1);
//  }
//  (*pphead)->prev = *pphead;
//  (*pphead)->next = *pphead;
//}
LTNode* ListInit()
{
  LTNode* DummyNode = (LTNode*)malloc(sizeof(LTNode));// 哨兵位
  if (DummyNode == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  DummyNode->prev = DummyNode;
  DummyNode->next = DummyNode;
  return DummyNode;
}
// 可以传二级,内部置空头结点
// 建议:也可以考虑用一级指针,让调用ListDestory的人置空(保持接口一致性)
// 销毁链表
void ListDestory(LTNode* phead)
{
  assert(phead);
  LTNode* cur = phead->next;
  while (cur != phead)
  {
    LTNode* next = cur->next;
    free(cur);
    cur = next;
  }
  free(phead);
  //phead = NULL; // 对形参的修改不会影响实参
}
// 打印链表
void ListPrint(LTNode* phead)
{
  assert(phead);
  LTNode* cur = phead->next;
  printf("phead<=>");
  while (cur != phead)
  {
    if (cur->next != phead)
    {
      printf("%d<=>", cur->data);
    }
    else
    {
      printf("%d<=>phead", cur->data);
    }
    cur = cur->next;
  }
  printf("\n");
}
// 申请节点
LTNode* BuyListNode(LTDataType x)
{
  LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
  if (newnode == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  newnode->data = x;
  newnode->prev = NULL;
  newnode->next = NULL;
  return newnode;
}
// 在pos位置之前插入数据
void ListInsert(LTNode* pos, LTDataType x)
{
  assert(pos);
  LTNode* newnode = BuyListNode(x);
  LTNode* prev = pos->prev;
  // prev newnode pos
  prev->next = newnode;
  newnode->prev = prev;
  newnode->next = pos;
  pos->prev = newnode;
}
// 尾插数据
void ListPushBack(LTNode* phead, LTDataType x)
{
  assert(phead);
  //LTNode* newnode = BuyListNode(x);
  //LTNode* tail = phead->prev;
  // phead ... tail newnode
  //tail->next = newnode;
  //newnode->prev = tail;
  //newnode->next = phead;
  //phead->prev = newnode;
  ListInsert(phead, x); // 调用ListInsert函数完成尾插
}
// 头插数据
void ListPushFront(LTNode* phead, LTDataType x)
{
  assert(phead);
  // 考虑链接顺序
  //LTNode* newnode = BuyListNode(x);
  //newnode->next = phead->next;
  //phead->next->prev = newnode;
  //phead->next = newnode;
  //newnode->prev = phead;
  // 不需要考虑链接顺序
  //LTNode* newnode = BuyListNode(x);
  //LTNode* first = phead->next;
  //phead->next = newnode;
  //newnode->prev = phead;
  //newnode->next = first;
  //first->prev = newnode;
  LTNodeInsert(phead->next, x); // 调用LTNodeInsert函数完成头插
}
// 判断链表是否为空链表
bool ListEmpty(LTNode* phead)
{
  assert(phead);
  return phead == phead->next; // 链表为空时,phead = phead->next
}
// 删除pos位置的数据
void ListErase(LTNode* pos)
{
  assert(pos);
  LTNode* prev = pos->prev;
  LTNode* next = pos->next;
  // prev pos next
  prev->next = next;
  next->prev = prev;
  free(pos);
}
// 尾删数据
void ListPopBack(LTNode* phead)
{
  assert(phead);
  assert(!ListEmpty(phead));
  // phead ... prev tail
  //LTNode* tail = phead->prev;
  //LTNode* prev = tail->prev;
  //prev->next = phead;
  //phead->prev = prev;
  //free(tail);
  //tail = NULL; // tail置不置空都可以,因为对形参的修改不会影响实参
  ListErase(phead->prev); // 调用ListErase函数完成尾删
}
// 头删数据
void ListPopFront(LTNode* phead)
{
  assert(phead);
  assert(!ListEmpty(phead));
  // phead first second
  //LTNode* first = phead->next;
  //LTNode* second = first->next;
  //phead->next = second;
  //second->prev = phead;
  //free(first);
  //first = NULL;
  ListErase(phead->next); // 调用ListErase函数完成头删
}
// 链表节点的个数
size_t ListSize(LTNode* phead)
{
  assert(phead);
  LTNode* cur = phead->next;
  size_t n = 0;
  while (cur != phead)
  {
    n++;
    cur = cur->next;
  }
  return n;
}
// 查找数据
LTNode* ListFind(LTNode* phead, LTDataType x)
{
  assert(phead);
  LTNode* cur = phead->next;
  while (cur != phead)
  {
    if (cur->data == x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return NULL;
}


初始化链表


初始化链表有两种方式,第一种方式是函数的参数为二级指针LTNode** pphead,返回值为void;第二种方式是函数没有参数,返回值为LTNode*。以上两种方式均可实现链表的初始化,个人推荐第二种实现方式。


// 链表初始化
void ListInit(LTNode** pphead)
{
  assert(pphead);
  *pphead = (LTNode*)malloc(sizeof(LTNode)); // 哨兵位
  if(*pphead == NULL)
  { 
    perror("malloc fail");
    exit(-1);
  }
  (*pphead)->prev = *pphead;
  (*pphead)->next = *pphead;
}
LTNode* ListInit()
{
  LTNode* DummyNode = (LTNode*)malloc(sizeof(LTNode));// 哨兵位
  if (DummyNode == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  DummyNode->prev = DummyNode;
  DummyNode->next = DummyNode;
  return DummyNode;
}


打印链表


因为循环链表中的任何一个节点都会指向NULL,所以循环链表的遍历就能想单向链表那样子遍历了。那该如何遍历呢?定义一个指针LTNode* cur = head->next,当cur == head时,循环遍历链表结束。

b843a59056734aebbd04061e8d5ee63d.png


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


销毁链表


销毁链表的函数参数可以是一级指针LTNode*,也可以是二级指针LTNode**。为了保持接口的一致性,本人采用一级指针的方式。如果销毁链表函数的参数为一级指针,则需要调用该函数的人将指向哨兵位的指针置为NULL。


// 可以传二级,内部置空头结点
// 建议:也可以考虑用一级指针,让调用ListDestory的人置空(保持接口一致性)
// 销毁链表
void ListDestory(LTNode* phead)
{
  assert(phead);
  LTNode* cur = phead->next;
  while (cur != phead)
  {
    LTNode* next = cur->next;
    free(cur);
    cur = next;
  }
  free(phead);
  //phead = NULL; // 对形参的修改不会影响实参
}


申请节点


在插入数据时,需要申请新的节点newnode来存储数据。如果申请节点失败,那就直接结束程序,没有必要继续往下执行代码了。如果申请节点成功,那么newnode->data = x, newnode->prev = NULL, newnode->next = NULL,最后将newnode的值返回。


LTNode* BuyListNode(LTDataType x)
{
  LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
  if (newnode == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  newnode->data = x;
  newnode->prev = NULL;
  newnode->next = NULL;
  return newnode;
}


判断链表是否为空链表


当phead == phead->next时,链表为空


// 判断链表是否为空链表
bool ListEmpty(LTNode* phead)
{
  assert(phead);
  return phead == phead->next; // 链表为空时,phead = phead->next
}

在pos位置之前插入数据


1.申请新节点newnode

2.插入数据

3.注意:prev-next为头结点,prev->prev为尾结点。ListInsert(phead->next, x)为头插,ListInsert(phead, x)为尾插


b55ddb9396cf44fbbfdde7f415552b00.png

// 在pos位置之前插入数据
void ListInsert(LTNode* pos, LTDataType x)
{
  assert(pos);
  LTNode* newnode = BuyListNode(x);
  LTNode* prev = pos->prev;
  // prev newnode pos
  prev->next = newnode;
  newnode->prev = prev;
  newnode->next = pos;
  pos->prev = newnode;
}


尾插数据


1.申请新节点newnode

2.插入数据可调用函数ListInsert(phead, x)

58c3a17ab91744e5bcf9dacab65b0757.png

// 尾插数据
void ListPushBack(LTNode* phead, LTDataType x)
{
  assert(phead);
  //LTNode* newnode = BuyListNode(x);
  //LTNode* tail = phead->prev;
  // phead ... tail newnode
  //tail->next = newnode;
  //newnode->prev = tail;
  //newnode->next = phead;
  //phead->prev = newnode;
  ListInsert(phead, x); // 调用ListInsert函数完成尾插
}


头插数据


1.申请新节点newnode

2.插入数据可调用函数ListInsert(phead->next, x)

71b6628335184c77ae25b2ed0a956709.png


// 头插数据
void ListPushFront(LTNode* phead, LTDataType x)
{
  assert(phead);
  // 考虑链接顺序
  //LTNode* newnode = BuyListNode(x);
  //newnode->next = phead->next;
  //phead->next->prev = newnode;
  //phead->next = newnode;
  //newnode->prev = phead;
  // 不需要考虑链接顺序
  //LTNode* newnode = BuyListNode(x);
  //LTNode* first = phead->next;
  //phead->next = newnode;
  //newnode->prev = phead;
  //newnode->next = first;
  //first->prev = newnode;
  LTNodeInsert(phead->next, x); // 调用LTNodeInsert函数完成头插
}


删除pos位置的数据


1.对pos位置进行断言

2.删除数据

3.注意:prev-next为头结点,prev->prev为尾结点。ListErase(phead->next,)为头删,ListErase(phead->prev)为尾删

343e71fe6b27419ca4eb292bdc0c90af.png


// 在pos位置之前插入数据
// 删除pos位置的数据
void ListErase(LTNode* pos)
{
  assert(pos);
  LTNode* prev = pos->prev;
  LTNode* next = pos->next;
  // prev pos next
  prev->next = next;
  next->prev = prev;
  free(pos);
}


尾删数据


1.判断链表是否为空assert(!ListEmpty(phead))

2.删除数据可调用函数ListErase(phead->prev)

9dd8939611ae4c7494bcbde32252bc33.png


// 尾删数据
void ListPopBack(LTNode* phead)
{
  assert(phead);
  assert(!ListEmpty(phead));
  // phead ... prev tail
  //LTNode* tail = phead->prev;
  //LTNode* prev = tail->prev;
  //prev->next = phead;
  //phead->prev = prev;
  //free(tail);
  //tail = NULL; // tail置不置空都可以,因为对形参的修改不会影响实参
  ListErase(phead->prev); // 调用ListErase函数完成尾删
}


头删数据


1.判断链表是否为空assert(!ListEmpty(phead))

2.删除数据可调用函数ListErase(phead->next,)


// 头删数据
void ListPopFront(LTNode* phead)
{
  assert(phead);
  assert(!ListEmpty(phead));
  // phead first second
  //LTNode* first = phead->next;
  //LTNode* second = first->next;
  //phead->next = second;
  //second->prev = phead;
  //free(first);
  //first = NULL;
  ListErase(phead->next); // 调用ListErase函数完成头删
}


链表节点的个数


利用while循环遍历链表,就能算出链表节点的个数。


// 链表节点的个数
size_t ListSize(LTNode* phead)
{
  assert(phead);
  LTNode* cur = phead->next;
  size_t n = 0;
  while (cur != phead)
  {
    n++;
    cur = cur->next;
  }
  return n;
}


查找数据


利用while循环遍历链表,如果有节点的数据等于要查找的数据x,就返回节点的地址cur;如果在链表中找不到x,就返回空指针NULL。


// 查找数据
LTNode* ListFind(LTNode* phead, LTDataType x)
{
  assert(phead);
  LTNode* cur = phead->next;
  while (cur != phead)
  {
    if (cur->data == x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return NULL;
}


Test.c


Test.c源文件负责测试函数接口的功能是否达到我们的预期。以下是本人写的一些测试案例,大家可以参考一下。


#include "List.h"
// 测试尾插、打印链表、销毁链表
void ListTest1()
{
  LTNode* plist = ListInit();
  ListPushBack(plist, 1);
  ListPushBack(plist, 2);
  ListPushBack(plist, 3);
  ListPushBack(plist, 4);
  ListPushBack(plist, 5);
  ListPrint(plist); // pehad<=>1<=>2<=>3<=>4<=>5<=>phead
  ListDestory(plist);
  plist = NULL; // 需要手动置空
}
// 测试头插、尾删、头删
void ListTest2()
{
  LTNode* plist = ListInit();
  ListPushFront(plist, 1);
  ListPushFront(plist, 2);
  ListPushFront(plist, 3);
  ListPushFront(plist, 4);
  ListPushFront(plist, 5);
  ListPrint(plist); // pehad<=>5<=>4<=>3<=>2<=>1<=>phead
  // 头删
  ListPopFront(plist);
  ListPopFront(plist);
  ListPrint(plist); // pehad<=>3<=>2<=>1<=>phead
  // 尾删
  ListPopBack(plist);
  ListPopBack(plist);
  ListPrint(plist); // pehad<=>3<=>phead
  ListDestory(plist);
  plist = NULL;
}
// 测试查找数据、在pos位置之前插入数据、删除pos位置的数据
void ListTest3()
{
  LTNode* plist = ListInit();
  ListPushBack(plist, 1);
  ListPushBack(plist, 2);
  ListPushBack(plist, 3);
  ListPushBack(plist, 4);
  ListPushBack(plist, 5);
  ListPrint(plist); // pehad<=>1<=>2<=>3<=>4<=>5<=>phead
  LTNode* pos = ListFind(plist, 3);
  if (pos != NULL)
  {
    ListInsert(pos, 30);
    ListPrint(plist); // pehad<=>1<=>2<=>30<=>3<=>4<=>5<=>phead
  }
  pos = ListFind(plist, 5);
  if (pos != NULL)
  {
    ListErase(pos);
    ListPrint(plist); // pehad<=>1<=>2<=>30<=>3<=>4<=>phead
  }
  ListDestory(plist);
  plist = NULL;
}
int main()
{
  //ListTest1();
  //ListTest2();
  ListTest3();
  return 0;
}

5cae72b10e2f4bb78f84568c23e37ef9.png


👉顺序表和带哨兵位双向循环链表的对比👈

757d8dc185e84527a3b0fa9cc262d447.png1ca3faf265ec4801bced653116dcff59.png


除了缓存利用率的区别比较难理解,我相信其它的区别都比较好理解。那现在我们就来学习一下上面是缓存利用率。学习这个之前,我们需要了解一下存储器的层次结构。


1acaec743b5d4635aec380ced3756383.png



存储器层次结构还能用以下的图片来形象地表示。


14b3d5ff2dc14f53a347ba073a0b33b1.png


为了了解高速缓存命中率,我们来对比一下顺序表的遍历打印和链表的遍历打印。程序经过编译转换成二进制指令,然后CPU会执行这些二进制指令。执行指令时,就需要相关的数据。而数据是存储在内存中的,并且CPU不会去直接访问内存。那么,CPU怎么拿到内存中的数据呢?这时候,就需要借助三级缓存。


CPU先看一下需要的数据在不在三级缓存中,在(专业术语为命中)就直接访问。如果不命中的话,先将需要的数据从内存加载到三级缓存,然后CPU再访问数据。缓存每次加载数据的多少取决于硬件。如果一个内存位置的数据被加载到三级缓存,那么这个位置的附近位置的数据也会被加载到三级缓存中,这是高速缓存的局部性原理。


因为顺序表的结构在物理上是连续的,而链表的结构在物理上是不一定连续的,所以顺序表的数据一次就能加载到三级缓存中去,而链表的数据需要好几次才能完全加载到三级缓存中。所以,顺序表的高速缓存命中率会比链表的高速缓存利用率高。


f9223133ef6d4d4e973d56d0e9bf4163.png

👉总结👈


有关于链表的内容已经全部讲解结束了,这里有一些经典的链表题,可以点击跳转:链表OJ题1、链表OJ题2、链表OJ题3以及经典的环形链表。以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家啦!💖💝❣️

相关文章
|
存储 算法 Perl
数据结构实验之链表
本实验旨在掌握线性表中元素的前驱、后续概念及链表的建立、插入、删除等算法,并分析时间复杂度,理解链表特点。实验内容包括循环链表应用(约瑟夫回环问题)、删除单链表中重复节点及双向循环链表的设计与实现。通过编程实践,加深对链表数据结构的理解和应用能力。
465 4
|
存储 机器学习/深度学习 算法
C 408—《数据结构》算法题基础篇—链表(下)
408考研——《数据结构》算法题基础篇之链表(下)。
795 30
|
存储 算法 C语言
C 408—《数据结构》算法题基础篇—链表(上)
408考研——《数据结构》算法题基础篇之链表(上)。
1150 25
|
存储 算法 物联网
解析局域网内控制电脑机制:基于 Go 语言链表算法的隐秘通信技术探究
数字化办公与物联网蓬勃发展的时代背景下,局域网内计算机控制已成为提升工作效率、达成设备协同管理的重要途径。无论是企业远程办公时的设备统一调度,还是智能家居系统中多设备间的联动控制,高效的数据传输与管理机制均构成实现局域网内计算机控制功能的核心要素。本文将深入探究 Go 语言中的链表数据结构,剖析其在局域网内计算机控制过程中,如何达成数据的有序存储与高效传输,并通过完整的 Go 语言代码示例展示其应用流程。
381 0
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】单链表的基本运算(头歌实践教学平台习题)【合集】
本内容介绍了单链表的基本运算任务,涵盖线性表的基本概念、初始化、销毁、判定是否为空表、求长度、输出、求元素值、按元素值查找、插入和删除数据元素等操作。通过C++代码示例详细解释了顺序表和链表的实现方法,并提供了测试说明、通 - **任务描述**:实现单链表的基本运算。 - **相关知识**:包括线性表的概念、初始化、销毁、判断空表、求长度、输出、求元素值、查找、插入和删除等操作。 - **测试说明**:平台会对你编写的代码进行测试,提供测试输入和预期输出。 - **通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了测试通过后的预期输出结果。 开始你的任务吧,祝你成功!
853 5
|
数据库
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
712 5
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
1974 4
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法

热门文章

最新文章