一、定義
堆是一種基于樹的數(shù)據(jù)結(jié)構(gòu),通常用完全二叉樹實現(xiàn)。
- 完全二叉樹:除了最后一層外,其他層的節(jié)點都達(dá)到最大,并且最后一層的節(jié)點從左到右排列。
- 滿二叉樹:每一層的節(jié)點都被完全填滿的二叉樹,并且每個非葉子節(jié)點都有兩個子節(jié)點
完全二叉樹
1
/ \
2 3
/ \ /
4 5 6
滿二叉樹
1
/ \
2 3
/ \ / \
4 5 6 7
滿二叉樹也是完全二叉樹
1.堆的特性
大頂堆(Max Heap)和小頂堆(Min Heap)是完全二叉樹的一種特殊形式
- 最頂層的節(jié)點(沒有父親)稱之為 root 根節(jié)點
- 在大頂堆中,任意節(jié)點 C 與它的父節(jié)點 P 符合 P.value ≥ C.value
- 小頂堆中,任意節(jié)點 C 與它的父節(jié)點 P 符合 P.value ≤ C.value
大頂堆:
10
/ \
5 3
/ \
2 4
小頂堆:
2
/ \
3 4
/ \
5 6
2.堆的存儲
完全二叉樹這種非線性的結(jié)構(gòu)可以使用數(shù)組這種線性的結(jié)構(gòu)來表示:
image.png
節(jié)點在數(shù)組中索引的計算:
從索引 0 開始存儲節(jié)點數(shù)據(jù)
- 節(jié)點
的父節(jié)點索引為
,(
)
- 節(jié)點
的左孩子節(jié)點索引為
,右孩子節(jié)點索引為
,計算的結(jié)果需要小于size
從索引 1 開始存儲節(jié)點數(shù)據(jù)
- 節(jié)點
的父節(jié)點索引為
,(
)
- 節(jié)點
的左孩子節(jié)點索引為
,右孩子節(jié)點索引為
,計算的結(jié)果需要小于size
二、實現(xiàn)優(yōu)先級隊列
1.無序數(shù)組實現(xiàn)
package com.hcx.algorithm.queue;
/**
* @Title: PriorityQueue1.java
* @Package com.hcx.algorithm.queue
* @Description: 優(yōu)先級隊列:無序數(shù)組實現(xiàn)
* 入隊:跟普通隊列一樣
* 出隊:優(yōu)先級最高的出隊
* @Author: hongcaixia
* @Date: 2025/1/12 17:33
* @Version V1.0
*/
public class PriorityQueue1<E extends Priority> implements Queue<E> {
Priority[] array;
// 數(shù)組當(dāng)前大小
int size;
public PriorityQueue1(int capacity) {
array = new Priority[capacity];
}
@Override
public boolean offer(E e) {
if(isFull()){
return false;
}
array[size++] = e;
return true;
}
@Override
public E poll() {
if (isEmpty()) {
return null;
}
// 找到優(yōu)先級最高的元素出隊
int maxIndex = 0;
for (int i = 0; i < size; i++) {
if (array[i].getPriority() > array[maxIndex].getPriority()) {
maxIndex = i;
}
}
E e = (E) array[maxIndex];
// 出隊后,該位置后的元素往前移動
if (maxIndex < size - 1) {
System.arraycopy(array, maxIndex + 1, array, maxIndex, size - maxIndex - 1);
}
// help gc
array[--size] = null;
return e;
}
@Override
public E peek() {
if (isEmpty()) {
return null;
}
// 找到優(yōu)先級最高的元素
int maxIndex = 0;
for (int i = 0; i < size; i++) {
if (array[i].getPriority() > array[maxIndex].getPriority()) {
maxIndex = i;
}
}
E e = (E) array[maxIndex];
return e;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean isFull() {
return size == array.length;
}
}
優(yōu)先級接口:
public interface Priority {
/**
* 返回對象的優(yōu)先級(數(shù)字越大,優(yōu)先級越高)
* @return
*/
int getPriority();
}
2.有序數(shù)組實現(xiàn)
package com.hcx.algorithm.queue;
/**
* @Title: PriorityQueue1.java
* @Package com.hcx.algorithm.queue
* @Description: 優(yōu)先級隊列:有序數(shù)組實現(xiàn)
* 入隊:跟普通隊列一樣
* 出隊:優(yōu)先級最高的出隊
* @Author: hongcaixia
* @Date: 2025/1/12 17:33
* @Version V1.0
*/
public class PriorityQueue2<E extends Priority> implements Queue<E> {
Priority[] array;
// 數(shù)組當(dāng)前大小
int size;
public PriorityQueue2(int capacity) {
array = new Priority[capacity];
}
@Override
public boolean offer(E e) {
if (isFull()) {
return false;
}
// 按照優(yōu)先級,插入到正確的位置
int index = size - 1;
// 從數(shù)組末尾開始往前找,如果數(shù)組中元素的優(yōu)先級比當(dāng)前的高,就繼續(xù)往前,同時把數(shù)組的元素往后移(空出位置給當(dāng)前元素)
while (index >= 0 && array[index].getPriority() > e.getPriority()) {
array[index + 1] = array[index];
index--;
}
// 找到了比當(dāng)前優(yōu)先級小的則退出了循環(huán),那么index所指向的上一個就是要插入的位置
array[index + 1] = e;
size++;
return true;
}
@Override
public E poll() {
if (isEmpty()) {
return null;
}
E e = (E) array[size - 1];
// help gc
array[--size] = null;
return e;
}
@Override
public E peek() {
if (isEmpty()) {
return null;
}
return (E) array[size - 1];
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean isFull() {
return size == array.length;
}
}
3.大頂堆實現(xiàn)
package com.hcx.algorithm.queue;
/**
* @Title: PriorityQueue1.java
* @Package com.hcx.algorithm.queue
* @Description: 優(yōu)先級隊列:使用大頂堆實現(xiàn)
* @Author: hongcaixia
* @Date: 2025/1/12 17:33
* @Version V1.0
*/
public class PriorityQueue3<E extends Priority> implements Queue<E> {
Priority[] array;
// 數(shù)組當(dāng)前大小
int size;
public PriorityQueue3(int capacity) {
array = new Priority[capacity];
}
/**
* 1.新元素添加到數(shù)組的尾部
* 2.要符合大頂堆的特性,還需要對堆進(jìn)行調(diào)整:
* 循環(huán)比較新元素與父節(jié)點的優(yōu)先級,如果父節(jié)點的優(yōu)先級低,則往下移動到子節(jié)點的位置,直到父節(jié)點的優(yōu)先級更高或者childIndex==0
* @param e
* @return
*/
@Override
public boolean offer(E e) {
if (isFull()) {
return false;
}
int childIndex = size;
int parentIndex = (childIndex - 1) / 2;
// array[childIndex] = e;
while (childIndex > 0 && e.getPriority() > array[parentIndex].getPriority()) {
// 把父節(jié)點放到子節(jié)點上 上層的元素依次往下一層放
array[childIndex] = array[parentIndex];
// 兩個指針繼續(xù)往上找,直到不符合條件為止
childIndex = parentIndex;
parentIndex = (childIndex - 1) / 2;
}
array[childIndex] = e;
size++;
return true;
}
/**
* 1.移除并返回優(yōu)先級最高的元素,即根元素
* 2.要符合堆的特性,還需要對堆進(jìn)行調(diào)整
* - 把堆頂元素與最末尾元素進(jìn)行交換,移除并返回最末尾的元素,大小減1
* - 調(diào)整堆:對堆頂?shù)母匾来闻c孩子節(jié)點作比較,如果優(yōu)先級比孩子節(jié)點小,往下移動,直到父節(jié)點優(yōu)先級大于孩子節(jié)點優(yōu)先級或者沒有孩子節(jié)點為止。
* @return
*/
@Override
public E poll() {
if (isEmpty()) {
return null;
}
// 1.堆頂元素和最末尾元素交換
Priority top = array[0];
array[0] = array[size - 1];
array[size - 1] = top;
// 2.數(shù)組大小-1
size--;
// 要返回的元素
Priority e = array[size];
// help gc
array[size] = null;
// 3.調(diào)整堆頂元素位置,依次往下找到正確的位置
downToProper(0);
return (E) e;
}
/**
* 將父節(jié)點元素下沉到正確的位置
* 從堆頂開始,依次將父元素與孩子節(jié)點中較大的進(jìn)行交換,直到父元素大于兩個孩子,或者沒有孩子為止。
* @param parentIndex 父節(jié)點索引
*/
public void downToProper(int parentIndex) {
// 該節(jié)點的左孩子
int leftChildIndex = parentIndex * 2 + 1;
// 右孩子
int rightChildIndex = leftChildIndex + 1;
// 父節(jié)點索引 先假設(shè)本身的優(yōu)先級最高,如果有比他高的 就替換掉他
int maxIndex = parentIndex;
if (leftChildIndex < size && array[leftChildIndex].getPriority() > array[parentIndex].getPriority()) {
// 左孩子節(jié)點的優(yōu)先級比父節(jié)點大,將maxIndex 設(shè)置為左孩子索引
maxIndex = leftChildIndex;
}
if (rightChildIndex < size && array[rightChildIndex].getPriority() > array[parentIndex].getPriority()) {
// 右孩子節(jié)點的優(yōu)先級比父節(jié)點大,將maxIndex設(shè)置為右孩子索引
maxIndex = rightChildIndex;
}
// 說明有更大的孩子節(jié)點
if (maxIndex != parentIndex) {
// 把父節(jié)點和該節(jié)點交換
Priority temp = array[maxIndex];
array[maxIndex] = array[parentIndex];
array[parentIndex] = temp;
// 繼續(xù)往下找
downToProper(maxIndex);
}
}
@Override
public E peek() {
if (isEmpty()) {
return null;
}
return (E) array[0];
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean isFull() {
return size == array.length;
}
}
總結(jié):
- 無序數(shù)組實現(xiàn)優(yōu)先級隊列:出隊效率較低:O(n)
- 有序數(shù)組實現(xiàn)優(yōu)先級隊列:入隊效率較低:O(n)
- 大頂堆實現(xiàn)優(yōu)先級隊列:出隊和入隊:log(n)
三、Floyd建堆算法
- 找到最后一個非葉子節(jié)點
- 從最后一個非葉子節(jié)點從后向前,對每一個節(jié)點執(zhí)行下沉操作(就是所有有孩子的父節(jié)點,都下沉到正確的位置(大頂堆:保證父節(jié)點是最大的,如果本身就是大的比較之后不需要下沉))
/**
* 建堆
* 1.找到最后一個非葉子節(jié)點
* 2.從最后一個非葉子節(jié)點從后向前,對每一個節(jié)點執(zhí)行下沉操作(就是所有有孩子的父節(jié)點,都下沉到正確的位置(大頂堆:保證父節(jié)點是最大的,如果本身就是大的比較之后不需要下沉))
*/
private void heapify() {
// 最后一個非葉子節(jié)點索引 最后一個元素的父元素 (size-2)/2;
int index = size / 2 - 1;
for (int i = index; i >= 0; i--) {
downToProper(i);
}
}
四、堆排序
- 1.建立大頂堆
- 2.讓堆頂?shù)脑嘏c堆底的元素交換,縮小堆的大小,調(diào)整堆
- 3.重復(fù)步驟二直到堆中僅剩一個元素
package com.hcx.algorithm.heap;
/**
* @Title: HeapSort.java
* @Package com.hcx.algorithm.heap
* @Description: 堆排序
* 1.建立大頂堆
* 2.讓堆頂?shù)脑嘏c堆底的元素交換,縮小堆的大小,調(diào)整堆
* 3.重復(fù)步驟二直到堆中僅剩一個元素
* @Author: hongcaixia
* @Date: 2025/1/14 11:52
* @Version V1.0
*/
public class HeapSort {
static int[] arr;
int size;
public HeapSort(int[] array) {
this.arr = array;
this.size = array.length;
heapify();
}
private void heapSort() {
// 1.建堆
heapify();
while (size > 1) {
// 2.堆頂元素與堆底交換
swap(0, size - 1);
// 縮小堆
size--;
// 重建堆
downToProper(0);
}
}
public static void main(String[] args) {
int[] array = {1, 9, 3, 2, 6, 8, 7, 5};
HeapSort heapSort = new HeapSort(array);
heapSort.heapSort();
for (int i = 0;i<arr.length;i++){
System.out.println(arr[i]);
}
}
public void heapify() {
int index = (arr.length - 1) / 2 - 1;
for (int i = index; i >= 0; i--) {
downToProper(i);
}
}
/**
* 交換兩個元素
* @param i
* @param j
*/
private void swap(int i,int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
/**
* 將元素下沉到正確的位置
* @param index 元素當(dāng)前下標(biāo)
*/
private void downToProper(int index) {
int maxIndex = index;
// 計算左右孩子節(jié)點
int leftChildIndex = 2 * index + 1;
if (leftChildIndex < size && arr[leftChildIndex] > arr[maxIndex]) {
maxIndex = leftChildIndex;
}
int rightChildIndex = leftChildIndex + 1;
if (rightChildIndex < size && arr[rightChildIndex] > arr[maxIndex]) {
maxIndex = rightChildIndex;
}
// 找到了更大的孩子 交換元素
if (maxIndex != index) {
swap(index, maxIndex);
downToProper(maxIndex);
}
}
}
五、實現(xiàn)堆
package com.hcx.algorithm.heap;
/**
* @Title: Heap.java
* @Package com.hcx.algorithm.heap
* @Description: 堆
* @Author: hongcaixia
* @Date: 2025/1/14 18:14
* @Version V1.0
*/
public class Heap {
int[] arr;
int size;
// 是否是大頂堆
boolean maxHeapFlag;
public int size() {
return size;
}
public Heap(int capacity, boolean maxHeapFlag) {
this.arr = new int[capacity];
this.maxHeapFlag = maxHeapFlag;
}
public Heap(int[] arr, boolean maxHeapFlag) {
this.arr = arr;
this.size = arr.length;
this.maxHeapFlag = maxHeapFlag;
heapify();
}
/**
* 獲取堆頂元素
*
* @return
*/
public int peek() {
return arr[0];
}
/**
* 移除堆頂元素并返回
* 1.堆頂元素和末尾元素交換
* 2.size--
* 3.堆頂?shù)脑匾来蜗鲁恋秸_的位置
*
* @return
*/
public int poll() {
int top = arr[0];
swap(0, size - 1);
size--;
// 對堆頂元素依次執(zhí)行下沉操作到正確位置
downToProper(0);
return top;
}
/**
* 移除指定索引的元素并返回
*
* @param index
* @return
*/
public int poll(int index) {
int ele = arr[index];
swap(index, size - 1);
size--;
downToProper(index);
return ele;
}
/**
* 替換堆頂元素
*
* @param replaced
*/
public void replaceTop(int replaced) {
arr[0] = replaced;
downToProper(0);
}
/**
* 在堆的尾部添加元素
*
* @param offered
* @return
*/
public void offer(int offered) {
if (size == arr.length) {
arrGrow();
}
upToProper(offered, size);
size++;
}
/**
* 將元素上浮到正確位置
*
* @param offered 元素
* @param index 元素當(dāng)前下標(biāo)
*/
private void upToProper(int offered, int index) {
int childIndex = index;
while (childIndex > 0) {
// 計算他的父節(jié)點下標(biāo)
int parentIndex = (childIndex - 1) >> 1;
boolean compare = maxHeapFlag ? offered > arr[parentIndex] : offered < arr[parentIndex];
if (compare) {
// 父節(jié)點往下移動
arr[childIndex] = arr[parentIndex];
} else {
break;
}
// 改變孩子節(jié)點為當(dāng)前的父節(jié)點
childIndex = parentIndex;
}
arr[childIndex] = offered;
}
/**
* 將元素下沉到正確的位置
*
* @param index 元素當(dāng)前下標(biāo)
*/
private void downToProper(int index) {
int minIndex = index;
// 計算左右孩子節(jié)點
int leftChildIndex = 2 * index + 1;
if ((leftChildIndex < size) && (maxHeapFlag ? arr[leftChildIndex] > arr[minIndex] : arr[leftChildIndex] < arr[minIndex])) {
minIndex = leftChildIndex;
}
int rightChildIndex = leftChildIndex + 1;
if ((rightChildIndex < size) && (maxHeapFlag ? arr[rightChildIndex] > arr[minIndex] : arr[rightChildIndex] < arr[minIndex])) {
minIndex = rightChildIndex;
}
// 找到了更大的孩子 交換元素
if (minIndex != index) {
swap(index, minIndex);
downToProper(minIndex);
}
}
/**
* 交換兩個元素
*
* @param i
* @param j
*/
private void swap(int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
/**
* 建堆
* 1.找到最后一個非葉子節(jié)點
* 2.從最后一個非葉子節(jié)點從后向前,對每一個節(jié)點執(zhí)行下沉操作(就是所有有孩子的父節(jié)點,都下沉到正確的位置(大頂堆:保證父節(jié)點是最大的,如果本身就是大的比較之后不需要下沉))
*/
private void heapify() {
// 最后一個非葉子節(jié)點索引 最后一個元素的父元素 (size-2)/2;
int index = size / 2 - 1;
for (int i = index; i >= 0; i--) {
downToProper(i);
}
}
public boolean isFull() {
return size == arr.length;
}
/**
* 數(shù)組擴容:每次擴為原來的1.5倍
*/
private void arrGrow() {
int capacity = size + (size >> 1);
int[] newArr = new int[capacity];
System.arraycopy(arr, 0, newArr, 0, size);
arr = newArr;
}
}
六、Leetcode703.數(shù)據(jù)流中的第K大元素
package com.hcx.algorithm.heap;
/**
* @Title: KthLargest.java
* @Package com.hcx.algorithm.heap
* @Description: Leetcode703.數(shù)據(jù)流中的第K大元素
* @Author: hongcaixia
* @Date: 2025/1/14 16:42
* @Version V1.0
*/
public class KthLargest {
MinHeap minHeap;
public KthLargest(int k, int[] nums) {
minHeap = new MinHeap(k);
for (int num : nums) {
add(num);
}
}
/**
* 插入數(shù)據(jù)流后,返回當(dāng)前第k大元素
* @param val
* @return
*/
public int add(int val) {
// 堆沒滿
if(!minHeap.isFull()){
minHeap.offer(val);
}else if (val > minHeap.peek()) {
minHeap.replaceTop(val);
}
return minHeap.peek();
}
}
七、Leetcode295.數(shù)據(jù)流的中位數(shù)
分成兩部分:一部分是較小的,一部分是較大的
- 較小數(shù)據(jù)中讓最大的在堆頂,較大的數(shù)據(jù)中讓最小的在堆頂
- 堆頂?shù)膬蓚€元素就是中間的兩個
- 左邊是大頂堆 右邊是小頂堆
public class MedianFinder {
// 大頂堆,存儲前半部分元素;
static Heap maxHeap = new Heap(10, true);
// 小頂堆,存儲后半部分元素
static Heap minHeap = new Heap(10, false);
public static void addNum(int num) {
if (maxHeap.size == minHeap.size) {
//元素加入左邊 加入左邊之前 要保證元素是最小的 先加入右邊,再從右邊的堆頂取出最小的加入左邊
minHeap.offer(num);
maxHeap.offer(minHeap.poll());
} else {
// 元素加入右邊,加入右邊之前,要保證元素是大的,先加入左邊,再從左邊的堆頂取出最大的加入右邊
maxHeap.offer(num);
minHeap.offer(maxHeap.poll());
}
}
public static double findMedian() {
if (maxHeap.size == minHeap.size) {
return (maxHeap.peek() + minHeap.peek()) / 2.0;
} else {
return maxHeap.peek();
}
}
}
使用jdk的堆實現(xiàn):
class MedianFinder {
// 使用優(yōu)先級隊列 默認(rèn)是小頂堆
// 存儲前半部分元素
static PriorityQueue<Integer> maxQueue = new PriorityQueue<>((o1, o2) -> Integer.compare(o2, o1));
// 存儲后半部分元素
static PriorityQueue<Integer> minQueue = new PriorityQueue<>((o1, o2) -> Integer.compare(o1, o2));
public void addNum(int num) {
// 往前半部分加
if (minQueue.size() == maxQueue.size()) {
minQueue.offer(num);
maxQueue.offer(minQueue.poll());
} else {
// 往右邊加 加在右邊的要保證是大的
maxQueue.offer(num);
minQueue.offer(maxQueue.poll());
}
}
public double findMedian() {
if(maxQueue.size() == minQueue.size()){
return (maxQueue.peek() + minQueue.peek()) / 2.0;
}else{
return maxQueue.peek();
}
}
}
救命,這個實現(xiàn)方法在力扣上一直不對,評論區(qū)的大神救救孩子,本地是好的,找不出來哪里的問題。