【数据结构】双向带头循环链表

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

一、什么是带头双向循环链表

【数据结构】单链表中提到,链表为数据结构中的一种存储结构,链表属于线性存储结构,同时链表共有八种,此次所讲的链表为双向带头循环链表,属于链表中结构最复杂的结构;

带头双向循环链表是链表中带头(哨兵位)、双向、循环三种属性的结合体;

带头即带哨兵位哨兵位只负责存储第一个具有有效数据的节点,本身不存放数据,该处因为为双向循环链表,代表也可访问该链表的尾节点

双向即表示,每个节点不仅能访问该节点的后一个节点,同时也可访问本节点的前一个节点

循环即表示,第一个节点的prev指向尾节点

  • 带头双向循环链表虽然在结构中是所有链表中最为复杂的,但是相比较于单链表的优势在于不需要多次对链表为空进行判断,避免了边界问题
  • 同时如单链表所言,单链表在进行每次尾删或者尾插时,都需要遍历到尾节点且保存尾节点的前一个节点,而对于双向链表而言,只需要访问头节点的prev即可
  • 总而言之,双向带头循环链表而言,虽然在结构上要比单链表复杂的多,但是在操作上确是比单链表而言简单;

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

(一)链表中结构体的声明

因为该链表属于双向循环链表,故需要使用两个指针,一个指针next来存放该节点的下一个节点并使用prev指针来存放该节点的上一个节点

同时,为了避免使用者在使用时需要修改所存储的数据,可以使用typedef对需要存储的数据类型进行重命名方便后期对存储类型的修改

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<assert.h>
typedef int DataType;
typedef struct ListNode {
  struct ListNode* next;
  struct ListNode* prev;
  DataType val;
}LTNode;

(二)头节点的创建(链表的初始化)

该链表为带头双向循环练表,因为存在哨兵位,所以在插入数据之前应给链表安排一个哨兵位,并对哨兵位进行初始化

LTNode* ListCreate()//创建头节点(哨兵位)
{
  LTNode* newnode = BuyNode();
  newnode->next = newnode;
  newnode->prev = newnode;
  return newnode;
}

在对哨兵位进行初始化时同时也要注意该链表的结构为双向循环链表,在只有哨兵位的情况下,哨兵位的next与prev都指向自己


(三)新节点的创建

新节点的创建也与单链表的相同,唯一不同的就是新创建的节点需要注意双向循环的格式;

LTNode* BuyNode()
{
  LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
  if (newnode == NULL) {
    perror("BuyNode::malloc");
    return NULL;
  }
  return newnode;
}

(四)链表的尾插

若是在单链表中,尾插需要遍历整个链表找到尾节点,而在该链表中,因为链表头节点(哨兵位)所指向的prev即为尾节点;

故在该链表进行尾插时,只需访问哨兵位的prev所指向的节点

void ListPushBack(LTNode* phead, DataType x)
{
  assert(phead);
  LTNode* tail = phead->prev;
  LTNode* newnode = BuyNode();
  newnode->val = x;
  tail->next = newnode;
  newnode->prev = tail;
  newnode->next = phead;
  phead->prev = newnode;
}

(五)链表的尾删

在双链表中,链表的尾删只需释放头节点head内prev所指向的节点(尾节点)即可;

void ListProBack(LTNode* phead)//尾删
{
  assert(phead);
  LTNode* cur = phead->prev->prev;
  free(cur->next);
  cur->next = phead;
  phead->prev = cur;
}

(六)链表的头插

因为该链表存在哨兵位,故在进行头插时需要插入至头节点(哨兵位)之后;

void ListPushFront(LTNode* phead, DataType x)//头插
{
  assert(phead);
  LTNode* newnode = BuyNode();
  newnode->val = x;
  LTNode* tail = phead->next;
  phead->next = newnode;
  newnode->prev = phead;
  newnode->next = tail;
  tail->prev = newnode;
}

(七)链表的头删

同样的带头双向循环链表在进行头删时只需要删除头节点(哨兵位)后的节点即可;

