详解排序之堆排序

数据结构堆

堆是什么

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

  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)
}
相关推荐
bobz96512 分钟前
ovs patch port 对比 veth pair
后端
Asthenia041222 分钟前
Java受检异常与非受检异常分析
后端
uhakadotcom36 分钟前
快速开始使用 n8n
后端·面试·github
JavaGuide42 分钟前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9651 小时前
qemu 网络使用基础
后端
Asthenia04121 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04121 小时前
Spring 启动流程:比喻表达
后端
Asthenia04122 小时前
Spring 启动流程分析-含时序图
后端
ONE_Gua2 小时前
chromium魔改——CDP(Chrome DevTools Protocol)检测01
前端·后端·爬虫
致心2 小时前
记一次debian安装mariadb(带有迁移数据)
后端