算法 - 堆排序 - 随时找到数据流的中位数
堆排序
注意,堆这个结构需要知道什么是满二叉树、完全二叉树。堆就是完全二叉树。
堆
使用数组存储数据,用数组模拟二叉堆结构,此时下标的关系有:
- 父结点 i 的左子结点为 2i + 1,右子结点为 2i + 2
- 子结点 i 找父结点公式为:(i - 1) / 2
堆分大根堆和小根堆,每一个结点为子树的最大值称为大根堆,同理可知小根堆。
以大根堆为例学习堆排序算法,小根堆同理做一些转换即可。
heapInsert
构建堆时,heapInsert 是非常重要的构建方法。
建立 i 个数的堆,算法的时间复杂度为
log(!(i-1)) = log1 + log2 + ··· + log(i-1)
数学证明,最终得到建立过程的时间复杂度为 O(N)。
public static void heapInsert(int[] arr, int i) {
//每一次插入新结点,与父结点做比较,调整堆结构
while (arr[i] > arr[(i - 1) / 2]) {
swap(arr, i, (i - 1) / 2);
i = (i - 1) / 2;
}
}
堆的大小 size
堆在内存中可以使用数组实现,数组有数组的长度 length,堆有堆的大小 size,关系为:size <= length。当堆增加一个数时,使用 heapInsert,size++;当堆大小减少时,直接使用 size--,数组中的元素可以保留。
heapify
两个字:下沉!堆排序的核心。
堆中的数据进行修改的时候 heapify 是非常重要的调整方法。调整的时间复杂度为 O(logN)。
当大根堆构建完成后,堆内第 i 个数变小,此时堆结构需要调整。
思路:确保 i 的子结点仍是堆内数据,比较得到两个子结点中的最大值,与 i 结点比较大小。如果 i 结点数大,则堆结构保持不变;否则,将 i 结点与子结点交换位置。循环上述堆结构调整流程。
public static void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1;
while (left < heapSize) {
//比较得到子结点较大值索引
int largest = (left + 1) < heapSize && arr[left] < arr[left + 1]
? left + 1
: left;
//父结点和子结点比较
largest = arr[index] < arr[largest]
? largest
: index;
//数据改变了,但是仍保持大根堆结构
if (largest == index) {
break;
}
swap(arr, index, largest);
//准备下一轮循环的条件
index = largest;
left = index * 2 + 1;
}
}
pop
将堆顶的数弹出,可以用 size - 1 位上的数和堆顶的数交换位置,然后 size--,最后进行 heapify 操作。
问题一:随时找到数据流的中位数
有一个源源不断地吐出整数的数据流,要求做到可以随时取得之前吐出所有数的中位数。这时可以使用两个堆:一个大根堆和一个小根堆存放数据,大根堆最大值小于小根堆的最小值。
思路:
//处理数据流
假设大根堆堆顶为 big,小根堆堆顶为 small
获取第一个数,先进大根堆,这一步不硬性规定的
获取第二个数,比较
if (num <= big) 进大根堆
else 进小根堆
在处理了数据后,根据 Math.abs(big_size - small_size) < 2 的条件决定是否调整堆结构
if true 不调整
else
if big_size 大,则大根堆执行 pop 操作,小根堆执行 heapInsert 操作
else 小根堆执行 pop 操作,大根堆执行 heapInsert 操作
//计算中位数
if big_size = small_size return (big + small) / 2
else
if big_size > small_size return big
else return small
public class _295_FindMedianfromDataStream {
PriorityQueue<Integer> small_heap;
PriorityQueue<Integer> big_heap;
/** initialize your data structure here. */
public _295_FindMedianfromDataStream() {
small_heap = new PriorityQueue<>();
big_heap = new PriorityQueue<>((a, b) -> b - a);
}
// version 1.0
/*public void addNum(int num) {
if (big_heap.isEmpty() && small_heap.isEmpty()) {
big_heap.offer(num);
return;
}
if (big_heap.size() == 1 && small_heap.isEmpty()) {
if (big_heap.peek() > num) {
small_heap.offer(big_heap.poll());
big_heap.offer(num);
} else {
small_heap.offer(num);
}
return;
}
//当堆为空时,num 和 堆内元素比较会出现空指针错误
if (small_heap.size() == big_heap.size()) {
if (num < big_heap.peek()) {
big_heap.offer(num);
} else {
small_heap.offer(num);
}
} else if (small_heap.size() < big_heap.size()) {
if (num > big_heap.peek()) {
small_heap.offer(num);
} else {
small_heap.offer(big_heap.peek());
big_heap.poll();
big_heap.offer(num);
}
} else {
if (num < small_heap.peek()) {
big_heap.offer(num);
} else {
big_heap.offer(small_heap.peek());
small_heap.poll();
small_heap.offer(num);
}
}
}
*/
// version 2.0
public void addNum(int num) {
if (big_heap.isEmpty() && small_heap.isEmpty()) {
big_heap.offer(num);
return;
}
if (num <= big_heap.peek()) {
big_heap.offer(num);
} else {
small_heap.offer(num);
}
if (Math.abs(big_heap.size() - small_heap.size()) > 1) {
if (big_heap.size() > small_heap.size()) {
small_heap.offer(big_heap.poll());
} else {
big_heap.offer(small_heap.poll());
}
}
}
public double findMedian() {
double res;
if (small_heap.size() == big_heap.size()) {
res = (small_heap.peek() + big_heap.peek()) / 2.0;
} else if (small_heap.size() > big_heap.size()) {
res = small_heap.peek();
} else {
res = big_heap.peek();
}
return res;
}
}
上面是我做 LeetCode_295 题的代码,原理是相近的。此前做的时候绕了一些弯路,得到 version 1.0 版。
代码中使用了 Java 的优先级队列 ProrityQueue,在创建大根堆过程中使用了比较器。其实优先级队列就是堆的一种实现,不需要自己实现 heapInsert 和 heapify。
注意:PriorityQueue 的一些方法异同点
作用 | 抛出异常 | 返回值 |
---|---|---|
插入 | add(e) | offer(e) |
删除 | remove() | poll() |
查看 | element() | peek() |
堆排序
三步:
- 堆顶和堆尾部交换位置
- 堆的大小减 1
- heapify 调整
大根堆完成从小到大的排序,小根堆完成从大到小的排序。
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int heapSize = arr.length;
while (heapSize != 0) {
swap(arr, 0, heapSize - 1);
heapSize--;
//此处可以简化成一步
//swap(arr, 0, --heapSize);
heapify(arr, 0, heapSize);
}
}
/*和左神代码局部对比*/
//basic
int heapSize = arr.length;
while (heapSize != 0) {
swap(arr, 0, heapSize - 1);
heapSize--;
heapify(arr, 0, heapSize);
}
//advanced
int size = arr.length;
swap(arr, 0, --size);
while (size > 0) {
heapify(arr, 0, size);
swap(arr, 0, --size);
}
从局部代码中可以看到,左神的代码利用了前面判断的 arr.length > 2 的条件,少使用了一次 while 判断 size 的过程。