惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Jina AI
Jina AI
C
Cybersecurity and Infrastructure Security Agency CISA
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
Threat Research - Cisco Blogs
L
LINUX DO - 热门话题
Simon Willison's Weblog
Simon Willison's Weblog
L
Lohrmann on Cybersecurity
S
Schneier on Security
T
The Exploit Database - CXSecurity.com
Know Your Adversary
Know Your Adversary
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Cyberwarzone
Cyberwarzone
T
Threatpost
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
Scott Helme
Scott Helme
WordPress大学
WordPress大学
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
W
WeLiveSecurity
L
LINUX DO - 最新话题
G
GRAHAM CLULEY
酷 壳 – CoolShell
酷 壳 – CoolShell
S
SegmentFault 最新的问题
Vercel News
Vercel News
Microsoft Azure Blog
Microsoft Azure Blog
有赞技术团队
有赞技术团队
Cisco Talos Blog
Cisco Talos Blog
V2EX - 技术
V2EX - 技术
Apple Machine Learning Research
Apple Machine Learning Research
H
Help Net Security
F
Fortinet All Blogs
The Hacker News
The Hacker News
IT之家
IT之家
Forbes - Security
Forbes - Security
月光博客
月光博客
S
Security @ Cisco Blogs
SecWiki News
SecWiki News
博客园 - 聂微东
GbyAI
GbyAI
S
Security Affairs
H
Heimdal Security Blog
人人都是产品经理
人人都是产品经理
大猫的无限游戏
大猫的无限游戏
AWS News Blog
AWS News Blog
T
Tenable Blog
P
Privacy International News Feed
Microsoft Security Blog
Microsoft Security Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
AI
AI

qlAD 的技术笔记

2026 年 6 月 7 日 -- 我有社交恐惧症,恐惧外面到处雷电风雨声(大学生活记录) 手把手带你实现哈希表(C/C++ 数据结构) 手把手带你实现图结构(C/C++ 数据结构) 手把手带你实现二叉树(C/C++ 数据结构) 手把手带你实现栈和队列(C/C++ 数据结构) 手把手带你实现单/双链表(C/C++ 数据结构) 2026 年 1 月 2 日 -- 我不怕冷,因为我内心炽热 【2025 年度总结】(大学生活记录) 2025 年 12 月 2 日 -- 省电模式:大一 100 天耗电日志(大学生活记录) 蓝桥杯备赛第一期 —— 枚举与模拟 2025 年 11 月 3 日 -- 用爱发电、大爱无私(大学生活记录) 使用循环嵌套输出规则图形找规律讲解(嵌套循环-图形输出) 2025 年 10 月 1 日 -- 二字开头第一年的生日记录及感悟随笔(大学生活记录) Python 初级 04 -- 目前为止 Python 中你们做题可能会遇到的问题以及七七八八 2025 年 9 月 24 日 -- 开学后的第一课及军训汇演(大学生活记录) Python 初级 03 -- 程序设计的三种结构之选择结构和循环结构 Python 初级 02 -- 基础的数据类型和输入输出 2025 年 9 月 17 日 -- 大连 2025 国际大体联足球世界杯(大学生活记录) Python 初级 01 -- 初识编程、理解计算机语言及程序运行方式 适合大一新生学习编程的前一课 -- 编程先导(编程的本质) 2025 年 9 月 12 日 -- 三天军训感受以及一些七七八八(大学生活记录) 2025 年 9 月 9 日 -- 体检、军训前的准备(大学生活记录) 适合大一新生的计算机基础知识 -- 个人认为足够版 2025 年 9 月 7 日 -- 既来之、则安之(大学生活记录) C++ 程序的内存布局 —— 代码区、全局/静态区、栈区和堆区 联想小新 pro 16 2025 开箱记录:跳过联网、office 激活、更换 win 11 专业版 R 语言中的数学函数 计算机软件著作权申请过程记录 从聚类到回归:用 Python 解析鸢尾花数据集的完整数据科学流程 条件概率、全概率与贝叶斯公式 —— 一篇文章带你死磕概率公式 高中化学物质反应规则表 —— 方程式之间也有规律可循 集合元素性质、关系、子集公式、基本运算 —— 一篇文章带你全方面死磕集合 如何为你的项目选择合适的许可证 -- 全方位指南 解释 3:1 和 9:3:3:1 的详细来源 —— 建立减数分裂与孟德尔遗传学定律之间的联系 重新设置磁盘分区以及 Btrfs 子卷(subvolume)布局的无损方案 零基础实战:用 Docker 启动 dockermailserver 绕过 25 端口封锁搭建个人邮箱服务器 基于 GitHub Actions + OSS 的自托管 APT 仓库全面指南 如何在安装 Debian 时配置 Btrfs 子卷 | 详细分步指南 一个合格的个人网站站长应该知道哪些事? CSS 基础入门教程 —— 选择器、样式声明 HTML 基础入门 —— 基本结构和语法 给博客添加一个文章数据统计的页面 Git 学习笔记(命令备忘表) Anki 牌组选项详细解释与设置 手动编译带有 KernelSU 的 OnePlus 6 内核 安卓类原生系统补全计划 —— PixelExperience(一加 OnePlus) 如何使用 use-sound 为 React 应用添加声音效果 标准化项目的 GitHub Flow 工作流 如何搭建设计自己的博客网站? 使用安知鱼主题同款的 Twikoo 评论组件 配置 GitHub Actions 实现自动化提交百度收录 第00期 | 环境搭建 & 递归 (一) | 基本数列递归 逐字符讲解 C 语言 HelloWorld 程序 2023 年回顾 🤟 Anki:记忆神器,助你高效学习 Gentoo 学习笔记 美化你的 PixelExperience OS Markdown 语法指南 搭建 OneDrive 目录索引 为什么用 65 表示大写字母 A
手把手带你实现动态数组(C/C++ 数据结构)
qlAD · 2026-02-07 · via qlAD 的技术笔记

