Golang堆排序

介绍

堆排序( Heap Sort )由威尔士-加拿大计算机科学家 J. W. J. Williams 在 1964 年发明,它利用了二叉堆 (A binary heap) 的性质实现了排序,并证明了二叉堆数据结构的可用性。同年,美国籍计算机科学家 R. W. Floyd 在其树排序研究的基础上,发布了一个改进的更好的原地排序的堆排序版本。

堆排序属于选择类排序算法。

优先队列

优先队列是一种能完成以下任务的队列:插入一个数值,取出最小或最大的数值(获取数值,并且删除)。

优先队列可以用二叉树来实现,我们称这种结构为二叉堆。

最小堆和最大堆是二叉堆的一种,是一棵完全二叉树(一种平衡树)。

最小堆的性质:

  • 父节点的值都小于左右儿子节点。
  • 这是一个递归的性质。

最大堆的性质:

  • 父节点的值都大于左右儿子节点。
  • 这是一个递归的性质。

最大堆和最小堆实现方式一样,只不过根节点一个是最大的,一个是最小的。

最大堆特征

最大堆实现细节(两个操作):

  • push:向堆中插入数据时,首先在堆的末尾插入数据,如果该数据比父亲节点还大,那么交换,然后不断向上提升,直到没有大小颠倒为止。
  • pop:从堆中删除最大值时,首先把最后一个值复制到根节点上,并且删除最后一个数值,然后和儿子节点比较,如果值小于儿子,与儿子节点交换,然后不断向下交换, 直到没有大小颠倒为止。在向下交换过程中,如果有两个子儿子都大于自己,就选择较大的。

最大堆有两个核心操作,一个是上浮,一个是下沉,分别对应 push 和 pop。

操作一次 push 的最好时间复杂度为:O(1),因为第一次上浮时如果不大于父亲,那么就结束了。最坏的时间复杂度为: O(logn),相当于每次都大于父亲,会一直往上浮到根节点,翻转次数等于树的高度,而树的高度等于元素个数的对数:log(n)。

创建堆

package zgo_algorithm

// 一个最大堆,一棵完全二叉树
// 最大堆要求节点元素都不小于其左右孩子
type Heap struct {
	// 堆的大小
	Size int
	// 使用内部的数组来模拟树
	// 一个节点下标为 i,那么父亲节点的下标为 (i-1)/2
	// 一个节点下标为 i,那么左儿子的下标为 2i+1,右儿子下标为 2i+2
	Array []int
}

// 初始化一个堆
func NewHeap(array []int) *Heap {
	h := new(Heap)
	h.Array = array
	return h
}

// 最大堆插入元素
func (h *Heap) Push(x int) {
	// 堆没有元素时,使元素成为顶点后退出
	if h.Size == 0 {
		h.Array[0] = x
		h.Size++
		return
	}

	// i 是要插入节点的下标
	i := h.Size

	// 如果下标存在
	// 将小的值 x 一直上浮
	for i > 0 {
		// parent为该元素父亲节点的下标
		parent := (i - 1) / 2

		// 如果插入的值小于等于父亲节点,那么可以直接退出循环,因为父亲仍然是最大的
		if x <= h.Array[parent] {
			break
		}

		// 否则将父亲节点与该节点互换,然后向上翻转,将最大的元素一直往上推
		h.Array[i] = h.Array[parent]
		i = parent
	}

	// 将该值 x 放在不会再翻转的位置
	h.Array[i] = x

	// 堆数量加一
	h.Size++
}

