详解排序之堆排序

数据结构堆

堆是什么

堆是一种数据结构,它需要同时满足两个条件:

  1. 是一个完全二叉树

完全二叉树首先是一种二叉树结构,每一个节点最多有两个子节点:左子节点和右子节点

每一层节点,从左到右,依次排列

只有当前层节点都添加满了之后,才能继续添加下一层节点

  1. 所有父节点的值 >= 子节点的值,一般称之为大顶堆;或所有父节点的值 <= 子节点的值,一般称之为小顶堆

怎么样来表示堆

我们从根节点开始,依次给每一个节点添加编号0,1,2,3,4,此时可以用一个一维数组来表示堆

使用一维数组来表示堆后,还可以通过数组下标,来快速定位指定节点的父节点,左子节点和右子节点

假设指定节点的下标为i,则可以通过以下公式快速定位:

  • 父节点下标 = (i - 1)/ 2
  • 左子节点下标 = 2 * i + 1
  • 右子节点下标 = 2 * i + 2

假设当前节点的下标为1

父节点下标 = (1 - 1) / 2 = 0

左子节点下标 = 2 * 1 + 1 = 3

右子节点下标 = 2 * 1 + 2 = 4

如何用一个未排序数组构建一个堆

主要分为两个步骤:

  1. 挑选节点
  2. 进行heapify操作

当我们拿到一个未排序的数组时,可以通过这个数组来构建一个完全二叉树

此时满足了堆的第一个条件,但不满足第二个条件

所有父节点的值 >= 子节点的值,一般称之为大顶堆;或所有父节点的值 <= 子节点的值,一般称之为小顶堆

为了满足第二个条件,我们需要对不满足条件的节点的值进行交换操作,这个操作一般称之为heapify

heapify代码:

go 复制代码
package test

import (
   "fmt"
   "testing"
)

// arr: 数组
// i: 需要进行heapify操作的节点
// arrLen: 数组长度
func heapify(arr []int, i int, arrLen int) {
   // 假设最大值下标为i
   max := i
   // i节点的左子节点和右子节点
   left := 2*i + 1
   right := 2*i + 2
   // 左子节点下标未越界 && 左子节点值 > 最大值
   if left < arrLen && arr[left] > arr[max] {
      // 最大值下标变为左子节点下标
      max = left
   }
   // 右子节点下标未越界 && 右子节点值 > 最大值
   if right < arrLen && arr[right] > arr[max] {
      // 最大值下标变为右子节点下标
      max = right
   }
   // 最大值下标不为i
   if max != i {
      // 交换节点的值
      swap(arr, max, i)
      // 因为max节点的值已改变,递归对下标max进行heapify,保证堆结构
      heapify(arr, max, arrLen)
   }
}

func swap(arr []int, i, j int) {
   temp := arr[i]
   arr[i] = arr[j]
   arr[j] = temp
}

// 测试代码
func TestHeapify(t *testing.T) {
   arr := []int{5, 7, 8, 9, 4}
   heapify(arr, 1, 5)
   fmt.Println(arr)
}

heapify可以保证一个节点满足堆结构,我们只要循环对目标节点进行heapify操作,就可以使完全二叉树成为堆结构,所以我们的下一步是解决如何挑选节点的问题

从哪一个节点开始进行heapify操作:从最后一个节点的父节点开始,依次向前,循环进行heapify操作

构建堆代码:

go 复制代码
func buildHeap(arr []int) {
   // 获取数组长度
   arrLen := len(arr)
   // 最后一个元素下标
   last := arrLen - 1
   // 从最后一个节点的父节点开始,依次向前,循环进行heapify操作
   for i := (last - 1) / 2; i >= 0; i-- {
      heapify(arr, i, arrLen)
   }
}

堆排序

如何使用数据结构堆来进行排序

上面我们学习了如何用一个未排序数组来构建一个堆,当我们构建一个大顶堆或小顶堆后,我们可以得到一个数组内最大的值或最小的值

