本系列文章是笔者复习自用,参考教材为严蔚敏编著的《数据结构(C语言版)》,如能对大家有所帮助实属荣幸
本篇的内容主要是线性表的链式表示和实现,主要包括线性链表的定义和常用函数的实现。
3.1 单链表的类型定义
单链表是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息外,还需要存放一个指向其后继的指针。单链表结构如下所示,data为数据域,存放数据元素;next为指针域,存放其后继结点的地址。
单链表中节点类型的描述如下:
typedef struct LNode{
ElemType data;//数据域
struct LNode *next;//指针域
}LNode,*LinkList;
利用单链表可以解决顺序表需要大量连续存储单元的缺点,但同时,单链表附加指针域,也存在浪费存储空间的缺点。
顺序表占用的是一段连续的空间,而链表占用的是离散的空间,属于非随机存取的存储结构,即不能直接找到表中某个特定的结点,需要从头开始遍历,依次查找。
通常用头指针来标识一个单链表,头指针的指针域指向该单链表的第一个元素,如果为null,则该单链表为空表。
3.2 单链表上基本操作的实现
1.采用头插法建立单链表
该方法从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头。其过程为,让新结点的指针域指向头结点的指针域所指结点,然后修改头结点的指针域,使其指向新结点。
头插法建立建立单链表的算法如下:
LinkList List_HeadInsert(LinkList &L){
LNode *s; int x;
L = (LinkList)malloc(sizeof(LinkList));//创建头结点空间
L->next = NULL;
printf("请输入一个数字\n");
scanf("%d", &x);
while (x!=9999)//当x等于9999时退出
{
s = (LNode *)malloc(sizeof(LNode));//创建新的结点空间
s->data = x;
s->next = L->next;//链式插入原则都是先建新链,再拆旧链
L->next = s;
printf("请输入一个数字\n");
scanf("%d", &x);
}
return L;
}
采用头插法建立链表时,链表的顺序和读入数据的顺序相反,每个结点插入的时间为O(1),设单链表长为n,则该算法时间复杂度为O(n)。
2.采用尾插法建立单链表
头插法虽然简单,但是生成链表与输入数据的顺序不一致,不便于操作。若使用尾插法,则可保持链表中次序和输入数据的顺序一致。尾插法的思路为,生成新结点,改变表中最后一个结点的指针域,使其指向新结点,并且使新结点的指针域为null,为方便操作,一般增加一个指针t,使其始终指向链表的尾结点。
尾插法建立单链表的算法如下:
LinkList List_TailInsert(LinkList &L){
LNode *s,*t; int x;//t为指向尾结点的指针
L = (LinkList)malloc(sizeof(LinkList));
L->next = NULL;
t = L;//空表时指向头结点
printf("请输入一个数字\n");
scanf("%d", &x);
while (x!=9999)
{
s = (LNode *)malloc(sizeof(LNode));//创建新的结点空间
s->data = x;
t->next = s;//始终指向尾结点
s->next = NULL;
t = s;
printf("请输入一个数字\n");
scanf("%d", &x);
}
return L;
}
时间复杂度与头插法系相同,都为O(n)。
3.按序号查找结点
在单链表中从头结点出发,沿着指针域依次往后搜索,直到找到第i个结点为止,否则返回最后一个结点指针域,即为null。
按序号查找结点值的算法如下:
LNode *GetElem(LinkList L, int i){
int index=1;
if (i < 1)
return NULL;
LNode *p = L->next;
while (i != index && p->next != NULL){//遍历,直到找到值或找到结尾
p = p->next;
index++;
}
return p;
}
按序号查找操作的时间复杂度为O(n)。
4.插入结点操作
插入结点操作指的是将值为x的新结点插入到单链表的第i的位置上,先检查位置i的合法性,然后找到待插入结点的前驱,然后生成新结点并插入。
算法思想为先生成一个新结点,其数据域为输入的数据,从头开始遍历,找到第i-1个元素,将新结点的指针域等于第i-1个元素的指针域,然后将第i-1个元素的指针域改为指向新结点,值得注意的是,这类插入算法都是先连后断。
插入结点操作的算法如下:
LinkList List_Insert(LinkList &L, ElemType x, int i){
if (i < 1)//判断位置取值是否合法
return L;
LNode *p; int index = 1;
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = x;
p = L;
while (i!=index && p->next!=NULL){//遍历,直到找到位置
p = p->next;
index++;
}
s->next = p->next;
p->next = s;
return L;
}
本算法的主要开销是遍历寻找到前驱结点,时间复杂度为O(n),将结点插入表中的时间复杂度仅为O(1)。
5.删除结点操作
删除结点操作指的是将链表中第i个结点删除。先检查位置i的合法性,然后找到待删除的结点,将其删去。
算法思想为,通过遍历找到第i个结点和第i-1个结点,改变第i-1个结点的指针域使其等于第i个结点的指针域,然后释放第i个结点的空间,值得注意的是,删除操作一般只需要连不需要断。
删除结点操作的算法如下:
LinkList List_Delete(LinkList &L, int i){
int index = 1;
LNode *p = L,*q=L->next;//p指向待删除结点的前驱,q指向待删除结点
while (i != index && q->next != NULL){
p = p->next;
q = q->next;
index++;
}
p->next = q->next;
free(q);//释放q
return L;
}
3.3 顺序表和链表的比较
1.存取方式
顺序表可以顺序存取,也可以随机存取,即顺序表可以按顺序遍历值,也可以使用次序直接访问值;链表只可以顺序存取,只能按顺序遍历值。因此在查找和修改方面,顺序表有优势。
2.逻辑结构和物理结构
使用顺序表时,在逻辑上相邻的元素,在物理上也相邻;使用链表时,在逻辑上相邻的元素,在物理上并不相邻,甚至可能不按序排列。
3.操作
按值查找时,顺序表和链表的算法相似,都是从头开始遍历,故时间复杂度也相同,为O(n);按序查找时,因为顺序表支持随机存取,所以时间复杂度为O(1),但链表的时间复杂度仍为O(n)。
4.空间分配
使用顺序表的静态分配时,往往会导致空间浪费或空间不够,需要预先计算所需的空间;使用顺序表的动态分配时,虽然可以扩容,但仍会造成资源浪费,效率低下。但使用链表时,空间可以随时增加,操作灵活、方便。