一、线性表的定义
线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列,其中n为表长,当 n 等于 0 时线性表是一个空表。若用L命名线性表,则其一般表示为
L= (a1, a2, a1, … ,ai+l, … ,an)
式中,a1是唯一的 “第一个” 数据元素,又称表头元素; an是唯一的 “最后一个” 数据元素,又称
表尾元素。除第一个元素外,每个元素有且仅有一个直接前驱。除最后-一个元素外, 每个元素有且仅有一个直接后继。
由以上定义可以得出线性表的特点如下,表中元素:
- 个数有限。
- 具有逻辑上的顺序性,有其先后次序。
- 都是数据元素,每个元素都是单个元素。
- 的数据类型都相同,这意味着每个元素占有相同大小的存储空间。
- 具有抽象性,即仅讨论元素间的逻辑关系,而不考虑元素究竞表示什么内容。
注意: 线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构,两者属于不同层面的概念。
二、线性表的基本操作
一个数据结构的基本操作是指其最核心、最基本的操作。其他较复杂的操作可通过调用其基
本操作来实现。线性表的主要操作如下:
- InitList(&L):初始化表。构造一个空的线性表。
- Length(L):求表长。返回线性表L的长度,即 L 中数据元素的个数。
- LocateElem(L,e):按值查找操作。在表 L 中查找具有给定关键字值的元素,并返回下标。
- GetElem(L,i):按位查找操作。获取表 L 中第 i 个位置的元素的值。
- ListInsert(&L,i,e):插入操作。在表 L中的第 i 个位置上插入指定元素e。
- ListDelete(&L,i,&e):删除操作。删除表 L 中第 i 个位置的元素,并用 e 返回删除元素的值。
- PrintList(L):输出操作。按前后顺序输出线性表 L 的所有元素值。
- Empty(L):判空操作。若L为空表,则返回true,否则返回false。
- DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间。
三、顺序存储结构表示
3.1 顺序表的定义
线性表的顺序存储又称顺序表。它是用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。
假定线性表的元素类型为ElemType,则线性表的顺序存储类型描述为:
typedef struct Sqlist
{
ElemType data[MaxSize];// 数组,存储数据元素
int length; // 线性表当前长度
}SqList;
一维数组可以是静态分配的,也可以是动态分配的。在静态分配时,由于数组的大小和空间事先已经固定,一旦空间占满,再加入新的数据将会产生溢出,进而导致程序崩溃。
而在动态分配时,存储数组的空间是在程序执行过程中通过动态存储分配语句分配的,一旦数据空间占满,就另外开辟一块更大的存储空间,用以替换原来的存储空间,从而达到扩充存储数组空间的目的,而不需要为线性表一次性地划分所有空间。
#define InitSize 100 //表长度的初始定义
typedef struct Sqlist
{
ElemType *data; // 指示动态分配数组的指针
int length; // 线性表当前长度
int MaxSizel //数组的最大容量
}SqList;
初始化动态分配语句为:
L.data= (ElemType* ) malloc (sizeof (ElemType) *InitSize) ;
顺序表的特点
- 顺序表最主要的特点是随机访问,即通过首地址和元素序号可在时间0(1)内找到指定的元素。
- 顺序表的存储密度高,每个结点只存储数据元素。
- 顺序表逻辑上相邻的元素物理上也相邻,所以插入和删除操作需要移动大量元素。
3.2 顺序表操作的基本实现
线性表的基本操作还是很多的,这里只介绍两种稍微难理解的两种方法:插入操作和删除操作。
3.2.1 顺序表的插入操作
在顺序表 L 的第 i (1<=i<=L.1ength+1) 个位置插入新元素e。若 i 的输入不合法,则返回 false ,表示插入失败。否则,将顺序表的第 i 个元素及其后的所有元素右移一个位置,腾出一个空位置插入新元素 e,顺序表长度增加 1 ,插入成功,返回true。插入操作过程如下图所示:
插入操作的思路:
- 如果插入位置不合理,抛出异常; .
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
- 从最后 一个元素开始向前遍历到第 i 个位置,分别将它们都向后移动一个位置;
- 将要插入元素填入位置 i 处;
- 表长加1。
实现代码如下:
/* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L), */
/* 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1 */
bool ListInsert(SqList *L, int i, ElemType e)
{
if(L->length >= MaxSize) //顺序表已满
return false;
if(i < 1 || i > L->length + 1) //当i不在范围内时
return false;
for(int j = L->length;j >= i;j--) //将第j个元素之后的向后移,从最后一个元素开始移动,直到第j个元素移动完成
{
L->data[j] = L->data[j-1];
}
L->data[i-1] = e; //在位置i出放e
L->length++;
return true;
}
注意:区别顺序表的位序和数组下标。为何判断插入位置是否合法时if语句中用 length+1,而移动元素的for语句中只用length?
接下来分析一下时间复杂度:
- 最好情况:在表尾插入(即i=n+ 1),元素后移语句将不执行,时间复杂度为O(1)。
- 最坏情况:在表头插入(即i= 1),元素后移语句将执行n次,时间复杂度为O(n)。
- 平均情况:假设pi (pi=1/(n+1))是在第i个位置上插入一个结点的概率,则在长度为n的线性表中插入一个结点时,所需移动结点的平均次数为
因此,线性表插入算法的平均时间复杂度为O(n)。
3.2.2 顺序表的删除操作
删除顺序表L中第i (1<=i<=L. length)个位置的元素,若成功则返回true,并将被刪除的元素用引用变量e返回,否则返回false. 删除思路如下:
删除算法的思路:
- 如果删除位置不合理,抛出异常;
- 取出删除元素;
- 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一
个位置; - 表长减1。
实现代码如下:
bool ListDelete(SqList *L, int i, ElemType *e)
{
if(L->length == 0)//线性表为空
return 0;
if(i < 1 || i > L->length) //删除位置不正确
return false;
*e = L->data[i - 1];
for(int j = i;j < L->length;j++) //将删除位置后的元素前移
{
L->data[j - 1] = L->data[j];
}
L->length--;
return true;
}
接下来分析一下时间复杂度:
- 最好情况:删除表尾元素(即i=n),无须移动元素,时间复杂度为O(1)。
- 最坏情况:删除表头元素(即i= 1),需移动除第一个元素外的所有元素,时间复杂度为O(n)。
- 平均情况:假设pi (pi= 1/n)是删除第i个位置上结点的概率,则在长度为n的线性表中删除一个结点时,所需移动结点的平均次数为:
因此,线性表删除算法的平均时间复杂度为O(n)。
3.3 完整代码如下
#include<stdio.h>
#include<stdlib.h>
#define MaxSize 50
typedef int ElemType;
typedef struct Sqlist
{
ElemType data[MaxSize];// 数组,存储数据元素
int length; // 线性表当前长度
}SqList;
int InitList(SqList *L); //初始化顺序表
bool AppendList(SqList *L, ElemType e); //在顺序表末尾插入数据
bool ListInsert(SqList *L, int i, ElemType e); //在顺序表第i个位置插入数据
void PrintList(SqList L); //输出顺序表数据
int LocateElem(SqList L, ElemType e); //按值查找,返回在顺序表中的位置
int GetElem(SqList L, int i,ElemType *e); //按位查找操作
int ListLength(SqList L);
bool Empty(SqList L);//判断空操作
bool ListDelete(SqList *L, int i, ElemType *e);//删除操作,删除第i个位置元素,并返回删除的元素
int main()
{
SqList L;
ElemType e;
int i = InitList(&L);
bool b;
printf("初始化List后:L.length = %d\n",L.length);
AppendList(&L, 145);
AppendList(&L, 59);
AppendList(&L, 6);
AppendList(&L, 26);
AppendList(&L, 61);
AppendList(&L, 98);
AppendList(&L, 74);
PrintList(L);
printf("\n");
b = ListInsert(&L,5,999);
PrintList(L);
i = LocateElem(L,999);
printf("\n999元素在顺序表中第 %d 个\n",i);
i = GetElem(L,3,&e);
if(i == 1)
printf("位置 3 的元素为:%d\n",e);
else
printf("获取无效!!");
ListDelete(&L, 5, &e);
printf("删除的元素为:%d\n",e);
PrintList(L);
return 0;
}
/* 初始化顺序线性表 */
int InitList(SqList *L)
{
L->length = 0;
return 1;
}
bool AppendList(SqList *L, ElemType e)
{
if(L->length >= MaxSize)
return false;
L->data[L->length] = e;
L->length++;
return true;
}
/* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L), */
/* 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1 */
bool ListInsert(SqList *L, int i, ElemType e)
{
if(L->length >= MaxSize)
return false;
if(i < 1 || i > L->length + 1)
return false;
for(int j = L->length;j >= i;j--) //将第i个元素之后的向后移
{
L->data[j] = L->data[j-1];
}
L->data[i-1] = e; //在位置i出放e
L->length++;
return true;
}
void PrintList(SqList L)
{
printf("线性表元素为:");
for(int i = 0;i < L.length;i++)
{
printf("%d ",L.data[i]);
}
}
int LocateElem(SqList L, ElemType e)
{
if(L.length == 0)//为空判断
return 0;
for(int i = 0;i < L.length;i++)
{
if(L.data[i] == e)
return i + 1;
}
return 0;
}
int GetElem(SqList L, int i,ElemType *e)
{
if(L.length == 0 || i < 1 || i > L.length + 1)
return 0;
*e = L.data[i-1];
return 1;
}
/* 初始条件:顺序线性表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(SqList L)
{
return L.length;
}
bool Empty(SqList L)
{
if(L.length == 0)
return false;
else
return true;
}
bool ListDelete(SqList *L, int i, ElemType *e)
{
if(L->length == 0)//线性表为空
return 0;
if(i < 1 || i > L->length) //删除位置不正确
return false;
*e = L->data[i - 1];
for(int j = i;j < L->length;j++)
{
L->data[j - 1] = L->data[j];
}
L->length--;
return true;
}
四、顺序表存储结构的优缺点
优点
- 无须为表示表中元素之间的逻辑关系而增加额外的存储空间
- 可以快速地存取表中任一位置的元素
缺点
- 插入和删除操作需要移动大量元素
- 当线性表长度变化较大时,难以确定存储空间的容量
- 造成存储空间的 “碎片"