优先级队列(堆)PriorityQueue

1、概念

        队列是一种先进先出的结构(排队买饭)。然而有的时候我们需要让优先级更高的元素出去,此时就出现了优先级队列的概念。

        此时数据结构应该满足最基本的两个要求:1.可以输出最高优先级的对象。2.可以输入新对象。

        堆本质上,就是用数组或者链表来存储数据,通过模拟完全二叉树的方式,开始先优先级的区分。

2、堆的存储

        对于非完全二叉树而言,如果采用数组的方式进行存储,会导致数组空间浪费,此时应采用链表形式进行存储。

        将数组转化为一颗二叉树的时候,牢记二叉树的性质进行还原。

        假设i为结点在数组中的下标,则:

        a.如果i为0,则i表示根结点。否则,可以根据i计算出双亲地址,为(i-1) / 2 

        b.如果2*i+1小于总结点个数(其实就是根据a进行反推的,由父亲结点推孩子结点),则结点i的左孩子下标为2*i+1。否则,该父亲结点没有左孩子。

        c.如果2*i+2小于总结点个数,则结点i的右孩子下标为2*i+2。否则,该父亲结点没有右孩子。

3、堆的创建

        对于一个数组{27,15,19,18,28,34,65,49,25,37},如何将其创建成一个堆呢(其实就是对数组中的元素进行排序,排完序以后,这个数组就称为我要的堆)。原始的二叉树的图如下所示。

3.1向下调整法

向下调整的过程:

1.让parent表示需要调整的结点,child表示parent的左孩子。

2.如果parent的左孩子存在,即child<size,则进行一下操作,直到parent的左孩子不存在为止。

        a.判断parent的右孩子是否存在,如果存在,则找到左右孩子中最小的孩子,让child表示这个孩子。

        b.将parent表示的孩子与child表示的孩子进行比较

                如果parent小于child,则调整结束

                否则交换parent与child,交换完成以后,其他的树可能发生变化,导致不是小根堆,因此需要继续向下调整,将每棵树都调整一次。即parent = child(调整child的子树),child =

parent * 2 + 1(找到新parent的左子树,继续上述调整);

public void shiftDown(int[] array , int parent){
    //child
    int child = 2 * parent + 1;
    int size = array.length;
    while (child < size){
        //如果右孩子存在,并且右孩子比左孩子更小
        if (child + 1 < array.length && array[child+1] < array[child]){
            //那么就让child指向右孩子,因为它更小
            child += 1;
        }

        if (array[parent] < array[child]){
            //此时已经是小根堆了
            break;
        }else {
            //交换parent和child,让最小的上去
            int tmp = array[child];
            array[child] = array[parent];
            array[parent] = tmp;
            //换完以后可能导致其他的树不是小根堆了,要继续调整
            parent = child;
            child = 2 * parent + 1;
        }
    }
}

/**
 * 一颗普通的二叉树,即每棵树都有可能不是小根堆,需要自己调整
 */
public  void createHeap(int[] array){
    //找到倒数第一个非叶子结点(即最后一个父亲结点),从该节点位置一直向前直到根结点,每遇到一个结点就调用向下调整法。
    //array.length-1表示最后一个叶子结点下标
    //(叶子结点-1) / 2表示最后一个根结点下标
    int root = ((array.length-1)-1) /2 ;
    for (;root>0;root--) {
        shiftDown(array,root);
    }
}

4、堆的插入与删除

堆的插入总共有两个步骤:

1.先将元素放入到底层空间中(注意扩容)

2.将最后的新加入的结点向上调整,直至满足堆性质。

public void shiftUp(int child) {
    //找到child的父亲
    int parent = (child-1)/2;
    while (child > 0){
        //如果父亲比孩子大,满足性质
        if (array[parent] < array[child]){
            break;
        }else {
            //父亲儿子结点交换
            int tmp = array[child];
            array[child] = array[parent];
            array[parent] = tmp;

            //牵一发而动全身,需要将其他的子树也都调整
            //向上的过程也就体现在这个地方。儿子不断替代父亲的位置
            child = parent;
            parent = (child-1)/2;
        }
    }

}

堆的删除:堆的删除,一定是删除堆顶的元素,具体如下:

1.将堆顶元素与最后一个元素进行交换。

2.将堆中有效数据个数减少一个(表示删除操作)。

3.对堆顶元素进行向下调整

public void delete(){
    //1.交换
    int top = array[0];
    int last = array[array.length-1];

    swap(array,top,last);

    shiftDown(top);

    usedSize--;
    
}

5、用堆模拟实现优先级队列

public class MyPriorityQueue {
// 演示作用,不再考虑扩容部分的代码
    private int[] array = new int[100];
    private int size = 0;
    public void offer(int e) {
    array[size++] = e;
    shiftUp(size - 1);
}

public int poll() {
    int oldValue = array[0];
    array[0] = array[--size];
    shiftDown(0);
    return oldValue;
}

public int peek() {
    return array[0];
    }
}

