数据结构:堆

简介: 数据结构:堆

什么是堆?

堆结构就是用数组实现完全二叉树结构。

什么是最大堆

完全二叉树中,如果所有的节点都大于等于它的子节点(即每颗子树的最大值都在顶部),那么这就是最大堆

比如[6,5,3,4,2,1]表示的最大堆是:

image.png

什么是最小堆

完全二叉树中,如果所有的节点都小于等于它的字节点(即每颗子树的最小值都在顶部),那么这就是最小堆

比如[1,3,2,4,6,5]表示的最小堆是:


image.png

节点的位置

  • 左侧子节点的位置是2∗index+12*index+12index+1
  • 右侧子节点的位置是2∗index+22*index+22index+2
  • 父节点位置是(index−1)/2(index-1)/2(index1)/2

网络异常,图片无法展示
|

堆的基本操作-HeapInsert 插入

思路(以最小堆为例)

  • 将值插入堆的底部,即数组的尾部
  • 然后将这个值与它的父节点进行比较,如果比父节点的值小,则进行交换,交换之后再继续跟上一级的父节点比较,直到父节点小于等于这个插入值。

复杂度

大小为k的堆中插入元素的时间复杂度为O(logk)O(log{k})O(logk)。因为对于一个长度为n的数组,也就是节点数为n的二叉树,那么这个二叉树的层级是lognlog{n}logn,插入元素时,是逐层进行比较,因此时间复杂度就是lognlog{n}logn

Javascript版(以最小堆为例)

class MinHeap{
    constructor(){
        this.heap=[]
    }
    swap(i1,i2){
        const temp = this.heap[i1];
        this.heap[i1] = this.heap[i2];
        this.heap[i2] = this.heap[i1];
    }
    getParentIndex(i){
        // return Math.floor((i-1)/2);
        return (i-1)>>1;
    }
    shiftUp(index){
        if(index === 0) return;
        const parentIndex = this.getParentIndex(index);
        if(this.heap[parentIndex] > this.heap[index]){
            this.swap(parentIndex, index);
            this.shiftUp(parentIndex);
        }
    }
    insert(value){
        this.heap.push(value);
        this.shiftUp(this.heap.length - 1);
    }
}
const h = new MinHeap();
h.insert(3);
复制代码

Java版(以最大堆为例)

public static void heapInsert(int[] arr, int index){
    while(arr[index] > arr[(index - 1)/2]){
        swap(arr, index, (index - 1)/2);
        index = (index - 1)/2;
    }
}
复制代码

堆的基本操作-删除堆顶(Heapify 堆化)

思路(以最小堆为例)

  • 用数组尾部元素替换堆顶,数组长度-1(直接删除堆顶会破坏堆结构)
  • 然后下移进行heapify 堆化

复杂度

大小为k的堆中删除堆顶的时间复杂度为O(logk)O(log{k})O(logk)

Javascript版(以最小堆为例)

class MinHeap{
    constructor() {
        this.heap = [];
    }
    swap(i1, i2) {
        const temp = this.heap[i1];
        this.heap[i1] = this.heap[i2];
        this.heap[i2] = temp;
    }
    getParentIndex(i) {
        // return Math.floor((i-1)/2)]
        // 取商
        return (i - 1) >> 1;
    }
    getLeftIndex(i) {
        return i * 2 + 1
    }
    getRightIndex(i) {
        return i * 2 + 2
    }
    // 上移
    shiftUp(index) {
        if (index == 0) return;
        const parentIndex = this.getParentIndex(index);
        if (this.heap[parentIndex] > this.heap[index]) {
            this.swap(parentIndex, index);
            this.shiftUp(parentIndex)
        }
    }
    shiftDown(index) {
        const leftIndex = getLeftIndex(index);
        const rightIndex = getRightIndex(index);
        if (this.heap[leftIndex] < this.heap[index]) {
            this.swap(leftIndex, index);
            this.shiftDown(leftIndex)
        }
        if (this.heap[rightIndex] < this.heap[index]) {
            this.swap(rightIndex, index);
            this.shiftDown(rightIndex)
        }
    }
    insert(value) {
        this.heap.push(value);
        this.shiftUp(this.heap.length - 1)
    }
    pop() {
        this.heap[0] = this.heap.pop();
        this.shiftDown(0)
    }
}
const h = new MinHeap();
h.insert(3);
h.insert(2);
h.insert(1);
h.pop()
复制代码

