您当前的位置:首页 > 电脑百科 > 程序开发 > 算法

堆和二叉堆的实现和特性

时间:2021-04-06 11:16:39  来源:今日头条  作者:一角钱技术
堆和二叉堆的实现和特性

作者公众号:一角钱技术(org_yijiaoqian)

堆 Heap

Heap:可以迅速找到一堆数中的最大或者最小值的数据结构。

将根节点最大的堆叫做大顶堆或大根堆,根节点最小的堆叫做小顶堆或小根堆

常见的堆有二叉堆裴波那契堆等。

堆本身是一个相对比较抽象的数据结构,那么它有具体的实现就分为二叉堆(二项堆、Binary)、裴波那契堆(基于树的)。那么要实现的话一般面试来说或者经常会用的话就是二叉堆来实现,当然在工业级比较牛逼的应用都是裴波那契以及非常严格裴波那契。裴波那契堆因为相对比较复杂,你可以不知道去怎么实现,但是你可以看它的时空复杂度更好,它的实现也是基于树,但是并不少二叉树而是多叉的。

假设是大顶堆,则常见操作(API):

  • find-max:O(1)
  • delete-max:O(logN)
  • insert(create):O(logN) or O(1)

“堆” 这种数据结构为什么会有用? 在很多线上中的情形经常会在使用,比如说经常一个数一个数的过来,同时还有从另外一边的化经常去删除一些数据,问你这些数据里面它最大值是多少?这里的最大值可能就代表优先级最高的结点或者是那个数需要你先处理的,比如说你的任务流里面随时你要拿出优先级最高的任务优先处理,那么这种数据结构就更加的好用。

有些人可能会想我用数组来实现可不可以?或者是我直接排序可不可以?这里我们可以思考一下: 假设维护一个数组,每次有个新元素插入进来,你就需要把整个数组进行一次所谓的排序,这样的话时间复杂度就会是 nlogn,这样的话就是不够高效的,另外删除也可能比较麻烦,假设删除的是最大值或最小值在最后面的话还好,可以是 O(1) 的时间复杂度,但是如果在最前面的话,你就必须把整个数组最前面元素删除掉之后,把整个数组往前挪,这样时间复杂度又变差了。

不同的实现比较:
https://en.wikipedia.org/wiki/Heap_(data_structure)

堆和二叉堆的实现和特性

 

当你提到“堆” 的时候,不要默认认为是二叉堆,同时你要知道堆的实现又很多种,而二叉堆本身的话只是因为它相对比较容易实现,它的时间效率是堆里面算比较差的。

二叉堆的性质

通过完全二叉树来实现(注意:不是二叉搜索树);

什么是完全二叉树? 就是它的根和每一级结点都是满的,除了最下面一层叶子结点可能不满之外,其他上面结点都是满的。

二叉堆(大顶)它满足下列性质:

  • 性质1:是一颗完全树;
  • 性质2:树中任意结点的值总数 >= 其子节点的值;
堆和二叉堆的实现和特性

 

由于性质2,就可以保证根结点是最大的结点,所以我们访问最大值就直接返回这个根结点值。这里思考一个问题:为什么要做成树的结构呢?其实就要方便,因为找最大值是O(1)了是满足的,但是问题是后续你要删除一个元素或者你要添加一个元素,怎么让这个操作高效,至少是 logn 的。

二叉堆的实现细节

  1. 二叉堆一般都通过 “数组” 来实现
  2. 假设“第一个元素” 在数组中的索引为 0 的话,则父结点和子结点的位置关系如下:索引为 i 的左孩子的索引是 (2∗i+1);索引为 i 的右孩子的索引是 (2∗i+2);索引为 i 的父结点的索引是 floor((i−1)/2);

因为把一个完全二叉树依次放到一维数组里面去,那么它的孩子和父亲之间的关系,就可以直接用下班的数学关系表示了。

堆和二叉堆的实现和特性

 

Insert 插入操作

  1. 新元素一律先插入到堆的尾部
  2. 依次向上调整整个堆的结构(一直到根即可) HeapifyUp

当这个堆要进行维护操作,也就是插入元素的时候以及你要删除元素的时候要怎么做?

例子:85 添加到二叉堆中

堆和二叉堆的实现和特性

 

操作的细节分为两步:

  • 第一步:首先把新元素插入到堆的尾部再说;(新的元素可能是特别大或者特别小,那么要做的一件事情就是重新维护一下堆的所有元素,把新元素挪到这个堆的相应的位置)
  • 第二步:依次向上调整整个堆的结构,就叫 HeapifyUp
