序:
本章目的:
解决线性表的计算机实现问题
2.1 线性表的定义和特点(逻辑结构)
2.2 线性表的顺序存储实现(顺序表)
2.3 线性表的链式存储实现(链表)
重点:特点、基本操作和有关算法
(1)线性表顺序存储; (2)线性表的链式存储; (3)双向链表; (4)循环链表。
难点:
线性表链式存储实现,包括单链表、双向链表、循环链表的基本操作和有关算法。
2.1线性表的类型定义(逻辑结构)
- 线性表的定义:
线性表是n 个类型相同
数据元素的有限序列,通常记作(a1, a2, a3, …, an )。
线性表本身是一种逻辑结构
。
-
线性结构的基本特征为:
(1)集合中必存在唯一的一个“第一元素”;
(2)集合中必存在唯一的一个 “最后元素”;
(3)除最后元素之外,均有唯一的后继
;
(4)除第一元素之外,均有唯一的前驱
。 -
线性结构的
抽象数据类型描述
:
ADT List
{
数据对象:D={ ai | ai ∈ElemSet, i=1,2,...,n, n≥0 }
{称 n 为线性表的表长;
称 n=0 时的线性表为空表。}
数据关系:R1={ <ai-1 ,ai >|ai-1 ,ai∈D, i=2,...,n }
基本操作:
InitList( &L ) //构造一个空的线性表L。
DestroyList( &L ) //销毁线性表 L
ListEmpty( L ) //判断栈L是否空
ListLength( L ) //求L的长度
PriorElem( L, cur_e, &pre_e ) //求前驱的值
NextElem( L, cur_e, &next_e ) //求后继的值
GetElem( L, i, &e ) //取i位置的值
LocateElem( L, e, compare( ) ) //在线性表中查找e
ListTraverse(L, visit( )) //遍历线性表
ClearList( &L ) //清空线性表
ListInsert( &L, i, e ) //在i位置插入e
ListDelete(&L, i, &e) //删除i位置的元素
}ADT List
- 基本操作的应用
例 2-1 合并 求线性表并集
假设:有两个集合 A 和 B 分别用两个线性表 LA 和 LB 表示,
即:线性表中的数据元素即为集合中的成员。
现要求一个新的集合A=A∪B。
例2-1操作步骤:
(1) 从线性表LB
中依次察看每个数据元素
; GetElem(LB, i,&e)//获取B中的每个e
(2) 依值
在线性表LA
中进行查访; LocateElem(LA, e, equal( ))//将获取到的e从A中查访
(3) 若不存在
,则插入LA
中。 ListInsert(LA, n+1, e)
例2-1 求线性表并集的算法
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);//取Lb中第i个数据元素赋给e
if(!LocateElem(La,e,equal()))// La中不存在和e相同的数据元素,则插入之
ListInsert(La,++La_len,e);
}
}
例2-1算法时间复杂性
O( ListLength( La )*ListLength( Lb ) )
例 2-2 有序归并 (归并排序的基础)
将两个 “数据元素按值递增有序
排列” 的线性表La和Lb归并
到线性表Lc,且Lc具有同样的性质
。
设 La=(a1,…ai,….an)
Lb=(b1,…bj,….bm)
Lc=(c1,…ck,…cm+n)
例2-2算法 归并算法
//递增有序La和Lb归并到Lc,依旧递增有序排列
//ABC中元素的下标也是从1开始,0号不存数据
void MergeList(List La,List Lb,List &Lc)
{
InitList(Lc);//空线性表Lc
i=j=1;k=0;//注意是算法
La_len=ListLength(La);
Lb_len=ListLength(Lb);
/*
1. 分别获取 A 和 B 的元素
2. 判断元素哪个小,小的添加到 LC 中
3. 如果不是小的,那就等待下一次循环比较大小进行添加
4. 当循环结束后,分别将剩余的元素按照顺序添加进链表 LC 中
*/
while( (i<=La_len)&&(j<=Lb_len))//范围内
{
GetElem(La,i,&ai);//分别获取 A 和 B 的元素
GetElem(Lb,j,&bj);
if(ai<=bj)//判断元素哪个小,小的添加到 LC 中
{
ListInsert(Lc,++k,ai);//表,位置,元素
++i;
}
else
{
ListInsert(Lc, ++k, bj);
++j;
}
}
//当循环结束后,分别将剩余的元素按照顺序添加进链表 LC 中
while(i<=La_len)//La不空
{
GetElem(La,i,&ai);
i++;
ListInsert(Lc,++k,ai);
}
while(j<=Lb_len)//Lb不空
{
GetElem(Lb,j,&bj);
j++;
ListInsert(Lc,++k,bj);
}
}// merge_list
例2-2算法时间复杂性
O( ListLength( La )+ListLength( Lb ) )
2.2 线性表的顺序表示和实现(顺序表)
存储线性表,就是要保存至少两类信息
:
- 线性表中的
数据元素
; - 线性表中数据元素的
顺序关系
。
-
线性表的
顺序表示
的定义:
以数据元素x 的存储位置
和数据元素 y 的存储位置
之间的某种关系
表示逻辑关系<x, y>
。
线性表的顺序表示
最简单的一种顺序映象方法
是:令x的存储位置和y 的存储位置相邻
。 -
线性表的顺序表示图示:
用一组地址连续
的存储单元依次存放线性表中的数据元素。
-
数据元素地址之间的关系
以“存储位置相邻
”表示有序对<ai-1,ai>
时, ai-1和ai的地址关系如下:
LOC(ai) = LOC(ai-1) + C
C为一个数据元素所占存储量
所有数据元素的存储位置
均取决于第一个数据元素的存储位置
。
LOC(ai) = LOC(a1) + (i-1)×C
LOC(a1) 基地址
顺序表的实现
1、数据元素的数据类型Elemtype实现
方式1:
struct data
{
char name[8];
int age;
char sex[2];
float score;
};
typedef struct data ElemType;
方式2:
typedef struct data
{
char name[8];
int age;
char sex[2];
float score;
}ElemType;
方式3:
typedef struct
{
char name[8];
int age;
char sex[2];
float score;
}ElemType;
2、顺序表数据类型的实现
方式1:静态数组实现
typedef struct
{
ElemType data[30];
int length;// 线性表长度(现存储)
int listsize;// 线性表规模(可容纳)
}SqList;
//SqList是一个顺序表数据类型;
Sqlist L ;//声明线性表变量L
//L.data是数组名
方式2:动态数组实现
#define LIST_INIT_SIZE 80//线性表存储空间的初始分配量
#define LISTINCREMENT 10//线性表存储空间的分配增量
typedef struct
{
ElemType *elem;//存储空间的基址
int length;//当前长度
int listsize;//当前分配的存储容量 (以sizeof(ElemType)为单位)
}SqList;//顺序表
Sqlist L;//声明线性表变量L
L.elem = (ElemType*) malloc (LIST_INIT_SIZE*sizeof (ElemType));
//以后用realloc
//L.elem是数组名
3.顺序表的优缺点
(1) 优点
a.节省存储空间;
b.对线性表中第i个结点的操作易于实现;
c.容易查找一个结点的前驱和后继。
(2)缺点
a.插入和删除操作需要移动数据
;
b.建立空表时,较难确定所需的存储空间
。
算法:
算法: 构造一个空的线性表
算法:
Status InitList_Sq(SqList &L)
{
L.elem=(ElemType*)malloc(LIST_INIT_SIZE*sizeof (ElemType));
if (!L.elem)
exit(OVERFLOW);
L.length = 0;
L.listsize = LIST_INIT_SIZE;
return OK;
}// InitList_Sq
程序:(几乎一致)
//初始化
int initSq(SqList &L)
{
L.elem=(Elemtype *)malloc(LIST_INIT_SIZE*sizeof(Elemtype));//初分配
if(!L.elem )
return 0;
L.listsize=LIST_INIT_SIZE;
L.length=0;
return 1;
}//initSq
注意!
应用时:先声明线性表变量Sqlist L;
然后调用函数InitList_Sq(L);
题:
已定义初始化函数Status InitList_Sq( SqList &L ) {……} 下面哪种调用方式是正确的 D
SqList &L; InitList_Sq( L)
SqList L; InitList_Sq( *L)
SqList *L; InitList_Sq( L)
SqList P; InitList_Sq( P ) 正确
算法---- 销毁线性表
算法:
Status DestoryList(SqList &L)
{
free(L.elem);
return OK;
}
程序:(一致)
//摧毁线性表
int destorySq(SqList &L)
{
free(L.elem);
return 0;
}
算法---- 判断顺序表是否为空
算法:
Status ListEmpty(SqList L)
{
//return (L.length==0)
if(L.length==0)
return TRUE;
else
return FALSE;
} // InitList_Sq
程序:(一致)
//判断顺序表是否为空
int listEmpty(SqList L)
{
if(L.length==0)
return 1;
else
return 0;
}
算法---- 顺序表求前驱的值PriorElem
算法:程序:(一致)
Status PriorElem(SqList L,int cur_e,ElemType &pre_e)//线性表L,当前元素的位置,前驱元素的值
{
if(cur_e<=0||L.length==0||cur_e>L.length)//容错
return ERROR;
else
pre_e=L.elem[cur_e-1];
}
算法:顺序表长度:
算法:程序:(写法一致)
Status ListLength( SqList L )
{
return L.length;
} // InitList_Sq
算法:顺序表插入:
算法:
//防止忘记存储结构
typedef struct
{
ElemType *elem;//存储空间的基址
int length;//当前长度
int listsize;//当前分配的存储容量
}SqList;//顺序表
//插入
Status ListInsert_Sq(SqList &L,int i,ElemType e )//在L的第i个元素之前插入e
{
if(i<1 || i>L.length+1 )
return ERROR;//位置不合法
if(L.length >=L.listsize )// 当前存储空间已满,增加分配
{
newbase=(ElemType *)realloc(L.elem,(L.listsize+LISTINCREMENT)*sizeof(ElemType));//这是算法
if(!newbase)
exit(OVERFLOW);
L.elem=newbase;//新基址
L.lisesize+=LISTINCREMENT;
}
//数组实现
for(j=L.length-1;j>=i-1;--j)//从最后开始
{
L.elem[j+1]=L.elem[j];//插入位置及之后的元素后移
}
L.elem[i-1]=e;//插入e
++L.length;//表长增加1
return OK;
//指针实现
q=&(L.elem[i-1]);
for(p=&(L.elem[L.length-1];p>=q;p--))
{
*(p+1)=*p;//后移
}
*q=e;//
++L.length;//
return OK;
}//ListInsert_Sq
程序:
//插入 输入的是序号 0号不存 序号=下标
int insertSq(SqList &L,int pos,Elemtype e)//在L的第i个元素之前插入
{//新元素应该在L.elem[i-1]这个下标
if(pos<1 || pos>L.length+1 )//位置不合法
return 0;
if(L.length>=L.listsize )//当前存储空间已满,增加分配
{
L.elem=(Elemtype*)realloc(L.elem, (L.listsize+LISTINCREMENT)*sizeof(Elemtype));
if(!L.elem )return 0;
L.listsize=L.listsize+LISTINCREMENT;
}
//插入 倒序
//1.数组实现
for(int j=L.length-1 ; j>=pos-1;j-- )//跟算法不同在于变量j需要声明
{
L.elem[j+1]=L.elem[j];//插入位置及之后的元素后移
}
L.elem[pos-1]=e;
L.length++;
//2.指针实现
Elemtype* q=&(L.elem[pos-1]);//应该在这个位置
for( Elemtype* p=&(L.elem[L.length-1]; p>=q; p--))//从最后一个元素
{
*(p+1)=*p;//后移
}
*q=e;
L.length++;
return 1;
}
插入算法时间复杂性分析
设在第i个元素之前插入的概率为pi,则在长度为n的线性表中插入一个元素时所需平均移动元素次数
为:
若假定在任何一个位置上插入元素的概率相同,则平均移动次数为:
算法:线性表删除——判断删除位置:
算法:程序:(差一个变量的定义,其他一样)
Status ListDelete_Sq(SqList &L,int i,ElemType &e)
{
if( i<1 || i>L.length)
return ERROR;
//数组实现
e=L.elem[i-1];//e记录删除的元素
for(j=i-1; j<L.length-1; j++)
{
L.elem[j]=L.elem[j+1];//后一位赋值给当前位
}
--L.length;//表长-1
return OK;
//指针实现
//程序多了一个定义变量Elemtype *p;
p=&(L.elem[i-1]);//p为被删除元素的位置
e=*p;
q=L.elem + L.length -1;//表尾元素的位置
for(++p;p<=q;++p)//从删除元素之后一直到表尾
{
*(p-1)=*p;//后一位赋值给当前位
}
--L.length;
return OK;
}
- 删除操作时间复杂性分析
设删除第i个元素的概率为pi,则在长度为n的线性表中删除一个元素时所需平均移动次数为:
若假定在任何一个位置上删除元素的概率相同,则平均移动次数为:
程序:遍历输出
void traverseSq(SqList L)
{
cout<<"\n---traverseSq----\n";
for(int i=0;i<L.length;i++)
{
cout<<"age:"<<L.elem[i].age<<" sex:"<<L.elem[i].sex <<endl;
}
}
2.3 线性表的链式表示和实现(链表)
2.3.1 单链表
-
单链表结构示意图
-
结构特点 :
逻辑次序和物理次序不一定
相同;
元素之间的逻辑关系用指针
表示;需要额外空间存储元素之间的关系
。 -
链表的定义
一个线性表由若干个结点组成,每个结点至少含有两个域
:数据域(信息域)
和指针域(链域)
,由这样的结点存储的线性表称作链表
。 -
头指针的概念
在链式存储结构中以第一个结点的存储地址作为线性表的基地址
,通常称它为头指针
。
线性表的最后一个数据元素没有后继,因此最后一个结点中的“指针域"是一个特殊的值 “NULL
”,通常称它为"空指针
"。 -
头结点的概念
在单链表上设置一个结点,它本身不存放数据
,它的指针域指向第一个元素的地址。 -
头结点的作用
使对第一个元素的操作与对其它元素的操作
保持一致。
单链表的C语言实现
0