Java版(以最大堆为例)

// 某个数在index位置,能否向下移动
public static void heapify(int[] arr, int index, int heapSize){
    int left = index * 2 + 1;
    while(left < heapSize){ // 下方还有孩子的时候
        // 两个孩子中,谁的值大,把下标给largest
        int largest = left + 1 < heapSize && arr[left+1] > arr[left] ? left + 1 : left;
        if(largest == index){
            break;
        }
        swap(arr, largest, index);
        index = largest;
        left = index * 2 +1;
    }
}
复制代码

堆的基本操作-获取堆顶和堆的大小

  • 获取堆顶:直接返回数组的头部
  • 获取堆的大小:直接返回数组的长度
class MinHeap{
    ...
    peek() {
        return this.heap[0]
    }
    size() {
        return this.heap.length
    }
}
复制代码

堆排序

思路

  • 先将数组进行堆化,成为一个最大堆
  • 然后将数组首尾元素交换,尾元素移出堆(heapSize-1),首元素继续进行堆化
  • 堆化之后重复上一个步骤
  • heapSize=0时,就排好序了(移出堆的元素是越来越小的) 时间复杂度:O(nlogn)O(nlog{n})O(nlogn),空间复杂度:O(1)O(1)O(1),比快排更优
public static heapSort(int[] arr){
    if(arr==null || arr.length < 2){
        return;
    }
    for(int i=0; i<arr.length; i++){      // O(N)
        heapInsert(arr, i);                // O(logN )
    }
    int heapSize = arr.length;
    swap(arr, 0, --heapSize);
    while(heapSize > 0){               // O(N)
        heapify(arr, 0, heapSize);      // O(logN )
        swap(arr, 0, --heapSize);       // O(1)
    }
}
复制代码

优化堆化过程

上面这个堆化实现过程,是将元素一个一个插入逐个进行堆化的。而现实情况往往是直接就拿到一个元素已经确定的数组,此时,我们就可以不用上面的方式进行逐个堆化。

比如[1,3,2,4,6,5,8,10,9,7,11,18,17,13,12 ]表示的最小堆是:


QQ图片20230106144230.png

那么我们可以先从数组最后一个元素开始进行堆化。先堆化一个小子树然后往左往上进行扩展:

网络异常,图片无法展示
|

代码优化

public static heapSort(int[] arr){
    if(arr==null || arr.length < 2){
        return;
    }
    // for(int i=0; i<arr.length; i++){      // O(N)
    //     heapInsert(arr, i);                // O(logN )
    // }
    for(int i=arr.length-1;i>=0;i--){
        heapify(arr,i,arr.length);
    }
    int heapSize = arr.length;
    swap(arr, 0, --heapSize);
    while(heapSize > 0){               // O(N)
        heapify(arr, 0, heapSize);      // O(logN )
        swap(arr, 0, --heapSize);       // O(1)
    }
}
复制代码

数学论证

一个长度为N的数组,那么形成的二叉树中的最底层有N/2个节点(假设有子节点,看了一眼,但不移动),倒数第二层有N/4个节点(最多移动1步),倒数第三层有N/8个节点(最多移动1 2步),...,整体复杂度就是:T(N)=N/2+N/4∗2+N/8∗3+...T(N)=N/2+N/4*2+N/8*3+...T(N)=N/2+N/42+N/83+...

T(N)=N/2+N/4∗2+N/8∗3+...T(N)=N/2+N/4*2+N/8*3+...T(N)=N/2+N/42+N/83+...

2T(N)=N+N/2∗2+N/4∗3+...2T(N)=N+N/2*2+N/4*3+...2T(N)=N+N/22+N/43+...

