09堆基础之堆的插入、删除操作及用堆实现排序

在讨论堆的两个基本操作之前,我们首先要弄清楚什么是堆。堆是树的一种,什么样的树可以称之为堆?需要满足以下两个条件:

  • 堆是一个完全二叉树
  • 堆中任意一个节点的值都必须大于等于(或者小于等于)其子树每个节点的值

1. 往堆中插入一个元素(也是建堆的过程)

从下往上的方法实现(大顶堆):

  • 主要的时间消耗在于堆化操作,所以时间复杂度为o(logn)
  • 空间复杂度为o(1)
public class Heap{
    int []a;  //存储着一个堆,从下标1开始存储数据
    int n;    //堆可以存储的元素最大个数
    int count; //现有的堆中元素个数

    public Heap(int capacity){
        this.a = new int[capacity];
        this.n = capacity
        this.count = 0;
    };

    public void insert(int value){
        if(count >= n) return;  //表示堆满
        a[++count] = value;
        int i = count;
        while(i/2 > 0 && a[i] > a[i/2]){  //不断与跟节点对比,若大于根节点,则向上替换
            swap(a, i, i/2);
            i = i/2;
        } 
    };
    
    public void swap(int[]a, int i, int j){
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    };
}

2. 从堆中删除堆顶元素

从堆需要满足的第二个条件,我们可以得知,堆顶元素要么是最大值,要么是最小值。删除堆顶元素,也就是删除堆中的最小值或者最大值。为了维持堆的特性,我们可以采取以下措施:

将堆中的最后一个元素放在堆顶,然后从上往下进行调节,如果父子节点不满足大顶堆(或小顶堆)的定义,则继续往下,直至满足条件,具体代码实现如下:

  • 主要的时间消耗在于堆化操作,所以时间复杂度为o(logn)
  • 空间复杂度为o(1)
class Solution{
    public void removeMax(int[] a, int count){
        a[1] = a[count--];   //将根节点的值替换为大顶堆的最后一个值,并将count--
        heapify(a, count, 1);
    };
    public void heapify(int[]a, int count, int i){
        while(true){
            int maxChild = i;
            if(2*i <= count && a[2*i] > a[i]){    //若左子节点大于根
                maxChild = 2*i;
            }
            if((2*i+1) <= count && a[2*i+1] > a[i]){  //若右子节点大于根
                maxChild = 2*i + 1;
            }
            if(i==maxChild){                     //子节点的值都小于根时,已经调整完成
                break;
            }
            swap(a, i, maxChild);
            i = maxChild;                         //继续调整过程
        }
    };
    public void swap(int[]a, int i, int j){
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    };
}

3. 堆排序的过程

堆排序需要两个步骤:

  • 建堆,采用从下往上的步骤建堆,其时间复杂度为o(n)(其推导过程可以使用递归树进行分析)0,空间复杂度为o(1)。
  • 排序,输出根节点,再重新建堆的过程。时间复杂度为o(nlogn),空间复杂度为o(1)
  • 堆排序的过程中存在堆的最后一个元素和交换堆顶元素的过程,这有可能会改变相等的元素的相对顺序,所以堆排序并不是稳定的
class Solution{
    public void sort(int[] a,int count){
        buildHeap(a, count);   //建堆过程
        //排序过程
        int k = count;
        while(k > 0){
            swap(a, k--, 1);   //将最大值置于数组末尾,并将待排序数组长度减1
            heapify(a, k, 1);  //恢复大顶堆
        }
    };
    
    public void buildHeap(int[]a, count){
        for(int i = count/2; i>0; i--){
            heapify(a,  count, i);           //类似于删根节点后,将最后一个节点拿到第一个节点,并向下调整的过程,由于完全二叉树的性质,只需要从非叶子节点开始调整
        }
    }
    
    public void heapify(int[]a, int count, int i){
        while(true){
            int maxChild = i;
            if(2*i < count && a[i] < a[2*i]){
                maxChild = 2*i;
            } 
            if((2*i+1) < count && a[i] < a[2*i+1]){
                maxChild = 2*i +1;
            }
            if(maxChild == i)
                break;
            i = maxChild;
        }
    };

    public void swap(int[]a, int i, int j){
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    };
}

4. 思考:堆排序和快速排序谁更适合于实际的应用,换句话说堆排序和快排谁的性能更好?

从时间复杂度上来说,堆排序的时间复杂度稳定在o(nlogn),快速排序在最坏情况下时间复杂度会从o(nlogn)退化到o(n^2),仅仅从时间复杂度的角度考虑,似乎堆排序要优于快速排序,事实上是这样吗?

其实实际应用中,快排的性能是要比堆排序更好的,原因如下:

  • 堆排序的数据访问方式不如快排友好。快排的数据访问是局部顺序的,而堆化后的数据访问是跳跃式的,这对cpu的缓存是不友好的。
  • 同样的数据,堆排序中数据需要交换的次数要多于快速排序。主要原因在于,建堆的过程中会将原本有序的数组变得杂乱无序,数组本身的有序程度在建堆时反倒是下降了。
  • 快排性能退化到o(nlogn)是小概率事件,并且我们还有随机法、三取数法等方法减少快排性能退化的概率,所以实际应用中在基于比较的排序算法里面,快排是非常高效的。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值