数据结构-双向链表

简介: 数据结构-双向链表

1.带头双向循环链表:

前面我们已经知道了链表的结构有8种,我们主要学习下面两种:

前面我们已经学习了无头单向非循环链表,今天我们来学习带头双向循环链表:

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

带头双向循环链表不需要二级指针,因为我们刚开始就为其开辟了一个节点,叫做哨兵位头节点,它是结构体中的指针,用结构体指针就能改变它,而要改变结构体外面的指针才会用二级指针。

双向循环链表顾名思义,除了哨兵位头节点以外,每个节点里面应该有两个指针,下面我们定义一个结构体:

typedef int ListDatatype;
typedef struct ListNode
{
  struct ListNode* prev;
  struct ListNode* next;
  ListDatatype data;
}LTNode;

prev指向前一个节点,next指向后一个节点。

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

双向链表初始化:

LTNode* InitList()
{
  LTNode* phead = BuyList(-1);
  phead->next = phead;
  phead->prev = phead;
  return phead;
}

双向循环链表开始时头和尾都指向自己:

BuyList函数的功能是创建节点,我们在初始化时用它创建哨兵位头节点,因为后面还有多次使用,所以把它封装为函数。

双向链表打印:

void Print(LTNode* phead)
{
  LTNode*cur = phead->next;
  printf("guard<->");
  while (cur != phead)
  {
    printf("%d<->", cur->data);
    cur = cur->next;
  }
  printf("\n");
}

开辟节点函数:

LTNode* BuyList(ListDatatype x)
{
  LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
  if (newnode == NULL)
  {
    perror("malloc fail");
    return NULL;
  }
  newnode->prev = NULL;
  newnode->next = NULL;
  newnode->data = x;
  return newnode;
}

双向链表头插:

头插实际是在哨兵位头节点phead的后面插入,保存住head->next的位置,然后把newnode前后分别和head、head->next链接起来就行。

代码如下:

void ListPushFront(LTNode* phead, ListDatatype x)
{
  assert(phead);
  LTNode* newnode = BuyList(x);
  LTNode* next = phead->next;
  phead->next = newnode;
  next->prev = newnode;
  newnode->next = next;
  newnode->prev = phead;
}

这段代码神奇的地方在于,即使链表为空,它也能头插,并且不需要判断链表是否为空

因为就算链表为空,我们有哨兵位头节点存在,就不用担心空指针的问题。

双向链表尾插:

双向链表相对于单链表的优势就是不用找尾,因为它的phead->prev就是尾,尾插同头插差不多, 把newnode的前后分别和链表的尾和头链接起来即可。

代码如下:

void ListPushBack(LTNode* phead, ListDatatype x)
{
  assert(phead);
  LTNode* newnode = BuyList(x);
  LTNode* tail = phead->prev;
  tail->next = newnode;
  phead->prev = newnode;
  newnode->prev = tail;
  newnode->next = phead;
}

和头插一样,尾插也不用判断链表为空的情况。

双向链表头删:

头删指的是删除哨兵位头节点后面一个节点,只要将头节点与要删除的节点后面的节点相连接,然后free掉要删除的节点即可。

代码如下:

void ListPopFront(LTNode* phead)
{
  assert(phead);
  assert(!ListEmpty(phead));
  LTNode* cur = phead;
  LTNode* first = cur->next;
  LTNode* second = first->next;
  second->prev = phead;
  phead->next = second;
  free(first);
}

删除时要注意不能删除哨兵位头节点,所以要断言一下,链表为空就不能再删了,我们也可以封装一个判断链表是否为空的函数ListEmpty():

bool ListEmpty(LTNode* phead)
{
  assert(phead);
  return phead->next == phead;
}

bool类型的返回值是true或者false。

双向链表尾删:

尾删要保存尾节点的前一个节点,然后把前一个节点和头节点链接起来,free尾节点即可。

代码如下:

void ListPopBack(LTNode* phead)
{
  assert(phead);
  assert(!ListEmpty(phead));
  LTNode* tail = phead->prev;
  LTNode* tailPrev = tail->prev;
  tailPrev->next = phead;
  phead->prev = tailPrev;
  free(tail);
}

注意:同头删一样,尾删也要判断是否为空链表。

双向链表查找:

双向循环链表的查找和单链表的查找不同,遍历时从head的下一个节点开始,到head的上一个节点(即尾节点)结束,所以判断条件有所不同,注意区分。找到时,返回该节点位置。

代码如下:

