跳到主要内容数据结构核心:顺序表的原理与模拟实现 | 极客日志C++算法
数据结构核心:顺序表的原理与模拟实现
顺序表作为线性表的顺序存储结构,利用连续内存单元存储数据。文章详细解析了静态与动态顺序表的区别,重点演示了动态顺序表的底层模拟实现,包括初始化、扩容策略及增删改查操作。同时对比了竞赛中常用的静态数组封装方式与 C++ STL 中 vector 容器的应用,帮助读者深入理解内存管理与数据结构设计。
樱花落尽1 浏览 线性表
线性表(linear list)是 n 个具有相同特性的数据元素的有限序列。作为一种在实际中广泛使用的数据结构,常见的线性表包括顺序表、链表、栈、队列和字符串等。
从逻辑结构上看,线性表是连续的直线;但在物理存储上并不一定连续。通常以数组或链式结构进行物理存储。
顺序表
概念与结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组来实现。
顺序表和数组的区别
顺序表的底层结构是数组,可以看作是对数组的封装,实现了常用的增删改查等接口。
简单来说,数组 ⊆ 线性表。数组只是顺序表的一种具体实现形式。
分类
静态顺序表
使用定长数组存储元素。

缺陷:空间给少了不够用,给多了造成空间浪费。不过在竞赛场景中,经常直接使用静态申请数组的方式。
动态顺序表
动态顺序表按需申请内存,不会造成空间浪费。