大顶堆的根节点就是数组内最大的值,此时我们交换根节点和最后一个节点,就可以把最大值放到最后一个节点中,也就是数组的最后一个元素

绿色元素代表已排序的元素,此时我们无需关心这些已排序的元素,只用看还未排序的元素即可

此时,由于我们交换了根节点和最后一个节点的值,为了满足堆结构,还需要对交换后的根节点进行heapify操作

进行完heapify操作后,我们又可以循环进行以上操作,交换,heapify操作。。。

完整堆排序

完整堆排序步骤:

  1. 构建堆
  2. 交换根节点和未排序的最后一个节点
  3. 对根节点进行heapify
  4. 数组长度-1,循环进行步骤2,步骤3

完整堆排序代码:

go 复制代码
package test

import (
   "fmt"
   "testing"
)

// 堆排序
func heapSort(arr []int) {
   // 构建堆
   buildHeap(arr)
   // 循环减少数组长度
   for i := len(arr) - 1; i > 0; i-- {
      // 交换根节点和未排序的最后一个节点
      swap(arr, 0, i)
      // 对根节点进行heapify
      heapify(arr, 0, i)
   }
}

func buildHeap(arr []int) {
   // 获取数组长度
   arrLen := len(arr)
   // 最后一个元素下标
   last := arrLen - 1
   // 从最后一个节点的父节点开始,依次向前,循环进行heapify操作
   for i := (last - 1) / 2; i >= 0; i-- {
      heapify(arr, i, arrLen)
   }
}

// arr: 数组
// i: 需要进行heapify操作的节点
// arrLen: 数组长度
func heapify(arr []int, i int, arrLen int) {
   // 假设最大值下标为i
   max := i
   // i节点的左子节点和右子节点
   left := 2*i + 1
   right := 2*i + 2
   // 左子节点下标未越界 && 左子节点值 > 最大值
   if left < arrLen && arr[left] > arr[max] {
      // 最大值下标变为左子节点下标
      max = left
   }
   // 右子节点下标未越界 && 右子节点值 > 最大值
   if right < arrLen && arr[right] > arr[max] {
      // 最大值下标变为右子节点下标
      max = right
   }
   // 最大值下标不为i
   if max != i {
      // 交换节点的值
      swap(arr, max, i)
      // 因为max节点的值已改变,递归对下标max进行heapify,保证堆结构
      heapify(arr, max, arrLen)
   }
}

func swap(arr []int, i, j int) {
   temp := arr[i]
   arr[i] = arr[j]
   arr[j] = temp
}

// 测试代码
func TestHeapify(t *testing.T) {
   arr := []int{5, 7, 8, 9, 4}
   heapSort(arr)
   fmt.Println(arr)
}
相关推荐
luky!12 分钟前
算法--解决熄灯问题
python·算法
鸽鸽程序猿17 分钟前
【算法】【优选算法】二分查找算法(下)
java·算法·二分查找算法
_OLi_19 分钟前
力扣 LeetCode 150. 逆波兰表达式求值(Day5:栈与队列)
算法·leetcode·职场和发展
远望清一色29 分钟前
基于MATLAB身份证号码识别
开发语言·图像处理·算法·matlab
王二端茶倒水1 小时前
大龄程序员兼职跑外卖第五周之亲身感悟
前端·后端·程序员
醉颜凉2 小时前
【NOIP提高组】潜伏者
java·c语言·开发语言·c++·算法
lapiii3582 小时前
图论-代码随想录刷题记录[JAVA]
java·数据结构·算法·图论
夜色呦2 小时前
现代电商解决方案:Spring Boot框架实践
数据库·spring boot·后端
爱敲代码的小冰2 小时前
spring boot 请求
java·spring boot·后端
Dontla3 小时前
Rust泛型系统类型推导原理(Rust类型推导、泛型类型推导、泛型推导)为什么在某些情况必须手动添加泛型特征约束?(泛型trait约束)
开发语言·算法·rust