LTNode* ListSearch(LTNode*phead,ListDatatype x)
{
  assert(phead);
  LTNode* cur = phead->next;
  while (cur != phead)
  {
    if (cur->data == x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return NULL;
}

双向链表在pos之前插入:

保存pos的前一个节点,把newnode的前后分别与pos的前一个节点和pos链接起来即可。

要测试该功能,可以配合查找函数,先找到pos。

代码如下:

void ListInsert(LTNode* pos, ListDatatype x)
{
  assert(pos);
  LTNode* newnode = BuyList(x);
  LTNode* posPrev = pos->prev;
  newnode->next = pos;
  newnode->prev = posPrev;
  posPrev->next = newnode;
  pos->prev = newnode;
}

这段代码可以直接在头插和尾插中复用,也就是说,我们要实现头插、尾插和任意位置插入,只用这一个函数就可以解决。

头插:

ListInsert(phead->next, x);

尾插:

ListInsert(phead, x);

双向链表在pos位置删除:

配合查找函数先找到pos位置,然后删除就行。

代码如下:

void ListErase(LTNode* pos)
{
  assert(pos);
  LTNode* posPrev = pos->prev;
  LTNode* posNext = pos->next;
  posPrev->next = posNext;
  posNext->prev = posPrev;
  free(pos);
}

这段代码也可以同时实现头删、尾删和任意位置删除:

头删:

 

ListErase(phead->next);

尾删:

ListErase(phead->prev);

双向链表的销毁:

销毁也是从哨兵位的下一个节点开始,注意每次都要保存要销毁节点的后面一个节点的位置,防止找不到后面的节点,最终要把哨兵位也销毁掉。

代码如下:

void ListDestory(LTNode* phead)
{
  assert(phead);
  LTNode* cur = phead->next;
  while (cur != phead)
  {
    LTNode* next = cur->next;
    free(cur);
    cur = next;
  }
  free(phead);
}

以上就是双向链表的全部功能实现,下面给出完整代码:

3.完整代码:

test.c

#define  _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
//测试
ListTest1()
{
  LTNode* plist = InitList();
  Print(plist);
  //头插
  ListPushFront(plist, 1);
  ListPushFront(plist, 2);
  ListPushFront(plist, 3);
  ListPushFront(plist, 4);
  Print(plist);
  //尾插
  ListPushBack(plist, 5);
  ListPushBack(plist, 6);
  ListPushBack(plist, 7);
  ListPushBack(plist, 8);
  Print(plist);
  //头删
  ListPopFront(plist);
  ListPopFront(plist);
  Print(plist);
  //尾删
  ListPopBack(plist);
  ListPopBack(plist);
  Print(plist);
  //在pos位置之前插入
  LTNode* pos = ListSearch(plist, 1);
  if (pos != NULL)
    ListInsert(pos, 666);
  Print(plist);
  //在pos位置删除
  pos = ListSearch(plist, 6);
  if(pos!=NULL)
    ListErase(pos);
  Print(plist);
}
int main()
{
  ListTest1();
  return 0;
}

List.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int ListDatatype;
typedef struct ListNode
{
  struct ListNode* prev;
  struct ListNode* next;
  ListDatatype data;
}LTNode;
//双向链表初始化
LTNode* InitList();
//双向链表打印
void Print(LTNode* phead);
//双向链表头插
void ListPushFront(LTNode* phead, ListDatatype x);
//双向链表尾插
void ListPushBack(LTNode* phead, ListDatatype x);
//双向链表头删
void ListPopFront(LTNode* phead);
//双向链表尾删
void ListPopBack(LTNode* phead);
//双向链表查找
LTNode* ListSearch(LTNode*phead,ListDatatype x);
//在pos位置之前插入
void ListInsert(LTNode*pos, ListDatatype x);
//在pos位置删除
void ListErase(LTNode* pos);
//销毁链表
void ListDestory(LTNode* phead);

List.c

#define  _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
//开辟节点函数
LTNode* BuyList(ListDatatype x)
{
  LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
  if (newnode == NULL)
  {
    perror("malloc fail");
    return NULL;
  }
  newnode->prev = NULL;
  newnode->next = NULL;
  newnode->data = x;
  return newnode;
}
//双向链表初始化
LTNode* InitList()
{
  LTNode* phead = BuyList(-1);
  phead->next = phead;
  phead->prev = phead;
  return phead;
}
//打印函数
void Print(LTNode* phead)
{
  LTNode*cur = phead->next;
  printf("guard<->");
  while (cur != phead)
  {
    printf("%d<->", cur->data);
    cur = cur->next;
  }
  printf("\n");
}
//双向链表头插
void ListPushFront(LTNode* phead, ListDatatype x)
{
  assert(phead);
  LTNode* newnode = BuyList(x);
  LTNode* next = phead->next;
  phead->next = newnode;
  next->prev = newnode;
  newnode->next = next;
  newnode->prev = phead;
  /*ListInsert(phead->next, x);*/
}
//双向链表尾插
void ListPushBack(LTNode* phead, ListDatatype x)
{
  assert(phead);
  LTNode* newnode = BuyList(x);
  LTNode* tail = phead->prev;
  tail->next = newnode;
  phead->prev = newnode;
  newnode->prev = tail;
  newnode->next = phead;
  /*ListInsert(phead, x);*/
}
//判断空链表函数
bool ListEmpty(LTNode* phead)
{
  assert(phead);
  return phead->next == phead;
}
//双向链表头删
void ListPopFront(LTNode* phead)
{
  assert(phead);
  assert(!ListEmpty(phead));
  LTNode* cur = phead;
  LTNode* first = cur->next;
  LTNode* second = first->next;
  second->prev = phead;
  phead->next = second;
  free(first);
  /*ListErase(phead->next);*/
}
//双向链表尾删
void ListPopBack(LTNode* phead)
{
  assert(phead);
  assert(!ListEmpty(phead));
  LTNode* tail = phead->prev;
  LTNode* tailPrev = tail->prev;
  tailPrev->next = phead;
  phead->prev = tailPrev;
  free(tail);
  /*ListErase(phead->prev);*/
}
//双向链表查找
LTNode* ListSearch(LTNode*phead,ListDatatype x)
{
  assert(phead);
  LTNode* cur = phead->next;
  while (cur != phead)
  {
    if (cur->data == x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return NULL;
}
//双向链表在pos之前插入
void ListInsert(LTNode* pos, ListDatatype x)
{
  assert(pos);
  LTNode* newnode = BuyList(x);
  LTNode* posPrev = pos->prev;
  newnode->next = pos;
  newnode->prev = posPrev;
  posPrev->next = newnode;
  pos->prev = newnode;
}
//双向链表在pos位置删除
void ListErase(LTNode* pos)
{
  assert(pos);
  LTNode* posPrev = pos->prev;
  LTNode* posNext = pos->next;
  posPrev->next = posNext;
  posNext->prev = posPrev;
  free(pos);
}
//双向链表销毁
void ListDestory(LTNode* phead)
{
  assert(phead);
  LTNode* cur = phead->next;
  while (cur != phead)
  {
    LTNode* next = cur->next;
    free(cur);
    cur = next;
  }
  free(phead);
}

4.测试:

5.顺序表和链表的区别

不同点  顺序表 链表
存储空间上  物理上一定连续  逻辑上连续,但物理上不一定连续
随机访问 支持O(1)  不支持:O(N)
任意位置插入或者删除元
可能需要搬移元素,效率低O(N) 只需修改指针指向
插入  动态顺序表,空间不够时需要扩
没有容量的概念
应用场景 元素高效存储+频繁访问 任意位置插入和删除频繁
缓存利用率

总结一下:

下面我们再来补充一些内容:

这里有个问题,在计算机中使用顺序表效率高还是使用链表效率高呢?

答案是:顺序表。

因为在计算机中,由于运行速度不匹配的问题,CPU不会直接和主存交换数据,而是先把数据从主存中取出来放到高速缓存中,然后再进行访问数据,而访问数据会出现两种情况:

1.如果数据在缓存中,就叫做缓存命中,可以直接访问。

2.如果数据不在缓存中,就叫做缓存不命中,这时候需要先把数据加载到缓存中,然后再访问数据

当缓存不命中时,计算机会把数据加载到缓存中,而加载时会将这个数据后面的数据也一起加载进去(局部性原理),如果是顺序表,因为它的内存空间是连续的,后面的数据会直接命中,这样它的缓存命中率就高;如果是链表,它一旦命中不了,也会加载一段数据,但是这些数据不一定会用,这就造成了浪费,还会导致数据污染,这样它的缓存命中率就低了。

这就是今天关于双向链表的全部内容了,未完待续。。。

成屿
+关注
目录
打赏
0
0
1
0
3
分享
相关文章
|
4月前
|
数据结构实验之链表
本实验旨在掌握线性表中元素的前驱、后续概念及链表的建立、插入、删除等算法,并分析时间复杂度,理解链表特点。实验内容包括循环链表应用(约瑟夫回环问题)、删除单链表中重复节点及双向循环链表的设计与实现。通过编程实践,加深对链表数据结构的理解和应用能力。
88 4
C 408—《数据结构》算法题基础篇—链表(下)
408考研——《数据结构》算法题基础篇之链表(下)。
88 29
C 408—《数据结构》算法题基础篇—链表(上)
408考研——《数据结构》算法题基础篇之链表(上)。
99 25
【C++数据结构——线性表】单链表的基本运算(头歌实践教学平台习题)【合集】
本内容介绍了单链表的基本运算任务,涵盖线性表的基本概念、初始化、销毁、判定是否为空表、求长度、输出、求元素值、按元素值查找、插入和删除数据元素等操作。通过C++代码示例详细解释了顺序表和链表的实现方法,并提供了测试说明、通 - **任务描述**:实现单链表的基本运算。 - **相关知识**:包括线性表的概念、初始化、销毁、判断空表、求长度、输出、求元素值、查找、插入和删除等操作。 - **测试说明**:平台会对你编写的代码进行测试,提供测试输入和预期输出。 - **通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了测试通过后的预期输出结果。 开始你的任务吧,祝你成功!
71 5
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
105 5
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
197 4
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
4月前
|
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
77 0
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等