❤️《画解数据结构》七个动画 “画“ 解链表 (本文出自那个被吊打的面试官,建议收藏)❤️

Posted 英雄哪里出来

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了❤️《画解数据结构》七个动画 “画“ 解链表 (本文出自那个被吊打的面试官,建议收藏)❤️相关的知识,希望对你有一定的参考价值。

本文已收录于专栏
🌳《画解数据结构》🌳

零、前言

  「 数据结构 」「 算法 」 是密不可分的,两者往往是「 相辅相成 」的存在,所以,在学习 「 数据结构 」 的过程中,不免会遇到各种「 算法 」
  到底是先学 数据结构 ,还是先学 算法,我认为不必纠结这个问题,一定是一起学的。
  数据结构 常用的操作一般为:「 增 」「 删 」「 改 」「 查 」。基本上所有的数据结构都是围绕这几个操作进行展开的。
  那么这篇文章,作者将用 「 七张动图 」 来阐述一种最基础的链式结构

「 单向链表 」



🙉饭不食,水不饮,题必须刷🙉

C语言免费动漫教程,和我一起打卡!
🌞《光天化日学C语言》🌞

LeetCode 太难?先看简单题!
🧡《C语言入门100例》🧡

数据结构难?不存在的!
🌳《画解数据结构》🌳

LeetCode 太简单?算法学起来!
🌌《夜深人静写算法》🌌

  今天要讲的内容,浓缩一下就是下面这张图:


  看不懂没有关系,我会把它拆开来一个一个讲,首先来看一些简单的概念。

一、概念

  • 对于顺序存储的结构,如数组,最大的缺点就是:插入删除 的时候需要移动大量的元素。所以,基于前人的智慧,他们发明了链表。

1、链表定义

  链表 是由一个个 结点 组成,每个 结点 之间通过 链接关系 串联起来,每个 结点 都有一个 后继节点,最后一个 结点后继结点空结点。如下图所示:

  • 由链接关系A -> B组织起来的两个结点,B被称为A的后继结点,A被称为B的前驱结点。
  • 链表 分为 单向链表双向链表循环链表 等等,本文要介绍的链表是 单向链表
  • 由于链表是由一个个 结点 组成,所以我们先来看下 结点 的实现。

2、结点结构体定义

typedef int DataType;
struct ListNode {
    DataType data;  // (1)
    ListNode *next; // (2)
};
  • ( 1 ) (1) (1) 数据域:可以是任意类型,由编码的人自行指定;这段代码中,利用typedef将它和int同名,本文的 数据域 也会全部采用int类型进行讲解;
  • ( 2 ) (2) (2) 指针域:指向 后继结点 的地址;
  • 一个结点包含的两部分如下图所示:

3、结点的创建

  • 我们通过 C语言 中的库函数malloc来创建一个 链表结点,然后对 数据域指针域 进行赋值,代码实现如下:
ListNode *ListCreateNode(DataType data) {
    ListNode *node = (ListNode *) malloc ( sizeof(ListNode) ); // (1)
    node->data = data;                                         // (2)
    node->next = NULL;                                         // (3)
    return node;                                               // (4)
}
  • ( 1 ) (1) (1) 利用系统库函数malloc分配一块内存空间,用来存放ListNode即链表结点对象;
  • ( 2 ) (2) (2)数据域 置为函数传参data
  • ( 3 ) (3) (3)指针域 置空,代表这是一个孤立的 链表结点
  • ( 4 ) (4) (4) 返回这个结点的指针。
  • 创建完毕以后,这个孤立结点如下所示:

二、链表的创建 - 尾插法

  • 那么接下来,让我们看下如何通过一个 数组中的数据 来创建一个链表。

1、算法描述

  首先介绍 尾插法 ,顾名思义,即 从链表尾部插入 的意思,就是记录一个 链表尾结点,然后遍历给定数组,将数组元素一个一个插到链表的尾部,每插入一个结点,则将它更新为新的 链表尾结点。注意初始情况下,链表尾结点 为空。

2、动画演示