cover

一、指针与数组

1、指针

在 C/C++ 中,指针是一个变量,它存储了另一个变量的内存地址

#include <iostream>
using namespace std;

int main() {
    int a, b, c;
    int *p1, *p2, *p3;

    p1 = &a;
    p2 = &b;
    p3 = &c;

    cout << (long long)&a << endl;  // 140733276388388
    cout << (long long)&b << endl;  // 140733276388384
    cout << (long long)&c << endl;  // 140733276388380

    cout << (long long)p1 << endl;  // 140733276388388
    cout << (long long)p2 << endl;  // 140733276388384
    cout << (long long)p3 << endl;  // 140733276388380

    return 0;
}

为什么每个地址都相差 4?因为在 64 位系统中,int 类型占用 4 个字节,所以每个变量的地址相差 4 个字节。

2、数组

数组是在内存中连续存储的一组相同类型的数据。

#include <iostream>
using namespace std;

int main() {
    int arr[3] = {1, 2, 3};

    cout << (long long)&arr[0] << endl; // 140722471371700
    cout << (long long)&arr[1] << endl; // 140722471371704
    cout << (long long)&arr[2] << endl; // 140722471371708

    cout << (long long)arr << endl; // 140722471371700

    return 0;
}

由于是连续的,所以每个元素的地址相差 4 个字节,并且数组名 arr 就是数组第一个元素的地址

3、指针与数组的关系

那既然这样,我们就可以通过指针来操作数组了。

#include <iostream>
using namespace std;

int main() {
    int arr[3] = {10, 20, 30};
    int *p = arr;

    // 写法1:指针偏移 + 解引用
    cout << *(p + 0) << endl;   // 10
    cout << *(p + 1) << endl;   // 20
    cout << *(p + 2) << endl;   // 30

    // 写法2:数组下标语法糖
    cout << p[0] << endl;   // 10
    cout << p[1] << endl;   // 20
    cout << p[2] << endl;   // 30

    return 0;
}

