目录
3. 删除堆顶元素(删除最小/最大值),返回被删除的元素。(替换 + 向下调整)
一、堆的基本操作
堆是一棵逻辑上的完全二叉树,物理存储上表现为一个数组。(不需要按照链式结构进行组织,所以没有结点概念)堆分为大堆和小堆。大堆:在整棵完全二叉树中,任取一棵树上的元素,都大于等于其两个孩子的值。小堆同理。即把最大值维护在跟上是大堆,把最小值维护在根上就为小堆。以下操作中均以小堆为例。
要实现堆的基本操作,需要先知道下标关系。假设现在已知一个元素的下标为 [k]:
① 这个元素的左孩子的下标:2 * k + 1
这个元素的右孩子的下标:2 * k + 2
② 这个元素的双亲的下标:
当这个元素是其双亲的左孩子时: ( k - 1 ) / 2
当这个元素是其双亲的右孩子时: ( k - 2 ) / 2
注意:因为在计算机中除法本来就是向下取整的,所以可以直接表示为:( k - 1 ) / 2
利用下标关系,我们就可以判断出元素是否有孩子(孩子下标是否合法),以及是否为叶子结点等。
1. 向下调整
前提:完全二叉树基本已经满足堆的性质,只有要进行操作的元素位置和其孩子的关系还不明确(也许满足,也许不满足)。只有满足这个前提,才能进行向下调整操作。
操作方法:假设“我”是待调整的元素。
(1)判断“我”是否为叶子结点,“我” 不是叶子 -> “我”有孩子。
(2)找到最小的孩子。
(3)比较 ”我“ 和最小孩子的大小:
① 我 <= 最小的孩子:满足堆的性质,调整停止;
② 我 > 最小的孩子:交换“我”和最小的孩子。
(4)交换完成后,堆的条件可能破坏了,需要继续向下调整。
时间复杂度:O(log(n)) 完全二叉树的高度
空间复杂度:
非递归:O(1)
递归:O(n) 调用n次
代码:有两种方式:递归和非递归。其中非递归为常用的向下调整的方法。
public static void adjustDown递归(long[] array, int size, int index) {
// index 是要进行向下调整的元素下标
//1.判断 index 是否为叶子结点:如果为叶子结点,不需要调整,直接结束即可;如果不为叶子结点,进行接下来的操作。
int leftIndex = 2 * index + 1;
if (leftIndex >= size) {
//判断 index 位置元素是否为叶子结点:判断其左孩子下标是否合法
return;
}
//2.找到最小的孩子
//不是叶子 -> 一定有左孩子
//当没有右孩子时,左孩子就为最小的孩子
//当有右孩子时:
// ① 右孩子 > 左孩子 :最小孩子为左孩子
// ② 右孩子 = 左孩子 :最小孩子为左孩子(右孩子)
// ③ 右孩子 < 左孩子 :最小孩子为右孩子
int minIdx = leftIndex; //假设最小孩子为左孩子
int rightIindex = leftIndex + 1;
if (rightIindex < size && array[rightIindex] < array[leftIndex]) {
minIdx = rightIindex;
}
//3.比较 ”我“ 和最小孩子的值
//当 我 小于等于 最小孩子的值时,结束
//当 我 大于最小孩子的值时,进行接下来的操作(向下调整)
if (array[index] < array[minIdx]) {
return;
}
//此时 我 > 最小孩子,不满足堆的性质,所以将我和最小孩子的位置进行调换
swap(array, index, minIdx);
//交换完后需要继续判断,处在新位置的“我 ”是否依然满足堆的性质
adjustDown递归(array,size,minIdx);
}
public static void swap (long[] array, int p, int q) {
long tmp = array[p];
array[p] = array[q];
array[q] = tmp;
}
//常用向下调整方法
public static void adjustDown小堆(long[] array, int size, int index) {
while (index * 2 + 1 < size) {
//说明不是叶子结点
//找到最小孩子
int minIdx = index * 2 + 1; //假设最小孩子为左孩子
if (minIdx + 1 < size && array[minIdx + 1] < array[minIdx]) {
minIdx++;
}
//判断我和最小孩子的大小关系
if (array[index] <= array[minIdx]) {
return;
}
//交换
swap(array, index, minIdx);
//以最小孩子的位置再次这个过程
index = minIdx;
}
}
2. 建堆
建堆过程就是利用向下调整,从最后一个有孩子的双亲结点开始向下调整,依次到根结点。
时间复杂度:O(n)
代码:
public static void creatHeap小堆(long[] array, int size) {
//找到最后一个结点的双亲结点
// pIdx = ((size - 1) - 1) / 2
//size - 1 为最后一个结点
int pIdx = (size - 2) / 2;
//O(n * log(n)) -> O(n)
for (int i = pIdx; i >= 0; i--) {
adjustDown小堆(array, size, i); //O(log(n))
}
}
二、堆的应用——优先级队列
这里的优先级队列中的元素类型只为基础类型。
1. 查看堆顶元素(查看最小/最大元素)
直接返回根结点元素即可。
时间复杂度:O(1)
代码:
public long peek() {
if (size <= 0) {
throw new RuntimeException("空的");
}
return array[0];
}
2. 添加元素(向上调整)
先将要插入的元素放到堆的最后,并且 size+1。此时新插入的元素可能不满足堆的性质(这里是小堆,所以可能新插入的元素小于其父母结点元素)
所以要进行判断:
(1)判断自己是不是根结点。(如果是根结点,不需要进行操作,调整结束)
(2)找到自己的双亲结点,进行比较(向上调整)
① ”我“ >= 双亲:满足堆的性质,调整结束;
② “我” < 双亲:交换我和双亲位置,再持续这个过程。
时间复杂度:O(log(n))
代码:
public void offer(long e) {
array[size] = e;
size++;
int index = size - 1;
while (index != 0) {
int pIdx = (index - 1) / 2;
if (array[index] >= array[pIdx]) {
break;
}
swap(array, index, pIdx);
//持续双亲位置继续向上调整
index = pIdx;
}
}
3. 删除堆顶元素(删除最小/最大值),返回被删除的元素。(替换 + 向下调整)
不能直接删除堆顶元素。要先将堆中的最后一个元素替换堆顶元素,并且 size--。然后再从堆顶开始向下调整。
时间复杂度:O(log(n))
代码:
public long poll() {
if (size <= 0) {
throw new RuntimeException("空的");
}
long e = array[0];
array[0] = array[size - 1];
array[size - 1] = 0; //可不写,只是为了调试好看
size--;
adjustDown小堆(array, size, 0); //O(log(n))
return e;
}
4. 完整代码
public class MyPriorityQueuePrimitiveType {
//使用小堆
//不考虑扩容情况
//long 类型代表元素
private final long[] array = new long[1000];
private int size;
public MyPriorityQueuePrimitiveType(){
//构造方法:构造一个空的优先级队列
this.size = 0;
}
//查看堆顶元素(查看最小元素)
//O(1)
public long peek() {
if (size <= 0) {
throw new RuntimeException("空的");
}
return array[0];
}
//插入元素(向上调整)
//先将要插入的元素放到堆的最后,并且 size+1。此时新插入的元素可能不满足堆的性质(这里是小堆,所以可能新插入的元素小于其父母结点元素)
//所以要进行判断:
//1. 判断自己是不是根结点。(如果是根结点,不需要进行操作,调整结束)
//2. 找到自己的双亲结点,进行比较(向上调整)
// ① ”我“ >= 双亲:满足堆的性质,调整结束;
// ② “我” < 双亲:交换我和双亲位置,再持续这个过程
//O(log(n))
public void offer(long e) {
array[size] = e;
size++;
int index = size - 1;
while (index != 0) {
int pIdx = (index - 1) / 2;
if (array[index] >= array[pIdx]) {
break;
}
swap(array, index, pIdx);
//持续双亲位置继续向上调整
index = pIdx;
}
}
//删除堆顶(最小)元素(trick + 向下调整)返回被删除的元素
//不能直接删除堆顶元素
//先将堆中的最后一个元素替换堆顶元素,并且 size--。然后再从堆顶开始向下调整
//O(log(n))
public long poll() {
if (size <= 0) {
throw new RuntimeException("空的");
}
long e = array[0];
array[0] = array[size - 1];
array[size - 1] = 0; //可不写,只是为了调试好看
size--;
adjustDown小堆(array, size, 0); //O(log(n))
return e;
}
public void swap(long[] array, int p, int q) {
long tmp = array[p];
array[p] = array[q];
array[q] = tmp;
}
public void adjustDown小堆(long[] array, int size, int index) {
while (index * 2 + 1 < size) {
//说明不是叶子结点
//找到最小孩子
int minIdx = index * 2 + 1; //假设最小孩子为左孩子
if (minIdx + 1 < size && array[minIdx + 1] < array[minIdx]) {
minIdx++;
}
//判断我和最小孩子的大小关系
if (array[index] <= array[minIdx]) {
return;
}
//交换
swap(array, index, minIdx);
//以最小孩子的位置再次这个过程
index = minIdx;
}
}
}
三、时间复杂度总结
1. 堆的基本操作:
(1)向下调整:O(log(n)) 完全二叉树的高度
(2)建堆:O(n)
2. 优先级队列(堆的应用):
(1)查看堆顶元素:O(1)
(2)添加元素(向上调整):O(log(n))
(3) 删除堆顶元素(替换 + 向下调整):O(log(n))