2.6.堆排序——从堆结构到 Top-K,一套思路贯穿排序与选择

2.6.堆排序------从堆结构到 Top-K,一套思路贯穿排序与选择

系列 :搜索与排序 | 第 6 篇,共 16 篇
难度 :⭐⭐⭐☆☆ 中等
标签排序 堆排序 优先队列 Top-K


上一篇2.5.归并排序------分而治之再合而并,为什么它能稳定保持 O(n log n)?
下一篇2.7.希尔排序------让插入排序先大步走,再小步收尾


前言

快速排序平均最快,归并排序最稳,那有没有一种排序,能把"排序 "和"选择"两件事统一在同一套结构里?

有,这就是堆排序

堆排序背后依赖的是堆结构,而这个结构本身可以不止用来排序:

  • 你想排一个数组 → 用堆排序
  • 你想拿到第 k 大 / 第 k 小 → 用堆维护 Top-K
  • 你想在动态数据流里随时获取最值 → 用优先队列

只要你真的把堆排序讲透,后面的很多"堆题"都会顺下来。

这篇重点讲四件事:

  • 堆排序到底在维护什么结构
  • 为什么建堆是 O(n),不是很多人以为的 O(n log n)
  • 为什么它在最好、平均、最坏情况下都能保持 O(n log n)
  • 它和快排、归并该怎么取舍,以及它在"选择问题"里的独特价值

一、算法思想:先建大顶堆,再反复取最大值

堆排序依赖的底层结构是二叉堆 ,最常用的是大顶堆

1)什么是大顶堆?

大顶堆满足:

每个节点的值,都不小于它的左孩子和右孩子。

因此:

  • 堆顶一定是整个堆里的最大值

如果用数组来存完全二叉树,下标关系是:

  • 父节点:(i - 1) / 2 (整除)
  • 左孩子:2 * i + 1
  • 右孩子:2 * i + 2

2)堆排序分成两个阶段

阶段 A:建堆

建堆不是"随便调一调",而是要把整个数组原地调整成一个大顶堆

建好后:

  • 最大值会出现在下标 0
  • 整个区间 [0, n - 1] 都满足大顶堆性质

标准建堆做法是:

  • 默认下标从 0 开始,n 个点
  • 从最后一个非叶子节点开始,位置是 n / 2 - 1
  • 按下标从后往前,依次对每个节点做一次"下沉"
  • 一直处理到下标 0

为什么要从最后一个非叶子节点开始?

因为:

  • 叶子节点本身可以视为一个元素的堆,自身就是有序的,不需要调整
  • 非叶子节点作为根的树才可能违反"父节点 >= 子节点"的规则
  • 从下往上调,能保证你处理当前节点时,它的左右子树已经先变成大顶堆了

这一步可以理解成:

先把每棵小子树调成局部正确,再一路向上合并成整棵大顶堆。

阶段 B:排序

反复执行下面的操作:

  1. 把堆顶最大值和当前末尾元素交换
  2. 这样最大值就被放到最终位置上了
  3. 堆的有效范围缩小 1
  4. 对新的堆顶继续执行"下沉",恢复剩余区间的大顶堆性质

重复直到堆大小变成 1,排序完成。

3)"下沉"到底怎么操作?

下沉不是只比一次大小,而是一个一路往下找位置的过程。

假设当前要处理下标 i,操作顺序是:

  1. 找到它的左孩子 2 * i + 1 和右孩子 2 * i + 2
  2. 在"当前节点 + 左右孩子"里找出最大的那个
  3. 如果最大值就是当前节点,说明这一层已经合法,直接停止
  4. 如果最大值在孩子那里,就交换
  5. 交换后,节点跑到了更深一层,要继续往下检查,直到不能再下沉为止

你可以把它想成:

一个不够大的父节点,被更大的孩子一路往下"挤",直到落到自己该待的位置。
📌 核心不变量 :每轮开始时,当前有效区间 [0, heapSize - 1] 始终保持为大顶堆;而区间右侧则已经是排好序的结果区。


二、完整图解过程

以数组 [5, 3, 8, 1, 2] 为例。

第 1 步:把无序数组建成大顶堆

原数组:

text 复制代码
[5, 3, 8, 1, 2]

