[入门必看]数据结构2.2:线性表的顺序表示
第二章 线性表
小题考频:5
大题考频:12
2.2 线性表的顺序表示
难度:☆☆☆
知识总览
2.2.1 顺序表的定义
2.2.2_1 顺序表的插入删除
2.2.2_2 顺序表的查找
2.2.1 顺序表的定义
顺序表——用顺序存储的方式实现线性表
顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
各个元素数据类型相同,所占的内存空间一样大。
顺序表中各元素在物理内存上连续存放。
第二个元素存放的位置 - 顺序表起始地址+数据元素的大小
第三个元素存放的位置 - 顺序表起始地址+2*数据元素的大小
- Question:如何知道一个数据元素大小?
——C语言中:sizeof(ElemType)
Eg1.
sizeof(int) = 4B
Eg2.
typedef struct{
int num; //号数
int people; //人数
}Customer;
sizeof(Customer) = 8B
ElemType就是顺序表中存放的数据元素类型
顺序表的实现——静态分配
#define MaxSize 10 //定义最大长度
typedef struct{
ElemType data[MaxSize]; //用静态的“数组”存放数据元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义(静态分配方式)
数组长度大小确定后不可以改变
给各个数据元素分配连续的存储空间,大小为MaxSize*sizeof(ElemType)
Sq:sequence —— 顺序,序列
代码实现:
#include <stdio.h>
#define MaxSize 10 //定义最大长度
typedef struct{
int data[MaxSize]; //用静态的“数组”存放数据元素 - 数据元素类型(ElemType)是Int
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
//基本操作 - 初始化一个顺序表
void InitList(SqList &L){
for(int i = 0; i < MaxSize; i++) //Step 2
L.data[i] = 0; //将所有数据元素设置为默认值
L.length = 0; //顺序表初始长度为0 - Step 3
}
int main(){
SqList L; //声明一个顺序表 - Step 1
InitList(L); //初始化顺序表
//……
return 0;
}
Step1:在内存中分配存储顺序表L的空间。
包括:MaxSize*sizeof(ElemType)
和 存储length的空间
Step2:把各个数据元素的值设为默认值(可省略)
Step3:将Length的值设为0
- 如果Step2中,不把各个数据元素的值设为默认值:
——内存中会有遗留的“脏数据”
- 以上这种打印方式为 - 打印整个data数组:
//尝试“违规”打印整个data数组
for(int i = 0; i < MaxSize; i++)
printf("data[%d]=%d\n", i, L.data[i]);
return 0;
打印时,不该从第一个元素访问到最后一个元素。
—— i < MaxSize;
应该是访问到顺序表中实际存储的最后一个元素。
—— i < L.length;
所以此时大于顺序表实际长度的部分是不会被访问的 - Step2设置默认值可省略的原因。
更好的方式是使用基本操作来访问各个数据元素
GetElem(L,i)
-
Question1:Step3将Length的值设为0能否省略?
——不能省略,无法预知在length区域之前存的是什么数据。 -
Question2:如果“数组”存满了怎么办?
——放弃治疗,顺序表的表长刚开始确定后就无法更改(存储空间是静态的)
思考:如果刚开始就声明一个很大的内存空间呢?存在什么问题?
——浪费!
顺序表的实现——动态分配
#define InitSize 10 //顺序表的初始长度
typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList; //顺序表的类型定义(动态分配方式)
顺序表的容量可变
- Key:
动态申请
和释放
内存空间
—— C语言:malloc
、free
函数
——C++:new、delete关键字
L.data = (ElemType*) malloc (sizeof(ElemType)* InitSize);
(ElemType*) :malloc函数返回一个指针, 空间需要强制转型为你定义的数据元素类型指针
malloc(Δ):malloc函数的参数Δ,指明要分配多大的连续内存空间
代码实现:
#include <stdlib.h> //malloc、free函数的头文件
#define InitSize 10 //默认的最大长度
typedef struct{
int *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList;
//初始化顺序表
void InitList(SeqList &L){
//用malloc函数申请一片连续的存储空间
L.data = (int*)malloc(InitList*sizeof(int));
//增加动态数组的长度
void Increasesize(SeqList &L, int len){
int *p = L.data;
L.data=(int*)malloc((L.MaxSize + len) * sizeof(int));
for(int i = 0; i < L.length; i++){
L.data[i]=p[i]; //将数据复制到新区域 - 时间开销大
}
L.MaxSize = L.MaxSize + len; //顺序表最大长度增加len
free(p); //释放原来的内存空间
}
int main(){
SeqList L; //声明一个顺序表
InitList(L); //初始化顺序表
//…往顺序表中随便插入几个元素…
IncreaseSize(L,5);
return 0;
}
顺序表的特点
顺序表的特点:
①随机访问,即可以在O(1)时间内找到第i个元素。
代码实现:data[i-1];
静态分配、动态分配都一样
②存储密度高,每个节点只存储数据元素
链式存储中,还需要一部分空间存放指针
③拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
④插入、删除操作不方便,需要移动大量元素
2.2.2_1 顺序表的插入和删除
用存储位置的相邻来体现数据元素之间的逻辑关系。
顺序表的插入
ListInsert(&L,i,e):插入操作。在表L中的第i个位置【位序】上插入指定元素e。
注:本节代码建立在顺序表的“静态分配”实现方式之上,“动态分配”也雷同。
逻辑结构:
代码实现:
#define MaxSize 10 //定义最大长度
typedef struct{
int data[MaxSize]; //用静态的“数组”存放数据元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
//插入一个元素
void ListInsert(SqList &L, int i, int e){ //基本操作:在L的位序i处插入元素e
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++; //长度加1
}
int main(){
SqList L; //声明一个顺序表
InitList(L); //初始化顺序表
//…插入几个元素
ListInsert(L,3,3);
return 0;
}
基操——让自己实现的数据结构可以让别人很方便地使用
检查顺序表是否已满;插入的位置是否合法
反馈操作结果!
修改后代码实现:
#define MaxSize 10 //定义最大长度
typedef struct{
int data[MaxSize]; //用静态的“数组”存放数据元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
//插入一个元素 - 返回bool型变量
bool ListInsert(SqList &L, int i, int e){
if(i < 1||i > L.length + 1) //判断i的范围是否有效
return false;
if(L.length >= MaxSize) //当前存储空间已满,不能插入
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++; //长度加1
return ture;
}
int main(){
SqList L; //声明一个顺序表
InitList(L); //初始化顺序表
//…插入几个元素
ListInsert(L,3,3);
return 0;
}
算法中针对异常情况给出返回值。
——return true; return false;
好的算法,应该具有“健壮性”。
能处理异常情况,并给使用者反馈。
插入操作的时间复杂度
bool ListInsert(SqList &L, int i, int e){
if(i < 1||i > L.length + 1) //判断i的范围是否有效
return false;
if(L.length >= MaxSize) //当前存储空间已满,不能插入
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++; //长度加1
return ture;
}
关注最深层循环语句执行次数与问题规模n的关系
L.data[j] = L.data[j - 1];
问题规模n = L.length(表长)
最好情况:新元素插入到表尾,不需要移动元素
——i = n + 1【i = length + 1】,循环0次;最好时间复杂度 = O(1)
最坏情况:新元素插入到表头,需要后移所有n个元素
——i = 1,循环n次;最坏时间复杂度 = O(n)
平均情况:假设新元素插入到任何一个位置的概率相同,即
i
=
1
,
2
,
3
,
…
,
l
e
n
g
t
h
+
1
i = 1,2,3, … , length+1
i=1,2,3,…,length+1的概率都是
p
=
1
n
+
1
p=\frac{1}{n+1}
p=n+11
i = 1,循环n次;i = 2时,循环n-1次;i =3 ,循环n-2次……i = n+1时,循环0次。
平均循环次数: n p + ( n − 1 ) p + ( n − 2 ) p + … … + 1 ⋅ p = n ( n + 1 ) 2 1 n + 1 = n 2 np+\left( n-1 \right) p+\left( n-2 \right) p+……+1\cdot p=\frac{n\left( n+1 \right)}{2}\frac{1}{n+1}=\frac{n}{2} np+(n−1)p+(n−2)p+……+1⋅p=2n(n+1)n+11=2n
平均时间复杂度= O ( n ) O(n) O(n)
顺序表的删除
ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
逻辑结构:
代码实现:
//删除
bool ListDelete(SqList &L, int i, int &e){
if(i < 1||i > L.Length) //判断i的范围是否有效
return false;
e = L.data[i-1]; //将被删除的元素赋值给e
for(int j = i; j < L.length; j++) //将第i个位置后的元素前移
L.data[j-1] = L.data[j]; //注意位序、数组下标的关系,并从前面元素依次移动
L.length--; //线性表长度减1·
return true;
}
int main(){
SqList L; //声明一个顺序表
InitList(L); //初始化顺序表
//…插入元素
int e = -1; //用变量e把删除的元素“带回来”
if(ListDelete(L, 3, e))
printf("Delete successfully, element = %d\n", e);
else
printf("Delete failed\n");
return 0;
Question:如果参数没有加引用符号,会怎样?
——子函数和主函数的变量e在内存中不是同一个东西,返回主函数后e的值不会改变!
删除操作的时间复杂度
bool ListDelete(SqList &L, int i, int &e){
if(i < 1||i > L.Length) //判断i的范围是否有效
return false;
e = L.data[i-1]; //将被删除的元素赋值给e
for(int j = i; j < L.length; j++) //将第i个位置后的元素前移
L.data[j-1] = L.data[j];
L.length--; //线性表长度减1·
return true;
}
关注最深层循环语句的执行次数与问题规模n的关系
L.data[j-1] = L.data[j];
问题规模n = L.length(表长)
最好情况:删除表尾元素,不需要移动其他元素
—— i = n,循环0次;最好时间复杂度= O(1)
最坏情况:删除表头元素,需要将后续的n-1个元素全都向前移动
—— i = 1,循环n-1次;最坏时间复杂度= O(n)
平均情况:假设删除任何一个元素的概率相同,即
i
=
1
,
2
,
3
,
…
,
l
e
n
g
t
h
i = 1,2,3, … , length
i=1,2,3,…,length的概率都是
p
=
1
n
p=\frac{1}{n}
p=n1
i = 1,循环n-1次;i = 2时,循环n-2次;i = 3,循环n-3次……i = n时,循环0次。
平均循环次数: ( n − 1 ) p + ( n − 2 ) p + … … + 1 ⋅ p = n ( n − 1 ) 2 1 n = n − 1 2 (n-1)p+\left( n-2 \right) p+……+1\cdot p=\frac{n\left( n-1 \right)}{2}\frac{1}{n}=\frac{n-1}{2} (n−1)p+(n−2)p+……+1⋅p=2n(n−1)n1=2n−1
平均时间复杂度= O ( n ) O(n) O(n)
2.2.2_2 顺序表的查找
按位查找
GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。
静态分配顺序表
GetElem实现:
ElemType GetElem(SqList L, int i){ //返回值和数据元素类型相同
return L.data[i-1]
//健壮性 - 判断i是否合法
}
动态分配顺序表
GetElem实现:
ElemType GetElem(SeqList L, int i){
return L.data[i-1]; //指针
}
//和访问普通数组的方法一样
ElemType *data 【指针】
指针指向malloc函数分配的一整片连续空间的起始地址。
Eg.1
假设指针data指向的地址为2000;
一个ElemType占6B,即sizeof(ElemType) == 6
ElemType *data
data[0] - 从2000开始数6B的内容 - 第一个数据元素
data[1] - 从2000+6开始数6B的内容 - 第二个数据元素
Eg.2
换一个类型的指针,指向同一个地址
int *p (注:一个int占4B)
对不上
- 再次理解malloc:为何malloc函数返回的存储空间起始地址要转换为与数据元素的数据类型相对应的指针
——虽然指针指向同一地址,但是指针所指的数据类型定义错了,那么访问数据元素时也会出现问题。
按位查找的时间复杂度
由于顺序表的各个数据元素在内存中连续存放,因此可以根据起始地址和数据元素大小立即找到第i个元素——“随机存取”特性
时间复杂度: O ( 1 ) O(1) O(1)
按值查找
LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。
//在顺序表L中查找第一个元素值等于e的元素,并返回其位序
int LocateElem(SeqList L,ElemType e){
for(int i=0;i<L.length;i++) //依次检索判断
if(L.data[i]==e)
return i+1; //数组下标为i的元素值等于e,返回其位序i+1
return 0; //退出循环,说明查找失败
}
- 数组下标为i的元素值等于e,返回其位序i+1
Eg.
typedef struct{
int *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList;
//在顺序表L中查找第一个元素值等于e的元素,并返回其位序
int LocateElem(SeqList L,int e){
for(int i=0;i<L.length;i++)
if(L.data[i]==e) //基本数据类型:int、char、double、float等可以直接用运算符“==”比较
return i+1;
return 0;
}
//调用LocateElem(L, 9);
- 基本数据类型:int、char、double、float等可以直接用运算符“==”比较
——Question:两个结构类型的数据元素,是否可以用运算符“==”比较?
——不能!
Answer实现:
typedef struct{
int num;
int people;
}Customer;
void test(){
Customer a;
a.num = 1;
a.people = 1;
Customer b;
b.num = 1;
b.people = 1;
if(a == b){ //Invalid operands to binary expression ('Customer'and'Customer') - 报错!
printf("Equal");
}else{
printf("Unequal");
}
}
注意:C语言中,结构体的比较不能直接用“==”
——需要依次对比各个分量来判断两个结构体是否相等,如下:
if(a.num == b.num && a.people == b.people){
printf("Equal");
}else{
printf("Unequal");
}
- 更好的办法:定义一个函数!
bool isCustomerEqual(Customer a, Customer b){
if(a.num == b.num && a.people == b.people)
return true;
else
return false;
}
Tips:
《数据结构》考研初试中,手写代码可以直接用“==”,无论ElemType是基本数据类型还是结构类型
手写代码主要考察学生是否能理解算法思想,不会严格要求代码完全可运行
按值查找的时间复杂度
//在顺序表L中查找第一个元素值等于e的元素,并返回其位序
int LocateElem(SeqList L,ElemType e){
for(int i=0;i<L.length;i++) //依次检索判断
if(L.data[i]==e)
return i+1; //数组下标为i的元素值等于e,返回其位序i+1
return 0; //退出循环,说明查找失败
}
关注最深层循环语句的执行次数与问题规模n的关系
if(L.data[i]==e)
问题规模n = L.length(表长)
最好情况:目标元素在表头
—— 循环1次;最好时间复杂度= O(1)
最坏情况:目标元素在表尾
—— 循环n次;最坏时间复杂度= O(n)
平均情况:假设目标元素出现在任何一个位置的概率相同,都是
1
n
\frac{1}{n}
n1
目标元素在第1位,循环1次;在第2位,循环2次;……;在第n位,循环n次
平均循环次数: 1 ⋅ 1 n + 2 ⋅ 1 n + 3 ⋅ 1 n + … … + n ⋅ 1 n = n ( n + 1 ) 2 1 n = n + 1 2 1\cdot \frac{1}{n}+2\cdot \frac{1}{n}+3\cdot \frac{1}{n}+……+n\cdot \frac{1}{n}=\frac{n\left( n+1 \right)}{2}\frac{1}{n}=\frac{n+1}{2} 1⋅n1+2⋅n1+3⋅n1+……+n⋅n1=2n(n+1)n1=2n+1
平均时间复杂度= O ( n ) O(n) O(n)
知识回顾与重要考点
2.2.1顺序表的定义
- 存储结构:逻辑上相邻的数据元素物理上也相邻
- malloc申请一片更大的空间;free释放原区域
2.2.2_1 顺序表的插入和删除
- 增删时,更改length值
- 注意位序i和数组下标的区别
- 健壮性 - 对必要的条件进行判断
- 参数加“&”引用 - 被调用函数中处理的参数和主函数中的一样。
2.2.2_2 顺序表的查找
- 位序与下标的区别
- 判断两个结构体相等 - 依次对比各个分量