概述

堆是一颗完全二叉树。分为大根堆(父节点>=所有的子节点)和小根堆(父节点<=所有的子节点)。

插入、删除堆顶都是O(logN),查询最值是O(1)。

 


完全二叉树(Complete Binary Tree)

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

完全二叉树是由满二叉树而引出来的。对于深度为K的,有N个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。

若一棵二叉树至多只有最下面的两层上的结点的度数可以小于2,并且最下层上的结点都集中在该层最左边的若干位置上,则此二叉树成为完全二叉树。

完全二叉树的特性:对于结点t,t/2为它的父节点,2*t、2*t+1为它的子节点。所以可以直接用一个线性的数组存放堆。

 


堆的基本操作:(以小根堆为例)

1.堆元素上移:和自己的父节点比较,若满足条件则交换。再次比较。

void up(int x)
{
    int p,q;
    p=x;
    while (p/2>=1)
    {
        q=p/2;
        if (a[q]>a[p])
            swap(&a[p],&a[q]);
        else
            break;
        p=q;
    }
}

2.堆元素下移:和自己的子节点中更满足条件(更大or更小)的一个比较,若满足条件则交换。再次比较。

void down(int x)
{
    int p,q;
    p=x;
    while (p*2<=n)
    {
        q=2*p;
        if ((a[q+1]<a[q])&&(q+1<=n)) q++;       
        if (a[q]<a[p])
            swap(&a[q],&a[p]);
        else
            break;
        p=q;
    }
}

3.建堆:

    cin>>n;
    for (i=1;i<=n;i++)
        cin>>a[i];
    for (i=n/2;i>=1;i--)      //1
        down(i);
注:对1处语句的解释:
1. a[(n/2)+1]----a[n]的元素都是堆上的叶子节点,无需down(i)操作。 2. 倒序循环是因为,对于一个节点i,只有等它的子节点都完成了down操作之后,才可以对它进行操作。即保证循环到i时,i+1、i+2、…..、n都是一个最大\最小堆的根。

4.删除元素(删除堆顶的最值元素)

用堆最末端(二叉树的最右下角)的元素替换堆顶元素,n--,然后对其进行down(i)操作

 

5.添加元素

将元素添加到堆的最末端(二叉树的最右下角),n++,然后对其进行up(i)操作

 

6. 删除任意一个点(事先要保证堆中没有重复元素)

首先在堆中找到该元素的位置(可以提前用hashmap记录)

用堆最末端(二叉树的最右下角)的元素替换要删除的元素,n--。然后对其进行up(i)或者down(i)操作,根据元素的大小而定。

 

注意:一个已从小到大排好序的数组是一个小根堆,但一个小根堆数组里面的元素不一定排好序   (source:算法导论P153    Exercise6.1-5、6.1-6)

 

 


 

题目

https://www.jiuzhang.com/solution/heapify/

 

基于 Siftup 的版本    O(NlogN)

 

 

public class Solution {
    /**
     * @param A: Given an integer array
     * @return: void
     */
    private void siftup(int[] A, int k) {
        while (k != 0) {
            int father = (k - 1) / 2;
            if (A[k] > A[father]) {
                break;
            }
            int temp = A[k];
            A[k] = A[father];
            A[father] = temp;
            
            k = father;
        }
    }
    
    public void heapify(int[] A) {
        for (int i = 0; i < A.length; i++) {
            siftup(A, i);
        }
    }
}

 

算法思路:
    对于每个元素A[i],比较A[i]和它的父亲结点的大小,如果小于父亲结点,则与父亲结点交换。
    交换后再和新的父亲比较,重复上述操作,直至该点的值大于父亲。

时间复杂度分析
    对于每个元素都要遍历一遍,这部分是 O(n)。
    每处理一个元素时,最多需要向根部方向交换 logn 次。

因此总的时间复杂度是 O(nlogn)

 

 

 

基于 Siftdown 的版本    O(N)

public class Solution {
    /**
     * @param A: Given an integer array
     * @return: void
     */
    private void siftdown(int[] A, int k) {
        while (k * 2 + 1 < A.length) {
            int son = k * 2 + 1;   // A[i] 的左儿子下标。
            if (k * 2 + 2 < A.length && A[son] > A[k * 2 + 2])
                son = k * 2 + 2;     // 选择两个儿子中较小的。
            if (A[son] >= A[k])      
                break;
            
            int temp = A[son];
            A[son] = A[k];
            A[k] = temp;
            k = son;
        }
    }
    
    public void heapify(int[] A) {
        for (int i = (A.length - 1) / 2; i >= 0; i--) {
            siftdown(A, i);
        }
    }
}

算法思路:
    初始选择最接近叶子的一个父结点,与其两个儿子中较小的一个比较,若大于儿子,则与儿子交换。
    交换后再与新的儿子比较并交换,直至没有儿子。
    再选择较浅深度的父亲结点,重复上述步骤。

时间复杂度分析
这个版本的算法,乍一看也是 O(nlogn), 但是我们仔细分析一下,算法从第 n/2 个数开始,倒过来进行 siftdown。也就是说,相当于从 heap 的倒数第二层开始进行 siftdown 操作,倒数第二层的节点大约有 n/4 个, 这 n/4 个数,最多 siftdown 1次就到底了,所以这一层的时间复杂度耗费是 O(n/4),然后倒数第三层差不多 n/8 个点,最多 siftdown 2次就到底了。所以这里的耗费是 O(n/8 * 2), 倒数第4层是 O(n/16 * 3),倒数第5层是 O(n/32 * 4) ... 因此累加所有的时间复杂度耗费为:

T(n) = O(n/4) + O(n/8 * 2) + O(n/16 * 3) ...

然后我们用 2T - T 得到:

2 * T(n) = O(n/2) + O(n/4 * 2) + O(n/8 * 3) + O(n/16 * 4) ...
T(n)      = O(n/4)     + O(n/8 * 2) + O(n/16 * 3) ...

2 * T(n) - T(n) = O(n/2) +O (n/4) + O(n/8) + ...
                       = O(n/2 + n/4 + n/8 + ... )
                       = O(n)

因此得到 T(n) = 2 * T(n) - T(n) = O(n)

 

 

 


堆的应用

  1. 堆排序

原理:对输入数据建堆,然后每次输出堆顶元素,然后删除堆顶元素。循环n次即可输出完排序好的n个数。

      从小到大排序选小根堆,从大到小排序选大根堆。

 1 #include <iostream>
 2 using namespace std;
 3 int a[1000];
 4 int n,i,tx;
 5 
 6 void swap(int *a,int *b)
 7 {
 8     int tmp;
 9     tmp=*a;
10     *a=*b;
11     *b=tmp;
12 }
13 
14 void down(int x)
15 {
16     int p,q;
17     p=x;
18     while (p*2<=n)
19     {
20         q=2*p;
21         if ((a[q+1]<a[q])&&(q+1<=n)) q++;
22         if (a[q]<a[p])
23             swap(&a[q],&a[p]);
24         else
25             break;
26         p=q;
27     }
28 }
29 
30 int main()
31 {
32     cin>>n;
33     for (i=1;i<=n;i++)
34         cin>>a[i];
35         
36     for (i=n/2;i>=1;i--)
37         down(i);
38 
39     tx=n;
40     for (i=1;i<=tx;i++)
41     {
42         cout<<a[1]<<"  ";
43         a[1]=a[n];
44         n--;
45         down(1);
46     }
47     cout<<endl;
48 }
View Code

相关文章: