第二章 线性表
2.3 线性表的链式存储结构
一、单链表
用一组任意的存储单元存储线性表的数据元素。
以元素(数据元素的映象)
+ 指针(指示后继元素存储位置)
= 结点
(表示数据元素 或 数据元素的映象)
以线性表中第一个数据元素 的存储地址作为线性表的地址,称作线性表的头指针。
有时为了操作方便,在第一个结点之前虚加一个“头结点”,以指向头结点的指针为链表的头指针。
二、结点和单链表的 C 语言描述
typedef struc LNode{ // 定义单链表结点
ElemType data;
struct LNode *next; // 指向后继的指针域
}LNode, *LinkList
(*p)表示p所指向的结点
(*p).dataÛp->data表示p指向结点的数据域
(*p).nextÛp->next表示p指向结点的指针域
三、单链表操作的实现
GetElem(L, i, e) // 取第i个数据元素
ListInsert(&L, i, e) // 插入数据元素
ListDelete(&L, i, e) // 删除数据元素
ClearList(&L) // 重置线性表为空表
CreateList(&L, n) // 生成含 n 个数据元素的链表
线性表的操作
GetElem(L, i, &e)
在单链表中的实现:
单链表是一种顺序存取的结构,为找第 i 个数据元素,必须先找到第 i-1 个数据元素。
因此,查找第 i 个数据元素的基本操作为:移动指针,比较 j 和 i 。
令指针 p 始终指向线性表中第 j 个数据元素。
Status GetElem_L(LinkList L, int i, ElemType &e) {
// L是带头结点的链表的头指针,以 e 返回第 i 个元素
p = L->next; j = 1; // p指向第一个结点,j为计数器
while (p && j<i) { p = p->next; ++j; }
// 顺指针向后查找,直到 p 指向第 i 个元素
// 或 p 为空
if ( !p || j>i )
return ERROR; // 第 i 个元素不存在
e = p->data; // 取得第 i 个元素
return OK;
} // GetElem_L 算法时间复杂度为:O(ListLength(L))
线性表的操作ListInsert(&L, i, e)
在单链表中的实现
有序对 <ai-1, ai>改变为 <ai-1, e> 和<e, ai>
可见,在链表中插入结点只需要修改指针。但同时,若要在第 i 个结点之前插入元素,修改的是第 i-1 个结点的指针。
因此,在单链表中第 i 个结点之前进行插入的基本操作为:找到线性表中第i-1个结点,然后修改其指向后继的指针。
Status ListInsert_L(LinkList L, int i, ElemType e) {
// L 为带头结点的单链表的头指针,本算法
// 在链表中第i 个结点之前插入新的元素 e
p = L; j = 0;
while (p && j < i-1)
{ p = p->next; ++j; } // 寻找第 i-1 个结点
if (!p || j > i-1)
return ERROR; // i 大于表长或者小于1
……
} // LinstInsert_L
算法的时间复杂度为:O(ListLength(L))
s = (LinkList) malloc ( sizeof (LNode));
// 生成新结点
s->data = e;
s->next = p->next; p->next = s; // 插入
return OK;
线性表的操作ListDelete (&L, i, &e)
在链表中的实现:
有序对<ai-1, ai> 和 <ai, ai+1>
改变为 <ai-1, ai+1>
在单链表中删除第 i 个结点的基本操作为:找到线性表中第i-1个结点,修改其指向后继的指针。
q = p->next; p->next = q->next;
e = q->data; free(q);
Status ListDelete_L(LinkList L, int i, ElemType &e) {
// 删除以 L 为头指针(带头结点)的单链表中第 i 个结点
p = L; j = 0;
while (p->next && j < i-1) { p = p->next; ++j; }
// 寻找第 i 个结点,并令 p 指向其前趋
if (!(p->next) || j > i-1)
return ERROR; // 删除位置不合理
q = p->next; p->next = q->next; // 删除并释放结点
e = q->data; free(q);
return OK;
} // ListDelete_L
算法的时间复杂度为:O(ListLength(L))
操作 ClearList(&L) 在链表中的实现:
void ClearList(&L) {
// 将单链表重新置为一个空表
while (L->next) {
p=L->next; L->next=p->next;
}
} // ClearList
算法时间复杂度:O(ListLength(L))
如何从线性表得到单链表?
链表是一个动态的结构,它不需要予分配空间,因此生成链表的过程是一个结点“逐个插入” 的过程。
例如:逆位序输入 n 个数据元素的值,建立带头结点的单链表。
操作步骤:
一、建立一个“空表”;
二、输入数据元素an,建立结点并插入;
三、输入数据元素an-1,建立结点并插入;
四、依次类推,直至输入a1为止。
void CreateList_L(LinkList &L, int n) {
// 逆序输入 n 个数据元素,建立带头结点的单链表
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; // 插入
}
} // CreateList_L
算法的时间复杂度为:O(Listlength(L))
单链表的优点
它是一种动态结构,整个存储空间为多个链表共用
不需预先分配空间
插入、删除操作方便
单链表的缺点
指针占用额外存储空间
不能随机存取,查找速度慢
四、其他形式的链表
循环链表:最后一个结点的指针域的指针又指回第一个结点的链表。
和单链表的差别仅在于,判别链表中最后一个结点的条件不再是“后继是否为空”,而是“后继是否为头结点”。
双向链表
typedef struct DuLNode {
ElemType data; // 数据域
struct DuLNode *prior;
// 指向前驱的指针域
struct DuLNode *next;
// 指向后继的指针域
双向循环链表
双向链表的操作特点:
“查询” 和单链表相同。
“插入” 和“删除”时需要同时修改两个方向上的指针。
插入
s->prior= p->prior; p->prior ->next=s;
s->next = p; p->prior = s ;
删除
p->prior->next = p->next;
p->next->prior = p->prior;