【数据结构】【堆】二叉堆

本文详细介绍了二叉堆的原理与实现,包括最大堆和最小堆,以及如何用二叉堆解决TOPK问题。通过数组实现二叉堆,提供了添加、删除、替换等接口,并对上滤和下滤操作进行了优化。此外,文章还探讨了批量建堆的方法和构建最小堆的策略。最后,展示了如何利用二叉堆在O(nlogk)的时间复杂度内解决从大量数据中找出最大k个数的问题。
摘要由CSDN通过智能技术生成

二叉堆

1.设计堆的目的

设计一种数据结构,用来存放整数,要求提供 3 个接口:

  • 添加元素
  • 获取最大值
  • 删除最大值

用已学过的数据结构来对比一下时间复杂度:
在这里插入图片描述
为了迎合此需求出现了一种新的数据结构 —— 堆

  • 获取最大值: O(logn)O(logn)
  • 删除最大值:O(logn)O(logn)
  • 添加元素:O(logn)

2. 堆的种类

堆(Heap)是一种 树状 的数据结构,常见的堆实现有:

  1. 二叉堆(Binary Heap,完全二叉堆)
  2. 多叉堆(D-heap、D-ary Heap)
  3. 索引堆(Index Heap)
  4. 二项堆(Binomial Heap)
  5. 斐波那契堆(Fibonacci Heap)
  6. 左倾堆(Leftist Heap,左式堆)
  7. 斜堆(Skew Heap)

堆的重要性质
任意节点的值总是 ≥ \geq≥ ( ⩽ \leqslant⩽) 子节点的值

最大堆和最小堆

  • 任意节点的值总是 ≥ 子节点的值,称为:最大堆、大根堆、大顶堆
  • 任意节点的值总是 ⩽ 子节点的值,称为:最小堆、小根堆、小顶堆

堆中的元素必须具备可比较性(跟二叉搜索树一样)
在这里插入图片描述

3. 二叉堆(Binary Heap)

3.1 二叉堆的数组实现逻辑

  • 二叉堆的逻辑结构就是一棵完全二叉树,所以也叫完全二叉堆
  • 完全二叉树的特点是层序遍历的时候,节点都是挨着的,根据这个特点,只要能根据索引就可以找到一个节点的父节点、左子节点、右子节点。因此可以用数组来实现完全二叉树
    在这里插入图片描述

一颗 n个节点的完全二叉树中,索引为i的节点的父子关系:
  • i = 0 为根节点
  • 左子节点索引:2i + 1
  • 右子节点索引:2i + 2
  • 当 2i + 1 > n - 1 ,此节点无左子节点
  • 当 2i + 2 > n - 1 ,此节点无右子节点
  • 当i > 0时,父 节点的索引为 floor( (i - 1) / 2 )

3.2 二叉堆对外提供的接口

public interface Heap<E> {
	int size();	// 元素的数量
	boolean isEmpty();	// 是否为空
	void clear();	// 清空
	void add(E element);	 // 添加元素
	E get();	// 获得堆顶元素
	E remove(); // 删除堆顶元素
	E replace(E element); // 删除堆顶元素的同时插入一个新元素
}

3.3 添加实现

思路:

  1. 首先将新节点放入数组的最后一位
  2. 循环执行上滤操作(Sift Up)
上滤操作:
- 如果 node > 父节点,与父节点交换位置
- 如果 node <= 父节点,或者 node 没有父节点,退出循环

图示:
在这里插入图片描述
假设当前添加新节点80,其过程如上图所示;
时间复杂度和层数有关,因此为O(logn)

private void elementNotNullCheck(E element){
     if(element == null){
         throw new RuntimeException("element can not be null");
     }
 }

 private void ensureCapacity(int size){
     if(size == 0){
         elements = (E[])new Object[DEFAULT_CAPACITY];
     }
     if(size == elements.length){
         int oldCapacity = size;
         int newCapacity = oldCapacity + (oldCapacity >> 1);
         E[] newElements = (E[]) new Object[newCapacity];

         for(int i = 0;i<size;i++){
             newElements[i] = elements[i];
         }
         elements = newElements;
     }
 }
 
  private void swift(int i,int j){
     E tmp = elements[i];
     elements[i] = elements[j];
     elements[j] = tmp;
  }

 @Override
 public void add(E element) {
     elementNotNullCheck(element);
     ensureCapacity(size + 1);
     elements[size++] = element;

     siftUp(size - 1);

 }

 //logN
 private void siftUp(int index){
     int parentIndex = (index - 1) >> 1;
     while(parentIndex >= 0){
         if(compare(elements[index],elements[parentIndex]) > 0){
             swift(index,parentIndex);
             index = parentIndex;
             parentIndex = (index - 1) >> 1;
         }else{
             break;
         }
     }
 }

3.4 上滤操作优化

思路:

  • 如果父节点比新节点小,copy 父节点内容到当前节点;
  • 最后找到目标index,才插入进去;
  • 减少了交换过程

