二叉堆

在介绍堆之前, 先看一下一些概念

完全二叉树(Complete Binary Tree)

若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。

维基百科的定义

树的深度:对于任意节点n,n的深度为从根到n的唯一路径长,根的深度为0;
树的高度:对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0;

二叉堆

二叉堆是一个数组, 可以看成近似的完全二叉树,树上的每一节点对应数组中的一个数,除最低层外,该树是完全充满的,且从左往右填充

下标计算

由于数组下标从 0 开始,故处理方式:将把索引从 0 开始转化成从 1 开始,然后算出结果 x,则实际索引为 x-1

// 先加1, 再减1
left = ((i+1)x2-1 
right = leftChild-1
parent = (i+1)/2-1

各个操作时间复杂度

1. keep_heap: 时间复杂度O(lgn),维护最大堆性质的关键
2. build_heap: 时间复杂度O(n), 作用是将一个无序数组建立成一个堆
3. heap_sort: 时间复杂度O(nlgn),对一个数组原址排序
4. pop_heap, push_heap: 在堆中, 插入删除元素时间复杂度都是 O(lgn)

等比数列求和公式

如果等比数列的公比为 \(q\), 等比数列的和为 \(S_{n}=a_{1}+a_{2}+a_{3}+\cdots +a_{n}\), 求和如下

\[{S_{n}= \begin{cases}{ \frac {a_{1}-a_{n}q}{1-q}}&q\neq 1\\ na_{1}&q=1\end{cases} } \]

深度为 h 的满二叉树的节点个数为

\[\begin{align*} S(n) &= 2^0 + 2^1+2^2 \cdots+2^h \\ &=\frac{1-2^{h}\cdot 2}{1-2} \\ &= 2^{(h+1)}-1 \end{align*} \]

屏幕快照 2018-08-27 下午11.54.24-w656

简单代码实现二叉堆

#include<iostream>
#include<vector>
#include <map>
using namespace std;

// 这里操作的是大顶堆

// keep_heap 维持堆的性质, 时间复杂度 O(lgn)
// 1. 输入数组 nums 和下标 parent,假设左右孩子节点 left 和 right 的二叉树都是最大堆
// 2. nums[parent] 可能小于其孩子,这时违反了最大堆的性质
// 3. keep_heap 的作用就是让 A[parent] 在最大堆里逐级下降,
//    最后使得每一个子堆都满足最大堆的性质

void keep_heap(vector<int> &nums, int parent){
    int left = (parent+1)*2-1;
    int right = left + 1;
    int largest = parent;

    if (left < nums.size() && nums[left] > nums[largest]) largest = left;
    if (right < nums.size() && nums[right] > nums[largest]) largest = right;

    if (largest != parent) {
        swap(nums[largest], nums[parent]);
        keep_heap(nums, largest);
    }
}

// 时间复杂度为线性时间复杂度 O(n)
// 证明: 当用数组表示存储 n 个元素的堆时, 叶节点的下标分别是 floor(n/2)+1, floor(n/2)+2, ..., n
// 我们知道,堆是一个完全二叉树,最后一个节点 n 的父节点为 parent = floor(n/2) (索引从 1 开始);
// 假设父节点的右兄弟存在, 那么父节点的右兄弟 (parent+1) 的左孩子节点为 2x(floor(n/2)+1);
// 当 n 为偶数时, floor(n/2) > (n-1)/2;
// 当 n 为奇数时, floor(n/2) = (n-1)/2,
// 所以 floor(n/2) ≥ (n-1)/2
// 故 2x(floor(n/2)+1) ≥ 2x((n-1)/2+1) = n+1, 而 n 是最大的索引, 故假设不成立
// 与题设矛盾 所以说存储 n 个元素的堆的叶节点的下标分别是 floor(n/2)+1, floor(n/2)+2, ..., n

void build_heap(vector<int> &nums) {
    for (int i = nums.size()/2-1; i >= 0; i--) {
        keep_heap(nums, i);
    }
}

// 顶端节点出队, 然后将最后一个元素放在顶部, 然后下滤
int pop_heap(vector<int> &nums) {
    int answer = -1;
    if (nums.size() == 0) {
        cout << "heap is already empty!";
        return answer;
    }
    answer = nums[0];
    nums[0] = nums[nums.size()-1];
    nums.pop_back();
    keep_heap(nums, 0);
    return answer;
}

// 将插入元素放在数组尾部, 然后上滤
void push_heap(vector<int> &nums, int val) {
    nums.push_back(val);
    int child = nums.size() - 1;
    while(child > 0) {
        // child > 0, father >= 0
        int father = (child+1)/2-1;
        if (nums[father] < nums[child]) {
            swap(nums[father], nums[child]);
            child = father;
        } else {
            break;
        }
    }
}

int main() {

    vector<int> v = {4, 5 ,7, 10};
    build_heap(v);
    for (auto e : v) {
        cout << e << " ";
    }
    cout << endl << endl;

    int size = v.size();
    for (int i = 0; i < size; i++) {
        cout << pop_heap(v) << " ";
    }
    cout << endl << endl;

    push_heap(v, 1);
    push_heap(v, 5);
    push_heap(v, 7);
    size = int(v.size());
    for (int i = 0; i < size; i++) {
        cout << v[i] << " ";
    }
    cout << endl;
}

C++ 自带的与堆相关的函数

下面介绍 STL 中与堆相关的 4 个函数

1. 建立堆 make_heap(_First, _Last, _Comp)

默认是建立最大堆的。对int类型,可以在第三个参数传入greater()得到最小堆。

2. 在堆中添加数据 push_heap (_First, _Last)

要先在容器中加入数据,再调用 push_heap()

3. 在堆中删除数据 pop_heap(_First, _Last)**

要先调用 pop_heap() 再在容器中删除数据

4. 堆排序 sort_heap(_First, _Last)**

排序之后就不再是一个合法的heap了

#include <iostream>
#include <vector>
#include <map>
using namespace std;

int main() {
    vector<int> vec = {2, 3, 5, 1, 34, 5};
    make_heap(vec.begin(), vec.end(), greater<int>());
    for (auto e : vec) {
        cout << e << " ";
    }
    cout << endl;

    // 不管是 push 还是 pop, 都要提供 comp 函数, 不然有错

    // 1. 先在容器中加入元素, 然后调用push_heap进行调整
    vec.push_back(10);
    push_heap(vec.begin(), vec.end(), greater<int>());

    for (auto e : vec) {
        cout << e << " ";
    }
    cout << endl;
    // 2. 删除容器元素, 先调用pop_heap, 将删除元素放到容器末尾, 然后删除
    pop_heap(vec.begin(), vec.end(), greater<int>());
    vec.pop_back();
    for (auto e : vec) {
        cout << e << " ";
    }
    return 0;
}
posted @ 2018-08-28 00:24  nowgood  阅读(563)  评论(0编辑  收藏  举报