数据结构---堆(Heap)

堆是计算机科学中核心的数据结构之一,基于完全二叉树构建,兼具高效的插入、删除和极值查询能力,广泛应用于优先队列、堆排序、TopK问题等场景。

一、堆的定义与核心性质

1.1 本质定义

堆是一种完全二叉树 (Complete Binary Tree),同时满足堆序性(Heap Property)。完全二叉树的定义是:除最后一层外,每一层的节点数均为最大值,且最后一层的节点从左到右连续排列(无空洞)。这种结构决定了堆可以用数组高效存储,无需额外指针开销。

1.2 堆序性规则

堆序性是堆与普通完全二叉树的核心区别,分为两种类型:

  • 大根堆(Max Heap) :每个父节点的值 大于等于 其左右子节点的值(parent.val ≥ left.val && parent.val ≥ right.val),堆顶(根节点)是整个堆的最大值。
  • 小根堆(Min Heap) :每个父节点的值 小于等于 其左右子节点的值(parent.val ≤ left.val && parent.val ≤ right.val),堆顶是整个堆的最小值。

1.3 与二叉搜索树(BST)的区别

堆和BST常被混淆,但核心目标完全不同:

特性 堆(大根堆/小根堆) 二叉搜索树(BST)
结构要求 完全二叉树 任意二叉树(通常平衡化)
有序性 仅父子节点满足堆序(全局无序) 左子树 < 根 < 右子树(全局有序)
核心操作效率 插入/删除堆顶 O(logn),查极值 O(1) 插入/删除/查找 O(logn)(平衡BST)
适用场景 优先队列、TopK、堆排序 动态查找、有序遍历

二、堆的存储结构

由于堆是完全二叉树,无空洞节点,因此数组是堆的最优存储方式,无需额外空间存储指针。数组与二叉树节点的映射关系如下:

假设堆的数组为 vector<T> heap,对于索引为 i 的节点(从0开始计数):

  • 父节点索引:parent = (i - 1) / 2(整数除法,自动向下取整)
  • 左子节点索引:left = 2 * i + 1
  • 右子节点索引:right = 2 * i + 2

示例 :小根堆 [2, 5, 3, 8, 7, 6] 对应的完全二叉树结构:

复制代码
      2 (i=0)
    /   \
   5(i=1) 3(i=2)
  / \     /
8(i=3)7(i=4)6(i=5)

验证映射关系:i=1 的父节点是 (1-1)/2=0i=2 的左子节点是 2*2+1=5,完全符合数组存储逻辑。

三、堆的核心操作(C++模板实现)

堆的所有功能基于两个核心操作:上浮(Sift Up)下沉(Sift Down) 。下面实现一个支持大根堆/小根堆的通用模板类 Heap<T, Compare>,其中 Compare 是比较函数对象(默认大根堆)。

3.1 模板类框架

cpp 复制代码
#include <vector>
#include <algorithm>
#include <stdexcept>

// 比较函数对象:大根堆(默认)
template <typename T>
struct MaxHeapCompare {
    bool operator()(const T& a, const T& b) const {
        return a < b; // 父节点需大于子节点,故a<b时需交换
    }
};

// 比较函数对象:小根堆
template <typename T>
struct MinHeapCompare {
    bool operator()(const T& a, const T& b) const {
        return a > b; // 父节点需小于子节点,故a>b时需交换
    }
};

// 堆模板类:T为元素类型,Compare为比较规则
template <typename T, typename Compare = MaxHeapCompare<T>>
class Heap {
private:
    std::vector<T> data; // 存储堆的数组
    Compare cmp;         // 比较函数对象

    // 上浮操作:从索引i向上调整堆
    void siftUp(int i) {
        while (i > 0) { // 未到达根节点
            int parent = (i - 1) / 2; // 父节点索引
            // 若当前节点与父节点满足堆序,无需调整
            if (!cmp(data[parent], data[i])) break;
            // 不满足堆序,交换父节点与当前节点
            std::swap(data[parent], data[i]);
            i = parent; // 继续向上调整
        }
    }

