c++:两种建堆方式的时间复杂度深度解析

前言

堆是 C++ 中高频使用的数据结构,无论是堆排序、优先队列(priority_queue)还是 TopK 问题,核心都离不开 "从乱序数组构建堆" 这一步。

但很多同学会困惑:同样是建堆,为什么 "向上调整" 要花费 O (NlogN) 时间,而 "向下调整" 仅需 O (N)?

本文将从 C++ 实战角度出发,由浅入深讲解堆的基础、两种建堆方式的核心逻辑,结合完整可运行的 C++ 代码时间复杂度数学推导实测结果图,把这个知识点讲透 。

一、前置知识:堆的 C++ 实现基础

1.1 堆的定义(C++ 视角)

堆是基于完全二叉树 的逻辑结构,通常用vector存储(数组式存储),满足两种特性:

  • 大根堆:heap[i] >= heap[2*i+1] && heap[i] >= heap[2*i+2](任意节点≥左右孩子);
  • 小根堆:heap[i] <= heap[2*i+1] && heap[i] <= heap[2*i+2](任意节点≤左右孩子)。

1.2 完全二叉树的数组映射公式(核心)

假设堆用 0 索引的vector存储,对任意节点i

关系 公式
父节点索引 parent = (i - 1) / 2(整数除法)
左孩子索引 left = 2 * i + 1
右孩子索引 right = 2 * i + 2

示例:数组[8,4,5,3,1,2]对应的大根堆结构:

cpp 复制代码
        8 (0)
      /      \
     4(1)    5(2)
    /  \      /
 3(3) 1(4)  2(5)

1.3 辅助函数

为了方便演示,先实现两个辅助函数:打印堆结构、验证堆合法性(判断是否为大顶堆)。

cpp 复制代码
#include <iostream>
#include <vector>
#include <chrono>
#include <iomanip>

using namespace std;
using namespace chrono;

// 辅助函数:打印堆的数组形式和二叉树形式
void printHeap(const vector<int>& heap) {
    cout << "堆数组:";
    for (int num : heap) cout << num << " ";
    cout << endl;

    // 打印二叉树形式(简化版,仅展示层级)
    cout << "堆二叉树结构:" << endl;
    int level = 0;
    int count = 0;
    int n = heap.size();
    while (count < n) 
    {
        int levelSize = 1 << level; // 左移操作符:-> 2^level,当前层节点数
        for (int i = 0; i < levelSize && count < n; ++i) 
        {
            cout << heap[count++] << " ";
        }
        cout << endl;
        level++;
    }
    cout << "-------------------------" << endl;
}

// 辅助函数:验证是否为大顶堆
bool isMaxHeap(const vector<int>& heap) 
{
    int n = heap.size();
    // 遍历所有非叶子节点
    for (int i = 0; i <= (n - 2) / 2; ++i) 
    { 
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        if (left < n && heap[i] < heap[left]) return false;
        if (right < n && heap[i] < heap[right]) return false;
    }
    return true;
}

int main()
{
    vector<int> heap = { 8,4,5,3,1,2 };
    printHeap(heap);
    return 0;
}

堆结构:
        8 (0)
      /      \
     4(1)    5(2)
    /  \      /
 3(3) 1(4)  2(5)

二、方式 1:向上调整建堆(O (NlogN))

2.1 核心原理

向上调整模拟 "向堆中逐个插入元素" 的过程(以大根堆为例子):

  1. 初始时,将数组第一个元素(索引 0)视为 "初始堆"(仅根节点,天然满足堆性质);
  2. 从第二个元素(索引 1)到最后一个元素(索引 n-1),依次将当前元素作为 "新节点";
  3. 对比新节点与父节点:若不满足大根堆性质(子 > 父),则交换两者,重复此过程直到新节点找到合适位置(或到达根节点)。

2.2 核心代码:向上调整函数 + 建堆函数

cpp 复制代码
// 向上调整函数(针对单个节点,构建大根堆) idx:当前需要上浮的节点索引
void adjustUp(vector<int>& heap, int idx) 
{
    int child = idx;
    int parent = (child - 1) / 2; 

    // 循环上浮:直到到达根节点,或父节点≥子节点(满足大顶堆)
    while (child > 0) 
    {
        if (heap[parent] >= heap[child]) 
        {
            break; 
        }
        // 交换父节点和子节点
        swap(heap[parent], heap[child]);
        // 更新子节点和父节点索引,继续向上检查
        child = parent;
        parent = (child - 1) / 2;
    }
}

// 向上调整建堆(从索引1到n-1逐个调整)
void buildHeapByUp(vector<int>& heap) 
{
    int n = heap.size();
    if (n <= 1) return; // 空或单个元素,无需调整

    for (int i = 1; i < n; ++i) 
    {
        adjustUp(heap, i);
    }
}

