经过了上篇顺序表的学习,我们大概清楚了顺序表的特性,这篇我们来学习线性表的另一种结构:链表。
1. 链表概述
还记得在顺序表(数组)中,我们插入、删除一个元素,都需要移动大量的元素,来达到顺序排列的目的,所以计算量较大,是否有一种不需要移动元素即能插入或者删除元素的线性表结构呢,答案是有的,那就是链表结构。
链表分为:单链表、循环链表、双向链表、静态链表等。我们先看单链表,结构大致如下图所示。一个链表结点包含有数据域和指针域两部分,前一个结点的指针域存有下一个结点的指针地址,所以就形成了链式数据结构,而最后一个结点的指针域是指向NULL的。注意:一般链表头部都会有一个结点,它不包含数据域,只有指向第一个结点的指针,这个结点叫做头结点,指针叫做头指针。
![36c880da5b524a28069d2d71e74e7e0e.png](https://i-blog.csdnimg.cn/blog_migrate/386adba2f82da3c6dac3ba357d75c75a.png)
所以链表的插入和删除是不需要移动元素的,只需要将插入位置的前一个结点的指针指向插入结点,插入结点的指针指向插入位置的下一个结点,说起来很拗口,其实理解起来并不费劲。插入过程如下图所示。
![1686da97e203ecb46337b531fb52a07c.png](https://i-blog.csdnimg.cn/blog_migrate/86c87c7c6b21cb780eeed588d66c6f47.png)
删除也同理,只需将删除结点的前一个结点直接指向下一个结点即可,然后释放删除结点的内存。过程如下图所示。
![16f63845035e09eff29b1a6344ca5e60.png](https://i-blog.csdnimg.cn/blog_migrate/7085336eed7d01c08c72724218658ba7.png)
2. 实现单链表
我们依次来实现单链表的插入、删除、清空。
我们还是沿用顺序表的数据元素ElementType结构体。
typedef struct {
int id;
char* name;
}ElementType;
接着定义单链表的“结点”,结点包含一个数据域和一个指针域。
/**定义链表的结点,包含数据域和指针域*/
typedef struct Node {
ElementType date; ///数据域
struct Node* next; ///指针域
}Node;
然后定义头结点,包含一个头指针和链表的元素个数(length)。可以看到我们将头结点就命名为LinkList,是因为单链表的初始化都是从头结点开始,然后根据指针一个一个地遍历下去的。
/**头结点*/
typedef struct LinkList {
Node* next; ///头指针(如果链表有头结点,next就指向头结点,没有就指向第一个结点)
int length; ///链表的长度,初始值为0
}LinkList;
然后我们开始实现插入。插入位置分第一个结点位置、中间位置、尾结点位置。第一个结点不需要遍历链表,直接头结点指向即可。如果插入的不是第一个结点位置,那我们必须遍历链表,找到插入位置的前后结点,然后插入结点,重新设置前后结点的指针域。最后一个结点位置,将插入结点的指针域置为空即可。记得插入后链表长度加一。
/**在指定位置pos处,插入元素element*/
void InsertLinkList(LinkList* linklist, int pos, ElementType element)
{
///1、创建空结点并为数据域赋值
Node* node = (Node*)malloc(sizeof(Node));
node->date = element;
node->next = NULL;
///2、找到要插入位置的结点
if (pos == 1)///如果插入的是第一个元素
{
linklist->next = node;
linklist->length++;
return;
}
///通过循环找到要插入的结点位置
Node* currNode = linklist->next;///第一个结点
for (int i = 1; currNode && i < pos - 1; i++)
{
///将下一个结点的地址赋值给当前结点(相当于移动到下一个结点)
currNode = currNode->next;
}
///3、将结点插入并对接前面的结点
if (currNode)
{
///必须先后再前,不然currNode->next值被改了
node->next = currNode->next;///node和后面结点对接
currNode->next = node;///node和前面结点对接
linklist->length++;
}
}
接着实现下删除操作,也同样分删除的是第一个结点位置、中间结点位置、尾结点位置,删除结点后,重新设置前后结点的指针,最后链表长度减一。注意:必须释放删除结点内存。
ElementType DeleteLinkListElement(LinkList* linkList, int pos)
{
Node* node = linkList->next;//初始化第一个结点(头结点指向第一个结点)
Node* prevNode = NULL;//删除结点的前一个结点
ElementType delElement = {};//初始化删除结点数据
//删除结点超出范围
if (pos > linkList->length)
{
printf("超出数组范围n");
return delElement;
}
//删除第一个结点
if (pos == 1)
{
if (node)
{
linkList->next = node->next;//头结点指向第二个结点
delElement = node->date;//保存删除结点的数据
free(node);//删除结点,释放内存
linkList->length--; //链表长度减一
}
return delElement;
}
//删除除第一个以外的结点
//循环遍历到要删除的元素处
for (int i = 1; node && i < pos; i++)
{
prevNode = node;//记录上一个结点
node = node->next;
}
//删除元素并返回
if (node)
{
prevNode->next = node->next;//前一个结点指向删除结点的下一个结点
delElement = node->date;//保存删除结点的数据
free(node);//删除结点,释放内存
linkList->length--;//链表长度减一
return delElement;
}
}
最后实现清空链表。我们使用循环,按指针顺序一个一个的遍历并删除结点,最终删完所有结点,并将链表长度清零,头指针置为空。
/*清空链表*/
void ClearLinkList(LinkList* linkList)
{
Node* node = linkList->next;//头指针指向第一个结点
Node* nextNode = NULL;
while (node)
{
nextNode = node->next;
free(node);
node = nextNode;
}
linkList->length = 0;
linkList->next = NULL;
}
然后我们实现初始化函数,将结点挨个插入。
/**初始化链表*/
void InitLinkList(LinkList* linklist, ElementType* dataArray, int length)
{
for (int i = 0; i < length; i++)
{
InsertLinkList(linklist, i + 1, dataArray[i]);
}
}
最后为了测试,我们再实现一个遍历链表并打印的函数。链表的遍历:node = node->next;这是链表遍历中最常用到的代码,这在插入和删除函数中遍历链表时也同样用到。
void PrintLinkList(LinkList* linklist)
{
Node* node = linklist->next;
if (!node)
{
printf("LinkList is null!n");
linklist->length = 0;
return;
}
for (int i = 1; i <= linklist->length; i++)
{
printf("%dt%sn", node->date.id, node->date.name);
node = node->next;
}
}
我们开始测试,在主函数中执行链表测试函数。链表测试函数如下,首先初始化链表,然后删除3号结点,最后清空链表。
void TestLinkList()
{
LinkList linklist;
linklist.length = 0;
InitLinkList(&linklist, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
printf("初始化列表:n");
PrintLinkList(&linklist);
printf("删除后的列表:n");
ElementType delData = DeleteLinkListElement(&linklist, 3);
PrintLinkList(&linklist);
printf("删除的是:n%dt%sn", delData.id, delData.name);
ClearLinkList(&linklist);
printf("链表清空后n");
PrintLinkList(&linklist);
}
编译运行,3号结点“山治”被删除了,清空链表后,打出了“LinkList is null!”的字符串。链表插入、删除、清空成功。
![625a3f420affc8685b62bf52a5079bca.png](https://i-blog.csdnimg.cn/blog_migrate/5c3232b2fd364bf47b4bd90991297d39.png)
3. 单链表总结
我们简单地对单链表结构和顺序表结构做对比:
存储分配方式:(1)顺序表结构用一段连续地存储单元依次存储线性表的数据元素。
(2)单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素。
时间性能:(1)顺序表查找直接使用下标,速度非常快,时间为0[1]。插入和删除需要平均移动表长一半的元素,时间为0[n]。
(2)单链表查找需要根据指针从头遍历,速度比顺序表慢(根据遍历的次数)时间为n[n]。链表插入和删除时,遍历出插入或删除位置的前后结点,再设置他们的指针,而不需要移动元素,所以时间为0[1]。
空间性能:(1)顺序表结构需要预分配存储空间,分大了,浪费,分小了,容易溢出。
(2)单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制。
所以我们总结:如果线性表需要频繁查找,很少进行插入和删除操作时,我们宜采用顺序表存储结构。如果需要频繁插入和删除时,宜采用单链表结构。当线性表中元素个数变化较大或者根本不知道有多大时,宜采用单链表结构。如果事先知道线性表的大致长度,宜采用顺序表结构。