数据结构——堆,小顶堆,大顶堆,堆排序

1.简介

 堆是什么?堆是一种特殊的完全二叉树,就像下面这棵树一样:

这棵树有一个很显著的特点,那就是所有父结点都要比子结点要小。符合这样要求的完全二叉树我们成为“最小堆”。反之,如果所有父结点都要比子结点大,这样的完全二叉树被称为”最大堆“。

2.下移ShiftDown

如果我们现在要删除掉最小的数字,并重新插入一个数字,再从中找出最小的数字。目前只能够先扫描所有的数字,找到最小的数,插入新的数字后再扫描所有的数字。这样子的时间复杂复是O(N²),那么有没有更好的方法呢?”堆“的结构恰好能够很好地解决了这个问题。

首先将数字按照最小堆的要求放进一颗完全二叉树,就像下面这棵树一样。然后用数组h存储这些数字。

很显然最小的数就在堆顶,假设存储这个堆的数组是h,那么最小的数字就是h[1]。接下来将堆顶的数字删除。将新增的数字28放入堆顶。此时显然不满足小顶堆的特性,我们需要将新增的数字调整到合适的位置。如何调整呢?

向下调整!我们需要将这个数字与它的两个儿子2和5比较,选择较小的一个和它交换,交换之后如下:

我们发现此时还是不符合最小堆的特性,因此还需要继续向下调整。于是继续将28与两个儿子12和7比较,选择较小的一个进行交换

此时还是不满足,继续向下调整:

现在我们已经找到符合最小堆的特性了。综上所述,当新增加一个数被放到顶堆时候,如果发现不符合最小堆的特性,就必须要向下调整,直到找到合适的位置位置。使其重新符合最小堆的特性。

向下调整的代码如下:

void shiftdown(int i)
{
    int tmp; //临时变量,用来存储更新结点编号
    int flag=0;//用来标记此顶点需不需要向下调整,0是需要调整,1是不需要
    while(i*2<=n && flag==0)//当结点存在儿子时,至少存在左儿子,执行循环
    {
        //首先判断与左儿子的辨析,用tmp记录值较小的结点编号
        if(h[i]>h[i*2])
        {
            tmp=i*2;
        }
        else
        {
            tmp=i;
        }
        if(i*2+1<=n) //如果有右儿子,再对右儿子进行讨论
        {
            if(h[i*2+1]<h[i])
            {
                tmp=i*2+1;
            }
        }
        if(tmp!=i) //如果发现最小的结点不是自己,说明子结点中还有值更小的结点
        {
            swap(tmp,i);
            i=tmp; //更新刚才交换之后的儿子结点的编号,便于接下来向下调整。
        }
    }
    return;
}

3. 上移ShiftUp

如果只是想新增一个值,而不是删除最小值呢?即在原有的堆上面直接插入一个新元素。其实只需要在堆尾直接插入,再根据情况判断是否需要将新元素上移,直到满足最小堆的特性位置。例如我们现在要新增一个元素3:

 

先将3和它的父节点25比较,发现比父结点要小,交换。

交换之后发现还是比此时的父结点6小,再进行交换。此时满足了最小堆的特性。

向上调整的代码如下:

void shiftup(int i)
{
    int flag=0;
    if(i==1)
        return;//此时是堆顶,不需要上调
    while(i!=1 && flag=0)
    {
        if(h[i]<h[i/2])
        {
            swap(i,i*2);
        }
        else
        {
            flag=1;
        }
        i=i/2;//更新编号i为它父结点的编号。
    }
    return;
}

4.建堆 

 说了半天,我们忽略了一个很重要的问题,就是如何建立这个堆。我们可以把所有数值直接放入一个完全二叉树中,并且用一个数组存储:

在这课完全二叉树中,我们从最有一个结点开始,依次判断以这个结点为根的子树是否符合最小堆的特性。如果所有的子树都符合最小堆的特性,那么这个就是最小堆了。

首先我们从叶结点开始。因为叶结点没有儿子,所以所有以叶结点为根结点的子树都符合最小堆的特性。因此所有叶结点可以不处理,直接跳过。

完全二叉树有一个很重要的性质:最有一个非叶结点是第n/2个结点。

利用这个性质,我们从第n/2个结点开始处理这颗完全二叉树。

下面是已经调整好的状态:

