java实现排序(4)-堆排序

news/2024/7/7 20:01:04

引言

在上一篇博文中,尝试实现了二叉堆的结构。在本篇博文中,将建立在堆的基础之上,讨论如何用堆实现排序。二叉堆的代码直接引用昨天的实现源码,在代码的基础上做一些修改使其变成堆排序。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290

二叉堆源码

如果对二叉堆不是很了解,可以查阅相关资料,或者阅读一下我写的这篇文章《java实现-堆》,地址是:http://blog.csdn.net/u012403290/article/details/71126768

下面是上篇博文二叉堆的实现源码:


package com.brickworkers;

public class Heap<T extends Comparable<? super T>> {

    private static final int DEFAULT_CAPACITY = 10; //默认容量

    private T[] table; //用数组存储二叉堆

    private int size; //表示当前二叉堆中有多少数据

    public Heap(int capactiy){
        this.size = 0;//初始化二叉堆数据量
        table = (T[]) new Comparable[capactiy + 1];//+1是因为我们要空出下标为0的元素不存储
    }

    public Heap() {//显得专业些,你就要定义好构造器
        this(DEFAULT_CAPACITY);
    }


    //插入
    public void insert(T t){
        //先判断是否需要扩容
        if(size == table.length - 1){
            resize();
        }
        //开始插入
        //定义一个预插入位置下标
        int target = ++size;
        //循环比较父节点进行位置交换
        for(table[ 0 ] = t; t.compareTo(table[target/2]) < 0; target /= 2){
            table[target] = table[target/2];//如果满足条件,那么两者交换,知道找到合适位置(上滤)
        }
        //插入数据
        table[target] = t;
        print();

    }


    //删除最小
    //删除过程中,需要重新调整二叉堆(下滤)
    public void deleteMin(){
        if(size == 0){
            throw new IllegalAccessError("二叉堆为空");
        }

        //删除元素
        table[1] = table[size--];

        int target  = 1;//从顶部开始重新调整二叉堆

        int child;//要处理的节点下标
        T tmp = table[ target ];

        for( ; target * 2 <= size; target = child )
        {
            child = target * 2;
            if( child != size &&table[ child + 1 ].compareTo( table[ child ] ) < 0 ){//如果右孩子比左孩子小
                child++;
            }
            if( table[ child ].compareTo( tmp ) < 0 ){
                table[ target ] = table[ child ];
                table[child] = null;
            }
            else{
                 break;
            }
        }
        table[ target ] = tmp;

        print();
    }





    //如果插入数据导致达到数组上限,那么就需要扩容
    private void resize(){
          T [] old = table;
          table = (T [])  new Comparable[old.length*2 + 1];//把原来的数组扩大两倍
          for( int i = 0; i < old.length; i++ )
              table[ i ] = old[ i ];        //数组进行拷贝
    }


    //打印数组
    private void print(){
        System.out.println();
        for (int i = 1; i <= size; i++) {
            System.out.print(table[i] + " ");
        }
        System.out.println("二叉堆大小:"+size);
    }


    public static void main(String[] args) {
        Heap<Integer> heap = new Heap<>();

        //循环插入0~9的数据
        for (int i = 0; i < 10; i++) {
            heap.insert(i);
        }

        //循环删除3次,理论上是删除0,1,2 
        for (int i = 0; i < 3; i++) {
            heap.deleteMin();
        }
    }

}


思考

因为二叉堆中在堆顶,要么是最大值,要么是最小值。我们每次进行一次deleteMin(deleteMax)就可以把这个数据存储起来,一直到二叉堆删除结束。那么,我们考虑两个方面:①如何把数据放入二叉堆;②如何把排好序的数据提取出来。

对于第一个问题,如何把数据放入二叉堆,最简单的就是可以调用二叉堆的insert方法,把要排序的数据一个个放入其中,或者优雅一些,我们在源码中新增一个构造函数,直接入参一个数组,直接转化成二叉树。那或许有的小伙伴又要问了,这个优雅不优雅有必要吗?构造函数里面进行insert操作?不!我们不在构造函数中调用insert操作(但是在源码中,我们还是要进行比较),我们可以直接把所有数据全部插入,然后在把它整个调整成二叉堆。在后面的代码中,我们将比较这两者的优劣。

