1、前言
在了解堆排序之前,需要知道堆的一些特征,那就是堆就是一个完全二叉树,所以需要了解完全二叉树的特点
完全二叉树的特点:
(1)叶子节点只能在最大的两层出现
(2)如果i=1,结点就是根结点,如果i>1,则其双亲parent(i)=i/2
(3)如果2i>n,则结点无左孩子,如果2i+1>n,则结点无右孩子
(4)如果下标从0开始,则第一个非叶子结点的下标为 length/2-1
堆排序会用到大顶堆和小顶堆,升序排序时用大顶堆,降序排序时用小顶堆。大顶堆的特点是节点值大于其左右孩子节点值,即value(n)>=value(n->left)并且value(n)>=value(n->right),小顶堆相反
2、稳定性、时间复杂度、空间复杂度
堆排序为什么不稳定:因为构建大顶堆/小顶堆的时间不稳定
平均时间复杂度:O(n*logn)
空间复杂度几乎为0(只用到几个临时变量)
3、具体思路(以升序、大顶推为例,数组小标为0,长度为length)
(1)构建初始堆(或者叫调整堆结构,就是将一个数组构建成一个标准堆结构)
选取第一个非叶子节点,数据下标为length/2-1,倒序遍历整个数组,将堆元素按自下到上的顺序,将最大的数字交换到堆顶,构建大顶堆。为什么初始化堆时要选取第一个非叶子节点呢?以及为什么第一个非叶子节点的下标为length/2-1呢?对于完全二叉树(N层)第一个非叶子节点在N-1层,因为需要将最大的元素通过拉的方式拉倒堆顶,所以肯定从N-1层开始拉,并且只有父节点需要做这个工作,所以从最后一个父节点开始拉。因为完全二叉树每个父节点(下标i)的孩子左节点下标为2i+1右孩子2i+2,假设只有左孩子则length为偶数,此时2i+1=length-1,即i=length/2-1。假设有右孩子则length为奇数,此时2i+2=length-1,即i=(length-1)/2-1,由于整数的除法是向下取整,(length-1)/2=length/2,所以i=length/2-1
(2)将堆顶元素与数组最后一个元素交换,此时最大的元素在数组最后面
(3)将数组0到length-2之间的元素继续调整为堆结构,然后交换下标0和length-2的元素值 ,继续调整 0到length-3的之间的数据元素,直到所有元素都有序
4、代码
package api
//SortByHeap 堆排序
func SortByHeap(arr []int, length int, desc bool) {
//第一个for循环用来构建初始堆,从第一个非叶子节点开始自下向上,让最大/最小的数值从下面一步步的升到堆顶
for i := length/2 - 1; i >= 0; i-- {
if desc {
adjustSmallHeap(arr, i, length)
} else {
adjustBigHeap(arr, i, length)
}
}
//交换、再调整
for j := length - 1; j >= 0; j-- {
swap(arr, 0, j)
//交换后,只有数组的第一个元素会破坏堆结构,所以只需要调整第一个元素,通过调整,将第一个元素放到合适的位置,同时将子数组中最大/最小的值交换到堆顶
if desc {
adjustSmallHeap(arr, 0, j)
} else {
adjustBigHeap(arr, 0, j)
}
}
}
//构建一个大顶堆
func adjustBigHeap(arr []int, i int, length int) {
var temp int = arr[i]
for k := 2*i + 1; k < length; k = 2*k + 1 {
if k+1 < length && arr[k] < arr[k+1] {
k = k + 1
}
if arr[k] > temp {
arr[i] = arr[k]
i = k
} else {
break
}
}
arr[i] = temp
}
//构建一个小顶堆
func adjustSmallHeap(arr []int, i int, length int) {
var temp int = arr[i]
for k := 2*i + 1; k < length; k = 2*k + 1 {
if k+1 < length && arr[k+1] < arr[k] {
k = k + 1
}
if arr[k] < temp {
arr[i] = arr[k]
i = k
} else {
break
}
}
arr[i] = temp
}