// 最大堆移除根节点元素,也就是最大的元素
func (h *Heap) Pop() int {
	// 没有元素,返回-1
	if h.Size == 0 {
		return -1
	}

	// 取出根节点
	ret := h.Array[0]

	// 因为根节点要被删除了,将最后一个节点放到根节点的位置上
	h.Size--
	x := h.Array[h.Size]  // 将最后一个元素的值先拿出来
	h.Array[h.Size] = ret // 将移除的元素放在最后一个元素的位置上

	// 对根节点进行向下翻转,小的值 x 一直下沉,维持最大堆的特征
	i := 0
	for {
		// a,b为下标 i 左右两个子节点的下标
		a := 2*i + 1
		b := 2*i + 2

		// 左儿子下标超出了,表示没有左子树,那么右子树也没有,直接返回
		if a >= h.Size {
			break
		}

		// 有右子树,拿到两个子节点中较大节点的下标
		if b < h.Size && h.Array[b] > h.Array[a] {
			a = b
		}

		// 父亲节点的值都大于或等于两个儿子较大的那个,不需要向下继续翻转了,返回
		if x >= h.Array[a] {
			break
		}

		// 将较大的儿子与父亲交换,维持这个最大堆的特征
		h.Array[i] = h.Array[a]

		// 继续往下操作
		i = a
	}

	// 将最后一个元素的值 x 放在不会再翻转的位置
	h.Array[i] = x
	return ret
}

测试堆

package zgo_algorithm

import (
	"fmt"
	"testing"
)

func TestHeap(t *testing.T) {
	list := []int{5, 9, 1, 6, 8, 14, 6, 49, 25, 4, 6, 3}

	// 构建最大堆
	h := NewHeap(list)
	for _, v := range list {
		h.Push(v)
	}

	// 将堆元素移除
	for range list {
		h.Pop()
	}

	// 打印排序后的值
	fmt.Println(list)
}

堆排序实现


// 先自底向上构建最大堆,再移除堆元素实现堆排序
func SortHeap(array []int) {
	// 堆的元素数量
	count := len(array)

	// 最底层的叶子节点下标,该节点位置不定,但是该叶子节点右边的节点都是叶子节点
	start := count/2 + 1

	// 最后的元素下标
	end := count - 1

	// 从最底层开始,逐一对节点进行下沉
	for start >= 0 {
		sift(array, start, count)
		start-- // 表示左偏移一个节点,如果该层没有节点了,那么表示到了上一层的最右边
	}

	// 下沉结束了,现在要来排序了
	// 元素大于2个的最大堆才可以移除
	for end > 0 {
		// 将堆顶元素与堆尾元素互换,表示移除最大堆元素
		array[end], array[0] = array[0], array[end]
		// 对堆顶进行下沉操作
		sift(array, 0, end)
		// 一直移除堆顶元素
		end--
	}
}

// 下沉操作,需要下沉的元素时 array[start],参数 count 只要用来判断是否到底堆底,使得下沉结束
func sift(array []int, start, count int) {
	// 父亲节点
	root := start

	// 左儿子
	child := root*2 + 1

	// 如果有下一代
	for child < count {
		// 右儿子比左儿子大,那么要翻转的儿子改为右儿子
		if count-child > 1 && array[child] < array[child+1] {
			child++
		}

		// 父亲节点比儿子小,那么将父亲和儿子位置交换
		if array[root] < array[child] {
			array[root], array[child] = array[child], array[root]
			// 继续往下沉
			root = child
			child = root*2 + 1
		} else {
			return
		}
	}
}

测试堆排序


func TestSortHeap(t *testing.T) {
	list := []int{5, 9, 1, 6, 8, 14, 6, 49, 25, 4, 6, 3}
	SortHeap(list)
	fmt.Println(list)

	list = []int{-1, 5, 9, 1, 6, 8, 14, 6, 49, 25, 4, 6, 3}
	SortHeap(list)
	fmt.Println(list)

	list = []int{-1, 11, 22, 33}
	SortHeap(list)
	fmt.Println(list)
}

测试结果

=== RUN   TestSortHeap
[1 3 4 5 6 6 6 8 9 14 25 49]
[-1 1 3 4 5 6 6 6 8 9 14 25 49]
[-1 11 22 33]
--- PASS: TestSortHeap (0.00s)
PASS

版权声明:本文为qq_37703224原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。