目录
一、顺序表的定义
1.定义
线性表的顺序存储叫作顺序表
用一组地址连续的存储单元依次存储线性表的数据元素
也称作线性表的顺序存储结构或顺序映像
2.实现方法
在内存中找了块地儿,通过占位的形式,把一定内存空间给占了,然后把相同数据类型的数据元素依次存放在这块空地中
数组实现
3.顺序表的实现
逻辑相邻的两个元素物理也相邻
把线性表中所有元素按照逻辑顺序,依次存储到从指定位置开始的一块连续存储空间
第i+1个元素的存储位置紧接在第i个元素的存储位置后面
顺序表可以实现随机存取
4.顺序表访问第i个元素
准备知识:
LOC(L):表示线性表L存储的起始位置,也是a1的存储位置,即LOC(L)=LOC(a1);刚好也数组名所指向的位置
sizeof(ElemType):表示线性表中每个元素所占用的存储空间大小
线性表中第i个元素地址:
LOC(ai)=LOC(L)+(i-1)*sizeof(ElemType)
LOC(ai)=LOC(a1)+(i-1)*sizeof(ElemType)
LOC(ai) = LOC(ai-1)+sizeof(ElemType)
访问顺序表中任意一个数据元素,时间复杂度为O(1), 因为可以直接通过公式计算得到地址,这就是随机存取
注意数组下标和线性表位序的对应:
线性表的位序从1开始;实际存储到数组中是从下标0开始的
例子:
#include <iostream> using namespace std; int main() { int L[] = { 1,2,3,4 }; cout << "LOC(L) = " << L << endl; cout << "LOC(a1) = " << &L[0] << endl; cout << "a4的地址 = " << &L[3] << endl; cout << "LOC(a4)=LOC(L) + (4-1)*sizeof(int) = " << L + (4 - 1) << endl; //注:指针做加法隐含有乘sizeof(ElemType) // L + (4-1)==>转换成十六进制加法:L的地址值 + (4-l)*sizeof(int) return 0; }
代码运行结果截图如下
5.线性表的顺序存储类型描述
一维数组静态分配:
#define MAXSIZE 50 //线性表的最大长度 typedef struct { ElemType data[MAXSIZE]; //顺序表的元素 //ElemType是由typedef定义的数据元素的类型 int length; //顺序表的当前长度 }SqList; //使用typedef重新定义的顺序表的类型
静态分配特点为数组的大小空间已经固定(MAXSIZE);空间满时,再加入新数据会导致溢出
注意代码中的ElemType根据具体需求确定,比如int,则再加一句typedef int ElemType;
一维数组的动态分配:
#define InitSize 100 //表长的初始定义 typedef struct { ElemType* data; //指示动态分配数组的指针 //ElemType是由typedef定义的数据元素类型 int length, //顺序表的当前长度 MaxSize; //动态数组的最大容量 }SqList; //使用typedef重新定义的顺序表的类型 //动态分配空间 L.data = (ElemType*)malloc(InitSize * sizeof(ElemType));
动态分配特点为在执行过程中根据需要,动态分配;空间满时,可以开辟另外一块更大空间,达到扩充目的;还是一次性选择一块大的空间,只不过可以在程序运行时动态指定空间大小
注意动态分配不是链式存储,分配的空间依然是连续的,依然采用随机存取方式
区分MaxSize与length:
MaxSize是数组的最大容量:数组长度是创建顺序表的时候就定下来的
length是线性表的当前长度:每存放一个数据元素,线性表长加一,每删除一个数据元素,线性表长减一,length<=MaxSize
6.顺序表特点
随机访问
存储密度高(不用额外存储指示信息)
逻辑相邻在物理上也相邻,插删需要移动大量元素
二、顺序表上基本操作的实现
1.插入操作
例子:
算法思想:
在顺序表L的第i(1<=i<=L.length+1)个位置插入新元素e
如果i的输入不合法,则返回false,表示插入失败
否则将顺序表的第i个元素以及其后的所有元素右移一个位置(从后向前处理),腾出一个空位置插入新元素e,顺序表长度增加1,插入成功,返回true
代码实现:
/* * 将元素e插入到顺序表L中位序i的位置 */ bool ListInsert(SqList &L, int i,ElemType e){ //下表[1, L.length + 1]合法 if ( i < 1 || i > L.length + 1) { return false; } //当前存储空间已满,不能插入 if (L.length >= MaxSize) return false; //移位 for (int j = L.length; j >= i; j--) { L.data[j] = L.data[j - 1]; } //插入 L.data[i - 1] = e; //表长+1 L.length++; return true; }
L必须需要引用
静态数组实现:涉及修改data中元素的时候必须使用引用,否则形参和实参的data是两个数组
动态数组实现:可以不使用引用,这时形参的data和实参的data指向的地址值都一样
不管是静态数组还是动态数组,形参修改length的时候都必须要引用,实参才能同步变化
复杂度分析:
(1)最好情况:在表尾插入(即i=n+1)元素后移语句将不执行,时间复杂度为O(1)
(2)最坏情况:在表头插入(即i=1)元素后移语句将执行n次,时间复杂度为O(N)
(3)平均情况:假设pi(pi=1/(n+1))是在第i个位置上插入一个结点的概率,有n+1个插入位置(1到length+1),总的移动次数为0+1+2+3+······+n=n(1+n)/2,在每个位置插入的概率相等,则除以n+1,即n(1+n) / 2(n+l) = n/2,因此插入算法的平均复杂度为O(N)
(4)插入位置举例:插入到位序15不需要移动;插入到位序1需要移动14个元素
2.删除操作
例子:
算法思想:
删除顺序表L中第i(1<=i<=L.length)个位置的元素
删除位置非法则返回false
否则将第i个元素赋值给e,将顺序表的第i个元素以及其后的所有元素左移一个位置(从前向后处理),顺序表长度减少1,删除成功,返回true
代码实现:
/* * L:线性表 * i:待删除元素在线性表的位置 * e:记录删除元素的数据,通过引用返回 */ bool ListdDelete(SqList &L, int i, ElemType &e) { //非法判断 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; }
复杂度分析:
(1)最好情况:删除表尾元素(即i=n)无须移动元素,时间复杂度为O(1)
(2)最坏情况:删除表头元素(即i=1)需要移动除第一个元素外的所有元素,时间复杂为O(N)
(3)平均情况:假设pi(pi=1/n)是删除第i个位置上结点的概率,有n个位置可以删除(1到length),总的移动次数为0+1+2+3+······+n-1=(n-1)n/2,在每个位置删除的概率相等,则除以n,即(n-1)n/2n = (n-1)/2,因此删除算法的平均复杂度为O(N)
(4)删除位置举例:删除位序14不需要移动,直接修改表长减少1就行;删除位序1需要移动13个元素
3.按值查找(顺序查找)
算法思想:
在顺序表L中查找第一个元素值等于e的元素,并返回其位序
从头到尾遍历L,在遍历的过程中进行比较(出现了等于e的元素则查找成功,返回位序;遍历完毕都没有出现等于e的元素则查找失败,返回0)
代码实现:
/* * 查找顺序表中值为e的元素,如果查找成功,返回元素位序,否则返回0 */ int LocateElem(SqList L, ElemType e) { int i; for (i = 0; i < L.length; ++i) { if (L.data[i] == e) { //位序与下标有1的差距 return i + 1; } } //查找失败 return 0; }
复杂度分析:
(1)最好情况:查找的元素就在表头,仅需比较一次,时间复杂度为O(1)
(2)最坏情况:查找的元素在表尾(或不存在),需要比较0次,时间复杂度为O(N)
(3)平均情况:假设pi(pi=1/n)是查找的元素在第i个位置上结点的概率,有n个位置可以查找,总的比较次数为1+2+3+······+n=n(1+n)/2,在每个位置比较的概率相等,则除以n,即n(1+n)/2n = (n+1)/2,因此按值查找算法的平均复杂度为O(N)
(4)查找位置举例:查找20,仅比较1次;查找150比较14次