跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

数据结构核心:顺序表的原理与模拟实现

顺序表作为线性表的顺序存储结构,利用连续内存单元存储数据。文章详细解析了静态与动态顺序表的区别,重点演示了动态顺序表的底层模拟实现,包括初始化、扩容策略及增删改查操作。同时对比了竞赛中常用的静态数组封装方式与 C++ STL 中 vector 容器的应用,帮助读者深入理解内存管理与数据结构设计。

樱花落尽发布于 2026/3/23更新于 2026/7/2436 浏览
数据结构核心:顺序表的原理与模拟实现

线性表

线性表(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);
    
    // pos 及之后数据向后挪动一位
    for (int i = ps->size; i > pos; i--) {
        ps->arr[i] = ps->arr[i - 1];
    }
    ps->arr[pos] = x;
    ps->size++;
}
删除 pos 位置的数据

删除后,pos 之后的数据向前覆盖:

// 删除 pos 位置的数据
void SLErase(SL* ps, int pos) {
    assert(ps);
    assert(pos >= 0 && pos < ps->size);
    
    // pos 后面的数据向前挪动一位
    for (int i = pos; i < ps->size - 1; i++) {
        ps->arr[i] = ps->arr[i + 1];
    }
    ps->size--;
}

竞赛中的静态顺序表

在算法竞赛中,为了追求极致的运行效率,往往不依赖动态内存管理,而是直接使用静态数组。

静态申请数组

单个顺序表

通过全局大数组模拟,手动维护元素个数 n:

#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) {
    // 先把 [1, n] 的元素统一向后移动一位
    for (int i = n; i >= 1; i--) {
        a[i + 1] = a[i];
    }
    // 把 x 放在表头
    a[1] = x;
    n++; // 元素个数 +1
}

// 在任意位置插入
void insert(int p, int x) {
    // 先把 [p, n] 的元素统一向后移动一位
    for (int i = n; i >= p; i--) {
        a[i + 1] = a[i];
    }
    a[p] = x;
    n++;
}

// 尾删
void pop_back() {
    n--;
}

// 头删
void pop_front() {
    // 先把 [2, n] 区间内的所有元素,统一左移一位
    for (int i = 2; i <= n; i++) {
        a[i - 1] = a[i];
    }
    n--;
}

// 任意位置删除
void erase(int p) {
    // 把 [p + 1, n] 的元素,统一左移一位
    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
// 1. 创建 vector 常用的四种方式
vector<int> a1; // 创建了一个名字为 a1 的空的可变长数组,里面都是 int 类型的数据
vector<int> a2(N); // 创建了一个大小为 10 的可变长数组,里面的值默认都是 0
vector<int> a3(N, 2); // 创建了一个大小为 10 的可变长数组,里面的值都初始化为 2
vector<int> a4 = {1, 2, 3, 4, 5}; // 初始化列表的创建方式

// <> 里面可以存放任意的数据类型,这就体现了模板的作用
vector<string> a5; // 存字符串
vector<node> a6; // 存结构体
vector<vector<int>> a7; // 创建了一个二维的可变长数组

// 注意:vector 数组和原生数组的区别
int a10[N]; // 创建了一个大小为 N 的 int 类型数组 数组名叫 a10
vector<int> a9[N]; // 创建了一个大小为 N 的 vector 数组 数组名叫 a9
size / empty
// 2. size / empty
// size 返回实际元素的个数
print(a2);
print(a3);
print(a4);

// empty 返回顺序表是否为空,返回类型是一个 bool 类型的返回值。
// 如果为空返回 true,不空返回 false
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);
    
    // 迭代器的类型是 vector<int>::iterator,但是一般使用 auto 简化
    for (auto it = a.begin(); it != a.end(); it++) {
        cout << *it << " ";
    }
    cout << endl << endl;
    
    // 使用语法糖 - 范围 for 遍历
    for (auto x : a) {
        cout << x << " ";
    }
    cout << endl << endl;
}
push_back / pop_back
// 4. 尾插以及尾删
for (int i = 0; i < 5; i++) {
    a1.push_back(i);
    print(a1);
}
while (!a1.empty()) {
    a1.pop_back();
    print(a1);
}
front / back
// 5. front / back
// front:返回首元素;
// back:返回尾元素;
// cout << a4.front() << " " << a4.back() << endl;

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;
}

// 6. resize
void test_resize() {
    vector<int> a(5, 1);
    a.resize(10); // 扩大
    print(a);
    a.resize(3); // 缩小
    print(a);
    // 扩大成 5,并且多余的修改为 2
    a.resize(5, 2);
    print(a);
}
clear

清空 vector,底层实现时会遍历整个元素,一个一个删除,因此时间复杂度 O(N)。

// 底层实现的时候,会遍历整个元素,一个一个删除,因此时间复杂度 O(N)
cout << a.size() << endl;
a.clear();
cout << a.size() << endl;
insert / erase

参数为迭代器,支持在任意位置插入或删除。

// 8. insert/erase 参数为迭代器
a4.insert(a4.begin() + 2, 0);
print(a4);
a4.erase(a4.begin() + 2);
print(a4);
return 0;
}

目录

  1. 线性表
  2. 顺序表
  3. 概念与结构
  4. 顺序表和数组的区别
  5. 分类
  6. 静态顺序表
  7. 动态顺序表
  8. 动态顺序表模拟实现
  9. 定义动态顺序表结构
  10. 顺序表初始化
  11. 顺序表销毁
  12. 顺序表打印
  13. 顺序表动态扩容
  14. 尾插
  15. 头插
  16. 尾删
  17. 头删
  18. 查找
  19. 指定位置之前插入
  20. 删除 pos 位置的数据
  21. 竞赛中的静态顺序表
  22. 静态申请数组
  23. 单个顺序表
  24. 多个顺序表
  25. 封装静态顺序表
  26. 动态顺序表 -- vector
  27. 创建 vector
  28. size / empty
  29. begin / end
  30. pushback / popback
  31. front / back
  32. resize
  33. clear
  34. insert / erase
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • ROS1 机器人 SLAM 详解:Gmapping 算法原理与实战
  • AI 提示词工程:原理、策略与精通指南
  • Windows 下创建并激活 Python 虚拟环境 venv
  • C 语言指针与数组的深层关系及实战
  • 前端表单验证策略与最佳实践
  • Flutter 三方库 flutter_cors 应对鸿蒙 Web 与混合开发中的跨域挑战
  • Python 基础语法入门(一)
  • AI Skills 核心概念与实战搭建指南
  • Git 如何将特定提交合并到另一个分支?
  • C++伸展树介绍以及红黑树的实现
  • 内网渗透基础
  • 各高校学位论文 AIGC 检测率要求汇总及应对指南
  • 基于 FPGA 的海德汉 1313 Endat 绝对值编码器 PG 卡源码解析
  • 基于 DevUI 与 MateChat 构建企业级 AI 智能助手
  • Adaptive RAG 系统搭建:从向量检索到 Streamlit 前端全流程
  • Linux 内网离线安装 Docker 与 docker-compose 完整指南
  • 基于无人机搭载摄像头网络的交互式监控分布式方法
  • Llama 开源家族演进:从 Llama-1 到 Llama-3 技术详解
  • 深度学习模型优化策略与实战调参
  • Android Framework 源码开发揭秘:系统启动与核心组件深度解析

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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