堆的原理以及实现O(lgn)

2023年 9月 27日 148.9k 0

大家好,我是蓝胖子,我一直相信编程是一门实践性的技术,其中算法也不例外,初学者可能往往对它可望而不可及,觉得很难,学了又忘,忘其实是由于没有真正搞懂算法的应用场景,所以我准备出一个系列,囊括我们在日常开发中常用的算法,并结合实际的应用场景,真正的感受算法的魅力。

今天我们就来看看堆这种数据结构。

源码已经上传到github

https://github.com/HobbyBear/codelearning/tree/master/heap

原理

在详细介绍堆之前,先来看一种场景,很多时候我们并不需要对所有元素进行排序,而只需要取其中前topN的元素,这样的情况如果按性能较好的排序算法,比如归并或者快排需要n*log( n)的时间复杂度,n为数据总量,排好序后取出前N条数据,而如果用堆这种数据结构则可以在n*log(N)的时间复杂度内找到这N条数据,N的数据量远远小于数据总量n。

接着我们来看看堆的定义和性质,堆是一种树状结构,且分为最小堆和最大堆,最大堆的性质有父节点大于左右子节点,最小堆的性质则是父节点小于左右子节点。如下图所示:

image.png

并且堆是一颗完全二叉树,完全二叉树的定义如下:

若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。

因为结点都集中在左侧,所以我们可以从上到下,从左到右对堆中节点进行标号,如下图所示:

image.png

从0开始对堆中节点进行标号后,可以得到以下规律:

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

有了标号和父子节点的标号间的关系,我们可以用一个数组来保存堆这种数据结构,下面以构建一个最大堆为例,介绍两种构建堆的方式。

HeapInsert

heapInsert的方式是从零开始,逐个往堆中插入数组中的元素,并不断调整新的节点,让新节点的父节点满足最大堆父节点大于其子节点的性质,这个调整的过程被称作ShiftUp。当数组中元素全部插入完成时,就构建了一个最大堆。代码如下:

func HeapInsert(arr []int) *Heap {  
   h := &Heap{arr: make([]int, 0, len(arr))}  
   for _, num := range arr {  
      h.Insert(num)  
   }  
   return h  
}

Heapify

heapify的方式是假设数组已经是一个完全二叉树了,然后找到树中的最后一个非叶子节点,然后通过比较它与其子节点的大小关系,让其满足最大堆的父节点大于其子节点的性质,这样的操作被称作ShifDown,对每个非叶子节点都执行ShifDown操作,直至根节点,这样就达到了将一个普通数组变成一个堆的目的。

如果堆的长度是n,那么最后一个非叶子节点是 n/2 -1 ,所以可以写出如下逻辑,

func Heapify(arr []int) *Heap {  
   h := &Heap{arr: arr}  
   lastNotLeaf := len(arr)/ 2 -1  
   for i:= lastNotLeaf;i >= 0; i-- {  
      h.ShiftDown(i)  
   }  
   return h  
}

取出根节点

取出根节点的逻辑比较容易,将根节点结果保存,之后让它与堆中最后一个节点交换位置,然后从索引0开始进行ShiftDown操作,就又能让整个数组变成一个堆了。

func (h *Heap) Pop() int {  
   num := h.arr[0]  
   swap(h.arr, 0, len(h.arr)-1)  
   h.arr = h.arr[:len(h.arr)-1]  
   h.ShiftDown(0)  
   return num  
}

ShiftUp,ShiftDown实现

下面我将shiftUp和shiftDown的源码展示出来,它们都是一个递归操作,因为在每次shiftUp或者shiftDown成功后,其父节点或者子节点还要继续执行shifUp或shiftDown操作。

// 从标号为index的节点开始做shifUp操作  
func (h *Heap) ShiftUp(index int) {  
   if index == 0 {  
      return  
   }  
   parent := (index - 1) / 2  
   if h.arr[parent] < h.arr[index] {  
      swap(h.arr, parent, index)  
      h.ShiftUp(parent)  
   }  
}  
  
// 从标号为index的节点开始做shifDown操作  
func (h *Heap) ShiftDown(index int) {  
   left := index*2 + 1  
   right := index*2 + 2  
   if left < len(h.arr) && right = h.arr[right] && h.arr[left] > h.arr[index] {  
         swap(h.arr, left, index)  
         h.ShiftDown(left)  
      }  
      if h.arr[right] > h.arr[left] && h.arr[right] > h.arr[index] {  
         swap(h.arr, right, index)  
         h.ShiftDown(right)  
      }  
   }  
   if left >= len(h.arr) {  
      return  
   }  
   if right >= len(h.arr) {  
      if h.arr[left] > h.arr[index] {  
         swap(h.arr, left, index)  
         h.ShiftDown(left)  
      }  
   }  
}

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论