对于第二个问题,其实也有两种解决办法,第一种是我们创立一个新的数组,这个数组核心功能就是接收二叉堆删除的数据,这样一来,直到二叉堆删除结束,那么这个数组就是排序好了的数据,实现了数据存储和提取。第二种,就显得要高明一点点,我们试想,在二叉堆的实现过程中,我们本身就是用数组来实现的。而且,在上篇博文中,我们讨论它的删除的时候,最后一个元素的位置肯定会变动,所以,这种情况下,我们完全可以在二叉堆删除堆顶元素的时候,把删除的数据存储在最后一个元素的位置。这么一来在二叉堆删除结束的时候,那么这个原本表示二叉堆的数组已经是有序的了。

新增一个接收数组的构造函数

以下的所有代码,都是建立在上述的二叉堆源码上进行修改的。

1、接收数组的第一种方式:


    //实现方式一,直接用insert来实现数据放入二叉堆中
    public Heap(T[] array){
        super();
        for (T t : array) {
            insert(t);//直接调用二叉堆的insert方法进行插入
        }
    }

第二种方式,我们先把数据全部随意插入到数组中,然后把数组调整成二叉堆。如何调整呢?在上一篇博文中,我们在删除的情况下,我们需要通过下滤的方式从新调整二叉堆,其实思想是一样的,只是删除的时候调整从最上层开始,而在这里我们从最下层(标准来说是倒数第二层)开始。下图表达了操作过程:
这里写图片描述

在源码中,我把删除节点后面的重新调整二叉堆抽离了出来:

    private void dealHeap(int target) {
        int child;//要处理的节点下标
        T tmp = table[ target ];

        for( ; target * 2 <= size; target = child )
        {
            child = target * 2;
            if( child != size &&table[ child + 1 ].compareTo( table[ child ] ) < 0 ){//如果右孩子比左孩子小
                child++;
            }
            if( table[ child ].compareTo( tmp ) < 0 ){
                table[ target ] = table[ child ];
                table[child] = null;
            }
            else{
                 break;
            }
        }
        table[ target ] = tmp;
    }

2、下面就是第二种实现放置,把数据全部放进去然后调整二叉堆:

public Heap( T [ ] array )
    {
            size = array.length;
            array = (T[]) new Comparable[ size + 2 ];//存储的数组要略微长
            for (int i = 0; i < array.length; i++) {
                table[ i++ ] = array[i];
            }
            //到这里为止,数据已经全部都放入了table中
            //接下来要做的就是要把这些数据转变成二叉堆
            for(int i = size/2; i > 0; i--){//从倒数第一个有儿子的节点开始处理
                dealHeap(i);
            }
    }

新增有序数据存储

1、第一种实现,我们在二叉堆中新增一个resultTable,接收每次删除的数据:


//成员变量
    private T[] resultTable;//用于存储有序数据(每次删除的数据就放入其中)

//修改构造函数,确认好存储数组的长度
resultTable = (T[]) new Comparable[ size ];//指定存储数组大小


//在resultTable末端添加数据
    //把数据插入到resultTable末端
    private void insertResult(T t){
        resultTable[resultTable.length - size] = t;
    }


//在delateMin方法中,把insertResult方法加入其中,使得每次删除的时候,就把数据移除出来

2、不新增数组直接把要删除的数据放到二叉堆数组的最末端,为什么可以如此做,在思考模块已经表述过了。

    //删除最小
    //删除过程中,需要重新调整二叉堆(下滤)
    public void deleteMin(){
        if(size == 0){
            throw new IllegalAccessError("二叉堆为空");
        }

//      //把删除的元素放入resultTable中
//      insertResult(table[1]);

        //把要删除的数据先存储起来
        T temp = table[1];

        //删除元素
        table[1] = table[size--];
        //把要删除的数据放到末尾
        table[size -- ] = temp;

        int target  = 1;//从顶部开始重新调整二叉堆

        dealHeap(target);
        print();
    }

这样一来,最后打印的时候,就可以直接打印二叉堆数组,不过这个时候的二叉堆不存在了。

效率比较

在上面所有的描述中,存在许多情况,接下来我们对他们进行比较,看看哪种效率会更高:

1、比较是把目标排序数组一个个insert块还是直接放入数组在调整二叉堆快
在测试过程中,我们保证存储数据一致,都采用额外的数组存储。
下面是我比较的源码和结果:

