四方显神

导航

数据结构017_树的应用(堆排序)

一、堆排序基本介绍

1)堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序也是一种选择排序,它的最坏、最好、平均时间复杂度都是O(nlogn),不稳定排序。

2)堆是完全二叉树(完全二叉树概念可以参考上一篇,我自己都忘记,又回头看)。大顶堆:每个结点的值都大于或者等于其左右孩子结点的值。小顶堆:每个结点的值都小于或者等于其左右孩子结点的值。注意,这里没有要求结点左孩子和右孩子的值的大小关系。

3)它使用顺序存储,arr作为存储堆的数组,大顶堆特点:arr[i]>=arr[2*i+1] && arr[i]>=arr[2*i+2]。(i对应第几个结点,从0开始编号)小顶堆也是这么个意思,我不写啦。

4)一般升序采用大顶堆,降序采用小顶堆。【为什么?】【这里我搜到的原因是如果使用小顶堆进行升序排序,当取出最小的堆顶元素,小顶堆的性质就变了,找不到第二小的元素了,还要重新建堆。这里我很疑惑,使用大顶堆取出最大的堆顶元素,也依旧要重新调整堆啊,这里还是不懂,等下面写完代码看看有什么新的体会吧,自己试试用小顶堆排升序最好】

二、堆排序基本思想

以大顶堆排升序为例:

1)将排序序列构造成大顶堆

2)此时整个序列的最大值就是堆顶的根节点

3)将其与末尾元素进行交换,此时末尾就是最大值

4)然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次大值。如此反复执行,就能得到一个有序序列。

 

三、图例

步骤一.构造大顶堆(升序一般采用大顶堆,降序小顶堆)

1)假定给定无序序列结构如下:

 

2)此时我们从最后一个非叶子结点开始(叶子结点不用调整,第一个非叶子节点arr.length/2-1=5/2-1=1,也就是结点6)【这又是为什么,公式来源】从左到右,从下至上进行调整。6、5、9结点调整成大顶堆。

 

 

 3)找到第二个非叶子节点4,将4、9、8结点调整成大顶堆结构,4和9交换。

 

4)这时,交换导致子根[4,5,6]结构混乱,继续调整,交换4和6。

 

 

 

 

 

 5)这时,我们就将一个无序序列构造成一个大顶堆。

步骤二.将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整,再将堆顶元素与末尾元素交换。得到第二大元素。如此反复进行交换、重建、交换。

 1)将堆顶元素9和末尾元素4交换

 

2)重新调整结构,使其继续满足堆定义

 

 

 3)再将堆顶元素8与末尾元素5进行交换,得到第二大元素8

 

 

 4)后续过程继续进行调整,交换,反复进行,最终整个序列有序

 

 

 四、代码实现

 

package com.njcx.tree;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

import javax.xml.crypto.Data;

public class HeapSort {

    // 要求将数组进行升序排序
    public static void main(String[] args) {
        // int[] arr = { 4, 6, 8, 5, 9 };

        // 堆排序的速度非常快,时间复杂度O(nlogn),测试八百万个数据的排序时间,才两秒,很厉害
        // 创建要给80000个随机的数组
        int[] arr = new int[8000000];
        for (int i = 0; i < 8000000; i++) {
            arr[i] = (int) (Math.random() * 8000000);
        }
        System.out.println("排序前");
        Date date1 = new Date();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-mm-dd hh:mm:ss");
        String dateStr = simpleDateFormat.format(date1);
        System.out.println("排序前的时间是:" + dateStr);

        heapSort(arr);
        Date date2 = new Date();
        String date2Str = simpleDateFormat.format(date2);
        System.out.println("排序后的时间是:" + date2Str);
        System.out.println("");

        // System.out.println("排序后的数组"+Arrays.toString(arr));
        // //小数据量测试一下对不对再进行时间测试

    }

    /**
     * 堆排序的方法
     * 
     * @param arr
     */
    public static void heapSort(int[] arr) {
        System.out.println("堆排序");
        int temp = 0;

        // 1.创造大顶堆
        for (int i = arr.length / 2 - 1; i >= 0; i--) {
            adjustHeap(arr, i, arr.length);
        }

        // 2.将大顶堆堆顶元素放在数组最末尾,对其余元素继续调整大顶堆,继续放
        for (int j = arr.length - 1; j > 0; j--) {
            temp = arr[j];
            arr[j] = arr[0];
            arr[0] = temp;

            adjustHeap(arr, 0, j); // 这里我不是很能理解为什么形参i传的是0
        }

        // System.out.println("堆排序的数组是" + Arrays.toString(arr));//测试时间的时候就不要打印了
    }

    /**
     * 将一个数组(二叉树)调整成一个大顶堆 举例:arr={4,6,8,5,9} → i=1 ->adjustHeap →
     * arr={4,9,8,5,6}; 如果我们再次调用adjustHeap,传入的是i=0 → {4,9,8,5,6} → {9,6,8,5,4}
     * 
     * @param arr
     *            待调整的数组
     * @param i
     *            表示非叶子结点在数组中的索引
     * @param length
     *            表示有多少个元素继续调整,length是在主键减小
     */
    public static void adjustHeap(int[] arr, int i, int length) {

        int temp = arr[i];// 先取出当前元素的值保存临时变量

        // 开始调整
        // 1.k代表的是以i为非叶子结点的左子结点
        for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
            if (k + 1 < length && arr[k] < arr[k + 1]) // 说明i结点的左子结点的值小于右子结点的值、
                k++;// k指向右子结点
            if (arr[k] > temp) { // 如果子结点大于父节点,
                arr[i] = arr[k]; // 把较大的值赋给当前结点
                i = k; // 让i指向k继续循环比较【!!!非常重要的一步】
            } else {
                break; // 【这里特别不好理解,因为我们调整的时候是从左到右从上到下,i下的都调整过了】
            }
        }
        // 当for循环结束后,我们已经将以i为父节点的树的最大值放在了i原先的位置上。这里是局部大顶堆
        arr[i] = temp;// 将temp值放在调整后的位置
    }

}

 

posted on 2020-10-28 20:01  szdbjooo  阅读(208)  评论(0编辑  收藏  举报