【数据结构】带头+双向+循环链表(DList)(增、删、查、改)详解

简介: 【数据结构】带头+双向+循环链表(DList)(增、删、查、改)详解

一、带头双向循环链表的定义和结构

1、定义

带头双向循环链表,有一个数据域两个指针域。一个是前驱指针,指向其前一个节点;一个是后继指针,指向其后一个节点。

// 定义双向链表的节点
typedef struct ListNode
{
  LTDataType data; // 数据域
  struct ListNode* prev; // 前驱指针
  struct ListNode* next; // 后继指针
}ListNode;

2、结构

带头双向循环链表:在所有的链表当中 结构最复杂,一般用在 单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多 优势,实现反而简单了。


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

1、创建文件

  • test.c(主函数、测试顺序表各个接口功能)
  • List.c(带头双向循环链表接口函数的实现)
  • List.h(带头双向循环链表的类型定义、接口函数声明、引用的头文件)


2、List.h 头文件代码

// List.h
// 带头+双向+循环链表增删查改实现
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
 
typedef int LTDataType;
 
// 定义双向链表的节点
typedef struct ListNode
{
  LTDataType data; // 数据域
  struct ListNode* prev; // 前驱指针
  struct ListNode* next; // 后继指针
}ListNode;
 
// 动态申请一个新节点
ListNode* BuyListNode(LTDataType x);
// 创建返回链表的头结点
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(ListNode* plist);
// 双向链表打印
void ListPrint(ListNode* plist);
// 双向链表尾插
void ListPushBack(ListNode* plist, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* plist);
// 双向链表头插
void ListPushFront(ListNode* plist, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* plist);
// 双向链表查找
ListNode* ListFind(ListNode* plist, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
// 双向链表的判空
bool ListEmpty(ListNode* phead);
// 获取双向链表的元素个数
size_t ListSize(ListNode* phead);

三、在 List.c 上是实现各个接口函数

1、动态申请一个新结点

// 动态申请一个新节点
ListNode* BuyListNode(LTDataType x)
{
  ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
  newnode->data = x;
  newnode->prev = NULL;
  newnode->next = NULL;
  return newnode;
}

2、创建返回链表的头结点(初始化头结点)

// 创建返回链表的头结点
ListNode* ListCreate()
{
  ListNode* phead = (ListNode*)malloc(sizeof(ListNode)); // 哨兵位头结点
  phead->next = phead;
  phead->prev = phead;
  return phead;
}

也可以用下面这个函数(道理一样):

// 初始化链表
void ListInit(ListNode** pphead)
{
  *pphead = BuyListNode(-1); // 动态申请一个头节点
  (*pphead)->prev = *pphead; // 前驱指针指向自己
  (*pphead)->next = *pphead; // 后继指针指向自己
}

头指针初始指向 NULL,初始化链表时,需要改变头指针的指向,使其指向头节点,所以这里需要传二级指针。


初始化带头双向循环链表,首先动态申请一个头结点头结点的前驱和后继指针都指向自己,形成一个循环


3、双向链表的销毁

// 双向链表的销毁
void ListDestroy(ListNode** pphead)
{
  assert(pphead);
  assert(*pphead);
 
  ListNode* cur = (*pphead)->next;
  while (cur != *pphead)
  {
    ListNode* next = cur->next; // 记录cur的直接后继节点
    free(cur);
    cur = next;
  }
  free(*pphead); // 释放头节点
  *pphead = NULL; // 置空头指针
}

销毁链表,最后要将头指针 plist 置空,所以用了二级指针来接收。这里也可以用一级指针,但要在函数外面置空 plist 。

一级指针写法:

void ListDestroy(ListNode* phead)
{
  assert(phead);
  ListNode* cur = phead->next;
  while (cur != phead)
  {
    ListNode* next = cur->next;
    free(cur);
    cur = next;
  }
  free(phead);
  phead = NULL;
}

4、双向链表的打印

// 打印双向链表
void ListPrint(ListNode* phead)
{
  assert(phead);
 
  ListNode* cur = phead->next; // 记录第一个节点
  printf("head <-> ");
  while (cur != phead)
  {
    printf("%d <-> ", cur->data);
    cur = cur->next;
  }
  printf("head\n");
}

5、双向链表的尾插

// 双向链表尾插
void ListPushBack(ListNode* phead, LTDataType x)
{
  assert(phead); // 头指针不能为空
 
  /* ListNode* newnode = BuyListNode(x); // 动态申请一个节点
  ListNode* tail = phead->prev; // 记录尾节点
  tail->next = newnode; // 尾节点的后继指针指向新节点
  newnode->prev = tail; //2、新节点的前驱指针指向尾节点
  newnode->next = phead; // 新节点的后继指针指向头节点
  phead->prev = newnode; // 头节点的前驱指针指向新节点 */
 
    ListInsert(phead, x);
}


6、双向链表的尾删

// 双向链表的尾删
void ListPopBack(ListNode* phead)
{
  assert(phead);
  assert(phead->next != phead); // 只剩头节点时 链表为空 不能再继续删除
 
  /* ListNode* tail = phead->prev; // 记录尾节点
  ListNode* tailPrev = tail->prev; // 记录尾节点的直接前驱
  
  tailPrev->next = phead; // 尾节点的前驱节点的next指针指向头节点
  phead->prev = tailPrev; // 头节点的prev指针指向尾节点的前驱节点
  free(tail); // 释放尾节点 */
 
    ListErase(pHead->prev);
}


7、双向链表的头插

// 双向链表的头插
void ListPushFront(ListNode* phead, LTDataType x)
{
  assert(phead);
 
  /* ListNode* newnode = BuyListNode(x); // 申请新节点
  ListNode* pheadNext = phead->next; // 记录第一个节点
  
  // 头节点和新节点建立链接
  phead->next = newnode;
  newnode->prev = phead;
  // 新节点和第一个节点建立链接
  newnode->next = pheadNext;
  pheadNext->prev = newnode; */
 
    ListInsert(phead->next, x);
}


8、双向链表的头删

// 双向链表的头删
void ListPopFront(ListNode* phead)
{
  assert(phead);
  assert(phead->next != phead); // 只剩头节点时 链表为空 不能再继续删除
 
  /* ListNode* pheadNext = phead->next; // 记录第一个节点
  // 头节点和第一个节点的后继节点建立链接
  phead->next = pheadNext->next;
  pheadNext->next->prev = phead;
    free(pheadNext); // 头删 */
 
    ListErase(phead->next);
}


9、查找双向链表中的元素

// 在双向链表中查找元素,并返回该元素的地址
ListNode* ListFind(ListNode* phead, LTDataType x)
{
  assert(phead);
 
  ListNode* cur = phead->next;
  while (cur != phead)
  {
    if (cur->data == x)
    {
      return cur;  //找到了 返回该元素的地址
    }
    cur = cur->next;
  }
  return NULL;  //没找到 返回NULL
}

10、在指定pos位置之前插入元素

// 在指定pos位置之前插入元素
void ListInsert(ListNode* pos, LTDataType x)
{
  assert(pos);
 
  ListNode* newnode = BuyListNode(x); // 申请一个节点
  ListNode* posPrev = pos->prev; // 记录pos的直接前驱
 
  // pos的直接前驱和新节点建立链接
  posPrev->next = newnode;
  newnode->prev = posPrev;
 
  // 新节点和pos建立链接
  newnode->next = pos;
  pos->prev = newnode;
}

实现了该函数后,可以尝试改进头插函数(pos相当于链表的第一个节点)和尾插函数(pos相当于链表的头节点),这样写起来更简便


11、删除指定pos位置的元素

// 删除指定pos位置的元素
void ListErase(ListNode* pos)
{
  assert(pos);
 
  ListNode* posPrev = pos->prev; // 记录pos的直接前驱
  ListNode* posNext = pos->next; // 记录pos的直接后继
 
  // pos的直接前驱和直接后继建立链接
  posPrev->next = posNext;
  posNext->prev = posPrev;
  
  free(pos); // 释放pos位置的元素
    //pos = NULL;
}

实现了该函数后,可以尝试改进函数(pos相当于链表的第一个节点)和尾删函数(pos相当于链表的最后一个节点),这样写起来更简便


12、双向链表的判空

// 双向链表的判空
bool ListEmpty(ListNode* phead)
{ 
  assert(phead);
  return phead->next == phead; //为空 返回ture 否则返回false
}

13、获取双向链表的元素个数

// 获取双向链表的元素个数
size_t ListSize(ListNode* phead)
{
  assert(phead);
 
  size_t size = 0;
  ListNode* cur = phead->next; // 记录第一个节点
  while (cur != phead)
  {
    size++;
    cur = cur->next;
  }
  return size;
}

四、代码整合

// List.c
#include "List.h"
 
// 动态申请一个新节点
ListNode* BuyListNode(LTDataType x)
{
  ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
  newnode->data = x;
  newnode->prev = NULL;
  newnode->next = NULL;
  return newnode;
}
 
// 创建返回链表的头结点
ListNode* ListCreate()
{
  ListNode* phead = (ListNode*)malloc(sizeof(ListNode)); // 哨兵位头结点
  phead->next = phead;
  phead->prev = phead;
  return phead;
}
 
// 双向链表的销毁
void ListDestroy(ListNode** pphead)
{
  assert(pphead);
  assert(*pphead);
 
  ListNode* cur = (*pphead)->next;
  while (cur != *pphead)
  {
    ListNode* next = cur->next; // 记录cur的直接后继节点
    free(cur);
    cur = next;
  }
  free(*pphead); // 释放头节点
  *pphead = NULL; // 置空头指针
}
 
// 打印双向链表
void ListPrint(ListNode* phead)
{
  assert(phead);
 
  ListNode* cur = phead->next; // 记录第一个节点
  printf("head <-> ");
  while (cur != phead)
  {
    printf("%d <-> ", cur->data);
    cur = cur->next;
  }
  printf("head\n");
}
 
// 双向链表尾插
void ListPushBack(ListNode* phead, LTDataType x)
{
  assert(phead); // 头指针不能为空
    ListInsert(phead, x);
}
 
// 双向链表的尾删
void ListPopBack(ListNode* phead)
{
  assert(phead);
  assert(phead->next != phead); // 只剩头节点时 链表为空 不能再继续删除
    ListErase(pHead->prev);
}
 
// 双向链表的头插
void ListPushFront(ListNode* phead, LTDataType x)
{
  assert(phead);
    ListInsert(phead->next, x);
}
 
// 双向链表的头删
void ListPopFront(ListNode* phead)
{
  assert(phead);
  assert(phead->next != phead); // 只剩头节点时 链表为空 不能再继续删除
    ListErase(phead->next);
}
 
// 在双向链表中查找元素,并返回该元素的地址
ListNode* ListFind(ListNode* phead, LTDataType x)
{
  assert(phead);
 
  ListNode* cur = phead->next;
  while (cur != phead)
  {
    if (cur->data == x)
    {
      return cur;  //找到了 返回该元素的地址
    }
    cur = cur->next;
  }
  return NULL;  //没找到 返回NULL
}
 
// 在指定pos位置之前插入元素
void ListInsert(ListNode* pos, LTDataType x)
{
  assert(pos);
 
  ListNode* newnode = BuyListNode(x); // 申请一个节点
  ListNode* posPrev = pos->prev; // 记录pos的直接前驱
 
  // pos的直接前驱和新节点建立链接
  posPrev->next = newnode;
  newnode->prev = posPrev;
 
  // 新节点和pos建立链接
  newnode->next = pos;
  pos->prev = newnode;
}
 
// 删除指定pos位置的元素
void ListErase(ListNode* pos)
{
  assert(pos);
 
  ListNode* posPrev = pos->prev; // 记录pos的直接前驱
  ListNode* posNext = pos->next; // 记录pos的直接后继
 
  // pos的直接前驱和直接后继建立链接
  posPrev->next = posNext;
  posNext->prev = posPrev;
  
  free(pos); // 释放pos位置的元素
    //pos = NULL;
}
 
// 双向链表的判空
bool ListEmpty(ListNode* phead)
{ 
  assert(phead);
  return phead->next == phead; //为空 返回ture 否则返回false
}
 
// 获取双向链表的元素个数
size_t ListSize(ListNode* phead)
{
  assert(phead);
 
  size_t size = 0;
  ListNode* cur = phead->next; // 记录第一个节点
  while (cur != phead)
  {
    size++;
    cur = cur->next;
  }
  return size;
}


相关文章
|
1天前
|
Java
java数据结构,双向链表的实现
文章介绍了双向链表的实现,包括数据结构定义、插入和删除操作的代码实现,以及双向链表的其他操作方法,并提供了完整的Java代码实现。
java数据结构,双向链表的实现
|
23天前
|
存储 Java 索引
【数据结构】链表从实现到应用,保姆级攻略
本文详细介绍了链表这一重要数据结构。链表与数组不同,其元素在内存中非连续分布,通过指针连接。Java中链表常用于需动态添加或删除元素的场景。文章首先解释了单向链表的基本概念,包括节点定义及各种操作如插入、删除等的实现方法。随后介绍了双向链表,说明了其拥有前后两个指针的特点,并展示了相关操作的代码实现。最后,对比了ArrayList与LinkedList的不同之处,包括它们底层实现、时间复杂度以及适用场景等方面。
41 10
【数据结构】链表从实现到应用,保姆级攻略
|
1月前
|
存储 C语言
【数据结构】c语言链表的创建插入、删除、查询、元素翻倍
【数据结构】c语言链表的创建插入、删除、查询、元素翻倍
【数据结构】c语言链表的创建插入、删除、查询、元素翻倍
|
2月前
【数据结构OJ题】环形链表
力扣题目——环形链表
32 3
【数据结构OJ题】环形链表
|
1月前
【数据结构】双向带头(哨兵位)循环链表 —详细讲解(赋源码)
【数据结构】双向带头(哨兵位)循环链表 —详细讲解(赋源码)
82 4
|
2月前
【数据结构OJ题】复制带随机指针的链表
力扣题目——复制带随机指针的链表
48 1
【数据结构OJ题】复制带随机指针的链表
|
2月前
【数据结构OJ题】环形链表II
力扣题目——环形链表II
22 1
【数据结构OJ题】环形链表II
|
2月前
【数据结构OJ题】相交链表
力扣题目——相交链表
27 1
【数据结构OJ题】相交链表
|
1月前
|
存储 算法
【初阶数据结构篇】顺序表和链表算法题
此题可以先找到中间节点,然后把后半部分逆置,最近前后两部分一一比对,如果节点的值全部相同,则即为回文。
|
1月前
|
存储 测试技术
【初阶数据结构篇】双向链表的实现(赋源码)
因为头结点的存在,plist指针始终指向头结点,不会改变。