堆&&堆排序&&N个数中找出K个最大值&&优先级队列

来源:互联网 发布:淘宝店能注销吗 编辑:程序博客网 时间:2024/05/16 14:56

学习二叉树后,有一个东西需要我们来关注下,就是堆,对于堆,来说我们可以把堆看作一颗完全二叉树。这里我们也可以叫做二叉堆。

二叉堆满足二个特性:

1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。

2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。

另外,需要关注的就是有一个大堆和一个小堆。
大堆就是父节点的数值大于任何一个子节点的数值。
小堆就是父节点的数值小于任何一个子节点的数值。

1.堆


接下来,我们进行堆的创建:
首先我们要清楚,堆的储存结构,在这里作为一颗完全二叉树,我们可以使用数组来存储堆,然后调整数组的下标,这样就可以抽象出一颗完全二叉树。
我们接下来使用STL中的vector来代替数组。

然后这里我们需要它们父节点和子节点的关系:
父节点=(子节点-1)*2;
子节点=父节点*2+1;

1.1堆的创建


这样我们就可以根据上述的关系实现找到某个节点的父亲节点或者孩子节点。

比如,我们给出一个数组:

int a[] = { 10, 16, 18, 12, 11, 13, 15, 17, 14, 19 };

那么我们就相当于创建了这样的一颗完全二叉树
这里写图片描述

这就是这颗完全二叉树的关系。

接下来我们需要进行建堆。所谓建堆,其实就是我们所说的进行这颗完全二叉树的调整,根据我们想要大堆还是小堆,进行调整。

调整的方法是,从第倒数第一个非叶子节点进行调整,与它的两个叶子节点进行比较,调整。这个就是堆中的所谓的向下调整法。在这里要注意调整的时候要取出叶子节点中的最值节点(即最小值或者最大值)。然后和父节点进行比较。