动态顺序表模拟实现
下面我们来动手实现一个动态顺序表,重点理解其底层逻辑。
定义动态顺序表结构
在头文件中,我们需要定义数据类型和结构体:
typedef int SLTDataType;
typedef struct SeqList {
SLTDataType* arr;
int size;
int capacity;
} SL;
顺序表初始化
初始化时,将指针置空,计数归零:
void SLInit(SL* ps) {
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
顺序表销毁
销毁时需要释放动态分配的内存,并重置状态,避免重复释放:
void SLDestroy(SL* ps) {
if (ps->arr) {
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
顺序表打印
void SLPrint(SL* ps) {
for (int i = 0; i < ps->size; i++) {
printf("%d ", ps->arr[i]);
}
printf("\n");
}
顺序表动态扩容
当有效数据个数等于容量时,需要扩容。通常采用倍增策略,如果当前为 0 则初始化为 4:
void SLCheckCapacity(SL* ps) {
if (ps->size == ps->capacity) {
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLTDataType* tmp = (SLTDataType*)realloc(ps->arr, newCapacity * sizeof(SLTDataType));
if (tmp == NULL) {
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
这里要注意 realloc 可能失败,必须检查返回值。同时,扩容后更新容量字段。
尾插
void SLPushBack(SL* ps, SLTDataType x) {
assert(ps);
SLCheckCapacity(ps);
ps->arr[ps->size++] = x;
}
头插
void SLPushFront(SL* ps, SLTDataType x) {
assert(ps != NULL);
SLCheckCapacity(ps);
for (int i = ps->size; i > 0; i--) {
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
ps->size++;
}
尾删
void SLPopBack(SL* ps) {
assert(ps && ps->size);
ps->size--;
}
头删
void SLPopFront(SL* ps) {
assert(ps && ps->size);
for (int i = 0; i < ps->size - 1; i++) {
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
查找
int SLFind(SL* ps, SLTDataType x) {
assert(ps);
for (int i = 0; i < ps->size; i++) {
if (ps->arr[i] == x) {
return i;
}
}
return -1;
}
指定位置之前插入
在 pos 位置插入,需保证 pos 合法,且移动 pos 之后的数据:
void SLInsert(SL* ps, int pos, SLTDataType x) {
assert(ps);
assert(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 位置的数据
void SLErase(SL* ps, int pos) {
assert(ps);
assert(pos >= 0 && pos < ps->size);
for (int i = pos; i < ps->size - 1; i++) {
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
竞赛中的静态顺序表
在算法竞赛中,为了追求极致的运行效率,往往不依赖动态内存管理,而是直接使用静态数组。
静态申请数组
单个顺序表
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int a[N];
int n;
void print() {
for (int i = 1; i <= n; i++) {
cout << a[i] << " ";
}
cout << endl << endl;
}
void push_back(int x) {
a[++n] = x;
}
void push_front(int x) {
for (int i = n; i >= 1; i--) {
a[i + 1] = a[i];
}
a[1] = x;
n++;
}
void insert(int p, int x) {
for (int i = n; i >= p; i--) {
a[i + 1] = a[i];
}
a[p] = x;
n++;
}
void pop_back() {
n--;
}
void pop_front() {
for (int i = 2; i <= n; i++) {
a[i - 1] = a[i];
}
n--;
}
void erase(int p) {
for (int i = p + 1; i <= n; i++) {
a[i - 1] = a[i];
}
n--;
}
int find(int x) {
for (int i = 1; i <= n; i++) {
if (a[i] == x) return i;
}
return 0;
}
int at(int p) {
return a[p];
}
void change(int p, int x) {
a[p] = x;
}
void clear() {
n = 0;
}
多个顺序表
int a1[N], n1;
int a2[N], n2;
int a3[N], n3;
void push_back(int a[], int& n, int x) {
a[++n] = x;
}
void test() {
push_back(a1, n1, 1);
push_back(a3, n3, 2);
}
封装静态顺序表
使用类或结构体封装,提供类似 STL 的接口调用体验:
class SqList {
int a[N];
int n;
public:
SqList() { n = 0; }
void push_back(int x) {
a[++n] = x;
}
void pop_back() {
n--;
}
void print() {
for (int i = 1; i <= n; i++) {
cout << a[i] << " ";
}
cout << endl;
}
};
int main() {
SqList s1, s2;
for (int i = 1; i <= 5; i++) {
s1.push_back(i);
s2.push_back(i * 2);
}
s1.print();
s2.print();
return 0;
}
通过封装,我们可以像调用标准库一样使用自定义接口,避免了重复造轮子。比如 C++ 的 vector 就是这种思想的极致体现。
动态顺序表 -- vector
C++ 的 STL 提供了已经封装好的容器 vector,它本质上就是一个会自动扩容的动态顺序表。创建以及增删查改等逻辑都已实现好,我们只需关注业务逻辑。
创建 vector
vector<int> a1;
vector<int> a2(N);
vector<int> a3(N, 2);
vector<int> a4 = {1, 2, 3, 4, 5};
vector<string> a5;
vector<node> a6;
vector<vector<int>> a7;
int a10[N];
vector<int> a9[N];
size / empty
print(a2);
print(a3);
print(a4);
if (a2.empty()) cout << "空" << endl;
else cout << "不空" << endl;
if (a1.empty()) cout << "空" << endl;
else cout << "不空" << endl;
begin / end
begin 返回起始位置的迭代器(左闭),end 返回终点位置的下一个位置的迭代器(右开)。利用迭代器可以访问整个 vector,存在迭代器的容器就可以使用范围 for 遍历。
void test_it() {
vector<int> a(10, 1);
for (auto it = a.begin(); it != a.end(); it++) {
cout << *it << " ";
}
cout << endl << endl;
for (auto x : a) {
cout << x << " ";
}
cout << endl << endl;
}
push_back / pop_back
for (int i = 0; i < 5; i++) {
a1.push_back(i);
print(a1);
}
while (!a1.empty()) {
a1.pop_back();
print(a1);
}
front / back
void test_fb() {
vector<int> a(5);
for (int i = 0; i < 5; i++) {
a[i] = i + 1;
}
cout << a.front() << " " << a.back() << endl;
}
resize
如果大于原始的大小,多出来的位置会补上默认值(一般是 0);如果小于原始的大小,相当于把后面的元素全部删掉。
void print(vector<int>& a) {
for (auto x : a) {
cout << x << " ";
}
cout << endl;
}
void test_resize() {
vector<int> a(5, 1);
a.resize(10);
print(a);
a.resize(3);
print(a);
a.resize(5, 2);
print(a);
}
clear
清空 vector,底层实现时会遍历整个元素,一个一个删除,因此时间复杂度 O(N)。
cout << a.size() << endl;
a.clear();
cout << a.size() << endl;
insert / erase
a4.insert(a4.begin() + 2, 0);
print(a4);
a4.erase(a4.begin() + 2);
print(a4);
return 0;
}
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online