    // 下沉操作:从索引i向下调整堆
    void siftDown(int i) {
        int n = data.size();
        while (true) {
            int maxChild = i; // 初始化最大子节点为当前节点
            int left = 2 * i + 1;  // 左子节点索引
            int right = 2 * i + 2; // 右子节点索引

            // 比较左子节点与当前节点
            if (left < n && cmp(data[maxChild], data[left])) {
                maxChild = left;
            }
            // 比较右子节点与当前最大子节点
            if (right < n && cmp(data[maxChild], data[right])) {
                maxChild = right;
            }

            // 若当前节点已是最大/最小子节点,无需调整
            if (maxChild == i) break;
            // 交换当前节点与最大/最小子节点
            std::swap(data[i], data[maxChild]);
            i = maxChild; // 继续向下调整
        }
    }

public:
    // 构造函数:空堆
    Heap() = default;

    // 构造函数:从数组建堆(自底向上法)
    Heap(const std::vector<T>& arr) : data(arr) {
        buildHeap();
    }

    // 建堆操作:将无序数组转为堆
    void buildHeap() {
        int n = data.size();
        // 从最后一个非叶子节点开始,自底向上堆化
        int lastNonLeaf = (n - 2) / 2; // 最后一个节点的父节点
        for (int i = lastNonLeaf; i >= 0; --i) {
            siftDown(i);
        }
    }

    // 插入元素
    void push(const T& val) {
        data.push_back(val); // 插入到数组末尾(完全二叉树的最后一个叶子)
        siftUp(data.size() - 1); // 上浮调整堆
    }

    // 删除堆顶元素
    void pop() {
        if (empty()) {
            throw std::runtime_error("Heap is empty, cannot pop!");
        }
        int n = data.size();
        data[0] = data[n - 1]; // 堆顶替换为最后一个元素
        data.pop_back();       // 删除最后一个元素
        if (!empty()) {
            siftDown(0); // 下沉调整堆
        }
    }

    // 获取堆顶元素
    const T& top() const {
        if (empty()) {
            throw std::runtime_error("Heap is empty, cannot get top!");
        }
        return data[0];
    }

    // 判断堆是否为空
    bool empty() const {
        return data.empty();
    }

    // 获取堆的大小
    size_t size() const {
        return data.size();
    }
};

3.2 核心操作详解

3.2.1 上浮(siftUp)
  • 场景:插入新元素后,堆序可能被破坏(新元素在叶子,可能不满足与父节点的堆序)。
  • 原理:从新元素的位置(数组末尾)向上遍历,与父节点比较,若不满足堆序则交换,直到到达根节点或满足堆序。
  • 时间复杂度:O(logn)(树的高度为logn,最多交换logn次)。
3.2.2 下沉(siftDown)
  • 场景:删除堆顶后,堆序被破坏(最后一个元素移到堆顶,可能不满足与子节点的堆序)。
  • 原理:从堆顶开始,与左右子节点中"符合堆序的极值节点"(大根堆找最大子节点,小根堆找最小子节点)比较,若不满足堆序则交换,直到到达叶子节点或满足堆序。
  • 关键细节:需先判断左右子节点是否存在(避免数组越界),再选择极值子节点。
  • 时间复杂度:O(logn)(树的高度为logn)。
3.2.3 建堆(buildHeap)
  • 两种方法对比
    1. 自顶向下法:逐个将数组元素插入堆,时间复杂度O(nlogn)(每个元素插入需O(logn))。
    2. 自底向上法:从最后一个非叶子节点开始,逐个执行下沉操作,时间复杂度O(n)(核心考点)。
  • O(n)复杂度推导
    完全二叉树的第k层(根为第0层)有2^k个节点,每个节点的下沉深度最多为(树高 - k)。树高h = logn,总操作次数为:
    ∑k=0h−12k×(h−k)=O(n)\sum_{k=0}^{h-1} 2^k \times (h - k) = O(n)k=0∑h−12k×(h−k)=O(n)
  • 代码逻辑 :最后一个非叶子节点索引为(n-2)/2(最后一个节点n-1的父节点),从该节点向左遍历至根,依次执行siftDown

四、堆的经典应用

4.1 堆排序(原地排序)

堆排序是堆的核心应用,利用大根堆(升序)或小根堆(降序)实现排序,时间复杂度O(nlogn),空间复杂度O(1)(原地排序),但不稳定(相同元素可能交换位置)。

