当前位置 博文首页 > 英雄哪里出来:《画解数据结构》七个动画 “画“ 解链表
??「 数据结构 」 和 「 算法 」 是密不可分的,两者往往是「 相辅相成 」的存在,所以,在学习 「 数据结构 」 的过程中,不免会遇到各种「 算法 」。
??到底是先学 数据结构 ,还是先学 算法,我认为不必纠结这个问题,一定是一起学的。
??数据结构 常用的操作一般为:「 增 」「 删 」「 改 」「 查 」。基本上所有的数据结构都是围绕这几个操作进行展开的。
??那么这篇文章,作者将用 「 七张动图 」 来阐述一种最基础的链式结构
「 单向链表 」
🙉饭不食,水不饮,题必须刷🙉
C语言免费动漫教程,和我一起打卡! 🌞《光天化日学C语言》🌞
LeetCode 太难?先看简单题! 🧡《C语言入门100例》🧡
数据结构难?不存在的! 🌳《画解数据结构》🌳
LeetCode 太简单?算法学起来! 🌌《夜深人静写算法》🌌
??今天要讲的内容,浓缩一下就是下面这张图:
??看不懂没有关系,我会把它拆开来一个一个讲,首先来看一些简单的概念。
??链表 是由一个个 结点 组成,每个 结点 之间通过 链接关系 串联起来,每个 结点 都有一个 后继节点,最后一个 结点 的 后继结点 为 空结点。如下图所示:
A -> B
组织起来的两个结点,B
被称为A
的后继结点,A
被称为B
的前驱结点。typedef int DataType;
struct ListNode {
DataType data; // (1)
ListNode *next; // (2)
};
typedef
将它和int
同名,本文的 数据域 也会全部采用int
类型进行讲解;malloc
来创建一个 链表结点,然后对 数据域 和 指针域 进行赋值,代码实现如下:ListNode *ListCreateNode(DataType data) {
ListNode *node = (ListNode *) malloc ( sizeof(ListNode) ); // (1)
node->data = data; // (2)
node->next = NULL; // (3)
return node; // (4)
}
malloc
分配一块内存空间,用来存放ListNode
即链表结点对象;data
;??首先介绍 尾插法 ,顾名思义,即 从链表尾部插入 的意思,就是记录一个 链表尾结点,然后遍历给定数组,将数组元素一个一个插到链表的尾部,每插入一个结点,则将它更新为新的 链表尾结点。注意初始情况下,链表尾结点 为空。
上图演示的是 尾插法 的整个过程,其中:
??head 代表链表头结点,创建完一个结点以后,它就保持不变了;
??tail 代表链表尾结点,即动图中的 绿色结点;
??vtx 代表正在插入链表尾部的结点,即动图中的 橙色结点,插入完毕以后,vtx 变成 tail;
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) 返回链表头结点;
??头插法,顾名思义,就是每次从头结点前面进行插入,但是这样一来,就会导致插入的数据元素是 逆序 的,所以我们需要 逆序访问数组 执行插入,此所谓 负负得正 的思想。
上图所示的是 头插法 的整个插入过程,其中:
??head 代表链表头结点,即动图中的 绿色结点,每新加一个结点,头结点就变成了新加入的结点;
??tail 代表链表尾结点,创建完一个结点以后,它就保持不变了;
??vtx 代表正在插入链表头部的结点,即动图中的 橙色结点,插入完毕以后,vtx 变成 head;
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 n?1→0;
?? ( 4 ) (4) (4) 将当前创建的结点的 后继结点 置为 链表的头结点head
;
?? ( 5 ) (5) (5) 将链表头结点head
置为vtx
;
?? ( 6 ) (6) (6) 返回链表头结点;
ListCreateNode
在代码里出现了两次,而 头插法 只出现了一次,将流程简化了,所以还是推荐使用 头插法。那么,如何打印一个链表呢?我们可以这么思考:
??链表的每个结点都有一个 后继结点 ,我们可以用A -> B
代表结点B
是结点A
的 后继结点,而对于最后一个结点而言,它的后继可以用NULL
表示。所以,我们可以循环输出所有结点并且带上->
,然后在最后加上NULL
。
void ListPrint(ListNode *head) {
ListNode *vtx = head;
while(vtx) {