排序算法 --- 堆排序

堆排序是选择排序思想上的进阶,先来了解一下选择排序。

1.选择排序

选择排序的策略:

  • 一个数组中,将元素分为前后两堆:已排序 + 未排序;
  • 然后从未排序 序列中,去  “选择” 一个最小值的元素,把它放到前面已排序的序列中去;
  • 然后不断的重复这个过程。时间复杂度显而易见:O(n2)
public class sort {
	
    //写一个交换数组中两个元素的方法
    public static void swap(int[] nums, int indexA, int indexB) {
        int tmp = nums[indexA];
        nums[indexA] = nums[indexB];
        nums[indexB] = tmp;
    }

    //选择排序
    public static void selectionSort(int[] nums) {
        int size = nums.length;
		
        for(int i=0; i<size; i++) { //选择的范围 (i, size-1)
			
            int minNum = nums[i];
            int minIndex = i;
            for(int j=i; j<size; j++) {
                if(nums[j] < minNum) {
                    minNum = nums[j];
                    minIndex = j;
                }
            }
            swap(nums, i, minIndex);
        }
    }
	
    public static void main(String[] args) {
        int[] numbers = new int[] {81,94,11,96,12,35,17,95,28,58,41,75,15};
        
        selectionSort(numbers);
		
        for(int i=0; i<numbers.length; i++) {
            System.out.println(numbers[i]);
        }	
    }

}

2.堆排序

关于堆排序,有两种算法

堆排序算法1,策略如下:

  1. 首先,把原数组 A[ ] 所有的元素调整成为一个 最堆(用数组表示,仍为 A[ ] )
  2. 然后,从最大堆 A[ ] 中以O(1) 的时间,找到值最大的元素,放入到一个临时数组 TmpA[ ] 末尾,并在最大堆中删除这个最大元素。
  3. 接着,把 A[ ] 剩余的所有元素,调整成为一个最大堆
  4. 重复过程 2、3,直到 A[ ] 为空,此时TmpA[ ]就是一个已经排序好的数组。
  5. 将TmpA[ ]的元素都导回到  A[ ]

堆排序算法2,策略如下:

  1. 首先,把原数组 A[ ] 所有的元素调整成为一个 最堆(用数组表示,仍为 A[ ] )
  2. 将最大堆 A[ ]的根元素(即最大值元素),放到数组的末尾(交换处理)
  3. 继续把数组 A[ ]剩余的其他元素(除了放到末尾的元素),调整成为一个最大堆
  4. 重复过程2、3

从上面的两个算法中可以看到,要实现堆排序。

需要实现:

  • 如何将一个数组 调整成为一个 最大/小堆
  • 删除掉 最大/小 堆的根元素后,如何继续调整剩余的元素为一个最大/小堆

关于上面这两种调整方法,本质都是一样的,向上/下 过滤 元素,即可实现,以举例子说明过滤行为:

先来一个最简单的情况:如何把下面三个结点的二叉树,调整成为一个最大堆呢?

操作如下:

  • 从根结点 1 开始,去和左右儿子 都比较一下,发现这三个结点中,最大的是右儿子3
  • 然后根结点 和右儿子3交换。交换后:根3,左儿2,右儿1
  • 完毕

从上面的过程可以看出,根结点发生了两次和它的儿子结点交换的行为,也就是发生了两次向下过滤的行为。再来看一个稍微复杂点的例子:

假如结点7的子树  和结点9的子树。已经是一个最大堆了,那如何调整,才能使整颗树是一个最大堆呢?操作如下:

  1. 从根结点1开始,比较1、7、9。发现右儿子9最大。1、9交换位置
  2. 然后元素1开始,比较1、6、4。发现左儿子6最大。1,6交换位置
  3. 完毕

可以看到最大堆的建立过程,把左右子树都调整成为一个最大堆,然后根结点不断的向下过滤,最后整颗树都成为一个最大堆。

