数据结构---堆(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) 量级。

相关推荐
東隅已逝,桑榆非晚2 分钟前
数据结构常见英文术语速查手册
数据结构
Ivan CloudBay4 分钟前
服务器关机后,网站会发生什么?
运维·服务器
程序员小崔日记8 小时前
20块钱注册一年 .COM 域名!Spaceship 最新优惠教程,支付宝即可付款(附优惠码)
服务器·腾讯云·域名
实心儿儿9 小时前
Linux —— 进程间关系和守护进程
linux·运维·服务器
林浅不想努力9 小时前
LVS知识点总结
服务器·云原生·lvs
code_pgf9 小时前
C++11 / C++14 / C++17 / C++20 新特性总结
c++·c++20
魔力女仆10 小时前
分享一个 JS 鼠标跟随贪吃蛇背景库
开发语言·javascript·计算机外设
Dawn-bit10 小时前
Linux磁盘管理详解
linux·运维·服务器·计算机网络·云计算
2401_8949155311 小时前
GEO 搜索优化完整源码从零部署:环境配置、集群搭建全流程
开发语言·python·tcp/ip·算法·unity
麻瓜老宋11 小时前
AI开发C语言应用按步走,表达式计算器calc的第二十二步,分号赋值链式修复、TOKEN_ASSIGN
c语言·开发语言·atomcode