数据结构与算法之《带头双向循环链表》详解

简介: 数据结构与算法之《带头双向循环链表》详解

  我们知道,链表实际上结构有很多种。我们大致可分为带头或者不带头单向或者双向循环或者非循环三种大类情况。而每种大类都可与其他类别组合形成新的一种结构,纵沟会有八种情况。a973ceb4c50e4a6388e59b3e28bb41bc.png


每种情况也是大同小异。其中常用的只有两种:无头单向非循环链表带头双向循环链表。想要具体学习了解无头单向非循环链表可参考这篇文章:数据结构与算法之《单链表》详解。本篇文章会详细讲述带头双向循环链表。其实我们把最为常用的两篇文章掌握后,其他的链表结构也就很容易写出来了。



一、带头双向循环链表概念及结构

1、1 带头双向循环链表的概念


 我们学完无头单向非循环链表后,第一次见到带头双向循环链表感觉会很难,结构也很复杂。其实并不是这样的。带头双向循环链表甚至写起来和构思都十分简单。我们接着往下看就可以知道了。


 带头双向循环链表到底是个什么呢?如同正常链表一样,是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。无非结构上会有所差异,那我们来看一下结构带头双向循环链表的结构把。


1、2  带头双向循环链表的结构



 带头双向循环链表顾名思义,链表会有一个头节点(哨兵位),但是这个头节点不存储有效数据。同时,每个节点会有两个指针,一个指向后面的节点,另一个指向前面一个节点。循环的意思是尾结点指向头节点,头节点指向尾节点。如下图所示:


7a0ec890fb184dfebea3c6bd0870d244.png


带头双向循环链表结构看上去很复杂,实现起来却很容易,我们接着往下看带头双向循环链表的实现。

二、带头双向循环链表的思路及代码实现详解

2、1 带头双向循环链表实现思路



我们学习完无头单向非循环链表后,对带头双向循环链表理解上就会很简单。两者的实现思路大同小异。具体到每个模块细节的实现也是几乎相同的。我们先来看单链表整体思路分为多种不同的模块有哪几个:


定义一个结构体,该结构体包含一个可以存放数据的变量、一个能够指向下一个节点的指针和一个能够指向前面一个节点的指针。

初始化结构体。

打印链表数据。

开辟节点。

销毁链表。

判断链表是否为空。

头插。

尾插。

头删。

尾删。

查找节点。

任意位置插入。

任意位置删除。

  以上就是整个带头双向循环链表的不同模块,那我们接下来看一下不同模块思路及代码实现的细节吧。


2、2 带头双向循环链表的模块细节及代码实现

2、2、1 结构体的声明与定义

 上面我们提到,定义结构体时,该结构体包含一个可以存放数据的变量、一个能够指向下一个节点的指针和一个能够指向前面一个节点的指针。那么代码的实现就很简单了。

typedef int LNDataType;
typedef struct DListNode
{
  struct DListNode* prev;
  struct DListNode* next;
  LNDataType data;
}LTNode;



2、2、2 初始化结构体

 初始化结构体的方式有很多种,在这里我们是先定义一个结构体指针为空,在对其进行初始化,这个时候我们要改变结构体指针的话,我们就需要传二级指针。我们在初始化时,此时链表中只有一个头节点,所以我们要头结点的next和prev都指向自己。我们看代码的实现。

LTNode* plist = NULL;
LTInit(&plist);
void LTInit(LTNode** phead)
{
  (*phead) = BuyLTNode(-1);
  (*phead)->next = *phead;
  (*phead)->prev = *phead;
}


2、2、3 打印链表数据

 注意,带头双向循环链表可不是随便的遍历打印哦。带头双向循环链表是一个循环的结构,一但不加操作遍历,就会死循环。我们可以从头节点的下一个位置开始遍历,结束条件就是回到头节点。这样就解决问题了,我们结合代码理解一下:

void LTPrint(LTNode* phead)
{
  assert(phead);
  LTNode* cur = phead->next;
  printf("<-head->");
  while (cur != phead)
  {
    printf("%d<-->", cur->data);
    cur = cur->next;
  }
  printf("\n");
}


2、2、4 开辟节点

 这里为什么要把开辟节点单独里一个函数呢?在我们插入数据之前,不管是头插还是尾插,还是任意插入,我们都需要新开辟一个节点。然后把要插入的数据存储到新节点当中,然后进行连接即可。所以这里我们把开辟节点单独分为一个模块来解释。我们来看一下代码的是实现。  

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


2、2、5 销毁链表

 因为链表是我们动态开辟出来的,所以我们要释放掉,避免空间泄露。销毁链表时,我们不只是释放头节点的空间,每个节点的空间都要释放。  