图示:
在这里插入图片描述

  private void siftUp(int index){
      //todo 找应该放置的位置
      E cur = elements[index];
      //当index > 0的时候才有父节点
      while(index > 0){
          int parentIndex = (index  - 1) >> 1;
          E parent = elements[parentIndex];

          if(compare(cur,parent) <= 0) break;
          //break说明找到位置 就是index的位置
          
          elements[index] = parent;
          index = parentIndex;
      }
      //要么index = 0 要么 break出来的
      elements[index] = cur;
  }

3.5 删除

在这里插入图片描述

思路:

  • 删除操作先将数组中最后一个元素覆盖第一个元素;
  • 由于覆盖后,可能破坏了堆的性质,因此需要将这个元素下滤
  • 下滤就是将子结点中最大的元素和当前元素交换;

图示:
在这里插入图片描述
时间复杂度:O(logn)

public void siftDown(int index) {
    E old = elements[index];
    while (index <= (size - 2) >> 1) {
        int leftIndex = (index << 1) + 1;
        int rightIndex = leftIndex + 1;
        int maxIndex = leftIndex;

        if (index <= (size - 3) >> 1) {//左右都有
            maxIndex = compare(elements[leftIndex], elements[rightIndex]) > 0 ? leftIndex : rightIndex;
        }
        if (compare(elements[maxIndex], old) > 0) {
            elements[index] = elements[maxIndex];
            index = maxIndex;
        } else {
            break;
        }
    }
    elements[index] = old;
}

@Override
public E remove() {
    emptyCheck();
    E old = elements[0];
    elements[0] = elements[size - 1];
    elements[size - 1] = null;
    size--;
    siftDown(0);
    return old;
}

3.6 replace

删除堆顶元素并插入一个新的元素

    @Override
    public E replace(E element) {
        elementNotNullCheck(element);
        E old = null;
        if (size == 0) {
            elements[0] = element;
            size ++;
        }else{
            old = elements[0];
            elements[0] = element;
            siftDown(0);
        }
        return old;
    }

3.6 批量建堆

批量建堆指的是给定一个任意数组,将此数组建立成堆

  • 老土方法
    在这里插入图片描述
  • 自上而下的上滤

在这里插入图片描述
图中红色的节点是需要执行上滤操作的;判断条件是只要有父节点就需要上滤;

  • 自下而上的下滤

在这里插入图片描述
图中红色的节点是需要执行下滤操作的;判断条件是只要有子节点就需要下滤;


效率对比:

在这里插入图片描述

  • 自上而下的上滤时间复杂度:O ( nlogn )
  • 自下而上的下滤时间复杂度:O(nlogk)

自下而上的下虑效率要高一些;我们可以从上面这幅图观察出来,层越低,该层的节点数目越多;自上而下的上滤则存在大量的节点从最底层要比较到顶层;而自下而上的下虑,只有顶部的少数节点需要从顶层比较到底层;

3.7 全部代码

接口

package 二叉堆;

public interface Heap<E> {
    int size();    // 元素的数量

    boolean isEmpty();    // 是否为空

    void clear();    // 清空

    void add(E element);     // 添加元素

    E get();    // 获得堆顶元素

    E remove(); // 删除堆顶元素

    E replace(E element); // 删除堆顶元素的同时插入一个新元素
}

抽象类

package 二叉堆;

import java.util.Comparator;

public abstract class AbstractHeap<E> implements Heap<E> {

    protected int size;
    protected Comparator<E> comparator;


    public AbstractHeap(){}

    public AbstractHeap(Comparator<E> comparator){
        this.comparator = comparator;
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

}

实现类

package 二叉堆;

import java.util.Comparator;

public class BinaryHeap<E> extends AbstractHeap<E> {


    private E[] elements;
    private static final int DEFAULT_CAPACITY = 10;

    public BinaryHeap(E[] elements, Comparator<E> comparator)  {
        super(comparator);

        if (elements == null || elements.length == 0) {
            this.elements = (E[]) new Object[DEFAULT_CAPACITY];
        } else {
            size = elements.length;
            int capacity = Math.max(elements.length, DEFAULT_CAPACITY);
            this.elements = (E[]) new Object[capacity];
            for (int i = 0; i < elements.length; i++) {
                this.elements[i] = elements[i];
            }
            heapify();
        }
    }

    public BinaryHeap(E[] elements)  {
        this(elements, null);
    }

    public BinaryHeap(Comparator<E> comparator) {
        this(null, comparator);
    }

    public BinaryHeap() {
        this(null, null);
    }

    private int compare(E e1,E e2){
        if(comparator == null){
           return  ((Comparable<E>)e1).compareTo(e2);
        }else{
            return comparator.compare(e1,e2);
        }
    }

    private void nullCheck(E e){
        if(e == null){
            throw new RuntimeException("param can not be null");
        }
    }


    @Override
    public void clear() {
        for (int i = 0; i < size; i++) {
            elements[i] = null;
        }
        size = 0;
    }

