1 线性表的定义与特点
线性表(Linear List)定义:线性表是具有相同特性的数据元素的一个有限序列。
例1:26个英文字母组成的英文表
(A,B,C,D…,Z)
数据元素是字母,元素间为线性关系。
例2:学生信息表
数据元素是每个学生的信息,数据元素的关系为线性。
同一线性表中的元素必定有相同特性,数据间的关系为线性关系。
线性表的逻辑特征:在非空的线性表中,有且仅有一个起始结点和末尾节点,除了这两个节点外,其余每个结点都有唯一的直接前驱结点和直接后继节点。
2 案例引入
2.1 一元多项式的运算:实现加、减、乘
2.2 稀疏多项式的运算
倘若让你使用上述方法来表示一个稀疏多项式,那么应该如何表示呢?
这样可以当然也就可以表示一个系数多项式,但是使用这种方法表示的话会浪费大量的存储空间。比如上述稀疏多项式需要创建一个有2001个元素的数组,但实际却只使用了3个元素,造成了其余1998个元素的浪费。
接下来,我们创建一个数据元素既有下标又有指数的线性表来表示稀疏多项式。
接下来进行稀疏多项式的运算:
步骤:①创建一个新数组c;②分别从头遍历并比较两个运算多项式的每一项(指数相同,系数相加减;指数不同,将指数较小的复制到c中);③一个多项式遍历完毕时,将另一个复制到c中即可。
大家在这里有没有一个疑问,那就是我们创建的新数组应该多大呢?倘若:两个多项式相加和刚好为零,那么数组长度为零即可;倘若每个项数的指数都不同,那么数组长度应为两个多项式的项数和。由于顺序存储结构存在问题,在这里我们引出链式存储结构。
通过遍历得到:
2.3 图书信息管理系统
既然将图书表抽象为线性表,那么下来就该选择合适存储结构并实现此存储结构上的基本操作,最后利用基本操作完成相关功能即可。
总结:
①线性表中的数据元素可以是简单类型,也可以是复杂类型;
②许多实际应用问题所涉的基本操作有很大的相似性,不应为某个具体应用写一个单独的程序;
③从具体应用中抽象出共性的逻辑结构和基本操作(抽象数据类型),然后实现其存储结构和基本操作。
3. 线性表的类型定义
3.1 线性表的基本操作
InitList(&L)
操作结果:构造一个空的线性表L
DestroyList(&L)
初始条件:线性表L已存在
操作结果:销毁线性表L
ClearList(&L)
初始条件:线性表L已存在
操作结果:将线性表L重置为空表
ListEmpty(L)
初始条件:线性表L已存在
操作结果:若L为空表,返回True,否则返回False
ListLength(L)
初始条件:线性表L已存在
操作结果:获取表长
GetElem(L,i,&e)
初始条件:线性表L已存在,1<=i<=ListLength(L)
操作结果:返回线性表第i个位置上的元素
LocateElem(L,e,Compare())
初始条件:线性表L已存在,Compare()是数据元素判定函数
操作结果:返回L上第一个与e满足Compare()函数的数据元素的位序,不存在返回0
PriorElem(L,cur_e,&pre_e)
初始条件:L已存在且cur_e不是第一个元素
操作结果:用pre_e返回cur_e的直接前驱元素
NextElem(L,cur_e,&next_e)
初始条件:L已存在且cur_e不是最后一个元素
操作结果:用next_e返回cur_e的直接后继元素
ListInsert(&L,i,e)
初始条件:L已存在且 1 <= i <= ListLength(L)+1
操作结果:在线性表L的第i个位置处插入元素e,L的长度加1
ListDelete(&L,i,&e)
初始条件:L已存在且 1 <= i <= ListLength(L)
操作结果:删除线性表L的第i个位置元素e并返回e的值,L的长度减1
ListTraverse(&L,visited())
初始条件:线性表L已存在
操作结果:依次对线性表中的每个元素调用visited()
此处所提及的运算是逻辑上的运算,至于“如何做”等实现细节,只有确定了存储结构之后才考虑。
4. 线性表的顺序表示及实现
4.1 顺序表的定义及特点
顺序存储(又称顺序存储结构或顺序映像)定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元之中的数据结构。简言之,逻辑上相邻,物理上也相邻。顺序存储结构首元素的位置称为基地址。
线性表顺序存储结构占用一片连续的存储空间。因此,知道某个元素的存储位置就可计算其他元素的存储位置。
特点:以物理结构表示逻辑结构,且任一元素均可随机读取。
4.2 顺序表的存储表示
大家有没有想到,顺序存储结构与高级语言中的数组数据类型很相似?两者都地址连续、任意存取、类型相同,但是顺序表的长度可变(删除、插入操作),而数组一旦初始化就无法改变长度了。那么我们有没有办法用一维数组表示顺序表呢?当然,我们通过使用一个变量来表示顺序表的长度属性,这样就可以表示线性表啦。定义如下:
例1:多项式的顺序存储结构类型定义:
代码如下:
#define MAXSIZE 1000 //多项式能达到的最大长度
typedef struct Polynomial //多项式非零项定义
{
float p; //系数
int e; //指数
};
typedef struct SeqList //多项式的顺序存储结构类型为SeqList
{
Polynomial* Elem; //多项式的基地址
int length; //多项式当前项的个数
};
例2:图书表的顺序存储结构类型定义:
代码如下:
#define MAXSIZE 1000 //图书表能可能达到的最大长度
typedef struct Book //图书信息定义
{
int ISBN; //图书ISBN
char name[20]; //图书名称
float price; //图书价格
};
typedef struct SeqList //图书表的顺序存储结构类型为SeqList
{
Book* Elem; //存储空间的基地址
int length; //图书表当前书的数量
};
注意:顺序表的逻辑位序与物理位序相差1!
4.3 顺序表基本操作的实现
顺序表的定义模板:
//静态数组定义顺序表
#define MAXSIZE 1000
typedef struct SeqList
{
ElemType Elem[MAXSIZE];
int length;
};
//动态数组定义顺序表
typedef struct SeqList
{
ElemType* Elem;
int length;
};
L.Elem = (ELemType*)malloc(sizeof(Elemtype) * MAXSIZE);
简单操作的实现:
//线性表的初始化
Status InitSeqList(SeqList &L) //顺序表的初始化
{
L.Elem = (ElemType*)malloc(sizeof(ElemType) * MAXSIZE); //为顺序表分配空间
if (!L.Elem) //判断空间是否分配成功
exit(OVERFLOW);
L.length = 0; //设置表长为0
return OK;
}
//销毁线性表
void DestroySeqList(SeqList& L)
{
if (L.Elem)
delete L.Elem; //释放存储空间
}
//清空线性表
void ClearSeqList(SeqList &L)
{
if (L.Elem)
L.length = 0;
}
//求线性表的长度L
int GetLength(SeqList L)
{
if (L.Elem)
return L.length;
}
//判断线性表L是否为空
int EmptySeqList(SeqList& L)
{
if (L.length == 0)
return 1;
else
return 0;
}
//根据位置提取数据
int GetElem(SeqList L,int i,ElemType &e)
{
if (i<1 || i>L.length)
return false;
e = L.Elem[i-1];
return OK;
}
4.4 顺序表的查找算法
int LocateElem(SeqList L, ElemType e)
{
for (int i = 0; i < L.length; i++) //遍历数组,查找与e相等的元素
if (L.Elem[i] == e)
return i + 1;
return 0;
}
查找算法分析:
算法的主要时间耗费在关键字的比较,当查找的数据是顺序表的第一个元素时,只需比较一次;是第二个元素时,要比较两次;…是最后一个元素时,则需要比较n次。 平均查找长度(ASL) = 第i个元素查找的概率*找到第i个元素需要比较的次数 (i从1到n),我们假设每个元素查找的概率相等,可得到平均查找长度为:0.5 *(n+1)。因此,查找算法的时间复杂度为O(n)。
4.5 顺序表的插入算法
顺序表的插入算法是指在表的第i个位置上插入一个新节点e,然后表长加1即可。
算法思想:
①判断插入的位置是否合法(1 <= i <= n+1);
②判断顺序表空间是否已满,若已满返回false;
③将第i个到最后一个的元素依次向后移动一个位置;
④将元素e插入第i个位置上;
⑤表长+1,插入结束。
Status ListInsert_Sq(SeqList& L, int i, ElemType e)
{
if (i<1 || i>L.length + 1)
return false;
if (L.length == MAXSIZE)
return false;
for (int j = L.length; j > i - 1; j--)
L.Elem[j] = L.Elem[j - 1];
L.Elem[i - 1] = e;
L.length++;
return OK;
}
插入算法分析:
算法的主要时间耗费在移动元素上,若在顺序表尾插入元素,则不用移动元素;若在首结点插入元素,则需要移动n个元素。插入的位置共有 n+1 个,算法的平均移动次数为 0.5*n,因此算法的平均时间复杂度为 O(n)。
4.6 顺序表的删除算法
顺序表的删除算法是指删除顺序表的第i个结点,使顺序表的长度变为n-1。
注意:删除某一结点之后,该结点之后的结点都需向前移动一个位置。
算法思想:
①判断位置是否合法(1 <= i <=length);
②将第 i 个往后的结点依次向前移动一个位置;
③表长-1。
代码如下:
Status ListDelete_Seq(SeqList& L, int i)
{
if (i<1 || i>L.length)
return ERROR;
for (int j = i; j < L.length; j++)
L.Elem[j - 1] = L.Elem[j];
L.length--;
return OK;
}
删除算法分析:
算法的主要时间耗费在移动元素上,若在删除表尾结点,则不用移动元素;若删除表首结点,则需要移动n-1个元素。插入的位置共有 n 个,算法的平均移动次数为 0.5*(n - 1),因此算法的平均时间复杂度为 O(n)。
5. 顺序表总结
5.1 顺序表的特点
①利用元素的存储位置表示相邻元素之间的前后关系,即逻辑结构与存储结构一致;
②访问线性表时,可快速计算出任意元素的存储位置。因此可粗略地认为访问每个元素所需的时间相等。(这种存取元素的方法被称为随机存取法。)
5.2 顺序表基本操作的实现
5.3 顺序表的操作算法分析
5.4 顺序表优缺点
优点:①可以随机访问。
缺点:①插入、删除元素时,需要移动大量元素;②容易造成空间浪费;③数据元素的个数不能自由扩充。