二叉堆和优先队列的学习笔记

一、什么是二叉堆?

首先我们要知道堆(Heap)是一类数据结构,它们拥有树状结构,且能够保证父节点比子节点大(或小)。

当根节点保存堆中最大值时,称为大根堆;反之,则称为小根堆

二叉堆(Binary Heap)是最简单、常用的堆,是一棵符合堆的性质的完全二叉树。它可以实现插入或删除某个值,并且查询最大(或最小)值。

  • 其主要操作有:sink(下沉)和 swim(上浮)

    ps:该操作用于维护二叉堆的性质

  • 其主要应用有:

  1. 排序方法(堆排序)
  2. 数据结构(优先队列)

二、二叉堆为什么是一颗完全二叉树?

二叉堆在逻辑上其实是一种特殊的二叉树(完全二叉树),只不过存储在数组里。一般的链表二叉树我们操作节点的指针,而在数组里我们把数组索引作为指针

// 父节点的索引
int parent(int root) {
    
    
    return root / 2;
}
// 左孩子的索引
int left(int root) {
    
    
    return root * 2;
}
// 右孩子的索引
int right(int root) {
    
    
    return root * 2 + 1;
}

在这里插入图片描述

由图可知,二叉堆就是一颗完全二叉树,arr[1]为整棵树的根,每个节点的

父节点和左右孩子的索引都可以通过简单的运算得到

三、如何实现上浮和下沉操作?

对于一个破坏堆性质的节点,我们可以使其上浮下沉。最差情况是:上浮到顶或是下沉到底。(以下代码以大根堆为例)

扫描二维码关注公众号,回复: 15185267 查看本文章

3.1破坏性质的情况

  • 如果某个节点 A 比它的子节点(中的一个)小,那么 A 就不配做父节点,应该下去,下面那个更大的节点上来做父节点,这就是对 A 进行下沉
  • 如果某个节点 A 比它的父节点大,那么 A 不应该做子节点,应该把父节点换下来,自己去做父节点,这就是对 A 的上浮

3.2上浮

上浮就是不断与父节点比较,如果比父节点大就与之交换,直到不大于父节点或成为根节点为止。

private void swim(int x) {
    
    
        // 如果浮到堆顶,就不能再上浮了
        while (x > 1 && less(parent(x), x)) {
    
    
            // 如果第 x 个元素比上层大
            // 将 x 换上去
            swap(parent(x), x);
            x = parent(x);
        }
    }
void swim(int n)
{
    
    
    for (int i = n; i > 1 && heap[i] > heap[i / 2]; i /= 2)
        swap(heap[i], heap[i / 2]);
}

3.3下沉

父节点不断与较大的子节点比较,如果比它小就与之交换,直到不小于任何子节点或成为叶子节点为止。

之所以要与较大的子节点比较,是为了保证交换上来的节点比两个子节点都大。

private void sink(int x) {
    
    
        // 如果沉到堆底,就沉不下去了
        while (left(x) <= size) {
    
    
            // 先假设左边节点较大
            int max = left(x);
            // 如果右边节点存在,比一下大小
            if (right(x) <= size && less(max, right(x)))
                max = right(x);
            // 结点 x 比俩孩子都大,就不必下沉了
            if (less(max, x)) break;
            // 否则,不符合最大堆的结构,下沉 x 结点
            swap(x, max);
            x = max;
        }
    }
int son(int n) // 找到需要交换的那个子节点,即找到最大子节点
{
    
    
    return n * 2 + (n * 2 + 1 <= size && heap[n * 2 + 1] > heap[n * 2]);
}
void sink(int n)
{
    
    
    for (int i = n, t = son(i); t <= size && heap[t] > heap[i]; i = t, t = son(i))
        swap(heap[i], heap[t]);
}

3.4 疑问:这两个操作不是互逆吗,所以上浮的操作一定能用下沉来完成,为什么还要费劲写两个方法?

是的,操作是互逆等价的,但是最终我们的操作只会在堆底和堆顶进行

也就是插入新元素时将该元素添加到堆底的最后面,删除某节点时把堆顶元素 A 和堆底最后的元素 B 对调,然后删除 A,最后让 B 下沉到正确位置。

显然堆底的「错位」元素需要上浮,堆顶的「错位」元素需要下沉。

四、其他操作

注意:插入和删除操作这两个方法就是建立在 swimsink 上的。

4.1插入操作

insert 方法先把要插入的元素添加到堆底的最后,然后让其上浮到正确位置

 public void insert(Key e) {
    
    
        size++;
        // 先把新元素加到最后
        heap[size] = e;
        // 然后让它上浮到正确的位置
        swim(size);
    }

4.2删除操作

delMax 方法先把堆顶元素 A 和堆底最后的元素 B 对调,然后删除 A,最后让 B 下沉到正确位置。

