堆和堆排序

1. 堆

  • (二叉)堆是一个数组,它可以看成是近似的完全二叉树,树上的每一个结点对应数组中的一个元素。
  • 结点A[i]的左右孩子结点对应为A[2i] & A[2i+1]。则A[i]的父结点为A[i/2](此处以及后文统一默认向下取整)。乘2与除2做操作在计算机中可以用移位操作实现,因此会比较快。
  • 由此可知这棵树除最后一层外,完全充满的,且有左向右填充。
  • 要构成堆还需要满足满足以下性质:
    构成最大堆:任意父结点的值不小于其左右子结点的值,即,对于任意的i: A[i] >= A[2i] && A[i] >= A[2i+1]。
    构成最小堆: 任意父结点的值不大于其左右子结点的值,即,对于任意的i: A[i] <= A[2i] && A[i] >= A[2i+1]。

1.1 维护堆的性质

堆的性质的维护,以下都以最大堆为例。
堆的维护的主要思想是“逐层下降”。举例:某个结点i, 假设其左右子结点left(i),right(i)都已经是最大堆,那么需要调节(或者说是调换)i, left(i), right(i)的值,并保证调换后的子树继续调换下去直到子树继续满足堆的性质。首先,如果结点i的值大于左右子结点的值(即i的值是三个值中的最大值),则不需要调换,因为左右子结点均满足堆的性质,同时结点i也满足了结点值大于等于子结点的值,所以整棵树就是最大堆(也有叫大顶堆)。其次,如果结点i的值不是三个中的最大值,不妨设左结点的值最大(即左结点的值比右结点以及i的值都大),则把i的值跟左结点的值调换,此时i, left, right满足了父结点值大于等于左右子结点值的情况,那么被调换后的左结点的值(变小了),以该结点为参考对象,它因为变小了,那么它还满足堆的要去吗,所以需要继续进行检测和调换,即逐层的进行下去,直到原来的i值滑落到稳定的位置。时间复杂度是O(lgn)。以下是算法代码的具体验证:

/*
最大堆的维持输入参数是一个数组,和一个小标i,
表示维持i到heap-size使该范围的二叉树为一个最大堆。
注意一个前提:结点i的左右子树都已经是最大堆。
此处假设a的数组长度size与heap-size一致。
*/
void max_heapify(vector<int>& a, int i){
    int largest = i;
    int left = 2 * i+ 1;//因为数组的下标是从0开始的,与算法表述里的数组下标都是从1开始的,故应该进行相应转化,以使其对应
    int right = 2 * i + 2;
    if (left < a.size() && a[left] > a[i])
        largest = left;

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

    if (largest != i){
        int tmp = a[i];
        a[i] = a[largest];
        a[largest] = tmp;
        max_heapify(a, largest);
    }
}

int main()
{
    vector<int> a(10, 0);
    for (int i= 0; i < 10; i++)
        a[i] = 10-i;
    for (auto val : a)
        cout << val << " ";
    cout << endl;
    a[0] = 1;
    max_heapify(a, 0);
    for (auto val : a)
        cout << val << " ";
    return 0;
}

维持最大堆化示例

1.2 建堆

知道堆的性质的维护方法以后,建堆的思路也就有了,就是从已经堆化的部分逐步往上调用最大堆化函数,使整个数组逐渐全部满足堆的要求。底层的每一个单独的叶结点是满足堆的要求的,叶结点有A.length - A.length/2 个。所以从叶结点的父结点那一层开始建堆,这些点的下标范围是:1– A.length/2 建堆的过程是从后往前每个点调用最大堆化函数,所以可能会有人认为复杂度是O(nlgn),这个结果虽然正确但不是渐近紧确的,经过证明可以得到建堆过程的一个更紧确的界:O(n)。证明方法请参考算法导论。一下是建堆过程的代码示例:

void build_max_heap(vector<int>& a){
    for (int i = a.size() / 2-1; i >= 0; i--)
        max_heapify(a, i);
}
int main()
{
    srand(time(0));
    vector<int> a(10, 0);
    for (int i= 0; i < 10; i++)
        a[i] = rand()%20;
    for (auto val : a)
        cout << val << " ";
    cout << endl;
    cout << "buid max-heap: " << endl;
    build_max_heap(a);
    for (auto val : a)
        cout << val << " ";
    cout << endl;
    return 0;
}

建堆代码示例

2. 堆排序算法

给定数组A,如要对数组A进行排序输出,则第一步是根据A数组建堆,此步时间复杂度为O(n), 建好堆的数组A的最大元素一定在A[1](算法描述里数组的首个位置下标为1,代码里数组的首个位置下标为0,此处再做一次区分)。那么我把A[1]与A[n]调换,则此时数组里的最大元素已经跑到数组的最后位置,那么数组的前n-1个元素是什么样的情况,由于A[1]和A[n]互换,所以新的A[1]结点以及其左右子结点组成的triangle不一定满足最大堆的性质了(父结点的值大于等于子结点的值),但是其左右子结点对应的树,依然是满足堆的性质的,所以对于A的前n-1个元素,只需要对A[1]结点调用维持堆化过程,整个n-1个元素就又变成了新的最大堆,其第一个元素A[1]就是n-1个元素中的最大值,也即是数组A中的次大值, 此时把A[1]与A[n-1]交换,则次大值放入了正确的位置,依次类推,直到位置2-n都放入了相应的值,则排序结束,时间复杂度O(nlgn)。以下是代码示例:

void max_heapify(vector<int>& a, int i, int heap_size){
    int largest = i;
    int left = 2 * i + 1;//因为数组的下标是从0开始的,与算法表述里的下标略微不对应,故应该用此表述
    int right = 2 * i + 2;
    if (left < heap_size && a[left] > a[i])
        largest = left;

    if (right < heap_size && a[right] > a[largest])
        largest = right;

    if (largest != i){
        int tmp = a[i];
        a[i] = a[largest];
        a[largest] = tmp;
        max_heapify(a, largest, heap_size);
    }
}

void build_max_heap(vector<int>& a, int heap_size){
    for (int i = heap_size / 2-1; i >= 0; i--)
        max_heapify(a, i, heap_size);
}

void heapsort(vector<int>& a){
    int n = a.size();
    build_max_heap(a, a.size());
    for (int i = n - 1; i >= 1; i--){
        int tmp = a[0];
        a[0] = a[i];
        a[i] = tmp;
        max_heapify(a, 0, i);
    }   
}

int main()
{
    srand(time(0));
    vector<int> a(10, 0);
    for (int i= 0; i < 10; i++)
        a[i] = rand()%20;
    for (auto val : a)
        cout << val << " ";
    cout << endl;
    cout << "heap sort: " << endl;  
    heapsort(a);
    for (auto val : a)
        cout << val << " ";
    cout << endl;
    return 0;
}

堆排序代码示例

3. 优先队列

参考:算法导论

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值