【数据结构与算法】双链表&循环链表&静态链表

深碍√TFBOYSˉ_ 2024-04-26 02:19 161阅读 0赞

? 本文由 程序喵正在路上 原创,CSDN首发!
? 系列专栏:数据结构与算法
? 首发时间:2022年9月22日
? 欢迎关注?点赞?收藏?留言?
? 一以贯之的努力 不得懈怠的人生

阅读指南

  • 单链表 VS 双链表
  • 双链表
    • 双链表的初始化(带头结点)
    • 双链表的插入
    • 双链表的删除
    • 双链表的遍历
  • 循环单链表
  • 循环双链表
    • 循环双链表的初始化
    • 循环双链表的插入
    • 循环双链表的删除
  • 静态链表
    • 什么是静态链表
    • 定义静态链表
    • 基本操作的实现
      • 初始化
      • 查找
      • 插入位序为 i 的结点
      • 删除某个结点
  • 顺序表和链表的比较

单链表 VS 双链表

我们都知道,单链表只有一个指向下一个结点的指针,当我们想要找到前一个结点时就比较麻烦,而双链表拥有两个指针

总的来说:

  • 单链表 —— 无法逆向检索,有时候不太方便
  • 双链表 —— 可进可退,存储密度更低一丢丢

定义双链表结点类型

  1. typedef struct DNode{
  2. ElemType data; //数据域
  3. struct DNode *prior, *next; //前驱和后继指针
  4. }DNode, *DLinklist;

双链表

双链表的初始化(带头结点)

定义一个 InitLinklist 函数,参数为双链表的引用,加引用是因为要改变这个双链表

注意:头结点的前驱指针永远指向 NULL

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. typedef int ElemType;
  4. typedef struct DNode{
  5. ElemType data; //数据域
  6. struct DNode *prior, *next; //前驱和后继指针
  7. }DNode, *DLinklist;
  8. //初始化双链表
  9. bool InitLinklist(DLinklist &L) {
  10. L = (DNode *)malloc(sizeof(DNode)); //分配一个头结点
  11. if (L == NULL) return false; //内存不足,分配失败
  12. L->prior = NULL; //头结点的 prior 永远指向 NULL
  13. L->next = NULL; //头结点之后暂时还没有结点
  14. return true;
  15. }
  16. //判断双链表是否为空(带头结点)
  17. bool Empty(DLinklist L) {
  18. if (L->next == NULL)
  19. return true;
  20. else
  21. return false;
  22. }
  23. void testDLinklist() {
  24. //初始化双链表
  25. DLinklist L;
  26. InitLinklist(L);
  27. }

双链表的插入

后插法

  1. //在p结点之后插入s结点
  2. bool InsertNextDNode(DNode *p, DNode *s) {
  3. if (p == NULL || s == NULL) return false; //非法参数
  4. s->next = p->next;
  5. if (p->next != NULL) //如果p结点有后继结点
  6. p->next->prior = s;
  7. s->prior = p;
  8. p->next = s;
  9. return true;
  10. }

学会了后插操作,我们也就学会了按位序插入和前插法,大概思路为找到目标结点的前驱结点,然后对其进行后插操作

双链表的删除

  1. //删除p结点的后继结点
  2. bool DeleteNextDNode(DNode *p) {
  3. if (p == NULL) return false;
  4. DNode *q = p->next; //找到p结点的后继结点q
  5. if (q == NULL) return false; //p没有后继
  6. p->next = q->next;
  7. if (q->next != NULL) //q结点不是最后一个结点
  8. q->next->prior = p;
  9. free(p); //释放结点空间
  10. return true;
  11. }
  12. //销毁双链表
  13. void DestoryList(DLinklist &L) {
  14. //循环释放各个数据结点
  15. while (L->next != NULL) {
  16. DeleteNextDNode(L);
  17. }
  18. free(L); //释放头结点
  19. L = NULL; //头指针指向NULL
  20. }

双链表的遍历

由于双链表不可随机存取,所以按位查找、按值查找等操作都只能用遍历的方式实现,时间复杂度为 O(n)

  1. //后向遍历
  2. while (p != NULL) {
  3. //对结点p做相应处理,比如打印
  4. p = p->next;
  5. }
  6. //前向遍历
  7. while (p != NULL) {
  8. //对结点p做相应处理
  9. p = p->prior;
  10. }
  11. //前向遍历(跳过头结点)
  12. while (p->prior != NULL) {
  13. //对结点p做相应处理
  14. p = p->prior;
  15. }

循环单链表

我们都知道,单链表的表尾结点的 next 指针是指向 NULL,顾名思义,循环单链表的表尾结点的 next 指针就是指向头结点的

循环单链表的优点:从一个结点出发可以找到其他任何一个结点

  1. typedef int ElemType;
  2. typedef struct LNode{
  3. ElemType data; //每个节点存放一个数据元素
  4. struct LNode *next; //指针指向下一个节点
  5. }LNode, *LinkList;
  6. //初始化一个循环单链表
  7. bool InitList(LinkList &L) {
  8. L = (LNode *)malloc(sizeof(LNode)); //分配一个头结点
  9. if (L == NULL) return false; //内存不足,分配失败
  10. L->next = L; //头结点next指针指向头结点
  11. return true;
  12. }
  13. //判断循环单链表是否为空
  14. bool Empty(LinkList L) {
  15. if (L->next == L)
  16. return true;
  17. else
  18. return false;
  19. }
  20. //判断结点p是否为循环单链表的表尾结点
  21. bool isTail(LinkList L, LNode *p) {
  22. if (p->next == L)
  23. return true;
  24. else
  25. return false;
  26. }