public Key delMax() {
    
    
    // 最大堆的堆顶就是最大元素
    Key max = heap[1];
    // 把这个最大元素换到最后,删除之
    swap(1, size);
    heap[size] = null;
    size--;
    // 让 pq[1] 下沉到正确位置
    sink(1);
    return max;
}

五、时间复杂度

插入和删除元素的时间复杂度为 O(logK)K 为当前二叉堆(优先级队列)中的元素总数。

因为我们时间复杂度主要花费在 sink 或者 swim 上,而不管上浮还是下沉,最多也就树(堆)的高度,也就是 log 级别

六、基于二叉堆实现的优先级队列是什么?

优先级队列这种数据结构有一个很有用的功能,你插入或者删除元素的时候,元素会自动排序,这底层的原理就是二叉堆的操作。

数据结构的功能无非增删查改,优先级队列有两个主要 API:

insert` 插入一个元素

delMax删除最大元素(如果底层用最小堆,那么就是delMin)。

优先级队列的代码框架:

其实这个框架也是二叉堆的代码框架

以大根堆为底层

注意:这里用到 Java 的泛型,Key 可以是任何一种可比较大小的数据类型,比如 Integer 等类型。

public class MaxPQ
    <Key extends Comparable<Key>> {
    
    
    // 存储元素的数组
    private Key[] pq;
    // 当前 Priority Queue 中的元素个数
    private int size = 0;

    public MaxPQ(int cap) {
    
    
        // 索引 0 不用,所以多分配一个空间
        pq = (Key[]) new Comparable[cap + 1];
    }

    /* 返回当前队列中最大元素 */
    public Key max() {
    
    
        return pq[1];
    }

    /* 插入元素 e */
    public void insert(Key e) {
    
    ...}

    /* 删除并返回当前队列中最大元素 */
    public Key delMax() {
    
    ...}

    /* 上浮第 x 个元素,以维护最大堆性质 */
    private void swim(int x) {
    
    ...}

    /* 下沉第 x 个元素,以维护最大堆性质 */
    private void sink(int x) {
    
    ...}

    /* 交换数组的两个元素 */
    private void swap(int i, int j) {
    
    
        Key temp = pq[i];
        pq[i] = pq[j];
        pq[j] = temp;
    }

    /* pq[i] 是否比 pq[j] 小? */
    private boolean less(int i, int j) {
    
    
        return pq[i].compareTo(pq[j]) < 0;
    }

    /* 还有 left, right, parent 三个方法 */
}

七、总结

二叉堆就是一种完全二叉树,所以适合存储在数组中,而且二叉堆拥有一些特殊性质。

二叉堆的操作很简单,主要就是上浮和下沉,来维护堆的性质(堆有序),核心代码也就十行。

优先级队列是基于二叉堆实现的,主要操作是插入和删除。插入是先插到最后,然后上浮到正确位置;删除是调换位置后再删除,然后下沉到正确位置。核心代码也就十行。

八、PriorityQueue类提供了6种在Java中构造优先级队列的方法。

  • PriorityQueue():使用默认初始容量(11)构造空队列,该容量根据其自然顺序对其元素进行排序。
  • PriorityQueue(Collection c):构造包含指定集合中元素的空队列。
  • PriorityQueue(int initialCapacity):构造具有指定初始容量的空队列,该容量根据其自然顺序对其元素进行排序。
  • PriorityQueue(int initialCapacity,Comparator comparator):构造具有指定初始容量的空队列,该容量根据指定的比较器对其元素进行排序。
  • PriorityQueue(PriorityQueue c):构造包含指定优先级队列中元素的空队列。
  • PriorityQueue(SortedSet c):构造包含指定有序集合中元素的空队列。

九、Java PriorityQueue方法

PriorityQueue类下面给出了重要的方法,你应该知道。

  • boolean add(object):将指定的元素插入此优先级队列。
  • boolean offer(object):将指定的元素插入此优先级队列。
  • boolean remove(object):从此队列中删除指定元素的单个实例(如果存在)。
  • Object poll():检索并删除此队列的头部,如果此队列为空,则返回null。
  • Object element():检索但不删除此队列的头部,如果此队列为空,则返回null。
  • Object peek():检索但不删除此队列的头部,如果此队列为空,则返回null。
  • void clear():从此优先级队列中删除所有元素。
  • Comparator comparator():返回用于对此队列中的元素进行排序的比较器,如果此队列根据其元素的自然顺序排序,则返回null。
  • boolean contains(Object o):如果此队列包含指定的元素,则返回true。
  • Iterator iterator():返回此队列中元素的迭代器。
  • int size():返回此队列中的元素数。
  • Object [] toArray():返回包含此队列中所有元素的数组。

猜你喜欢

转载自blog.csdn.net/weixin_52055811/article/details/129355536
今日推荐