DS:带头双向循环链表的实现

简介: DS:带头双向循环链表的实现

创作不易,友友们给个三连吧!!!

     博主的上篇文章介绍了链表,以及单链表的实现。

单链表的实现(超详细!!)

   其实单链表的全称叫做不带头单向不循环链表,本文会重点介绍链表的分类以及双链表的实现!

一、链表的分类

  链表的结构⾮常多样,组合起来就有8种(2 x 2 x 2)链表结构:

1.1 单向或者双向

   双向链表,即上一个结点保存着下一个结点的地址,且下一个结点保存着上一个结点的地址,即我们可以从头结点开始遍历,也可以从尾结点开始遍历

1.2 带头或者不带头

    单链表中我们提到的“头结点”的“头”和“带头”链表的头是两个概念!单链表中提到的“头结点”指的是第一个有效的结点,“带头”链表里的“头”指的是无效的结点(即不保存任何有效的数据!)

   

1.3 循环或者不循环

    不循环的链表最后一个结点的next指针指向NULL,而循环的链表,最后一个结点的next指针指向第一个结点!!

     虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构: 单链表(不带头单向不循环链表)和 双向链表(带头双向循环链表)

1. 无头单向非循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结 构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。

2. 带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带 来很多优势,实现反⽽简单了,后⾯我们代码实现了就知道了。

二、带头双向循环链表的结构

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

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

三、双向链表结点结构体的创建

    与单链表结点结构体不同的是,双向链表的结点结构体多了一个前驱结点!!

typedef int LTDataType;//对类型进行重命名,后面可以通过修改存储其他数据类型
typedef struct ListNode
{
  LTDataType data;//保存的数据
  struct ListNode* prev;//指针保存前一个结点的地址
  struct ListNode* next;//指针保存后一个结点的地址
}LTNode;

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

4.1 新结点的申请

     涉及到需要插入数据,都需要申请新节点,所以优先封装一个申请新结点的函数!利用返回值返回该结点

LTNode* LTBuyNode(LTDataType x)
{
  LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
  if (newnode == NULL)
  {
    perror("malloc fail");
    exit(1);//申请失败需要强制退出程序
  }
  //申请成功,则新节点的前驱结点和后驱结点都指向自己
  newnode->data = x;
  newnode->prev = newnode->next = newnode;
    return newnode;
}

4.2 初始化(哨兵位结点)

      对于双向链表来说,需要优先创建一个哨兵结点,和其他结点不同的是,该哨兵结点可以不存储数据,这里我们默认给他一个-1。并利用返回值返回该结点。

LTNode* LTInit()
{
  LTNode* phead = LTBuyNode(-1);//哨兵结点可以不存储数据,我们默认给个-1
  return phead;//返回哨兵结点
}

4.3 尾插

      如图,因为这个一个循环链表,相当于我们要把新节点插在最后一个结点和哨兵结点之间,并且最后一一个结点可以用哨兵结点的前驱结点(phead->prev)就可以找到,然后建立phead phead->prev newnode的联系!

void LTPushBack(LTNode* phead, LTDataType x)
{
  assert(phead);
  LTNode* newnode = LTBuyNode(x);//申请新节点
  //建立phead phead->prev newnode的联系
  newnode->prev = phead->prev;
  newnode->next = phead;
  phead->prev->next = newnode;//尾结点的后继指针指向新节点
  phead->prev = newnode;//哨兵结点的前驱指针指向新结点
}

单链表中我们的参数选择二级指针,为什么这里选择一级指针???

     对于单链表来收,单链表的头节点是会改变的,所以我们需要用二级指针,但是双链表的头节点相当于哨兵位,哨兵位是不需要被改变的,他是固定死的,所以我们选择了一级指针。(单链表改了完全头节点,但是双链表只会改变头结点的成员——prev和next)

注:phead->prev->next = newnode和phead->prev = newnode不能替换顺序,因为尾结点是通过头节点找到的,所以要优先让他与newnode建立联系,双链表虽然不需要像单链表一样找最后一个结点需要遍历链表,但是要十分注意修改指针指向的先后顺序!!