堆和二叉堆的实现和特性

 

85 按照上面讲的先插入到堆的尾部,也就是一维数组的尾部,一维数组的尾部的话就上图的位置,因为这是一个完全二叉树,所以它的尾部就是50后面这个结点。插进来之后这个时候就破坏了堆,它的每一个结点都要大于它的儿子的这种属性了,接下来要做的事情就是要把 85 依次地向上浮动,怎么浮动?就是 85 大于它的父亲结点,那么就和父亲结点进行交换,直到走到根如果大于根的话,就和根也进行交换。

堆和二叉堆的实现和特性

 

85 再继续往前走之后,它要和 80 再进行比较,同理可得:也就是说这个结点每次和它的父亲比,如果它大于它的父亲的话就交换,直到它不再大于它的父亲。

堆和二叉堆的实现和特性

 

Delete Max 删除堆顶操作

  1. 将堆尾元素替换到顶部(即堆顶被替代删除掉)
  2. 依次从根部向下调整整个堆的结构(一直到堆尾即可) HeapifyDown

例子:90 从二叉堆中删除

堆和二叉堆的实现和特性

 

堆的实现代码模版

JAVA 版本

// Java

import java.util.Arrays;
import java.util.NoSuchElementException;


public class BinaryHeap {


    private static final int d = 2;
    private int[] heap;
    private int heapSize;


    /**
     * This will initialize our heap with default size.
     */
    public BinaryHeap(int capacity) {
        heapSize = 0;
        heap = new int[capacity + 1];
        Arrays.fill(heap, -1);
    }


    public boolean isEmpty() {
        return heapSize == 0;
    }


    public boolean isFull() {
        return heapSize == heap.length;
    }




    private int parent(int i) {
        return (i - 1) / d;
    }


    private int kthChild(int i, int k) {
        return d * i + k;
    }


    /**
     * Inserts new element in to heap
     * Complexity: O(log N)
     * As worst case scenario, we need to traverse till the root
     */
    public void insert(int x) {
        if (isFull()) {
            throw new NoSuchElementException("Heap is full, No space to insert new element");
        }
        heap[heapSize] = x;
        heapSize ++;
        heapifyUp(heapSize - 1);
    }


    /**
     * Deletes element at index x
     * Complexity: O(log N)
     */
    public int delete(int x) {
        if (isEmpty()) {
            throw new NoSuchElementException("Heap is empty, No element to delete");
        }
        int maxElement = heap[x];
        heap[x] = heap[heapSize - 1];
        heapSize--;
        heapifyDown(x);
        return maxElement;
    }


    /**
     * Maintains the heap property while inserting an element.
     */
    private void heapifyUp(int i) {
        int insertValue = heap[i];
        while (i > 0 && insertValue > heap[parent(i)]) {
            heap[i] = heap[parent(i)];
            i = parent(i);
        }
        heap[i] = insertValue;
    }


    /**
     * Maintains the heap property while deleting an element.
     */
    private void heapifyDown(int i) {
        int child;
        int temp = heap[i];
        while (kthChild(i, 1) < heapSize) {
            child = maxChild(i);
            if (temp >= heap[child]) {
                break;
            }
            heap[i] = heap[child];
            i = child;
        }
        heap[i] = temp;
    }


    private int maxChild(int i) {
        int leftChild = kthChild(i, 1);
        int rightChild = kthChild(i, 2);
        return heap[leftChild] > heap[rightChild] ? leftChild : rightChild;
    }


    /**
     * Prints all elements of the heap
     */
    public void printHeap() {
        System.out.print("nHeap = ");
        for (int i = 0; i < heapSize; i++)
            System.out.print(heap[i] + " ");
        System.out.println();
    }


    /**
     * This method returns the max element of the heap.
     * complexity: O(1)
     */
    public int findMax() {
        if (isEmpty())
            throw new NoSuchElementException("Heap is empty.");
        return heap[0];
    }


    public static void main(String[] args) {
        BinaryHeap maxHeap = new BinaryHeap(10);
        maxHeap.insert(10);
        maxHeap.insert(4);
        maxHeap.insert(9);
        maxHeap.insert(1);
        maxHeap.insert(7);
        maxHeap.insert(5);
        maxHeap.insert(3);


        maxHeap.printHeap();
        maxHeap.delete(5);
        maxHeap.printHeap();
        maxHeap.delete(2);
        maxHeap.printHeap();
    }
}

C/C++ 版本

C/C++
#include <IOStream>
using namespace std;

