面试官说,你会堆排序吗?会,那好手写一个吧。

前言

最近明显文章更新频率下降了,那是由于我在恶补数据结构和算法的相关知识,至关因而从零开始学习。算法

找了不少视频和资料,最后发现 b 站尚硅谷的视频教程仍是相对不错的,总共 195 集。每一个小节都是按先概念、原理,而后代码实现的步骤讲解。若是你也准备入门数据结构和算法,我推荐能够看下这个系列教程。数组

昨天一天一会儿肝了 40 多集,从树的后半部分到图的所有部分。能够看到,每一集其实时间也不算长,短的几分钟,长的也就半个小时。开 2 倍速看,倍儿爽。数据结构

话很少说,下面进入正题。数据结构和算法

二叉堆介绍

咱们知道,树有不少种,最经常使用的就是二叉树了。二叉树又有满二叉树和彻底二叉树。而二叉堆,就是基于彻底二叉树的一种数据结构。它有如下两个特性。学习

  1. 首先它是一个彻底二叉树
  2. 其次,堆中的任意一个父节点的值都大于等于(或小于)它的左右孩子节点。

所以,根据第二个特性,就把二叉堆分为大顶堆(或叫最大堆),和小顶堆(或叫最小堆)。code

顾名思义,大顶堆,就是父节点大于等于左右孩子节点的堆,小顶堆就是父节点小于左右孩子节点的堆。视频

看一下大顶堆的示例图,小顶堆相似,只不过是小值在上而已。blog

注意:大顶堆只保证父节点大于左右孩子节点的值,不须要保证左右孩子节点之间的大小顺序。如图中,7 的左子节点 6 比右子节点 1 大,而 8 的左子节点 4 却比右子节点 5 小。(小顶堆同理)排序

构建二叉堆

二叉堆的定义咱们知道了,那么给你一个无序的彻底二叉树,怎么把它构建成二叉堆呢?教程

咱们以大顶堆为例。给定如下一个数组,(彻底二叉树通常用数组来存储)

{4, 1, 9, 3, 7, 8, 5, 6, 2}

咱们画出它的初始状态,而后分析怎么一步一步构建成大顶堆。

因为大顶堆,父节点的值都大于左右孩子节点,因此树的根节点确定是全部节点中值最大的。所以,咱们须要从树的最后一层开始,逐渐的把大值向上调整(左右孩子节点中较大的节点和父节点交换),直到第一层。

其实,更具体的说,应该是从下面的非叶子节点开始调整。想想,为何。

反向思考一下,若是从第一层开始调整的话,例如图中就是 4 和 9 交换位置以后,你不能保证 9 就是全部节点的最大值(额,图中的例子可能不是太好,正好是 9 最大)。若是下边还有比 9 大的数字,你最终仍是须要从下面向上遍历调整。那么,我还不如一开始就直接从下向上调整呢。

另外,为何从从最下面的非叶子节点(图中节点 3 )开始。由于叶子节点的下面已经没有子节点了,它只能和父节点比较,从叶子节点开始没有意义。

第一步,以 3 为父节点开始,比较他们的子节点 6和 2 ,6最大,而后和 3 交换位置。

①

第二步,6 和 7 比较,7 最大,7 和 1 交换位置。

②

第三步,7 和 9 比较,9 最大,9 和 4 交换位置。

③

第四步,咱们发现交换位置以后,4 下边还有比它大的,所以还须要以 4 为父节点和它的左右子节点进行比较。发现 8 最大,而后 8 和 4 交换位置。

④

最终,实现了一个大顶堆的构建。下面以代码实现交换过程。

/**
 * 调整为大顶堆
 * @param arr   待调整的数组
 * @param parent   当前父节点的下标
 * @param length   须要对多少个元素进行调整
 */