先确定建堆从哪里开始。

  • 数组长度 n = 5
  • 最后一个非叶子节点下标是 n / 2 - 1 = 1
  • 所以建堆顺序是:从i = 1 开始,循环到 i = 0

初始树的结构:

text 复制代码
[8, 3, 5, 1, 2]
      
       5
      / \
     3   8
    / \
   1   2
先处理 i = 1

下标 1 上的数是 3,它的两个孩子分别是:

  • 左孩子下标 3,值为 1
  • 右孩子下标 4,值为 2
text 复制代码
    3
   / \
  1   2

因为 3 已经不小于两个孩子,所以这里不用动

text 复制代码
[5, 3, 8, 1, 2]
再处理 i = 0

下标 0 上的数是 5,它的两个孩子分别是:

  • 左孩子 3
  • 右孩子 8
text 复制代码
    5
   / \
  3   8

这时最大的其实是右孩子 8,所以要交换:

text 复制代码
[5, 3, 8, 1, 2] -> [8, 3, 5, 1, 2]

    5                 8
   / \       →       / \
  3   8             3   5

交换后,原来的 5 来到下标 2

  • 下标 2 已经没有孩子了
  • 所以下沉到这里就结束

最终建堆结果:

text 复制代码
[8, 3, 5, 1, 2]
      
       8
      / \
     3   5
    / \
   1   2

这时堆顶 8 就是全局最大值。

这一步的本质不是"直接把最大值找出来",而是通过从下到上的局部调整,让整个数组满足大顶堆结构。


第 2 步:把最大值放到末尾

交换堆顶和最后一个元素:

text 复制代码
[8, 3, 5, 1, 2] -> [2, 3, 5, 1, 8]

注意:

  • 末尾的 8 已经到最终位置
  • 后面的 8 不再属于堆区间
  • 当前真正要维护的堆,只剩前面的 [2, 3, 5, 1]
text 复制代码
       2
      / \
     3   5
    /
   1 

这时问题出在堆顶:2 太小了,不符合大顶堆规则,所以要开始下沉。

这一次下沉具体怎么走?

当前堆顶是下标 0,值为 2

  • 左孩子是 3
  • 右孩子是 5
  • 两个孩子里更大的是 5

所以先交换 25

text 复制代码
[2, 3, 5, 1, | 8] -> [5, 3, 2, 1, | 8]

    2                 5
   / \       →       / \
  3   5             3   2

交换后,2 来到了下标 2

  • 下标 2 在当前堆范围里已经没有孩子
  • 所以这一轮下沉结束

现在前半段重新恢复成大顶堆,右侧 8 保持已排序状态。

用树的视角再看一眼这次下沉

交换堆顶和末尾后,当前有效堆区间其实是 [2, 3, 5, 1],它对应的树是:

text 复制代码
    2
   / \
  3   5
 /
1

这时根节点 2 明显太小,不可能继续当堆顶。

下沉时只做一件事:

  • 在当前节点和左右孩子里找最大值
  • 发现 5 最大,就让 5 上来,2 下去

于是会变成:

text 复制代码
    5
   / \
  3   2
 /
1

因为 2 在当前堆范围里已经没有孩子了,所以这次下沉到这里停止。

下沉不是把整个堆重新排一遍,而是只修复一条从上往下的路径。


第 3 步:继续重复

再把新的堆顶 5 和末尾有效位置交换:

text 复制代码
[5, 3, 2, 1, 8] -> [1, 3, 2, 5, 8]

重新下沉:

text 复制代码
[1, 3, 2, | 5, 8] -> [3, 1, 2, | 5, 8]

再继续:

text 复制代码
[3, 1, 2, 5, 8] -> [2, 1, 3, 5, 8]

最后得到:

text 复制代码
[1, 2, 3, 5, 8]

排序完成。✅


整体过程汇总

阶段 操作 结果
建堆 [5, 3, 8, 1, 2] 调整为大顶堆 [8, 3, 5, 1, 2]
第 1 轮 交换堆顶与末尾,再下沉 [5, 3, 2, 1, 8]
第 2 轮 交换堆顶与倒数第 2 位,再下沉 [3, 1, 2, 5, 8]
第 3 轮 继续缩小堆区间 [2, 1, 3, 5, 8]
完成 最后剩 1 个元素 [1, 2, 3, 5, 8]

三、代码实现