class BinaryHeap {
public:
    BinaryHeap(int capacity);
    void insert(int x);
    int erase(int x);
    int findMax();
    void printHeap();

    bool isEmpty() { return heapSize == 0; }
    bool isFull() { return heapSize == capacity; }
    ~BinaryHeap() { delete[] heap; }

private:
    void heapifyUp(int i);
    void heapifyDown(int i);
    int maxChild(int i);

    int parent(int i) { return (i - 1) / 2; }
    int kthChild(int i, int k) { return 2 * i + k; }

private:
    int *heap;
    int heapSize;
    int capacity;
};

/**
 * This will initialize our heap with default size.
*/
BinaryHeap::BinaryHeap(int capacity) {
    this->heapSize = 0;
    this->capacity = capacity;
    this->heap = new int[capacity + 5];
}

/**
 * Inserts new element in to heap
 * Complexity: O(log N)
 * As worst case scenario, we need to traverse till the root
 */
void BinaryHeap::insert(int x) {
    try {
        if (isFull()) 
            throw -1;
        
        heap[heapSize] = x;
        heapSize ++;
        heapifyUp(heapSize - 1);
        return ;
    } catch (int e) {
        cout << "Heap is full, No space to insert new element" << endl;
        exit(-1);
    }
}

/**
 * Deletes element at index x
 * Complexity: O(log N)
 */
int BinaryHeap::erase(int x) {
    try {
        if (isEmpty()) 
            throw -1;

        int maxElement = heap[x];
        heap[x] = heap[heapSize - 1];
        heapSize--;
        heapifyDown(x);
        return maxElement;
    } catch (int e) {
        cout << "Heap is empty, No element to delete" << endl;
        exit(-1);
    }
}

/**
 * Maintains the heap property while inserting an element.
 */
void BinaryHeap::heapifyUp(int i) {
    int insertValue = heap[i];
    while (i > 0 && insertValue > heap[parent(i)]) {
        heap[i] = heap[parent(i)];
        i = parent(i);
    }
    heap[i] = insertValue;
}

/**
 * Maintains the heap property while deleting an element.
 */
void BinaryHeap::heapifyDown(int i) {
    int child;
    int temp = heap[i];
    while (kthChild(i, 1) < heapSize) {
        child = maxChild(i);
        if (temp >= heap[child]) {
            break;
        }
        heap[i] = heap[child];
        i = child;
    }
    heap[i] = temp;
}

int BinaryHeap::maxChild(int i) {
    int leftChild = kthChild(i, 1);
    int rightChild = kthChild(i, 2);
    return heap[leftChild] > heap[rightChild] ? leftChild : rightChild;
}

/**
 * This method returns the max element of the heap.
 * complexity: O(1)
 */
int BinaryHeap::findMax() {
    try {
        if (isEmpty()) 
            throw -1;

        return heap[0];
    } catch (int e) {
        cout << "Heap is empty." << endl;
        exit(-1);
    }
}

/**
 * Prints all elements of the heap
 */
void BinaryHeap::printHeap() {
    cout << "nHeap = ";
    for (int i = 0; i < heapSize; i++)
        cout << heap[i] << " ";
    cout << endl;
    return ;
}


int main() {
    BinaryHeap maxHeap(10);

    maxHeap.insert(10);
    maxHeap.insert(4);
    maxHeap.insert(9);
    maxHeap.insert(1);
    maxHeap.insert(7);
    maxHeap.insert(5);
    maxHeap.insert(3);

    maxHeap.printHeap();
    maxHeap.erase(5);
    maxHeap.printHeap();
    maxHeap.erase(2);
    maxHeap.printHeap();

    return 0;
}

JavaScript 版本

// JavaScript
class BinaryHeap {
  constructor(compare) {
    this.data = [];
    this.compare = compare;
  }

  insert(value) {
    this.insertAt(this.data.length, value);
  }

  insertAt(index, value) {
    this.data[index] = value;
    // 对比当前节点与其父节点,如果当前节点更小就交换它们
    while (index > 0 && this.compare(value, this.data[Math.floor((index - 1) / 2)]) < 0) {
      this.data[index] = this.data[Math.floor((index - 1) / 2)];
      this.data[Math.floor((index - 1) / 2)] = value;
      index = Math.floor((index - 1) / 2);
    }
  }

