前言
上一期介绍了一种最基本的数据结构——顺序表,也通过编写程序详细地了解了内部的每一个细节。虽然数组很简单,但是实际编写程序时容易出错的细节还是挺多的,初学者还是需要认真编写一遍才更有体会。
本章介绍线性表的链式存储——链表。同样,我们会结合具体的代码实现来讨论链表的各种操作。
简介
我们以单向链表为例作详细介绍。和数组不一样的是,链表的存储数据单元我们称之为结点,一个结点由数据域和指针域组成,数据域存放数据,指针域指向下一个结点,如此实现存储单元无需连续分配,形成链表。
那么问题是,这个指针域如何指向下一个存储单元呢?在C语言中,我们就采用指针的方式来记录每一个结点所在的内存位置,因此,只需将指针域的值设置为下一个存储单元的指针即可实现链表的串联工作。
那如何在程序中来表示一个链表呢?也很简单,只需要能够记录这个链表的第一个结点(也称作头结点)的位置,即可通过其内部的指针域访问到整个链表了。就比如上图中的node1的指针就可以作为整个链表的起点位置。
对于一个结点,邻近它的前后各有一个结点,它前面的结点称为前驱,它后面的结点称为后继。
头文件编写
声明
首先是和顺序表一样的,进行存储数据类型定义。
其次是声明一个结构体的指针名,方便后续程序中参数的理解。
typedef int ElementType;
typedef struct Node *LinkList;
typedef LinkList Position;
这里我们对声明的指针名再次进行了一次重命名,实际上用两个类型名(LinkList和Position)在程序中没有任何区别,这样做只是为了方便理解,后续结合具体代码会详细解释。
函数
这里大多数函数是与顺序表是相似的,我们只需要在相似函数中将变量类型改为LinkList即可。这里不再赘述,详情可以参考上一期。我们只提一些不同的地方。
void NullAndExit(LinkList L); //操作指针为空时异常退出
void PositionInvalid(Position P); //输入位置无效时异常退出
与在数组操作中可能出现数组越界问题类似,传入一个无效的指针很有可能导致程序异常,因此需要在有Position指针传入的函数中检查指针是否为空。这里我们单独声明了一个函数是因为在很多函数内部这个操作是重叠的。
至于为什么没有在顺序表中这么做,那是因为在插入和删除访问时位置判断条件是不同的,没办法统一封装。
int IsLast(Position P, LinkList L); //判断当前位置是否为表尾
int IsEmpty(LinkList L); //判断表是否为空
LinkList CreateList(); //创建空表
对于链表,由于不存在像数组一样受限于空间提前申请分配的问题,所以链表理论上只要内存够用,可以存放无限多的元素,也就不需要输入最大元素个数作为限制了。同理,链表也不会出现Full的情况,所以没有IsFull函数 。不过我们可以添加一个判断输入位置是否是链表尾部的函数。
void MakeEmpty(LinkList L); //清空表
void DeleteList(LinkList L); //删除表
ElementType Retrieve(Position P, LinkList L); //返回表中指定位置元素
Position Find(ElementType X, LinkList L); //返回表中指定元素所在位置
Position FindPrevious(ElementType X, LinkList L); //返回表中指定元素前驱位置
void Insert(ElementType X, Position P, LinkList L); //向表中指定位置后插入元素
void Delete(ElementType X, LinkList L); //删除表中指定元素
Position Advance(Position P