前言
前面讲了线性表的顺序存储结构,并在最后总结出了顺序存储结构的缺点,那就是在插入、删除元素时,需要移动大量的元素,易造成存储空间的浪费,为此,我们可以通过线性表的链式存储结构来解决。
1.定义
为了表示每个数据元素与其直接后继元素之间的逻辑关系,每个元素除了存储本身的信息外,还需要存储指示其直接后继的信息。其示意图如下:
n个这样的结点链结成一个链表,由于此链表的每个结点中只包含一个指针域,故称为线性链表或单链表。
2.基本概念
表头结点:链表中的第一个结点,包含指向第一个数据元素的指针以及链表自身的一些信息。
数据结点:链表中代表数据元素的结点,包含指向下一个数据元素的指针和数据元素的信息。
尾结点:链表中的最后一个数据结点,其下一个元素指针为空,表示无后继。
3.单链表的常用操作
同线性表顺序存储结构一样,链式存储结构的常用操作有创建、销毁、清空、返回单链表的长度、插入、获取元素位置、删除。
在C语言中,可以用结构体来定义链表中的指针域,链表中的表头结点也可以用结构体来实现。
//结点指针域定义,这里相当于一条地址链,一个地址里存着下一个地址,下一个地址存着下下个地址,以此类推。
typedef struct _tag_LinkListNode LinkListNode;
struct _tag_LinkListNode
{
LinkListNode* next;
};
typedef struct _tag_LinkList //单链表的头结点
{
LinkListNode header; //指向下一元素的指针
int length; //该单链表的长度
} TLinkList;
3.1创建
创建单链表时,我们先动态地申请表头结点,令链表的长度为0,表头结点的指针域为空。
typedef void LinkList;
LinkList* LinkList_Create()
{
TLinkList* ret = (TLinkList*)malloc(sizeof(TLinkList)); //动态地申请表头节点
if (ret != NULL)
{
ret -> length = 0;
ret -> header.next = NULL;
}
return ret;
}
3.2销毁
同顺序存储结构一样,单链表的销毁直接free掉就行。
void LinkList_Destroy(LinkList* list)
{
free(list);
}
3.3清空
清空单链表就只需让它回到初始状态即可,也就是长度为0,表头结点的指针域为空。
void LinkList_Clear(LinkList* list)
{
TLinkList* sList = (TLinkList*)list;
if (sList != NULL) //对单链表的合法性检测
{
sList -> length = 0;
sList -> header.next = NULL;
}
}
3.4获取长度
获取单链表的长度时,首先对其合法性进行检测,接着返回长度即可。
int LinkList_Length(LinkList* list)
{
TLinkList* sList = (TLinkList*)list;
int ret = -1;
if (sList != NULL) //对单链表的合法性检测
{
ret = sList -> length;
}
return ret;
}
3.5插入元素
在单链表中插入一个元素时,我们首先确定出插入的位置,然后先让插入元素的next指针指向插入的位置,再将原来的next指针断开,让其指向插入元素,其示意图为:
完整的操作步骤为:
(1)判断线性表是否合法;
(2)判断插入位置是否合法;
(3)由表头开始通过next指针移动pos次后,当前元素的next指针指向要插入的位置;
(4)将新元素插入;
(5)线性表长度加1。
int LinkList_Insert(LinkList* list, LinkListNode* node, int pos)
{
TLinkList* sList = (TLinkList*)list;
int ret = (sList != NULL) && (pos >= 0) && (node != NULL);
int i = 0;
if (ret)
{
LinkListNode* current = (LinkListNode*)sList; //让current指针指向表头
for (i = 0; (i < pos) && (current -> next != NULL); i++)
{
current = current -> next;
}
node -> next = current -> next;
current -> next = node;
sList -> length++;
}
return ret;
}
3.6获取元素
获取单链表中的一个元素的操作为:
(1)判断线性表是否合法;
(2)判断位置是否合法;
(3)由表头开始通过next指针移动pos次后,当前元素的next指针指向要获取的元素。
LinkListNode* LinkList_Get(LinkList* list, int pos)
{
TLinkList* sList = (TLinkList*)list;
LinkListNode* ret = NULL;
int i = 0;
if ((sList != NULL) && (pos >= 0) && (pos < sList -> length))
{
LinkListNode* current = (LinkListNode*)sList; //让current指针指向表头
for (i = 0; i < pos; i++)
{
current = current -> next;
}
ret = current -> next; //返回指向元素的指针
}
return ret;
}
3.7删除元素
在单链表中删除一个元素时,我们首先找到要删除元素的位置,然后设一个变量等于当前元素的next指针指向的元素,接着让当前元素的next指针指向变量的next指针指向的元素,其示意图为:
其操作步骤如下:
(1)判断线性表是否合法;
(2)判断删除位置是否合法;
(3)获取第pos个元素;
(4)将第pos个元素从链表中删除;
(5)线性表长度减1.
LinkListNode* LinkList_Delete(LinkList* list, int pos)
{
TLinkList* sList = (TLinkList*)list;
LinkListNode* ret = NULL;
int i = 0;
if ((sList != NULL) && (pos >= 0) && (pos < sList -> length))
{
LinkListNode* current = (LinkListNode*)sList; //让current指针指向表头
for (i = 0; i < pos; i++)
{
current = current -> next;
}
ret = current -> next; //指向元素的指针
current -> next = ret -> next;
sList -> length--;
}
return ret;
}
4.测试
在对单链表进行测试前,我们还要对数据元素进行结构体定义。
struct Value //数据元素的定义
{
LinkListNode header;
int val;
};
4.1头插法
头插法是通过将新结点逐个插入链表的表头结点来创建链表,每次申请一个新结点,写入相应的数据元素值,然后将新结点插入到表头结点之后。
#include <stdio.h>
#include <stdlib.h>
#include "LinkList.h"
struct Value //数据元素的定义
{
LinkListNode header;
int val;
};
int main(int argc, char *argv[])
{
LinkList* list = LinkList_Create(); //创建空链表
int i = 0;
struct Value v1;
struct Value v2;
struct Value v3;
struct Value v4;
struct Value v5;
v1.val = 1;
v2.val = 2;
v3.val = 3;
v4.val = 4;
v5.val = 5;
LinkList_Insert(list, (LinkListNode*)&v1, 0); //头插法,插入链表
LinkList_Insert(list, (LinkListNode*)&v2, 0);
LinkList_Insert(list, (LinkListNode*)&v3, 0);
LinkList_Insert(list, (LinkListNode*)&v4, 0);
LinkList_Insert(list, (LinkListNode*)&v5, 0);
for (i = 0; i < LinkList_Length(list); i++)
{
struct Value* pv = (struct Value*)LinkList_Get(list, i);
printf("%d\n", pv -> val);
}
LinkList_Destroy(list); //当不需要单链表时,必须清除掉
return 0;
}
得到的结果为:
可以看出元素每次都从表头插入。
4.2尾插法
尾插法是通过将新结点逐个插到链表的尾部来创建链表。
#include <stdio.h>
#include <stdlib.h>
#include "LinkList.h"
struct Value //数据元素的定义
{
LinkListNode header;
int val;
};
int main(int argc, char *argv[])
{
LinkList* list = LinkList_Create(); //创建空链表
int i = 0;
struct Value v1;
struct Value v2;
struct Value v3;
struct Value v4;
struct Value v5;
v1.val = 1;
v2.val = 2;
v3.val = 3;
v4.val = 4;
v5.val = 5;
LinkList_Insert(list, (LinkListNode*)&v1, LinkList_Length(list)); //尾插法,插入链表
LinkList_Insert(list, (LinkListNode*)&v2, LinkList_Length(list));
LinkList_Insert(list, (LinkListNode*)&v3, LinkList_Length(list));
LinkList_Insert(list, (LinkListNode*)&v4, LinkList_Length(list));
LinkList_Insert(list, (LinkListNode*)&v5, LinkList_Length(list));
for (i = 0; i < LinkList_Length(list); i++)
{
struct Value* pv = (struct Value*)LinkList_Get(list, i);
printf("%d\n", pv -> val);
}
LinkList_Destroy(list); //当不需要单链表时,必须清除掉
return 0;
}
得到的结果为:
5.总结
对于线性表的链式存储结构,其优点为:
(1)无需一次性定制链表的容量;
(2)在插入和删除元素时无需移动大量的元素。
其缺陷为:
(1)数据元素必须保存后继元素的位置信息;
(2)获取指定数据的元素操作需要访问之前的元素。