6、常用接口

上面都是自己模拟实现优先级队列的特性,下面是直接调用java中已有的代码。

static void TestPriorityQueue(){
    // 创建一个空的优先级队列,底层默认容量是11
    PriorityQueue<Integer> q1 = new PriorityQueue<>();
    // 创建一个空的优先级队列,底层的容量为initialCapacity
    PriorityQueue<Integer> q2 = new PriorityQueue<>(100);
    ArrayList<Integer> list = new ArrayList<>();
    list.add(4);
    list.add(3);
    list.add(2);
    list.add(1);
    // 用ArrayList对象来构造一个优先级队列的对象
    // q3中已经包含了三个元素
    PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
    System.out.println(q3.size());
    System.out.println(q3.peek());
}
// 注意:默认情况下,PriorityQueue队列是小堆,如果需要大堆需要用户提供比较器
// 用户自己定义的比较器:直接实现Comparator接口,然后重写该接口中的compare方法即可
class IntCmp implements Comparator<Integer>{
    @Override
    public int compare(Integer o1, Integer o2) {
    return o2-o1;
    }
}
public class TestPriorityQueue {
    public static void main(String[] args) {
        PriorityQueue<Integer> p = new PriorityQueue<>(new IntCmp());
        p.offer(4);
        p.offer(3);    
        p.offer(2);
        p.offer(1);
        p.offer(5);
        System.out.println(p.peek());
    }
}
boolean offer(E e)
插入元素 e ,插入成功返回 true ,如果 e 对象为空,抛出 NullPointerException 异常,时间复杂度O(logN),注意:空间不够时候会进行扩容
E peel()
获取优先级最高的元素,如果优先级队列为空,返回 null
E poll()
移除优先级最高的元素并返回,如果优先级队列为空,返回 null
int size()获取有效元素个数
void clear()

清空

boolean isEmpty()判断是否为空

扩容:

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // Double size if small; else grow by 50%
    int newCapacity = oldCapacity + ((oldCapacity < 64) ?
    (oldCapacity + 2) :
    (oldCapacity >> 1));
    // overflow-conscious code
    if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);
    queue = Arrays.copyOf(queue, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
    throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
    Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}

优先级队列扩容说明:

1.如果容量小于64,则2倍扩容。

2.如果容量大于等于64,则1.5倍扩容。

3.如果容量大于MAX_ARRAY_SIZE,则按照MAX_ARRAY_SIZE扩容

7、关于比较,如equals、comparble、compareTo

7.1 equals比较

基本数据类型可以直接比较

自定义的对象,需要重写比较方法。

自定义类型都继承Object类,而Object类中提供了equals方法,因此需要在创建的类对象中重写该方法。

equals特点:只能比较是否相等,不能比较谁大谁小。

public class Card {
    public int rank; // 数值
    public String suit; // 花色
    public Card(int rank, String suit) {
    this.rank = rank;
    this.suit = suit;
    }

    @Override
    public boolean equals(Object o) {
    // 自己和自己比较
    if (this == o) {
    return true;
    }
    // o如果是null对象,或者o不是Card的子类
    if (o == null || !(o instanceof Card)) {
    return false;
    }
    // 注意基本类型可以直接比较,但引用类型最好调用其equal方法
    Card c = (Card)o;
    return rank == c.rank && suit.equals(c.suit);
    }
}

7.2 comparable比较

public interface Comparable<E> {
    // 返回值:
    // < 0: 表示 this 指向的对象小于 o 指向的对象
    // == 0: 表示 this 指向的对象等于 o 指向的对象
    // > 0: 表示 this 指向的对象大于 o 指向的对象
    int compareTo(E o);
}

对用用户自定义类型,如果要想按照大小与方式进行比较时:在定义类时,实现Comparble接口即可,然后在类中重写compareTo方法。

public class Card implements Comparable<Card> {

    @Override
    public int compareTo(Card o) {
    if (o == null) {
    return 1;
    }
    return rank - o.rank;
    }

}

7.3 基于比较器比较

用户自定义比较器类,实现 Comparator 接口
相当于自己写了一个接口,叫做 Comparator,其中有一个方法,叫compare,以后继承这个接口的时候,直接复写里面的compare方法就可以实现比较了。
本质与comparble类似
public interface Comparator<T> {
    // 返回值:
    // < 0: 表示 o1 指向的对象小于 o2 指向的对象
    // == 0: 表示 o1 指向的对象等于 o2 指向的对象
    // > 0: 表示 o1 指向的对象等于 o2 指向的对象
    int compare(T o1, T o2);
}
Object.equals
因为所有类都是继承自 Object 的,所以直接覆写即可,不过只能比较相等与否
Comparable.compareTo
需要手动实现接口,侵入性比较强,但一旦实现,每次用该类都有顺序,属于内部顺序
Comparator.compare
需要实现一个比较器对象,对待比较类的侵入性弱,但对算法代码实现侵入性强

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值