重温堆排序及二叉堆的应用场景

21 篇文章 1 订阅

堆排序

  • 算法基本思路:

    将待排序数组,看成是一个数组形式的完全二叉树(节点在层序遍历中的顺序,即是元素在数组中的顺序),先对数组进行调整,使其成为一个大根堆,或者小根堆。

    调整完后,每次将堆顶元素(数组首元素),与最后一个元素交换,然后把最后一个元素从堆中删除(逻辑上删除),将剩余元素进行调整,形成一个新的堆,再把堆顶元素和堆尾元素交换,把堆尾元素删除,再调整剩余元素… 一直到堆为空,即得到一个有序的数组。

    若要对数组进行升序排列,则需要构建一个大根堆(每次交换,会把堆顶的最大元素,替换到最后面)

    若要进行降序排列,则构建一个小根堆。

  • 过程:

    • 建堆

      从后往前,从最后一个非叶子节点开始,对节点进行向下调整,调整到数组首元素位置。

    • 排序

      每次将堆顶元素和堆尾元素交换,并将堆尾元素从堆中移除,再对剩余元素进行向下调整,形成新的堆…一直到堆为空,排序结束

  • 代码

    /** 一开始我是从0号位置,依次上浮调整,其实这应该是使用二叉堆实现优先队列时的入队操作 **/
    
    		/** 从最后一个非叶子节点开始进行下沉调整,叶子节点无需下沉 **/
    		/** 假设父节点的数组下标为 n
    		 *  那么其左儿子的下标为 2n + 1
    		 *  右儿子下标为 2n + 2
    		 *  那么从后往前数,第一个非叶子节点,肯定是最后一个叶子节点的父节点
    		 *  最后一个叶子节点下标为   arr.length - 1
    		 *  那么其父节点的下标为   (arr.length - 2) / 2
    		 * **/
    public void buildHeap(int[] arr){
    	for (int i = (arr.length - 2) / 2; i >= 0; i--){
    			adjustDown(arr,i,arr.length - 1);
    	}
    }
    
    /** 对某个节点进行下沉 **/
    	public void adjustDown(int[] arr,int target,int last){
    		int value = arr[target];
    		int cur = target;
    		while (cur <= last){
    			int leftSon = cur * 2 + 1;
    			int rightSon = leftSon + 1;
    			int max;
                //取左右儿子中最大的那个
    			if (rightSon <= last)
    				max = arr[leftSon] > arr[rightSon] ? leftSon : rightSon;
    			else if (leftSon <= last)
    				max = leftSon;
    			else
    				break;
                //若子节点比父节点大,则父节点下沉
                //这里使用单向覆盖,不使用交换,提高效率
    			if (arr[max] > value){
    				arr[cur] = arr[max];
    				cur = max;
    			}else
    				break;
    		}
    		arr[cur] = value;
    	}
    
    /** 进行排序 **/
    public void heapSort(int[] arr){
    		//先建堆
        	buildHeap(arr);
    		for (int i = arr.length - 1; i >= 0; i--){
                //将堆顶与堆尾交换
    			int temp = arr[0];
    			arr[0] = arr[i];
    			arr[i] = temp;
                //逻辑上把i从堆中删除掉后
                //i-1为新的堆尾,对新的堆顶元素进行下沉
    			adjustDown(arr,0,i - 1);
    		}
    	}
    

堆排的时间复杂度

更正一下,建堆时的复杂度是O(n),大致推导过程为:假设有一颗满二叉树,高度为h,那么建堆时,从第h-1层往上,每一层需要向下调整。第h-1层的节点,都需要往下调整一层,第h-2层的节点,都需要往下调整二层,… 第1层的节点,需要往下调整h-1层。通过计算得知,所有需调整的节点,向下调整的总次数为2h - 1 - h,而总结点数n = 2h - 1,那么总的调整次数就为 n - log(n) ,故建堆复杂度为O(n)

排序时每次取堆顶元素,再对堆顶元素进行下沉,是O(nlogn)

堆排的平均时间复杂度,最坏时间复杂度都是O(nlogn)

与快排相比:

快排在极端情况下会退化成冒泡,最坏时间复杂度为O(n2)

堆排和快排都是不稳定的。可以想象,一个叶子节点,和它的父节点,值是相同的,但是由于堆顶和最后一个元素交换,最后叶子节点一定会跑到父节点前面,顺序变了。

快排的空间复杂度为O(logn),因为存在递归,或是用栈模拟的递归。而堆排的空间复杂度是O(1),无需使用额外的空间

二叉堆的其他应用

优先队列

优先队列使用二叉堆实现,和堆排序有共通的地方。

不过使用优先队列,入队操作就是将一个新的节点添加到堆中,这个新节点一定是最后一个叶子节点,插入之后,需要对该节点进行向上调整。也就是说,优先队列的建堆过程,是将节点一个一个的插入到堆里,每次插入后向上调整。

而堆排一开始就知道了所有节点,所以是直接从最后的一个非叶子节点往前,每个节点进行向下调整

优先队列的出队操作就是将堆顶元素删除,这和堆排中的排序过程是类似的,不过优先队列的出队,是直接把堆顶元素删掉,然后将堆尾元素补上堆顶的位置,再进行向下调整,堆中是真的少了一个元素;而堆排中,是将堆顶和堆尾元素交换,后从逻辑上删除一个节点,堆中元素实际没有减少,只是进行向下调整时,堆的结束位置会逐渐向前移动,也就是堆的大小逐渐变小。

/** 对某个节点,进行上浮 **/
	public void adjustUp(int[] arr,int target){
		int cur = target;
		int value = arr[target];
		while (cur > 0){
			int parent = (cur - 1) / 2;
			if (arr[parent] > value){
				arr[cur] = arr[parent];
				cur = parent;
			}else
				break;
		}
		arr[cur] = value;
	}
求解海量数据的TOP K问题

这是之前在面试中遇到过的一道题目。假设给定2000万个元素,要求用尽可能快的方式,找出其中最大的100个。
大概思路:二叉堆 + 分治归并
可以先将2000万个元素,拆分成2000组,每组1万个元素。每组用这1万个元素,进行建堆,建一个大小为100的大根堆(建堆时超出100范围的节点,下沉后直接扔掉即可)。这样,第一轮就得到了2000个大小为100的大根堆。之后的思路就很容易想到了,对这2000个大根堆进行两两合并,每2个大根堆,合并成一个大小为100的大根堆。这样第二轮就得到1000个大小为100的大根堆。继续这样两两合并下去,最终只剩下一个大小为100的大根堆。这个大根堆里的100个元素,就是最大的100个。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值