详解排序之堆排序

数据结构堆

堆是什么

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

  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)
}
相关推荐
huangyingying202517 分钟前
03-分支结构
后端
00后程序员19 分钟前
【Flutter -- 基础组件】Flutter 导航栏
后端
bobz96522 分钟前
ovs internal port 对比 veth-pair 性能
后端
Auroral15622 分钟前
基于RabbitMQ的异步通知系统设计与实现
前端·后端
易元36 分钟前
设计模式-代理模式
java·后端
嘻嘻哈哈开森37 分钟前
Java开发工程师转AI工程师
人工智能·后端
trust Tomorrow40 分钟前
每日一题-力扣-2278. 字母在字符串中的百分比 0331
算法·leetcode
LTPP1 小时前
自动化 Rust 开发的革命性工具:lombok-macros
前端·后端·github
一个热爱生活的普通人1 小时前
Go语言中 Mutex 的实现原理
后端·go
Victor3561 小时前
Dubbo(31)如何优化Dubbo的启动速度?
后端