双向链表(详解)

简介: 双向链表(详解)

在单链表专题中我们提到链表的分类,其中提到了带头双向循环链表,今天小编将详细讲下双向链表。

话不多说,直接上货。

1.双向链表的结构

带头双向循环链表

注意

这几的“带头”跟前面我们说的“头节点”是两个概念,实际前面的在单链表阶段称呼不严

谨,但是为了更好的理解就直接称为单链表的头节点。

带头链表里的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这里“放哨 的”。

“哨兵位”存在的意义: 遍历循环链表避免死循环。


插入操作时,不需要检查是否在头部插入,因为哨兵节点作为头结点,总是存在。

删除操作时,不需要处理删除的是否是头节点的情况,因为哨兵节点不会被删除。

简化了代码,因为不需要为头节点和普通节点编写不同的处理逻辑。

双向链表文字上没有什么好说的,具体主要是代码的实现,有了单链表的基础铺垫,双向链表实现也会轻松很多, 主要是理清楚前后节点的关系。

2.双向链表的实现

我们同样是按照项目的格式。

1.头文件的建立(函数库引入,所需函数的导入)

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//#include <stdbool.h>
typedef int Datatype;
//链表节点创建
typedef struct  ListNode {
  Datatype data;
  struct ListNode* next;
  struct ListNode* prev;
}Node;
//相关函数实现
// 链表初始化,双向链表头节点不能为空
//void LTInit(Node** pphead);
Node* LTInit();
//链表销毁
void LTDestroy(Node* phead);
//链表打印
void LTPrint(Node* phead);
//bool LTEmpty(Node* phead);
//尾插和头插
void LTPushBack(Node* phead, Datatype x);
void LTPushFront(Node* phead, Datatype x);
//尾删和头删
void LTPopBack(Node* phead);
void LTPopFront(Node* phead);
//在pos位置之后插⼊数据
void LTInsert(Node* pos, Datatype x);
//删除指定节点
void LTErase(Node* pos);
//节点找寻,方便指定插入和删除
Node* LTFind(Node* phead, Datatype x);

2.相关函数的实现

2.1 新节点建立

Node* Buynode(Datatype x) {
  Node* node = (Node*)malloc(sizeof(Node));
  if (node == NULL) {
    perror("malloc error");
    exit(1);
  }
  node->data = x;
  node->next = node->prev=node;
  return node;
}

这里为什么节点前后没有指向空指针,因为在后续中链表初始化时,若指向都是空指针,则创建的链表不是双向链表。

在双向链表中,每个节点都有两个指针,一个指向前一个节点(prev),一个指向后一个节点(next)。这样可以实现双向遍历和操作。 当初始化一个双向链表时,需要创建一个头节点(head node),这个头节点不存储实际的数据,只用于标识链表的起始位置。头节点的prev指针和next指针都应该指向自己,这样可以在链表中任意位置插入和删除节点而不需要特殊处理边界情况。

如果初始化时将prev和next指针都指向空,那么在插入和删除节点时就需要特殊处理头节点和尾节点的情况,增加了代码的复杂性。因此,在初始化时将prev和next指针都指向自己是一种简化设计,方便后续操作的方式。

总结来说,prev和next指针不能指向空,是为了简化双向链表的设计和操作。

2.2 链表初始化

Node* LTInit() {
  Node* pphead = Buynode(-1);
  if (pphead == NULL) {
    printf("初始化失败!");
  }
  return pphead;
}

2.3 链表的打印

//链表打印
void LTPrint(Node* phead) {
  Node* pcur = (Node*)malloc(sizeof(Node));
  pcur = phead->next;
  while (pcur!= phead) {
    printf("%d->", pcur->data);
    pcur = pcur->next;
  }
  printf("\n");
}

2.4 尾插和头插

/尾插和头插
void LTPushBack(Node* phead, Datatype x) {
  assert(phead);
  //Node* ptail = (Node*)malloc(sizeof(Node));
  Node* newnode = Buynode(x);
  newnode->prev = phead->prev;//新节点指向原来的尾节点
  newnode->next = phead;
  phead->prev->next = newnode; //让原本的尾节点指向新的节点
  phead->prev = newnode;
}
void LTPushFront(Node* phead, Datatype x) {
  assert(phead);
  Node* newnode = Buynode(x);
  newnode->next = phead->next;
  newnode->prev = phead;
  phead->next->prev= newnode;//两行不能完全交换,若交换
  phead->next = newnode;//phead->next=newnode;newnode->prev=newnode;
}

2.5 尾删和头删

//尾删和头删
void LTPopBack(Node* phead) {
  //链表必须有效且链表不能为空(只有一个哨兵位)
  assert(phead && phead->next!=phead);
  Node* del = phead->prev;
  Node* ptail = del->prev;
  ptail->next = phead;
  phead->prev = ptail;
  free(del);
  del = NULL;
}
void LTPopFront(Node* phead) {
  assert(phead && phead->next != phead);
  Node* del = phead->next;
  phead->next = del->next;
  del->next->prev = phead;
  free(del);
  del = NULL;
}

2.6 节点的找寻

方便在指定的节点前后进行相关操作

