第 2 章 线性表
2.1 线性表
线性结构
线性结构是一个数据元素的有序(次序)集合
例如:26 个英文字母、一副扑克牌的点数
线性结构的四个特征
- 集合中必存在唯一的一个“第一元素”
- 集合中必存在唯一的一个“最后元素”
- 除最后元素之外,其它数据元素均有唯一的“后继”
- 除第一元素之外,其它数据元素均有唯一的“前驱”
常见的线性结构:线性表、栈、队列、循环队列、数组、串
例如:成绩单、通讯录、工资表、图书目录。常用的操作是增删改查。
线性表定义
具有相同数据类型的 n(n >= 0)个数据元素的有限序列,通常记为 ,其中 n 为表长,n=0 时称为空表。
线性表的 ADT 定义
ADT {
-
数据对象(D):
-
数据关系(S):
-
基本操作(P):
- 线性表初始化
- 销毁线性表
- 将线性表置空
- 判断线性表是否为空
- 求线性表表长
- 取表元
- 按值查找
- 取前驱
- 取后继
- 插入操作
- 删除操作
- 遍历操作 }
例:两个线性表取并集
void union(List &La, List Lb) {
La_len = ListLength(La);
Lb_len = ListLength(Lb);
for (i = 1; i <= Lb_len; i++) {
GetElem(Lb, i, e);
}
}
2.2 线性表的顺序存储与实现
用一组地址连续的存储单元依次存放线性表中的数据元素,即以存储位置相邻表示位序相继的两个数据元素之间的前驱和后继的关系,并以表中第一个元素的存储位置作为线性表的起始位置,称作线性表的基地址。
typedef struct {
ElemType *elem;
int length;
int listsize;
} SqList;
顺序表的初始化(时间复杂度为 )
bool InitList_Sq(SqList &L)
{
// 构造一个空的线性表 L
L.elem = (ElemType*)malloc(LIST_INIT_SIZE*sizeof(ElemType));
if (!L.elem)
exit(OVERFLOW); // 存储分配失败
L.length = 0; // 顺序表的初始长度为0
L.listsize = LIST_INIT_SIZE; // 初始存储容量
return OK;
} // InitList_Sq
按位序取表元
bool GetElem_Sq(SqList L, int i, ElemType &e)
{
if (i < 1 || i > L.length)
return ERROR; //i值不合法
e = L.elem[i - 1];
return OK;
}
顺序表的插入(时间复杂度为 )
bool Insert_Sq(SqList &L, int pos, ElemType e)
{
if (pos < 1 || pos > L.length + 1)
return ERROR; //插入位置不合法
if (L.length > L.lisesize)
return ERROR; //当前存储空间已满,无法插入
q = &(L, elem[i - 1]);
for (p = &(L.elem[L.length - 1]); p <= q; p--) { //从最后一个到插入位置
*(p + 1) = *p; //插入最后位置及之后元素右移
}
L.elem[pos - 1] = e; // 插入 e
L.length++; // 表长增1
return TRUE;
}
删除元素(时间复杂度为 )
bool ListDelete_Sq(SqList &L, int pos, ElemType &e)
{
if ((pos < 1) || (pos > L.length))
return FALSE ; // 删除位置不合法
e = L.elem[pos - 1];
for (j = pos + 1; j <= L.length; ++j) { //从pos后面一个开始一直到最后
L.elem[j - 2] = L.elem[j - 1]; } // 被删除元素之后的元素左移
L.length--; // 表长减1
return TRUE;
} // ListDelete
元素定位(时间复杂度为 )
在顺序表中“查询”是否存在一个和给定值满足判定条件的元素的最简单的办法是,依次取出结构中的每个元素和给定值进行比较。
int LocateElem_Sq(SqList L, ElemType e)
{ // 在顺序表L中查找第1个值与 e 满足相等条件的元素
// 若找到,则返回其在 L 中的位序,否则返回0。
i = 1; // i 的初值为第1元素的位序
p = L.elem; // p 的初值为第1元素的存储位置
while (i <= L.length && !equal(*p++, e))
++i; // 依次进行判定
if (i <= L.length)
return i; // 找到满足判定条件的数据元素为第 i 个元素
else
return 0; // 该线性表中不存在满足判定的数据元素
} // LocateElem
顺序表应用举例
应用1:将顺序表(a1,a2,... ,an)重新排列为以a1为界的两部分:a1 前面的值均比 a1 小,a1 后面的值都比a1大。
基本思路:
从第二个元素开始到最后一个元素,逐一向后扫描
1)当前数据元素比 a1 大时,表明它已经在后面,不必改变位置,继续比较下一个
2)当前数据元素比 a1 小,需要将它前面的元素都依次向后移动一个位置,然后把它置于最前面。(可以优化)
应用2:有顺序表A和B,其元素均按从小到大的升序排列,编写一个算法将它们合并成一个顺序表C,要求C的元素也是从小到大的升序排列。
2.3 线性表的链式存储与实现
单链表
为建立起数据元素之间的线性关系,对每个数据元素ai,除了存放数据元素的自身的信息ai之外,还需要和ai一起存放其后继元素ai+1所在的存储单元的地址,这两部分信息组成一个“结点”
typedef struct LNode {
ElemType data;
struct LNode *next;
} LNode,*LinkList;
建立链表
LinkList CreateList_T() {
LinkList L = NULL;
LNode *s, *r = NULL;
int x;
std:cin >> x;
while (x != flag) {
s = new LNode;
s->data = x;
if (L == NULL) L = s;
else r->next = s;
r = s;
std::cin >> x;
}
if (r != NULL) r->next = NULL;
return L;
}
求表长
int ListLength_1(LinkList L) {
LNode *p = L;
int j = 0;
while (p->next) {
p = p->next;
j++;
}
return j;
}
查找操作
- 按序号查找
Status GetElem(LinkList L, int i, ElemType &e)
{
LNode *p = L->next;
int j = 1;
while (p && j < i) {
p = p->next;
++j;
}
if (!p || j > i) return ERROR;
e = p->data;
return OK;
}
- 按值查找
LNode *LocateElem(LinkList L, ElemType e)
{
LNode *p = L->next;
while (p && p->data != e)
p = p->next;
return p;
}
插入操作
Status ListInsert(LinkList &L, int i, ElemType e)
{
LNode *p = L;
int j = 0;
while (p && (j < i - 1)) {
p = p->next;
++j;
}
if (!p || j > i - 1) return ERROR;
LNode *s = new LNode;
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
删除操作
Status ListDelete(LinkList &L, int i)
{
LNode *p = L;
int j = 0;
while (p && (j < i - 1)) {
p = p->next;
++j;
}
if (!p || j > i - 1) return ERROR;
LNode *q = p->next;
p->next = q->next;
delete q;
return OK;
}
循环链表
对于单链表而言,最后一个结点的指针域是空指针,如果将该链表头指针置入该指针域,则使得链表头尾结点相连,就构成了单循环链表。
双向链表
单链表的结点中只有一个指向其后继结点的指针域next,找后继的时间性能是O(1),找前驱的时间性能是O(n);可以付出空间的代价使得找前驱的时间性达到O(1):每个结点再加一个指向前驱的指针域。
用这种结点组成的链表称为双向链表。
typedef struct dlnode {
ElemType data;
struct dlnode *prior,*next;
} DLNode, *DLinkList;
和单链表类似,双向链表通常也是用头指针标识,也可以带头结点和做成循环结构。
在双向链表中,通过某结点的指针p既可以直接得到它的后继结点的指针p->next,也可以直接得到它的前驱结点的的指针p->prior。
在双向链表中插入一个结点:设p指向双向链表中某结点,s指向待插入的值为x的新结点,将*s插入到*p的前面。
在双向链表中删除指定结点:设p指向双向链表中某结点,删除*p。
单链表应用举例
- 已知单链表,写一算法将其逆置
算法思路:依次取原链表中的每个结点,将其作为第一个结点插入到新链表中去,指针q用来当前要处理的结点,指针p指向下一个要处理结点,p为空时结束。
void reverse(LinkList H)
{
LNode *p;
p = H->next;
H->next = NULL;
while (p) {
q = p;
p = p->next;
q->next = H->next;
H->next = q;
}
}
- 已知单链表,写一算法,删除其重复结点
算法思路:用指针p指向第一个数据元素结点,从它的后继结点开始到表的结束,查找与其值相同的结点并删除之;p指向下一个结点;依此类推,p指向最后结点时算法结束。
void pur_LinkList(LinkList H)
{
LNode *p, *q, *r;
p = H->next;
if (p == NULL) return;
while (p->next) {
q = p;
while (q->next) {
if (q->next->data == p->data) {
r = q->next;
q->next = r->next;
free(r);
} else {
q = q->next;
}
}
p = p->next;
}
}
- 设有两个单链表A、B,其中元素递增有序,编写算法将A、B归并成一个按元素值递减(允许有相同值)有序的链表C,要求用A、B中的原结点形成,不能重新申请结点。
算法思路:利用A、B两表有序的特点,依次进行比较,将当前值较小者摘下,插入到C表的头部,得到的C表则为递减有序的。
LinkList merge(LinkList A, LinkList B)
{
LinkList C;
LNode *p, *q, *s;
p = A->next;
q = B->next;
C = A;
C->next = NULL;
free(B);
while (p && q) {
if (p->data < q->data) {
s = p;
p = p->next;
} else {
s = q;
q = q->next;
}
s->next = C->next;
C->next = s;
}
if (p == NULL) p = q;
while (p) {
s = p;
p = p->next;
s->next = C->next;
C->next = s;
}
return C;
}
2.4 顺序表和链表的比较
顺序存储有3个优点:
-
方法简单,各种高级语言中都有数组,容易实现。
-
不用为表示结点间的逻辑关系而增加额外的存储开销。
-
顺序表具有按元素序号随机访问的特点。
顺序存储两个缺点:
-
在顺序表中做插入删除操作时,平均移动大约表中一半的元素,因此对n较大的顺序表效率低。
-
需要预先分配足够大的存储空间,估计过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。
链表的优缺点恰好与顺序表相反。
在实际中怎样选取存储结构呢?通常有以下几点考虑:
-
基于存储的考虑
-
基于运算的考虑
-
基于环境的考虑
2.5 单链表的应用:多项式
可以看这个题解
2.6 本章小结
-
线性表的概念
-
线性表的顺序存储
-
顺序存储线性表的操作
-
线性表的链式存储
-
链式存储线性表的操作
-
头结点及其作用
-
单链表
-
循环链表
-
双向链表
-
顺序存储线性表的特点
-
链式存储线性表的特点
-
单链表的应用:多项式及其运算