2.0 线性表
线性表的顺序和链式结构
线性链表、循环链表、双向链表
主要内容
线性结构特点
在非空有限集内,存在唯一始末元素、除头全有唯一前驱,除尾全有唯一后继。
2.1 线性表的类型定义
2.2 线性表的顺序表示与实现
2.3 线性表的链式表示与实现
2.3.1 线性链表
2.3.2 循环链表
2.3.3 双向链表
线性表的逻辑结构
定义
一个线性表是n个数据元素的有限序列
特点
1.线性表中所有元素的性质相同。
2.除第一个和最后一个数据元素之外,其它数据元素有且仅有一个前驱和一个后继。第一个数据元素无前驱,最后一个数据元素无后继。
3.数据元素在表中的位置只取决于它自身的序号。
ADT
ADT List{
数据对象:D={ai|ai∈ElemSet,i=1,2,…,n,n≥0}
数据关系:R={<ai-1,ai>|ai,ai-1∈D,1=2,…,n}
基本操作:
InitList( &L);
DestroyList(&L);
ClearList(&L);
ListEmpty(L);
ListLength(L);
GetElement(L,i,&e);
LocateElement(L,e,compare( ))
PriorElement(L,cur_e,&pre_e)
NextElement(L,cur_e,&next_e)
ListInsert(&L,i,e);
ListDelete(&L,i,&e);
ListTraverse(L,visit( ))
}ADT List
例题
线性表合并
假设有两个集合A和B分别用两个线性表LA和LB表示,现要求一个新的集合A=A∪B。
void union( List &La, List Lb)
{
// 将所有在线性表Lb中但不在La中的数据元素插入到La中
La_Len = ListLength( La ); // 求线性表的长度
Lb_Len = ListLength( Lb );
for( i = 1; i <= Lb_Len; i++)
{
GetElem( Lb, i, e); // 取Lb中第i个数据元素赋给e
if( !LocateElem( La, e, equal))
ListInsert( La, ++La_Len, e);
// La中不存在和 e 相同的数据元素,则插入之
}
} // union
时间复杂度:
O(ListLength( La ) * ListLength( Lb ))
非递减线性表La,Lb的合并
void MergeList( List La, List Lb, List &Lc )
{
InitList( Lc );
i = j = 1; // i和j分别是La和Lb的序号
k = 0; //k是Lc的序号
La_Len = ListLength( La );
Lb_Len = ListLength( Lb );
while((i <= La_Len) && (j <= Lb_Len))
{
GetElem( La, i, ai );
GetElem( Lb, j, bj );
if( ai < = bj ) {
ListInsert( Lc, ++k, ai); ++i;
}
else {
ListInsert( Lc, ++k, bj ); ++j;
}
}
while( i <= La_Len ){//若La非空,把La剩余的数据元素插入到Lc中
GetElem( La, i++, ai );
ListInsert( Lc, ++k, ai );
}
while( j <= Lb_Len ){//若Lb非空,把La剩余的数据元素插入到Lc中
GetElem( Lb, j++, bj );
ListInsert( Lc, ++k, bj );
}
}//MergeList
时间复杂度:
O(ListLength(La ) + ListLength( Lb))
线性表的顺序存储结构
顺序表:用一组地址连续的存储单元存放一个线性表
- 元素地址计算方法:
LOC(ai)=LOC(a1)+(i-1)*L
L—一个元素占用的存储单元个数
LOC(ai)—线性表第i个元素的地址 - 特点:
实现逻辑上相邻—物理地址相邻
实现随机存取
顺序表的类型定义
#define LIST_INIT_SIZE 100 // 线性表存储空间的初始分配量
#define LISTINCREMENT 10 // 线性表存储空间的分配增量
typedef struct{
ElemType *elem;//存储空间基址
int length; //当前长度
int listsize //当前分配的存储容量(以sizeof(ElemType)为单位)
}Sqlist;
顺序表的重要操作
初始化顺序表
Status 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
顺序表的插入操作
思路
1.输入是否有效?
2.当前表是否已经满?
3.移动 i 后的元素
4.插入元素
5.表长增1
代码
Status ListInsert_Sq(Sqlist &L, int i, ElemType e)
{ // 在顺序线性表L的第i个位置之前插入新的元素e
// i的合法值为1<=i<=ListLength_Sq(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));
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;
}
顺序表的删除操作
思路
1.输入是否有效?
2.删除(前移元素)
3.表长减1
代码
Status ListDelete_Sq(SqList &L,int i,ElemType &e){
//在顺序线性表L中删除第.i个元素,并用e返回其值
//i的合法值为 1≤i≤L.length
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
顺序表的查找操作
int LocateElem(SqList L,ElemType e, Status(*compare)(ElemType,ElemType)){
ElemType *p;
int i=1; // i的初值为第1个元素的位序
p=L.elem; // p的初值为第1个元素的存储位置
while(i<=L.length&&!compare(*p++,e))
++i;
if(i<=L.length)
return i;
else
return 0;
}
顺序表的遍历(函数指针使用说明书)
- 初始条件:
顺序线性表L已存在 - 操作结果:
依次对L的每个数据元素调用函数vi()。一旦vi()失败,则操作失败。
若在vi()的形参加'&',表明可通过调用vi()改变元素的值。
Status ListTraverse(SqList ,void(*vi)(ElemType&)){
ElemType *p;
int i;
p=L.elem;
for(i=1;i<=L.length;i++)
vi(*p++);
return OK;
}
顺序表的一般操作
销毁顺序表
Status DestroyList_Sq ( SqList &L) {
if (!L.elem)
return ERROR; // 若表L不存在
free (L.elem); // 若表L已存在,回收动态分配的存储空间
L.elem = null;
L.length = 0;
L.Listsize = 0;
return OK;
}// DetroyList_Sq
置空线性表
Status ClearList_Sq ( SqList &L) {
if (!L.elem)
return ERROR; // 若表L不存在
L.length = 0; //若表L已存在,将L置空
return OK;
}// ClearList_Sq
判断空表
Status ListEmpty(SqList L)
{ // 初始条件:顺序线性表L已存在。
//操作结果:若L为空表,则返回TRUE,否则返回FALSE
if(L.length==0)
return TRUE;
else
return FALSE;
}
求表长
int ListLength(SqList L){
//初始条件:顺序线性表L已存在。
//操作结果:返回L中数据元素个数
return L.length;
}
取元素操作
Status GetElem_Sq ( SqList L, int i, ElemType &e ) {
if((i< 1)||(i>L.length))
return ERROR; // i 非法
e=L.elem[i-1]; //将顺序表中第i 个元素赋值给 e
return OK;
}// GetElem_Sq
顺序存储结构的优缺点
优点
- 逻辑相邻,物理相邻
- 可随机存取任一元素
- 存储空间使用紧凑
缺点
- 插入、删除操作需要移动大量的元素
- 预先分配空间需按最大空间分配,利用不充分
- 表容量难以扩充
线性表的链式存储结构
特点
- 用一组任意的存储单元存储线性表的数据元素
- 利用指针实现了用不相邻的存储单元存放逻辑上相邻的元素
- 每个数据元素ai,除存储本身信息外,还需存储其直接后继的信息
- 结点{
数据域:元素本身信息
指针域:指示直接后继的存储位置
}
线性链表的定义
结点中只含一个指针域的链表叫线性链表,也叫单链表。
typedef struct LNode {
ElemType data;
struct LNode *next;
}Lnode,*LinkList;
LNode* p 和 LinkList p
意思一样,都是建立一个Lnode型的单链表
定义出来的都是1个Lnode型的指针变量,通常用他指向头结点
特别地,注意
LinkList P,Q; /P,Q都是指针 /
LNode P,Q;/只有P是指针* /
- 头结点
在单链表第一个结点前附设一个结点叫头结点
头结点指针域为空表示线性表为空
链表的重要操作
初始化链表
Status InitList_L (LinkList &L) {
L = (LinkList)malloc(sizeof(LNode));
if (!L)
exit(OVERFLOW);
L->next = null;
Return OK;
}// InitList_L
链表的按值查找
Status LocateNode_L(LinkList L,Elemtype key,LNode &e){
p=L–>next;
while( p && p–>data!=key)
p=p–>next;
if(!p)
return ERROR;
e=p;
return OK;
}
链表的插入操作
思路
1.寻找第i-1个结点:
顺指针向后查找,直到p指向第i-1个元素或p->next为空
2.分配新空间
3.插入节点
代码
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 则 j>i-1
// i大于表长+1 则p为空
s=(LinkList)malloc(sizeof(LNode)); //分配新结点
s->data=e;
s->next=p->next; p->next=s; //插入新结点
return OK;
}//LinstInsert_L
链表的删除操作
思路
1.寻找第i-1个结点:
顺指针向后查找,直到p指向第i-1个元素或p->next为空
2.删除结点(修改其后继指针)
3.回收(释放)结点空间
代码
Status ListDelete_L(LinkList &L, int i, ElemType &e){
//在带头结点的单链线性表L中,删除第i个元素,并由e返回其值
p=L; j=0;
while (p->next&&j<i-1){ //寻找第i-1个结点
p=p->next; ++j;
}
if(!(p->next)||j>i-1)
return ERROR; // 表中无第i个结点(i不合法)
// i<1 则 j>i-1
// i>表长 则 p->next为空
q=p->next;p->next=q->next; //删除结点
e =q->data; free(q); // 释放结点空间
return OK;
}//LinstDelete_L
链表的一般操作
取元素操作
Status GetElem_L(LinkList L, int i, ElemType &e){
//L为带头结点的单链表的头指针。
//当第i个元素存在时,其值赋给e并返回OK,否则返回ERROR
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个元素不存在
e=p->data; //取第i个元素
return OK;
}//GetElem_L
头插法建立单链表
思路
① 建立新节点
② 向新节点中添入内容
③ 使新节点指向链头(第一个元素)
④ 改变头指针,指向新节点
代码
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
头插法的幻想图插了一个想不明白就想两个节点
尾插法建立单链表
思路
① 建立新节点
② 向新节点中添入内容
③ 将新节点链入链尾
④ 改变尾指针
代码
void CreateList_L(LinkList &L, int n) {
//输入n个元素的值,建立带表头结点的线性链表
L=(LinkList)malloc(sizeof (LNode));
L->next=NULL; //先建立一个带头结点的单链表
r=L;
for (i=1; i<=n;i++){
p=(LinkList)malloc(sizeof(LNode));//生成新结点
scanf(&p->data);//输入元素值
p->next=NULL;
r->next=p;
r=p;
}
}//CreateList_L
归并2个有序链表
void MergeList_L(LinkList &La, LinkList &Lb, LinkList &Lc){
pa=La->next;
pb=Lb->next;
Lc=pc=La; //用La的头结点作为Lc的头结点
while(pa && pb){
if (pa->data<=pb->data){
pc->next=pa;
pc=pa;
pa=pa->next;
}
else {
pc->next=pb;
pc=pb;
pb=pb->next;
}
}
pc->next=pa?pa:pb;//插入剩余段
free(Lb);//释放Lb的头结点
}//MergeList_L
单链表结构的优缺点
优点
- 动态结构,整个存储空间为多个链表共用
- 不需预先分配空间
缺点
- 指针占用额外存储空间
- 不能随机存取,查找速度慢
循环链表
- 在单链表中,将终端结点的指针域NULL改为指向表头结点的或开始结点,就得到了单链形式的循环链表,并简单称为单循环链表。
- 为了使空表和非空表的处理一致,循环链表中也可设置一个头结点。
- 由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p—>next是否为空,而是判断它们是否等于头指针。
双向链表
结构定义
typedef struct DulNode {
ElemType data;
struct DuLNode *prior;
struct DuLNode *next;
}DuLNode, *DuLinkList;
插入结点程序
Status ListInsert_DuL(DuLinklist L, int i, ElemType e)
{
DuLinklist s,p;
if (!(p=GetElemP_DuL(L,i)))
return ERROR; // 在L中确定第i个元素的位置指针p
if(!(s = (DuLinklist)malloc(sizeof(DuLNode)))) return ERROR;
s->data = e; // 构造数据为e的结点s
s->prior = p->prior; p-> prior ->next = s;
s->next = p; p->prior = s;
return OK;
} // ListInsert_DuL
删除结点程序
Status ListDelete_DuL(DuLinklist L, int i, ElemType &e)
{
DuLinklist p;
if (!(p=GetElemP_DuL(L,i)))
return ERROR; // 在L中确定第i个元素的位置指针p
e = p->data; // 删除的结点的值存入e
p-> prior ->next = p->next;
p->next->prior = p->prior;
free(p);
return OK;
} // ListDelete_DuL
带头结点的线性链表类型
具有实用意义的线性链表
typedef struct LNode // 结点类型
{
ElemType data;
LNode *next;
}*Link,*Position;
struct LinkList // 链表类型
{
Link head,tail; // 分别指向线性链表中的头结点和最后一个结点
int len; // 指示线性链表中数据元素的个数
};