package com.brickworkers;

public class Heap<T extends Comparable<? super T>> {

    private static final int DEFAULT_CAPACITY = 10; //默认容量

    private T[] table; //用数组存储二叉堆

    private T[] resultTable;//用于存储有序数据(每次删除的数据就放入其中)

    private int size; //表示当前二叉堆中有多少数据

    public Heap(int capactiy){
        this.size = 0;//初始化二叉堆数据量
        table = (T[]) new Comparable[capactiy + 1];//+1是因为我们要空出下标为0的元素不存储
    }

    public Heap() {//显得专业些,你就要定义好构造器
        this(DEFAULT_CAPACITY);
    }


    //实现方式二,直接先插入,然后对数据进行整理成二叉堆
    public Heap( T [ ] array , boolean methodType){
        if(methodType){
            size = 0;
            table = (T[]) new Comparable[DEFAULT_CAPACITY + 1];
            //实现方式一,直接用insert来实现数据放入二叉堆中
            long insertStratTime = System.currentTimeMillis();
            for (T t : array) {
                    insert(t);
            }
            System.out.println("直接插入的方式耗时:"+(System.currentTimeMillis() - insertStratTime));
        }else{
            size = array.length;
            table = (T[]) new Comparable[ size + 2 ];//存储的数组要略微长
            long dealHeapStratTime = System.currentTimeMillis();
            for (int i = 0; i < array.length; i++) {
                table[ i+1] = array[i];//arr[0]位置不存放数据
            }
            //到这里为止,数据已经全部都放入了table中
            //接下来要做的就是要把这些数据转变成二叉堆
            for(int i = size/2; i > 0; i--){//从倒数第二层开始处理
                dealHeap(i);
            }
            System.out.println("随机插入重新调整的方式耗时:"+(System.currentTimeMillis() - dealHeapStratTime));
        }
         resultTable = (T[]) new Comparable[ size ];//指定存储数组大小    
    }


    //插入
    public void insert(T t){
        //先判断是否需要扩容
        if(size == table.length - 1){
            resize();
        }
        //开始插入
        //定义一个预插入位置下标
        int target = ++size;
        //循环比较父节点进行位置交换
        for(table[ 0 ] = t; t.compareTo(table[target/2]) < 0; target /= 2){
            table[target] = table[target/2];//如果满足条件,那么两者交换,知道找到合适位置(上滤)
        }
        //插入数据
        table[target] = t;
        print();

    }


    //删除最小
    //删除过程中,需要重新调整二叉堆(下滤)
    public void deleteMin(){
        if(size == 0){
            throw new IllegalAccessError("二叉堆为空");
        }

//      //把删除的元素放入resultTable中
        insertResult(table[1]);

        //把删除的元素放到末尾
//      T temp = table[1];

        //删除元素
        table[1] = table[size--];

//      table[size -- ] = temp;

        int target  = 1;//从顶部开始重新调整二叉堆

        dealHeap(target);


        print();
    }

    private void dealHeap(int target) {
        int child;//要处理的节点下标
        T tmp = table[ target ];

        for( ; target * 2 <= size; target = child )
        {
            child = target * 2;
            if( child != size &&table[ child + 1 ].compareTo( table[ child ] ) < 0 ){//如果右孩子比左孩子小
                child++;
            }
            if( table[ child ].compareTo( tmp ) < 0 ){
                table[ target ] = table[ child ];
                table[child] = null;
            }
            else{
                 break;
            }
        }
        table[ target ] = tmp;
    }





    //如果插入数据导致达到数组上限,那么就需要扩容
    private void resize(){
          T [] old = table;
          table = (T [])  new Comparable[old.length*2 + 1];//把原来的数组扩大两倍
          for( int i = 0; i < old.length; i++ )
              table[ i ] = old[ i ];        //数组进行拷贝
    }


    //打印数组
    private void print(){
    /*  System.out.println();
        for (int i = 1; i <= size; i++) {
            System.out.print(table[i] + " ");
        }
        System.out.println("二叉堆大小:"+size);*/
    }



    //把数据插入到resultTable末端
    private void insertResult(T t){
        resultTable[resultTable.length - size] = t;
    }

    public static void main(String[] args) {
        Integer[] target = new Integer[100000];
        for (int i = 0; i < 100000; i++) {
            target[i] = i;
        }

        new Heap<Integer>(target, true);
        new Heap<Integer>(target, false);

    }

}

