【数据结构】 [特殊字符] 顺序表详解——数据结构的第一块基石
“所有复杂的数据结构,都是从最简单的线性表演化而来。”
——《大话数据结构》
一、什么是“顺序表”?
在学习数据结构的第一步,我们会接触一个最基本、最常用的概念:线性表(Linear List)。
所谓线性表,就是“零个或多个数据元素的有限序列”,听起来有点抽象,我们换个说法👇
可以把它想象成一条火车,每一节车厢就是一个“元素”,每一节车厢有且只有一个前车厢(除第一节)和一个后车厢(除最后一节)。
📦 顺序表是什么?
顺序表(Sequential List)是一种线性表的顺序存储结构,即:
用一组连续的存储单元依次存放线性表中的各个元素。
通俗理解:
- 它像是一排整齐排列的“格子”(内存空间连续),
- 每个格子里存放一个数据元素,
- 元素之间的逻辑顺序与它们在内存中的物理顺序一致。
🔍 举个例子:
假设我们要存储学生的学号 [1001, 1002, 1003, 1004]
在内存中,顺序表的存放情况大致是这样的:
| 内存地址 | 内容(元素值) |
|---|---|
| 0x0010 | 1001 |
| 0x0014 | 1002 |
| 0x0018 | 1003 |
| 0x001C | 1004 |
这些内存地址是连续的,这就是“顺序存储”的特点。
二、顺序表的基本结构
顺序表通常用数组来实现。
在 C/C++、Java、Python 等语言中都可以用数组(或列表)来代表。
🎯 定义结构(C语言示例)
#define MAXSIZE 100 // 最大容量
typedef int ElemType; // 元素类型可自定义typedef struct {ElemType data[MAXSIZE]; // 存储元素的数组int length; // 当前长度(有效元素个数)
} SqList;
这里的 SqList 就是一个顺序表结构体,包含两部分:
- data[MAXSIZE]:存放数据元素的数组;
- length:记录当前表中实际存放了多少个元素。
三、顺序表的主要操作
顺序表支持一系列常见操作:增、删、查、改、遍历。
我们来一步步理解。
1️⃣ 插入(Insert)
在第
i个位置插入一个新元素。
思路:
- 检查插入位置是否合法(1 ≤ i ≤ length+1)
- 从最后一个元素开始,依次向后移动;
- 把新元素放到空出来的位置;
- 长度加1。
示例代码:
bool ListInsert(SqList *L, int i, ElemType e) {if (i < 1 || i > L->length + 1) return false; // 位置非法if (L->length >= MAXSIZE) return false; // 空间已满for (int j = L->length - 1; j >= i - 1; j--) {L->data[j + 1] = L->data[j]; // 元素后移}L->data[i - 1] = e; // 插入新元素L->length++;return true;
}
时间复杂度:O(n)(因为可能要移动很多元素)
2️⃣ 删除(Delete)
删除第
i个位置的元素。
思路:
- 检查删除位置是否合法;
- 记下被删除元素(方便返回);
- 从
i位置后一个元素开始依次前移; - 长度减1。
示例代码:
bool ListDelete(SqList *L, int i, ElemType *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;
}
时间复杂度:O(n)(同样因为移动元素)
3️⃣ 查找(Locate)
查找值为
e的元素位置。
思路: 从头到尾逐个比较,直到找到目标元素。
int LocateElem(SqList L, ElemType e) {for (int i = 0; i < L.length; i++) {if (L.data[i] == e)return i + 1; // 位置从1开始}return 0; // 未找到
}
时间复杂度:O(n)
如果是有序顺序表,可以使用二分查找,效率更高(O(log n))。
4️⃣ 访问(GetElem)
直接访问第 i 个元素。
因为顺序表在内存中是连续存储的,所以:
L.data[i - 1]
即可直接访问。
时间复杂度:O(1) ✅
这也是顺序表最大的优势之一!
5️⃣ 遍历(Traverse)
void TraverseList(SqList L) {for (int i = 0; i < L.length; i++) {printf("%d ", L.data[i]);}printf("\n");
}
四、顺序表的优缺点分析
| 优点 👍 | 缺点 👎 |
|---|---|
| 随机访问速度快(O(1)) | 插入、删除效率低(O(n)) |
| 内存连续,结构简单 | 容量固定(除非动态扩展) |
| 适合存储静态数据 | 不适合频繁增删的场景 |
⚖️ 举个生活例子
想象你有一排整齐放好的书架(顺序表),
每本书都有固定位置编号,你可以瞬间找到第N本书(随机访问)。
但如果要在第2本和第3本之间插入一本新书,
就得把后面的书都往后挪一格 —— 这就比较麻烦了。
🧠 五、动态顺序表(可扩展版)
上面的顺序表用的是固定数组(ElemType data[MAXSIZE]),
这在实际中有很大局限:一旦容量满了,就不能再存新元素。
为了解决这个问题,我们需要一种能自动扩展容量的顺序表,也就是:
✅ “动态顺序表”(Dynamic Sequential List)
🌱 动态顺序表的基本思想
动态顺序表使用堆区动态分配的数组来存储数据,
当容量不够时,它会自动:
- 分配一块更大的新内存;
- 把旧数据复制过去;
- 释放旧的内存;
- 更新容量信息。
这就像一个“会自己长大的容器”🌾。
📦 结构定义(C++实现)
#include <iostream>
#include <cstring> // memcpytemplate <typename T>
class SeqList {
private:T* data; // 指向动态数组的指针int length; // 当前元素个数int capacity; // 当前容量(最大可存元素数)// 私有函数:扩容操作void expandCapacity() {int newCapacity = capacity * 2; // 扩容为原来的2倍T* newData = new T[newCapacity]; // 分配新空间// 拷贝旧数据std::memcpy(newData, data, sizeof(T) * length);delete[] data; // 释放旧空间data = newData; // 更新指针capacity = newCapacity;std::cout << "[扩容成功] 新容量: " << capacity << std::endl;}public:// 构造函数SeqList(int initCap = 10) {capacity = initCap;data = new T[capacity];length = 0;}// 析构函数~SeqList() {delete[] data;}// 获取长度int size() const { return length; }// 判断是否为空bool empty() const { return length == 0; }// 按位置访问元素T& operator[](int index) {if (index < 0 || index >= length)throw std::out_of_range("索引越界");return data[index];}// 插入元素(在末尾)void push_back(const T& value) {if (length >= capacity) expandCapacity();data[length++] = value;}// 插入元素(指定位置)void insert(int pos, const T& value) {if (pos < 0 || pos > length)throw std::out_of_range("插入位置非法");if (length >= capacity) expandCapacity();for (int i = length; i > pos; i--) {data[i] = data[i - 1];}data[pos] = value;length++;}// 删除元素(指定位置)void erase(int pos) {if (pos < 0 || pos >= length)throw std::out_of_range("删除位置非法");for (int i = pos; i < length - 1; i++) {data[i] = data[i + 1];}length--;}// 遍历输出void print() const {for (int i = 0; i < length; i++) {std::cout << data[i] << " ";}std::cout << std::endl;}
};
⚙️ 使用示例
int main() {SeqList<int> list(3); // 初始容量为3list.push_back(10);list.push_back(20);list.push_back(30);list.push_back(40); // 触发自动扩容list.insert(2, 99); // 在第2个位置插入元素list.print(); // 输出: 10 20 99 30 40list.erase(3); // 删除第3个位置的元素list.print(); // 输出: 10 20 99 40std::cout << "当前长度: " << list.size() << std::endl;return 0;
}
🧩 输出结果:
[扩容成功] 新容量: 6
10 20 99 30 40
10 20 99 40
当前长度: 4
可以看到:
- 当容量不足时,自动打印出“扩容成功”提示;
- 插入、删除、访问都和静态顺序表一样;
- 但它能动态增长,更灵活!
📊 动态顺序表的性能分析
| 操作 | 平均时间复杂度 | 说明 |
|---|---|---|
| 随机访问 | O(1) | 支持下标访问 |
| 插入(尾部) | O(1)* | 摊还复杂度(扩容时为O(n)) |
| 插入(中间) | O(n) | 需要移动元素 |
| 删除 | O(n) | 需要移动元素 |
| 扩容 | O(n) | 需重新分配与复制数据 |
⚠️ “摊还复杂度”是指虽然单次扩容很耗时,但总体平均下来仍然高效。
💬 与 C++ STL vector 的关系
其实,你刚才实现的 SeqList
就是一个简化版的 std::vector!
自定义 SeqList | C++ STL vector |
|---|---|
push_back() | ✅ 同名函数 |
insert() | ✅ 同功能 |
erase() | ✅ 同功能 |
| 自动扩容 | ✅ 同逻辑(2倍扩容) |
| 支持随机访问 | ✅ 支持 [] 运算符 |
区别只是:
vector有更多边界检查与异常安全;- 支持迭代器;
- 扩容策略更智能(可能不是严格2倍)。
🪴 内存扩容原理图示
假设初始容量为 3:
容量 = 3
[10][20][30]
插入第4个元素 → 容量不足 → 自动扩容:
新容量 = 6
[10][20][30][40][_][_]
扩容完成后,所有旧数据被复制到了新内存空间中。
🧠 小结
| 特点 | 描述 |
|---|---|
| 动态分配内存 | 支持自动增长 |
| 连续存储 | 支持快速随机访问 |
| 高效尾插 | 适合栈、队列、动态数组实现 |
| 不适合频繁中间插入/删除 | 因为要移动大量元素 |
六、应用场景
顺序表特别适合:
- 数据量相对固定;
- 不需要频繁插入删除;
- 需要随机访问的场景。
例如:
- 学生成绩表;
- 商品价格列表;
- 图形界面中控件位置表;
- 或各种“批量处理”类数据。
七、小结:一句话记住顺序表
顺序表 = 一块连续的内存 + 有序的数据元素
访问快,插删慢,适合存静态数据。
八、延伸阅读
- 《大话数据结构》第三章:线性表
- STL 源码剖析:
vector实现原理 - 数据结构可视化工具:https://visualgo.net
✏️ 总结表
| 操作 | 平均时间复杂度 | 最佳时间复杂度 | 说明 |
|---|---|---|---|
| 访问 | O(1) | O(1) | 随机访问速度最快 |
| 插入 | O(n) | O(1)(尾插) | 需移动元素 |
| 删除 | O(n) | O(1)(删尾) | 需移动元素 |
| 查找 | O(n) | O(log n)(若有序) | 线性或二分查找 |
💡 结语
顺序表看似简单,却是理解数据结构的起点。
只有真正弄懂它的存储方式和操作逻辑,
你才能更轻松地理解链表、栈、队列、树乃至图的结构。
从顺序表开始,我们正式踏上数据结构的世界。
