数据结构之带头循环双向链表

简介: 数据结构之带头循环双向链表

在学习了单链表之后,我们发现单链表弥补了了顺序表的一些缺点,同时链表的结构体繁多,仔细计算链表的结构总共有8种,分为单链表与双链表,带头结点与不带头结点,循环与非循环。对于单链表,我们发现其自身也存在了一些问题

1.无法从后往前走

2.数据操作时间复杂度较高

3.无法找到结点的前驱

为了解决单链表的不足,我们引入的双链表的学习。

对于单向不带头不循环链表:结构简单,一般不会单独用来存放数据。实际中更多是用作其他数据结构的子结构,,如哈希桶,图的邻接表等等,另外这种结构在笔试面试中更多。

7933cf5bbbc0407a9714d1629a9e3166.png

对于双向带头循环链表:结构最复杂,一般用于单独存放数据。实际中使用的链表数据结构都是带头双向循环链表。另外这个结构索然复杂,但是用代码实现起来会发现其结构有更大的优势,实现反而简单。

e46507bb93f94259b1e135c6f50d29bb.png

因为双向带头循环链表更适合我们实际中保存数据,所以我们今天就来学一下带头循环双向链表

1.何为双链表?

顾名思义,在单链表的基础上又添加了一个指针,与单链表中的next指针效果一样,对于链表的物理模型不同的是,这里的指针与next是反向的。双链表完善了单链表的一些缺陷,在数据操作的时候也大大节省了时间。

ba9a4413ac654f2d9af8754b5aa2bb12.png

2.带头循环双向链表

cac69b86a80b447fae720cf3a9172a26.png

所谓带头循环双向链表就是在双链表的基础上添加了哨兵位,且尾节点的next->头节点,头结点的prev->指向尾节点,形成循环。此链表的结构看似复杂,但设计巧妙,我们在实现数据的一些操作功能时,巧妙的避免了许多麻烦,其次,不在考虑头节点为空的情况,省去了很多步骤。

1.函数接口与结构体

结构体的定义还是与双链表一样,其数据操作的函数接口也是一样的。

typedef int ST;
typedef struct ListNode
{
  struct ListNode* prev;
  ST data;
  struct ListNode* next;
}ListNode,*List;
ListNode* Listinit();//初始化
void Listdestroy(ListNode* phead);//销毁链表
void printlist(ListNode* phead);//打印链表
void pushback(ListNode* phead, int x);//尾插
void popback(ListNode* phead);//尾删
void pushfront(ListNode* phead, int x);//头插
void popfront(ListNode* phead);//头删
ListNode* findNode(ListNode* phead, ST x);//查找
void insertNode(ListNode* pos, ST x);//pos位置前插入
void EraseNode(ListNode* pos);//删除pos位置的值

2.初始化链表

因为我们需要的是带头循环双向链表,故初始化时,我们给定义的空链表初始化一个哨兵位,这里我们也不会用到哨兵位的数据,直接置为0.

ListNode*Listinit()
{
  //初始化给一个哨兵节点
     struct ListNode*phead = creatNode(0);
    phead->next =phead;
    phead->prev = phead;
    return phead;
}

3.销毁链表

销毁链表时都要删除包括哨兵位,所以我们直接从哨兵位开始遍历:

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

4.打印链表

还是很简单,这里需要注意的是在遍历链表时,循环中,phead->next==phead就结束。

d44d1baa9d374a2d92a4f2d0c0f04006.png

void printlist(ListNode* phead)
{
  ListNode* cur = phead->next;
  while (cur != phead)
  {
    printf("%d->", cur->data);
    cur = cur->next;
  }
  printf("\n");
}

5.创建节点

该步骤只是为了后续操作方便。

ListNode* creatNode(ST x)//创建新节点
{
  ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
  if (newnode == NULL)
  {
    return NULL;
  }
  else
  {
     newnode->next = NULL;
   newnode->prev = NULL;
   newnode->data = x;
   return newnode;
  }
}

6.尾插

f810f69e9f764088a4196121ec12b99e.png

需要注意的一点是某个操作若与某些节点有关,我们可以先找到并保存这些节点,之后再惊醒操作,思路会更加清晰一些。

void pushback(ListNode* phead, int x)
{
  //先保存尾节点
  ListNode* tail = phead->prev;
  ListNode* newnode = creatNode(x);
  //链接
  tail->next = newnode;
  newnode->prev = tail;
  newnode->next = phead;
  phead->prev = newnode;
}

7.尾删

尾删需要注意的是链表传过来除了哨兵位,还有其它节点。


631268623f504a53ba4b4d9191b334d4.png

