目录
存储方式
在一个集合的数据中,找出最大值或者最小值
逻辑上完全二叉树
实际上是可以通过数组保存的
满足任意结点的值都大于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆 ,反之就是小堆
使用数组保存二叉树结构,方式即将二叉树用层序遍历方式放入数组中,一般只适合表示完全二叉树,因为非完全二叉树会有空间的浪费,这种方式的主要用法就是堆的表示
下标关系
已知双亲(parent)的下标:
左孩子(left)下标 = 2 * parent + 1
右孩子(right)下标 = 2 * parent + 2
已知孩子(不区分左右)(child)下标:
双亲(parent)下标 = (child - 1) / 2
操作-向下调整(示例:建立一个小堆)
它的搜索比较是往下搜索比较
向下调整的前提条件是,除了需要调整的位置和它的两个孩子不知道是否满足大堆的条件,但其他地方一定满足
小堆不一定有序,有序(升序的)的一定是小堆
向下调整就是一个皇帝便乞丐的过程
- 判断index(需要调整的位置)的位置是不是叶子结点,如果是,直接返回
- 找到两个孩子中的最小值
小孩子的值和index 的位置进行比较- 如果小孩子的值 ==/ > index的值直接return
- < 就直接交换,把最小的孩子视为index 再次循环
/**
* 向下调整
* 因为向下调整是有前提的,所以需要建堆
* 时间复杂度 最坏就是log(n)
* @param array
* @param size
* @param index
*/
public static void adjustDown(int[] array,int size,int index){
int leftIndex = 2*index+1;
//1.判断index 是不是叶子
if(leftIndex >= size) return;//说明需要调整的结点是叶子结点 直接跳出
//现在已经确定了它有左孩子
//2.找最小的的孩子
//需要找到它的孩子里面的最小值 --所以需要判断有没有右孩子
int midIndex = leftIndex;//这种先赋值之后再次进行更改的做法比较不错,可以学习
int rightIndex = leftIndex+1;
if(rightIndex < size && array[rightIndex] < array[index]){
//说明最小的孩子是右孩子
midIndex = rightIndex;
}
//3.比较最小孩子的值
if(array[index] <= array[midIndex]){
return;
}
//4.交换
int t = array[index];
array[index] = array[midIndex];
array[midIndex] = t;
//5.把最小的孩子看作index 继续循环
index = midIndex;
adjustDown(array,size,index);
}
写代码注意事项
- 怎么判断index的位置是不是叶子(原来的想法是,看它的左右结点是不是null,但是思想误区是现在是一个数组,不能用是不是null来判断)而是和size判断
所以没有孩子就是叶子结点,同时又因为是一个完全二叉树,所以没有左孩子就说明是一个叶子结点。 - 确定有左孩子之后,就去判断有没有右孩子
操作-向上调整(示例:建立大堆)
搜索比较是向上搜索比较
向上调整只需要和它的父节点比较即可,如果比父节点大,就交换。是一个类似乞丐变皇帝的过程
/**
* 向上调整
* index 是需要向上调整的起始位置
* 或者也可以写成循环的方式
*/
public static void adjustUp(int[] array,int size,int index){
//如果是要调整的是根调整结束
if(index == 0) return;
//index父节点
int parentIndex = (index -1)/2; // 这里的下标永远大于等于0
//和父亲比较
if (array[parentIndex] <= array[index]){
return;
}
//交换
int t = array[index];
array[index] = array[parentIndex];
array[parentIndex] = t;
//把父节点看作index 接续循环
index = parentIndex;
adjustUp(array,size,index);
}
建堆(小堆)
基于向下调整(向下搜索–找孩子) 的方式建堆,遍历方式必须从后往前遍历
基于向上调整(向上搜索–找父亲)的方式建堆,遍历方式必须从前往后遍历
对一个数组利用向下调整的方式进行建堆,关键就是找到第一个非叶子结点,然后从第一个非叶子结点开始依次向下调整。采用向下调整的过程必须是从后往前遍历。(因为向下调整一定要满足一个前提条件,就是除了要调整的位置,其他满足了堆关系)。
建堆
* 找到第一个非叶子结点
* 第一个非叶子结点其实就是最后一个结点的的父节点
public static void createHeap(int[] array,int size){
//找到最后一个非叶子结点,然后不断进行向下调整
int lastIndex = size -1;
int lastParentIndex = (lastIndex -1)/2;
//从【lastParent ,0】 不断地进行向下调整
for(int i = lastParentIndex;i >= 0;i--){
adjustDown(array,size,i); //使用上面的小堆代码
}
}
优先队列(示例建小堆)
优先级队列的实现方式有很多,但最常见的是使用堆来构建。
- 入队列的时候,就把元素插入到数组末尾,然后向上调整。
- 进行出队列的时候,就把堆顶的元素删除同时让最后一个元素来顶替第一个元素的位置,并且进行向下调整
时间复杂度
入队列 O(logN)
出队列 O(logN)
取队首元素 O(1)
建堆O(N)
入队列
/**
* 插入的操作
* 采用尾插,使用向上调整
* 乞丐变皇帝 o(log n)
*/
public void add(Integer e){
array[size] = e;
size++;
adjustUp(size-1);
}
private void adjustUp(int index){
if(index == 0) return;//要调整的是不是根
while (index != 0){
int parentIndex = (index -1)/2; // 这里的下标永远大于等于0
//和父亲比较
if (array[parentIndex] <= array[index]){
return;
}
//交换
int t = array[index];
array[index] = array[parentIndex];
array[parentIndex] = t;
//把父节点看作index 接续循环
index = parentIndex;
}
}
出队列
/**
*删除实现的操作就是 把数组的第一个元素删除,
* 然后让最后一个元素来顶替第一个元素的位置,之后进行向下调整即可
*/
public Integer remove(){
if(size == 0){
throw new RuntimeException("数组是空的");
}
int e = array[0];
array[0] = array[size-1];
//维护size
size--;
//向下调整
//只是说这里的向下调整的index变成了0的位置
//Priority.adjustDown();或者去调用之前写好的
adjustDown(0);
return e;
}
private void adjustDown(int index){
int leftIndex = index * 2+1;
if(leftIndex >= size) return;
int minIndex = leftIndex;
int rightIndex = leftIndex+1;
if(rightIndex < size && array[rightIndex] < array[leftIndex]){
minIndex = rightIndex;
}
if(array[index] <= array[minIndex]) {
return;
}
//交换
int t = array[index];
array[index] = array[minIndex];
array[minIndex] = t;
index = minIndex;
adjustDown(index);
}
堆的应用
1.TopK问题
海量数据,找到集合中最大的前几个
* 用大堆还是小堆-- 用小堆
* 海量数据的问题决定了我们不能对所有数据进行建堆
* 建立一个五个数据的小堆,每一个要比较的数据都和这个堆里面堆顶去比较
* 如果比堆顶大,那么就可以替换这个堆顶,然后对堆进行向下调整即可。
2. 堆排序
待补充
3. 自定义类堆的构建
/**
* 堆的应用3
* 由于堆的构建,需要比较以及调整的能力
* 所以如果用自定义类进行堆的构建,那么需要实现比较能力
* 自定义类
*/
public static void main(String[] args) {
// PriorityQueue<Person> queue = new PriorityQueue<Person>();
//本质上不是在new 接口,而是new了一个匿名内部类
//按照年龄
/*PriorityQueue<Person> queue = new PriorityQueue<Person>(new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.age - o2.age;
}
});*/
//按照名字的字典序
/*PriorityQueue<Person> queue = new PriorityQueue<Person>(new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.name.compareTo(o2.name);
}
});*/
//简化lambda表达式
PriorityQueue<Person> queue = new PriorityQueue<Person>(
(Person o1, Person o2) -> {
return o1.age - o2.age;
}
);
Person p1 = new Person("gb",35);
Person p2 = new Person("cpx",34);
Person p3 = new Person("tz",45);
queue.add(p1);
queue.add(p2);
queue.add(p3);
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
}
class Person implements Comparable<Person>{ //注意这个类为什么是这样写的,相当于是用了泛型
String name;
int age;
Person(String name,int age){
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Person o) {
//由于上面使用了泛型,所以这里就不用传递泛型了
return age -o.age;
}
}
JDK的优先队列
默认JDK实现的是一个小堆。标准库中的优先队列默认的是小堆,定义了“值越小,优先级越高”
public static void main1(String[] args) {
MyPriorityQueue queue = new MyPriorityQueue();
//PriorityQueue<Integer> queue= new PriorityQueue<>();
queue.add(3);
queue.add(5);
queue.add(2);
queue.add(7);
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
}