void _AdjustDown(int root)    {        assert(!_a.empty());        size_t parent = root;        size_t child = parent * 2 + 1;        while (parent<_a.size())        {            Compare com;            if (child + 1<_a.size()                && com(_a[child + 1], _a[child]))                //在这里对两个子节点进行判断。看应该取出哪一个            {                ++child;            }            if (child<_a.size() && com(_a[child], _a[parent]))            //进行子节点和父节点之间的判断调整            {                std::swap(_a[child], _a[parent]);                parent = child;//调整完后的原父节点依然要和下面的节点进行继续调整。                child = parent * 2 + 1;            }            else            {                break;            }        }    }

这就是重要的向下调整法,为了方便对于大小堆的操控,我们引入仿函数,通过仿函数返回值,控制比较的条件。

template<typename T>//当需要小堆时:class Less{public:    bool operator ()(const T& a, const T& b)    {        return a < b;    }};template<typename T>//当需要大堆时class Greater{public:    bool operator ()(const T& a, const T& b)    {        return a > b;    }};

通过仿函数,我们只需要传入一个模板参数,然后通过创建的对象就可以实现控制是大堆还是小堆。

当我们每次对整个堆进行增加以后,我们都可以采用向下调整法,进行调整,这样就可以调整出新的满足要求的堆。
这里写图片描述

建堆的时间复杂度:O(N*logN)

1.2堆的插入


所以,堆的插入算法也就简单的实现了。
因为我们这里使用的是vector,所以我们直接使用vector 的算法进行添加。
插入算法就是把一个节点插入到堆的最后,然后进行向上调整算法。
向上调整法就是从一个节点开始,进行与他的父节点进行比较。
然后根据大堆或者小堆进行调整。

    void _AdjustUp(size_t child)    {        assert(!_a.empty());        while (child>0)        {            Compare com;            size_t parent = (child - 1) / 2;            if (com(_a[child], _a[parent]))            {                std::swap(_a[child], _a[parent]);                child = parent;            }            else            {                break;            }        }    }

这里写图片描述

void Push(const T& d)    {        _a.push_back(d);        _AdjustUp(_a.size() - 1);    }

插入的时间复杂度:O(logN)

1.3堆的删除


堆的删除算法,堆可以进行pop(),这里就是对堆顶进行pop(),
在这里的算法就是我们需要交换堆顶和最后一个堆节点,然后进行堆的向下调整算法。
这里写图片描述

void Pop(){    assert(!_a.empty());    swap(_a[0], _a[_a.size() - 1]);    _a.pop_back();    _AdjustDown(0);}

删除的时间复杂度:O(logN)


然后附上堆实现的完整代码:

//堆#pragma once#include<iostream>#include<cstdlib>#include<cassert>#include<vector>using namespace std;template<typename T>class Less{public:    bool operator ()(const T& a, const T& b)    {        return a < b;    }};template<typename T>class Greater{public:    bool operator ()(const T& a, const T& b)    {        return a > b;    }};//大堆template<typename T, typename Compare = Greater<T>>class Heap{public:    Heap(T *a, size_t size)    {        _a.reserve(size);        for (size_t i = 0; i < size; i++)        {            _a.push_back(a[i]);        }        for (int j = (_a.size() - 2) / 2; j >= 0; j--)        {            _AdjustDown(j);        }    }    void Pop()    {        assert(!_a.empty());        swap(_a[0], _a[_a.size() - 1]);        _a.pop_back();        _AdjustDown(0);    }    void Push(const T& d)    {        _a.push_back(d);        _AdjustUp(_a.size() - 1);    }    const T& Top()    {        assert(!_a.empty());        return _a[0];    }    size_t Size()    {        return _a.size();    }    bool empty()    {        return _a.empty();    }protected:    void _AdjustDown(int root)    {        assert(!_a.empty());        size_t parent = root;        size_t child = parent * 2 + 1;        while (parent<_a.size())        {            Compare com;            if (child + 1<_a.size()                && com(_a[child + 1], _a[child]))            {                ++child;            }            if (child<_a.size() && com(_a[child], _a[parent]))            {                std::swap(_a[child], _a[parent]);                parent = child;                child = parent * 2 + 1;            }            else            {                break;            }        }    }    void _AdjustUp(size_t child)    {        assert(!_a.empty());        while (child>0)        {            Compare com;            size_t parent = (child - 1) / 2;            if (com(_a[child], _a[parent]))            {                std::swap(_a[child], _a[parent]);                child = parent;            }            else            {                break;            }        }    }protected:    std::vector<T > _a;};

2.堆排序


堆的一个重要的应用就是堆排序。堆排序是一种选择排序,是利用堆这种二叉树的性质进行的排序。

初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。

堆排序的重要的思路就是当排升序的时候需要用到大堆,当拍降序的时候需要用到小堆

所以堆排序需解决两个问题:
1. 如何将n 个待排序的数建成堆;
2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。

例如:

int arr[] = { 5,6,3,2,1,4 };

这里写图片描述

2.1堆排序之建堆


在这里,我们就可仿照前面的思路,把这排序的N个数建一个堆,然后执行向下调整算法。

    //建堆    for (int i = (size - 2) / 2; i >= 0; i--)    {        AdjustDown(arr, i,size);    }

这里写图片描述

2.2堆排序之排序


建完堆以后,这个时候我们在这个堆中进行排序就好了,排序的思想就是把第一个节点和最后一个节点进行交换,然后对除了最后一个节点之外的进行向下调整。这个就是让最后一个数作为有序序列,然后其他作为无序序列,从无序序列中招出需要的有序数,进行继续的排序,所以说堆排序就是选择排序。

    //排序算法    size_t end = size - 1;    while (end > 0)    {        swap(arr[0], arr[end]);        AdjustDown(arr, 0, end);        end--;    }

这里写图片描述
完整堆排序代码:

#include<iostream>#include<cassert>#include<cstdlib>using namespace std;template<typename T>void AdjustDown(T* arr, size_t root,size_t count){    assert(arr);    size_t parent = root;    size_t child = parent * 2 + 1;    while (parent<count)    {        if (child + 1 < count&&arr[child + 1]<arr[child])        {            ++child;        }        if (child<count&&arr[child] < arr[parent])        {            std::swap(arr[child], arr[parent]);            parent = child;            child = parent * 2 + 1;        }        else         {            break;        }    }}template<typename T>void HeapSort(T* arr, size_t size){    assert(arr);    for (int i = (size - 2) / 2; i >= 0; i--)    {        AdjustDown(arr, i,size);    }    size_t end = size - 1;    while (end > 0)    {        swap(arr[0], arr[end]);        AdjustDown(arr, 0, end);        end--;    }}void test1(){    int arr[] = { 5,6,3,2,1,4 };    HeapSort(arr, sizeof(arr) / sizeof(arr[0]));}int main(){    test1();    system("pause");    return 0;}

接下来是两道关于堆的应用。

3.N个数中找出K个最大的值


这道题就是相关海量数据的一道题。我们利用堆来解决这一道题,首先我们来构建K个数的堆,然后调整这个堆,最后把剩下的数拿出来和这个堆中的数进行比较调整。

在这里要注意的是找最大的值用小堆,找最小的值用大堆
因为核心思想是你剩下的N-K个数和堆顶中的最值相比较,然后调整。

示例代码:

#define _CRT_SECURE_NO_WARNINGS 1#define N 10000#define K 10#include<iostream>#include<cstdlib>#include<cassert>using namespace std;template<typename T>void Adjustdown(T *top, size_t root){    assert(root < K);    size_t parent = root;    size_t child = parent * 2 + 1;    while (parent<K)    {        if (child + 1 < K&&top[child + 1] < top[child])        {            child++;        }        if (child<K&&top[child]<top[parent])        {            std::swap(top[child], top[parent]);            parent = child;            child = parent * 2 + 1;        }        else            break;    }}template<typename T>void GetKNum(T *arr, T *top){    assert(K < N);    for (size_t i = 0; i < K; i++)    {        top[i] = arr[i];    }    for (int j = (K - 2) / 2; j >= 0; j--)    {        Adjustdown(top, j);    }    for (size_t i = K; i < N; i++)    {        if (arr[i]>top[0])        {            std::swap(arr[i], top[0]);            Adjustdown(top, 0);        }    }}template<typename T>void print(T *top){    for (size_t i=0; i < K; i++)    {        cout << top[i] << " ";    }    cout << endl;}void test1(){    int arr[N] = { 0 };    int top[K] = { 0 };    for (size_t i = 0; i < N; i++)    {        arr[i] = i;    }    GetKNum(arr, top);    print(top);}int main(){    test1();    system("pause");    return 0;}

最后计算一下时间复杂度:
建K个数的堆:KlogK
进行N-K个数的比较操作调整:(N-K)logK
所以最后的时间复杂度:O(N*logK)

4.优先级队列


堆的另外一个应用就是优先级队列,我们可以用堆来创建对象来管理这个优先级队列。

对于优先级通过给的模板参数来控制。

然后这样就可以根据优先级进行调整出你想要的。

#pragma once#include"heap.h"#include<iostream>#include<cstdlib>#include<cassert>using namespace std;template<typename T,typename Compare=Greater<T>>class PriorityQueue{public:    PriorityQueue(T* a,size_t size)        :_h(a,size)    {    }    void Push(const T& d)    {        _h.Push(d);    }    void Pop()    {        _h.pop();    }    size_t Size()    {        return _h.Size();    }    const T& Top()    {        return _h.Top();    }    bool empty()    {        return _h.empty();    }protected:    Heap<T, Compare> _h;};
2 0
原创粉丝点击