堆的基本操作与堆排序(C/C++实现)

原理参考:堆和堆排序原理介绍

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

  • 基本数组的定义

int n; //数组元素的个数
int heap[100005];  //堆数组
  • 向下调整操作

向下调整操作一般是针对一个节点而言的,通过对其进行向下调整,保证其值是其所在的二叉树中的最小的数。

//对输入的heap数组在[low, high]范围内向下调整,调整为最小堆,即小的数在最上面 
void downHeap(int low, int high){
    //i初始化为欲调整节点,j为其左孩子 
    int i = low, j = i * 2;
    //如果还存在孩子节点则一直比较并调整 
    while(j <= high){
        //如果右孩子存在且值比左孩子还小,那么就应该将右孩子向上调整 
        if(j + 1 <= high && heap[j+1] < heap[j])
            j = j + 1;
        //当孩子的值比欲调整节点大时,则进行交换,保证小的数在上面 
        if(heap[j] < heap[i]){
            swap(heap[j], heap[i]);
            //继续向下调整 
            i = j;
            j = j * 2;
        }
        //如果欲调整节点的值已经比孩子都小则结束调整 
        else
            break;
    }
}
  • 建堆操作

建堆操作则利用向下调整的方法对堆数组的每一个元素进行向下调整,从而使得每一个节点都是以其为根结点的树中的最小元素。而通过二叉树我们可以知道,叶子节点是没有孩子的,因此对于叶子节点可以不用再进行向下调整,因此第一个调整的节点则是最后一个元素的父节点,因为它是树中最后一个还拥有孩子的节点。实现代码如下:

//建堆操作 
void createHeap(){
    //建堆从最后一个还具有孩子的节点开始,依次往前遍历到根结点,到最后便建立了最小堆 
    for(int i = n / 2; i >= 1; i--){
        downHeap(i, n);
    }
}
  • 删除堆顶元素

对于堆的删除操作则一般只针对堆的堆顶元素,而一般不对其他元素进行删除。对堆顶元素进行删除时,只需要将堆的最后一个元素覆盖堆顶元素然后再进行一次向下调整即可满足。删除后的同时需要将堆的元素个数减一。

//删除堆顶元素 
void deleteHeap(){
    //将堆最后一个元素覆盖堆顶元素,再将元素个数n简易 
    heap[1] = heap[n--];
    //从堆顶到堆最后进行一次向下调整,保证删除后仍是最小堆 
    downHeap(1, n);
} 
  • 向堆插入一个元素

向堆中插入一个元素和删除一个元素则不同,因为程序预先是不知道这个元素应该放在什么位置的,而如果直接进行查找再添加则会更加的麻烦,体现不出堆的一个优势。如果说把它放在第一个元素位置利用向下调整,仍会带来很多麻烦,比如原本的堆顶元素又放到哪里,如果将原本的堆顶元素放到末尾,那岂不是不是最小堆了?或者将所有元素往后移动,将插入的元素放到第一个位置,再调整,这不明显更加复杂。
因此在插入一个元素时,堆中采用向上调整的方法,而不是向下调整,首先将插入的元素放入到堆数组的最后,并将元素个数加1,此时则对这插入的最后一个元素进行向上调整,直到其值比父亲节点大或者到达了根结点时退出。实现如下:

//对heap中[low,high]范围内进行向上调整 
void upHeap(int low, int high){
    //i初始化为欲调整节点,j为其父亲节点 
    int i = high, j = i / 2;
    //当未到达根结点时则继续操作 
    while(j >= low){
        //如果父亲节点仍然比欲调整节点的值大则继续向上,因为需要调整为最小堆 
        if(heap[j] > heap[i]){
            swap(heap[i], heap[j]);
            i = j;
            j = i / 2;
        }
        //比父亲节点值大,则达到退出条件 
        else
            break;
    }
}
//插入一个元素,利用向上调整方法 
void insert(int x){
    //将新插入的元素放到堆数组的最后,并将元素个数加1 
    heap[++n] = x;
    //放入后进行一次向上调整 
    upHeap(1, n);
}

堆排序(最小堆为例)

有了以上的堆的基本操作之后则可以进行堆排序了。对于堆排序,此处以最简单的为例。

首先对于堆数组,则是需要用户进行输入的,输入之后,便可以对每一个非叶子节点元素(从最后一个具有孩子的节点开始)进行一次向下调整,从而在遍历之后可以建立一个最小堆。不过这样之后的数组并不是有序的,因为我们只保证了当前节点的值比孩子结点的值小,而对于左右孩子结点的值是没有顺序的,同理的是左右子树都不是有序的,不像二叉搜索树那样左子树的值都是比右子树要小的。

因此在建立起最小堆之后,我们已知的是堆顶元素是最小的,因此可以依次将堆顶元素取出来,排成一个序列那便是有序的了。而对于每次取出的操作可以不必再新开一个数组,直接在原堆数组中进行操作,操作方法为:将堆顶元素和堆末尾的元素交换,然后针对前n-1个元素对交换后的堆顶元素进行一次向下调整,这样堆顶便是剩下的n-1个元素中的最小的元素,而该元素就是所有元素的第二小的元素。然后依次这样操作,第二次则将堆顶元素和n-1位置的元素交换再进行一次向下调整(因为n位置已经放了最小元素,第二次的n-1个元素中的最小元素则放在n-1位置)。这样当操作到只剩一个元素便可结束操作,得到的堆数组便是降序排列的,如果需要升序则逆序输出即可。示例代码如下:

#include
#include
#include
#include
#include
#include
#include
#include
#include


using namespace std;
int n;
int heap[100005]; 


//对输入的heap数组在[low, high]范围内向下调整,调整为最小堆,即小的数在最上面 
void downHeap(int low, int high){
    //i初始化为欲调整节点,j为其左孩子 
    int i = low, j = i * 2;
    //如果还存在孩子节点则一直比较并调整 
    while(j <= high){
        //如果右孩子存在且值比左孩子还小,那么就应该将右孩子向上调整 
        if(j + 1 <= high && heap[j+1] < heap[j])
            j = j + 1;
        //当孩子的值比欲调整节点大时,则进行交换,保证小的数在上面 
        if(heap[j] < heap[i]){
            swap(heap[j], heap[i]);
            //继续向下调整 
            i = j;
            j = j * 2;
        }
        //如果欲调整节点的值已经比孩子都小则结束调整 
        else
            break;
    }
}
//建堆操作 
void createHeap(){
    //建堆从最后一个还具有孩子的节点开始,依次往前遍历到根结点,到最后便建立了最小堆 
    for(int i = n / 2; i >= 1; i--){
        downHeap(i, n);
    }
}
//堆排序操作 
void heapSort(){
    //首先建立初始最小堆 
    createHeap();
    //从后遍历每一个元素,每次将最小元素放入到i位置,这样第一次最小放到n
    //第二次的最小则放入到n-1,第三次则会放入到n-2...... 
    for(int i = n; i > 1; i--){
        swap(heap[i], heap[1]);
        downHeap(1, i-1);
    }
}

//10
//2 8 4 6 1 10 7 3 5 9
int main() {
    while(scanf("%d", &n) != EOF) {
        for(int i = 1; i <= n; i++)
            scanf("%d", &heap[i]);
        heapSort();
        for(int i = n; i >= 1; i--)
            printf("%d ", heap[i]);
    }
    return 0;
}

练习题目:堆排序

你可能感兴趣的:(堆的基本操作与堆排序(C/C++实现))