Python 版本

python 复制代码
def heapify(nums, heap_size, i):
    # 先假设当前节点最大
    largest = i
    left = 2 * i + 1
    right = 2 * i + 2

    # 和左孩子比较
    if left < heap_size and nums[left] > nums[largest]:
        largest = left

    # 和右孩子比较
    if right < heap_size and nums[right] > nums[largest]:
        largest = right

    # 如果最大值不是当前节点,就交换并继续下沉
    if largest != i:
        nums[i], nums[largest] = nums[largest], nums[i]
        heapify(nums, heap_size, largest)


def heap_sort(nums):
    n = len(nums)

    # 阶段 A:从最后一个非叶子节点开始,自底向上建堆
    for i in range(n // 2 - 1, -1, -1):
        heapify(nums, n, i)

    # 阶段 B:不断把堆顶最大值放到末尾
    for end in range(n - 1, 0, -1):
        nums[0], nums[end] = nums[end], nums[0]
        # 末尾已经有序,因此新的堆大小是 end
        heapify(nums, end, 0)

    return nums


nums = [5, 3, 8, 1, 2]
print(heap_sort(nums))  # [1, 2, 3, 5, 8]

C++ 版本

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

void heapify(vector<int>& nums, int heapSize, int i) {
    int largest = i;                 // 先假设当前节点最大
    int left = 2 * i + 1;
    int right = 2 * i + 2;

    // 找到当前节点、左孩子、右孩子三者中的最大值
    if (left < heapSize && nums[left] > nums[largest]) {
        largest = left;
    }
    if (right < heapSize && nums[right] > nums[largest]) {
        largest = right;
    }

    // 如果最大值不是当前节点,说明当前节点要下沉
    if (largest != i) {
        swap(nums[i], nums[largest]);
        heapify(nums, heapSize, largest);  // 继续向下恢复堆性质
    }
}

void heapSort(vector<int>& nums) {
    int n = (int)nums.size();

    // 阶段 A:从最后一个非叶子节点开始,自底向上建成大顶堆
    for (int i = n / 2 - 1; i >= 0; i--) {
        heapify(nums, n, i);
    }

    // 阶段 B:每次把堆顶最大值交换到末尾
    for (int end = n - 1; end > 0; end--) {
        swap(nums[0], nums[end]);
        // end 之后的位置已经有序,不再属于堆
        heapify(nums, end, 0);
    }
}

int main() {
    vector<int> nums = {5, 3, 8, 1, 2};
    heapSort(nums);

    for (int x : nums) {
        cout << x << " ";
    }
    return 0;
}

这段代码,重点在这三行:

  • for (int i = n / 2 - 1; i >= 0; i--):这就是阶段 A 建堆,从最后一个非叶子节点开始往上遍历调整
  • swap(nums[0], nums[end]):这一步是在把当前最大值放到最终位置
  • heapify(nums, end, 0):这一步是在对新的堆顶做下沉修复,让剩余部分继续保持大顶堆

如果这三行你都能对应上正文里的"建堆 → 交换 → 下沉",那堆排序就已经真的明白了。

补充:递归版 heapify 和循环版 heapify 怎么对应?

上面正文里的代码是递归版,它的优点是短、好理解。

但很多人在写代码时习惯写成循环版,因为它更贴近"元素一路往下沉"的过程。

C++ 循环版 heapify

cpp 复制代码
void heapifyIter(vector<int>& nums, int heapSize, int i) {
    while (true) {
        int largest = i;
        int left = 2 * i + 1;
        int right = 2 * i + 2;

        if (left < heapSize && nums[left] > nums[largest]) {
            largest = left;
        }
        if (right < heapSize && nums[right] > nums[largest]) {
            largest = right;
        }

        if (largest == i) {
            break;
        }

        swap(nums[i], nums[largest]);
        i = largest;
    }
}

它和递归版本质上是同一件事

  • 递归版:交换后,把"继续修下一层"的任务交给下一次函数调用
  • 循环版:交换后,直接把 i 改成新的位置,然后继续 while
  • 两者的停止条件完全一样:当前节点已经不小于左右孩子

你只要记住:

递归版和循环版只是"写法不同",不是"思路不同"。它们都在做同一件事:把一个偏小的节点一路下沉到正确位置。


四、复杂度分析

指标 复杂度 原因
建堆 O(n) 不是每个节点都要下沉到最底层
排序阶段 O(n log n) n-1 次"交换堆顶 + 下沉"
总时间复杂度 O(n log n) 最好 / 平均 / 最坏都稳定
空间复杂度 O(1) 原地排序,不需要额外数组
稳定性 ❌ 不稳定 交换堆顶与尾元素会打乱相等元素顺序

堆排序的优点很鲜明:

  • 时间上界稳
  • 空间开销小

它的短板也很鲜明:

  • 不稳定
  • 常数通常不如快排讨喜
  • 局部性也不如快排友好

五、为什么建堆是 O(n),不是 O(n log n)?

这是堆排序最容易被误解的地方。

很多人会下意识地想:

  • 一共有 n 个节点
  • 每个节点堆化一次像是 O(log n)
  • 所以建堆应该是 O(n log n)

但这个想法忽略了一点:

不是每个节点都会下沉 log n 层。

事实上:

  • 靠近底层的节点很多,但它们能下沉的层数很少
  • 能下沉很多层的节点很少,主要集中在上层

于是所有节点下沉代价加起来,整体是:

text 复制代码
O(n)

也可以用直觉理解:

  • 叶子节点根本不用调
  • 倒数第二层最多下沉 1 层
  • 倒数第三层最多下沉 2 层
  • 真正可能下沉很多层的,只有极少数靠近根的节点

所以从最后一个非叶子节点开始,自底向上建堆,比"逐个插入建堆"更快。

这也是标准堆排序一定采用原地建堆的原因。


六、堆排序与 Top-K、优先队列的关系

堆排序虽然本身是"把所有元素排完",但它更重要的意义,是让你理解这类题:

  • 实时取最大 / 最小值
  • 维护前 k 大 / 前 k
  • 优先队列调度
  • 数据流问题

1)Top-K