4.4 头插

      如图可知,相当于将新节点插入在头节点和头节点下一个结点之间,头节点下一个结点可以通过phead->next找到,然后建立phead、phead->next、newnode的联系!!

void LTPushFront(LTNode* phead, LTDataType x)
{
  assert(phead);
  LTNode* newnode = LTBuyNode(x);//申请新节点
  //建立phead phead->next newnode的联系
  newnode->prev = phead;
  newnode->next = phead->next;
  phead->next->prev = newnode;//头节点的下一个结点的前驱指针指向新结点
  phead->next = newnode;//头节点的后继指针指向新节点
}

4.5 打印

     因为是循环链表,所以为了避免死循环打印,我们要设置一个指针接收头节点的下一个结点,然后往后遍历,直到遍历到头节点结束。

void LTPrint(LTNode* phead)
{
  assert(phead);
  LTNode* pcur = phead->next;
  while (pcur != phead)
  {
    printf("%d->", pcur->data);
    pcur = pcur->next;
  }
  printf("\n");
}

4.6 尾删

     由图可知,要建立phead和phead->prev->prev的联系,同时由于还要释放最后一个结点(phead->prev),所以在建立联系之前要现保存这个被释放的空间,等建立联系完再释放!!同时要注意一条规则,就是当链表中只有哨兵结点的时候,我们称该链表为空链表!因此如果链表只存在哨兵结点,那么删除是没有意义的,所以必须断言!

void LTPopBack(LTNode* phead)
{
  assert(phead);
  assert(phead->next != phead);//链表只有哨兵结点时删除没意义
  LTNode* del = phead->prev;//del记录最后一个结点
  del->prev->next = phead;//倒数第二个结点的后驱指针指向头结点
  phead->prev = del->prev;//头节点的前驱结点指向倒数第二个结点
  free(del);//释放最后一个结点
  del = NULL;
}

4.7 头删

      由图可知,要建立phead和phead->next->next的联系,同时由于还要释放第二个结点(phead->next),所以在建立联系之前要现保存这个被释放的空间,等建立联系完再释放!!

void LTPopFront(LTNode* phead)
{
  assert(phead);
  assert(phead->next != phead);//链表只有哨兵结点时删除没意义
  LTNode* del = phead->next;//del记录第二个结点
  del->next->prev = phead;//第二个结点的前驱指针指向头结点
  phead->next = del->next;//头节点的后驱指针指向第三个结点
  free(del);//释放第二个结点
  del = NULL;
}

4.8 查找

    涉及到对指定位置进行操作的时候,需要设置一个查找函数,根据我们需要的数据返回他的结点地址

LTNode* LTFind(LTNode* phead, LTDataType x)
{
  assert(phead);
  LTNode* pcur = phead->next;
  while (pcur != phead)//遍历链表
  {
    if (pcur->data == x)
      return pcur;//找到的话返回该结点
    pcur = pcur->next;
  }
  //循环结束还是没找到
  return NULL;
}

4.9 指定位置之后插入

       由图可知,指定位置插入相当于将新结点插入到指定位置(pos)和指定位置下一个结点的位置(pos->next),然后建立pos pos->next newnode的联系,而且这里用不到头节点!

void LTInsert(LTNode* pos, LTDataType x)
{
  assert(pos);//保证pos为有效结点
  LTNode* newnode = LTBuyNode(x);//申请新节点
  //建立pos pos->next newnode的联系
  newnode->prev = pos;
  newnode->next = pos->next;
  pos->next->prev = newnode;//pos的后一个结点的前驱结点指向新节点
  pos->next = newnode;//pos结点的后继结点指向新结点
}

4.10 指定位置删除

      右图可知建立指定位置的前一个结点(pos->prev)和指定位置的后一个结点(pos->next)的联系,并释放pos。