//运行结果:
//直接插入的方式耗时:5
//随机插入重新调整的方式耗时:3
//

从试验的结果来看,先随机插入,后调整二叉堆的效率会高很多。当然了,不排除扩容机制占时的影响,至于为什么会如此,我只能告诉你第一种方式的时间复杂度为O(N),而第二种方式的时间复杂度为O(NlogN),所以第二种是优先的。

2、我们比较是新数组存储排序数据还是直接用二叉堆数组存储排序数据。

package com.brickworkers;

public class Heap<T extends Comparable<? super T>> {

    private static final int DEFAULT_CAPACITY = 10; //默认容量

    private T[] table; //用数组存储二叉堆

    private T[] resultTable;//用于存储有序数据(每次删除的数据就放入其中)

    private int size; //表示当前二叉堆中有多少数据

    public Heap(int capactiy){
        this.size = 0;//初始化二叉堆数据量
        table = (T[]) new Comparable[capactiy + 1];//+1是因为我们要空出下标为0的元素不存储
    }

    public Heap() {//显得专业些,你就要定义好构造器
        this(DEFAULT_CAPACITY);
    }


    //实现方式二,直接先插入,然后对数据进行整理成二叉堆
    public Heap( T [ ] array , boolean methodType){
        if(methodType){
            size = 0;
            table = (T[]) new Comparable[DEFAULT_CAPACITY + 1];
            //实现方式一,直接用insert来实现数据放入二叉堆中
            for (T t : array) {
                    insert(t);
            }
        }else{
            size = array.length;
            table = (T[]) new Comparable[ size + 2 ];//存储的数组要略微长
            for (int i = 0; i < array.length; i++) {
                table[ i+1] = array[i];//arr[0]位置不存放数据
            }
            //到这里为止,数据已经全部都放入了table中
            //接下来要做的就是要把这些数据转变成二叉堆
            for(int i = size/2; i > 0; i--){//从倒数第二层开始处理
                dealHeap(i);
            }
        }
         resultTable = (T[]) new Comparable[ size ];//指定存储数组大小    
    }


    //插入
    public void insert(T t){
        //先判断是否需要扩容
        if(size == table.length - 1){
            resize();
        }
        //开始插入
        //定义一个预插入位置下标
        int target = ++size;
        //循环比较父节点进行位置交换
        for(table[ 0 ] = t; t.compareTo(table[target/2]) < 0; target /= 2){
            table[target] = table[target/2];//如果满足条件,那么两者交换,知道找到合适位置(上滤)
        }
        //插入数据
        table[target] = t;
        print();

    }


    //删除最小
    //删除过程中,需要重新调整二叉堆(下滤)
    public void deleteMin(boolean store){
        if(size == 0){
            throw new IllegalAccessError("二叉堆为空");
        }

        if(store){
//          //把删除的元素放入resultTable中
            insertResult(table[1]);

            //删除元素
            table[1] = table[size--];
        }else{
            //把删除的元素放到末尾
            T temp = table[1];

            //删除元素
            table[1] = table[size--];

            table[size + 1] = temp;
        }




        int target  = 1;//从顶部开始重新调整二叉堆

        dealHeap(target);


        print();
    }

    private void dealHeap(int target) {
        int child;//要处理的节点下标
        T tmp = table[ target ];

        for( ; target * 2 <= size; target = child )
        {
            child = target * 2;
            if( child != size &&table[ child + 1 ].compareTo( table[ child ] ) < 0 ){//如果右孩子比左孩子小
                child++;
            }
            if( table[ child ].compareTo( tmp ) < 0 ){
                table[ target ] = table[ child ];
                table[child] = null;
            }
            else{
                 break;
            }
        }
        table[ target ] = tmp;
    }





    //如果插入数据导致达到数组上限,那么就需要扩容
    private void resize(){
          T [] old = table;
          table = (T [])  new Comparable[old.length*2 + 1];//把原来的数组扩大两倍
          for( int i = 0; i < old.length; i++ )
              table[ i ] = old[ i ];        //数组进行拷贝
    }


    //打印数组
    private void print(){
        /*System.out.println();
        for (int i = 1; i < table.length; i++) {
            System.out.print(table[i] + " ");
        }
        System.out.println("二叉堆大小:"+size);*/
    }