原理步骤:
  1. 对数组执行buildHeap,构建大根堆(升序排序);
  2. 交换堆顶(最大值)与数组末尾元素,此时末尾元素为有序区;
  3. 对剩余无序区(0~n-2)执行siftDown(0),重新构建大根堆;
  4. 重复步骤2~3,直到无序区为空。
C++实现:
cpp 复制代码
// 原地堆排序(升序,基于大根堆)
template <typename T>
void heapSort(std::vector<T>& arr) {
    int n = arr.size();
    if (n <= 1) return;

    // 步骤1:构建大根堆(自底向上)
    Heap<T> maxHeap(arr);
    arr = maxHeap.getData(); // 简化:直接复用堆的数组(实际可原地修改)

    // 步骤2~4:交换堆顶与末尾,调整堆
    for (int i = n - 1; i > 0; --i) {
        std::swap(arr[0], arr[i]); // 堆顶(最大值)移到末尾
        // 对剩余无序区(0~i-1)执行下沉调整
        Heap<T> tempHeap(arr.substr(0, i));
        for (int j = 0; j < i; ++j) {
            arr[j] = tempHeap.getData()[j];
        }
    }

    // 优化版:原地建堆+调整(无需额外堆对象)
    /*
    // 原地建大根堆
    auto buildMaxHeap = [&](std::vector<T>& a, int size) {
        int lastNonLeaf = (size - 2) / 2;
        for (int i = lastNonLeaf; i >= 0; --i) {
            int parent = i;
            while (true) {
                int maxChild = parent;
                int left = 2 * parent + 1;
                int right = 2 * parent + 2;
                if (left < size && a[left] > a[maxChild]) maxChild = left;
                if (right < size && a[right] > a[maxChild]) maxChild = right;
                if (maxChild == parent) break;
                std::swap(a[parent], a[maxChild]);
                parent = maxChild;
            }
        }
    };

    buildMaxHeap(arr, n);
    for (int i = n - 1; i > 0; --i) {
        std::swap(arr[0], arr[i]);
        buildMaxHeap(arr, i); // 调整剩余无序区
    }
    */
}

// 测试代码
int main() {
    std::vector<int> arr = {5, 3, 8, 4, 2, 7, 1, 6};
    heapSort(arr);
    std::cout << "堆排序结果:";
    for (int num : arr) std::cout << num << " "; // 输出:1 2 3 4 5 6 7 8
    return 0;
}

4.2 优先队列(Priority Queue)

优先队列是堆的典型应用,元素按优先级排序,每次出队的是优先级最高的元素。C++ STL提供priority_queue,底层基于堆实现。

4.2.1 STL priority_queue详解
  • 默认行为:大根堆(优先级高的元素值大)。
  • 核心接口push(val)(插入)、pop()(删除堆顶)、top()(获取堆顶)、empty()size()
  • 改为小根堆的两种方式
    1. 使用greater<T>(需包含<functional>);
    2. 自定义比较函数对象。
4.2.2 示例:任务调度(按优先级执行)
cpp 复制代码
#include <queue>
#include <functional>
#include <iostream>
#include <string>

// 任务结构体
struct Task {
    std::string name;
    int priority; // 优先级:数字越大越优先

    Task(std::string n, int p) : name(n), priority(p) {}
};

// 自定义比较函数对象(小根堆:优先级低的在前)
struct TaskCompare {
    bool operator()(const Task& a, const Task& b) const {
        return a.priority > b.priority; // 与大根堆相反
    }
};

int main() {
    // 1. 大根堆(默认,优先级高的先执行)
    std::priority_queue<Task> maxPq;
    maxPq.push(Task("任务A", 3));
    maxPq.push(Task("任务B", 5));
    maxPq.push(Task("任务C", 2));

    std::cout << "大根堆优先队列执行顺序:" << std::endl;
    while (!maxPq.empty()) {
        auto task = maxPq.top();
        maxPq.pop();
        std::cout << "执行:" << task.name << "(优先级:" << task.priority << ")" << std::endl;
    }
    // 输出:任务B(5)→ 任务A(3)→ 任务C(2)

    // 2. 小根堆(自定义比较函数)
    std::priority_queue<Task, std::vector<Task>, TaskCompare> minPq;
    minPq.push(Task("任务A", 3));
    minPq.push(Task("任务B", 5));
    minPq.push(Task("任务C", 2));

    std::cout << "\n小根堆优先队列执行顺序:" << std::endl;
    while (!minPq.empty()) {
        auto task = minPq.top();
        minPq.pop();
        std::cout << "执行:" << task.name << "(优先级:" << task.priority << ")" << std::endl;
    }
    // 输出:任务C(2)→ 任务A(3)→ 任务B(5)

    return 0;
}

