目录
一、线性表的类型定义
线性表是最常用且最简单的一种数据结构。一个线性表是n个数据元素的有限序列。线性表中的每个元素可以是一个数、一个符号甚至更复杂的内容,但是同一线性表中元素必定有相同的特性,即属同一数据对象,相邻数据元素之间存在着序偶关系。
除此之外,线性表一定满足线性结构的特点。
线性结构的特点:在数据元素的非空有限集中,(1)存在唯一的一个被称做“第一个”的数据元素;(2)存在唯一的一个被称做“最后一个”的数据元素;(3)除第一个之外,集合中的每个数据元素均只有一个前驱;(4)除最后一个之外,集合中每个数据元素均只有一个后继。
线性表有两种表示方式(存储方式):一种是顺序结构存储,我们叫做顺序表;另一种是链式结构存储,我们叫做链表。这会在后面分别介绍。
线性表的基本操作:
InitList(&L) // 构造一个空的线性表L
DestroyList(&L) // 销毁已存在的线性表L
ClearList(&L) // 将L重置为空表
ListInsert(&L, i, e) // 在线性表L中的第i个位置插入新元素e
ListDelete(&L, i, &e) // 删除线性表L中的第i个元素,并用e返回其值
IsEmpty(L) // 判断L是否为空表
ListLength(L) // 返回线性表L中的数据元素个数
LocateElem(L, e) // 查找与给定值e满足特定关系的数据元素的位序,若不存在则返回0
GetElem(L, i, &e) // 用e返回线性表L中第i个数据元素的值
二、顺序表
顺序表是用一组地址连续的存储单元依次存储线性表的数据元素。
假设线性表的每个元素占 个字节,那么根据顺序表的特性,可以得到第 i 个元素的地址和第 1 个元素的地址关系为
顺序表的图示结构:
接下来看看顺序表的代码实现:
1.存储结构:
#define LIST_INIT_SIZE 100 // 线性表存储空间的初始分配量
#define LISTINCREMENT 10 // 线性表存储空间的分配增量
typedef struct{
ElemType *elem; // 存储空间基址
int length; // 当前长度
int listsize; // 当前分配的存储容量(以sizeof(ElemType)为单位)
}SqList;
2.初始化操作:
Status InitList_Sq(SqList &L){
// 构造一个空的线性表L
L.elem = (ElemType *)malloc(LIST_INIT_SIZE * sizeof(ElemType)); // malloc()为头文件stdlib.h下的分配地址函数
if(!L.elem) exit(OVERFLOW); // 存储分配失败
L.length = 0; // 空表长度为0
L.listsize = LIST_INIT_SIZE; // 初始存储容量
return OK;
} // InitList_Sq
时间复杂度:
3.销毁操作:
void DestroyList_Sq(SqList &L){
// 销毁线性表L
if(L.elem) free(L.elem); // 释放存储空间
} // DestroyList_Sq
时间复杂度:
4.清空操作:
void ClearList_Sq(SqList &L){
// 将线性表L重置为空表
L.length = 0; // 将线性表长度重置为0
} // ClearList_Sq
时间复杂度:
5.插入操作:
Status ListInsert_Sq(SqList &L, int i, ElemType e){
// 在顺序线性表L中第i个位置之前插入新的元素e
// i的合法值为1<=i<=ListLength(L)+1
if(i < 1 || i > L.length + 1) return ERROR; // i值不合法
if(L.length >= L.listsize){ // 当前存储空间已满,增加分配
newbase = (ElemType *)realloc(L.elem, (L.listsize + LISTINCREMENT) * sizeof(ElemType)); // realloc()为头文件stdlib.h下的重新分配地址函数
if(!newbase) exit(OVERFLOW); // 存储分配失败
L.elem = newbase; // 新基址
L.listsize += LISTINCREMENT; // 增加存储容量
}
q = &(L.elem[i - 1]); // q为插入位置
for(p = &(L.elem[L.length - 1]); p >= q; --p) *(p + 1) = *p; // 插入位置及之后的元素右移
*q = e; // 插入e
++L.length; // 表长增1
return OK;
} // ListInsert_Sq
平均操作次数:
时间复杂度:
6.删除操作:
Status ListDelete_Sq(SqList &L, int i, ElemType &e){
// 在顺序线性表L中删除第i个元素,并用e返回其值
// i的合法值为1<=i<=ListLength(L)
if(i < 1 || i > L.length) return ERROR; // i值不合法
p = &(L.elem[i - 1]); // p为被删除元素的位置
e = *p; // 被删除元素的值赋给e
q = L.elem + L.length - 1; // 表尾元素的位置
for(++p; p <= q; ++p) *(p - 1) = *p; // 被删除元素之后的元素左移
--L.length; // 表长减1
return OK;
} // ListDelete_Sq
平均操作次数:
时间复杂度:
7.判断空表:
int IsEmpty_Sq(SqList L){
// 判断线性表L是否为空
if(L.length == 0) return TRUE; // 数据元素长度为0,返回1;反之返回0
else return FALSE;
} // IsEmpty_Sq
时间复杂度:
8.求长度操作:
int ListLength_Sq(SqList L){
// 求线性表L的长度
return L.length;
} // ListLength_Sq
时间复杂度:
9.查找操作:
int LocateElem_Sq(SqList L, ElemType e){
// 在顺序线性表L中查找第1个值与e满足compare()的元素的位序
// 若找到,则返回其在L中的位序,否则返回0
i = 1; // i的初值为第1个元素的位序
p = L.elem; // p的初值为第1个元素的存储位置
while(i <= L.length && !compare(*p++, e)) ++i; // compare()为查找的条件
if(i <= L.length) return i; // 若找到则返回数据元素的位序
else return 0;
} // LocateElem_Sq
平均操作次数:(其中n为顺序表中数据元素的个数)
时间复杂度:
10.取值操作:
Status GetElem_Sq(SqList L, int i, ElemType &e){
// 用e返回顺序表L中第i个元素的值
if(i < 1 || i > L.length) return ERROR; // 判断i的值是否合理
e = L.elem[i - 1]; // 将第i个元素值赋给e
return OK;
} // GetElem_Sq
时间复杂度:
总结
顺序表的优点:
(1)存储密度大(结点本身所占存储量/结点结构所占存储量)
(2)可以随机存取表中任意元素
顺序表的缺点:
(1)再插入、删除某一元素时,需要移动大量元素
(2)浪费存储空间
(3)属于静态存储形式,数据元素的个数不能自由扩充
三、链表
链表是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。
链表中每个数据都有两部分信息,一部分是自身元素的信息(数据域),另一部分是指示前继或后继的信息(指针域),这两部分组成的数据元素的存储映像称为结点。指针域中存储的信息称为指针或链。
1.单链表(线性链表)
单链表是每个结点只有一个数据域和一个指针域构成,且指针域指示后继结点的信息,尾元结点的指针为空(NULL)。
单链表的图示结构:
接下来看看单链表的代码实现:
1.存储结构:
typedef struct LNode{
ElemType data; // 数据元素
struct LNode *next; // 指针元素
}LNode, *LinkList;
2.初始化操作:
Status InitList_L(LinkList &L){
// 初始化单链表L
L = (LinkList)malloc(sizeof(LNode)); // 生成头结点
L->next = NULL; // 头结点指针初始化为空
return OK;
} // InitList_L
时间复杂度:
3.销毁操作:
Status DestroyList_L(LinkList &L){
// 销毁单链表L
LNode *p; // 或LinkList p;
while(L){ // 从头结点开始依次释放结点
p = L;
L = L->next;
free(p); // 释放结点
}
return OK;
} // DestroyList_L
时间复杂度:
4.清空操作:
Status ClearList_L(LinkList &L){
// 清空单链表L
LNode *p, *q; // 或LinkList p, q;
p = L->next;
while(p){ // 循环单链表,从首元结点开始释放结点
q = p->next;
free(p);
p = q;
}
L->next = NULL; // 头结点指针变为空
return OK;
} // ClearList_L
时间复杂度:
5.插入操作:
Status ListInsert_L(LinkList &L, int i, ElemType e){
// 在带头结点的单链线性表L中第i个位置之前插入元素e
p = L; j = 0;
while(p && j < i - 1){ // 寻找第i-1个结点
p = p->next; ++j;
}
if(!p || j > i - 1) return ERROR; // i小于1或者大于表长加1
s = (LinkList)malloc(sizeof(LNode)); // 生成新结点
s->data = e; // 插入L中
s->next = p->next;
p->next = s;
return OK;
} // ListInsert_L
时间复杂度:查找部分,插入部分
6.删除操作:
Status ListDelete_L(LinkList &L, int i, ElemType &e){
// 在带头结点的单链线性表L中,删除第i个元素,并由e返回其值
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; // 删除并释放结点
p->next = q->next;
e = q->data;
free(q);
return OK;
} // ListDelete_L
时间复杂度:查找部分,删除部分
7.判断空表:
int IsEmpty_L(LinkList L){
// 判断链表L是否为空
if(L->next) return FALSE; // 非空
else return TRUE;
} // IsEmpty_L
时间复杂度:
8.求长度操作:
int ListLength_L(LinkList L){
// 求单链表L的表长
LNode *p;
p = L->next; // p指向首元结点
i = 0;
while(p){ // 遍历单链表,统计结点数
i++;
p = p->next;
}
return i; // 返回结点数
} // ListLength_L
时间复杂度:
9.查找操作:
LNode *LocateElem_L(LinkList L, ElemType e){
// 在单链表L中查找满足条件compare()的数据元素
// 若找到则返回该数据元素的地址,否则返回NULL
p = L->next;
// int j = 0; 若要返回位序则定义一个j记数据的位置
while(p && !compare(p->data, e)){
p = p-> next;
// j++;
}
if(L->next != NULL) return p; // 若要返回位序,则return j;
else return NULL;
} // LocateElem_L
时间复杂度:
10.取值操作:
Status GetElem_L(LinkList L, int i, ElemType &e){
// L为带头结点的单链表的头指针。
// 当第i个元素存在时,其值赋给e并返回OK,否则返回ERROR
p = L -> next; j = 1; // 初始化,p指向第1个结点,j为计数器
while(p && j < i){ // 顺指针向后查找,直到p指向第i个元素或p为空
p = p->next; ++j;
}
if(!p || j > i) return ERROR; // 第i个元素不存在
e = p->data; // 取第i个元素
return OK;
} // GetElem_L
时间复杂度: 查找部分,取值部分
11.头插法创建单链表
void CreateList_H(LinkList &L, int n){
// 逆位序输入n个元素的值,建立带表头结点的单链线性表L
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_H
时间复杂度:创建 n 个元素的表为,每次插入元素为
12.尾插法创建单链表
void CreateList_R(LinkList &L, int n){
// 正位序输入n个元素的值,建立带表头结点的单链表L
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL; // 先建立一个带头结点的单链表
r = L;
for(i = 0; i < n; ++i){
p = (LinkList)malloc(sizeof(LNode)); // 生成新结点
scanf(&p->data); // 输入元素值
p->next = NULL;
r->next = p; // 插入到表尾
r = p; // 指向新的尾元结点
}
} // CreateList_R
时间复杂度:创建 n 个元素的表为,每次插入元素为
2.循环链表
循环链表是每个结点只有一个数据域和一个指针域构成,且指针域指示后继结点的信息,尾元结点的指针域指示头结点(或首元结点)的信息。
循环链表有两种表示形式:
第一种是用头指针表示,那么表示如图所示:
这种方法查找的时间复杂度为
,但查找
的时间复杂度为
,实用性较低;
第二种是用尾指针表示,表示如图所示:
这种方法查找不管是查找还是
时间复杂度都是
,实用性较高。
循环链表的大部分操作都与单链表类似,这里就不再赘述了,可以自行进行分析。
3.双向链表
双向链表是每个结点只有一个数据域和两个指针域构成,指针域指示前驱结点和后继结点的信息,头结点的前驱指针为NULL,尾元结点的后继指针为NULL。
双向链表的图示结构:
双向循环链表的图示结构:
这里重点来看双向(循环)链表存储结构和双向循环链表的插入、删除操作:
1.存储结构:
typedef struct DuLNode{
ElemType data; // 数据元素
struct DuLNode *prior; // 指向前驱的指针
struct DuLNode *next; // 指向后继的指针
}DuLNode, *DuLinkList;
2.插入操作:
Status ListInsert_DuL(DuLinkList &L, int i, ElemType e){
// 在带头结点的双链循环线性表L中第i个位置之前插入元素e
// i的合法值为1<=i<=表长+1
if(!(p == GetElemP_DuL(L, i))) // 在L中确定插入位置
return ERROR; // p = NULL,即插入位置不合法
if(!(s == (DuLinkList)malloc(sizeof(DuLNode)))) return ERROR;
s->data = e;
s->prior = p->prior; p->prior->next = s;
s->next = p; p->prior = s;
return OK;
} // ListInsert_DuL
时间复杂度:
3.删除操作:
Status ListDelete_DuL(DuLinkList &L, int i, ElemType &e){
// 删除带头结点的双链循环线性表L的第i个元素,i的合法值为1<=i<=表长
if(!(p == GetElemP_DuL(L, i))) // 在L中确定删除位置
return ERROR; // p = NULL,即插入位置不合法
e = p->data;
p->prior->next = p->next;
p->next->prior = p->prior;
free(p);
return OK;
} // ListDelete_DuL
时间复杂度:
总结
链表的优点:
(1)能灵活地分配内存空间
(2)插入或删除元素效率高,每次操作仅需的时间
链表的缺点:
(1)不像数组一样通过下表读取元素,而是每次都要从表头开始一个一个读取
(2)查询第k个元素需要的时间