void ListProFront(LTNode* phead)//头删
{
  assert(phead);
  LTNode* tail = phead->next->next;
  free(phead->next);
  phead->next = tail;
  tail->prev = phead;
}

(八)链表的查找

双链表的查找,利用while循环对链表进行遍历,循环条件为当指针指向不为哨兵位;

若为哨兵位,则代表链表已经遍历了一次,若是找到对应val的值,则返回节点的地址,否则返回空指针;

LTNode* ListFind(LTNode* phead, DataType x)//查
{
  LTNode* tail = phead->next;
  while (tail->next != phead) {
    if (tail->val == x) {
      return tail;
    }
    tail = tail->next;
  }
  return NULL;
}

(九)链表节点的修改

参数给定节点的地址,通过地址访问该节点所对应的val并对其进行修改;

void ListModify(LTNode* pos, DataType x) //改
{
  assert(pos);
  pos->val = x;
}

(十)链表的中间插入(pos前)

相比较单链表来说,双向链表可以访问所给pos节点的前一个节点,而单链表若是需要进行在pos节点前进行插入则各项消耗都会增大;

参数给定一个pos指针,指针指向为需要插入的节点位置,将新的数据(节点)插入pos所指向节点的prev所指向的位置;

void ListInsert(LTNode* pos, DataType x) //中间插入(pos前)
{
  assert(pos);
  LTNode* tail = pos->prev;
  LTNode* newnode = BuyNode();
  newnode->val = x;
  pos->prev = newnode;
  newnode->next = pos;
  tail->next = newnode;
  newnode->prev = tail;
}

(十一)链表的中间删除

链表的中间删除则不需要再进行pos之后或者pos之前,双向链表既可以访问pos节点的next,又可访问pos节点的prev;

即在进行中间删除时,可以直接将pos节点进行删除

void ListErase(LTNode* pos)
{
  LTNode* tail = pos->prev;
  LTNode* cur = pos->next;
  tail->next = cur;
  cur->prev = tail;
  free(pos);
  pos = NULL;
}

(十二)链表的销毁

当使用完空间并不需要再使用时,应及时将空间进行释放,避免出现内存泄漏等问题,在顺序表中,可直接将malloc后的空间进行释放,因为在物理因素上,顺序表为一块连续不断的空间

而链表则只在逻辑上具有连续不断的属性,在物理上本质是一块一块不同位置的空间

双链表的销毁也与单链表的销毁逻辑相同;

void ListDestory(LTNode** pphead)//销毁
{
  LTNode* tail = (*pphead)->next;
  LTNode* cur = NULL;
  while (tail != *pphead) {
    cur = tail->next;
    free(tail);
    tail = cur;
  }
  free(*pphead);
  *pphead = NULL;
}

总结

  • 带头双向循环链表在链表为空的情况下(只有头节点),头节点的next与prev指针都指向自己;
  • 在写双链表的各个接口时应注意应尽量使用断言判断形参所接收的指针是否为有效指针;
  • 使用完内存后应及时将内存还给操作系统(释放);
  • 带头双向循环链表与不带头单向不循环链表为链表中的两个极端,一种为八种链表中结构最复杂的链表;一种为八种链表中结构最简单的链表;但是反而再逻辑中,由于结构的特性,带头双向循环链表的逻辑是几种链表中逻辑最简单的;
相关文章
|
23天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
50 4
|
24天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
24天前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
1月前
|
存储 Java
数据结构第三篇【链表的相关知识点一及在线OJ习题】
数据结构第三篇【链表的相关知识点一及在线OJ习题】
26 7
|
1月前
|
存储 安全 Java
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
25 3
|
1月前
|
算法 Java
数据结构与算法学习五:双链表的增、删、改、查
双链表的增、删、改、查操作及其Java实现,并通过实例演示了双向链表的优势和应用。
17 0
数据结构与算法学习五:双链表的增、删、改、查
|
23天前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
40 0
【数据结构】——双向链表详细理解和实现
【数据结构】——双向链表详细理解和实现
|
1月前
|
存储 Java
【数据结构】链表
【数据结构】链表
18 1
|
1月前
|
存储 缓存
数据结构3——双向链表
数据结构3——双向链表
114 1