指针偏移的 “单位” 是元素类型大小,所以 p + 1 实际上是 p 的地址加上一个元素的大小(4 字节),也就是指向下一个元素的地址。

二、静态数组

静态数组的大小在编译时就已经确定了,无法动态调整。

数组的名字就是数组第一个元素的地址,其他元素之所以能用下标访问,是因为数组在内存中是连续存储的。

比如,访问 arr[1] 就相当于访问 *(arr + 1),也就是访问地址 arr 加上一个元素的大小(4 字节)的位置。

#include <iostream>
using namespace std;

int main() {
    int arr[3] = {1, 2, 3};
    cout << arr[0] << endl;

    cout << *(arr + 1) << endl;
    cout << arr[1] << endl;

    return 0;
}

这样,静态数组的随机访问的时间复杂度是 O(1),因为我们可以直接通过地址计算访问到任意元素。

三、增删改查

1、查找和修改

很简单,直接通过下标访问即可:

#include <iostream>
using namespace std;

int main() {
    int arr[3] = {1, 2, 3};

    // 查找
    cout << arr[1] << endl; // 输出 2

    // 修改
    arr[1] = 20;
    cout << arr[1] << endl; // 输出 20

    // 输出整个数组
    for (int i = 0; i < 3; i++) {
        cout << arr[i] << " "; // 输出 1 20 3
    }

    return 0;
}

查找和修改的时间复杂度都是 O(1),因为我们可以直接通过下标访问到任意元素。

2、删除

删除末尾元素:

#include <iostream>
using namespace std;

int main() {
    int arr[3] = {1, 2, 3};
    arr[2] = -1; // 设置为 -1 表示删除
}

删除末尾元素的时间复杂度是 O(1),因为我们只需要将最后一个元素设置为一个特殊值(如 -1)即可。

删除非末尾元素,涉及到元素的移动:

#include <iostream>
using namespace std;

int main() {
    // 初始化一个长度为 5 的静态数组
    int arr[5] = {1, 2, 3, 4, 5};

    // 删除 arr[2],即元素 3
    int indexToDelete = 2;

    // 移动元素
    for (int i = indexToDelete; i < 4; i++) {
        arr[i] = arr[i + 1]; // 将后面的元素向前移动
    }

    // 设置最后一个元素为 -1 表示删除
    arr[4] = -1;
}

删除非末尾元素的时间复杂度是 O(n),因为我们需要移动后面的元素来填补被删除元素的位置。

3、增加

数组未满时,直接在末尾添加:

#include <iostream>
using namespace std;

int main() {
    int arr[5] = {1,2,3};

    // 追加 4 到末尾
    arr[3] = 4;

    // 追加 5 到末尾
    arr[4] = 5;

    return 0;
}

增加元素的时间复杂度是 O(1),因为我们直接在末尾添加即可。

数组未满时,在非末尾位置插入元素:

#include <iostream>
using namespace std;

int main() {
    int arr[5] = {1, 2, 4};

    // 在索引 2 的位置插入 3
    int indexToInsert = 2;

    // 移动元素
    for (int i = 3; i > indexToInsert; i--) {
        arr[i] = arr[i - 1]; // 将后面的元素向后移动
    }

    // 插入元素
    arr[indexToInsert] = 3;

    return 0;
}

在非末尾位置插入元素的时间复杂度是 O(n),因为我们需要移动后面的元素来为新元素腾出位置。

数组已满时,这时候无法直接添加元素了,需要创建一个更大的数组来存储更多的元素,这就是动态数组的核心思想。

#include <iostream>
using namespace std;

