顺序表可以随时存储表中的任意一个元素,它的存储位置可以用一个简单直观的公式表示,但插入和删除操作需要移动大量元素。链式存储线性表时,不需要使用地址连续的存储单元,即不要求逻辑上相邻的元素在物理位置上也相邻,它通过“链”建立起数据元素之间的逻辑关系,因此插入和删除操作不需要移动元素,而只需要修改指针,但也会失去顺序表可随机存取的优点。
线性表的链式存储又称单链表它是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除存放数据元素自身的信息外,还需要存放一个指向其后继的指针。单链表结点结构如图所示。其中data为数据域,存放数据元素;next为指针域存放其后继结点的地址。
data | next |
---|
单链表中结点类型的描述如下:
typedef struct LNode // 定义单链表结点类型
{
ElemType data; // 数据域
struct LNode *next; // 指针域
}LNode, *LinkList;
利用单链表可以解决顺序表需要大量连续存储单元的缺点,但单链表附加指针域,也存在浪费存储空间的缺点。由于单链表的元素离散地分布在存储空间中,所以单链表时非随机存取的存储结构,即不能直接找到表中某个特定的结点,查找某个特定的结点时,需要从表头开始遍历,依次查找。
通常用头指针来标识一个单链表,如单链表L,头指针为NULL时表示一个空表。此外,为了操作上的方便,在单链表第一个结点之前附加一个结点,称为头结点。头结点的数据域可以不设任何信息,也可以记录表长等信息,头结点的指针域指向线性表的第一个元素结点;
头结点和指针的区分:
不管带不带头结点,头指针都始终指向链表的第一个结点,而头结点是带头结点的链表的第一个结点,结点通常不存储信息。
引入头结点的优点
1、由于第一个数据结点的位置被存放在头结点的指针域中,因此在此链表的第一个位置上的操作和在表的其它位置上的操作一致,无需进行特殊处理。
2、无论链表是否为空,其头指针都指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表也就得到了统一。
该方法从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头,即头结点之后。
头插法建立单链表:
// 头插法建立单链表
LinkList List_HeadInsert(LinkList &L) // 逆向建立单链表
{
LNode *s;
int x;
L=(LinkList)malloc(sizeof(LNode)); // 创建头结点
L->next=NULL; // 初始为空链表
scanf("%d", &x) // 输入结点的值
while(x!=9999) // 输入9999表示结束
{
s=(LNode*)malloc(sizeof(LNode)); // 创建新结点
s->data = x;
s->next = L->next; // 将新结点插入表中,L为头指针
l-next = s;
scanf("%d", &x);
}
return L;
}
采用头插法建立单链表时,读入数据的顺序与生成的链表中的元素的顺序是相反的,每个结点插入的时间为O(1),设单链表表长为n,则总时间复杂度为O(n)。
头插法建立单链表的算法虽然简单,但生成的链表中结点的次序和输入数据顺序不一样,若希望两次次序一样,则可采用尾插法。该方法将新结点插入到当前链表的表尾,为此必须增加一个尾指针r,使其始终指向当前链表的尾结点。
实现算法:
// 尾插法建立单链表
LinkList List_TailInsert(LinkList &L) // 正向建立单链表
{
int x; // 设元素类型为整型
L = (LinkList)malloc(sizeof(LNode));
LNode *s, *r = L; // r为表尾指针
scanf("%d", &x); // 输入结点的值
while(x != 9999) // 输入9999表示结束
{
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s; // r指向新的表尾结点
scanf("%d", );
}
r->next = NULL; // 尾结点指针置空
return L;
}
在单链表中从第一个结点出发,顺指针next域逐个往下搜索,直到找到第i个结点位置,否则返回最后一个结点指针域NULL。
算法实现:
// 按序号查找结点值
LNode *GetElem(LinkList L, int i)
{
int j = 1; // 计数,初始为1
LNode *p = L->next; // 第1个结点指针赋值给p
if(i==0)
{
return L; // 若i=0,则返回头结点
}
if(1 < 1)
{
return NULL; // 若i无效,则返回NULL
}
while(p&&j < i) // 从第1个结点开始找,查找到第i个结点
{
p = p-> next;
j++;
}
return p; // 返回第i个结点的指针,若i大于表长,则返回NULL;
}
按序号查找操作的时间复杂度为O(n)
从单链表的第一个结点开始,由前往后依次比较表中各结点数据域的值,若某结点数据域的值等于给定值e,则返回该结点的指针:若整个单链表中没有这样的结点,则返回NULL。
算法实现:
// 按值查找
LNode *LocateElem(LinkList L, ElemType e)
{
LNode *p = L->next;
while(p != NULL && p->data != a) // 从第1个结点开始查找data域为e的结点
{
p = p->next;
}
return p; // 找到后返回该结点指针,否则返回NULL
}
按值查找的时间复杂度为O(n)
插入节点操作将值为 x 的新结点插入到单链表的第 i 个位置上,先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第 i - 1个结点,再在其后插入新结点。
算法首先调用按序号查找算法GetElem(L, i - 1),查找第 i - 1 个结点。假设返回的第 i - 1个结点为 p,然后令新结点s的指针域指向 *p 的后继结点,再令结点 *p 的指针域指向新插入的结点 *s。
操作过程如下:
算法实现:
// 单链表的插入操作
1) p = GetElem(L, i-1) // 查找插入位置的前去结点
2) s->next = p->next; // 图中的操作步骤1
3) p->next = s; // 图中的操作步骤2
算法中,语句 2)和 3) 的顺序不能颠倒,否则,当先执行 p->next=s后,指向其后继的指针就不存在,再执行 s->next = p->next时,相当于执行了 s->next = s, 显然是错误的。本算法的主要时间开销在于查找第 i - 1 个元素,时间复杂度为O(n)。若在给定的结点后面插入新结点,则时间复杂度仅为O(1)。
前插操作是指在某结点的前面插入一个新结点,后插操作的定义刚好与之相反,在单链表插入算法中,通常都采用后插操作。
以上面的算法为例,首先调用函数GetElem()找到第 i - 1 个结点,即插入结点的前驱结点后,再对其执行后插操作,由此可知,对结点的前插操作均可转换为后插操作,前提是从单链表的头结点开始顺序查找到其前驱结点,时间复杂度为O(n)。
此外,可采用另一种方式将其转化为后插操作来实现,设待插入结点为 *s ,将 *s插入到 *p 的前面。我们仍然将 *s插入到 *p 的后面,然后将 p->data 与 s->data交换,这样既满足了逻辑关系,又能使时间复杂度为O(1).
算法实现:
// 对某一结点进行前插操作
// 将*s结点插入到*p之前的主要代码片段
s->next = p->next; // 修改指针域,不能颠倒
p->next = s;
temp = p->data; // 交换数据域部分
p->data = s->data;
s->data = temp;
删除结点操作是将单链表的第 i 个结点删除,先检查删除位置的合法性,后查找表第 i - 1 个结点,即被删除结点的前驱结点,再将其删除。过程如下:
假设结点 *p 为找到的被删除结点的前驱结点,为实现这一操作后的逻辑关系的变化,仅需修改 *p的指针域,即将 *p的指针域 next 指向 *q的下一结点。
// 删除结点
p = GetElem(L, i-1) // 查找删除位置的前驱结点
q = p->next; // 令q指向被删除结点
p->next = q->next //将*q结点从链中“断开”
free(q); // 释放结点的存储空间
该算法的主要时间也耗费在查找操作上,时间复杂度为O(n)
要删除某个特定的结点 *p,通常的做法是先从链表的头结点开始顺序找到其前驱结点,然后执行删除操作,算法的时间复杂度为O(n)
删除结点 *p 的操作可用删除 *p 的后继结点操作来实现,实质就是将其后继结点的值赋予其自身,然后删除后继结点,也能使得时间复杂度为 O(1)
// 删除结点*p
q = p->next; // 令q指向 *p 的后继结点
p->data = p->next->data; // 和后继结点交换数据域
p->next = q->next; // 将 *q 结点从链中“断开”
free(q); // 释放后继结点的存储空间
求表长操作就是计算单链表中数据结点(不含头结点)的个数,需要从第一个结点开始顺序依次访问表中的每个结点,为此需要设置一个计数器变量,每访问一个结点,计数器加 1 ,直到访问到空间结点为止,算法的时间复杂度为O(n)
【注意】因为单链表的长度是不包括头结点的,因此不带头结点和带头结点的单链表在求表长操作上会略有不同,对不带头结点的单链表,当表为空时,需要单独处理。