目录
2.1 线性表的定义和基本操作
先从线性表的定义(逻辑结构)与基本操作(运算)上讲。
*数据结构三要素:逻辑结构、数据的运算、存储结构(物理结构)
存储结构不同,运算的实现方式不同。
线性表定义(逻辑结构)
数据类型相同代表每个数据元素所占空间一样大;
有限数据元素有限个数;
序列代表数据元素之间是有次序的;
位序从1开始,数组下标从0开始。
线性表基本操作(运算)
Tips:
- 对数据的操作(记忆思路):创销、增删改查;
- C语言函数的定义:<返回值类型> 函数名 (<参数1类型> 参数1, <参数2类型> 参数2, ......);
- 实际开发中,可根据实际需求定义其他的基本操作;
- 函数名和参数的形式、命名都可改变,此处参考的是严蔚敏版《数据结构》,命名需要具有可读性;
- 什么时候要传入参数的引用“&”:对参数的修改结果需要“带回来”
实验:(要用C++运行环境)
(第一段代码中修改的变量是自定义函数中,一份名为x的“复制品”;
第二段代码中自定义函数接受的是 &x,x的引用,修改的就是main函数中的原变量)
Q:为什要实现对数据结构的基本操作?
- 团队合作编程,你定义的数据结构要让别人能够很方便地使用(封装);
- 将常用的操作、运算封装成函数,避免重复工作,降低出错风险;
Tips:比起学会“How”,更重要的是想明白“Why”。
2.2_1 顺序表的定义
顺序表:用顺序存储方式实现线性表顺序存储。
把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
线性表:具有相同数据类型(每个数据元素所占空间一样大)的n(n>=0)个数据元素的有限序列。
顺序表的实现方式一:静态分配
初始化顺序表:在内存中分配存储顺序表 L 的空间。包括: MaxSize * sizeof(ElemType) 和存储 length 的空间。
#include <iostream>
#include <cstdio>
#define MaxSize 10 // 定义最大长度
using namespace std;
typedef struct{
int data[MaxSize]; // 用静态的“数组”存放数据元素
int length; // 顺序表的当前长度
}SqList; // 顺序表的类型定义
// 基本操作——初始化一个顺序表
void InitList(SqList &L){
cout << "InitList(SqList &L) 初始化中..." << endl;
for(int i = 0; i < MaxSize; i++){
L.data[i] = 0; // 将所有数据元素设置为默认初始值
}
L.length = 0; // 顺序表初始长度为0
cout << "InitList(SqList &L) 初始化完毕!" << endl;
}
int main(){
SqList L; // 声明一个顺序表
InitList(L); // 初始化顺序表
// 打印顺序表
for(int i = 0; i < MaxSize; i++){ // 应为 i < L.length
cout << L.data[i] << endl;
}
return 0;
}
Q :如果不初始化会怎样?
A:使用的时候可能数据中会出现随机数字。因为内存中可能会有遗留的“脏数据”。
当我们打印顺序表中的数据时,不应该手动取出数组中的数据,而是应该根据顺序表的长度length 的值来访问,只能访问小于 length 的值。访问顺序表时,只能访问length长度内的数据。
打印顺序表中顺序的限制条件应该为 i <= L.length 。
Q:如果“数组”存满了怎么办?
A:可以放弃治疗,顺序表的表长刚开始确定后就无法更改(存储空间是静态的)
思考:如果刚开始就声明一个很大的内存空间呢?存在什么问题?
回答:空间的浪费。
顺序表的实现方式二:动态分配
#define InitSize 10 // 顺序表的初始长度
typedef struct{
ElemType *data; // 指示动态分配数组的指针 ,指向第一个数据元素
int MaxSize; // 顺序表的做大容量
int length; // 顺序表的当前长度
}SeqList; // 顺序表的类型定义(动态分配方式)
C语言中提供了 malloc 、free 两个函数来进行申请和释放内存空间。
malloc :申请一整片连续的内存空间,返回这片空间起始地址的指针,需要强制转型为你定义的数据元素类型指针。如:L.data = (ElemType*)malloc(sizeof(ElemType) * InitSize);
以下代码是使用了动态分配的方法定义顺序表,记录顺序表数组的指针,存满了就申请一块另外的更大的内存空间;
对于增加动态数组长度的解决方法是将数据复制到新区域,时间开销大。
realloc函数?
C 库函数 void *realloc(void *ptr, size_t size) 尝试重新调整之前调用 malloc 或 calloc 所分配的 ptr 所指向的内存块的大小。
增加动态数组的长度中 free(p); 这行代码的解释:
free函数会把p这个指针所指向的这一整片的存储空间给释放掉,把它归还给系统;
p变量是一个局部于这个函数的变量,所以当这个函数执行结束后,存储p这个变量的这些内存空间会被系统自动回收。
#include <iostream>
#include <cstdio>
#include <stdlib.h> // malloc、free 的头文件
using namespace std;
#define InitSize 10 // 默认的最大长度
typedef struct{
int *data; // 指示动态分配数组的指针 ,指向第一个数据元素
int MaxSize; // 顺序表的做大容量
int length; // 顺序表的当前长度
}SeqList; // 顺序表的类型定义(动态分配方式)
void InitList(SeqList &L){
// 用 malloc 函数申请一片连续的存储空间
cout << "InitList(SeqList &L)初始化中..." << endl;
L.data = (int *)malloc(InitSize * sizeof(int));
L.length = 0;
L.MaxSize = InitSize;
cout << "InitList(SeqList &L)初始化结束!" << endl;
}
// 增加动态数组的长度
void IncreaseSize(SeqList &L, int len){
cout << "IncreaseSize(SeqList &L, int len)增加动态数组长度中..." << endl;
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; // 顺序表最大长度增加
free(p); // 释放原来的内存空间
cout << "IncreaseSize(SeqList &L, int len)增加动态数组结束!" << endl;
}
int main(){
SeqList L; // 声明一个顺序表
InitList(L); // 初始化顺序表
// ...往顺序表中随便插入几个元素
// 此时 lenth 与 MaxSize 的值都为 10
IncreaseSize(L, 5);
return 0
顺序表的特点:(不管是静态分配还是动态分配)
- 随机访问,即可以在 O(1) 时间内找到第 i 个元素(因为数据在内存中连续存放);
- 存储密度高,每个节点只存储数据元素;
- 拓展容量不方便(即使采用动态分配的方式实现,拓展长度的时间复杂度也比较高);
- 插入、删除操作不方便,需要移动大量元素;
2.2_2 顺序表的插入删除
一、顺序表的插入操作
ListInsert(&L, i, e):插入操作。在表 L 中的第 i 个位置上(位序)插入指定元素 e 。
因为顺序表是用存储位置的相邻来体现数据元素之间的逻辑关系,所以数据与元素要想插入,必须还要和前驱、后继元素相邻。
这一小节讲的代码全都基于静态分配来实现:
#define MaxSize 10 // 定义最大长度
typedef struct{
ElemType data[MaxSize]; // 用静态的“数组”存放数据元素
int length; // 顺序表存放当前的长度
}SQList; // 顺序表的类型定义
ListInsert(SqList &L, int i, int e) 是将在顺序表 L 的位序 i 处插入元素 e,
因为想要插入的位序是 i ,所以还要把下标 i-1 (位序为i)的数字放到下标 i (位序为i+1)的地方 ,下标 i-1 对应的是插入位序为 i 的地方。
#include <iostream>
#include <cstdio>
#include <stdlib.h>
using namespace std;
#define MaxSize 10 // 定义最大长度
typedef struct{
int data[MaxSize]; // 用静态的“数组”存放数据元素
int length; // 顺序表的当前长度
}SqList; // 顺序表的类型定义
// 基本操作——初始化一个顺序表
void InitList(SqList &L){
cout << "InitList(SqList &L) 初始化中..." << endl;
for(int i = 0; i < MaxSize; i++){
L.data[i] = 0; // 将所有数据元素设置为默认初始值
}
L.length = 0; // 顺序表初始长度为0
cout << "InitList(SqList &L) 初始化完毕!" << endl << endl;
}
// 插入:在 L 的位序 i 处插入元素 e
bool ListInsert(SqList &L, int i, int e){
cout << "ListInsert(SqList &L, int i, int e)插入操作开始..." << endl;
// 判断插入位序 i 是否合法
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
cout << "ListInsert(SqList &L, int i, int e)插入操作结束!" << endl << endl;
return true;
}
int main(){
SqList L; // 声明一个顺序表
InitList(L); // 初始化顺序表
// ...此处省略一些代码,插入几个元素
L.length = 10; // 测试——这里我写的不规范,因为只增加了长度,没有具体的数值
ListInsert(L, 3, 3);
printf("L.length的值为 %d\n", L.length);
// 打印顺序表
printf("print打印%d\n",L.length);
for(int i = 0; i < L.length; i++){
printf("%d\n", L.data[i]);
}
printf("cout打印%d\n",L.length);
for(int j = 0; j < MaxSize; j++){ // 应为 j < L.length
cout << L.data[j] << endl;
}
return 0;
}
插入操作的时间复杂度:
计算时间复杂度时,关注最深层循环语句的执行次数与问题规模 n 的关系;
for(int j = L.length; j >= i; j--){
31行的 for 循环是本程序中最深层循环,问题规模 n = L.length(表长),
最好情况:新元素插入到表尾,不需要移动元素 i = n + 1,循环 0 次;最好时间复杂度 = O(1),也就是常数级别的时间复杂度;
最坏情况:新元素插入到表头,需要将原有的 n 个元素全部向后移动,循环 n 次;最坏时间复杂度 = O(n);
平均情况:假设新元素插入到任何一个位置的概率相同,即 i = 1, 2, 3, ..., lenth+1 的概率是 p = 1/(n+1);
二、顺序表的删除操作
删除元素:删除位置后面的元素向前移动一位,线性表长度 lenth -1;
#include <iostream>
#include <cstdio>
#include <stdlib.h>
using namespace std;
#define MaxSize 10 // 定义最大长度 10
typedef struct{
int data[MaxSize]; // 用静态的“数组”存放数据元素
int length; // 顺序表的当前长度
}SqList; // 顺序表的类型定义
// 基本操作——初始化一个顺序表
void InitList(SqList &L){
cout << "InitList(SqList &L) 初始化中..." << endl;
for(int i = 0; i < MaxSize; i++){
L.data[i] = 0; // 将所有数据元素设置为默认初始值
}
L.length = 0; // 顺序表初始长度为0
cout << "InitList(SqList &L) 初始化完毕!" << endl;
}
// 插入:在 L 的位序 i 处插入元素 e
bool ListInsert(SqList &L, int i, int e){
cout << "ListInsert(SqList &L, int i, int e)插入操作开始..." << endl;
// cout << "传入的三个值" << i << " " << e << endl;
// 判断插入位序 i 是否合法
if(i < 1 || i > L.length + 1){ // 判断 i 的范围是否有效
cout << "插入操作——错误!操作 i 的值无效。" << endl;
return false;
}
if(L.length >= MaxSize){ // 当前存储空间已满,不能插入
cout << "插入操作——错误!当前存储空间已满,不能插入。" << endl;
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
cout << "ListInsert(SqList &L, int i, int e)插入操作结束!" << endl;
return true;
}
// 删除:在 L 的位序 i 处删除元素 e
bool ListDelete(SqList &L, int i, int &e){
cout << "ListDelete(SqList &L, int i, int &e)删除操作开始..." << endl;
// 判断删除位序 i 是否合法
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
cout << "ListDelete(SqList &L, int i, int &e)删除操作结束!" <<endl;
return true;
}
int main(){
SqList L; // 声明一个顺序表
InitList(L); // 初始化顺序表
// ...此处省略一些代码,插入几个元素
L.length = 9; // 测试:手动增 lenth 值
// cout << "测试手动增lenth——L.length = " << L.length << endl;
// 插入操作
ListInsert(L, 3, 3);
printf("添加操作:结果打印测试\n");
for(int i = 0; i < L.length; i++){
printf("%d\n", L.data[i]);
}
// 删除操作
int e = -1; // 用变量 e 把删除的元素“带回来”
if(ListDelete(L, 3, e)){
printf("已删除第3个元素,删除元素值为 = %d\n", e);
} else{
printf("位序 i 不合法,删除失败\n");
}
printf("删除操作:结果打印测试\n");
for(int i = 0; i < L.length; i++){
printf("%d\n", L.data[i]);
}
return 0;
“&e”是引用数据类型,如果参数没有加引用符号,那么主函数内并不会接受到 ListDelete 返回的删除参数 e 的值。
删除元素操作是:将目标数据删除,然后将删除掉的目标元素的下一位补充到该位置,即一个元素一个元素向上移;
添加元素的操作是:将数值一位一位向下移,直至进行插入操作的数据元素位置,完成插入操作。
删除操作的时间复杂度:
关注最深层循环语句的执行次数与问题规模 n 的关系,问题规模 n = L.lenth 表长。
L.data[j-1] = L.data[j];
最好情况:删除表尾元素,不需要移动其他元素;
i = n ,循环 0 次;最好时间复杂度 = O(1);
最坏情况:删除表头元素,需要将后续的 n - 1 个元素全都向前移动;
i = 1,循环 n - 1 次;最坏时间复杂度 = O(n);
平均情况:假设删除任何一个元素的概率相同,即 i = 1, 2, 3, ... , lenth 的概率都是 p = 1 / n;
i = 1,循环 n - 1 次,i = n 时,循环 0 次;
2.2_3 顺序表的查找
一、按位查找
(1)顺序表的静态分配中的按位查找
*需要注意的是第 i 个元素(位序为 i 的元素)对应的数组下标是 i - 1
(可优化代码——加判断看需要查找的位序i是否合法)
// 20210623顺序表的静态分配中的按位查找
#include <iostream>
using namespace std;
#define MaxSize 10 // 定义最大长度
typedef struct{
int data[MaxSize]; // 用静态的“数组”存放数据元素
int length; // 顺序表当前的长度
}SqList; //顺序表的类型定义(静态分配方式)
// 初始化顺序表
void InitList(SqList &L){
for(int i = 0; i < MaxSize; i++){
L.data[i] = 0; // 将所有数据设置为默认值
}
L.length = 0; // 顺序表初始长度为0
}
// 顺序表的按位查找
int GetElem(SqList L, int i){
return L.data[i - 1];
}
int main(){
SqList L; // 声明一个顺序表
InitList(L); // 初始化顺序表
// 插入数据
for(int i = 0; i < MaxSize; i++){
L.data[i] = i;
}
// 打印顺序表
for(int i = 0; i < MaxSize; i++){
cout << L.data[i] << endl;
}
// 按位查找
int num = GetElem(L,5);
cout << "按位查找位序 5 上的值为" << num << endl;
return 0;
}
(2)顺序表的动态分配中的按位查找
当用数组存储数据时,指针根据数组的数据类型所占用的内存来计算每一个数据元素在的位置。
“用某一个类型的指针 + 数组下标" 的方式访问数据的时候,每次取几个字节的数据和定义的指针类型有关。
这也是之前强调过的,为什么使用 malloc 定义一片连续的内存空间时,需要把返回的指针类型强制转化为和数据类型相对应的同类型的指针。
// 20210623顺序表的动态分配中的按位查找
#include <iostream>
#include <stdlib.h>
using namespace std;
#define InitSize 10 // 顺序表的初始长度
typedef struct{
int *data; // 指示动态分配数组的指针
int MaxSize; // 顺序表的最大容量
int length; // 顺序表的当前长度
}SeqList; // 顺序表的类型定义(动态分配方式)
// 初始化
void InitList(SeqList &L){
L.data = (int *)malloc(InitSize * sizeof(int));
L.length = 0;
L.MaxSize = InitSize;
}
// 增加动态数组的长度
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;
free(p);
}
// 按位查找
int GetElem(SeqList L, int i){
return L.data[i - 1];
}
int main(){
SeqList L; // 声明一个顺序表
InitList(L); // 初始化顺序表
// 插入数据
for(int i = 0; i < L.MaxSize; i++){
L.data[i] = i;
L.length++;
}
// 打印顺序表
for(int i = 0; i < L.length; i++){
cout << L.data[i] << endl;
}
// 按位查找
int num = GetElem(L, 5);
cout << "按位查找 位序5 存放的数据为:" << num << endl;
return 0;
}
二、按值查找
顺序表的动态分配中的按值查找
从顺序表中的第一个元素依次往后检索,返回相等的值所对应的位序( i + 1)
*基本数据类型:int, char, double, float 等可以直接用运算符 "==" 比较
思考:结构类型的数据元素也这样吗?
回答:不能,编译阶段就会报错。
- C语言中,结构体的比较不能直接用“==”,需要依次对比各个分量来判断两个结构体是否相等。
- 可以直接写一个自定义判断函数,通过返回值来判断两个结构体的值是否相等。
- 在C++中,尝试使用运算符重载。
《数据结构》考研初试中,手写代码可以直接使用“==”,无论 ElemType 是基本数据类型还是结构类型。
手写代码主要考察学生是否能理解算法思想,不会要求代码完全可运行,
有的学校考《C语言程序设计》,就要语法严格一点。
按值查找的时间复杂度
关注最深层循环语句的执行次数与问题规模 n 的关系,问题规模 n = L.lenth (表长)
if(L.data[i] == e)
- 最好情况:目标元素在表头,循环一次,最好时间复杂度 = O(1)
- 最坏情况:目标元素在表尾,循环 n 次,最坏时间复杂度 = O(n)
- 平均情况:假设目标元素出现在任何一个位置的概率相同,都是 1/n ,