int main() {
    int arr[5] = {1, 2, 3, 4, 5};

    // 需要创建一个更大的数组(扩容 2 倍)
    int newArr[10];

    // 将原数组的元素复制到新数组中
    for (int i = 0; i < 5; i++) {
        newArr[i] = arr[i];
    }

    // 现在我们可以在 newArr 中添加更多的元素了
    newArr[5] = 6;
    newArr[6] = 7;

    return 0;
}

当数组已满时,增加元素的时间复杂度是 O(n),因为我们需要创建一个更大的数组并将原数组的元素复制到新数组中。

四、动态数组 Vector

C++ 标准库提供了一个动态数组的实现,叫做 std::vector,它封装了动态数组的功能,提供了方便的接口来进行增删改查操作。

#include <iostream>
#include <vector>

using namespace std;

int main() {
    vector<int> vec;

    // 末尾添加元素
    vec.push_back(1);   // [1]
    vec.push_back(4);   // [1, 4]

    // 在指定位置插入元素
    vec.insert(vec.begin() + 1, 2); // [1, 2, 4]
    vec.insert(vec.begin() + 2, 3); // [1, 2, 3, 4]
    vec.insert(vec.begin(), 0); // [0, 1, 2, 3, 4]

    // 删除末尾元素
    vec.pop_back(); // [0, 1, 2, 3]

    // 删除指定位置的元素
    vec.erase(vec.begin() + 1); // [0, 2, 3]

    // 修改元素
    vec[1] = 20; // [0, 20, 3]

    // 输出整个 vector
    for (int i = 0; i < vec.size(); i++) {
        cout << vec[i] << " "; // 输出 0 20 3
    }

    // 其他工具函数 / 常见操作
    cout << vec.size() << endl; // 输出 3
    cout << vec.empty() << endl; // 输出 0(false)
    cout << find(vec.begin(), vec.end(), 20) - vec.begin(); // 查找元素 20 的索引
    clear(vec); // 清空 vector
}

vector 的时间复杂度分析:

  • 末尾添加元素(push_back)的平均时间复杂度是 O(1),因为当 vector 的容量不足时会进行扩容,扩容的时间复杂度是 O(n),但这种情况发生的频率较低,所以平均下来是 O(1)。
  • 在指定位置插入元素(insert)的时间复杂度是 O(n),因为需要移动后面的元素来为新元素腾出位置。
  • 删除末尾元素(pop_back)的时间复杂度是 O(1),因为只需要将最后一个元素移除即可。
  • 删除指定位置的元素(erase)的时间复杂度是 O(n),因为需要移动后面的元素来填补被删除元素的位置。
  • 修改和访问元素的时间复杂度是 O(1),因为可以直接通过下标访问到任意元素进行修改。
  • 查找元素索引的时间复杂度是 O(n),因为需要遍历整个 vector 来找到目标元素。
  • 清空 vector 的时间复杂度是 O(n),因为需要将所有元素销毁。

五、动态数组的实现

1、注意事项

就需要注意以下几点:

  • 当数组已满时,需要创建一个更大的数组来存储更多的元素
  • 扩容时,通常会将容量增加一倍,以减少频繁扩容
  • 删除元素时,如果当前元素个数过少(如容量的 1/4),可以考虑缩容来节省内存。
  • 需要实现增删改查等基本操作,并且要处理好边界情况(如索引越界等)。

2、完整代码

看懂以下代码需要对 C++ 的类、模板、异常处理等有一定的了解。

#include <iostream>
#include <stdexcept>
#include <algorithm>

template<typename T>
class MyArrayList {
private:
    T *data_;
    int size_;
    int capacity_;

    static constexpr int DEFAULT_CAPACITY = 1;

    // 检查索引是否为有效元素索引(0 <= index < size_)
    bool isElementIndex(int index) const noexcept {
        return index >= 0 && index < size_;
    }

    // 检查索引是否为有效插入位置(0 <= index <= size_)
    bool isPositionIndex(int index) const noexcept {
        return index >= 0 && index <= size_;
    }

