一. 了解顺序表
顺序表定义:
顺序表(也称为线性数组)是一种线性数据结构,它将数据元素按顺序存储在一块连续的内存空间中。顺序表的基本特征包括:
-
元素的顺序性:顺序表中的元素具有线性关系,每个元素都有一个唯一的位置(索引),可以通过索引直接访问。
-
存储方式 :顺序表的元素在内存中是连续存储的,这使得可以通过简单的下标访问实现快速的随机访问,时间复杂度为 O(1)。
-
固定大小:在创建顺序表时,通常需要指定其最大容量,虽然可以通过动态数组等形式实现扩展,但扩展过程可能涉及到大量的数据复制。
-
插入与删除操作 :在顺序表中进行插入和删除操作时,可能需要移动大量元素以保持顺序,这使得这些操作的时间复杂度为 O(n)。
-
适用场景:顺序表适合存储固定数量的数据,尤其是当数据量较小且访问频繁时,能够提供高效的性能。
顺序表优缺点:
优点包括快速随机访问,能够通过索引直接访问元素,时间复杂度为O(1)。实现相对简单,结构清晰,易于理解,适合初学者。此外,数据在内存中连续存储,能够提高缓存利用率和访问速度。对于小规模数据,内存开销较小。
缺点方面,顺序表的大小是固定的,必须预先定义容量,超出时需要重新分配内存,增加复杂性。插入和删除操作效率较低,因为需要移动大量元素,时间复杂度为 O(n)。如果预设容量过大,可能导致内存浪费。对于需要频繁插入和删除的场景,顺序表的性能表现不佳。
总的来说,顺序表适合随机访问和数据量相对固定的情况,但在动态数据管理和频繁修改方面存在不足。选择时需根据具体需求进行权衡。
二、顺序表的基本操作(C语言)
1. 静态分配与动态分配
静态分配
// 静态分配
#define MaxSize 50 // 定义线性表最大长度
typedef struct {
// ElemType data[MaxSize]; // 顺序表的元素 ElemType泛指你想设置的类型
int data[MaxSize]; // 顺序表的元素 这里以int作为参考
int length; // 顺序表当前长度(有效内容)
}SqList;
动态分配
// 动态分配
#define InitSize 100 // 表长度的初始化定义
typedef struct {
// ElemType *data; // 指示动态分配数组的指针
int *data; //
int Maxsize, length; // 指示2最大的容量和当前个数
}SeqList;
// 初始化动态分配语句(C语言)
// #include <stdlib.h>
// L.data = (ElemType*)malloc(sizeof(ElemType) * InitSize);
// free(L.data);//释放
2. 顺序表的初始化
静态初始化
// SqList L; // 假设创建了一个顺序表
// 初始化静态分配的顺序表
void InitList_Static(SqList* L) {
L->length = 0;
}
动态初始化
// 动态
void InitList_Dynamic(SeqList* L) {
L->data = (int*)malloc(sizeof(int) * InitSize); // 分配存储空间
L->length= 0; // 顺序表初始化长度为0
L->Maxsize = InitSize; // 初始存储容量
}
3. 插入操作
注意:bool报错可能是版本问题加入 // #include < stdbool.h >
静态插入
// 静态插入
bool ListInsert_Static(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--) {
L->data[j] = L->data[j - 1]; // 注意这里j是从length开始的,所以j-1是安全的
}
// 在用户指定的位置(从1开始)插入新元素
L->data[i - 1] = e; // 注意这里要减去1来访问数组的实际位置
L->length++; // 线性表长度加1
return true;
}
最好情况
在最好情况下,即当 i
等于 L->length + 1
时,新元素将被添加到列表的末尾,而不需要移动任何现有元素。因此,最好情况下的时间复杂度是 O(1) ,因为只执行了常数次数的操作(主要是更新 L->data[i - 1]
和 L->length
)。
最坏情况
在最坏情况下,即当 i
等于 1 时,新元素将被插入到列表的开头。这要求将列表中的所有现有元素都向后移动一个位置,以为新元素腾出空间。因此,最坏情况下的时间复杂度是 O(n) ,其中 n
是列表的当前长度(L->length
),因为需要执行与列表长度成正比的移动操作。
平均情况
平均情况的时间复杂度也取决于 i
的值。然而,由于 i
可以是 1 到 L->length + 1
之间的任何值,并且每个值被选择的概率(在不知道具体应用场景的情况下)可以假设是相等的,因此平均情况下需要移动的元素数量大约是列表长度的一半。但这并不直接转化为一个简单的时间复杂度表达式,因为"平均"的概念在这里涉及到的是一个分布,而不是一个固定的值。
然而,从渐进复杂度的角度来看,平均情况下的时间复杂度仍然是 O(n),因为即使在最平均的情况下,也需要执行与列表长度成线性关系的操作(尽管可能不是完整的列表长度)。
总结
- 最好情况:O(1)
- 最坏情况:O(n)
- 平均情况:虽然具体分布可能复杂,但从渐进复杂度的角度来看,也是 O(n)
这里的关键是理解,无论 i
的值如何,都可能需要移动一定数量的元素(在最坏情况下是全部,在最好情况下是零),而平均情况下这个数量与列表长度成正比。
动态插入
#define AddNewSize 10
bool InitInsert_Dynamic(SeqList* L, int i, int e) {
if (i < 1 || i > L->length + 1) return false;
// 如果需要扩容
if (L->length >= L->Maxsize) {
int new_MaxSize = L->Maxsize + AddNewSize; // 计算新的最大容量
int* new_data = (int*)malloc(sizeof(int) * new_MaxSize); // 分配新的内存块
if (new_data == NULL) return false; // 内存分配失败
// 复制旧数据到新内存块
for (int j = 0; j < L->length; j++) {
new_data[j] = L->data[j];
}
// 释放旧内存块
free(L->data);
// 更新SeqList的指针和大小信息
L->data = new_data;
L->Maxsize = new_MaxSize;
}
// 在指定位置插入新元素
for (int j = L->length; j >= i; j--) {
L->data[j] = L->data[j - 1]; // 将第i位及之后的元素后移
}
L->data[i - 1] = e; // 插入新元素
L->length++; // 线性表长度加1
return true;
}
分析 InitInsert_Dynamic
函数的时间复杂度,我们需要考虑两个主要部分:扩容(如果需要)和元素插入。
扩容部分
-
时间复杂度 :如果扩容发生,时间复杂度主要由内存分配和旧数据复制决定。内存分配通常被认为是常数时间操作(尽管实际复杂度可能依赖于系统和分配器的实现),但这里我们将其视为
O(1)
。旧数据复制需要遍历整个现有列表,因此其时间复杂度是O(n)
,其中n
是当前列表的长度(L->length
)。 -
发生概率 :扩容只在列表已满(即
L->length >= L->Maxsize
)时发生。如果L->Maxsize
足够大或插入操作不频繁,这个操作可能很少发生。
元素插入部分
-
时间复杂度 :无论是否需要扩容,插入操作都需要将从
i
位置开始的元素向后移动一个位置。在最坏情况下(即i = 1
),需要移动所有元素,时间复杂度为O(n)
。在最好情况下(即i = L->length + 1
),不需要移动任何元素,时间复杂度为O(1)
。 -
平均情况 :由于
i
可以是1
到L->length + 1
之间的任何值,且假设每个值被选择的概率相等,平均情况下需要移动的元素数量大约是列表长度的一半。然而,从渐进复杂度的角度来看,平均情况仍然被认为是O(n)
,因为存在一个与n
成正比的项(即使这个项是n
的一半)。
总结
-
最好情况 :如果不需要扩容且
i = L->length + 1
(即在列表末尾插入),则时间复杂度为O(1)
。 -
最坏情况 :如果需要扩容且
i = 1
(即在列表开头插入),则时间复杂度主要由扩容操作(O(n)
)和元素插入操作(O(n)
)决定,总体为O(n)
。但由于扩容通常不经常发生,更关注单次插入操作的最坏情况,也是O(n)
。 -
平均情况 :尽管平均情况下移动的元素数量可能较少,但从渐进复杂度的角度来看,时间复杂度仍然是
O(n)
,因为存在一个与n
成正比的项。 -
InitInsert_Dynamic
函数的时间复杂度在最好情况下为O(1)
,在最坏和平均情况下为O(n)
。注意,这里的n
是指插入操作执行时列表的当前长度。
3. 删除操作
静态删除
// 静态
bool ListDelete_Static(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;
}
动态删除
// 动态
bool ListDelete_Dynamic(SeqList* 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;
}
时间复杂度
这两个函数 ListDelete_Static
和 ListDelete_Dynamic
的时间复杂度分析是相似的,因为它们都执行了几乎相同的操作:检查索引的有效性、移动元素、减少长度,并返回结果。它们的主要区别在于它们操作的数据结构(SqList
和 SeqList
)的潜在性质,但这些性质在上下文中并不直接影响时间复杂度。
- 最好情况 发生在索引
i
正好是要删除的最后一个元素(即i == L->length
的前一个位置,因为索引从 1 开始)。在这种情况下,你不需要移动任何元素(除了更新长度),因此时间复杂度是 O(1) 。然而,由于i > L->length
的检查会立即返回false
,严格来说最好情况不会执行到元素移动的代码。但如果我们考虑的是一旦检查通过就立即开始的操作,那么可以认为最好情况的时间复杂度是 O(1)(尽管这种情况在函数内部逻辑上并不实际发生)。 - 最坏情况 发生在索引
i
指向列表的第一个元素(即i == 1
)。在这种情况下,你需要将除第一个元素之外的所有元素都向前移动一个位置,这涉及L->length - 1
次赋值操作。因此,时间复杂度是 O(n) ,其中 n 是列表的长度(L->length
)。 - 平均情况 很难精确计算,因为它取决于索引
i
的分布。但是,由于最坏情况(需要移动几乎所有元素)和最好情况(不需要移动元素)之间的显著差异,我们可以合理地假设平均情况接近于最坏情况,即 O(n) 。这是因为无论索引i
的值如何,只要它位于有效范围内,你都需要至少移动一定数量的元素(在最坏情况下是n-1
个,在最好情况下是 0 个)。
注意
- 请注意,虽然这两个函数在逻辑上相似,但它们的命名和上下文(即它们操作的数据结构)不同。
SqList
和SeqList
分别具有类似的属性(如data
指针和length
字段),但它们的实现细节(如内存分配和释放策略)不同。 SeqList
是一个动态分配的数据结构,并且data
指针指向的内存是动态分配的,还需要考虑内存管理的复杂性,但这与函数本身的时间复杂度分析不直接相关。- 请注意,如果
i
的值在调用函数之前没有得到有效的验证,那么可能会导致数组越界访问,这是一个严重的运行时错误。
4. 按值查找
静态查找
// 静态
int LocateElem_Static(SqList* L, int e) {
int i = 0;
for (i = 0; i < L->length; i++) {
if (L->data[i] == e)
return i + 1;
}
return 0; //
}
动态查找
int LocateElem_Dynamic(SeqList* L, int e) {
int i = 0;
for (i = 0; i < L->length; i++) {
if (L->data[i] == e)
return i + 1;
}
return 0; //
}
时间复杂度
对于 LocateElem_Static
和 LocateElem_Dynamic
这两个函数,它们的时间复杂度分析是相同的,因为它们的内部逻辑是完全一致的。这两个函数都是在顺序表中查找一个特定的元素 e
,并返回该元素的逻辑索引(如果找到的话;否则返回0)。
- 最好情况 发生在顺序表的第一个元素就是要查找的元素
e
。在这种情况下,函数只需要进行一次比较就可以找到元素,因此时间复杂度是 O(1)。然而,这种情况非常罕见,因为它要求目标元素恰好位于列表的开头。 - 最坏情况 发生在目标元素
e
不在顺序表中,或者它位于顺序表的最后一个位置。在这种情况下,函数需要遍历整个顺序表,比较L->length
次(因为索引是从0开始的,所以要比较到L->length - 1
),因此时间复杂度是 O(n) ,其中 n 是顺序表的长度(L->length
)。 - 平均情况 的时间复杂度很难精确计算,因为它取决于目标元素
e
在顺序表中出现的概率和位置。然而,由于最好情况和最坏情况之间的差异很大(O(1) vs O(n)),我们可以合理地假设平均情况的时间复杂度接近于最坏情况,即 O(n) 。这是因为,在不知道e
是否存在于顺序表中以及它位于何处的情况下,我们需要做好遍历整个顺序表的准备。
总结:
- 无论是
LocateElem_Static
还是LocateElem_Dynamic
,它们的时间复杂度在最好情况下是 O(1),在最坏情况下是 O(n),平均情况下也接近于 O(n)。 - 函数名的"Static"和"Dynamic"主要反映了它们操作的数据结构类型(尽管在这个上下文中,这种差异对于查找操作的时间复杂度分析来说并不重要),而不是函数本身的静态或动态特性。
- 查找操作的时间复杂度主要取决于顺序表的长度和元素在顺序表中的分布,而不是查找函数的具体实现细节。
三、简单线性表操作(C语言)
1. 求表长
// 求表长
int Length_Static(SqList *L) {
return L->length;
}
int Length_Dynamic(SeqList* L) {
return L->length;
}
2. 按位查找
// 按位查找
int GetElem_Static(SqList* L,int i) {
return L->data[i - 1];
}
int GetElem_Dynamic(SeqList* L, int i) {
return L->data[i - 1];
}
3. 输出操作
// 输出操作
void PrintList_Static(SqList* L) {
if (L == NULL || L->data == NULL) {
printf("顺序表为空或未初始化!\n");
return;
}
int i = 0;
for (i = 0; i < L->length; i++)
printf(" %d ", L->data[i]);
}
void PrintList_Dynamic(SeqList* L) {
if (L == NULL || L->data == NULL) {
printf("顺序表为空或未初始化!\n");
return;
}
int i = 0;
for (i = 0; i < L->length; i++)
printf(" %d ", L->data[i]);
}
4. 判空操作
// 判空操作(针对SqList)
bool Empty_Static(SqList * L) {
// 如果L是NULL,我们可以认为这是一个未初始化的顺序表,但在许多情况下,
// 我们可能期望在这种情况下调用函数是一个错误,或者已经由其他逻辑确保了L不是NULL。
// 这里我们只检查顺序表的长度是否为0。
if (L == NULL) {
// 可选:打印一个错误消息或处理异常情况
printf("警告:传入的顺序表指针为NULL!\n");
return true; // 或者可以抛出异常或退出函数,取决于您的错误处理策略
}
return L->length == 0;
}
// 判空操作(针对SeqList,SqList类似)
bool Empty_Dynamic(SeqList* L) {
if (L == NULL) {
// 同样的警告
printf("警告:传入的顺序表指针为NULL!\n");
return true; // 或者处理错误
}
return L->length == 0;
}
5. 销毁操作
// 销毁操作
void DestroyList_Static(SqList *L) {
L->length = 0;
}
void DestroyList_Dynamic(SeqList* L) {
if (L != NULL && L->data != NULL) {
free(L->data);
L->data = NULL; // 避免悬空指针
L->length = 0;
}
// 如果需要,也可以将L设置为NULL,但这取决于L是如何被管理的
}
四、总代码(C语言)
#include <stdio.h>
#include <stdlib.h>
#include < stdbool.h >
// 仔细说明,争取小白也能看懂
// 静态分配
#define MaxSize 50 // 定义线性表最大长度
typedef struct {
// ElemType data[MaxSize]; // 顺序表的元素 ElemType泛指你想设置的类型
int data[MaxSize ]; // 顺序表的元素 这里以int作为参考
int length; // 顺序表当前长度(有效内容)
}SqList;
// 动态分配
#define InitSize 100 // 表长度的初始化定义
typedef struct {
// ElemType *data; // 指示动态分配数组的指针
int *data; //
int Maxsize, length; // 指示2最大的容量和当前个数
}SeqList;
// 初始化动态分配语句(C语言)
// #include <stdlib.h>
// L.data = (ElemType*)malloc(sizeof(ElemType) * InitSize);
// free(L.data);//释放
// 顺序表的初始化
// 静态
// SqList L; // 假设创建了一个顺序表
// 初始化静态分配的顺序表
void InitList_Static(SqList* L) {
L->length = 0; // 顺序表初始化长度为0
}
// 动态
void InitList_Dynamic(SeqList* L) {
L->data = (int*)malloc(sizeof(int) * InitSize); // 分配存储空间
L->length = 0; // 顺序表初始化长度为0
L->Maxsize = InitSize; // 初始存储容量
}
// 插入操作
// 静态插入
bool ListInsert_Static(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--) {
L->data[j] = L->data[j - 1]; // 注意这里j是从length开始的,所以j-1是安全的
}
// 在用户指定的位置(从1开始)插入新元素
L->data[i - 1] = e; // 注意这里要减去1来访问数组的实际位置
L->length++; // 线性表长度加1
return true;
}
// 动态插入
#define AddNewSize 10
bool InitInsert_Dynamic(SeqList* L, int i, int e) {
if (i < 1 || i > L->length + 1) return false;
// 如果需要扩容
if (L->length >= L->Maxsize) {
int new_MaxSize = L->Maxsize + AddNewSize; // 计算新的最大容量
int* new_data = (int*)malloc(sizeof(int) * new_MaxSize); // 分配新的内存块
if (new_data == NULL) return false; // 内存分配失败
// 复制旧数据到新内存块
for (int j = 0; j < L->length; j++) {
new_data[j] = L->data[j];
}
// 释放旧内存块
free(L->data);
// 更新SeqList的指针和大小信息
L->data = new_data;
L->Maxsize = new_MaxSize;
}
// 在指定位置插入新元素
for (int j = L->length; j >= i; j--) {
L->data[j] = L->data[j - 1]; // 将第i位及之后的元素后移
}
L->data[i - 1] = e; // 插入新元素
L->length++; // 线性表长度加1
return true;
}
// 删除操作
// 静态
bool ListDelete_Static(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;
}
// 动态
bool ListDelete_Dynamic(SeqList* 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 LocateElem_Static(SqList* L, int e) {
int i = 0;
for (i = 0; i < L->length; i++) {
if (L->data[i] == e)
return i + 1;
}
return 0; //
}
// 动态
int LocateElem_Dynamic(SeqList* L, int e) {
int i = 0;
for (i = 0; i < L->length; i++) {
if (L->data[i] == e)
return i + 1;
}
return 0; //
}
// 求表长
int Length_Static(SqList *L) {
return L->length;
}
int Length_Dynamic(SeqList* L) {
return L->length;
}
// 按位查找
int GetElem_Static(SqList* L,int i) {
return L->data[i - 1];
}
int GetElem_Dynamic(SeqList* L, int i) {
return L->data[i - 1];
}
// 输出操作
void PrintList_Static(SqList* L) {
if (L == NULL || L->data == NULL) {
printf("顺序表为空或未初始化!\n");
return;
}
int i = 0;
for (i = 0; i < L->length; i++)
printf(" %d ", L->data[i]);
}
void PrintList_Dynamic(SeqList* L) {
if (L == NULL || L->data == NULL) {
printf("顺序表为空或未初始化!\n");
return;
}
int i = 0;
for (i = 0; i < L->length; i++)
printf(" %d ", L->data[i]);
}
/
// 判空操作(针对SqList)
bool Empty_Static(SqList * L) {
// 如果L是NULL,我们可以认为这是一个未初始化的顺序表,但在许多情况下,
// 我们可能期望在这种情况下调用函数是一个错误,或者已经由其他逻辑确保了L不是NULL。
// 这里我们只检查顺序表的长度是否为0。
if (L == NULL) {
// 可选:打印一个错误消息或处理异常情况
printf("警告:传入的顺序表指针为NULL!\n");
return true; // 或者可以抛出异常或退出函数,取决于您的错误处理策略
}
return L->length == 0;
}
// 判空操作(针对SeqList,假设与SqList类似)
bool Empty_Dynamic(SeqList* L) {
if (L == NULL) {
// 同样的警告
printf("警告:传入的顺序表指针为NULL!\n");
return true; // 或者处理错误
}
return L->length == 0;
}
// 销毁操作
void DestroyList_Static(SqList *L) {
L->length = 0;
}
void DestroyList_Dynamic(SeqList* L) {
if (L != NULL && L->data != NULL) {
free(L->data);
L->data = NULL; // 避免悬空指针
L->length = 0;
}
// 如果需要,也可以将L设置为NULL,但这取决于L是如何被管理的
}
int main() {
return 0;
}
五、总结
顺序表是一种简单而高效的数据结构,适合于需要快速随机访问的场景。然而,由于其固定大小和插入、删除操作的低效性,在需要频繁修改数据的情况下,可能需要考虑其他数据结构(如链表、动态数组等)。在实际应用中,选择合适的数据结构需要根据具体需求进行权衡。