1. 程式人生 > 其它 >堆(優先佇列)之習題分析

堆(優先佇列)之習題分析

技術標籤:演算法佇列資料結構演算法java堆疊

堆(優先佇列)之習題分析

一、堆以及優先佇列的概念

(一)、堆的概念

嚴格來講,堆有不同的種類,但是我們在演算法學習中,主要用的還是二叉堆,而二叉堆有最大堆和最小堆之分。

最大(最小)堆是一棵每一個節點的鍵值都不小於(大於)其孩子(如果存在)的鍵值的樹。大頂堆是一棵完全二叉樹,同時也是一棵最大樹。小頂堆是一棵完全完全二叉樹,同時也是一棵最小樹。

需要注意的問題是:堆中的任一子樹也還是堆,即大頂堆的子樹也都是大頂堆,小頂堆同樣。

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();