  delete(index) {
    if (this.data.length === 0) return;

    let value = this.data[index];
    let i = index;
    // fix heap
    while (i < this.data.length) {
      let left = i * 2 + 1;
      let right = i * 2 + 2;
      // 没有左子节点
      if (left >= this.data.length) break;
      // 没有右子节点
      if (right >= this.data.length) {
        this.data[i] = this.data[left];
        i = left;
        break;
      }
      // 比较左右子节点的大小,更小的补到父节点
      if (this.compare(this.data[left], this.data[right]) < 0) {
        this.data[i] = this.data[left];
        i = left;
      } else {
        this.data[i] = this.data[right];
        i = right;
      }
    }
    // 查看最后的空位是不是最后的叶子节点
    if (i < this.data.length - 1) {
      this.insertAt(i, this.data.pop());
    } else {
      this.data.pop();
    }
    return value;
  }

  printHeap() {
    console.log("nHeap = ");
    console.log(this.data);
  }
}

let maxHeap = new BinaryHeap((a, b) => b - a);
maxHeap.insert(10);
maxHeap.insert(4);
maxHeap.insert(9);
maxHeap.insert(1);
maxHeap.insert(7);
maxHeap.insert(5);
maxHeap.insert(3);

maxHeap.printHeap();
maxHeap.delete(5);
maxHeap.printHeap();
maxHeap.delete(2);
maxHeap.printHeap();

Python 版本