private static void adjustHeap(int[] arr, int parent, int length){
    //临时保存父节点
    int temp = arr[parent];
    //左子节点的下标
    int child = 2 * parent + 1;
	//若是子节点的下标大于等于当前须要比较的元素个数,则结束循环
    while(child < length){
        //判断左子节点和右子节点的大小,若右边大,则把child定位到右边
        if(child + 1 < length && arr[child] < arr[child + 1]){
            child ++;
        }
        //若child大于父节点,则交换位置,不然退出循环
        if(arr[child] > temp){
            //父子节点交换位置
            arr[parent] = arr[child];
            //由于交换位置以后,不能保证当前的子节点是它子树的最大值,因此须要继续向下比较,
            //把当前子节点设置为下次循环的父节点,同时,找到它的左子节点,继续下次循环
            parent = child;
            child = 2 * parent + 1;
        }else{
            //若是当前子节点小于等于父节点,则说明此时的父节点已是最大值了,
            //所以无需继续循环
            break;
        }
    }
    //把当前节点值替换为最开始暂存的父节点值
    arr[parent] = temp;
}

public static void main(String[] args) {
    int[] arr = {4,1,9,3,7,8,5,6,2};
    //构建一个大顶堆,从最下面的非叶子节点开始向上遍历
    for (int i = arr.length/2 - 1 ; i >= 0; i--) {
        adjustHeap(arr,i,arr.length);
    }
    System.out.println(Arrays.toString(arr));
}	 
//打印结果:  [9, 7, 8, 6, 1, 4, 5, 3, 2]。 和咱们分析的结果如出一辙

在 while 循环中,if(arr[child] > temp) else的逻辑, 对应的就是图中的第三步和第四步。即须要确保,交换后的子节点要比它下边的孩子节点都大,否则须要继续循环,调整位置。

堆排序

堆排序就是利用大顶堆或者小顶堆的特性来进行排序的。

它的基本思想就是:

  1. 把当前数组构建成一个大顶堆。
  2. 此时,根节点确定是全部节点中最大的值,让它和末尾元素交换位置,则最后一个元素就是最大值。
  3. 把剩余的 n - 1个元素从新构建成一个大顶堆,就会获得 n-1 个元素中的最大值。重复执行此动做,就会把全部的元素调整为有序了。

步骤:

仍是以上边的数组为例,看一下堆排序的过程。

一共有九个元素,把它调整为大顶堆,而后把堆顶元素 9 和末尾元素 2 交换位置。

此时,9已经有序了,不须要调整。而后把剩余八个元素调整为大顶堆,再把这八个元素的堆顶元素和末尾元素交换位置,以下,8 和 3 交换位置。

此时,8和 9 已经有序了,不须要调整。而后把剩余七个元素调整为大顶堆,再把这七个元素的堆顶元素和末尾元素交换位置。以下, 7 和 2 交换位置。

以此类推,通过 n - 1 次循环调整,到了最后只剩下一个元素的时候,就不须要再比较了,由于它已是最小值了。

看起来好像过程很复杂,但实际上是很是高效的。没有增删,直接在原来的数组上修改就能够。由于咱们知道数组的增删是比较慢的,每次删除,插入元素,都要移动数组后边的 n 个元素。此外,也不占用额外的空间。

代码实现:

//堆排序,大顶堆,升序
private static void heapSort(int[] arr){
    //构建一个大顶堆,从最下面的非叶子节点开始向上遍历
    for (int i = arr.length/2 - 1 ; i >= 0; i--) {
        adjustHeap(arr,i,arr.length);
    }
    System.out.println(Arrays.toString(arr));
    //循环执行如下操做:1.交换堆顶元素和末尾元素 2.从新调整为大顶堆
    for (int i = arr.length - 1; i > 0; i--) {
        //将堆顶最大的元素与末尾元素互换,则数组中最后的元素变为最大值
        int temp = arr[i];
        arr[i] = arr[0];
        arr[0] = temp;
        //从堆顶开始从新调整结构,使之成为大顶堆
        // i表明当前数组须要调整的元素个数,是逐渐递减的
        adjustHeap(arr,0,i);
    }

}

时间复杂度和空间复杂度:

堆排序,每次调整为大顶堆的时间复杂度为 O(logn),而 n 个元素,总共须要循环调整 n-1 次 ,因此堆排序的时间复杂度就是 O(nlogn)。它的数学推导比较复杂,感兴趣的同窗能够本身查看相关资料。

因为没有占用额外的内存空间,所以,堆排序的空间复杂度为 O(1)。

相关文章
相关标签/搜索