void LTDestory(LTNode** pphead)
{
  LTNode* cur = (*pphead)->next;
  while (cur != *pphead)
  {
    LTNode* tmp = cur->next;
    free(cur);
    cur = tmp;
  }
  free(*pphead);
  *pphead = NULL;
}



2、2、6 判断链表是否为空

 判断链表为空的情况在我们后面对链表删除节点的时候需要的,所以这里我们单独列出一个函数,判断也较为简单,我们直接看代码。

bool LTEmpty(LTNode* phead)
{
  assert(phead);
  return phead->next == phead;
}


2、2、7 头插

 在带头双向循环链表中,头插我们直接插入就行,并没什么特殊的情况。我们只需要把结构修改到位就行。我们看代码实现。

void LTPushFront(LTNode* phead, LNDataType x)
{
  assert(phead);
  LTNode* newnode = BuyLTNode(x);
  newnode->next = phead->next;
  phead->next->prev = newnode;
  phead->next = newnode;
  newnode->prev = phead;
}



2、2、8 尾插

 尾插同样也很简单。我们可以直接通过头节点phead的prev找到尾节点,然后直接连接截取就行。也无特殊情况。而在无头单向非循环链表中,我们不仅要遍历找尾节点,还要判断特使为空的情况。带头双向循环链表的头节点和双向循环的结构给我们带来了很大的便利。我们直接看代码的实现。

void LTPushBack(LTNode* phead, LNDataType x)
{
  assert(phead);
  LTNode* newnode = BuyLTNode(x);
  LTNode* tail=phead->prev;
  tail->next = newnode;
  newnode->prev = tail;
  newnode->next = phead;
  phead->prev = newnode;
}

2、2、9 头删

 在删除之前,我们应该首先判断链表中是否包含有效的节点(不包含头节点)。然后正常删除链接即可。我们看代码的实现。

void LTPopFront(LTNode* phead)
{
  assert(phead);
  assert(!LTEmpty(phead));
  LTNode* tmp = phead->next;
  phead->next = tmp->next;
  tmp->next->prev = phead;
}


2、2、10 尾删

 尾删的情况与头删的情况相似。同样,  在删除之前,我们应该首先判断链表中是否包含有效的节点(不包含头节点)。然后正常删除链接即可。我们看代码:

void LTPopFront(LTNode* phead)
{
  assert(phead);
  assert(!LTEmpty(phead));
  LTNode* tmp = phead->next;
  phead->next = tmp->next;
  tmp->next->prev = phead;
  free(tmp);
}


2、2、11 查找结点

 这里查找节点是为我们后面的任意插入和删除做铺垫,我们是需要在查找出的位置进行删除和插入操作。我们遍历查找的方法与上述打印查找的方法是一样的,避免陷入死循环。

LTNode* LTNodeFind(LTNode* phead, LNDataType x)
{
  assert(phead);
  LTNode* cur = phead->next;
  while (cur != phead)
  {
    if (cur->data == x)
      return cur;
    cur = cur->next;
  }
  return NULL;  // 找不到
}



2、2、12 在pos位置前插入

 在pos位置前插入和头插基本相同。我们在这里不过多解释,直接看代码。

void LTInsert(LTNode* pos, LNDataType x)
{
  assert(pos);
  LTNode* newnode = BuyLTNode(x);
  LTNode* prev = pos->prev;
  prev->next = newnode;
  newnode->prev = prev;
  newnode->next = pos;
  pos->prev = newnode;
}



2、2、13 删除pos位置节点

 删除pos位置节点与尾删也是大同小异,直接看代码:

void LTErase(LTNode* pos)
{
  assert(pos);
  LTNode* prev = pos->prev;
  prev->next = pos->next;
  pos->prev = prev;
  free(pos);
}

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


由于代码量相对来说有一点多,所以我们就将函数的声明的定义分开,这样有利于提高代码的可读性,同时会保持一个良好的思路,且方便编写代码。


 我们将函数的声明放在单独的一个QList.h的头文件,函数的实现放在一个单独的QList.c源文件,函数的主方法及调用放在另一个单独的test.c源文件。


QList.h

#define _CRT_SECURE_NO_WARNINGS 1
#include<assert.h>
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int LNDataType;
typedef struct DListNode
{
  struct DListNode* prev;
  struct DListNode* next;
  LNDataType data;
}LTNode;
LTNode* BuyLTNode(LNDataType x);
void LTInit(LTNode** phead);
void LTPrint(LTNode* phead);
void LTDestory(LTNode** phead);
bool LTEmpty(LTNode* phead);
void LTPushBack(LTNode* phead, LNDataType x);
void LTPopBack(LTNode* phead);
void LTPushFront(LTNode* phead, LNDataType x);
void LTPopFront(LTNode* phead);
// 在pos前一个位置插入
LTNode* LTNodeFind(LTNode* pos, LNDataType x);
void LTInsert(LTNode* pos, LNDataType x);
// 删除pos节点
void LTErase(LTNode* pos);