    //把数据插入到resultTable末端
    private void insertResult(T t){
        resultTable[resultTable.length - size] = t;
    }

    public static void main(String[] args) {
        Integer[] target = new Integer[100000];
        for (int i = 0; i < 100000; i++) {
            target[i] = i;
        }


        Heap<Integer> heap1 = new Heap<>(target, true);
        long copyArrStartTime = System.currentTimeMillis();
        //循环删除
        for (int i = 0; i < 100000; i++) {
            heap1.deleteMin(true);
        }
        System.out.println("拷贝数组耗时:"+(System.currentTimeMillis() - copyArrStartTime));


        Heap<Integer> heap2 = new Heap<>(target, true);
        long ArrStartTime = System.currentTimeMillis();
        //循环删除
        for (int i = 0; i < 100000; i++) {
            heap2.deleteMin(false);
        }
        System.out.println("直接存储耗时:"+(System.currentTimeMillis() - ArrStartTime));
    }

}

//输出结果:
//拷贝数组耗时:23
//直接存储耗时:14
//

其实这个效率问题是显而易见的,少一个数组之间的拷贝,当然会显得高效很多。

希望对你有所帮助。


http://www.niftyadmin.cn/n/4544277.html

相关文章

【第三组】第四次冲刺例会(2017.7.14)

开发小组&#xff1a;Geomystery 冲刺经理&#xff1a;程立智&#xff08;李明伦代&#xff09; 小组成员&#xff1a;李明伦 蔡镇泽 郑昊 王涵 温志成 一、会议内容 1、 昨天都做了什么&#xff1a; 程立智&#xff1a;&#xff08;夏令营请假未归&#xff09; 李明伦&#xf…

ASP.NET Core中地址栏传入数据会影响Controller向ViewModel赋值

今日发现一个BUG&#xff0c;经过仔细调试&#xff0c;发现之前没有注意到的一个特性&#xff0c;或者说是很郁闷的一个设定。 需求大致是这样的&#xff1a;1-N个用车账单&#xff0c;可以共同选择出来生成一个报销单&#xff0c;现在要修改报销单。 上部分相关代码&#xff0…

java实现排序(5)-归并排序

引言 归并排序也是一种效率非常高的排序算法&#xff0c;它的时间复杂度是O&#xff08;NlogN&#xff09;。在本文中&#xff0c;会详细介绍归并排序的概念和排序的基本原理。最后用代码实现归并排序&#xff0c;供大家参考。笔者目前整理的一些blog针对面试都是超高频出现的…

闭包的优点及使用闭包的注意点

一. 闭包的优点&#xff1a; 1&#xff09; 减少全局变量 <script>function f(){var a 0;return function () {a;alert(a);}}var result f();result(); //1result(); //2 </script> 2&#xff09; 减少传递给函数的参数数量 <script>function calFactory(…

java实现排序(6)-快速排序

引言 快速排序&#xff0c;作为一个编程人员来说&#xff0c;肯定都是接触过的。那么&#xff0c;你还记得怎么去实现么&#xff0c;怎么优化呢&#xff1f;在本篇博文中&#xff0c;会详细介绍快速排序的过程&#xff0c;对于不是唯一的过程&#xff08;可变或者可选&#xf…

kerberos配置方法

为什么80%的码农都做不了架构师&#xff1f;>>> 客户端配置 kerberos客户端配置&#xff0c;理论上很简单。安装客户端程序&#xff0c;然后拿到正确的kerberos配置信息&#xff0c;理论上就可以使用kerberos来验证身份了。下面以red hat enterprise server 6.5为例…

java I/O系统(1)-File类

引言 自己对java的IO系统不是非常了解。所以我想进一步一点点去整理好它。在本篇博文中&#xff0c;我们详细介绍一下File类的意义&#xff0c;包括它很大部分的功能。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接&#xff1a;http://blog.csdn.net/u01…

java I/O系统(2)-装饰器模式

引言 IO系统是使用了装饰器模式的典型。所以对装饰器模式的深入研究对IO系统的理解肯定大有裨益。在本文中会详细介绍装饰器模式&#xff0c;会用以demo展示&#xff0c;同时会举出例子在IO系统中是如何呈现了这种模式&#xff0c;最后&#xff0c;我们探讨一下装饰器模式与代…