4.3 TopK问题(海量数据前K大元素)

TopK问题是面试高频题,核心需求是从海量数据中快速找到前K个最大值(或最小值)。使用小根堆可实现O(nlogK)时间复杂度、O(K)空间复杂度。

原理(前K大元素):
  1. 构建一个大小为K的小根堆;
  2. 遍历所有数据,若元素大于堆顶(当前K个元素的最小值),则替换堆顶并调整堆;
  3. 遍历结束后,堆中元素即为前K大元素。
C++实现:
cpp 复制代码
#include <vector>
#include <queue>
#include <iostream>
#include <random>

// 海量数据前K大元素(小根堆实现)
std::vector<int> topK(std::vector<int>& data, int k) {
    if (data.empty() || k <= 0 || k > data.size()) {
        throw std::invalid_argument("Invalid input!");
    }

    // 步骤1:构建大小为K的小根堆(用STL priority_queue,greater<int>)
    std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
    for (int i = 0; i < k; ++i) {
        minHeap.push(data[i]);
    }

    // 步骤2:遍历剩余数据,更新堆
    for (int i = k; i < data.size(); ++i) {
        if (data[i] > minHeap.top()) { // 比堆顶大,替换
            minHeap.pop();
            minHeap.push(data[i]);
        }
    }

    // 步骤3:提取堆中元素
    std::vector<int> result;
    while (!minHeap.empty()) {
        result.push_back(minHeap.top());
        minHeap.pop();
    }
    return result;
}

// 测试:生成1000个随机数,找前10大
int main() {
    std::vector<int> data;
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(1, 10000);

    for (int i = 0; i < 1000; ++i) {
        data.push_back(dis(gen));
    }

    int k = 10;
    auto topKResult = topK(data);
    std::cout << "前" << k << "大元素:";
    for (int num : topKResult) {
        std::cout << num << " ";
    }
    return 0;
}

4.4 中位数问题(动态数据实时获取中位数)

中位数是有序数组中间的元素(奇数长度取中间,偶数长度取平均)。对于动态插入的数据,使用两个堆(大根堆+小根堆)可实现O(logn)插入和O(1)查询。

原理:
  1. 大根堆(leftHeap):存储左半部分数据(≤中位数),堆顶为左半部分最大值;
  2. 小根堆(rightHeap):存储右半部分数据(≥中位数),堆顶为右半部分最小值;
  3. 平衡条件:两堆大小差≤1(奇数长度时,大根堆多1个元素;偶数长度时,两堆大小相等);
  4. 插入逻辑
    • 若元素≤大根堆顶,插入大根堆;否则插入小根堆;
    • 调整两堆大小,确保平衡条件;
  5. 查询逻辑
    • 奇数长度:中位数 = 大根堆顶;
    • 偶数长度:中位数 = (大根堆顶 + 小根堆顶) / 2。
C++实现:
cpp 复制代码
#include <vector>
#include <queue>
#include <iostream>
#include <stdexcept>

class MedianFinder {
private:
    std::priority_queue<int> leftHeap; // 大根堆(默认):存储左半部分
    std::priority_queue<int, std::vector<int>, std::greater<int>> rightHeap; // 小根堆:存储右半部分

    // 平衡两堆大小
    void balance() {
        // 大根堆比小根堆多2个,移一个到小根堆
        if (leftHeap.size() - rightHeap.size() == 2) {
            rightHeap.push(leftHeap.top());
            leftHeap.pop();
        }
        // 小根堆比大根堆多1个,移一个到大根堆
        else if (rightHeap.size() - leftHeap.size() == 1) {
            leftHeap.push(rightHeap.top());
            rightHeap.pop();
        }
    }

public:
    MedianFinder() = default;