再来总结一下把一个无序的数组调整成为一个最大堆策略:

  1. 因为堆是一个完全二叉树,因此首先让数组满足该特性,只需要元素在二叉树的中的顺序是顺序摆放

  2. 先让小的子树 通过向下过滤行为 变成最大堆。然后在此基础上,向下过滤子树的根结点,变成大子树的最大堆。

  3. 重复过程2,直到整棵树都变成最大堆

来写一下建立最大堆的算法:


public class sort1 {
	
    //写一个交换两个元素的方法
    public static void swap(int[] nums, int indexA, int indexB) {
        int tmp = nums[indexA];
        nums[indexA] = nums[indexB];
        nums[indexB] = tmp;
    }

    //向下过滤算法
    private static void pervDown(int[] nums, int parent, int N) { //变量N代表当前数组的长度, 用来防止超界
        int temp = nums[parent];
        int child = 2 * parent + 1; //先获得左儿子的下标
        while(child < N) {
            //如果右孩子存在 且右孩子的值大于左孩子
            if(child+1<N && nums[child+1]>nums[child]) {
                child++;
            }//到这里,child变量已经指向了parent结点较大的一个儿子
			
            if(temp >= nums[child])//这就不过滤了
                break;
			
            nums[parent] = nums[child];
			
            //更新parent和 child,准备下一轮循环
            parent = child;
            child = 2 * parent + 1;
        }
        nums[parent] = temp;	    
    }
	
    //删除最大堆根元素,继续调整剩余元素为最大堆
    private static int deleteMax(int[] nums) {
        int maxItem = nums[0];
        nums[0] = nums[nums.length-1];
        nums[nums.length-1] = Integer.MIN_VALUE;
		
        pervDown(nums, 0, nums.length);
		
        return maxItem;
    }
	
    //建立最大堆算法
    private static int[] buildHeap(int[] nums) {
        //从完全二叉树的最后一个有儿子的结点开始,向下过滤算法
        //最后一个结点是nums.length-1,它的父结点是 (nums.length-1-1)/2 ,(这里画图就可以确定)
        for(int i= (nums.length-2)/2; i>=0; i--) {
            pervDown(nums, i, nums.length);
        }
        return nums;
    }
//---------------------------------------------------------------------------------------
    //堆排序1(插入排序的改进)
    public static void heapSort1(int[] nums) {
        int size = nums.length;
        int[] tmpA = new int[nums.length];
		
        buildHeap(nums);
        for(int i=nums.length-1; i>=0; i--)
            tmpA[i] = deleteMax(nums);
        for(int i=0; i<nums.length; i++)
            nums[i] = tmpA[i];
    }
	
	
    //堆排序2(插入排序的改进)
    public static void heapSort2(int[] nums) {
        buildHeap(nums);
        for(int i=nums.length-1; i>0; i--) {
            swap(nums, 0, i);
			
            pervDown(nums, 0, i);
        }
    }

//---------------------------------------------------------------------------------------	
	
    public static void main(String[] args) {
        int[] numbers = new int[] {81,94,11,96,12,35,17,95,28,58,41,75,15};
        heapSort2(numbers);
		
        for(int i=0; i<numbers.length; i++) {
            System.out.println(numbers[i]);    
        }
    }

}

最后再来分析一下时间复杂度

 

算法1(需要额外数组):

  • 过滤算法:pervDown,时间复杂度和树高度有关,O(log N)
  • 建堆:buildHeap --> O(N)
  • 第一个循环:O(N log N)
  • 第二个循环:O(N)

因此总的时间复杂度为 O(N log N),需要额外的物理空间

 

算法2:

  • 建堆:buildHeap --> O(N)
  • 交换过滤(循环):O(N log N)

因此总的时间复杂度为 O(N log N)

 

 

具体的时间复杂度推导看下这篇:https://blog.csdn.net/qq_34228570/article/details/80024306

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值