void LTErase(LTNode* pos)
{
  assert(pos);//保证pos为有效结点
  pos->prev->next = pos->next;//pos的前一个结点的后继指针指向pos后一个结点
  pos->next->prev = pos->prev;//pos的后一个结点的前驱指针指向pos的前一个结点
  free(pos);//释放pos
  pos = NULL;
}

4.11 销毁链表

void LTDestroy(LTNode* phead)
{
  assert(phead);
  LTNode* pcur = phead->next;
  LTNode*next = NULL;
  while (pcur != phead)
  {
    next = pcur->next;
    free(pcur);
    pcur = next;
  }
  //除了头结点都释放完毕
  free(phead);
  //phead = NULL;//没有用!
}

为什么phead=NULL没有用??

      因为我们使用的是一级指针,这里相当于是值传递,值传递形参改变不了实参,所以将phead置空是没有意义的,其实如果这里使用二级指针,然后传地址就可以了,但是为了保持接口一致性,我们还是依照这种方法,但是phead=NULL必须在主函数中去使用,所以我们在调用销毁链表的函数的时候,别忘记了phead=NULL!!

五、带头双向循环链表实现的全部代码

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;//指针保存后一个结点的地址
}LTNode;
LTNode* LTBuyNode(LTDataType x);//申请新的链表结点
LTNode* LTInit();//初始化(申请一个哨兵结点)
void LTPushBack(LTNode* phead, LTDataType x);//尾插 (最后一个结点后插入或哨兵结点前插入)
void LTPushFront(LTNode* phead, LTDataType x);//头插 (哨兵结点后的插入)
void LTPrint(LTNode* phead);//打印
void LTPopBack(LTNode* phead);//尾删
void LTPopFront(LTNode* phead);//头删
LTNode* LTFind(LTNode* phead, LTDataType x);//查找
void LTInsert(LTNode* pos, LTDataType x);//指定位置之后插入
void LTErase(LTNode* pos);//指定位置删除
void LTDestroy(LTNode* phead);//销毁链表

List.c

#include"List.h"
LTNode* LTBuyNode(LTDataType x)
{
  LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
  if (newnode == NULL)
  {
    perror("malloc fail");
    exit(1);//申请失败需要强制退出程序
  }
  //申请成功,则新节点的前驱结点和后驱结点都指向自己
  newnode->data = x;
  newnode->prev = newnode->next = newnode;
  return newnode;
}
LTNode* LTInit()
{
  LTNode* phead = LTBuyNode(-1);//哨兵结点可以不存储数据,我们默认给个-1
  return phead;//返回哨兵结点
}
void LTPushBack(LTNode* phead, LTDataType x)
{
  assert(phead);
  LTNode* newnode = LTBuyNode(x);//申请新节点
  //建立phead phead->prev newnode的联系
  newnode->prev = phead->prev;
  newnode->next = phead;
  phead->prev->next = newnode;//尾结点的后继结点指向新节点
  phead->prev = newnode;//哨兵结点的前驱指针指向新结点
}
void LTPushFront(LTNode* phead, LTDataType x)
{
  assert(phead);
  LTNode* newnode = LTBuyNode(x);//申请新节点
  //建立phead phead->next newnode的联系
  newnode->prev = phead;
  newnode->next = phead->next;
  phead->next->prev = newnode;//头节点的下一个结点的前驱指针指向新结点
  phead->next = newnode;//头节点的后继指针指向新节点
}
void LTPrint(LTNode* phead)
{
  assert(phead);
  LTNode* pcur = phead->next;
  while (pcur != phead)
  {
    printf("%d->", pcur->data);
    pcur = pcur->next;
  }
  printf("\n");
}
void LTPopBack(LTNode* phead)
{
  assert(phead);
  assert(phead->next != phead);//链表只有哨兵结点时删除没意义
  LTNode* del = phead->prev;//del记录最后一个结点
  del->prev->next = phead;//倒数第二个结点的后驱指针指向头结点
  phead->prev = del->prev;//头节点的前驱结点指向倒数第二个结点
  free(del);//释放最后一个结点
  del = NULL;
}
void LTPopFront(LTNode* phead)
{
  assert(phead);
  assert(phead->next != phead);//链表只有哨兵结点时删除没意义
  LTNode* del = phead->next;//del记录第二个结点
  del->next->prev = phead;//第二个结点的前驱指针指向头结点
  phead->next = del->next;//头节点的后驱指针指向第三个结点
  free(del);//释放第二个结点
  del = NULL;
}
LTNode* LTFind(LTNode* phead, LTDataType x)
{
  assert(phead);
  LTNode* pcur = phead->next;
  while (pcur != phead)//遍历链表
  {
    if (pcur->data == x)
      return pcur;//找到的话返回该结点
    pcur = pcur->next;
  }
  //循环结束还是没找到
  return NULL;
}
void LTInsert(LTNode* pos, LTDataType x)
{
  assert(pos);//保证pos为有效结点
  LTNode* newnode = LTBuyNode(x);//申请新节点
  //建立pos pos->next newnode的联系
  newnode->prev = pos;
  newnode->next = pos->next;
  pos->next->prev = newnode;//pos的后一个结点的前驱结点指向新节点
  pos->next = newnode;//pos结点的后继结点指向新结点
}
void LTErase(LTNode* pos)
{
  assert(pos);//保证pos为有效结点
  pos->prev->next = pos->next;//pos的前一个结点的后继指针指向pos后一个结点
  pos->next->prev = pos->prev;//pos的后一个结点的前驱指针指向pos的前一个结点
  free(pos);//释放pos
  pos = NULL;
}
void LTDestroy(LTNode* phead)
{
  assert(phead);
  LTNode* pcur = phead->next;
  LTNode*next = NULL;
  while (pcur != phead)
  {
    next = pcur->next;
    free(pcur);
    pcur = next;
  }
  //除了头结点都释放完毕
  free(phead);
  //phead = NULL;//没有用!
}