    // 插入元素
    void addNum(int num) {
        if (leftHeap.empty() || num <= leftHeap.top()) {
            leftHeap.push(num);
        } else {
            rightHeap.push(num);
        }
        balance(); // 平衡堆大小
    }

    // 获取中位数
    double findMedian() {
        if (leftHeap.empty() && rightHeap.empty()) {
            throw std::runtime_error("No data!");
        }
        // 奇数长度:左堆多1个
        if (leftHeap.size() > rightHeap.size()) {
            return leftHeap.top();
        }
        // 偶数长度:两堆大小相等
        else {
            return (leftHeap.top() + rightHeap.top()) / 2.0;
        }
    }
};

// 测试
int main() {
    MedianFinder mf;
    mf.addNum(1);
    mf.addNum(3);
    std::cout << "中位数:" << mf.findMedian() << std::endl; // (1+3)/2=2.0
    mf.addNum(2);
    std::cout << "中位数:" << mf.findMedian() << std::endl; // 2
    mf.addNum(4);
    std::cout << "中位数:" << mf.findMedian() << std::endl; // (2+3)/2=2.5
    return 0;
}

五、堆的优缺点与适用场景

5.1 优点

  • 极值查询(堆顶):O(1) 常数时间;
  • 插入/删除堆顶:O(logn) 高效;
  • 存储紧凑:数组存储,无额外指针开销;
  • 支持动态数据:适合元素频繁插入/删除的场景。

5.2 缺点

  • 随机访问低效:需遍历数组,O(n);
  • 查找任意元素低效:O(n);
  • 堆排序不稳定:相同元素可能交换位置;
  • 调整堆开销:插入/删除需维护堆序,比数组直接操作耗时。

5.3 适用场景

  • 优先队列(任务调度、事件驱动);
  • 排序(堆排序);
  • TopK问题(海量数据前K大/小);
  • 中位数查询(动态数据);
  • 贪心算法(如霍夫曼编码、Prim最小生成树)。

六、常见误区与注意事项

  1. 建堆时间复杂度:自底向上建堆是O(n),而非O(nlogn),面试高频考点;
  2. STL priority_queue的坑pop()不返回元素,需先top()pop();默认大根堆,小根堆需显式指定greater<T>
  3. 下沉操作的边界:必须判断左右子节点是否存在,避免数组越界;
  4. 堆与BST的区别:堆是"父子有序,全局无序",BST是"左小右大,全局有序",切勿混淆;
  5. TopK问题的堆选择:前K大用小根堆,前K小用大根堆,可最小化堆大小(O(K))。

堆是基于完全二叉树的高效数据结构,核心优势在于O(1)极值查询和O(logn)插入/删除。掌握堆的关键在于理解"上浮"和"下沉"的维护逻辑,以及建堆的O(n)复杂度推导,这也是面试中的重点考察内容。在实际开发中,优先使用STL的priority_queue,但自定义堆实现能更深入理解其底层原理,应对复杂场景(如中位数查询、自定义比较规则)。


补充建堆的O(n)复杂度推导

∑k=0h−12k×(h−k)=O(n)\sum_{k=0}^{h-1} 2^k \times (h - k) = O(n)∑k=0h−12k×(h−k)=O(n),核心是两步走 :先算出求和式的精确值 ,再结合 hhh 与 nnn 的内在关系(通常来自二叉树场景),推导其时间复杂度量级。

一、第一步:计算求和式的精确值(错位相减法)

求和式 ∑k=0h−12k×(h−k)\sum_{k=0}^{h-1} 2^k \times (h - k)∑k=0h−12k×(h−k) 是典型的「等差×等比」数列求和(其中 2k2^k2k 是等比数列,(h−k)(h-k)(h−k) 是等差数列),适合用错位相减法求解。

步骤1:展开求和式,定义 SSS