Node* LTFind(Node* phead, Datatype x) {
  Node* find = phead->next;
  while (find!=phead) {
    if (find->data == x)
      return find;
    else
    find = find->next;
  }
  return NULL;
}

2.7 指定节点处的操作

//在pos位置之后插⼊数据
void LTInsert(Node* pos, Datatype x) {
  assert(pos);
  Node* newnode = Buynode(x);
  newnode->next = pos->next;
  newnode->prev = pos;
  pos->next->prev = newnode;
  pos->next = newnode;
}
//删除指定节点
void LTErase(Node* pos) {
  assert(pos);
  pos->next->prev = pos->prev;
  pos->prev->next = pos->next;
  free(pos);
  pos = NULL;
}

2.8 链表的销毁

void LTDestroy(Node* phead) {
  assert(phead);
  Node* pcur = phead->next;
  while (pcur->next != phead) {
    Node* next = pcur->next;
    free(pcur);
    pcur = next;
  }
  free(phead);
  phead = NULL;
}

在实现相关函数时,都是从前后节点入手,改变next和prev的指向。

3. 链表的测试

#define _CRT_SECURE_NO_WARNINGS
#include "List.h"
void test1() {
  Node*plist=LTInit();
  //printf("%d", phead->data);
  //头插
  LTPushFront(plist, 1);
  LTPushFront(plist, 2);
  LTPrint(plist);
  //尾插
  LTPushBack(plist, 5);
  LTPushBack(plist, 3);
  //LTPrint(plist);
  
  //尾删
  //LTPopBack(plist, 3);
  //LTPopBack(plist, 5);
  //LTPrint(plist);
  //头删
  //LTPopFront(plist, 1);
  //LTPopFront(plist, 2);
  //LTPrint(plist);
  //节点查找
  Node* find = LTFind(plist, 5);
  if (find == NULL)
    printf("Not Found\n");
  else
    printf("找到了\n");
   //指定插入
  LTInsert(find, 66);
  LTPrint(plist);
   //指定节点删除
  LTErase(find);
  LTPrint(plist);
   //链表销毁
  LTDestroy(plist);
  plist = NULL;
}
int main() {
  test1();
  return 0;
}

3.顺序表和链表的优缺点对比(了解)

看完给小编留下点赞,关注加三连吧!!!

相关文章
|
Linux Docker 容器
.net Core WebApi发布到Docker并推送到阿里云容器服务
.net Core WebApi发布到Docker并推送到阿里云容器服务
1235 0
.net Core WebApi发布到Docker并推送到阿里云容器服务
|
12月前
|
传感器 算法 数据安全/隐私保护
基于PI控制算法的异步感应电机转速控制系统simulink建模与仿真
本课题研究基于PI控制算法的异步感应电机转速控制系统,利用Simulink建模与仿真。PI控制器结合比例与积分部分,实现快速响应和稳态误差消除。系统通过速度传感器反馈实际转速,经SPWM调制驱动电机,形成闭环控制。仿真中设置不同参考速度(如600-&gt;800、1500-&gt;2200等),验证系统性能。模型基于MATLAB 2022a开发,适用于电机高效稳定运行的研究与应用。
|
存储 测试技术
【数据结构】最最基础的链式结构——单链表,还不会你就吃大亏了!
【数据结构】最最基础的链式结构——单链表,还不会你就吃大亏了!
823 5
|
数据采集 Web App开发 iOS开发
如何使用 Python 语言的正则表达式进行网页数据的爬取?
使用 Python 进行网页数据爬取的步骤包括:1. 安装必要库(requests、re、bs4);2. 发送 HTTP 请求获取网页内容;3. 使用正则表达式提取数据;4. 数据清洗和处理;5. 循环遍历多个页面。通过这些步骤,可以高效地从网页中提取所需信息。
|
算法
简单的KMP的next、nextval数组求解办法(存档自己用来复习)
简单的KMP的next、nextval数组求解办法(存档自己用来复习)
1508 2
|
关系型数据库 MySQL 数据处理
针对MySQL亿级数据的高效插入策略与性能优化技巧
在处理MySQL亿级数据的高效插入和性能优化时,以上提到的策略和技巧可以显著提升数据处理速度,减少系统负担,并保持数据的稳定性和一致性。正确实施这些策略需要深入理解MySQL的工作原理和业务需求,以便做出最适合的配置调整。
1809 6
|
存储 算法 C语言
c语言实现跳表(skiplist)
本文介绍了跳表(Skip List)的数据结构及其实现,包括节点定义、创建、随机层数生成、插入、查找、打印和释放操作,展示了跳表作为一种高效有序序列查找、插入和删除操作的数据结构。
407 0
c语言实现跳表(skiplist)
链表的时间复杂度和空间复杂度
链表的时间复杂度和空间复杂度
1807 1
|
数据采集 前端开发 小程序
分享76个Python管理系统源代码总有一个是你想要的
分享76个Python管理系统源代码总有一个是你想要的
519 3
|
存储 缓存 C语言
FIFO基础知识
本文介绍了什么是FIFO,FIFO的用途、功能和重要参数。最后,利用C语言数组实现了FIFO,给出了详细的程序设计。
1414 0

热门文章

最新文章