如果你只想要前 k 大元素,其实不必完整排序。

常见做法是:

  • 维护一个大小为 k 的小顶堆 (对,你没看错,是小顶堆)
  • 新元素比堆顶大就替换
  • 最终堆里就是前 k

这里用到小顶堆是因为,咱们维护前K大,如果当前已经有了K个元素,每次有新元素符合条件,那么舍弃的就是最小的那个元素,正好对应小顶堆的堆顶。

2)优先队列

很多题本质上都在做:

  • 谁最小 / 谁最大,谁先出队

这就是堆最擅长的事。

所以"堆排序"这一章,真正带走的应该不只是一个排序模板,而是:

对堆这种数据结构的动态维护能力有感觉。


七、与快速排序、归并排序对比

对比项 堆排序 快速排序 归并排序
平均时间复杂度 O(n log n) O(n log n) O(n log n)
最坏时间复杂度 O(n log n) O(n²) O(n log n)
空间复杂度 O(1) O(log n) O(n)
稳定性 不稳定 不稳定 稳定
工程常用度 中等 很高 很高
典型优势 原地且上界稳 平均快、常数小 稳定、适合链表
典型短板 常数与局部性一般 会退化 额外空间大

一句话理解:

  • 堆排序:想要"空间省 + 最坏也稳",它很合适
  • 快速排序:想要"平均快 + 工程常用",它通常更香
  • 归并排序:想要"稳定 + 上界稳",它更放心

八、OJ 例题讲解

例题 1:LeetCode 215 --- 数组中的第 K 个最大元素(堆的经典入门题)

题目来源 :LeetCode,题号 215 难度:⭐⭐⭐☆☆ 中等

题目链接https://leetcode.cn/problems/kth-largest-element-in-an-array/

题目描述

给定整数数组 nums 和整数 k,返回数组中第 k 个最大的元素。

为什么这题适合放在堆排序章节?

因为它几乎是"堆思想最直接的应用题":

  • 要么建大顶堆弹 k
  • 要么维护大小为 k 的小顶堆

两种思路都和堆高度相关。

C++ 解法(小顶堆)

cpp 复制代码
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        priority_queue<int, vector<int>, greater<int>> pq;

        for (int x : nums) {
            if ((int)pq.size() < k) {
                pq.push(x);
            } else if (x > pq.top()) {
                pq.pop();
                pq.push(x);
            }
        }

        return pq.top();
    }
};