六、顺序表和链表的优缺点分析

1、存储空间

顺序表物理上连续

链表逻辑上连续,但是物理上不连续

2、随机访问

顺序表可以通过下标去访问

链表不可以直接通过下标去访问

3、任意位置插入或者删除元素

顺序表需要挪移元素,效率低

链表只需修改指针指向

4、插入

动态顺序表空间不够时需要扩容

链表没有容量的概念

5、应用场景

顺序表应用于元素高效存储+频繁访问的场景

链表应用于任意位置插入和删除频繁的场景

总之:没有绝对的优劣,都要各自适合的应用场景!!

相关文章
|
6月前
|
存储 编译器
DS:单链表的实现
DS:单链表的实现
|
存储 Java
【DS】链表的介绍和实现(单/双链表)
【DS】链表的介绍和实现(单/双链表)
96 0
【DS】链表的介绍和实现(单/双链表)
|
存储 机器学习/深度学习 缓存
【算法社区】从零开始的DS生活 轻松从0基础写出链表LRU算法
本文从ds概念说起,详细介绍了顺序表(数组)和链表的相关知识与源码解析,并配合LRU链表实战,文内有大量练习,适合点赞+收藏。有什么错误希望大家直接指出~
【算法社区】从零开始的DS生活 轻松从0基础写出链表LRU算法
|
存储 缓存 程序员
|
5月前
|
存储 SQL 算法
LeetCode力扣第114题:多种算法实现 将二叉树展开为链表
LeetCode力扣第114题:多种算法实现 将二叉树展开为链表
|
5月前
|
存储 SQL 算法
LeetCode 题目 86:分隔链表
LeetCode 题目 86:分隔链表
|
5月前
|
存储 算法 Java
【经典算法】Leetcode 141. 环形链表(Java/C/Python3实现含注释说明,Easy)
【经典算法】Leetcode 141. 环形链表(Java/C/Python3实现含注释说明,Easy)
56 2
|
6月前
<数据结构>五道LeetCode链表题分析.环形链表,反转链表,合并链表,找中间节点.
<数据结构>五道LeetCode链表题分析.环形链表,反转链表,合并链表,找中间节点
53 1