堆(优先队列)之习题分析

本文详细介绍了堆(优先队列)的概念,包括最大堆和最小堆,以及Java中的PriorityQueue实现。接着讨论了如何利用优先队列解决数据流中位数问题,通过维护两个堆来高效地获取中位数。最后,提出了寻找数据流中第K大元素的三种方法,包括快速队列和优先队列的解决方案。
摘要由CSDN通过智能技术生成

一、堆以及优先队列的概念

(一)、堆的概念

严格来讲,堆有不同的种类,但是我们在算法学习中,主要用的还是二叉堆,而二叉堆有最大堆和最小堆之分。

最大(最小)堆是一棵每一个节点的键值都不小于(大于)其孩子(如果存在)的键值的树。大顶堆是一棵完全二叉树,同时也是一棵最大树。小顶堆是一棵完全完全二叉树,同时也是一棵最小树。

需要注意的问题是:堆中的任一子树也还是堆,即大顶堆的子树也都是大顶堆,小顶堆同样。

img

图一为大顶堆,图二为小顶堆

(二)、优先队列——PriorityQueue

1、优先队列的概念

​ PriorityQueue类在Java1.5中引入。PriorityQueue是基于优先堆的一个无界队列,这个优先队列中的元素可以默认自然排序或者通过提供的Comparator(比较器)在队列实例化的时排序。要求使用Java Comparable和Comparator接口给对象排序,并且在排序时会按照优先级处理其中的元素。

2、优先队列的数据结构

​ 优先队列底层的数据结构其实是一颗二叉堆,优先队列使用二叉堆的特点,可以使得插入的数据自动排序(升序或者是降序)

​ 二叉堆:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-djYv4zYK-1609923407678)(https://pics3.baidu.com/feed/f2deb48f8c5494ee1523ac36af9877f899257e14.jpeg?token=43d94f8e2c6a6a839bde47f73f4dc955)]

​ 特点:

  • 二叉堆是一个完全二叉树
  • 根节点总是大于左右子节点(大顶堆),或者是小于左右子节点(小顶堆)
3、优先队列的源码分析
(1)、属性
/**
 * 默认初始量
 */
private static final int DEFAULT_INITIAL_CAPACITY = 11;

/**
 * 维持一个队列:因为基于二叉堆来实现优先队列,queue[i]的子节点为queue[2*i+1]/queue[2*i+2]
 */
transient Object[] queue; 

/**
 * 优先队列中元素个数
 */
private int size = 0;

/**
 * 比较器:用于降序或者是比较自定义的对象
 */
private final Comparator<? super E> comparator;

/**
 * 优先队列的结构:被修改的次数
 */
transient int modCount = 0;
(2)、构造方法
/**
 * 创建一个 PriorityQueue,并根据其自然顺序对元素进行排序
 */
public PriorityQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
}

/**
 * 使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序
 */
public PriorityQueue(int initialCapacity) {
    this(initialCapacity, null);
}

/**
 * 创建包含指定 collection 中元素的 PriorityQueue
 */
public PriorityQueue(Comparator<? super E> comparator) {
    this(DEFAULT_INITIAL_CAPACITY, comparator);
}

/**
 * 使用指定的初始容量创建一个 PriorityQueue,并根据指定的比较器对元素进行排序
 */
public PriorityQueue(int initialCapacity,
                     Comparator<? super E> comparator) {
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.queue = new Object[initialCapacity];
    this.comparator = comparator;
}

/**
 * 创建包含指定 collection 中元素的 PriorityQueue
 */
@SuppressWarnings("unchecked")
public PriorityQueue(Collection<? extends E> c) {
    if (c instanceof SortedSet<?>) {
        SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
        this.comparator = (Comparator<? super E>) ss.comparator();
        initElementsFromCollection(ss);
    }
    else if (c instanceof PriorityQueue<?>) {
        PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
        this.comparator = (Comparator<? super E>) pq.comparator();
        initFromPriorityQueue(pq);
    }
    else {
        this.comparator = null;
        initFromCollection(c);
    }
}

/**
 * 创建包含指定优先级队列元素的 PriorityQueue
 */
@SuppressWarnings("unchecked")
public PriorityQueue(PriorityQueue<? extends E> c) {
    this.comparator = (Comparator<? super E>) c.comparator();
    initFromPriorityQueue(c);
}

/**
 * 创建包含指定有序set元素的PriorityQueue
 */
@SuppressWarnings("unchecked")
public PriorityQueue(SortedSet<? extends E> c) {
    this.comparator = (Comparator<? super E>) c.comparator();
    initElementsFromCollection(c);
}
(3)、常用方法
/**
 * 插入一个元素
 */
public boolean add(E e) {
    return offer(e);
}

/**
 * 插入一个元素
 */
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}

/**
 * 查询队顶元素
 */
@SuppressWarnings("unchecked")
public E peek() {
    return (size == 0) ? null : (E) queue[0];
}

/**
 * 删除一个元素,并返回删除的元素
 */
@SuppressWarnings("unchecked")
public E poll() {
    if (size == 0)
        return null;
    int s = --size;
    modCount++;
    E result = (E) queue[0];
    E x = (E) queue[s];
    queue[s] = null;
    if (s != 0)
        siftDown(0, x);
    return result;
}

/**
 * 查询对象o的索引
 */
private int indexOf(Object o) {
    if (o != null) {
        for (int i = 0; i < size; i++)
            if (o.equals(queue[i]))
                return i;
    }
    return -1;
}

/**
 * 删除一个元素
 */
public boolean remove(Object o) {
    int i = indexOf(o);
    if (i == -1)
        return false;
    else {
        removeAt(i);
        return true;
    }
}

