排序算法-堆排序详解

堆:必须满足完全二叉树的定义

大顶堆:父节点的值始终比左右子节点的值都要大,且堆顶的值最大。

大顶堆如下图所示:

        如上图:我们构建了一个大顶堆,任何一个节点都比自己的左右子节点的值要大,但不要求左右子节点之间的大小关系,堆顶的值最大,然后根据堆构建一个数组[16,14,10,8,7,9,3,2,4,1],可以看出该数组是由二叉树的层序遍历得到的。

        大顶堆的性质:

        1、对于下标为i的节点,它的父节点的下标为(i-1)/2;

        2、对于下标为i的节点,它的左孩子下标为(2*i)+1,它的右孩子下标为(2*i)+2;且i对应节点的         值要大于左右孩子的值

堆排序-升序

复杂度

        对O(n)级别个非叶子结点进行堆调整操作O(logn);时间复杂度为O(nlogn);之后每一次堆调整操作确定一个数的次序,时间复杂度为O(nlogn);合起来时间复杂度O(nlogn);属于不稳定排序。

        大顶堆适用于升序排序,原理是先构造大顶堆,然后将堆顶的值与最后一个节点的值进行交换,再将最后一个节点从堆中移除,这样数组的最后一个值就是最大的,然后继续维护大顶堆的性质(将除最后一个元素以外的其他所有元素重新构造为一个大顶堆),继续交换堆顶与当前的最后一个值,重复维护堆性质与交换过程;最后就得到一个升序的数组。接下来看一下详细步骤

 构造大顶堆

        

        上图中给了一个无序的数组,先将其构造成堆,然后按照大顶堆的性质来进行构造;

        上图中,由于0号节点16比左右孩子(4和10)都大,所以不需要交换;再到1号节点4,左孩子14比4大,所以交换14和4,右孩子不比14大,所以不发生交换;再到2号节点10,左右孩子都不比2号节点大,所以不发生交换;

        再到3号节点,发现此时值为4,比右孩子8要小,所以需要进行交换;继续到4号节点,发现7比左孩子1要大,不发生交换,节点5、6、7、8、9都是叶子节点,故不需要交换,此时这就是一个大顶堆了,可以看到每一个节点都比自己的左右孩子节点的值要大。

维护堆性质

        维护堆性质是针对当前节点,例如当前节点的左右孩子都必须要比当前节点小,否则就要发生交换,交换后,还要继续看交换节点的左右孩子是否比交换节点小,如若不然,则继续发生交换。

        通常构造大顶堆,首先先从最后一个父节点开始,例如我们有n个元素,根据大顶堆性质,可以直到一个节点i的父节点的下标为(i-1)/2,那么n个元素的最后一个元素的下标为n-1,所以父节点的下标为(n-1-1)/2,即n/2-1;上图中我们可以直到最后一个父节点的的下标为10/2 - 1 = 4,也就是说从4号节点开始,按照父节点比左右孩子都要大的原则来进行调整,然后左孩子9号节点不需要调整,就继续调整3号节点,以此类推,注意,假如当前调整的父节点是1号节点,那么在1号节点与3号节点交换后,需要继续对3号节点维护堆性质,即3号节点4继续与8号节点8进行交换。

交换堆顶元素

        在构造完大顶堆后,将堆顶元素和最后一个元素进行交换,这样最后一个元素成为了最大的值,将交换完的末尾称为“有序区”,而除了交换完之后的最后一个元素,剩下的元素称为“堆区”,

        堆区元素需要进行维护堆性质,使其再次称为一个大顶堆,然后再将堆顶元素与堆区末尾元素进行交换,交换完之后,堆区末尾元素划分到有序区,继续重复。

        上图中交换堆顶元素与堆中最后一个元素,数组中16已经排到最后一位;此时将16从堆中移除;接下来继续对堆进行维护;

        上图中,从堆顶开始维护堆性质,首先将堆顶与左孩子交换,然后继续向下进行调整;

        经过上图中调整过后,此时重新形成了一个大顶堆,那么继续按照堆顶元素与末尾元素进行交换,可以得到下图:

        图中将堆顶元素14与堆中末尾元素进行交换,然后将末尾元素从堆中去除,放入有序区;重复这个维护堆与交换的过程,中间过程可自行推导,最终可以得到如下图示的升序数组。

代码实现

以Go语言为例,实现一个堆排序,升序版

package main

import "fmt"

func heapify(num []int, n int, i int) {
	largest := i
	l := 2*i + 1
	r := 2*i + 2
	if l < n && num[largest] < num[l] {
		largest = l
	}
	if r < n && num[largest] < num[r] {
		largest = r
	}
	if largest != i {
		num[largest], num[i] = num[i], num[largest]
		heapify(num, n, largest)
	}
}

func heapSort(num []int, n int) {
	//构建堆
	for i := n/2 - 1; i >= 0; i-- {
		heapify(num, n, i)
	}
	//交换堆顶元素,然后继续调整堆
	for j := n - 1; j >= 0; j-- {
		num[0], num[j] = num[j], num[0]
		heapify(num, j, 0)
	}
}

func main() {
	arr := []int{16, 4, 10, 14, 7, 9, 3, 2, 8, 1}
	heapSort(arr, len(arr))
	fmt.Println(arr)
}
输出:
[1 2 3 4 7 8 9 10 14 16]

堆排序-降序

        小顶堆与大顶堆相反,都是节点数组要比左右孩子都要小,堆顶元素的值最小,所以这里不做过多讲解,直接改动一下代码就可以得到利用小顶堆的降序排序

package main

import "fmt"

func heapify(num []int, n int, i int) {
        s := i
        l := 2*i + 1
        r := 2*i + 2
        if l < n && num[s] > num[l] {
                s = l
        }
        if r < n && num[s] > num[r] {
                s = r
        }
        if s != i {
                num[s], num[i] = num[i], num[s]
                heapify(num, n, s)
        }
}

func heapSort(num []int, n int) {
        //构建堆
        for i := n/2 - 1; i >= 0; i-- {
                heapify(num, n, i)
        }
        //打印构造的小顶堆
        fmt.Println(num)
        //交换堆顶元素,然后继续调整堆
        for j := n - 1; j >= 0; j-- {
                num[0], num[j] = num[j], num[0]
                heapify(num, j, 0)
        }
}

func main() {
        arr := []int{16, 4, 10, 14, 7, 9, 3, 2, 8, 1}
        heapSort(arr, len(arr))
        fmt.Println(arr)
}
输出:
[1 2 3 8 4 9 10 14 16 7]
[16 14 10 9 8 7 4 3 2 1]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值