循环双链表

双链表:

  • 表头结点的 prior 指向 NULL
  • 表尾结点的 next 指向 NULL

循环双链表

  • 表头结点的 prior 指向表尾结点
  • 表尾结点的 next 指向头结点

循环双链表的初始化

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. typedef int ElemType;
  4. typedef struct DNode{
  5. ElemType data; //数据域
  6. struct DNode *prior, *next; //前驱和后继指针
  7. }DNode, *DLinklist;
  8. //初始化空的循环双链表
  9. bool InitDLinklist(DLinklist &L) {
  10. L = (DNode *)malloc(sizeof(DNode)); //分配一个头结点
  11. if (L == NULL) return false; //内存不足,分配失败
  12. L->prior = L; //头结点的 prior 指向头结点
  13. L->next = L; //头结点的 next 指向头结点
  14. return true;
  15. }
  16. //判断循环双链表是否为空
  17. bool Empty(DLinklist L) {
  18. if (L->next == L)
  19. return true;
  20. else
  21. return false;
  22. }
  23. //判断结点p是否为循环双链表的表尾结点
  24. bool isTail(DLinklist L, DNode *p) {
  25. if (p->next = L)
  26. return true;
  27. else
  28. return false;
  29. }
  30. void testDLinklist() {
  31. //初始化双链表
  32. DLinklist L;
  33. InitDLinklist(L);
  34. }

循环双链表的插入

  1. //在p结点之后插入s节点
  2. bool InsertNextDNode(DNode *p, DNode *s) {
  3. s->next = p->next;
  4. p->next->prior = s;
  5. s->prior = p;
  6. p->next = s;
  7. return true;
  8. }

循环双链表的删除

  1. //删除p的后继结点q
  2. p->next = q->next;
  3. q->next->prior = p;
  4. free(q);

静态链表

什么是静态链表

单链表:各个结点在内存中星罗棋布、散落天涯

静态链表:分配一整片连续的内存空间,各个结点集中安置,0 号结点充当 “头结点”,下一个结点的数组下标(也称为游标)充当 “指针”,游标为 -1 时表示已经到达表尾

静态链表是用数组的方式来实现的链表,其优点为 —— 增、删操作不需要大量移动元素;缺点为 —— 不能随机存取,只能从头结点开始依次往后查找;容量固定不可变

定义静态链表

  1. #define MaxSize 10 //静态链表的最大长度
  2. struct Node{
  3. ElemType data; //存储数据元素
  4. int next; //下一个元素的数组下标
  5. };

或者

  1. #define MaxSize 10 //静态链表的最大长度
  2. typedef struct {
  3. ElemType data; //存储数据元素
  4. int next; //下一个元素的数组下标
  5. } SLinkList[MaxSize];

SLinkList a 相当于 struct Node a[MaxSize]

基本操作的实现

初始化

  • a[0]next 设置为 -1
  • 把空的结点的 next 设置为 -2

查找

从头结点出发依次往后遍历结点

插入位序为 i 的结点

  1. 找到一个空的结点,存入数据元素
  2. 从头结点出发找到位序为 i-1 的结点
  3. 修改新结点的 next
  4. 修改 i-1 号结点的 next

删除某个结点

  1. 从头结点出发找到前驱结点
  2. 修改前驱结点的游标
  3. 被删除结点的 next 设置为 -2

顺序表和链表的比较

从逻辑结构来说,顺序表和链表都属于线性表,都是线性结构

从存储结构来说,顺序表采用顺序存储,而链表采用链式存储

顺序表

  • 优点:支持随机存取,存取密度高
  • 缺点:大片连续空间分配不方便,改变容量不方便

链表:

  • 优点:离散的小空间分配方便,改变容量方便
  • 缺点:不可随机存取,存储密度低

从基本操作来看

  • 顺序表需要预分配大片连续空间,若分配空间过小,则之后不方便扩展容量;若分配空间过大,则浪费内存资源。如果采取静态分配的方式,则容量不可改变;如果采取动态分配的方式,则容量可改变,但需要移动大量元素,时间代价高
  • 链表只需分配一个头结点(也可以不要头结点,只声明一个头指针),之后方便拓展

  • 对链表来说,你只需扫描整个链表,依次删除(free)各个结点即可
  • 对顺序表来说,首先你需要修改 length = 0,如果是采用静态分配的方式,当静态数组的生命周期结束时,系统会自动回收空间;如果是采用动态分配的方式,用 malloc 申请的空间是属于内存中的堆区,在堆区的内存空间不会由系统自动回收,需要我们手动 free

增删

  • 对顺序表来说,插入或删除都要讲后续元素全部后移或前移,时间复杂度为 O(n),时间开销主要来自移动元素
  • 对链表来说,插入或删除元素只需要修改指针即可,时间复杂度为 O(n),时间开销主要来自查找目标元素
  • 虽然时间复杂度一样,但是结合实际因素,链表增删的效率要比顺序表高得多

  • 对顺序表来说,按位查找的时间复杂度为 O(1);按值查找的时间复杂度为 O(n),如果表内元素有序,可采用折半查找等方法在 O(log2n) 时间内找到
  • 对链表来说,按位查找的时间复杂度为 O(n);按值查找的时间复杂度也为 O(n)

综上所述

  • 表长难以预估、经常要增加或删除元素 —— 链表
  • 表长可预估、查询操作较多 —— 顺序表

发表评论

表情:
评论列表 (有 0 条评论,161人围观)

还没有评论,来说两句吧...

相关阅读