例题 2:LeetCode 347 --- 前 K 个高频元素(堆维护前 k 名)

题目来源 :LeetCode,题号 347 难度:⭐⭐⭐☆☆ 中等

题目链接https://leetcode.cn/problems/top-k-frequent-elements/

题目描述

给你一个整数数组 nums 和一个整数 k,返回其中出现频率前 k 高的元素。

为什么它和堆强相关?

因为这里比较的对象不再是数值大小,而是:

  • 频率

但维护"前 k 个最优元素"的动作没变,依然适合用堆。

思路是:

  1. 先统计每个数的出现次数
  2. 再维护一个大小为 k 的小顶堆
  3. 堆里按"频率"排序,而不是按"元素值"排序

这题很适合理解:

堆不是只能处理"最大值"问题,它处理的是"优先级"问题。

解题思路

先用哈希表统计每个元素出现了多少次。

比如:

text 复制代码
nums = [1,1,1,2,2,3], k = 2

统计后会得到:

text 复制代码
1 -> 3 次
2 -> 2 次
3 -> 1 次

这时目标就变成了:

  • 从这些"(元素, 频率)"对里
  • 找出频率最高的前 k

做法仍然是经典的"固定大小小顶堆":

  • 堆里最多放 k 个元素
  • 堆顶放当前这 k 个候选里频率最小的那个
  • 如果新元素频率更高,就把堆顶挤掉

这样扫完整个哈希表后,堆里剩下的就是前 k 个高频元素。

C++ 解法(哈希表 + 小顶堆)

cpp 复制代码
class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int, int> freq;
        for (int x : nums) {
            freq[x]++;
        }

        using PII = pair<int, int>;  // {频率, 元素值}
        auto cmp = [](const PII& a, const PII& b) {
            return a.first > b.first;
        };
        priority_queue<PII, vector<PII>, decltype(cmp)> pq(cmp);

        for (auto& [num, count] : freq) {
            if ((int)pq.size() < k) {
                pq.push({count, num});
            } else if (count > pq.top().first) {
                pq.pop();
                pq.push({count, num});
            }
        }

        vector<int> ans;
        while (!pq.empty()) {
            ans.push_back(pq.top().second);
            pq.pop();
        }
        return ans;
    }
};

代码讲解

  • unordered_map<int, int> freq:先统计每个元素出现频率
  • 堆中保存的是 {频率, 元素值},这样比较时就能按频率决定优先级
  • cmp 把堆写成小顶堆,保证堆顶始终是当前前 k 名里最弱的那个
  • count > pq.top().first 时,说明新元素比堆顶更值得留下,就弹出堆顶再加入新元素
  • 最后堆里剩下的所有元素,就是答案集合;这题不要求返回顺序固定,所以直接弹出即可

复杂度分析

  • 统计频率:O(n)
  • 维护大小为 k 的堆:设不同元素个数为 m,复杂度是 O(m log k)
  • 总体复杂度通常写作:O(n log k)
  • 额外空间:O(m + k)

这题适合拿来练什么?

  • 练"小顶堆维护前 k 名"这个核心套路
  • 练把堆的比较对象从"值"切换成"频率"
  • 练哈希表统计 + 堆筛选的组合题型

例题 3:LeetCode 703 --- 数据流中的第 K 大元素(动态维护堆)

题目来源 :LeetCode,题号 703 难度:⭐⭐☆☆☆ 简单

题目链接https://leetcode.cn/problems/kth-largest-element-in-a-stream/

题目描述

设计一个类,支持不断插入新元素,并在每次插入后返回当前数据流中的第 k 大元素。

为什么这题值得放在这里?

因为它进一步说明:

  • 排序只是堆的一种静态应用
  • 真正强的是"动态维护前 k 大"

这正是优先队列最擅长的事情。

解题思路

这题和 LeetCode 215 的核心套路几乎一样,只不过:

  • 215 是一次性给完整数组
  • 703 是数据一个一个到来,要边加边维护答案

所以我们仍然维护一个大小为 k 的小顶堆

  • 堆里始终保存当前看到的k 大元素
  • 堆顶就是这 k 个数里最小的那个
  • 也就正好是"当前第 k 大元素"