设 S=∑k=0h−12k×(h−k)S = \sum_{k=0}^{h-1} 2^k \times (h - k)S=∑k=0h−12k×(h−k),按 kkk 从 000 到 h−1h-1h−1 展开:
S=h×20+(h−1)×21+(h−2)×22+⋯+1×2h−1(1) S = h \times 2^0 + (h-1) \times 2^1 + (h-2) \times 2^2 + \dots + 1 \times 2^{h-1} \tag{1} S=h×20+(h−1)×21+(h−2)×22+⋯+1×2h−1(1)

(解释:当 k=0k=0k=0 时,项为 h×1h \times 1h×1;k=1k=1k=1 时,项为 (h−1)×2(h-1) \times 2(h−1)×2;...;k=h−1k=h-1k=h−1 时,项为 1×2h−11 \times 2^{h-1}1×2h−1)

步骤2:乘以等比数列的公比(此处公比为2)

将等式(1)两边同时乘以 222,得到 2S2S2S:
2S=h×21+(h−1)×22+(h−2)×23+⋯+1×2h(2) 2S = h \times 2^1 + (h-1) \times 2^2 + (h-2) \times 2^3 + \dots + 1 \times 2^h \tag{2} 2S=h×21+(h−1)×22+(h−2)×23+⋯+1×2h(2)

步骤3:错位相减,消去中间项

用(2)式减去(1)式,对齐同类项(核心是消去中间的 21,22,...,2h−12^1, 2^2, ..., 2^{h-1}21,22,...,2h−1 项):
2S−S=[h×21−h×20]+[(h−1)×22−(h−1)×21]+⋯+[1×2h−1×2h−1] 2S - S = \left[ h \times 2^1 - h \times 2^0 \right] + \left[ (h-1) \times 2^2 - (h-1) \times 2^1 \right] + \dots + \left[ 1 \times 2^h - 1 \times 2^{h-1} \right] 2S−S=[h×21−h×20]+[(h−1)×22−(h−1)×21]+⋯+[1×2h−1×2h−1]

步骤4:化简右边表达式

提取每一项的公因子,中间项会形成等比数列:
S=−h×20+21+22+⋯+2h⏟等比数列求和 S = -h \times 2^0 + \underbrace{2^1 + 2^2 + \dots + 2^h}_{等比数列求和} S=−h×20+等比数列求和 21+22+⋯+2h

  • 首项 −h×20=−h-h \times 2^0 = -h−h×20=−h(来自(2)式的首项减(1)式的首项);
  • 中间的等比数列:21+22+...+2h2^1 + 2^2 + ... + 2^h21+22+...+2h,共 hhh 项,公比 222,首项 222。
步骤5:计算等比数列的和

根据等比数列求和公式 S等比=a1⋅qm−1q−1S_{等比} = a_1 \cdot \frac{q^m - 1}{q - 1}S等比=a1⋅q−1qm−1(a1a_1a1 为首项,qqq 为公比,mmm 为项数):
21+22+⋯+2h=2⋅2h−12−1=2h+1−2 2^1 + 2^2 + \dots + 2^h = 2 \cdot \frac{2^h - 1}{2 - 1} = 2^{h+1} - 2 21+22+⋯+2h=2⋅2−12h−1=2h+1−2

步骤6:合并得到 SSS 的精确值

将等比数列和代入 SSS 的表达式:
S=−h+(2h+1−2)=2h+1−h−2 S = -h + (2^{h+1} - 2) = 2^{h+1} - h - 2 S=−h+(2h+1−2)=2h+1−h−2

二、第二步:建立 hhh 与 nnn 的关系(关键前提)

求和式 ∑k=0h−12k×(h−k)\sum_{k=0}^{h-1} 2^k \times (h - k)∑k=0h−12k×(h−k) 通常来自二叉树场景 (比如平衡二叉树的路径长度之和、操作次数总和等),此时 hhh 是二叉树的高度 ,nnn 是二叉树的节点数

对于平衡二叉树(或满二叉树,平衡树的极端情况),节点数 nnn 与高度 hhh 满足:
n=2h−1(满二叉树节点数公式:第1层到第h层,每层节点数为2k−1,总和为2h−1) n = 2^h - 1 \quad (\text{满二叉树节点数公式:第1层到第h层,每层节点数为} 2^{k-1},总和为 2^h - 1) n=2h−1(满二叉树节点数公式:第1层到第h层,每层节点数为2k−1,总和为2h−1)