上图演示的是 尾插法 的整个过程,其中:
  head 代表链表头结点,创建完一个结点以后,它就保持不变了;
  tail 代表链表尾结点,即动图中的 绿色结点
  vtx 代表正在插入链表尾部的结点,即动图中的 橙色结点,插入完毕以后,vtx 变成 tail

  • 看完这个动图,你应该已经大致理解了 链表的创建过程。那么接下来,我们用程序语言来描述一下整个过程,这里采用的是 C语言 的形式,如果你是 Java、C#、Python 技术栈的,也可以试着写出自己的版本。
  • 语言并不是关键,思维才是关键。

3、源码详解

  • C语言 实现如下:
ListNode *ListCreateListByTail(int n, int a[]) {
    ListNode *head, *tail, *vtx;         // (1) 
    int idx;                              
    if(n <= 0)
        return NULL;                     // (2) 
    idx = 0;
    vtx = ListCreateNode(a[0]);          // (3) 
    head = tail = vtx;                   // (4)  
    while(++idx < n) {                   // (5) 
        vtx = ListCreateNode(a[idx]);    // (6) 
        tail->next = vtx;                // (7) 
        tail = vtx;                      // (8)  
    } 
    return head;                         // (9) 
} 

对应的注释如下:
   ( 1 ) (1) (1) head存储头结点的地址,tail存储尾结点的地址,vtx存储当前正在插入结点的地址;
   ( 2 ) (2) (2) 当需要创建的元素个数为 0 时,直接返回空链表;
   ( 3 ) (3) (3) 创建一个 数据域a[0]的链表结点;
   ( 4 ) (4) (4) 由于初始情况下只有一个结点,所以将链表头结点head和链表尾结点tail都置为vtx
   ( 5 ) (5) (5) 从数组第 1 个元素 (0 - based) 开始,循环遍历数组;
   ( 6 ) (6) (6) 由于数组中第 0 个元素已经创建过了,所以这里只需要对除了第 0 个元素以外的数据创建链表结点;
   ( 7 ) (7) (7) 结点创建出来后,将当前链表尾结点tail后继结点 置为vtx
   ( 8 ) (8) (8) 将最近创建的结点vtx作为新的 链表尾结点
   ( 9 ) (9) (9) 返回链表头结点;


  • 尾插法 比较符合直观的思维逻辑,但是就代码量来说还是有点长(注意:在实现相同功能的情况下,代码应该是越简洁,越简单越好的)。
  • 于是,我们引入了另一种创建链表的方式 —— 头插法。

三、链表的创建 - 头插法

1、算法描述

  头插法,顾名思义,就是每次从头结点前面进行插入,但是这样一来,就会导致插入的数据元素是 逆序 的,所以我们需要 逆序访问数组 执行插入,此所谓 负负得正 的思想。

  • 它的特点是代码量短,且 常数时间复杂度 低。虽然没有 尾插法 那么直观,但是代码简洁,更加容易阅读。

2、动画演示

上图所示的是 头插法 的整个插入过程,其中:
  head 代表链表头结点,即动图中的 绿色结点,每新加一个结点,头结点就变成了新加入的结点;
  tail 代表链表尾结点,创建完一个结点以后,它就保持不变了;
  vtx 代表正在插入链表头部的结点,即动图中的 橙色结点,插入完毕以后,vtx 变成 head

3、源码详解

ListNode *ListCreateListByHead(int n, int *a) {
    ListNode *head = NULL, *vtx;       // (1) 
    while(n--) {                       // (2) 
        vtx = ListCreateNode(a[n]);    // (3) 
        vtx->next = head;              // (4) 
        head = vtx;                    // (5) 
    } 
    return head;                       // (6) 
} 

