【数据结构】双向带头(哨兵位)循环链表 —详细讲解(赋源码)

简介: 【数据结构】双向带头(哨兵位)循环链表 —详细讲解(赋源码)

前言

双向循环列表是一种特殊的数据结构,它结合了双向链表和循环链表的特点。在双向循环列表中,每个节点除了拥有指向下一个节点的指针外,还拥有指向上一个节点的指针。此外,列表的头节点和尾节点通过这些指针相互连接,形成一个闭环。这种结构允许从任何一个节点开始,既可以向前遍历,也可以向后遍历,直到回到起点,从而实现高效的双向遍历。

一、双向带头循环链表的基本介绍

  • 定义节点结构:每个节点包含数据部分和两个指针部分,分别指向前一个节点和后一个节点。
  • 创建头节点:头节点是双向循环列表的起始点,它的前驱指针指向自己,后继指针也指向自己,形成一个闭环。
  • 插入节点:在特定位置插入新节点时,需要更新新旧节点的前驱和后继指针,确保列表的完整性。
  • 删除节点:删除特定节点时,同样需要更新前后节点的指针,避免出现悬空指针。
  • 遍历列表:可以通过前驱指针或后继指针从任意节点开始,向前或向后遍历整个列表。

双向带头链表的操作

常见操作包括初始化插入删除查找遍历。这些操作通常涉及到对链表节点的指针进行操作,以实现数据的动态管理。

双向带头循环链表基本理解

image.png

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

2.1 双向带头循环链表的功能

//链表初始化创建哨兵位
ListNode* ListInit();
//连接表增加节点
ListNode* BuyNewNode(ListDataType x);
//链表打印
void DlistPrint(ListNode* phead);
//链表尾插
void DLlistPushBack(ListNode* phead, ListDataType x);
//链表尾删
void DLlistPopBack(ListNode* phead);
//链表头插
void DLlistPushFront(ListNode* phead, ListDataType x);
//链表头删
void DLlistPopFront(ListNode* phead);
//链表查找
ListNode* DListFind(ListNode* phead, ListDataType pos);
//链表pos插入
void DListInset(ListNode* pos, ListDataType);
//链表pos删除
void DListErase(ListNode* phead, ListNode* pos);
//链表销毁
void DListDestory(ListNode* phead);

2.2 定义节点结构体

由于是双向带头,需要定义一个nextprev

typedef int ListDataType;

typedef struct DList
{
  int data;
  struct Dlist* prev;
  struct Dlist* next;

}ListNode;

2.3 链表的增加节点

这里可以相比较于单链表的操作,进行操作。

ListNode* BuyNewNode(ListDataType x)
{
  ListNode* newcode = (ListNode*)malloc(sizeof(ListNode));
  if (newcode == NULL)
  {
    perror("malloc fail");
    return NULL;
  }

  newcode->data = x;
  newcode->next = NULL;
  newcode->prev = NULL;
  return newcode;

}

2.4 链表的初始化

这个操作就是带头

//链表初始化创建哨兵卫
ListNode* ListInit()
{
  ListNode* head = BuyNewNode(-1);
  head->next = head;
  head->prev = head;
  return head;
}

2.5 链表的遍历

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

2.6 链表的尾插

由于双向链表结构的特殊性,相比于单链表的效率更加高。

操作就是哨兵位的上一个节点,进行插入。

//链表尾插
void DLlistPushBack(ListNode* phead, ListDataType x)
{
  assert(phead);
  ListNode* newcode = BuyNewNode(x);
  ListNode* tail = phead->prev;
  tail->next = newcode;
  newcode->prev = tail;
  newcode->next = phead;
  phead->prev = newcode;

}

2,7 链表的尾删

单链表进行尾部删的时候会进行断言,判断链表是否为空,双向链表也是如此,但是进行的操作是哨兵位的上一个节点或者下一个节点是否指向自己。

判断是否为空链表

我们利用bool类型判断真或者假

bool ListEmpty(ListNode* phead)
{
  assert(phead);
  return phead->prev == phead;
}

尾删操作

//链表尾删
void DLlistPopBack(ListNode* phead)
{
  assert(phead);
  assert(!ListEmpty(phead));
  ListNode* tail = phead->prev;
  ListNode* tailprev = tail->prev;

  tailprev->next = phead;
  phead->prev = tailprev;
  free(tail);
  tail = NULL;
}

