堆、优先级队列

二叉树的存储方式

一颗完全二叉树逻辑上是以数组的形式进行保存的,是将完全二叉树以二叉树层序遍历的方式保存到数组中的。

完全二叉树的下标之间有一个固定的公式:

已知父结点的下标 parentIndex 
    左孩子的下标为:2*parentIndex +1 
    右孩子的下标为:2*parentIndex +2
已知某个结点的下标为 childIndex
    其父结点的下标为:(childIndex - 1/2

  1. 堆实质上就是在一个数据集合中找最值,能够快速在一个集合中找的最值。
  2. 堆在逻辑上是一个完全二叉树,但是以数组的形式进行存储
  3. 堆可以分为大堆和小堆;
  4. 任意结点的值都小于等于它的孩子的值(堆顶值最小)的称为小堆;
  5. 任意结点的值都大于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆。
  6. 小堆不一定是有序的,但是有序的一定是小堆。

堆的操作

向下调整

在进行向下调整时有一个前提

只有需要调整的位置不清楚,但其他位置都已经满足堆的性质了。
(即要调整的位置的左右子树必须已经是一个堆,才能进行调整)

向下调整操作的简单步骤:

  • 判断要调整的位置(index)是不是叶子结点,如果是直接return,不需要再进行向下调整
  • 否则,找到index位置的两个孩子中值最小的一个;
  • 将小孩子的值于index位置的值进行比较:
    • 小孩子的值 < index 位置的值
    • 小孩子的值 >= index 位置的值,调整结束
  • 交换小孩子的值 与 index位置的值
  • 把小孩子作为要调整的值(index),进行循环(从第一步开始判断)

这里循环结束的出口有两个:
1.当要调整的值为叶子结点时,结束调整
2.当小孩子的值大于等于要调整的值时,结束调整

向下调整的代码:

public class Heap {
    //向下调整
    public static void shiftDown(int[] array,int size,int index) {
        //1.判断index是不是叶子结点(有没有孩子),看左孩子的下标是否越界,如果越界则没有左孩子
        int leftIndex = 2*index +1;
        if(leftIndex >= size) {
            return;
        }
        //2.找出两个孩子中最小的
        int minIndex = leftIndex;
        int rightIndex = leftIndex +1;
        if(rightIndex < size && array[rightIndex] < array[leftIndex]) {
            minIndex = rightIndex;
        }
        //3.小孩子的值与index位置的值进行比较
        if(array[index] <= array[minIndex]) {
            return;
        }
        //4.交换小孩子的值与index位置的值
        int t = array[index];
        array[index] = array[minIndex];
        array[minIndex] = t;
        //4.把最小的孩子作为index,继续循环
        index = minIndex;
    }
}

建堆

建堆的过程实质上也是进行不断地向下调整。
要将一个完全二叉树变成最小堆的形式,以小堆为例:

如果保证左子树和右子树满足堆的性质,只需要对根结点进行向下调整,同理,对左子树和右子树进行相应的操作,就会得到堆。

需要进行向下调整的位置为从最后一个非叶子结点(最后一个结点的父节点)到根结点

public static void createHeap(int[] array,int size){
       //从最后一个非叶子结点(最后一个结点的父结点)开始进行向下调整,i--
        int lastIndex = size -1;
        int lastparentIndex = (lastIndex-1)/2;
        for(int i = lastparentIndex; i >= 0;i--){
            shiftDown(array,size,i);//调用上面的向下调整的方法
        }
    }

时间复杂度

向下调整:O(log(n)) (最坏情况为比较次数为二叉树的高度)
建堆:O(n*log(n)) (约等于O(n)

log(1000) 约等于 10
log(100w) 约等于 20
log(10亿) 约等于 30

优先级队列

实现了Queue接口

java.util.PriorityQueue implement Queue

优先级队列的操作

实际上是最小堆的形式
queue.add(); 入队列
queue.remove();出队列
queue.element();队首元素

import java.util.PriorityQueue;

public class PriorityQueueDemo {
    public static void main(String[] args) {
        PriorityQueue<Integer> queue = new PriorityQueue<>();
        queue.add(3);
        queue.add(4);
        queue.add(7);
        queue.add(8);
        queue.add(2);
        System.out.println(queue.remove());
        System.out.println(queue.remove());
    }
}

实现自己的优先级队列

  1. 返回队首元素,即堆顶元素,也就是数组的第一个元素array[0];
  2. 删除队首元素:
    不是直接将堆顶元素删除,而是用数组的最后一个元素替换堆顶元素,然后通过向下调整的方式重新调整成堆。
  3. 插入指定元素
    先将e插入到二叉树的最后一个位置,然后对该位置进行向上调整。
public class MyPriorityQueue {
    Integer[] array = new Integer[100];
    int size = 0;
//检索队首元素,但不删除
    public Integer element(){
        if(size == 0) {
            throw new RuntimeException("空");
        }
        return array[0];
    }
//删除队首元素:
//不是直接将堆顶元素删除,而是用数组的最后一个元素替换堆顶元素,然后通过向下调整的方式重新调整成堆
    public Integer remove() {
        int e = array[0];
        array[0] = array[size-1];
        size--;
        adjustDown(array ,size,0);
        return  e;
    }
    public void adjustDown(Integer[] array,int size,int index) {
        int leftIndex= 2 * index +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;
    }
//插入指定元素
    public void add(Integer e) {
        //先将e插入到二叉树的最后一个位置
        array[size] = e;
        size++;
        //将最后一个元素进行向上调整
        adjustUp(array,size,size-1);
    }
    //向上调整
    public void adjustUp(Integer[] array,int size,int index) {
        //1.先判断index位置是不是树的根
        if(index == 0) {
            return;
        }
        //2.找到父结点
        int parentIndex = (index-1)/2;
        //3.比较(index -1)/2 位置的值与index位置的值
        if(array[parentIndex] <= array[index]) {
            return;
        }
        //交换值
        int t = array[index];
        array[index] = array[parentIndex];
        array[parentIndex] = t;
        //将父结点作为index,进行循环
        index = parentIndex;
    }
}

结合自定义类的PriorityQueue

需要用到Comparable 或者 Comparator

public class Person implements Comparable<Person>{
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(Person o) {
        return age - o.age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class PriorityQueueDemo {
    public static void main(String[] args) {
        PriorityQueue<Person> queue = new PriorityQueue<>(new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.name.compareTo(o2.name);
            }
        });
        Person p1 = new Person("a",18);
        Person p2 = new Person("b",23);
        Person p3 = new Person("c",14);
        Person p4 = new Person("d",20);
        Person p5 = new Person("e",16);
        queue.add(p1);
        queue.add(p2);
        queue.add(p3);
        queue.add(p4);
        queue.add(p5);
       System.out.println(queue.remove());
    }
}

TopK问题

从arr[1, n]这n个数中,找出最大的k个数,这就是经典的TopK问题。
例如:从arr[1, 12]={5,3,7,1,8,2,9,4,7,2,6,6} 这n=12个数中,找出最大的k=5个。
使用堆进行解决

  1. 先用前k个元素生成一个堆,用于存储当前最大的k个元素。
  2. 接着,从第k+1个元素开始和堆顶(堆中最小的元素)比较,如果被扫描的元素大于堆顶,则替换堆顶的元素,并调整堆,以保证堆内的k个元素,总是当前最大的k个元素。
  3. 直到,扫描完所有n-k个元素,最终堆中的k个元素,就是要求的TopK。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值