目前这棵树还是不符合最小堆的特性,我们继续调整:

调整完毕后:

实现的部分代码:

void create()
{
    for(int i=n/2;i>=1;i--)
    {
        shiftdown(i);
    }
    return ;
}

5.堆排序

 堆还有一个作用就是堆排序,与快速排序一样,堆排序的时间复杂度是O(NlogN)。

堆排序的实现很简单,比如说我们现在要进行从小到大的排序,可以先建立最小堆,然后每次删除顶部元素并将顶部元素输出或者放入一个新的数组中,直到堆为空为止。最终输出或者存放在新数组中的数就已经是排序好了的。

int deletemin() //删除方法的堆排序
{
    int tmp;
    tmp=h[1];//临时记录第一个结点的值
    h[1]=h[n];//将最大的值放到第一个位置。
    n--;//将堆的大小-1
    shiftdown(1);
    return tmp;//返回最小值。
}

当然堆排序还有一种更好的办法。从小到大排序的时候不建立”最小堆“而是建立”最大堆“!

最大堆建立好之后,最大的元素是h[1],因为我们需要从小到大排序,希望最大的数放在最后,那么我们将h[1]和h[n]交换,此时h[n]就是数组中最大的元素。最大的元素归位后,将堆的大小减1,n--,并将交换后的新h[1]向下调整维持特性。如此递归,直到堆的大小是1位置。此时数组h已经是排好序了。

void HeapSort() //交换思维的堆排序
{
    while(n>1)
    {
        swap(1,n); //将最小的元素放到堆顶,最大的元素放到h[n],此时h[n]就是最大的元素。
        n--; //将堆的大小减小1
        shiftdown(1); //向下调整
    }
}

6.源代码 

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
using namespace std;

int h[1111]; //用来存储二叉树的数组。
int n;//堆的大小

void swap(int x,int y)
{
    int temp;
    temp=h[x];
    h[x]=h[y];
    h[y]=temp;
    return;
}

void shiftdown(int i)
{
    int tmp; //临时变量,用来存储更新结点编号
    int flag=0;//用来标记此顶点需不需要向下调整,0是需要调整,1是不需要
    while(i*2<=n && flag==0)//当结点存在儿子时,至少存在左儿子,执行循环
    {
        //首先判断与左儿子的辨析,用tmp记录值较小的结点编号
        if(h[i]<h[i*2])
        {
            tmp=i*2; //用tmp记录较大的结点编号
        }
        else
        {
            tmp=i;
        }
        if(i*2+1<=n) //如果有右儿子,再对右儿子进行讨论
        {
            if(h[i*2+1]<h[tmp])
            {
                tmp=i*2+1;
            }
        }
        if(tmp!=i) //如果发现最小的结点不是自己,说明子结点中还有值更小的结点
        {
            swap(tmp,i);
            i=tmp; //更新刚才交换之后的儿子结点的编号,便于接下来向下调整。
        }
    }
    return;
}

void shiftup(int i)
{
    int flag=0;
    if(i==1)
        return;//此时是堆顶,不需要上调
    while(i!=1 && flag==0)
    {
        if(h[i]<h[i/2])
        {
            swap(i,i*2);
        }
        else
        {
            flag=1;
        }
        i=i/2;//更新编号i为它父结点的编号。
    }
    return;
}

void create()
{
    for(int i=n/2; i>=1; i--)
    {
        shiftdown(i);
    }
    return ;
}

void HeapSort() //交换思维的堆排序
{
    while(n>1)
    {
        swap(1,n); //将最小的元素放到堆顶,最大的元素放到h[n],此时h[n]就是最大的元素。
        n--; //将堆的大小减小1
        shiftdown(1); //向下调整
    }
}

int main()
{
    int num;
    cout << "请输入元素的个数:"<< endl;
    cin >> num;
    cout << "请输入各结点的元素:" << endl;
    for (int i=1;i<=num;i++)
    {
        cin >> h[i];
    }
    n=num;
    create();//建堆
    HeapSort();//排序
    for (int i=1;i<=num;i++)
    {
        cout << h[i];
    }
    cout << endl;
    return 0;
}
发布了57 篇原创文章 · 获赞 9 · 访问量 3625

猜你喜欢

转载自blog.csdn.net/Jayphone17/article/details/102666274