2.8 链表的头插

记录哨兵位下一个节点,进行头插入。

//链表头插
void DLlistPushFront(ListNode* phead, ListDataType x)
{
  assert(phead);

  ListNode* newcode = BuyNewNode(x);
  ListNode* cur = phead->next;
  phead->next = newcode;
  newcode->prev = phead;
  newcode->next = cur;
  cur->prev = newcode;
}

2.9 链表的头部删

头删的操作同,尾部删除一样,就是换了哥位置。(但是还是要注意进行判断链表为空的问题)

void DLlistPopFront(ListNode* phead)
{
  assert(phead);
  assert(!ListEmpty(phead));

  ListNode* cur = phead->next;
  ListNode* curnext = cur->next;
  phead->next = curnext;
  curnext->prev = phead;
  free(cur);
  cur = NULL;

}

2.10 链表的查找

链表的查找的时候返回写成结构体指针的形式,以便于进行指定位置的插入和删除。

//链表查找
ListNode* DListFind(ListNode* phead, ListDataType pos)
{
  assert(phead);

  ListNode* cur = phead->next;
  while (cur != phead && cur->data != pos)
  {
    if (cur->data == pos)
    {
      break;
    }
    cur = cur->next;
  }
  return cur;
}

2.11 链表指定位置的插入

在这里面不运用二级指针的原因就是我们改变的是结构体的变量,并不是结构体指针。

//链表pos插入
void DListInset( ListNode* pos,ListDataType x)
{
  assert(pos);
  
  ListNode* newcode = BuyNewNode(x);

  ListNode* cur = pos->prev;

  cur->next = newcode;
  newcode->prev = cur;
  newcode->next = pos;
  pos->prev = newcode;

}

2.12 链表指定位置的删除

//链表pos删除
void DListErase(ListNode* phead, ListNode* pos)
{
  assert(!ListEmpty(phead));
  assert(pos);

  ListNode* posprev = pos->prev;
  ListNode* posnext = pos->next;

  posprev->next = posnext;
  posnext->prev = posprev;
  free(pos);
    //pos = NULL;
  //由于一级指针,NULL改变不了形式参数
  
}

2.13 链表的销毁

//链表销毁
void DListDestory(ListNode* phead)
{
  assert(phead);

  ListNode* cur = phead->next;

  while (cur != phead)
  {
    ListNode* curnext = cur->next;
    free(cur);
    //在循环第一句自动下一步
    cur = curnext;
    
  }
  //释放哨兵位头节点
  //不需要置空,形参数
  free(phead);
}

源码

DList.h

#pragma once


#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>


typedef int ListDataType;

typedef struct DList
{
  int data;
  struct Dlist* prev;
  struct Dlist* next;

}ListNode;

//链表初始化创建哨兵卫
ListNode* ListInit();
//连接表增加节点
ListNode* BuyNewNode(ListDataType x);
//链表打印
void DlistPrint(ListNode* phead);
//链表尾插
void DLlistPushBack(ListNode* phead, ListDataType x);
//链表尾删
void DLlistPopBack(ListNode* phead);
//链表头插
void DLlistPushFront(ListNode* phead, ListDataType x);
//链表头删
void DLlistPopFront(ListNode* phead);
//链表查找
ListNode* DListFind(ListNode* phead, ListDataType pos);
//链表pos插入
void DListInset(ListNode* pos, ListDataType);
//链表pos删除
void DListErase(ListNode* phead, ListNode* pos);
//链表销毁
void DListDestory(ListNode* phead);

DList.c

#define  _CRT_SECURE_NO_WARNINGS   1

#include "DList.h"

ListNode* BuyNewNode(ListDataType x)
{
  ListNode* newcode = (ListNode*)malloc(sizeof(ListNode));
  if (newcode == NULL)
  {
    perror("malloc fail");
    return NULL;
  }

  newcode->data = x;
  newcode->next = NULL;
  newcode->prev = NULL;
  return newcode;

}

//链表初始化创建哨兵卫
ListNode* ListInit()
{
  ListNode* head = BuyNewNode(-1);
  head->next = head;
  head->prev = head;
  return head;
}

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

