1.堆
1.1什么是堆
堆是一种特殊的树,它满足两个条件:
(1)堆是一种完全二叉树,即除了最后一层,其它层都是满二叉树,最后一个节点靠最左;
(2)堆中每一个节点值都必须大于等于(或小于等于)其左右子节点的值。
其中,每个节点的值都大于等于子树中每个节点的值的堆,叫做“大顶堆”;相反,对于每个节点的值都小于等于子树中每个节点的值的堆,叫做“小顶堆”。
1.2堆的实现
考虑到堆是一种特殊的完全二叉树,我们可以使用数组来存储,因为使用数组来存储完全二叉树是非常节省内存空间的(不需要使用额外的空间存储左右子节点的指针,直接可以利用下标之间的关系,就可以找到一个节点的父节点和子节点)。
如图,我们可以知道完全二叉树与数组 之间的对应关系:
数组中下标为i的节点的左子节点,就在下标为2*i+1的位置;右子节点就在下标为2*i+2的位置。
1.3堆的核心操作
(1)向堆中插入一个元素:
我们向堆中加入一个元素的过程,就是建堆的过程。当然,每次我们插入一个元素都是先放到最后一个节点的位置,然后按照从下往上 (二叉树中说法)进行比较调整的过程,直到满足成为堆的条件即可。
下面以大顶堆为例,从下往上进行建堆:
例如:初始数据序列为:{3, 6, 8, 5, 7},建堆的过程如下:
如上图,就是序列建堆的过程,每次加入一个元素都放到最后,自下向上与父节点进行比较,如果大于父节点,则进行交换,直到到达根节点的位置。
自下而上建堆,时间复杂度为:O(N)
(2)从堆顶弹出堆顶元素以及自上而下调整堆:
主要步骤:
1)将堆顶元素与堆中最后一个元素进行交换(出堆);
2)删除堆中最后一个元素(数组中则通过让数组长度size--,模拟删除最后一个元素的过程);
3)将堆顶元素进行自上而下,与子节点中元素比较,如果子节点中较大的元素大于父节点,则将父节点与该子节点进行交换,直到父节点大于子节点(调整堆)。
将(1)中建的堆中元素8进行出堆以及调整的过程如下:
对于堆中其他元素的出堆以及调整的过程同上,就是重复以上3个步骤。
2.堆排序的java实现
2.1堆的实现
堆是一种特殊的完全二叉树,我们采用数组结构即可实现堆,数组的下标可以实现父节点和子节点的查找。
2.2 建堆(依次向堆中插入元素)
自下而上进行建堆的核心代码:
// 构造大根堆(自下而上的建堆过程)
public static void heapInsert(int[] arr){ // arr存储着待排序元素
for (int i = 0; i < arr.length; i++) {
// 当前插入的索引
int currentIndex = i;
// 父节点索引
int fatherIndex = (currentIndex - 1) / 2;
// 如果当前插入的值大于父节点的值,则交换,并将索引指向父节点
// 然后继续向上与父节点比较,直到不大于父节点,退出循环
while (arr[currentIndex] > arr[fatherIndex]){
swap(arr, currentIndex, fatherIndex); // 交换父子元素的值
currentIndex = fatherIndex; // 继续向上比较
fatherIndex = (currentIndex - 1) / 2; //肯定能回归到索引0,保证到达根节点
}
}
}
2.3弹出堆顶元素并自上而下调整堆
弹出堆顶元素(数组模拟,不是真正的弹出),并将最后一个节点放到堆顶,再自上而下调整,使得其成为新的堆,核心代码如下:
// 弹出堆顶元素并将剩余的数构造成大根堆(通过自上而下进行比较)
public static void heapify(int[] arr, int index, int size){
int left = 2*index + 1; // 左子节点索引
int right = 2*index + 2; // 右子节点索引
while (left < size){ // 保证进行了一次完整的调整
int largestIndex;
// 判断左右孩子中较大的值的索引(要确保右孩子在size范围之内)
if (arr[left] < arr[right] && right < size){
largestIndex = right;
}else {
largestIndex = left;
}
// 比较父节点与孩子中较大的值,确定最大值的索引
if (arr[largestIndex] < arr[index]){
largestIndex = index;
}
// 如果父节点的索引是最大值的索引,退出循环
if (index == largestIndex){
break;
}
// 父节点不是最大值,与孩子中较大值交换
swap(arr, largestIndex, index);
index = largestIndex; //将索引指向孩子中的较大值
// 重新计算交换之后的孩子的索引
left = 2*index + 1;
right = 2*index + 2;
}
}
2.4堆排序的完整代码实现
import java.util.Arrays;
/**
* 堆排序 时间复杂度O(nlogn) 空间复杂度O(1)
* 1. 建堆
* 2. 出堆
*/
public class heapSort {
public static void main(String[] args) {
int[] arr = {3, 6, 8, 5, 7};
heapSort(arr); //大顶堆,排序后为从小到大
System.out.println(Arrays.toString(arr));
}
// 堆排序
public static void heapSort(int[] arr){
// 构造大根堆
heapInsert(arr);
int size = arr.length;
while (size > 1){
// 固定最大值
swap(arr, 0, size-1);
size--;
// 构造大根堆
heapify(arr, 0, size);
}
}
// 构造大根堆(通过新插入的数上升)
public static void heapInsert(int[] arr){
for (int i = 0; i < arr.length; i++) {
// 当前插入的索引
int currentIndex = i;
// 父节点索引
int fatherIndex = (currentIndex - 1) / 2;
// 如果当前插入的值大于父节点的值,则交换,并将索引指向父节点
// 然后继续向上与父节点比较,直到不大于父节点,退出循环
while (arr[currentIndex] > arr[fatherIndex]){
swap(arr, currentIndex, fatherIndex); // 交换
currentIndex = fatherIndex; // 继续向上比较
fatherIndex = (currentIndex - 1) / 2; //肯定能回归到索引0
}
}
}
// 将剩余的数构造成大根堆(通过顶端的数向下比较)
public static void heapify(int[] arr, int index, int size){
int left = 2*index + 1;
int right = 2*index + 2;
while (left < size){
int largestIndex;
// 判断左右孩子中较大的值的索引(要确保右孩子在size范围之内)
if (arr[left] < arr[right] && right < size){
largestIndex = right;
}else {
largestIndex = left;
}
// 比较父节点与孩子中较大的值,确定最大值的索引
if (arr[largestIndex] < arr[index]){
largestIndex = index;
}
// 如果父节点的索引是最大值的索引,退出循环
if (index == largestIndex){
break;
}
// 父节点不是最大值,与孩子中较大值交换
swap(arr, largestIndex, index);
index = largestIndex; //将索引指向孩子中的较大值
// 重新计算交换之后的孩子的索引
left = 2*index + 1;
right = 2*index + 2;
}
}
// 交换数组中的两个元素的值
public static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
堆排序的时间复杂度为:O(nlogn); 空间复杂度为:O(1)。
3.堆排序的应用
堆排序常用于解决处理中位数这类问题的效率非常高,可以借助两个堆一个大顶堆和一个小顶堆,将数据进行划分为两部分,然后根据数据长度(奇数还是偶数)就能快速找到中位数。
3.1问题描述(源自码题集)
数据流的中位数:
对于数据流问题,小码哥需要设计一个在线系统,这个系统不断的接受一些数据,并维护这些数据的一些信息。
中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。
请帮小码哥设计一个支持以下两种操作的系统:
+num -从数据流中添加一个整数k到系统中(0<k <232 )。
? -返回目前所有元素的中位数。
3.2解决思想
借助一个大顶堆q1和一个小顶堆q2,依次将输入的数据平均放到这两个堆中,如果数据总数为偶数,那么两个堆中的元素个数相同各存储一半的数据;如果数据总数为奇数,我们就约定将多出的这个放到大顶堆中。同时还要保证,大顶堆中的数据小于等于小顶堆中的数据,这样就知道前n/2小的数据存在大顶堆中,后n/2大的数据存在小顶堆中。
1.加入一个数x的处理思想:
1)如果大顶堆中元素个数大于小顶堆中的元素个数,先将该数x存到大顶堆中,堆调整好后,再将大顶堆堆顶元素弹出,存放到小顶堆中;
2)不满足1)条件,即只可能是大顶堆q1中的元素个数等于小顶堆q2中的元素个数(*默认约定是大顶堆q1元素个数大于等于小顶堆q2元素*),直接将数x压入到大顶堆q1中即可。然后,还需判断数x压入q1后,q1的堆顶(最大数)是否大于q2的堆顶(最小数),如果满足,则将q1栈顶弹出先压入到q2,然后q2调整后,再弹出栈顶元素压入到q1;不满足则不需要额外的调整。
2.根据数据元素的个数是奇数还是偶数进行分别处理:
1)如果元素个数为偶数,将q1和q2的栈顶元素弹出,求出这两个数的平均值即为该数据流的中位数;
2)如果元素个数为奇数,弹出q1的栈顶元素,即为该数据流的中位数。
3.3代码实现(java)
public class MidNum_heap {
public static void main(String[] args) {
PriorityQueue<Integer> queue1 = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer t1, Integer t2) {
return t2 - t1;
}
}); //大顶堆
PriorityQueue<Integer> queue2 = new PriorityQueue<>(); //小顶堆
Scanner input = new Scanner(System.in);
// code here
int n = input.nextInt();
input.nextLine(); // 读取回车
String[] strings = new String[n];
for (int i = 0; i < n; i++) {
strings[i] = input.nextLine();
String[] s = strings[i].split(" ");
if (s[0].equals("+")){
int x = Integer.parseInt(s[1]);
push(queue1, queue2, x);
}else{
if (queue1.size() > queue2.size()){
System.out.println(queue1.peek());
}else {
double res = queue1.peek() + queue2.peek();
System.out.println(res * 1.0 /2);
}
}
}
input.close();
}
public static void push(PriorityQueue<Integer> queue1, PriorityQueue<Integer> queue2, int x){
if (queue1.size() > queue2.size()){
queue1.add(x);
queue2.add(queue1.peek());
queue1.poll();
}else {
queue1.add(x);
if (queue2.size() != 0 && queue2.peek() < queue1.peek()){
queue2.add(queue1.peek());
queue1.poll();
queue1.add(queue2.peek());
queue2.poll();
}
}
}
}
注:此代码使用的是java中的优先队列实现,java的底层中优先队列就是使用的数据结构**堆**实现的,使用的是一个Object[ ]数组实现。