对应的注释如下:
   ( 1 ) (1) (1) head存储头结点的地址,初始为空链表, vtx存储当前正在插入结点的地址;
   ( 2 ) (2) (2) 总共需要插入 n n n 个结点,所以采用逆序的 n n n 次循环;
   ( 3 ) (3) (3) 创建一个元素值为a[i]的链表结点,注意,由于逆序,所以这里 i i i 的取值为 n − 1 → 0 n-1 \\to 0 n10
   ( 4 ) (4) (4) 将当前创建的结点的 后继结点 置为 链表的头结点head
   ( 5 ) (5) (5) 将链表头结点head置为vtx
   ( 6 ) (6) (6) 返回链表头结点;


  • 头插法 的代码量比 尾插法 少了三分之一,而且将 创建结点的逻辑 统一起来了。这句话什么意思呢?仔细观察可以发现,尾插法 在实现过程中,ListCreateNode在代码里出现了两次,而 头插法 只出现了一次,将流程简化了,所以还是推荐使用 头插法

四、链表的打印

1、打印的作用

  • 可视化 能够帮助我们更好的理解数据结构。所以,对于一种数据结构,如何通过 输出函数 将它 打印到控制台 上,就成了我们接下来要做的事情。
  • 我会用 C语言 来实现,但是只要你掌握了这套自己验证的方法,那么就算用其他语言,一样可以验证自己代码的正确性。

那么,如何打印一个链表呢?我们可以这么思考:
  链表的每个结点都有一个 后继结点 ,我们可以用A -> B代表结点B是结点A后继结点,而对于最后一个结点而言,它的后继可以用NULL表示。所以,我们可以循环输出所有结点并且带上->,然后在最后加上NULL

2、源码详解

  • C语言实现如下:
void ListPrint(ListNode *head) {
    ListNode *vtx = head;
    while(vtx) {                      // (1)
        printf("%d -> ", vtx->data);  // (2) 
        vtx = vtx->next;              // (3)
    }
    printf("NULL\\n");                 // (4)
}

对应的注释如下:
   ( 1 ) (1) (1) 从头结点开始,循环遍历所有结点;
   ( 2 ) (2) (2) 遍历到的结点,将结点的 数据域 带上->后输出;
   ( 3 ) (3) (3)当前结点 置为 当前结点后继结点,继续迭代;
   ( 4 ) (4) (4) 最后输出一个NULL,代表一个完整的链表;

  • 对于上面例子中的链表,调用这个函数,得到的结果为:
1 -> 3 -> 8 -> 2 -> 6 -> NULL

3、测试用例

  • 例如,我们在 头插法 的实现过程中,加上一句 链表的打印 语句,代码实现如下:
ListNode *ListCreateListByHead(int n, int *a) {
    ListNode *head = NULL, *vtx;
    while(n--) { 
        vtx = ListCreateNode(a[n]);
        vtx->next = head;
        head = vtx;
        ListPrint(head);    /*看这里,看这里!*/
    } 
    return head;
} 
  • 运行后得到的结果如下:
6 -> NULL
2 -> 6 -> NULL
8 -> 2 -> 6 -> NULL
3 -> 8 -> 2 -> 6 -> NULL
1 -> 3 -> 8 -> 2 -> 6 -> NULL
  • 这样,我们就能更加进一步的确保我们实现 头插法 这个算法的正确性了。

验证算法的正确性有两个有效的办法:
   ( 1 ) (1) (1) 构造大量的 测试数据 进行输入输出测试;
   ( 2 ) (2) (2) 打印每一个操作后,数据结构的 当前状态,看是否和预期相符;

  • 对 链表 进行打印,就是利用了这里的第 ( 2 ) (2) (2) 点,这个方法虽然原始,但是能够让你对每一步操作都了然于胸, 尤其是写到后面,代码量爆炸的时候,这个方法往往能够让你规避很多不必要的逻辑错误。

五、链表元素的索引

1、算法描述

  给定一个链表头结点head,并且给定一个索引值 i ( i ≥ 0 ) i (i \\ge 0) i(i0),求这个链表的第 i i i 个结点(为了和 C语言 的数组下标保持一致,我们假定链表头结点代表第 0 个结点)。

  • 这实际上是一个 遍历链表 的过程,我们先来看下动画演示。

2、动画演示

