引、由线性表到顺序表
所谓线性表就是逻辑结构呈线性的结构(这是人想象出来的结构),例如数组、链表、栈以及队列。然而逻辑上结构的线性并不代表物理结构上的线性(内存存储的实际结构),例如链表,他的每个数据并不是存储在“一条线”上的,只是每个节点可以指向下一个节点的地址,从而找到下一个节点。
而顺序表是一段物理地址连续的存储单元依次存放数据元素的线性结构,一般采用数组存储,是线性表的一种典型代表。
一、顺序表的分类
顺序表分为静态表和动态表。
静态顺序表 vs. 动态顺序表
特性 | 静态顺序表 | 动态顺序表 |
---|---|---|
空间分配 | 编译时确定,固定大小 | 运行时动态调整(如 realloc ) |
缺点 | 空间不足或浪费 | 需处理扩容逻辑 |
适用场景 | 数据量固定且已知 | 数据量变化频繁 |
动态顺序表通过倍增扩容策略(如容量不足时翻倍)减少频繁扩容的开销,提升效率。
二、动态顺序表的结构定义
创建头文件 seqlist.h ,函数文件 seqlist.c 以及主文件 test.c
在我前面的一篇博客(扫雷)中有一样的操作 ,此操作是为了让每个文件可以有自己的功能,方便管理。那这个seqlist.h要包含相应的头文件以及声明相应的函数,这里不做解释,直接给出代码。
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//定义动态顺序表的结构
typedef int SLDataType;
struct Seqlist
{
SLDataType* arr;
int size;
int capacity;
};
typedef struct Seqlist SL;
//顺序表的初始化
void SLInit(SL *ps);
//打印函数
void Print(SL* ps);
//尾插
void SLPushBack(SL* ps, SLDataType x);
//头插
void SLPushFront(SL* ps, SLDataType x);
//尾删
void SLPopBack(SL* ps);
//头删
void SLPopFront(SL* ps);
//在指定位置之前插入
void SLInsert(SL* ps, int pos, SLDataType x);
//删除指定位置的数据
void SLErase(SL* ps, int pos);
//查找
int SLFind(SL* ps, SLDataType x);
//销毁
void SLDestory(SL* ps);
一上来我们就应该定义好动态顺序表的结构,我们在第一行把 int 取名为 SLDataType 是为了更好的让数组中的 int 被统一管理起来。
typedef int SLDataType;
struct Seqlist {
SLDataType* arr; // 动态数组指针
int size; // 有效元素个数
int capacity; // 当前容量
};
typedef struct Seqlist SL;
-
arr
:指向动态分配的数组。 -
size
:记录实际存储的元素数量。 -
capacity
:当前数组的最大容量。
三、函数详解与实现逻辑
1.初始化与销毁
1.1 初始化SLInit(SL *ps)
功能:初始化顺序表,置空数组并重置大小和容量。
实现逻辑:
void SLInit(SL *ps) {
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
注意:初始化后需通过插入操作分配内存。
1.2 SLDestory(SL *ps)
功能:释放动态内存,防止内存泄漏。
实现逻辑:
void SLDestory(SL *ps) {
assert(ps);
if (ps->arr) free(ps->arr); // 释放数组
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
2.容量管理:
SLCheckCapacity(SL *ps)
功能:检查容量是否足够,不足时扩容。
实现逻辑:
void SLCheckCapacity(SL *ps) {
if (ps->size == ps->capacity) {
int newcapacity = (ps->capacity == 0) ? 4 : 2 * ps->capacity;
SLDataType* tmp = realloc(ps->arr, newcapacity * sizeof(SLDataType));
if (!tmp) {
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newcapacity;
}
}
关键点:
-
初始容量为4,后续按2倍扩容。
-
使用
realloc
而非malloc
,保留原有数据。 -
通过临时指针
tmp
接收扩容结果,避免内存泄漏。
3.插入与删除操作
3.1 尾插 SLPushBack(SL *ps, SLDataType x)
功能:在数组末尾插入元素。
实现逻辑:
void SLPushBack(SL *ps, SLDataType x) {
assert(ps);
SLCheckCapacity(ps);
ps->arr[ps->size++] = x;
}
时间复杂度:O(1)
3.2 头插 SLPushFront(SL *ps, SLDataType x)
功能:在数组头部插入元素。
实现逻辑:
void SLPushFront(SL *ps, SLDataType x) {
assert(ps);
SLCheckCapacity(ps);
// 所有元素后移一位
for (int i = ps->size; i > 0; i--) {
ps->arr[i] = ps->arr[i-1];
}
ps->arr[0] = x;
ps->size++;
}
时间复杂度:O(n),需移动所有元素。
3.3 指定位置插入 SLInsert(SL *ps, int pos, SLDataType x)
功能:在位置 pos
前插入元素。
实现逻辑:
void SLInsert(SL *ps, int pos, SLDataType x) {
assert(ps && pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
// 从后向前移动元素
for (int i = ps->size; i > pos; i--) {
ps->arr[i] = ps->arr[i-1];
}
ps->arr[pos] = x;
ps->size++;
}
注意:pos
的合法性需验证(0 ≤ pos ≤ size)。
3.4 删除操作(尾删、头删、指定位置删)
以尾删 SLPopBack
为例:
void SLPopBack(SL *ps) {
assert(ps && ps->size > 0);
ps->size--;
}
注意:仅减小 size
,内存不释放。
4.查找与打印
4.1 查找 SLFind(SL *ps, SLDataType x)
功能:返回元素 x
的索引,未找到返回 -1。
实现逻辑:
int SLFind(SL *ps, SLDataType x) {
for (int i = 0; i < ps->size; i++) {
if (ps->arr[i] == x) return i;
}
return -1;
}
时间复杂度:O(n)
4.2打印 Print(SL *ps)
功能:遍历并打印所有元素。
实现逻辑:
void Print(SL *ps) {
for (int i = 0; i < ps->size; i++) {
printf("%d ", ps->arr[i]);
}
}
5.测试用例与性能分析
测试代码(test.c)
void test01() {
SL sl;
SLInit(&sl);
SLPushFront(&sl, 1);
SLPushFront(&sl, 2);
SLPushFront(&sl, 3);
SLPushFront(&sl, 4);
SLErase(&sl, 2);
Print(&sl); // 输出:4 3 1
int index = SLFind(&sl, 3);
printf(index >= 0 ? "找到\n" : "未找到\n");
SLDestory(&sl);
}
结果分析:
-
插入 4, 3, 2, 1(头插顺序)。
-
删除位置2的元素(值为2),最终数组为 [4, 3, 1]。
-
查找元素3成功。
四、总结
动态顺序表通过数组和扩容策略平衡空间与时间效率。核心操作需注意边界条件和内存管理。我相信通过本文,读者可以全面掌握动态顺序表的设计与实现细节,并了解其在实际应用中的优化方向。如果读者有哪些地方不是很清楚可以私信我,我很乐意以图解形式给出解释,笔者在文中若有错误也请指正。