void popback(ListNode* phead)
{
  assert(phead);
  assert(phead->next);
  ListNode* oldtail = phead -> prev;
  ListNode* newtail = oldtail->prev;
  newtail->next = phead;
  phead->prev = newtail;
  free(oldtail);
  oldtail = NULL;
}

8.头插

8f299d9356b742ac82ae2a8ea2319fb9.png

void pushfront(ListNode* phead, int x)
{
  assert(phead);
  /*保存头节点地址*/
  ListNode* first = phead->next;
  ListNode* newnode = creatNode(x);
    newnode->next = first;
  first->prev = newnode;
  phead->next = newnode;
  newnode->prev = phead;
  //若没有保存头节点,在头插时应注意连接顺序。
  //先链接newnode与phead->next的节点
  /*ListNode* newnode = creatNode(x);
  phead->next->prev = newnode;
  newnode->next = phead->next;
  phead->next = newnode;
  newnode->prev = phead;*/
}

我们可以发现,根据连接顺序的不同,我们可以选择是否保存需要链接的哪些节点,否则就要注意链接的顺序,比如说在头插时,不保存头结点时,先与头节点链接,在与哨兵位节点链接,否则先与哨兵位链接,我们就会丢失头节点,找不到头结点。

9.头删

所谓头删原理与之前的一样,先保存哨兵位节点与头节点的下一个节点,之后链接哨兵位节点与头结点的下一个节点,之后再free掉头节点。

fd4fba9ec9234812b359a4a8a13ea51b.png

void popfront(ListNode* phead)
{
  assert(phead);
  if (phead->next==NULL)
  {
    printf("无元素可删\n");
    assert(phead);
  }
  ListNode* newhead = phead->next->next;
  ListNode* oldhead = phead->next;
  phead->next = newhead;
  newhead->prev = phead;
  free(oldhead);
  oldhead= NULL;
}

10 查找节点

查找节点与打印链表的遍历方式相同,也很简单

ListNode* findNode(ListNode* phead, ST x)
{
  assert(phead);
  ListNode* cur = phead->next;
  while (phead != cur)
  {
    if (cur->data == x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return NULL;
}

11.在pos前插入x

在这里,我们可以通过查找函数找到pos节点,之后在插入函数里传入pos节点。

插入的思路与之前的一样,先保存pos节点与pos前的节点,再链接新节点与pos,链接新节点与pos前的节点,比如插到d2前:

19f3db336894437a8af12561b9793373.png

void insertNode(ListNode* pos, ST x)
{
  ListNode* newnode = creatNode(x);
  ListNode* front = pos->prev;
  ListNode* cur = pos;
  newnode->next = cur;
  cur->prev = newnode;
  front->next = newnode;
  newnode->prev = front;
}

在调用时:

int main()
{
  ListNode* p = NULL;
  p=Listinit();
  pushback(p, 1);
  pushback(p, 2);
  pushfront(p,4);
  ListNode* pos = findNode(p, 2);
  if (pos)
  {
    printf("找到了\n");
  }
  else
  {
    printf("未找到");
  }
  insertNode(pos, 3);
     return 0;
}

12.删除pos位置的值

还是一样,找到pos前后位置的两个节点,连接pos前后两个节点,在释放pos处的节点,完成删除。

void EraseNode(ListNode* pos)
{
  assert(pos);
  ListNode* tmp = pos;
  ListNode* cur = pos->next;
  ListNode* front = pos->prev;
  front->next = cur;
  cur->prev = front;
  free(tmp);
  tmp = NULL;
}

总结:带头循环双向链表,因为其独特的结构,使得我们在编译代码时省去了很多麻烦,在插入时不用判断头节点是否为空,在删除时,不用判断当只有一个节点时,是否是尾删。因为其循环的特性,判断也很方便/

相关文章
|
5天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
26 4
|
6天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
6天前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
26天前
|
存储 Java
数据结构第三篇【链表的相关知识点一及在线OJ习题】
数据结构第三篇【链表的相关知识点一及在线OJ习题】
23 7
|
27天前
|
存储 安全 Java
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
21 3
|
25天前
|
算法 Java
数据结构与算法学习五:双链表的增、删、改、查
双链表的增、删、改、查操作及其Java实现,并通过实例演示了双向链表的优势和应用。
15 0
数据结构与算法学习五:双链表的增、删、改、查
|
5天前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
20 0
【数据结构】——双向链表详细理解和实现
【数据结构】——双向链表详细理解和实现
|
30天前
|
存储 Java
【数据结构】链表
【数据结构】链表
16 1
|
19天前
|
存储
[数据结构] -- 双向循环链表
[数据结构] -- 双向循环链表
17 0