2、数据结构——简单的线性表——顺序表(一)

一、线性表的基本概念

严谨定义

​ 线性表是具有相同数据类型的n(n≥0)个数据元素有限序列,其中n为表长,当n=0时线性表是一个空表。若用L命名线性表,则其一般表示为
$$
L=(a1,a2,…,aiai+1…,an)
$$

image-20240205205027913

专业概念

  • ai 是线性表的“第i个” 元素线性表中的位序

  • a1表头元素 ;an表尾元素

  • 除了第一个元素之外,每个元素都只有一个直接前驱,比如,**a2**是 a1直接前驱;除了最后一个元素之外,每个元素都只有一个直接后继例子和前面是同理的

二、理解到底什么是线性表

​ 实际上线性表使用大白话来讲,可以理解为:将一组一组数据按照一定的关系联系起来的结构。例子

image-20240205210833561

三、线性表的基本操作(实际上之后我们介绍的每一种结构都是按照这种思路进行分析学习的)

  • 初始化操作(InitList(&L))

  • 销毁操作(DestoryList(&L))

  • 插入操作(ListInsert(&L,i,e))

  • 删除操作(ListDelete(&L,i,&e))

  • 按值查找(LocateElem(L,.e))

  • 按位置查找(GetElem(L,i))

  • 求线性表长(Length(L))

  • 输出线性表所有元素

  • 判断表是否为空

补充知识

&这个符号的含义: 就是将参数修改的结果带回来。换句话说就是你给函数传了一个X,函数内部对X的处理会真正影响到函数外的X的数值,比如你调用一个函数实现X+1操作,那么你的原本数据也会随之加1.

​ 接下来我们就会讲解线性表的具体类型已经如何结合结构体来实现这种关系……

四、顺序表

定义

​ 用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。线性表是具有相同数据类型的所占空间大小也相同的n**(n≥0)**个数据元素的有限序列。

结构体定义

1
2
3
4
5
6
7
8
9
10
11
12
typedef int Elemtype; // 写Elemtype就相当于写int,好处是:之后你需要修改数据类型只需要修改此处的类型即可,不用全局修改
#define MaxSize 10
typedef struct{
ElemType data[MaxSize]; // 用静态的“数组”存放数据元素
int length; // 顺序表长度
}SqList;

typedef struct{
int *data;
int MaxSize;
int length;
}SqList; // 动态存储

对于动态存储的data的理解: 假设我们为p分配了40个字节的空间,由于1个int类型数据占4个字节,因此我们就可以为p分配10个int类型的数据,地址是从2000~2040(这个2000是我假设的指针p的第一个元素起始位置)

image-20240206124400825