    // 校验元素索引,越界则抛异常
    void checkElementIndex(int index) const {
        if (!isElementIndex(index)) {
            throw std::out_of_range(
                "Index out of bounds: " + std::to_string(index) + ", size: " + std::to_string(size_));
        }
    }

    // 校验插入位置索引,越界则抛异常
    void checkPositionIndex(int index) const {
        if (!isPositionIndex(index)) {
            throw std::out_of_range(
                "Position index out of bounds: " + std::to_string(index) + ", size: " + std::to_string(size_));
        }
    }

    // 扩容/缩容
    void resize(int newCapacity) {
        // 确保新容量不小于默认容量,且不小于当前元素个数
        newCapacity = std::max({newCapacity, size_, DEFAULT_CAPACITY});
        if (newCapacity == capacity_) {
            return;
        }

        // 创建新数组并复制元素
        T *newData = new T[newCapacity];
        for (int i = 0; i < size_; i++) {
            newData[i] = data_[i];
        }

        // 释放旧数组内存并更新
        delete[] data_;
        data_ = newData;
        capacity_ = newCapacity;
    }

public:
    // 默认构造函数
    MyArrayList() : data_(new T[DEFAULT_CAPACITY]), size_(0), capacity_(DEFAULT_CAPACITY) {
    }

    // 指定初始容量的构造函数
    explicit MyArrayList(int initialCapacity)
        : data_(new T[std::max(initialCapacity, DEFAULT_CAPACITY)]),
          size_(0),
          capacity_(std::max(initialCapacity, DEFAULT_CAPACITY)) {
    }

    // 插入元素到指定位置
    void add(int index, T value) {
        checkPositionIndex(index);
        if (size_ == capacity_) {
            resize(capacity_ * 2);
        }
        for (int i = size_ - 1; i >= index; i--) {
            data_[i + 1] = data_[i];
        }
        data_[index] = value;
        size_++;
    }

    void addFirst(T value) {
        add(0, value);
    }

    void addLast(T value) {
        add(size_, value);
    }

    // 删除指定位置元素
    T remove(int index) {
        checkElementIndex(index);
        T deletedValue = data_[index];
        for (int i = index + 1; i < size_; i++) {
            data_[i - 1] = data_[i];
        }
        size_--;
        if (0 < size_ && size_ == capacity_ / 4) {
            resize(capacity_ / 2);
        }
        return deletedValue;
    }

    T removeFirst() {
        return remove(0);
    }

    T removeLast() {
        return remove(size_ - 1);
    }

    // 修改指定位置元素
    T set(int index, T value) {
        checkElementIndex(index);
        T oldValue = data_[index];
        data_[index] = value;
        return oldValue;
    }

    // 获取指定位置元素
    T get(int index) const {
        checkElementIndex(index);
        return data_[index];
    }

    // 获取当前元素个数
    int getSize() const {
        return size_;
    }

    // 判断是否为空
    bool isEmpty() const {
        return size_ == 0;
    }

    // 析构函数,释放内存
    ~MyArrayList() {
        delete[] data_;
    }
};

int main() {
    MyArrayList<int> arr(5);
    for (int i = 0; i < 5; i++) {
        arr.addLast(i + 1);     // [1, 2, 3, 4, 5]
    }

    arr.addFirst(0);            // [0, 1, 2, 3, 4, 5]
    arr.addLast(6);             // [0, 1, 2, 3, 4, 5, 6]
    arr.remove(3);              // [0, 1, 2, 4, 5, 6]
    arr.set(3, 3);              // [0, 1, 2, 3, 5, 6]

    std::cout << arr.getSize() << std::endl; // 6

    for (int i = 0; i < arr.getSize(); i++) {
        std::cout << arr.get(i) << " "; // 输出:0 1 2 3 5 6
    }

    return 0;
}