1、思考
設(shè)計(jì)一種數(shù)據(jù)結(jié)構(gòu),用來(lái)存放整數(shù),要求提供 3 個(gè)接口
1. 添加元素
2. 獲取最大值
3. 刪除最大值
有沒(méi)有更優(yōu)的數(shù)據(jù)結(jié)構(gòu)?
堆:獲取最大值:O(1)、刪除最大值:O()、添加元素:O(
)
2、Top K問(wèn)題
- 什么是 Top K 問(wèn)題
- 從海量數(shù)據(jù)中找出前 K 個(gè)數(shù)據(jù)
- 比如
- 從 100 萬(wàn)個(gè)整數(shù)中找出最大的 100 個(gè)整數(shù)
- Top K 問(wèn)題的解法之一:可以用數(shù)據(jù)結(jié)構(gòu)“堆”來(lái)解決
3、堆(Heap)
-
(Heap)也是一種樹(shù)狀的數(shù)據(jù)結(jié)構(gòu)(不要跟內(nèi)存模型中的“堆空間”混淆),常見(jiàn)的堆實(shí)現(xiàn)有
(Binary Heap,
)
(D-heap、D-ary Heap)
(Index Heap)
(Binomial Heap)
(Fibonacci Heap)
(Leftist Heap,
)
(Skew Heap)
-
堆的一個(gè)重要性質(zhì):任意節(jié)點(diǎn)的值總是
(
)
的值
- 如果任意節(jié)點(diǎn)的值總是
的值,稱為:
、
、
- 如果任意節(jié)點(diǎn)的值總是
的值,稱為:
、
、
- 如果任意節(jié)點(diǎn)的值總是
由此可見(jiàn),堆中的元素必須具備可比較性(跟二叉搜索樹(shù)一樣)
4、堆的基本接口設(shè)計(jì)
int size(); // 元素的數(shù)量
boolean isEmpty(); // 是否為空
void clear(); // 清空
void add(E element); // 添加元素
E get(); // 獲得堆頂元素
E remove(); // 刪除堆頂元素
E replace(E element); // 刪除堆頂元素的同時(shí)插入一個(gè)新元素
5、二叉堆(Binary Heap)
-
的邏輯結(jié)構(gòu)就是一棵完全二叉樹(shù),所以也叫
- 鑒于完全二叉樹(shù)的一些特性,
的底層(物理結(jié)構(gòu))一般用數(shù)組實(shí)現(xiàn)即可
- 索引 i 的規(guī)律( n 是元素?cái)?shù)量)
- 如果 i = 0 ,它是
節(jié)點(diǎn)
- 如果 i > 0 ,它的
節(jié)點(diǎn)的索引為floor(
)
- 如果 2i + 1 ≤ n – 1,它的
子節(jié)點(diǎn)的索引為
- 如果 2i + 1 > n – 1 ,它
子節(jié)點(diǎn)
- 如果 2i + 2 ≤ n – 1 ,它的
子節(jié)點(diǎn)的索引為
- 如果 2i + 2 > n – 1 ,它
子節(jié)點(diǎn)
- 如果 i = 0 ,它是
6、代碼實(shí)現(xiàn)
6.1、二叉堆的構(gòu)造函數(shù)及部分方法的實(shí)現(xiàn)
public class BinaryHeap<E> implements Heap<E> {
private E[] elements;
private int size;
private Comparator<E> comparator;
private static final int DEFAULt_CAPACITY = 10;
public BinaryHeap(Comparator<E> comparator) {
this.comparator = comparator;
this.elements = (E[]) new Object[DEFAULt_CAPACITY];
}
public BinaryHeap() {
this(null);
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void clear() {
for (int i = 0; i < size; i++) {
elements[i] = null;
}
size = 0;
}
......
private void emptyCheck() {
if(size == 0) {
throw new IndexOutOfBoundsException("Heep is empty");
}
}
}
6.2、獲取最大值
@Override
public E get() {
emptyCheck();
return elements[0];
}
private void emptyCheck() {
if(size == 0) {
throw new IndexOutOfBoundsException("Heep is empty");
}
}
6.3、最大堆 - 添加
6.3.1、思路
- 循環(huán)執(zhí)行以下操作(圖中的
簡(jiǎn)稱為
node
)- 如果
node
> 父節(jié)點(diǎn),與父節(jié)點(diǎn)交換位置 - 如果
node
≤ 父節(jié)點(diǎn),或者node
沒(méi)有父節(jié)點(diǎn),退出循環(huán)
- 如果
- 這個(gè)過(guò)程,叫做上濾(Sift Up)
- 時(shí)間復(fù)雜度:O(
)
- 時(shí)間復(fù)雜度:O(
6.3.2、實(shí)現(xiàn)
public void add(E element) {
elementNotNullCheck(element);
emptyCheck();
elements[size++] = element;
siftUp(size - 1);
}
private void ensureCapacity(int capacity) {
int oldCapacity = elements.length;
if (oldCapacity >= capacity) return;
// 新容量為舊容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
elements = newElements;
}
/**
* 讓index位置的元素上濾
*/
private void siftUp(int index) {
E e = elements[index];
while (index > 0) {
int pindex = (index - 1) >> 1;// 性質(zhì):floor( (i - 1)/2 )
E p = elements[pindex];
if(compare(e, p) <= 0) return;
// 交換index、pindex位置的內(nèi)容
E tmp = elements[index];
elements[index] = elements[pindex];
elements[pindex] = tmp;
// 重新賦值index
index = pindex;
}
}
6.3.3、打印調(diào)試
這里二叉堆的邏輯結(jié)構(gòu)就是二叉樹(shù),所以這里是否可以使用之前的打印二叉樹(shù)的方法來(lái)實(shí)現(xiàn)呢?
其實(shí)是可以的,雖然它的邏輯結(jié)構(gòu)就是二叉樹(shù),而實(shí)際上是數(shù)據(jù)結(jié)構(gòu),但是可以將之前的打印方法進(jìn)行特殊處理一下。
public class BinaryHeap<E> implements Heap<E> ,BinaryTreeInfo{
......
@Override
public Object root() {
return 0;// 這里返回的是索引
}
@Override
public Object left(Object node) {
int index = ((int)node << 1) + 1;// 性質(zhì):左索引 2i + 1
return index >= size ? null : index;
}
@Override
public Object right(Object node) {
int index = ((int)node << 1) + 2;// 性質(zhì):左索引 2i + 2
return index >= size ? null : index;
}
@Override
public Object string(Object node) {
return elements[(int)node];
}
}
6.3.4、最大堆 – 添加 – 交換位置的優(yōu)化
上面的siftUp
方法里進(jìn)行交換時(shí)是需要三行代碼,這個(gè)是可以進(jìn)行優(yōu)化的:先將新添加節(jié)點(diǎn)進(jìn)行備份,在跟父節(jié)點(diǎn)進(jìn)行比較時(shí),父節(jié)點(diǎn)挪下來(lái),但是新添加節(jié)點(diǎn)先不覆蓋父節(jié)點(diǎn),繼續(xù)比較直到最后才把新添加節(jié)點(diǎn)覆蓋父節(jié)點(diǎn)。
- 一般交換位置需要3行代碼,可以進(jìn)一步優(yōu)化
- 將新添加節(jié)點(diǎn)備份,確定最終位置才擺放上去
- 僅從交換位置的代碼角度看:
- 可以由大概的
優(yōu)化到
- 可以由大概的
private void siftUp(int index) {
E e = elements[index];
while (index > 0) {
int pindex = (index - 1) >> 1;// 性質(zhì):floor( (i - 1)/2 )
E p = elements[pindex];
if(compare(e, p) <= 0) break;
// 將父元素存儲(chǔ)在index位置
elements[index] = p;
// 重新賦值index
index = pindex;
}
elements[index] = e;
}
6.4、抽取堆父類
我們上面實(shí)現(xiàn)的是二叉堆,其實(shí)堆又很多種,二項(xiàng)堆、斐波那契堆、左傾堆等,但是不管你是什么堆都應(yīng)該實(shí)現(xiàn)Heap
接口,而且堆還分大根堆和小根堆,也就是說(shuō)只要是堆它上面的元素都必須要具備可比較性的,那么現(xiàn)在完全可以將堆一些公共功能抽取出來(lái)。
public abstract class AbstractHeap<E> implements Heap<E> {
protected int size;
protected Comparator<E> comparator;
public AbstractHeap(Comparator<E> comparator) {
this.comparator = comparator;
}
public AbstractHeap() {
this(null);
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
protected int compare(E e1,E e2) {
return comparator != null ? comparator.compare(e1, e2) : ((Comparable<E>)e1).compareTo(e2);
}
}
6.5、最大堆 - 刪除
6.5.1、思路
刪除操作是刪除堆頂元素,因?yàn)檫@里的二叉堆是用數(shù)組實(shí)現(xiàn)的,如果直接刪除堆頂元素,則所有元素都需要向前挪動(dòng),性能太差。所以這里需要拿到數(shù)組的最后一個(gè)元素,先覆蓋堆頂元素,然后在清空最后一個(gè)元素。覆蓋后,肯定不具有最大堆了的性質(zhì)了,堆頂元素肯定小于它的下面兩個(gè)元素,所以現(xiàn)在還需要進(jìn)行比較然后進(jìn)行交換。
那么選擇子節(jié)點(diǎn)的那個(gè)進(jìn)行交換比較合適呢?當(dāng)然是子節(jié)點(diǎn)的最大值。
6.5.2、下濾
- 用最后一個(gè)節(jié)點(diǎn)覆蓋根節(jié)點(diǎn)
- 刪除最后一個(gè)節(jié)點(diǎn)
- 循環(huán)執(zhí)行以下操作(圖中的 43 簡(jiǎn)稱為
node
)
如果
node
< 最大的子節(jié)點(diǎn),與最大的子節(jié)點(diǎn)交換位置如果
node
≥ 最大的子節(jié)點(diǎn),或者node
沒(méi)有子節(jié)點(diǎn),退出循環(huán)這個(gè)過(guò)程,叫做下濾(Sift Down),時(shí)間復(fù)雜度:
同樣的,交換位置的操作可以像添加那樣進(jìn)行優(yōu)化
public E remove() {
emptyCheck();
E root = elements[0];
elements[0] = elements[size -1];
elements[size -1] = null;
size--;
siftDown(0);// 下濾
return root;
}
上面有兩個(gè)地方都是用了size - 1
,這里是可以進(jìn)行優(yōu)化的:
public E remove() {
emptyCheck();
E root = elements[0];
int lastIndex = --size;
elements[0] = elements[lastIndex];
elements[lastIndex] = null;
siftDown(0);// 下濾
return root;
}
下面專心實(shí)現(xiàn)下濾siftDown
的代碼,實(shí)現(xiàn)下濾siftDown
的代碼是需要while
循環(huán),那么進(jìn)入循環(huán)的條件就是下濾的節(jié)點(diǎn)必須要有子節(jié)點(diǎn),也就是說(shuō)必須是非葉子節(jié)點(diǎn)。我們通過(guò)之前講的完全二叉樹(shù)的性質(zhì)可以知道:從上到下、從左到右排序,只要遇到第一個(gè)葉子節(jié)點(diǎn),往后所有節(jié)點(diǎn)都是葉子節(jié)點(diǎn)。所以這里的下濾siftDown
的參數(shù)index
只有小于第一個(gè)葉子節(jié)點(diǎn)的索引,才能進(jìn)入while
循環(huán)
那么第一個(gè)葉子節(jié)點(diǎn)的索引怎么算呢?其實(shí)就是非葉子節(jié)點(diǎn)的數(shù)量( floor(n / 2) )。
private void siftDown(int index) {
// 第一個(gè)葉子節(jié)點(diǎn)的索引 == 非葉子節(jié)點(diǎn)的數(shù)量
while(index < 第一個(gè)葉子節(jié)點(diǎn)的索引) {// 必須保證index位置是非葉子節(jié)點(diǎn)
}
}
根據(jù)之前《十、二叉樹(shù)》中了解到計(jì)算非葉子節(jié)點(diǎn)個(gè)數(shù)的公式是:n1 + n2 = floor( n / 2 ) = ceiling( (n – 1) / 2 )
,因此可以實(shí)現(xiàn)下濾siftDown
的具體代碼
private void siftDown(int index) {
E element = elements[index];
int half = size >> 1; // floor(n / 2)
// 第一個(gè)葉子節(jié)點(diǎn)的索引 == 非葉子節(jié)點(diǎn)的數(shù)量
// index < 第一個(gè)葉子節(jié)點(diǎn)的索引
while(index < half) {// 必須保證index位置是非葉子節(jié)點(diǎn)
// index的節(jié)點(diǎn)有2種情況
// 1.只有左子節(jié)點(diǎn)
// 2.同時(shí)有左右子節(jié)點(diǎn)
// 默認(rèn)為左子節(jié)點(diǎn)跟它進(jìn)行比較
int childIndex = (index << 1) + 1;// 2i + 1
E child = elements[childIndex];
// 右子節(jié)點(diǎn)
int rightIndex = childIndex + 1;
// 選出左右子節(jié)點(diǎn)最大的那個(gè)
if (rightIndex < size && compare(elements[rightIndex], child) > 0) {
child = elements[childIndex = rightIndex];
}
if (compare(element, child) >= 0) break;
// 將子節(jié)點(diǎn)存放到index位置
elements[index] = child;
// 重新設(shè)置index
index = childIndex;
}
elements[index] = element;
}
6.7、replace方法實(shí)現(xiàn)
6.7.1、思路
replace
是刪除堆頂元素的同時(shí)插入一個(gè)新元素
這里我們可以想到直接先刪除,在添加操作就可以完成。
public E replace(E element) {
E root = remove();
add(element);
return root;
}
但是這樣會(huì)有一點(diǎn)點(diǎn)的浪費(fèi),因?yàn)?code>remove和add
都是級(jí)別的,所以這個(gè)
replace
也就是兩個(gè)級(jí)別。
6.7.2、實(shí)現(xiàn)
這里可以直接進(jìn)行將要添加的元素直接替換堆頂元素,然后做下濾操作就可以了。
public E replace(E element) {
elementNotNullCheck(element);
E root = null;
if (size == 0) {
elements[0] = element;
size++;
}else {
root = elements[0];
elements[0] = element;
siftDown(0);
}
return root;
}
7、最大堆 – 批量建堆(Heapify)
如果我有一批數(shù)據(jù),如何批量插入到堆里呢?我們想到了可以直接利用for循環(huán)一個(gè)一個(gè)添加。
Integer[] data = {88, 44, 53, 41, 16, 6, 70, 18, 85, 98, 81, 23, 36, 43, 37};
BinaryHeap<Integer> heap = new BinaryHeap<>();
for (int i = 0; i < data.length; i ++) {
heap.add(data[i]);
}
BinaryTrees.println(heap);
其實(shí)還有其它做法,而且有些做法可能效果更優(yōu)。這里講兩種做法:
- 批量建堆,有 2 種做法
- 自上而下的上濾
-
自下而上的下濾
7.1、最大堆 – 批量建堆 – 自上而下的上濾
自上而下的上濾是從除了跟節(jié)點(diǎn)開(kāi)始的(i = 1)
這個(gè)相當(dāng)于添加操作,差不多等價(jià)于挨個(gè)添加
7.2、最大堆 – 批量建堆 – 自下而上的下濾
自下而上的下濾式從非葉子節(jié)點(diǎn)開(kāi)始的
((size >> 1) -1)
,(size >> 1) -1
就是上圖的73的位置,它是最后一個(gè)非葉子節(jié)點(diǎn),在完全二叉樹(shù)中非葉子節(jié)點(diǎn)的數(shù)量是總節(jié)點(diǎn)數(shù)量的一半這個(gè)相當(dāng)于刪除操作
7.3、最大堆 – 批量建堆 – 效率對(duì)比
- 所有節(jié)點(diǎn)的深度之和
- 僅僅是葉子節(jié)點(diǎn),就有近
個(gè),而且每一個(gè)葉子節(jié)點(diǎn)的深度都是
級(jí)別的
- 因此,在葉子節(jié)點(diǎn)這一塊,就達(dá)到了
級(jí)別
-
的時(shí)間復(fù)雜度足以利用排序算法對(duì)所有節(jié)點(diǎn)進(jìn)行全排序
- 僅僅是葉子節(jié)點(diǎn),就有近
- 所有節(jié)點(diǎn)的高度之和
- 假設(shè)是滿樹(shù),節(jié)點(diǎn)總個(gè)數(shù)為 n,樹(shù)高為 h,那么
- 所有節(jié)點(diǎn)的樹(shù)高之和H(n) =
- H(n) =
- H(n) =
- H(n) =
- H(n) =
- 假設(shè)是滿樹(shù),節(jié)點(diǎn)總個(gè)數(shù)為 n,樹(shù)高為 h,那么
公式推導(dǎo)
- S(h) =
- 2S(h) =
- S(h) – 2S(h) =
- S(h) =
疑惑
-
以下方法可以批量建堆么
- 自上而下的下濾
-
自下而上的上濾
-
述方法不可行,為什么?
- 認(rèn)真思考【自上而下的上濾】、【自下而上的下濾】的本質(zhì)
- 自上而下的上濾本質(zhì)其實(shí)就是等價(jià)于挨個(gè)添加
- 自下而上濾的下本質(zhì)其實(shí)就是先讓其左右先變成一個(gè)堆,下濾后在變成堆
- 認(rèn)真思考【自上而下的上濾】、【自下而上的下濾】的本質(zhì)
7.4、代碼實(shí)現(xiàn):
這里因?yàn)槭侵苯訉⑼饷娴臄?shù)組賦值過(guò)來(lái)的(this.elements = elements;
),所以外面一旦對(duì)數(shù)組進(jìn)行了修改操作,那么就會(huì)導(dǎo)致有問(wèn)題,因此這里需要對(duì)外面?zhèn)鬟^(guò)來(lái)的數(shù)據(jù)進(jìn)行拷貝(深拷貝)
public BinaryHeap(E[] elements,Comparator<E> comparator) {
super(comparator);
if(elements == null || elements.length == 0) {
this.elements = (E[]) new Object[DEFAULt_CAPACITY];
}else {
size = elements.length;
int capacity = Math.max(elements.length, DEFAULt_CAPACITY);
this.elements = (E[]) new Object[capacity];
for (int i = 0; i < elements.length; i++) {
this.elements[i] = elements[i];
}
heapify();
}
}
8、最小堆
如何構(gòu)建一個(gè)小頂堆呢?
Integer[] data = {88, 44, 53, 41, 16, 6, 70, 18, 85, 98, 81, 23, 36, 43, 37};
BinaryHeap<Integer> heap = new BinaryHeap<>(data,new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
// return o1 - o2;// 最大堆
return o2 - o1;// 最小堆
}
});
BinaryTrees.println(heap);
9、Top K問(wèn)題
從
n
個(gè)整數(shù)中,找出最大的前k
個(gè)數(shù)(k
遠(yuǎn)遠(yuǎn)小于n
)如果使用排序算法進(jìn)行全排序,需要
的時(shí)間復(fù)雜度
-
如果使用二叉堆來(lái)解決,可以使用
的時(shí)間復(fù)雜度來(lái)解決
- 新建一個(gè)小頂堆
- 掃描
n
個(gè)整數(shù) - 先將遍歷到的前
k
個(gè)數(shù)放入堆中 - 從第
k + 1
個(gè)數(shù)開(kāi)始,如果大于堆頂元素,就使用replace
操作(刪除堆頂元素,將第k + 1
個(gè)數(shù)添加到堆中) - 掃描完畢后,堆中剩下的就是最大的前
k
個(gè)數(shù)
-
如果是找出最小的前
k
個(gè)數(shù)呢?- 用大頂堆
- 如果小于堆頂元素,就使用
replace
操作
// 新建一個(gè)小頂堆
BinaryHeap<Integer> heap = new BinaryHeap<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
// 找出最大的前k個(gè)數(shù)
int k = 3;
Integer[] data = {51, 30, 39, 92, 74, 25, 16, 93,
91, 19, 54, 47, 73, 62, 76, 63, 35, 18,
90, 6, 65, 49, 3, 26, 61, 21, 48};
for (int i = 0; i < data.length; i++) {
int value = data[i];
if(heap.size() < k) {// 前k個(gè)數(shù)添加到小頂堆
heap.add(value);
}else if(value > heap.get()){// 如果是第k + 1個(gè)數(shù),并且大于堆頂元素
heap.replace(value);
}
}
BinaryTrees.println(heap);
這里先把前k
個(gè)鍵入小頂堆里,以后的數(shù)據(jù)根堆頂?shù)脑?小頂堆的堆頂元素是最小的)進(jìn)行比較,如果大于堆頂元素,就會(huì)使用replace
方法,先刪除堆頂元素,最后在插入元素這樣就達(dá)到了,找出最大的前k
個(gè)數(shù)。
10、leetcode題:
215. 數(shù)組中的第K個(gè)最大元素
給定整數(shù)數(shù)組nums
和整數(shù)k
,請(qǐng)返回?cái)?shù)組中第k
個(gè)最大的元素。
請(qǐng)注意,你需要找的是數(shù)組排序后的第k
個(gè)最大的元素,而不是第k
個(gè)不同的元素。
示例 1:
輸入: [3,2,1,5,6,4] 和 k = 2
輸出: 5
示例 2:
輸入: [3,2,3,1,2,4,5,5,6] 和 k = 4
輸出: 4
class Solution {
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>();// 小頂端
for (int i = 0; i < nums.length; i++) {
int value = nums[i];
if(heap.size() < k) {// 前k個(gè)數(shù)添加到小頂堆
heap.offer(value);
}else if(value > heap.peek()){// 如果是第k + 1個(gè)數(shù),并且大于堆頂元素
heap.poll();
heap.offer(value);
}
}
return heap.peek();
}
}