QList.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"Qlist.h"
LTNode* BuyLTNode(LNDataType x)
{
  LTNode* tmp = (LTNode*)malloc(sizeof(LTNode));
  if (tmp == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  tmp->data = x;
  tmp->next = NULL;
  tmp->prev = NULL;
  return tmp;
}
void LTInit(LTNode** phead)
{
  (*phead) = BuyLTNode(-1);
  (*phead)->next = *phead;
  (*phead)->prev = *phead;
}
void LTPrint(LTNode* phead)
{
  assert(phead);
  LTNode* cur = phead->next;
  printf("<-head->");
  while (cur != phead)
  {
    printf("%d<-->", cur->data);
    cur = cur->next;
  }
  printf("\n");
}
void LTDestory(LTNode** pphead)
{
  LTNode* cur = (*pphead)->next;
  while (cur != *pphead)
  {
    LTNode* tmp = cur->next;
    free(cur);
    cur = tmp;
  }
  free(*pphead);
  *pphead = NULL;
}
bool LTEmpty(LTNode* phead)
{
  assert(phead);
  return phead->next == phead;
}
void LTPushBack(LTNode* phead, LNDataType x)
{
  assert(phead);
  LTNode* newnode = BuyLTNode(x);
  LTNode* tail=phead->prev;
  tail->next = newnode;
  newnode->prev = tail;
  newnode->next = phead;
  phead->prev = newnode;
}
void LTPopBack(LTNode* phead)
{
  assert(phead);
  assert(!LTEmpty(phead));
  LTNode* tail = phead->prev;
  LTNode* tailprev = tail->prev;
  tailprev->next = phead;
  phead->prev = tailprev;
  free(tail);
  tail = NULL;
}
void LTPushFront(LTNode* phead, LNDataType x)
{
  assert(phead);
  LTNode* newnode = BuyLTNode(x);
  newnode->next = phead->next;
  phead->next->prev = newnode;
  phead->next = newnode;
  newnode->prev = phead;
}
void LTPopFront(LTNode* phead)
{
  assert(phead);
  assert(!LTEmpty(phead));
  LTNode* tmp = phead->next;
  phead->next = tmp->next;
  tmp->next->prev = phead;
  free(tmp);
}
// pos位置插入
LTNode* LTNodeFind(LTNode* phead, LNDataType x)
{
  assert(phead);
  LTNode* cur = phead->next;
  while (cur != phead)
  {
    if (cur->data == x)
      return cur;
    cur = cur->next;
  }
  return NULL;
}
void LTInsert(LTNode* pos, LNDataType x)
{
  assert(pos);
  LTNode* newnode = BuyLTNode(x);
  LTNode* prev = pos->prev;
  prev->next = newnode;
  newnode->prev = prev;
  newnode->next = pos;
  pos->prev = newnode;
}
void LTErase(LTNode* pos)
{
  assert(pos);
  LTNode* prev = pos->prev;
  prev->next = pos->next;
  pos->prev = prev;
  free(pos);
}



test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "Qlist.h"
void TestNode()
{
  LTNode* plist = NULL;
  LTInit(&plist);
  LTPushBack(plist, 1);
  LTPushBack(plist, 2);
  LTPushBack(plist, 3);
  LTPushBack(plist, 4);
  LTPrint(plist);
  LTPopBack(plist);
  LTPrint(plist);
  LTPopBack(plist);
  LTPrint(plist);
  LTPopBack(plist);
  LTPrint(plist);
  LTPopBack(plist);
  LTPrint(plist);
  LTPushFront(plist, 1);
  LTPrint(plist);
  LTPushFront(plist, 2);
  LTPrint(plist);
  LTPushFront(plist, 3);
  LTPrint(plist);
  LTPushFront(plist, 4);
  LTPrint(plist);
  LTPushFront(plist, 5);
  LTPrint(plist);
  LTPopFront(plist);
  LTPrint(plist);
  LTPopFront(plist);
  LTPrint(plist);
  LTPopFront(plist);
  LTPrint(plist);
  LTPopFront(plist);
  LTPrint(plist);
  LTPopFront(plist);
  LTPrint(plist);
  LTDestory(&plist);
}
int main()
{
  TestNode();
  return 0;
}


 我们对带头双向循环链表的讲解就到这里,希望以上内容会对你有所帮助,感谢阅读ovo~

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