【排序算法】堆排序

之前面试有被问到堆排序的特点,但几乎忘了很多,之后看了一些网上关于堆排序的博文,留意看了评论部分,发现各种博文里的代码最后都有一些Bug。于是自己实现了一波,现在以一种更简洁易懂的语言告诉你它的工作原理。

堆排序基础知识

父节点比子节点都大的叫大顶堆,比子节点都小的叫小顶堆。如图是个大顶堆:

虽然说是堆排序,而且要把它理解为完全二叉树,但你千万别直接就这样告诉给面试官,因为它是从一维数组映射出来的,这种构思非常巧妙,而且你的整个排序过程也不需要任何的二叉树结构。有如下几点你需要知道:

  1. 根节点对应data[0]。
  2. 子节点k的父节点是(k-1)/2。
  3. 父节点k的左孩子是2*k+1,右孩子是2*k+2。

好了,知道如上的知识你就可以写出如下的代码了:

    private static int getParent(int index) {
        return (index - 1) >>> 1;
    }

    private static int getLeft(int index) {
        return (index << 1) + 1;
    }

    private static int getRight(int index) {
        return (index << 1) + 2;
    }

堆排序过程

有了上面的知识铺垫,我们就可以实现堆排序了。这个过程分两个环节:

  1. 建堆。
  2. 输出顶部元素,调整堆,重复。

建堆

建堆过程只需要从最后一个非叶子节点开始从下往上遍历调整节点即可。我们以大顶堆为例,这个调整是指将父节点、左孩子、右孩子的值相比较,最大的交换到父节点。为何从下往上?因为这样可以把局部最大的元素一直向上传递,最后可以让堆顶得到最大的元素,而从上往下的话,堆顶元素很显然不一定是最大的,这就不符合大顶堆的要求了。

输出堆顶、调整堆

以大顶堆为例,建堆完成后,堆顶就是最大的元素了。把堆顶元素最后一个元素交换(我们把整个数组分为两个部分,有序区无序区),这最后一个元素便成了当前的有序区,而前面的n-1个元素就是无序区。下面只需要再对无序区进行调整,恢复到大顶堆的要求,就可以重复“取堆顶,交换堆尾”这个过程了。
由于会把堆尾的元素交换到堆顶,一般情况下堆尾是较小的,所以此时就违反了大顶堆的要求,需要进行调整。调整过程也很容易,只需要从堆顶开始,重复上面建堆中讲的调整方法,比较父节点、左孩子、右孩子,把最大的交换到父节点后,再检查被交换的左孩子或者右孩子,以此递推下去即可。
引用一个图片:

相信聪明的你一定能看懂了。

代码

原理搞懂,代码就容易了,如果你注意到了我上面讲的,堆排序的两个环节里其实有一个过程是公用的,就是调整一个节点,让父节点成为左、右孩子中最大的,那么应当为此单独写一个方法,方法代码如下:

    private static int adjust(int[] data, int parent, int length) {
        if (parent >= length) return -1;
        int left = getLeft(parent), right = getRight(parent), i = parent;
        if (left < length && data[left] > data[parent]) i = left;
        if (right < length && data[right] > data[i]) i = right;
        if (i == parent) return parent;
        int temp = data[i];
        data[i] = data[parent];
        data[parent] = temp;
        return i;
    }

参数可能比你想象的多了一个length,它是用来检查要访问的父节点、左孩子、右孩子是否已经超出范围了。你可能会想,直接用data.length不就可以了,但就和上面说的第2个环节一样,无序区的长度是逐渐缩小的,所以而调整的过程是不需要访问到有序区的,所以这个length就顺理成章成为一个参数了。这个访问越界的检查很容易被忽略,也是网上很多代码有bug的原因之一。
下面就是排序的逻辑代码了:

    //n表示排序的数量。如果只需要得到前n个最大的,则传递相应的n即可。
    public static void sort(int[] data, int n) {
        int i = getParent(data.length - 1), temp;
        //建立堆,只需要倒着从最后一个非叶子节点开始往上处理即可。最后顶部元素是符合堆的。
        while (i >= 0) {
            adjust(data, i, data.length);
            i--;
        }
        //开始堆排序。
        i = data.length - 1;
        while (i > n) {
            //交换顶部和底部元素
            temp = data[0];
            data[0] = data[i];
            data[i] = temp;
            i--;
            //恢复堆,只需要从上往下检查即可
            int parent = 0;
            while (parent <= getParent(i)) {
                int top = adjust(data, parent, i + 1);
                if (parent == top) break;
                else parent = top;
            }
        }
    }

如果看懂了上面的原理,这个代码应该不难理解。而堆排序最好玩的地方也显现出来了,看一下这个方法的参数n,表示要排序的数量,这个很有趣,不像其他的排序算法,如果你想要得到前k个最大的数时,他们往往要全部排序完才可以得到,亦或是冒泡那样用O(n)复杂度得到,但都没有堆排序来的快,堆排序建堆用大概n/2的复杂度(n是数据长度),建堆后每次得到一个新的最大或最小值只需要log n(忽略数据总数变化),如果要排序k个数,其复杂度也就是n/2+k*log n,还是蛮快的。

如果有同学对堆排序有更多补充,欢迎评论哈!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值