    private void ensureCapacity(int size){
        if(size > elements.length){
            E[] newArr = (E[])new Object[elements.length << 1];
            for (int i = 0; i < size; i++) {
                newArr[i] = elements[i];
            }
            elements = newArr;
        }
    }

    @Override
    public void add(E element) {
        nullCheck(element);
        ensureCapacity(size+1);
        elements[size++] = element;
        siftUp(size - 1);
    }

    private void siftUp(int index){
        //todo 找应该放置的位置
        E cur = elements[index];
        //当index > 0的时候才有父节点
        while(index > 0){
            int parentIndex = (index  - 1) >> 1;
            E parent = elements[parentIndex];

            if(compare(cur,parent) <= 0) break;//找到位置 就是index的位置

            elements[index] = parent;
            index = parentIndex;
        }
        elements[index] = cur;
    }


    @Override
    public E get() {
        emptyCheck();
        return elements[0];
    }

    private void emptyCheck(){
       if(size == 0){
           throw new RuntimeException("empty heap");
       }
    }

    @Override
    public E remove() {
        //用最后一个节点覆盖根结点
        //删除最后一个节点
        //下虑 和 最大的子节点互换
        emptyCheck();
        E root = elements[0];
        int lastIndex = --size;
        elements[0]  = elements[lastIndex];
        elements[lastIndex] = null;

        siftDown(0);

        return root;
    }

    private void siftDown(int index){
        E cur = elements[index];

        int haveChild = (size - 2) >> 1;
        //index <= haveChild 有子节点
        while(index <= haveChild){

            int leftchildIndex = (index << 1) + 1;//左子节点
            int rightChildIndex = leftchildIndex + 1;//右子节点
            int maxChildIndex = leftchildIndex;

            //有右子节点的时候
            if( rightChildIndex < size ){
               maxChildIndex = compare(elements[leftchildIndex],elements[rightChildIndex]) > 0? leftchildIndex:rightChildIndex;
            }
            E maxchild = elements[maxChildIndex];

            if(compare(cur,maxchild) > 0) break; //index 就是目标位置

            elements[index] = maxchild;
            index = maxChildIndex;
        }
        elements[index] = cur;
    }


    /**
     * 删除堆顶元素的同时插入一个新元素
     * @param element
     * @return
     */
    @Override
    public E replace(E element) {
        nullCheck(element);

        E root = null;

        if (size == 0) {
            elements[0] = element;
            size++;
        } else {
            root = elements[0];
            elements[0] = element;
            siftDown(0);
        }
        return root;
    }


    /**
     * 批量建堆
     */
    private void heapify() {
        // 自上而下的上滤
//		for (int i = 1; i < size; i++) {
//			siftUp(i);
//		}

        // 自下而上的下滤
        for (int i = (size >> 1) - 1; i >= 0; i--) {
            siftDown(i);
        }
    }

    public static void main(String[] args) {
        Integer[] arr = {22,54,13,57,1,5,8,4,3,46,12,47,23};
        BinaryHeap<Integer> heap = new BinaryHeap<Integer>(arr, (o1,o2) -> o2-o1);

        System.out.println(heap.get());

    }
}

3.8 构建一个最小堆

在写完最大堆以后,实现最小堆不需要修改源代码,只需要在创建堆时,传入与最大堆比较方式相反的比较器即可。

4.堆的应用

4.1 TOP K 问题

什么是 TopK 问题

从 n 个整数中,找出最大的前 k 个数(k << n)
例如:从100万个整数中找出最大的100个整数

  • 如果使用排序算法进行全排序,需要 O ( n l o g n ) 的时间复杂度
  • 如果使用二叉堆来解决,可以使用 O ( n l o g k )的时间复杂度来解决

TOPK 实现方法
(1)新建一个小顶堆,扫描 n 个整数先将遍历到的前 k 个数放入堆中
(2)从第 k+1 个数开始,如果大于堆顶元素,就使用 replace 操作
(删除堆顶元素,将第k+1个数添加到堆中)
(3)扫描完毕后,堆中剩下的就是最大的前 k 个数

public static void main(String[] args) {
	// 新建一个小顶堆
	BinaryHeap<Integer> heap = new BinaryHeap<>(new Comparator<Integer>() {
		public int compare(Integer o1, Integer o2) {
			return o2 - o1;
		}
	});
	
	// 找出最大的前k个数
	int k = 3;
	Integer[] data = {51, 30, 39, 92, 74, 25, 16, 93, 
			91, 19, 54, 47, 73, 62, 76, 63, 35, 18, 
			90, 6, 65, 49, 3, 26, 61, 21, 48};
	for (int i = 0; i < data.length; i++) {
		if (heap.size() < k) { // 前k个数添加到小顶堆
			heap.add(data[i]); // logk
		} else if (data[i] > heap.get()) { // 如果是第 k + 1 个数,并且大于堆顶元素
			heap.replace(data[i]); // logk
		}
	}
	// O(nlogk)
}

如果是找出最小的前 k 个数呢?

  • 用大顶堆
  • 如果小于堆顶元素,就使用 replace 操作
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值