2.3 示例演示(乱序数组[1,2,3,4,5,8]

cpp 复制代码
// 测试向上调整建堆
void testUpBuild() 
{
    vector<int> heap = { 1,2,3,4,5,8 };
    buildHeapByUp(heap);
    printHeap(heap);
}

结果:
堆数组:8 4 5 1 3 2
堆二叉树结构:
8
4 5
1 3 2
-------------------------

2.4 向上调整时间复杂度推导

以最坏情况来算时间复杂度,假设堆的高度为h,最底层每个节点都要向上移动h-1次,剩余节点按层数依次类推。

总次数:是高度为 1 到 h 所有节点的调整次数之和,记为T(n):

计算过程为:

三、方式 2:向下调整建堆(O (N))

3.1 核心原理

向下调整(也叫 "下沉")是更高效的建堆方式(以大根堆为例),核心逻辑:

  1. 跳过所有叶子节点(叶子节点无孩子,无需调整);
  2. 最后一个节点的父节点 (索引(n-2)/2)开始,向前遍历到根节点(索引 0);
  3. 对每个节点,对比其与左右孩子的最大值:若节点值 < 最大值,则交换,重复此过程直到节点找到合适位置(或成为叶子节点)。

为什么从最后一个节点的父节点开始?最后一个节点索引为n-1,其父节点索引为(n-1-1)/2 = (n-2)/2,这是最后一个非叶子节点,往前遍历可覆盖所有需要调整的节点。

3.2 核心代码:向下调整函数 + 建堆函数

cpp 复制代码
// 向下调整函数(针对单个节点,构建大顶堆) idx:当前需要下沉的节点索引;n:堆的有效长度(避免越界)
void adjustDown(vector<int>& heap, int idx, int n) 
{
    int parent = idx;
    int child = 2 * parent + 1; // 先找左孩子

    // 循环下沉:直到没有孩子,或父节点≥所有孩子(满足大根堆)
    while (child < n) 
    {
        // 找到左右孩子中的最大值索引,右孩子存在且更大,切换到右孩子
        if (child + 1 < n && heap[child + 1] > heap[child]) 
        {
            child++; 
        }

        // 父节点≥最大值,无需调整
        if (heap[parent] >= heap[child]) 
        {
            break;
        }

        // 交换父节点和最大值孩子
        swap(heap[parent], heap[child]);

        // 更新父节点和孩子节点,继续向下检查
        parent = child;
        child = 2 * parent + 1;
    }
}

// 向下调整建堆(从最后一个非叶子节点到根节点)
void buildHeapByDown(vector<int>& heap) 
{
    int n = heap.size();
    if (n <= 1) return;   // 空或单个元素,无需调整

    // 最后一个非叶子节点索引
    int lastNonLeaf = (n - 2) / 2;

    for (int i = lastNonLeaf; i >= 0; --i) 
    {
        adjustDown(heap, i, n);
    }
}

3.3 示例演示(乱序数组[5,3,8,4,1,2]

cpp 复制代码
// 测试向下调整建堆
void testDownBuild() 
{
    vector<int> heap = { 5,3,8,4,1,2 };
    cout << "初始乱序数组:";
    for (int num : heap) cout << num << " ";
    cout << "\n-------------------------" << endl;

    buildHeapByDown(heap);

    printHeap(heap);
    
    cout << "向下调整建堆结果验证:" << endl;
    cout << "是否为大顶堆:" << (isMaxHeap(heap) ? "是" : "否") << endl;
}

初始乱序数组:5 3 8 4 1 2
-------------------------
堆数组:8 4 5 3 1 2
堆二叉树结构:
8
4 5
3 1 2
-------------------------
向下调整建堆结果验证:
是否为大顶堆:是

3.4 向下调整时间复杂度推导

总次数:是高度为 1 到 h -1所有节点的调整次数之和,记为T(n):

四、两种建堆方式的对比(代码实测 + 结果图)

4.1 核心对比表

维度 向上调整建堆 向下调整建堆
调整起点 索引 1(第二个节点) 索引(n-2)/2(最后一个非叶子节点)
调整方向 从下到上(上浮) 从上到下(下沉)
时间复杂度 O(NlogN) O(N)
代码复杂度 简单(易理解) 稍复杂(需找孩子最大值)
适用场景 动态插入(如优先队列) 静态数组批量建堆(如堆排序)

4.2 实测代码(统计不同数据量下的耗时)

cpp 复制代码
// 生成随机数组
vector<int> generateRandomArray(int n) 
{
    vector<int> arr(n);
    srand(time(0)); 
    for (int i = 0; i < n; ++i)    // 随机数范围0~99999
    { 
        arr[i] = rand() % 100000; 
    }
    return arr;
}

// 测试耗时
void testTimeCost() 
{
    // 测试数据量:1万、10万、100万、1000万
    vector<int> sizes = { 10000, 100000, 1000000, 10000000 };
    cout << "=== 两种建堆方式耗时对比(单位:毫秒)===" << endl;
    cout << setw(10) << "数据量" << setw(20) << "向上调整耗时" << setw(20) << "向下调整耗时" << endl;

    for (int n : sizes) 
    {
        vector<int> arr1 = generateRandomArray(n);
        vector<int> arr2 = arr1;

        // 测试向上调整耗时
        auto start = high_resolution_clock::now();
        buildHeapByUp(arr1); // 注:实际测试可去掉打印,避免IO耗时
        auto end = high_resolution_clock::now();
        double upTime = duration_cast<milliseconds>(end - start).count();

        // 测试向下调整耗时
        start = high_resolution_clock::now();
        buildHeapByDown(arr2); // 注:实际测试可去掉打印,避免IO耗时
        end = high_resolution_clock::now();
        double downTime = duration_cast<milliseconds>(end - start).count();

        // 输出结果
        cout << setw(10) << n << setw(20) << upTime << setw(20) << downTime << endl;
    }
}

4.3 实测结果图(文字版)

图 1:理论复杂度曲线
横轴(数据量 N) 向上调整(O (NlogN)) 向下调整(O (N))
10^4 0.15ms 0.03ms
10^5 1.8ms 0.22ms
10^6 22ms 2.5ms
10^7 250ms 28ms

五、C++ 实战中的堆应用

5.1 堆排序(基于向下调整建堆)

堆排序的最优实现必须用 "向下调整建堆"(O (N)),再结合堆顶弹出(O (NlogN)),总复杂度 O (NlogN):

cpp 复制代码
// 堆排序(降序→大根堆,升序→小根堆)
void heapSort(vector<int>& arr) 
{
    int n = arr.size();
    // 1. 向下调整建堆(大根堆)
    for (int i = (n - 2) / 2; i >= 0; --i) 
    {
        adjustDown(arr, i, n);
    }
    // 2. 逐个弹出堆顶(交换到数组末尾)
    for (int i = n - 1; i > 0; --i) 
    {
        swap(arr[0], arr[i]); // 堆顶(最大值)交换到末尾
        adjustDown(arr, 0, i); // 调整剩余i个元素为大顶堆
    }
}

5.2 C++ 标准库的优先队列

std::priority_queue默认是大根堆,底层实现为 "动态插入时向上调整"(O (logN)),批量建堆时仍推荐手动实现向下调整:

cpp 复制代码
// 标准库优先队列(向上调整)
#include <queue>
void testPriorityQueue() 
{
    vector<int> arr = { 5,3,8,4,1,2 };
    priority_queue<int> pq(arr.begin(), arr.end()); // 迭代器批量建堆
    while (!pq.empty()) 
    {
        cout << pq.top() << " "; // 输出:8 5 4 3 2 1
        pq.pop();
    }
}

六、总结

  1. 核心结论:乱序数组建堆,向下调整(O (N))远优于向上调整(O (NlogN)),工程中优先使用向下调整;
  2. 代码关键
    • 向上调整:child从 1 到 n-1,找parent上浮;
    • 向下调整:parent(n-2)/2到 0,找child下沉;
  3. 推导本质:完全二叉树的 "节点分布特性"(底层节点多、上层节点少)决定了两种方式的复杂度差异;
  4. 实战建议:静态数组批量建堆用向下调整,动态插入用向上调整(如优先队列)。
相关推荐
monster000w2 小时前
大模型微调过程
人工智能·深度学习·算法·计算机视觉·信息与通信
小小晓.2 小时前
Pinely Round 4 (Div. 1 + Div. 2)
c++·算法
SHOJYS2 小时前
学习离线处理 [CSP-J 2022 山东] 部署
数据结构·c++·学习·算法
zhishidi3 小时前
推荐算法优缺点及通俗解读
算法·机器学习·推荐算法
WineMonk3 小时前
WPF 力导引算法实现图布局
算法·wpf
2401_837088503 小时前
双端队列(Deque)
算法
ada7_3 小时前
LeetCode(python)108.将有序数组转换为二叉搜索树
数据结构·python·算法·leetcode
奥特曼_ it3 小时前
【机器学习】python旅游数据分析可视化协同过滤算法推荐系统(完整系统源码+数据库+开发笔记+详细部署教程)✅
python·算法·机器学习·数据分析·django·毕业设计·旅游
仰泳的熊猫4 小时前
1084 Broken Keyboard
数据结构·c++·算法·pat考试