目录
线性表的定义和特点
线性表是具有相同特性的数据元素的一个有限序列。
线性表的顺序表示
顺序表的基本操作
InitList(&L) //初始化操作,建立一个空的线性表L
DestroyList(&L) //销毁已存在的线性表L
Clearlist(&L) //将线性表清空
ListInsert(&L, i, e) //在线性表L中第i个位置插入新元素e
ListDelete(&L, i, &e) //删除线性表L中第i位置元素,用e返回
IsEmpty(L) //若线性表为空,返回true,否则false
ListLength(L) //返回线性表L的元素个数
LocateElem(L, e) //L中查找与给定值e相等的元素,若成功返回该元素在表中的序号,否则返回0
GetElem(L, i, &e) //将线性表L中的第i个位置元素返回给e
顺序表的存储结构
// ---------顺序表的存储结构--------
#define MAXSIZE 100 // 顺序表可能达到的最大长度
typedef struct
{
ElemType *elem; // 存储空间的基地址
int length; // 当前长度
} SqList; // 顺序表的结构类型为SqList
顺序表的初始化
① 为顺序表L动态分配一个预定义大小的数组空间,使elem指向这段空间的基地址。
② 将表的当前长度设为0。
Status InitList(SqList &L)
{// 构造一个空的顺序表 L
L.elem= new ElemType[MAXSIZE]; // 为顺序表分配一个大小为MAXSIZE的数组空间
if(! L.elem) exit(OVERFLOW); // 存储分配失败退出
L.length=O; // 空表长度为0
return OK;
}
动态分配线性表的存储区域可以更有效地利用系统的资源 , 当不需要该线性表时 , 可以使用销毁操作及时释放占用的存储空间。
顺序表的取值
① 判断指定的位置序号 i 值是否合理 (I ≤ i ≤ L.length), 若不合理,则返回ERROR。
② 若 i 值合理,则将第 i 个数据元素 L.elem[i-1]赋给参数 e, 通过 e 返回第 1 个数据元素的传值。
Status GetElem(SqList L,int i,ElemType &e)
{
if (i<1 || i>L.length) return ERROR; //判断i值是否合理,若不合理, 返回 ERROR
e=L.elem[i-1]; //elem[i-1] 单元存储第 i 个数据元素
return OK;
}
显然,顺序表取值算法的时间复杂度为O(1)。
顺序表的查找
① 从第一个元素起,依次和 e相比较,若找到与 e相等的元素 L.elem[i], 则查找成功,返回该元素的序号 i+1。
② 若查遍整个顺序表都没有找到,则查找失败, 返回0。
int LocateELem(SqList L,ElemType e)
{//在顺序表1中查找值为e的数据元素, 返回其序号
for(i=O;i< L.length;i++)
if(L.elem[i)==e) return i+1; // 查找成功,返回需要i+1
return O; // 查找失败, 返回 0
}
由此可见,顺序表按值查找算法的平均时间复杂度为 O(n)。
顺序表的插入
线性表的插入操作是指在表的第i个位置插入一个新的数据元素 e, 使长度为 n 的线性表变成长度为n+1的线性表。
① 判断插入位置i是否合法(i 值的合法范围是i ≤ i ≤ n+1), 若不合法 则返回 ERROR。
② 判断顺序表的存储空间是否已满,若满则返回 ERROR。
③ 将第n个至第i个位置的元素依次向后移动一个位置,空出第i个位置(i =n+1时无需
移动)。
④ 将要插入的新元素e放入第i个位置。
⑤ 表长加1。
Status ListInsert(SqList &L,int i ,ElemType e)
{ // 在顺序表 L 中第 l 个位置之前插入新的元素 e, i值的合法范围是 1≤i≤L.length+l
if((i<l) || (i>L.length+1)) return ERROR; // i值不合法
if(L.length==MAXSIZE) return ERROR; // 当前存储空间已满
for (j=L.length-1; j>=i-1; j--)
L.elem[j+1]=L.elem[j]; // 插入位置及之后的元素后移
L.elem[i-1]=e; // 将新元素e放入第i个位置
++L.length; // 表长加1
return OK;
}
顺序表的删除
① 判断删除位置 i 是否合法(合法值为1 ≤ i ≤ n), 若不合法则返回 ERROR。
② 将第i+1个至第n个的元素依次向前移动一个位置 (i = n时无需移动)。
③ 表长减1。
Status ListDelete(SqList &L,int i)
{//在顺序表L中删除第i个元素,i值的合法范围是 1≤ i≤L.length
if((i<1) || (i>L.length)) return ERROR; // i值不合法
for (j=i; j <=L.length-1; j ++)
L.elem[j-1]=1.elem[j]; // 被删除元素之后的元素前移
--L.length; // 表长减1
return OK;
}
顺序表的操作算法分析
时间复杂度
查找、插入、删除算法的平均时间复杂度为O(n)。
空间复杂度
显然,顺序表操作算法的空间复杂度S(n)=O(1)(没有占用辅助空间)
顺序表优缺点
优点
存储密度大(结点本身所占存储量 / 结点结构所占存储量)。
可以随机存取表中任一元素。
缺点
在插入、删除某一元素时,需要移动大量元素。
浪费存储空间(必须定义最大长度)。
属于静态存储形式,数据元素的个数不能自由扩充。
顺序表总结
顺序表的特点:以物理位置相邻表示逻辑关系。
顺序表的优点:任一元素均可随机存取。
顺序表的缺点:进行插入和删除操作时,需移动大量的元素。存储空间不灵活。
线性表的链式表示——单链表
结点只有一个指针域的链表,称为单链表或线性链表。
概念
头指针:是指向链表中第一个结点的指针。
首元结点:是指链表中存储第一个数据元素a1的结点。
头结点:是在链表的首元结点之前附设的一个结点。
如何表示空表
无头结点时,头指针为空时表示空表。
有头结点时,当头结点的指针域为空时表示空表。
设置头结点的好处
1.便于首元结点的处理
首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其他位置一致,无须进行特殊处理。
2.便于空表和非空表的统一处理
无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了。
链式存储结构的特点
1.结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
2.访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等。
单链表的基本操作
单链表的存储结构
typedef struct Lnode{ // 声明结点的类型和指向结点的指针类型
ElemType data; // 结点的数据域
struct Lnode *next;// 结点的指针域
} Lnode, *LinkList; // LinkList为指向结构体Lnode的指针类型
定义链表L:LinkList L;
定义结点指针p:Lnode *p; 或者LinkList p;
单链表的初始化
① 生成新结点作为头结点,用头指针L 指向头结点。
② 头结点的指针域置空。
Status InitList(LinkList &L)
{ // 构造一个空的单链表L
L=new LNode; // 生成新结点作为头结点, 用头指针L指向头结点
L->next=NULL; // 头结点的指针域置空
return OK;
}
单链表的建立
头插法
void CreateList_H(LinkList &L, int n) {
L=new LNode;
L->next=NULL; // 先建立一个带头结点的单链表
for(i=n;i>0;--i){
p=new LNode; // 生成新结点(p=(LNode*)malloc(sizeof(LNode));)
cin>>p->data; // 输入元素值scanf(&p->data);
p->next=L->next;// 插入到表头
L->next=p;
}
}
尾插法
// 正位序输入n个元素的值,建立带表头节点的单链表L
void CreateList_R(LinkList &L, int n) {
L=new LNode; L->next=NULL;
r=L // 尾指针r指向头结点
for(i=0;i<n;++i){
p=new LNode; cin>>p->data; // 生成新节点,输入元素值
p->next=NULL;
r->next=p; // 插入到表尾
r=p; // r指向新的尾结点
}
}
单链表的取值
① 用指针p指向首元结点,用 j 做计数器初值赋为1。
② 从首元结点开始依次顺着链域 next 向下访问,只要指向当前结点的指针 p 不为空(NULL), 并且没有到达序号为i的结点,则循环执行以下操作:
• p指向下一个结点;
• 计数器 j 相应加1。
③ 退出循环时, 如果指针p为空, 或者计数器 j 大于 i, 说明指定的序号 i 值不合法(i大于表长n或i小于等于0), 取值失败返回ERROR; 否则取值成功, 此时 j=i 时,p所指的结点就是要找的第 i 个结点,用参数e保存当前结点的数据域, 返回OK。
Status GetElem(LinkList L, int i, ElemType &e)
{ // 在带头结点的单链表L中根据序号i获取元素的值,用e返回L中第i个数据元素的值
p=L->next;j=l; // 初始化,p指向首元结点,计数器j初值赋为1
while(p&&j<i) // 顺链域向后扫描,直到p为空或p指向第i个元素
{
p=p->next; // p指向下一个结点
++j; // 计数器j相应加1
}
if(!p||j>i)return ERROR; // i值不合法 i>n或i≤0
e=p->data; // 取第i个结点的数据域
return OK;
}
单链表的按值查找
① 用指针p指向首元结点 。
② 从首元结点开始依次顺着链域next向下查找, 只要指向当前结点的指针p不为空, 并且p所指结点的数据域不等于给定值e, 则循环执行以下操作: p指向下一个结点 。
③ 返回p。若查找成功,p此时即为结点的地址值,若查找失败,p的值即为NULL。
LNode *LocateELem(LinkList L, Elemtype e)
{ // 在带头结点的单链表L中查找值为e的元素
p=L->next; // 初始化,p指向首元结点
while(p && p->data!=e) // 顺链域向后扫描,直到p为空或p所指结点的数据域等于e
p=p->next; // p指向下一个结点
return p; // 查找成功返回值为e的结点地址p, 查找失败p为NULL
}
单链表的插入
将值为 e 的新结点插人到表的第 i 个结点的位置上, 即插入到结点 ai-1 与 ai 之间,具体插入过程如图所示, 图中对应的 5 个步骤说明如下。
① 查找结点ai-1 并由指针p指向该结点。
② 生成一个新结点*s。
③ 将新结点*s 的数据域置为 e。
④ 将新结点*s 的指针域指向结点ai。
⑤ 将结点*p 的指针域指向新结点*s。
Status ListInsert(LinkList &L,int i,ElemType e)
{ // 在带头结点的单链表L中第i个位置插入值为e的新结点
p=L;j=0;
while (p && (j<i-1))
{p=p->next;++j;} // 查找第i-1个结点,p指向该结点
if (!p || j>i-1) return ERROR; // i>n+l或者i<1
s=new LNode; // 生成新结点*s
s->data= e; // 将结点*s的数据域置为e
s->next=p->next; // 将结点 *s的指针域指向结点 ai
p->next=s; // 将结点*p的指针域指向结点*s
return OK;
}
单链表的删除
删除单链表的 第 i 个结点 ai 的具体过程如图所示,图中的对应的4个步骤说明如下。
① 查找结点ai-1 并由指针p指向该 结点。
② 临时保存待删除结点ai的地址在q中 ,以备释放。
③ 将结点*p的指针域指向ai的直接后继结点。
④ 释放结点ai的空间。
Status ListDelete(LinkList &L,int i)
{ // 在带头结点的单链表L中,删除第i个元素
p=L;j=0;
while ((p->next) && (j<i-1)) // 查找第i-1个结点,p指向该结点
{p=p->next; ++j;}
if ( !(p->next) || (j>i-1)) return ERROR; //当i>n或i<1时,删除位置不合理
q=p->next; // 临时保存被删结点的地址以备释放
p->next=q->next; // 改变删除结点前驱结点的指针域
delete q; // 释放删除结点的空间
return OK;
}
判断单链表是否为空
空表:链表中无元素,称为空链表(头指针和头结点仍然存在)。
int ListEmpty(LinkList L){ // 若L为空表,则返回1,否则返回0
if(L->next) // 非空
return 0;
else
return 1;
}
销毁单链表
Status DestroyList_L(LinkList &L){ // 销毁单链表L
Lnode *p; // 或LinkList p;
while(L){
p=L;
L=L->next;
delete p;
}
return OK;
}
清空单链表
Status ClearList(LinkList &L){ // 将L重置为空表
Lnode *p,*q; // 或LinkList p,q;
p=L->next;
while(p){ // 没到表尾
q=p->next;
delete p;
p=q;
}
L->next=NULL; // 头结点指针域为空
return OK;
}
求单链表L的表长
int ListLength_L(LinkList L){ // 返回L中数据元素个数
LinkList p;
p=L->next; // p指向第一个结点
i=0;
while(p){ // 遍历单链表,统计结点数
i++;
p=p->next;
}
return i;
}
链式存储结构的优缺点
优点
① 结点空间可以动态申请和释放。
② 数据元素的逻辑次序靠结点的指针来表示,插入和删除时不需要移动数据元素。
缺点
① 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。
② 链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该节点,这增加了算法的复杂度。
线性表的链式表示——循环链表
首尾相接的链表称为循环链表。
循环链表的操作
遍历循环链表的判断
注意:由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p->next是否为空,而是判断它们是否等于头指针。
循环条件:
合并两个循环链表
带尾指针循环链表的合并(将Tb合并在Ta之后)
LinkList Connect(LinkList Ta, LinkList Tb){
// 假设Ta、Tb都是非空的单循环链表
p=Ta->next; // ① p存表头节点
Ta->next=Tb->next->next; //② Tb表头节点连结Ta表尾
delete Tb->next; // ③ 释放Tb表头节点
Tb->next=p; // ④ 修改指针
return Tb;
}
线性表的链式表示——双向链表
结点有两个指针域的链表,称为双链表。
双向链表的操作
双向链表的结构定义
双向链表结点结构
typedef struct DuLNode{
Elemtype data;
struct DulNode *prior, *next;
} DulNode, *DuLinkList;
双向链表的插入
void ListInsert_Dul(DuLinkList &L, int i, ElemType e) {
// 在带头结点的双向循环链表L中第i个位置之前插入元素e
if(!(p=GetElemP_Dul(L,i))) return ERROR;
s=new DulNode; s->data=e;
s->prior=p->prior; p->prior->next=s;
s->next=p; p->prior=s;
return OK;
}
双向链表的删除
void ListDelete_Dul(DuLink &L, int i, ElemType &e) {
// 删除带头结点的双向循环链表L的第i个元素,并用e返回。
if(!(p=GetElemP_Dul(L,i))) return ERROR;
e=p->data;
p->prior->next=p->next;
p->next-prior=p->prior;
free(p);
return OK;
}
双向循环链表
和单链表的循环表类似,双向链表也可以有循环表
· 让头结点的前驱指针指向链表的最后一个结点。
· 让最后一个结点的后继指针指向头结点。
各种线性表的对比
顺序表和链表的比较
顺序表 | 链表 | ||
空间 | 存储空间 | 预先分配,会导致空间闲置或溢出现象 | 动态分配,不会出现存储空间闲置或溢出现象 |
存储密度 | 不用为表示结点间的逻辑关系而增加额外的存储开销,存储密度等于1 | 需要借助指针来体现元素间的逻辑关系,存储密度小于1 | |
时间 | 存取元素 | 随机存取,按位置访问元素的时间复杂度未O(1) | 顺序存取,按位置访问元素时间复杂度为O(n) |
插入、删除 | 平均移动约表中一半元素,时间复杂度为O(n) | 不需移动元素,确定插入、删除位置后,时间复杂度为O(1) | |
适用情况 | ① 表长变化不大,且能事先确定变化的范围 ② 很少进行插入或删除操作,经常按元素位置序号访问数据元素 | ① 长度变化较大 ② 频繁进行插入或删除操作 |
单链表、循环链表和双向链表的时间效率比较
查找表头节点(首元结点) | 查找表尾结点 | 查找结点*p的前驱结点 | |
带头结点的单链表L | L->next 时间复杂度O(1) | 从L->next依次向后遍历 时间复杂度O(n) | 通过p->next无法找到其前驱 |
带头结点仅设头指针L的循环单链表 | L->next 时间复杂度O(1) | 从L->next依次向后遍历 时间复杂度O(n) | 通过p->next可以找到其前驱 时间复杂度O(n) |
带头结点仅设尾指针R的循环单链表 | R->next 时间复杂度O(1) | R 时间复杂度O(1) | 通过p->next可以找到其前驱 时间复杂度O(n) |
带头结点的双向循环链表L | L->next 时间复杂度O(1) | L->prior 时间复杂度O(1) | p->prior 时间复杂度O(1) |
线性表的合并
线性表的合并
① 分别获取 LA表长 m和 LB 表长n。
② 从 LB 中第 1 个数据元素开始, 循环n次执行以下操作:
• 从 LB 中查找第 i (1 ≤ i ≤ n) 个数据元素赋给 e;
• 在 LA 中查找元素 e, 如果不存在, 则将 e 插在表LA 的最后。
void MergeList(List &LA,List LB)
{ // 将所有在线性表 LB中但不在LA中的数据元素插入到LA中
m=ListLength(LA); n=ListLength(LB); //求线性表的长度
for(i=1;i<=n;i++)
{
GetElem(LB,i,e); // 取 LB中第1个数据元素赋给 e
if (! LocateElem(LA, e)) // LA中不存在和 e 相同的数据元素
ListInsert(LA,++m,e); // 将 e 插在LA的最后
}
}
算法时间复杂度为O(m*n)
有序表的合并
① 创建一个表长 为m+n 的空表LC。
② 指针pc初始化, 指向LC的第一个元素。
③ 指针pa和pb初始化,分别指向LA和LB的第一个元素。
④ 当指针pa和pb均未到达相应表尾时, 则依次比较pa和pb所指向的元素值 , 从LA或LB中 "摘取“ 元素值 较小的结点插入到LC的最后。
⑤ 如果pb巳到达LB的表尾, 依次将LA的剩余元素插入LC的最后。
⑥ 如果pa已到达LA的表尾, 依次将LB的剩余元素插入LC的最后。
void MergeList_Sq(SqList LA,SqList LB,SqList &LC)
{ // 已知顺序有序表LA和LB的元素按值非递减排列
// 归并LA和LB得到新的顺序有序表LC, LC的元素也按值非递减排列
LC.length=LA.length+LB.length; // 新表长度为待合并两表的长度之和
LC.elem=new ElemType[LC.length]; // 为合并后的新表分配一个数组空间
pc=LC.elem; // 指针pc 指向新表的第一个元素
pa=LA.elem; pb=LB.elem; // 指针pa 和pb 的初值分别指向两个表的第一个元素
pa_last=LA.elem+LA.length-1; // 指针pa_last指向LA的最后一个元素
pb_last=LB.elem+LB.length-1; // 指针pb_last指向LB的最后一个元素
while ((pa<=pa_last) && (pb<=pb_last)) // LA和LB均未到达表尾
{
if(*pa< =*pb ) *pc++ = *pa++; // 依次 "摘取“ 两表中值较小的结点插人到LC的最后
else *pc++ = *pb++;
}
while (pa<=pa_last) *pc++=*pa++; // LB已到达表尾,依次将LA的剩余元素插人LC的最后
while (pb<=pb_last) *pc++=*pb++; // LA已到达表尾,依次将LB的剩余元素插入LC的最后
}
算法的时间复杂度为O(m+ n)。
空间复杂度也为O(m+ n)。
链式有序表的合并
① 指针 pa和 pb 初始化,分别指向LA和LB的第一个结点。
② LC的结点取值为LA的头结点。
③ 指针 pc初始化,指向LC的头结点。
④ 当指针 pa 和 pb 均未到达相应表尾时, 则依次比较 pa 和 pb 所指向的元素值, 从 LA 或LB 中 "摘取“ 元素值较小的结点插入到 LC 的最后。
⑤ 将非空表的剩余段插入到 pc 所指结点之后。
⑥ 释放 LB 的头结点。
void MergeList_L(LinkList &LA,LinkList &LB,LinkList &LC)
{ // 已知单链表 LA和LB的元素按值非递减排列
// 归并LA 和 LB得到新的单链表 LC, LC的元素也按值非递减排列
pa=LA->next;pb=LB->next; // pa 和 pb的初值分别指向两个表的第一个结点
LC=LA; // 用LA的头结点作为LC的头结点
pc=LC; //pc的初值指向LC的头结点
while(pa&&pb)
{ // LA 和 LB均未到达表尾,依次 “摘取”两表中值较小的结点插人到LC的最后
if(pa->data<=pb->data) // '摘取"pa所指结点
{
pc->next=pa; // 将pa所指结点链接到pc所指结点之后
pc=pa; // pc指向pa
pa=pa->next; // pa指向下一结点
}
else // "摘取"pb所指结点
{
pc->next=pb; // 将pb所指结点链接到pc所指结点之后
pc=pb; // pc指向pb
pb=pb->next; // pb指向下一结点
}
} // while
pc->next=pa?pa:pb; // 将非空表的剩余段插入到pc所指结点之后
delete LB; // 释放LB的头结点
}
算法的时间复杂度为O(m+ n)。
空间复杂度为O(1)。