变形可得:
2h=n+1  ⟹  2h+1=2(n+1)=2n+2 2^h = n + 1 \implies 2^{h+1} = 2(n + 1) = 2n + 2 2h=n+1⟹2h+1=2(n+1)=2n+2

三、第三步:推导 S=O(n)S = O(n)S=O(n)

将 2h+1=2n+22^{h+1} = 2n + 22h+1=2n+2 代入 SSS 的精确值:
S=(2n+2)−h−2=2n−h S = (2n + 2) - h - 2 = 2n - h S=(2n+2)−h−2=2n−h

关键:分析 hhh 的量级

对于平衡二叉树,高度 hhh 是 对数级别 的:
h=log⁡2(n+1)≈log⁡2n(当n很大时,1可忽略) h = \log_2(n + 1) \approx \log_2 n \quad (\text{当n很大时,1可忽略}) h=log2(n+1)≈log2n(当n很大时,1可忽略)
log⁡2n\log_2 nlog2n 是低阶无穷小 (增长速度远慢于 nnn),例如:

  • 当 n=106n=10^6n=106 时,log⁡2n≈20\log_2 n \approx 20log2n≈20,h≈20h \approx 20h≈20;
  • 当 n=109n=10^9n=109 时,log⁡2n≈30\log_2 n \approx 30log2n≈30,h≈30h \approx 30h≈30。
用大O符号定义验证

大O符号的核心定义:若存在常数 C>0C>0C>0 和 n0>0n_0>0n0>0,当 n≥n0n \geq n_0n≥n0 时,S≤C⋅nS \leq C \cdot nS≤C⋅n,则 S=O(n)S=O(n)S=O(n)。

对于 S=2n−hS=2n - hS=2n−h:

  • 因为 h≥1h \geq 1h≥1(树的高度至少为1),所以 S=2n−h≤2nS = 2n - h \leq 2nS=2n−h≤2n(对所有 n≥1n \geq 1n≥1 成立);
  • 取 C=2C=2C=2,n0=1n_0=1n0=1,完全满足大O定义。

因此,S=2n−h=O(n)S = 2n - h = O(n)S=2n−h=O(n)(低阶项 hhh 不影响增长趋势,主导项是 2n2n2n,与 nnn 同量级)。


  1. 求和计算 :通过错位相减法,得到精确值 S=2h+1−h−2S=2^{h+1} - h - 2S=2h+1−h−2;
  2. 关联 hhh 与 nnn :平衡二叉树中,节点数 n=2h−1n=2^h -1n=2h−1,故 2h+1=2n+22^{h+1}=2n+22h+1=2n+2;
  3. 量级分析 :代入后 S=2n−hS=2n - hS=2n−h,其中 h=O(log⁡n)h=O(\log n)h=O(logn)(低阶项),主导项为 2n2n2n;
  4. 大O验证 :满足 S≤C⋅nS \leq C \cdot nS≤C⋅n,故 S=O(n)S=O(n)S=O(n)。

本质是:求和式的增长速度由 2h2^h2h 主导,而 2h2^h2h 与二叉树节点数 nnn 是线性关系(2h=n+12^h = n+12h=n+1),因此求和式最终是 O(n)O(n)O(n) 量级。

相关推荐
apocelipes1 小时前
Linux的binfmt_misc机制
linux·c语言·c++·python·golang·linux编程·开发工具和环境
渡我白衣1 小时前
哈希的暴力美学——std::unordered_map 的底层风暴、扩容黑盒与哈希冲突终极博弈
java·c语言·c++·人工智能·深度学习·算法·哈希算法
虾..1 小时前
Linux 进程控制
linux·运维·服务器
last demo1 小时前
pxe自动化安装系统实验
linux·运维·服务器·自动化
带土11 小时前
13. 某马数据结构整理(1)
数据结构
x***01061 小时前
Java框架SpringBoot(一)
java·开发语言·spring boot
qq_433554541 小时前
C++ 最大子段和(动态规划)
开发语言·c++·动态规划
lijiatu100861 小时前
[C++] lock_guard、unique_lock与条件变量wait()函数
开发语言·c++
2509_940880221 小时前
CC++链接数据库(MySQL)超级详细指南
c语言·数据库·c++