2T(N)−T(N)=T(N)=N+N/2+N/4+...2T(N)-T(N)=T(N)=N+N/2+N/4+...2T(N)T(N)=T(N)=N+N/2+N/4+...

根据等比数列求和公式:

网络异常,图片无法展示
|

就可以得出复杂度是:O(N)O(N)O(N)

堆的应用

  • 堆能高效、快速地找出最大值和最小值,时间复杂度为O(1)O(1)O(1)
  • 找出第K个最大(小)元素,这一类也可以用堆来找出

举个例子:找出第K个最大元素 思路如下:

  • 首先构建一个最小堆,并将元素依次插入堆中
  • 当堆的容量超过k,就删除堆顶
  • 插入结束后,堆顶就是第k个最大元素

扩展

已知有一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。

思路:维护一个大小为k的小根堆,从头到尾扫描n个数,如果当前数比堆顶大,替换堆顶,每个元素的移动距离不超过K,表示前K个数中一定有最小值, 所以堆顶为最小值,堆顶放到a0处,a[k]放到最小堆的堆顶,继续建堆,知道剩下最后k个元素,此时随着每次堆顶的弹出,堆的大小k--.  因为每个元素移动都在k以内,所以时间复杂度为O(N∗logK)O(N*logK)O(NlogK),空间复杂度O(k)。

1.21

假设k=6,遍历前7个数,扔到最小堆里,最小数一定在0位置,

Leetcode

LeetCode:215. 数组中的第 K 个最大元素

LeetCode:347. 前 K 个高频元素

LeetCode:23. 合并K个排序链表

相关链接:

三分钟搞懂桶排序

桶排序



相关文章
|
6天前
|
存储 算法
【数据结构】堆
【数据结构】堆
|
1月前
|
算法 安全 大数据
揭秘!Python堆与优先队列:数据结构的秘密武器,让你的代码秒变高效战士!
【7月更文挑战第8天】Python的heapq模块和queue.PriorityQueue提供堆与优先队列功能,助你提升算法效率。堆用于快速找大数据集的第K大元素,如示例所示,时间复杂度O(n log k)。PriorityQueue在多线程中智能调度任务,如模拟下载管理器,按优先级处理任务。掌握这些工具,让代码运行更高效!
49 1
|
6天前
|
存储 算法 Linux
【数据结构】树、二叉树与堆(长期维护)(1)
【数据结构】树、二叉树与堆(长期维护)(1)
|
11天前
|
搜索推荐 算法 Go
深入探索堆:Go语言中的高效数据结构
深入探索堆:Go语言中的高效数据结构
|
6天前
|
算法
【数据结构】树、二叉树与堆(长期维护)(2)
【数据结构】树、二叉树与堆(长期维护)(2)
【数据结构】树、二叉树与堆(长期维护)(2)
|
11天前
【数据结构】二叉树顺序实现(大堆)-->赋源码
【数据结构】二叉树顺序实现(大堆)-->赋源码
24 4
|
1月前
|
存储 算法
【数据结构】堆,堆的实现,堆排序,TOP-K问题
数据结构——堆,堆的实现,堆排序,TOP-K问题
22 1
【数据结构】堆,堆的实现,堆排序,TOP-K问题
|
4天前
|
存储
全局变量和局部变量在堆和栈的区别
全局变量和局部变量在堆和栈的区别
12 0
|
1月前
|
存储 算法 调度
惊呆了!Python高级数据结构堆与优先队列,竟然能这样优化你的程序性能!
【7月更文挑战第10天】Python的heapq模块实现了堆和优先队列,提供heappush和heappop等函数,支持O(log n)时间复杂度的操作。优先队列常用于任务调度和图算法,优化性能。例如,Dijkstra算法利用最小堆加速路径查找。堆通过列表存储,内存效率高。示例展示了添加、弹出和自定义优先级元素。使用堆优化程序,提升效率。
41 2
|
1月前
|
存储 缓存 算法
堆和栈的区别及应用场景
堆和栈的区别及应用场景