堆是一种特殊的完全二叉树
- 完全二叉树即除了最后一层其它层都是满的,且最后一层的数据全部靠左排列。
- 特殊在,他的每个节点的值都大于等于(或者小于等于)其子树节点,因此堆又分为大顶堆和小顶堆。
因为是完全二叉树,我们存储堆的时候一般使用数据来存储,第一个0号元素留空,这样的话节点是a[n],左节点就是a[2n],右节点就是a[2n+1],父节点就是a[n/2]。当然不留空也可以,推算父节点的时候总是要先进行-1操作。
堆主要有两个操作
- 删除堆顶元素
- 插入一个元素
这两个操作都涉及到了堆化(重新调整,使其满足堆的特性),删除堆顶元素我们选择的是把最后一个元素移到堆顶来,从上而下堆化(比较当前节点和子节点的大小,不满足则互换位置)。插入一个元素我们选择把元素插入到最后,然后从下往上堆化(比对当前节点和父节点的大小,不满足则互换位置,互换后一直)。
关于堆的应用
- 计算Top K
原理:先去数据中前K个数字建立一个大小为K的小顶堆(堆顶元素最大),然后每进来一下新数字,比对和堆顶元素的大小。如果比堆顶元素大,则删除堆顶元素,并插入新数字,重新堆化。 - 优先级处理,比如定时任务,等待队列等等
原理:每插入或者删除一个新元素,重新堆化处理。堆顶元素永远是优先级最高的一个。 - 堆排序
代码实现
/**
* 小顶堆的实现
* 堆顶元素最小,子元素均大于堆顶元素
*/
public class Heap {
private int[] a;//数组,从下标1开始存储数据
private int n;//堆存储的最大个数
private int count;//目前个数
public Heap(int n) {
this.n = n;
count = 0;
a = new int[n + 1];
}
public Heap(int[] a) {
this.a = a;
count = a.length - 1;
n = count;
}
public void removeHeader() {
if (count == 0) {
return;
}
a[1] = a[count];
int current = 1;
//从上而下堆化,需要用父元素和下面两个子节点比对
heapify(a, count, current);
}
public boolean insert(int data) {
if (count == n) {
return false;
}
count++;
a[count] = data;
int i = count;
//从下往上堆化,和父元素对比即可
while (i / 2 > 0 & a[i / 2] > a[i]) {
//父元素大于子元素
swap(a, i, i / 2);
i = i / 2;
}
return true;
}
/**
* 这个a的第一个元素应该是个空的
* 空,数字,数字,数字
* 0, 1 ,2 , 3 ,
*
* @param a
* @param n a数组中数据的个数,可能a的长度是100,数字确是6个,后93个忽略。
* @param current 给当前角标的数字寻找一个合适的位置存放堆化的a,
* current子节点的节点需要是已经满足堆化的数据,否则该方法不适用
*/
private static void heapify(int a[], int n, int current) {
while (true) {
int minxPos = current;
if (current * 2 <= n && a[current] > a[current * 2]) {
minxPos = current * 2;
}
//这里要用if而不是elseif,且是a[mixPos]对比,是因为要找出两个子节点中最小的一个
if (current * 2 + 1 <= n && a[minxPos] > a[current * 2 + 1]) {
minxPos = current * 2 + 1;
}
if (minxPos == current) {
break;
}
swap(a, current, minxPos);
current = minxPos;
}
}
/**
* 从下而上堆化
*
* @param a
* @return
*/
public static Heap buildHeap1(int[] a) {
Heap heap = new Heap(a.length);
for (int i = 0; i < a.length; i++) {
heap.insert(a[i]);
}
return heap;
}
/**
* 从上而下堆化
* 我们仅需要对下标从1到n/2的数据进行堆化,小标是n/2+1带n的节点是叶子节点,不需要堆化。
* 从上而下堆化本就是拿父节点和两个子节点对比,找出三者中最合适做父元素的一个值。
*
* 这种建堆方法的复杂度是O(n)
* @param b
* @return
*/
public static Heap buildHeap2(int[] b) {
int[] a = new int[b.length + 1];
for (int i = 0; i < b.length; i++) {
a[i + 1] = b[i];
}
//必须逆序,每个值动过一次之后就是最合适的位置了(最下面的没有自节点了),不需要再动了
for (int current = a.length / 2; current >= 1; current--) {
heapify(a, b.length, current);
}
return new Heap(a);
}
}
堆排序的思想,首先我们对数据进行从下到上的建堆操作(对每个元素进行堆化)。然后取出堆顶元素和最后一个元素交换位置。然后堆化对顶元素,直到剩余一个元素的时候,整体有序。
public class SortHeap {
public static void main(String[] args) {
int[] nums = new int[]{9, 1, 2, 3, 4, 5, 6, 7, 8};
new HeapSort().sort(nums);
}
public void sort(int[] nums) {
//倒着为每个数据找到一个合适的位置所有的数据,
//最终形成了一个堆
for (int i = nums.length / 2 - 1; i >= 0; i--) {
heap(nums, nums.length, i);
}
print(nums);
/**
* 已形成一个堆,堆顶元素已经符合规则
*/
for (int i = nums.length - 1; i >= 0; i--) {
//把已经排序好的数字放到最后一个,堆化剩下的数据
int temp = nums[i];
nums[i] = nums[0];
nums[0] = temp;
//第一个数字一直在改变,一直堆化第一个
heap(nums, i, 0);
print(nums);
}
print(nums);
}
private void print(int[] nums) {
for (int i = 0; i < nums.length; i++) {
System.out.print(nums[i] + ",");
}
System.out.println();
}
/**
* 一个完全二叉树,总是有(n+1)/2个子节点
* 一个满二叉树,总是有(n+1)/2个子节点(n总是奇数,+1变成偶数)。
* 然后追加成完全二叉树,每当n+1的时候,子节点数量不变,因为8/2和9/2是一样的值
* 然后再追加一个,此时子节点数量就会+1了,还是满足(n+1)/2个子节点,
* n是奇数的时候,有n+1/2个子节点,n为偶数的时候,有n/2个子节点
* 所以说,n奇数的时候有n-1/2个子节点,所以说总有n/2个子节点
* 所以index角标是n/2-1
* <p>
* <p>
* <p>
* 传进来的数组仅有index所在位置不合适,导致其不符合堆得规则。
* index和子节点对比找到最小的一个放到堆化的节点上,
* 然后改变了的节点在和子节点比对形成正式的堆
*
* @param nums
*/
private void heap(int[] nums, int needHeapLength, int index) {
while (true) {
//这里要一直找到nums[i]应该在的位置
int minIndex = index;
if (index * 2 + 1 < needHeapLength
&& nums[index * 2 + 1] < nums[index]) {
//设置当前minIndex的值,是树的左下角
minIndex = index * 2 + 1;
}
if (index * 2 + 2 < needHeapLength
&& nums[index * 2 + 2] < nums[minIndex]) {
//设置当前minIndex的值,是树的右下角
minIndex = index * 2 + 2;
}
if (minIndex == index) {
break;
}
//交换index和minIndex的位置
int num = nums[index];
nums[index] = nums[minIndex];
nums[minIndex] = num;
//一个值挪动了,接着验证被挪动走的值的下方是否符合堆化条件,不符合继续动
index = minIndex;
}
}
}
堆排序是不稳定排,因为初始化后最后一个元素,可能会被和堆顶元素交换。然后接着被堆化的时候移动到前面去了。
比如:98,86,68,58,421,422(堆化之后)
然后对顶元素移动到最后:86,68,58,421,98
然后堆化第一个元素:86,68,58,422,421,98
…
依次类推,最终422,421的局面会形成。