堆(優先佇列)之習題分析
堆(優先佇列)之習題分析
一、堆以及優先佇列的概念
(一)、堆的概念
嚴格來講,堆有不同的種類,但是我們在演算法學習中,主要用的還是二叉堆,而二叉堆有最大堆和最小堆之分。
最大(最小)堆是一棵每一個節點的鍵值都不小於(大於)其孩子(如果存在)的鍵值的樹。大頂堆是一棵完全二叉樹,同時也是一棵最大樹。小頂堆是一棵完全完全二叉樹,同時也是一棵最小樹。
需要注意的問題是:堆中的任一子樹也還是堆,即大頂堆的子樹也都是大頂堆,小頂堆同樣。
圖一為大頂堆,圖二為小頂堆
(二)、優先佇列——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();