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

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

前言

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

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

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

双向带头链表的操作

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

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

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;
}

目录
相关文章
|
14天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
90 9
|
13天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
56 16
|
13天前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
62 8
|
15天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
43 4
|
17天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
15天前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
35 0
|
5天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
14 1
|
7天前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
10天前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
12天前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
40 4