/**
 * 判断是否包含该元素
 */
public boolean contains(Object o) {
    return indexOf(o) != -1;
}

二、数据流的中位数

(一)、题目需求

​ 如何得到一个数据流中的中位数?

​ 如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

例如,

[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:

void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。
示例 1:

输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]

示例 2:

输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]

限制:

最多会对 addNum、findMedian 进行 50000 次调用。

(二)、解法

PriorityQueue<Integer> maxHeap;
PriorityQueue<Integer> minHeap;

/**
 * initialize your data structure here.
 */
public MedianFinder() {
    maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;
        }
    });
    minHeap = new PriorityQueue<>();
}

public void addNum(int num) {
    if (maxHeap.size() != minHeap.size()) {
        minHeap.add(num);
        maxHeap.add(minHeap.poll());
    } else {
        maxHeap.add(num);
        minHeap.add(maxHeap.poll());
    }
}

public double findMedian() {
    if (maxHeap.size() != minHeap.size()) {
        return minHeap.peek();
    } else {
        return (minHeap.peek() + maxHeap.peek()) / 2.0;
    }
}

(三)、代码分析

1、定义大顶堆与小顶堆

大顶堆:存储输入数据流中排序后,前半段的元素。堆顶为前半段元素的最后一位元素

小顶堆:存储输入数据流中排序后,后半段的元素。堆顶为后半段元素的第一位元素

PriorityQueue<Integer> maxHeap;
PriorityQueue<Integer> minHeap;

2、初始化大顶堆与小顶堆

public MedianFinder() {
    maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;
        }
    });
    minHeap = new PriorityQueue<>();
}

3、进行元素添加操作,并判断大顶堆与小顶堆的数量。

(1)、若大顶堆与小顶堆的数量不一致则先将其放入小顶堆中进行排序,小顶堆堆顶元素出队列,同时小顶堆重新进行排序。出队的元素进入大顶堆,同时大顶堆重新进行排序。

(2)、若大顶堆与小顶堆的数量一致则先将其放入大顶堆中进行排序,大顶堆堆顶元素出队列,同时大顶堆重新进行排序。出队的元素进入小顶堆,同时小顶堆重新进行排序。

public void addNum(int num) {
    if (maxHeap.size() != minHeap.size()) {
        minHeap.add(num);
        maxHeap.add(minHeap.poll());
    } else {
        maxHeap.add(num);
        minHeap.add(maxHeap.poll());
    }
}

4、计算中位数

(1)、若大顶堆与小顶堆的数量不一致,则数据流数量为奇数,中位数为小顶堆的堆顶元素。

(2)、若大顶堆与小顶堆的数量不一致,则数据流数量为偶数,中位数为小顶堆的堆顶元素加上大顶堆的堆顶元素之和/2。

public double findMedian() {
    if (maxHeap.size() != minHeap.size()) {
        return minHeap.peek();
    } else {
        return (minHeap.peek() + maxHeap.peek()) / 2.0;
    }
}

三、第K大元素

(一)、题目需求

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

示例 2:

输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

说明:

你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。

(二)、解法

1、快速队列
public int findKthLargestQuickSort(int[] nums, int k) {
    if (nums == null || nums.length == 0 || k < 1 || k > nums.length) {
        return -1;
    }
    sort(nums, 0, nums.length - 1);
    return nums[nums.length - k];
}

private void sort(int[] arr, int start, int end) {
    if (start >= end) {
        return;
    }
    int left = start;
    int right = end;
    int pivot = arr[start];
    while (left <= right) {
        while (left <= right && arr[left] < pivot) {
            left++;
        }
        while (left <= right && arr[right] > pivot) {
            right--;
        }
        if (left <= right) {
            int temp = arr[left];
            arr[left] = arr[right];
            arr[right] = temp;
            left++;
            right--;
        }
    }
    sort(arr, start, right);
    sort(arr, left, end);
}
2、快速队列改进版
public int findKthLargestQuickSort(int[] nums, int k) {
    if (nums == null || nums.length == 0 || k < 1 || k > nums.length) {
        return -1;
    }
    return partition(nums, 0, nums.length - 1, nums.length - k);
}

private int partition(int[] arr, int start, int end, int k) {
    if (start >= end) {
        return arr[k];
    }
    int left = start;
    int right = end;
    int mid = start + (end - start) / 2;
    int pivot = arr[mid];
    while (left <= right) {
        while (left <= right && arr[left] < pivot) {
            left++;
        }
        while (left <= right && arr[right] > pivot) {
            right--;
        }
        if (left <= right) {
            int temp = arr[left];
            arr[left] = arr[right];
            arr[right] = temp;
            left++;
            right--;
        }
    }
    if (k <= right) {
        partition(arr, start, right, k);
    }
    if (k >= left) {
        partition(arr, left, end, k);
    }
    return arr[k];
}
3、优先队列
public int findKthLargest(int[] nums, int k) {
    if (nums == null || nums.length == 0 || k < 1 || k > nums.length) {
        return -1;
    }
    PriorityQueue<Integer> heap = new PriorityQueue<>();

    for (int num : nums) {
        heap.add(num);
        if (heap.size() > k) {
            heap.poll();
        }
    }
    return heap.peek();
}

(三)、代码分析

1、定义并初始化小顶堆优先队列进行辅助

PriorityQueue<Integer> heap = new PriorityQueue<>();

2、逐个加入队列中,并进行判断当前队列元素数量是否超过k

for (int num : nums) {
    heap.add(num);
    if (heap.size() > k) {
        heap.poll();
    }
}

3、返回第K个大的元素——堆顶元素

return heap.peek();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值