Python
import sys 
class BinaryHeap: 
  
    def __init__(self, capacity): 
        self.capacity = capacity 
        self.size = 0
        self.Heap = [0]*(self.capacity + 1) 
        self.Heap[0] = -1 * sys.maxsize 
        self.FRONT = 1
  
    def parent(self, pos): 
        return pos//2
  
    def leftChild(self, pos): 
        return 2 * pos 
  
    def rightChild(self, pos): 
        return (2 * pos) + 1
  
    def isLeaf(self, pos): 
        if pos >= (self.size//2) and pos <= self.size: 
            return True
        return False
  
    def swap(self, fpos, spos): 
        self.Heap[fpos], self.Heap[spos] = self.Heap[spos], self.Heap[fpos] 
  
    def heapifyDown(self, pos): 
  
        if not self.isLeaf(pos): 
            if (self.Heap[pos] > self.Heap[self.leftChild(pos)] or 
               self.Heap[pos] > self.Heap[self.rightChild(pos)]): 
  
                if self.Heap[self.leftChild(pos)] < self.Heap[self.rightChild(pos)]: 
                    self.swap(pos, self.leftChild(pos)) 
                    self.heapifyDown(self.leftChild(pos)) 
  
                else: 
                    self.swap(pos, self.rightChild(pos)) 
                    self.heapifyDown(self.rightChild(pos)) 
  
    def insert(self, element): 
        if self.size >= self.capacity : 
            return
        self.size+= 1
        self.Heap[self.size] = element 
  
        current = self.size 
  
        while self.Heap[current] < self.Heap[self.parent(current)]: 
            self.swap(current, self.parent(current)) 
            current = self.parent(current) 
  
    def Print(self): 
        for i in range(1, (self.size//2)+1): 
            print(" PARENT : "+ str(self.Heap[i])+" LEFT CHILD : "+ 
                                str(self.Heap[2 * i])+" RIGHT CHILD : "+
                                str(self.Heap[2 * i + 1])) 
   
    def minHeap(self): 
  
        for pos in range(self.size//2, 0, -1): 
            self.heapifyDown(pos) 
  
    def delete(self): 
  
        popped = self.Heap[self.FRONT] 
        self.Heap[self.FRONT] = self.Heap[self.size] 
        self.size-= 1
        self.heapifyDown(self.FRONT) 
        return popped 
    def isEmpty(self):
        return self.size == 0
        
    def isFull(self): 
        return self.size == self.capacity
if __name__ == "__main__": 
      
    print('The minHeap is ') 
    minHeap = BinaryHeap(5)
    minHeap.insert(5) 
    minHeap.insert(3) 
    minHeap.insert(17) 
    minHeap.insert(10) 
    minHeap.insert(84) 
    minHeap.insert(19) 
    minHeap.insert(6) 
    minHeap.insert(22) 
    minHeap.insert(9) 
    minHeap.minHeap() 
  
    minHeap.Print() 
    print("The Min val is " + str(minHeap.delete())) 

高频题目

  • 剑指 Offer 40. 最小的k个数
  • 239. 滑动窗口最大值
  • 剑指 Offer 49. 丑数
  • 347. 前 K 个高频元素
  • HeapSort : https://www.geeksforgeeks.org/heap-sort/

总结

如果只是说堆什么?那么它是一个抽象的数据结构,表示可以非常迅速地拿到一堆数里面的最大值或者最小值,它并不少二叉堆,那么二叉堆和其他的各种堆,维基百科里面有详细说明:
https://en.wikipedia.org/wiki/Heap_(data_structure)

注意:二叉堆只是堆的一种实现形式,二叉堆为什么出现得多?是因为它较为常见且简单,但是它并不是最优的实现,正是因为这个原因,所以二叉堆很多时候并不是完全的那么实用,那么如果在工程的代码里面我们可以直接调 优先队列(priority_queue) 就行了。



Tags:二叉堆   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
作者公众号:一角钱技术(org_yijiaoqian)堆 HeapHeap:可以迅速找到一堆数中的最大或者最小值的数据结构。将根节点最大的堆叫做大顶堆或大根堆,根节点最小的堆叫做小顶堆或小根堆...【详细内容】
2021-04-06  Tags: 二叉堆  点击:(194)  评论:(0)  加入收藏
▌简易百科推荐
前言Kafka 中有很多延时操作,比如对于耗时的网络请求(比如 Produce 是等待 ISR 副本复制成功)会被封装成 DelayOperation 进行延迟处理操作,防止阻塞 Kafka请求处理线程。Kafka...【详细内容】
2021-12-27  Java技术那些事    Tags:时间轮   点击:(1)  评论:(0)  加入收藏
博雯 发自 凹非寺量子位 报道 | 公众号 QbitAI在炼丹过程中,为了减少训练所需资源,MLer有时会将大型复杂的大模型“蒸馏”为较小的模型,同时还要保证与压缩前相当的结果。这就...【详细内容】
2021-12-24  量子位    Tags:蒸馏法   点击:(9)  评论:(0)  加入收藏
分稀疏重建和稠密重建两类:稀疏重建:使用RGB相机SLAMOrb-slam,Orb-slam2,orb-slam3:工程地址在: http://webdiis.unizar.es/~raulmur/orbslam/ DSO(Direct Sparse Odometry)因为...【详细内容】
2021-12-23  老师明明可以靠颜值    Tags:算法   点击:(7)  评论:(0)  加入收藏
1. 基本概念希尔排序又叫递减增量排序算法,它是在直接插入排序算法的基础上进行改进而来的,综合来说它的效率肯定是要高于直接插入排序算法的;希尔排序是一种不稳定的排序算法...【详细内容】
2021-12-22  青石野草    Tags:希尔排序   点击:(6)  评论:(0)  加入收藏
ROP是一种技巧,我们对execve函数进行拼凑来进行system /bin/sh。栈迁移的特征是溢出0x10个字符,在本次getshell中,还碰到了如何利用printf函数来进行canary的泄露。ROP+栈迁移...【详细内容】
2021-12-15  星云博创    Tags:栈迁移   点击:(19)  评论:(0)  加入收藏
一、什么是冒泡排序1.1、文字描述冒泡排序是一种简单的排序算法。它重复地走访要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地...【详细内容】
2021-12-15    晓掌柜丶韶华  Tags:排序算法   点击:(16)  评论:(0)  加入收藏
在了解golang的map之前,我们需要了解哈希这个概念。哈希表,又称散列表(Hash table),是根据键(key)而直接访问在内存储存位置的数据结构。也就是说,它通过计算出一个键值的函数,将...【详细内容】
2021-12-07  一棵梧桐木    Tags:哈希表   点击:(13)  评论:(0)  加入收藏
前面文章在谈论分布式唯一ID生成的时候,有提到雪花算法,这一次,我们详细点讲解,只讲它。SnowFlake算法据国家大气研究中心的查尔斯&middot;奈特称,一般的雪花大约由10^19个水分子...【详细内容】
2021-11-17  小心程序猿QAQ    Tags:雪花算法   点击:(24)  评论:(0)  加入收藏
导读:在大数据时代,对复杂数据结构中的各数据项进行有效的排序和查找的能力非常重要,因为很多现代算法都需要用到它。在为数据恰当选择排序和查找策略时,需要根据数据的规模和类型进行判断。尽管不同策略最终得到的结果完...【详细内容】
2021-11-04  华章科技    Tags:排序算法   点击:(37)  评论:(0)  加入收藏
这是我在网上找的资源的一个总结,会先给出一个我看了觉得还行的关于算法的讲解,再配上实现的代码: Original author: Bill_Hoo Original Address: http://blog.sina.com.cn/s/bl...【详细内容】
2021-11-04  有AI野心的电工和码农    Tags: KMP算法   点击:(36)  评论:(0)  加入收藏
相关文章
    无相关信息
最新更新
栏目热门
栏目头条