线性表的链式存储结构是用一组任意的存储单元存储线性表的数据元素,而非连续的存储单元。每个结点中既包含数据元素又包含指向后继结点的指针。通常我们会设置一个头指针指向单链表,有时为了算法的一致性,我们会在第一个结点之前附加一个头结点,头结点的数据域内可以不存放任何信息,也可存放诸如线性表长度之类的附加信息。
1、存储结构
typedef struct LNode
{
ElemType data; //
数据域
struct LNode
*next; // 指针域,指向下一个结点,next为结构体变量
}LNode, *LinkList; //
LinkList为指向结构体LNode的指针
另一种定义
typedef struct LNode
*Link; // Link为指向结构体LNode的指针,即Link为 LNode* 的别名
struct LNode
{
ElemType data; //
数据域
Link next; //
指针域
};
注意:typedef的用法。
2、单链表的取数据元素操作
取第i个结点的数据,L为带头结点的单链表的头指针。
Status GetElem_L( LinkList L, int i,
ElemType &e )
{
LinkList p; int j;
p = L -> next; j = 1; //
p指向第一个数据结点,而非头结点。j作为计数器
while(
p && j<i ) // 向后查找,直到p指向第i个元素或p为空才跳出循环
{
p = p -> next;
++ j;
}
if( !p
|| j>i) return ERROR; // 第i个元素不存在,返回ERROR
e = p -> data; //
取得第i个元素的数据
return OK;
}
注意:(1)单链表并非随机存取的数据结构,每次取数据都要从前向后查找,这种操作效率低于顺序表。
(2)时间复杂度O(n)。
3、单链表的插入操作
在第i个结点前插入新结点。首先生成一个新结点,然后找到第i-1个结点,将新节点的指向第i个结点,最后修改第i-1个结点的指针域,使其指向新节点。第i-1个结点的指针域要最后改,否则会丢失后面的结点。
Status ListInsert_L( LinkList L, int i,
ElemType e ) // L带头结点
{
LinkList p, s; int j;
p = L; j = 0; //
p指向头结点
while(
p && j<i-1 ) // 寻找第i-1个结点
{
p = p -> next;
++ j;
}
if( !p
|| j>i-1) return ERROR; // 未找到第i-1个节点
s = ( LinkList )malloc( sizeof(LNode) ); //
生成新节点
s -> data = e;
s -> next = p -> next; //
新节点指向第i个结点
p -> next = s; // 第i-1个结点指向新节点
return OK;
}
注意:(1)加粗的两句话不能写颠倒,否则会丢失结点。
(2)时间复杂度是O(n)。时间浪费在查找第i-1个结点上了。
4、单链表的删除操作
删除单链表的第i个结点。首先查找第i-1个结点,然后让他指向第i+1个结点,最后释放第i个结点。
Status ListDelete_L( LinkList L, int i,
ElemType &e) // L带头结点
{
LinkList p, q; int j;
p = L; j = 0;
while(
p -> next && j < i-1 ) // 查找第i个结点,令p指向其前驱
{
p = p -> next;
++ j;
}
if(!(p
-> next)||j > i-1 ) return ERROR; // 删除位置不合理
q = p -> next; //
令q指向第i个结点
p -> next = q -> next; //
令第i-1个结点指向第i+1个结点
e = q -> data; free(q); //
释放第i个结点的内存
return OK;
}
注意:(1)先用p指针找第i-1个结点,然后令q指针指向第i个结点。
(2)时间复杂度是O(n)。
5、逆向建立带头结点的单链表(头插法)
从表尾到表头逆向插入节点。所建立的的单链表与原序列相反。
void CreateList_L_H( LinkList
*L, int n) // 建立n个数据元素的单链表
{
LinkList p; int i;
*L = ( LinkList )malloc( sizeof(LNode) ); //
建立头结点
(*L) -> next = NULL;
for(
i = n; i > 0; --i )
{
p = ( LinkList )malloc( sizeof(LNode) ); //
生成新节点
scanf( &p->data ); //
输入数据
p -> next = (*L) -> next; //
插入到表头
(*L) -> next = p;
}
}
注意:(1)这种方法建立的单链表与给定的序列相反。
(2)由于L的内容变化了,所以参数用了指针的指针*L。使用时可写成:CreateList_L_H(&L, 10)。
(3)时间复杂度:O(n)。
6、尾插法建立带头结点的单链表
每次都从表尾插入新节点。所建单链表与原序列顺序相同。
void CreateList_L_R(
LinkList *L, int n) // 建立n个数据元素的单链表
{
LinkList p, r; int i;
*L = ( LinkList )malloc( sizeof(LNode) ); //
建立头结点
(*L) -> next = NULL;
r = *L;
for(
i = n; i > 0; --i )
{
p = ( LinkList )malloc( sizeof(LNode) ); //
生成新节点
scanf( &p->data ); //
输入数据
r -> next = p; //
插入到表尾
r = p;
}
}
注意:(1)这种方法建立的单链表与给定的序列相同。
(2)时间复杂度:O(n)。
7、两个有序单链表的归并操作
将两个有序的单链表归并为一个有序的单链表。原有单链表是非递减的且都有头结点。
void MergeList_L( LinkList La, LinkList Lb, LinkList *Lc)
{
LinkList pa, pb, pc;
pa = La -> next; pb = Lb -> next; // pa指向La的第1个数据节点,pb指向Lb的第1个数据节点
*Lc = pc = La; //
La的头结点作为Lc的头结点,pc指向头结点
while( pa && pb ) //
pa,pb都不空
{
if( pa -> data < pb -> data)
{
pc -> next = pa; // pc的后继设为pa所指结点
pc = pa; //
pc指向pa所指结点
pa = pa -> next; // pa指向下一结点
}
else
{
pc -> next = pb; // pc的后继设为pb所指结点
pc = pb; //
pc指向pb所指结点
pb = pb -> next; // pb指向下一结点
}
}
pc -> next = pa ? pa : pb;
// 如果pa指针不为空,说明La还有剩余结点,则pc指向pa所指结点;反之,Lb不为空,pc指向pb所指结 // 点
free(Lb); // 释放Lb头指针
}
注意:(1)与顺序表的归并算法思想相同,但是链表的归并算法不需要申请Lc的空间,空间复杂度较低。
(2)这个算法中La,Lb的内容没变,Lc的内容变了,使用时可写成:MergeList_L(La, Lb, &Lc)。
(3)时间复杂度:O(n)。
8、销毁单链表
从前向后,依次将结点释放,最后释放头结点。
void DestroyList_L(
LinkList *L )
{
LinkList p, q;
q = NULL; p = (*L) -> next;
while(
p ) // 如果p不为空,则循环
{
q = p -> next; //
q指向p的后继结点
free(p); //
释放p所指结点
p = q; //
p指向q所指的结点
}
free(L); *L = NULL; //
释放头结点
}
注意:(1)从前向后遍历链表,依次删除。
(2)时间复杂度:O(n)。
9、删除区间操作
假设单链表的数据为实数,且无序。删除数据为某区间(a, b)的结点。
Status RemoveRange_L(LinkList L, int lower, int upper)
// 带头结点的情况
{
LinkList t, x;
if(!L
-> next) return REEOR; // L为空表
for(t
= L, x = t -> next; t && x = t -> next;)
{
if( x -> data < lower || x -> data > upper) //
范围之外的结点保留
{
t = t -> next;
continue;
}
t -> next = x -> next; free(x); //
范围值内的结点删除
}
return OK;
}
Status RemoveRange_L(LinkList L, int lower, int upper)
// 不带头结点的情况
{
LinkList t, x;
if(!L) return ERROR;
for( t = L, x = t -> next; t && ( x = t -> next );
) // 考察第2至n个结点
{
if( x -> data < lower || x -> data
> upper)
{
t = t -> next;
continue;
}
t -> next = x -> next; free(x);
}
if( L -> data >= lower && L -> data <= upper) //
考察第1个结点
{
x = L;
L = L -> next;
free(x);
}
return OK;
}
注意:(1)带头结点的链表操作起来比较方便,对于不带头结点的链表要多考虑一步,即第一个结点的情况。
(2)时间复杂度:O(n)。