上图演示的是通过遍历,索引到第 3 个结点(下标从 0 开始计数)的过程,其中:
  head 代表链表头结点;
  tail 代表链表尾结点;
  j / temp 代表当前枚举到的第 j ( j ≥ 0 ) j (j \\ge 0) j(j0)个结点,即动图中的 橙色实心结点

3、源码详解

ListNode *ListGetNode(ListNode *head, int i) {
    ListNode *temp = head;       // (1) 
    int j = 0;                   // (2) 
    while(temp && j < i) {       // (3) 
        temp = temp->next;       // (4) 
        ++j;                     // (5) 
    }
    if(!temp || j > i) {
        return NULL;             // (6) 
    }
    return temp;                 // (7) 
}
  • ( 1 ) (1) (1) temp代表从链表头开始的 游标指针,用于对链表进行 遍历 操作;
  • ( 2 ) (2) (2) j代表当前访问到了第 j j j 个结点;
  • ( 3 ) (3) (3) 如果 游标指针 非空,并且j < i,则代表还没访问到目标结点,继续执行循环;
  • ( 4 ) (4) (4)游标指针后继结点 作为新一轮的 游标指针,继续迭代;
  • ( 5 ) (5) (5) j自增,等价于j = j + 1;
  • ( 6 ) (6) (6)游标指针 为空,或者j > i,则说明给定的i超过了链表长度,返回 空结点
  • ( 7 ) (7) (7) 最后,返回找到的第i个结点;

4、测试用例

void testListGetNode(ListNode *head) {
    int i;
    for(i = 0; i < 7; ++i) {
        ListNode *node = ListGetNode(head, i);
        if(!node)
            printf("index(%d) is out of range.\\n", i);
        else 
            printf("node(%d) is %d.\\n", i, node->data);
    }    
}
int main() {    
    int a[5] = {1, 3, 8, 2, 6};
    ListNode *head = ListCreateListByHead(5, a);   // (1)
    testListGetNode(head);                         // (2)
    return 0;
}
  • 这个测试用例,首先第 ( 1 ) (1) (1) 步,利用 头插法 对给定数组创建了一个链表;然后第 ( 2 ) (2) (2) 步,枚举 i ∈ [ 0 , 6 ] i \\in [0, 6] i[0,6],分别去取链表的第 i i i 个结点,运行结果如下:
node(0) is 1.
node(1) is 3.
node(2) is 8.
node(3) is 2.
node(4) is 6.
index(5) is out of range.
index(6) is out of range.
  • 这表明当下标在链表元素个数范围内时,能够找到对应结点;否则,返回的是空节点;进一步验证了程序实现的正确性。

5、算法分析

1)时间复杂度

  • 索引结点的操作,最坏情况下需要遍历整个链表,所以时间复杂度为 O ( n ) O(n) O(n)

2)空间复杂度

  • 整个索引过程只记录了两个变量:游标结点当前索引值。和链表长度无关,所以空间复杂度为 O ( 1 ) O(1) O(1)

六、链表元素的查找

1、算法描述

  给定一个链表头head,并且给定一个值 v v v,查找出这个链表上 数据域 等于 v v v 的第一个结点。

  • 查找的过程,基本和索引类似,也是对链表的遍历操作,首先请看动画演示。

2、动画演示

上图演示的是通过遍历,查找到值为 2 的结点的过程,其中:
  head 代表链表头结点;
  tail 代表链表尾结点;
  j / temp 代表

以上是关于❤️《画解数据结构》七个动画 “画“ 解链表 (本文出自那个被吊打的面试官,建议收藏)❤️的主要内容,如果未能解决你的问题,请参考以下文章

❤️《画解数据结构》七张动图,画解单调队列❤️(建议收藏)

❤️《画解数据结构》全网最清晰哈希表入门,三张动图搞懂哈希 ❤️(建议收藏)

❤️《画解数据结构》之 顺序表八大算法总结❤️(建议收藏)

❤️《画解数据结构》两万字,十张动图,画解双端队列❤️(建议收藏)

❤️《画解数据结构》两万字,十张动图,画解双端队列❤️(建议收藏)

❤️五万字《十大排序算法》动图讲解❤️(建议收藏)