思路大纲:
因为堆是一棵完全二叉树,所以可以使用数组存储。
所以,首先需要创建数组,并对数组分配内存和初始化,模拟实现包括创建大根堆、向下调整、插入、删除等功能,其中这些功能需要许多交换、判断isFull或者empty,所以这些可以单独写一个方法
思维导图:
模拟实现:
完整代码可以参考我的码云:
堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储。
基本要素的创建:
包括:数组、记录元素个数的计数器
数组创建完成,1,首先分配内存2、其次数组的初始化
public class TestHeap {
//定义数组
public int[] elem;
//定义元素个数
public int usedSize;
//数组分配内存
public TestHeap(){
this.elem = new int[10];
}
//数组初始化
public void initElem(int[] array){
//初始化的个数取决于传过来的array的length
for (int i = 0; i < array.length; i++) {
elem[i] = array[i];
usedSize++;
}
}
}
创建大根堆:
如果有一个关键码的集合K = {k0,k1, k2,...,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2...,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
创建大根堆的思路:
usedSize是元素的个数,因为是使用数组实现的,所以usedSize-1才是真正子在树中第“usedSize”个元素
得到父母结点的公式
public void createHeap(){
//只需要得到有子树的父母结点
for (int parent = (usedSize -1 -1) / 2; parent >= 0 ; parent--) {
siftDown(parent , usedSize);
}
}
其中对树中的元素进行大根堆排序 进行调整称作向下调整,思路里第一条强调的很重要
向下调整:
向下调整的思路就是大根堆中的那个方法,思路一样
代码中的注释也是强调的重点:
public void siftDown(int parent,int len){
int childe = 2*parent +1;
//至少 有左孩子
while(childe < len){
//如果有右孩子 那么右孩子的下标也要小于len
//找出两个中间的最大值
if(childe+1<len&&elem[childe] < elem[childe+1]){
childe = childe+1;
}
if(elem[childe]>elem[parent]){
swap(childe,parent);
parent = childe;
childe = 2*parent + 1;
}else {
break;
}
}
插入元素:
插入之后也要保证大根堆的要求
先插入最后一个位置,然后进行child和parent的比较,下面c是child的缩写,p是parent的缩写
插入时要判断是否数组已经满了,如果满了就扩容
在把元素插入后,将插入的元素也按照大根堆排序完成的过程叫做向上调整
但传入向上调整的参数是usedSize加1前的值,也就是插入元素,计数器还没加,就传,原因是usedSize是可以存储元素的下标,此时传的usedSize正好是数组中元素的个数
public void push(int val){
//满了
if(isFull()){
elem = Arrays.copyOf(elem,2*elem.length);
//usedSize新增元素的下标
elem[usedSize] = val;
}
//向上调整
siftUp(usedSize);
usedSize++;
}
向上调整:
向上调整的过程中只需要child和parent进行比较不需要两个child之间再进行比较,因为如果原来的parent就是比child大,只要插入的这个比parent大,那他也比另一个child大
public void siftUp(int child){
int parent = (child -1)/2;
while(child > 0){
if(elem[child] > elem[parent]){
swap(child,parent);
child = parent;
parent = (child - 1)/2;
}else {
break;
}
}
}
判断是不是满了:
public boolean isFull(){
// if(usedSize == elem.length){
// return true;
// }
// return false;
return usedSize == elem.length;
}
当两个元素需要交换时:
public void swap(int i, int j){
int tmp = elem[i];
elem[i] = elem[j];
elem[j] = tmp;
}
删除元素:
删除的时候,只需要最后一个元素和第一个元素交换,并且计数器减1,也要注意传入向下调整的usedSize的值,siftDown和usedSIze的顺序有影响
因为是把原来的第usedSize和第1个互换了,所以删除之后,除了根节点,其他的都是保持着小堆或者大堆,只需要从 p = 0向下调整就可以了
//删除 /判断是不是空 /根节点和最后一个结点交换 /usedSize -- /向下调整
public int pop(){
//是不是empty
if(empty()){
return -1;
}
int oldVal = elem[0];
//usedSize才是最后一个叶子结点元素的下标
swap(0,usedSize-1);
usedSize--;
siftDown(0,usedSize);
return oldVal;
}
判断是不是空数组:
public boolean empty(){
return usedSize == 0;
}
堆排序:
复杂度O(N*logN)
建立大根堆
public void heapSort(){
int end = usedSize - 1;
while(end > 0){
swap(0,end);
siftDown(0,end);
end--;
}
}