//链表尾插
void DLlistPushBack(ListNode* phead, ListDataType x)
{
  assert(phead);
  ListNode* newcode = BuyNewNode(x);
  ListNode* tail = phead->prev;
  tail->next = newcode;
  newcode->prev = tail;
  newcode->next = phead;
  phead->prev = newcode;

}

//判断链表是不是空
bool ListEmpty(ListNode* phead)
{
  assert(phead);
  return phead->prev == phead;
}


//链表尾删
void DLlistPopBack(ListNode* phead)
{
  assert(phead);
  assert(!ListEmpty(phead));
  ListNode* tail = phead->prev;
  ListNode* tailprev = tail->prev;

  tailprev->next = phead;
  phead->prev = tailprev;
  free(tail);
  tail = NULL;
}

//链表头插
void DLlistPushFront(ListNode* phead, ListDataType x)
{
  assert(phead);

  ListNode* newcode = BuyNewNode(x);
  ListNode* cur = phead->next;
  phead->next = newcode;
  newcode->prev = phead;
  newcode->next = cur;
  cur->prev = newcode;
}

//链表头删
void DLlistPopFront(ListNode* phead)
{
  assert(phead);
  assert(!ListEmpty(phead));

  ListNode* cur = phead->next;
  ListNode* curnext = cur->next;
  phead->next = curnext;
  curnext->prev = phead;
  free(cur);
  cur = NULL;

}

//链表查找
ListNode* DListFind(ListNode* phead, ListDataType pos)
{
  assert(phead);

  ListNode* cur = phead->next;
  while (cur != phead && cur->data != pos)
  {
    if (cur->data == pos)
    {
      break;
    }
    cur = cur->next;
  }
  return cur;
}

//链表pos插入
void DListInset( ListNode* pos,ListDataType x)
{
  assert(pos);
  
  ListNode* newcode = BuyNewNode(x);

  ListNode* cur = pos->prev;

  cur->next = newcode;
  newcode->prev = cur;
  newcode->next = pos;
  pos->prev = newcode;

}

//链表pos删除
void DListErase(ListNode* phead, ListNode* pos)
{
  assert(!ListEmpty(phead));
  assert(pos);

  ListNode* posprev = pos->prev;
  ListNode* posnext = pos->next;

  posprev->next = posnext;
  posnext->prev = posprev;
  free(pos);
  //由于一级指针,NULL改变不了形式参数
  
}

//链表销毁
void DListDestory(ListNode* phead)
{
  assert(phead);

  ListNode* cur = phead->next;

  while (cur != phead)
  {
    ListNode* curnext = cur->next;
    free(cur);
    //在循环第一句自动下一步
    cur = curnext;
    
  }
  //释放哨兵位头节点
  //不需要值空,形参数
  free(phead);
}

test.c

#define  _CRT_SECURE_NO_WARNINGS   1

#include "DList.h"

void testDlist1()
{
  ListNode* plist = ListInit();
  DLlistPushBack(plist, 1);
  DLlistPushBack(plist, 2);
  DLlistPushBack(plist, 3);
  DLlistPushBack(plist, 4);

  
  ListNode* ret = DListFind(plist, 2); 
  //ret->data = (ret->data) * 2;
  DListInset(ret, 40);
  DListErase(plist, ret);

  DLlistPopBack(plist);

  DlistPrint(plist);

}
void testDlist2()
{
  ListNode* plist = ListInit();
  DLlistPushFront(plist, 1);
  DLlistPushFront(plist, 2);
  DLlistPushFront(plist, 3);
  DLlistPushFront(plist, 4);
  DLlistPushFront(plist, 5);

  DLlistPopFront(plist);
  DLlistPopFront(plist);



  DlistPrint(plist);

}


int main()
{
  testDlist1();
  //testDlist2();


  return 0;
}

目录
相关文章
|
1月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
159 9
|
25天前
|
存储 算法 Perl
数据结构实验之链表
本实验旨在掌握线性表中元素的前驱、后续概念及链表的建立、插入、删除等算法,并分析时间复杂度,理解链表特点。实验内容包括循环链表应用(约瑟夫回环问题)、删除单链表中重复节点及双向循环链表的设计与实现。通过编程实践,加深对链表数据结构的理解和应用能力。
51 4
|
17天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
38 5
|
1月前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
71 16
|
1月前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
120 8
|
25天前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
42 0
|
1月前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
48 0
|
1月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
26 1
|
1月前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
1月前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。