实现功能一:初始化一个顺序表(完整代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 1、静态存储方式
#include <stdio.h>
typedef int Elemtype;
#define MaxSize 10; // 这里最好不要无限大,否则会存在浪费空间的情况
typedef struct{
ElemType data[MaxSize];
int length;
}SqList; // 静态存储方式,就是说你事先已经规定好了存取的数组大小MaxSize


// 1、初始化功能
void InitList(SqList &L) {
for (int i = 0; i < MaxSize; i ++) {
L.data[i] = 0; // 一般的情况来说,全局变量的初始值均为0,但是肯呢个会存在脏数据,所以建议进行一下赋值为0的初始化操作
}
L.length=o;
}

int main() {
SqList L;
InitList(L); // 初始化
// 待续......
return 0;
}



// 2、动态存储方式
#include <stdio.h>
#include <stdilb.h>
typedef int Elemtype;
#define InitSize 10; // 这里最好不要无限大,否则会存在浪费空间的情况
typedef struct{
int *data;
int MaxSize;
int length;
}SqList; // 动态存储


// 1、初始化功能
void InitList(SqList &L) {
L.data=(int *)malloc(sizeof(int) * InitSize); // 为data分配一段连续的内存空间
L.length=0;
L.MaxSize=InitSize;
}

// 2、增长动态数组长度功能(注意:实际是给数组分配更多的空间)
void InsertSqlist(Sqlist &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 main() {
SqList L;
InitList(L); // 初始化
InsertSqlist(L,5);
return 0;
}


实现功能二:插入元素

image-20240206111442417 image-20240206112400460
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 1、插入代码1.0(尚未考虑特殊情况)
// 前面的初始化代码省略......
void ListInsert(Sqlist &L,int i,int e) {
for (int j=L.length;j >= i; j--) {1
L.data[j] = L.data[j - 1];
}
L.data[i - 1] = e;
L.length++;
}

int main() {
Sqlist L;
InitList(L);

// 在顺序表中插入几个元素
ListInsert(L,1,1);
ListInsert(L,2,2);
ListInsert(L,3,3);
ListInsert(L,4,4);
ListInsert(L,5,5);

// 现在在L的第2个位置后(第3个位置)面添加元素6
ListInsert(L,3,6);
return 0;
}

//2、插入代码2.0(考虑到空间不够或者空间饱满情况),修改bool ListInsert(Sqlist &L,int i, int e)
bool ListInsert(Sqlist &L,int i, int e) {
if (i < 1 || i > L.length + 1) return false;
if (L.length >= MaxSize) return false;
for (int j=L.length;j >= i; j--) {1
L.data[j] = L.data[j - 1];
}
L.data[i - 1] = e;
L.length++;
return true;
}

2.0是健壮的代码,可以应对特殊情况

image-20240206112941242

时间复杂度

  • 最好情况: 就是元素刚好插到顺序表尾部,这时就不需要移动元素,复杂度:O(1)

  • 最坏情况: 就是元素插在顺序表的最前面,这时就需要将顺序表所有元素向后移动一个位置 复杂度:O(n)

  • 平均情况: 新元素插入到任何一个位置的概率是相同的,为image-20240206113814574i=1开始,循环n次;i=2时,循环n-1次;……i=n+1,循环0次。现在计算一下平均循环次数 = n*p + (n - 1) p …… 1 * p = p(1 + 2 + 3 + … + n) = p * n(n +1)/2=n/2

    平均时间复杂度:O(n)

实现功能三:删除元素(其实和插入操作异曲同工)

image-20240206115122889
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 前面介绍过的代码就不写了......
bool ListDelete(Sqlist &L,int i,int &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;
}

int main() {
Sqlist L;
InitSqlist(L);
// 和前面一样插入几个元素...
int e = -1; // 记录删除的具体元素
if (ListDelete(L,3,e)) {
printf("删除第3个元素成功,第3个元素使%d",e);
} else {
printf("删除不合法!!!");
}
return 0;
}

时间复杂度

  • 和插入操作是一模一样的,推理过程几乎都是一样的,所以这里就不重复介绍了……

实现功能三:按位置查找元素

1
2
3
4
5
// 对于静态存储方式和动态存储方式两种写法,按照位置查找元素的方式都是相同的
ElemType GetElem(Sqlist L, int i) {
return L.data[i - 1];
}

时间复杂度:O(1)

实现功能四:按值查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 当我们需要查找的数据元素仅仅是个简单数据类型(int、float、double......) 1.0
int GetByElem(Sqlist L,ElemType e) {
for (int i = 0;i < L.length; i++) {
if (L.data[i] == e) {
return i + 1;
}
}
return 0;
}

// 当我们需要查找的数据元素是个结构体类型 2.0,我们就需要将内部的各个元素进行比较
for (int i = 0;i < L.length; i++) {
if (L.data[i].a == e.a && L.data[i] == e.b && L.data[i].c == e.c) {
return i + 1;
}
}
return 0;
}

时间复杂度

和之前的插入、删除是一模一样的思路,此处不再讲解。

顺序表特点

  • 随意访问,即可以在O(1)时间找到第i个元素
  • 存储密度高,每个节点只存储数据元素
  • 拓展容量不方便(即便使用动态分配的方式实现,时间复杂度也是很高的)
  • 插入删除操作不方便,需要移动大量元素

注意事项

​ 我们需要非常注意我们何时需要添加引用& ,何时不需要添加,加了之后,函数内部对元素的操作就会直接影响到传入的那个参数本身!!!

下一节内容是:3、简单的线性表——单链表!!!

一定要把本节内容看懂再往下看,不然会非常痛苦的哦o(╥﹏╥)oo(╥﹏╥)oo(╥﹏╥)o……..