每来一个新数 val,只做三件事:

  1. 如果堆里还没满 k 个,直接加入
  2. 如果堆满了,但 val 比堆顶大,就弹出堆顶、加入 val
  3. 如果 val 还不如堆顶大,说明它进不了前 k,直接忽略

这样每次 add() 结束后:

堆顶就是当前数据流里的第 k 大元素。

C++ 解法(固定大小小顶堆)

cpp 复制代码
class KthLargest {
private:
    int k;
    priority_queue<int, vector<int>, greater<int>> pq;

public:
    KthLargest(int k, vector<int>& nums) {
        this->k = k;
        for (int x : nums) {
            add(x);
        }
    }

    int add(int val) {
        if ((int)pq.size() < k) {
            pq.push(val);
        } else if (val > pq.top()) {
            pq.pop();
            pq.push(val);
        }
        return pq.top();
    }
};

代码讲解

  • pq 是一个小顶堆,大小始终不超过 k
  • 构造函数里把初始数组里的元素逐个喂给 add(),这样初始化逻辑和后续插入逻辑完全统一
  • pq.size() < k 时,说明前 k 大还没凑齐,先无脑加入
  • val > pq.top() 时,说明新元素足够大,可以挤进当前前 k 大集合
  • return pq.top() 这句非常关键:因为小顶堆堆顶恰好就是当前第 k

复杂度分析

  • 初始化:若初始数组长度为 n,总复杂度为 O(n log k)
  • 单次 add(val)O(log k)
  • 额外空间:O(k)

这题适合拿来练什么?

  • 练把静态 Top-K 思路迁移到数据流场景
  • 练"堆里保存前 k 大,堆顶就是第 k 大"这个关键认知
  • 练优先队列在类设计题里的落地写法

九、适用场景

场景 是否适合 原因
想要最坏也稳定的 O(n log n) 排序 不会像快排那样退化
内存紧张,希望原地排序 额外空间很小
Top-K / 优先队列类问题 堆思想天然契合
需要稳定排序 堆排序不稳定
特别追求平均常数性能 ⚠️ 往往不如快排讨喜

十、常见错误总结

错误 原因 正确做法
把建堆写成逐个插入 能做,但不是标准高效写法 优先使用自底向上建堆
下沉后没继续递归/循环 只修了一层,堆性质可能仍然被破坏 要一直下沉到合适位置
交换后没缩小堆区间 已经排好的尾部又被当成堆处理 heapSize 每轮减 1
误以为堆排序稳定 堆顶交换会打乱顺序 明确记住它不稳定
左右孩子下标写错 容易越界或逻辑错误 数组堆牢记 2*i+12*i+2

总结

要点 内容
核心思想 先建大顶堆,再反复把堆顶放到末尾
总时间复杂度 O(n log n)
建堆复杂度 O(n)
空间复杂度 O(1)
稳定性 ❌ 不稳定
关键优势 原地、最坏也稳、适合 Top-K
主要短板 常数与缓存友好性一般

一句话记住它:

堆排序本质上是在不停地"取当前最大值",只是它把这个动作做成了原地、批量、系统化的流程。


上一篇2.5.归并排序------分而治之再合而并,为什么它能稳定保持 O(n log n)?
下一篇2.7.希尔排序------让插入排序先大步走,再小步收尾


💬 看完有收获的话,点个赞再走~ 有问题欢迎评论区讨论 🙏

相关推荐
雪可问春风2 小时前
insightface进行视频中人脸识别
c++·音视频
木斯佳2 小时前
前端八股文面经大全:腾讯PCG暑期前端一面(2026-04-01)·面经深度解析
前端·算法·面经·计算机原理
小樱花的樱花2 小时前
C++权限对继承的影响
开发语言·c++
Q741_1472 小时前
每日一题 力扣 3661. 可以被机器人摧毁的最大墙壁数目 双指针 动态规划 C++ 题解
c++·算法·leetcode·机器人·动态规划
alphaTao2 小时前
LeetCode 每日一题 2026/3/30-2026/4/5
算法·leetcode·职场和发展
一定要AK8 小时前
刷题时的学习笔记
c++·笔记·学习
workflower12 小时前
用硬件换时间”与“用算法降成本”之间的博弈
人工智能·算法·安全·集成测试·无人机·ai编程
小樱花的樱花12 小时前
C++ new和delete用法详解
linux·开发语言·c++