有关堆的知识

堆分为最大堆和最小堆。最大堆定义:一个二叉树,每个节点的值大于其子节点。最小堆相反。

一般用数组来表示堆,当然,用树,链表都可以,想怎么来怎么来。

数组d,有n个节点,其中一些下标定义:

1)d[k]的子节点为d[2*k+1]和d[2*k+2],当然这里的2*k+1和2*k+2都得小于n

2)最后一个父节点的位置:d[n/2-1]

3)d[k]节点的父节点的位置d[(k-1)/2]

有了如上的坐标索引,就能有定义一些堆的操作。下面的例子都以最大堆为例。

 

一、堆的建立:

一个数组初始化为堆,需要分为从下至上,然后从上至下。

从下至上指的是,从最下面一个父节点开始,遍历节点到根节点。回顾最大堆的性质:每一个节点,都比其子节点要大,也就是说,每一棵子树的最大节点是根节点。那么从最后一层开始,一直调整到最顶层,就保证了每棵子树都是最大堆。

那如何调整某棵子树?我们从上至下调整。

特殊的,我们先定义,如果一棵树只含有一个节点,那么这棵树也为最大堆。

我们假设当前节点为d[k]处在第i层,由于我们是从下至上遍历的子树,所以当前节点的左右子树已经满足最大堆的条件。

如果满足d[k] > d[2*k+1]且d[k] > d[2*k+2],即可以保证d[k]为根节点的堆也是最大堆。

所以,我们取d[2*k+1]和d[2*k+2]的更大者,和d[k]交换,这样就能保证上面的不等式。比如d[2*k+1] > d[2*k+2],那么交换d[k]和d[2*k+1],然后令k = 2*k+1,也就是向下递归这个过程,直到遇到叶子节点位置。

代码如下:

void top_down(vector<int> &nums, int k){
    int n = nums.size();
    k = 2 * k + 1;
    while(k < n){
        if(k + 1 < n && nums[k + 1] > nums[k])
            k++;
        if(nums[k] > nums[(k - 1) / 2])
            swap(nums[k], nums[(k - 1) / 2]);
        else
            break;
        k = 2 * k + 1;
    }
}

void buildheap(vector<int> &nums){
    int n = nums.size();
    //bottom up
    for(int i = n / 2 - 1; i >= 0; i--){
        top_down(nums, i);
    }
}

 

二、堆的插入

建立好了堆,需要往堆里插入数据,思路和构建堆是一样的。在数组末尾插入了一个元素,也要想办法把当前的数组调整为最大堆。末尾插入元素后,最后一个父节点构成的子树就不一定是最大堆了,所以需要从最后一个父节点开始进行调整。把新插入的元素一直往上和父节点交换位置,直到父节点大于新插入的值为止,这样就能保证堆被调整为最大堆。

证明:

假设新插入元素为d[n-1]。叶子节点就是最大堆。

假设新插入的元素替换到第k层的时候,由新插入的元素构成的子树为最大堆。此时新插入的元素位置为d[m]

那么到k-1层的时候,d[(m-1)/2]为d[m]的父节点,假设d[q]为d[m]的兄弟节点,那么必然有d[(m-1)/2] > d[q]。

如果d[(m-1)/2] < d[m],交换d[m]和d[(m-1)/2],则d[(m-1)/2]根节点的子树为最大堆。如果d[(m-1)/2] > d[m],不交换,则已经是最大堆。数学归纳法知得证。

代码:

void heapinsert(vector<int> &nums, int x){
    nums.push_back(x);
    int i = nums.size() - 1;
    int parent = (i - 1) / 2;
    while(parent >= 0 && i != 0){
        if(nums[parent] < nums[i])
            swap(nums[parent], nums[i]);
        else
            break;
        i = parent;
        parent = (i - 1) / 2;
    }
}

 

三、删除堆顶

删除堆顶操作为:把堆顶元素和最后一个元素互换,然后弹出最后一个元素。再把当前堆调整为最大堆。

由于堆顶此时变成了最后一个元素,不一定是最大值,所以从第一个元素开始,进行从上到下的调整。

void heapdelete(vector<int> &nums){
    nums[0] = nums.back();
    nums.pop_back();
    top_down(nums, 0);
}

 

四、堆的应用。

由于从堆中可以O(1)的时间找到最大值或者最小值,这个特性能够解决很多问题。比如K个有序链表的合并,排序算法等等。

下面举堆排序为例:

1)输入一个乱序数组,先调整为堆

2)把堆顶弹出,相当于删除堆顶,和数组末尾元素交换,这时待处理的数组长度减1

3)从第一个元素进行top_down操作调整堆

4)循环2-3,待处理的子数组长度为0时候,原数组变为有序。

时间复杂度O(nlog(n))(每次调整堆O(logn),调整n次)

void fixdown(vector<int> &nums, int k, int n){
    k = 2 * k + 1;
    while(k < n){
        if(k + 1 < n && nums[k + 1] > nums[k])
            k++;
        if(nums[k] > nums[(k - 1) / 2])
            swap(nums[k], nums[(k - 1) / 2]);
        else
            break;
        k = 2 * k + 1;
    }
}

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

void heapsort(vector<int> &nums){
    buildheap(nums);

    for(int i = 0; i < n; i++){
        swap(nums[0], nums[n - i - 1]);
        fixdown(nums, 0, n - i - 1);
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值