【数据结构与算法】第九章:优先队列(最大、最小、索引)

普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在某些情况下,可能需要找出队列中的最大值或者最小值

例如:使用一个队列保存计算机的任务,一般情况下计算机的任务都是有优先级的,需要在这些计算机的任务中找出优先级最高的任务先执行,执行完毕后就需要把这个任务从队列中移除。普通的队列要完成这样的功能,需要每次遍历队列中的所有元素,比较并找出最大值,效率不是很高,这个时候,就可以使用一种特殊的队列来完成这种需求,优先队列

在这里插入图片描述

优先队列按照其作用不同,可以分为以下两种:

  • 最大优先队列: 可以获取并删除队列中最大的值

  • 最小优先队列: 可以获取并删除队列中最小的值

9.1、最大优先队列

这种结构是可以方便的删除最大的值,所以,可以基于堆实现最大优先队列

最大堆

1)API设计

在这里插入图片描述

2)代码实现

package chapter07;

/**
 * @author 土味儿
 * Date 2021/9/9
 * @version 1.0
 * 最大优先队列
 * 以堆的方式实现
 */
public class MaxPriorityQueue<T extends Comparable<T>> {
    /**
     * 存储元素的数组
     */
    private T[] items;
    /**
     * 元素个数
     */
    private int n;

    /**
     * 构造器
     *
     * @param capacity
     */
    public MaxPriorityQueue(int capacity) {
        this.items = (T[]) new Comparable[capacity + 1];
        this.n = 0;
    }

    /**
     * 元素个数
     *
     * @return
     */
    public int size() {
        return n;
    }

    /**
     * 是否为空
     *
     * @return
     */
    public boolean isEmpty() {
        return n < 1;
    }

    /**
     * 插入元素
     *
     * @param t
     */
    public void insert(T t) {
        // 在结尾插入
        items[++n] = t;
        // 让新元素上浮,找到合适的位置
        swim(n);
    }

    /**
     * 删除最大元素并返回
     *
     * @return
     */
    public T delMax() {
        // 最大元素
        T max = items[1];

        // 交换索引1处 和 最大索引处的元素,让完全二叉树中最右的元素成为临时根结点
        exch(1, n);

        // 删除交换后最大索引处的元素,即原根结点(最大元素)
        items[n] = null;

        // 元素数量减1
        n--;

        // 让新根结点下沉,找到合适位置
        sink(1);

        return max;
    }

    /**
     * 比较索引i处元素是否小于j处元素
     *
     * @param i
     * @param j
     * @return
     */
    private boolean less(int i, int j) {
        return items[i].compareTo(items[j]) < 0;
    }

    /**
     * 交换索引i处和j处元素
     *
     * @param i
     * @param j
     */
    private void exch(int i, int j) {
        T temp = items[i];
        items[i] = items[j];
        items[j] = temp;
    }

    /**
     * 让索引k处元素上浮到合适的位置
     *
     * @param k
     */
    private void swim(int k) {
        // 循环比较索引k处元素和它父结点元素,如果父结点小,就交换位置
        while (k > 1) {
            // 比较当前结点与父结点
            if (less(k / 2, k)) {
                // 父比子小,交换位置
                exch(k / 2, k);
            }
            k = k / 2;
        }
    }

    /**
     * 让索引k处元素下沉到合适的位置
     *
     * @param k
     */
    private void sink(int k) {
        // 循环比较当前结点k 和 max(左子结点2k,右子结点2k+1)的值,如果当前结点k小,就交换位置

        // 循环条件 2*k <= n :表示存在 左子结点
        while (2 * k <= n) {
            // 获取当前结点中的较大 子结点

            // 记录较大 子结点索引:默认为 左子结点
            int max = 2 * k;
            // 如果右结点存在,且左小于右,改变max
            if (2 * k + 1 <= n && less(2 * k, 2 * k + 1)) {
                max = 2 * k + 1;
            }

            if (!less(k, max)) {
                // 当前结点不小于子结点,退出循环
                break;
            }

            // 当前结点小于子结点,交换位置
            exch(k, max);

            // 交换k值,继续循环
            k = max;
        }
    }
}
    @Test
    public void testMaxPriorityQueue() {
        String[] arr = {"E", "B", "A", "C", "D"};
        MaxPriorityQueue<String> maxpq = new MaxPriorityQueue<>(20);
        for (String s : arr) {
            maxpq.insert(s);
        }
        System.out.println(maxpq.size());
        String del;
        while (!maxpq.isEmpty()) {
            del = maxpq.delMax();
            System.out.print(del + " ");
        }
    }
5
E D C B A

符合优先队列的要求:

插入时:在一端(尾部)

弹出时:在另一端(首部),并且首部都是最大的,即优先级最高的

9.2、最小优先队列

最小优先队列实现起来也比较简单,同样也可以基于堆来完成最小优先队列


最大堆

前面的堆中存放数据元素的数组要满足如下特性:

  1. 最大的元素放在数组的索引1处

  2. 每个结点的数据总是大于等于它的两个子结点的数据


最小堆

让堆中存放数据元素的数组满足如下特性:

  1. 最小的元素放在数组的索引1处

  2. 每个结点的数据总是小于等于它的两个子结点的数据

在这里插入图片描述

1)API设计

在这里插入图片描述

2)代码实现

package chapter07;

/**
 * @author 土味儿
 * Date 2021/9/9
 * @version 1.0
 * 最小优先队列
 * 以最小堆的方式实现
 */
public class MinPriorityQueue<T extends Comparable<T>> {
    /**
     * 存储元素的数组
     */
    private T[] items;
    /**
     * 元素个数
     */
    private int n;

    /**
     * 构造器
     * @param capacity
     */
    public MinPriorityQueue(int capacity) {
        this.items = (T[]) new Comparable[capacity + 1];
        this.n = 0;
    }

    /**
     * 元素个数
     *
     * @return
     */
    public int size() {
        return n;
    }

    /**
     * 是否为空
     *
     * @return
     */
    public boolean isEmpty() {
        return n < 1;
    }

    /**
     * 插入元素
     *
     * @param t
     */
    public void insert(T t) {
        // 在结尾插入
        items[++n] = t;
        // 让新元素上浮,找到合适的位置
        swim(n);
    }

    /**
     * 删除最小元素并返回
     *
     * @return
     */
    public T delMin() {
        // 最小元素
        T min = items[1];

        // 交换索引1处 和 最大索引处的元素,让完全二叉树中最右的元素成为临时根结点
        exch(1, n);

        // 删除交换后最大索引处的元素,即原根结点(最小元素)
        items[n] = null;

        // 元素数量减1
        n--;

        // 让新根结点下沉,找到合适位置
        sink(1);

        return min;
    }

    /**
     * 比较索引i处元素是否小于j处元素
     *
     * @param i
     * @param j
     * @return
     */
    private boolean less(int i, int j) {
        return items[i].compareTo(items[j]) < 0;
    }

    /**
     * 交换索引i处和j处元素
     *
     * @param i
     * @param j
     */
    private void exch(int i, int j) {
        T temp = items[i];
        items[i] = items[j];
        items[j] = temp;
    }

    /**
     * 让索引k处元素上浮到合适的位置
     *
     * @param k
     */
    private void swim(int k) {
        // 循环比较索引k处元素和它父结点元素,如果父结点大,就交换位置
        while (k > 1) {
            // 比较当前结点与父结点
            if (less(k, k/2)) {
                // 父比子大,交换位置
                exch(k, k/2);
            }
            k = k / 2;
        }
    }

    /**
     * 让索引k处元素下沉到合适的位置
     *
     * @param k
     */
    private void sink(int k) {
        // 循环比较当前结点k 和 min(左子结点2k,右子结点2k+1)的值,如果当前结点大,就交换位置

        // 循环条件 2*k <= n :表示存在 左子结点
        while (2 * k <= n) {
            // 获取当前结点中的较小 子结点

            // 记录较小 子结点索引:默认为 左子结点
            int min = 2 * k;
            // 如果右结点存在,且左大于右,改变min
            if (2 * k + 1 <= n && less(2 * k +1, 2 * k)) {
                min = 2 * k + 1;
            }

            if (!less(min, k)) {
                // 当前结点不大于子结点,退出循环
                break;
            }

            // 当前结点大于子结点,交换位置
            exch(k, min);

            // 交换k值,继续循环
            k = min;
        }
    }
}
    @Test
    public void testMaxPriorityQueue() {
        String[] arr = {"E", "B", "A", "C", "D"};
        //MaxPriorityQueue<String> queue = new MaxPriorityQueue<>(20);
        MinPriorityQueue<String> queue = new MinPriorityQueue<>(20);
        for (String s : arr) {
            queue.insert(s);
        }
        System.out.println(queue.size());
        String del;
        while (!queue.isEmpty()) {
            //del = queue.delMax();
            del = queue.delMin();
            System.out.print(del + " ");
        }
    }
5
A B C D E 

9.3、索引优先队列

在最大优先队列和最小优先队列中,可以分别快速访问到队列中最大元素和最小元素

但是有一个 缺点:就是没有办法通过索引访问已存在于优先队列中的对象,并更新它们

为了实现这个目的,在优先队列的基础上,学习一种新的数据结构,索引优先队列

以最小索引优先队列实现

1)实现思路

  • 步骤一

    • 存储数据时,给每一个数据元素关联一个整数

      例如:insert(int k,T t),可以看做 k 是 t 关联的整数,那么实现需要通过 k 这个值,快速获取到队列中 t 这个元素,此时 k 这个值需要具有唯一性

      最直观的想法就是可以用一个T[] items 数组来保存数据元素,在 insert(int k,T t) 完成插入时,可以把 k 看做是 items 数组的索引,把 t 元素放到 items 数组的索引 k 处,这样再根据 k 获取元素 t 时就很方便了,直接就可以拿到 items[k] 即可

在这里插入图片描述

  • 步骤二

    • 步骤一完成后的结果,虽然给每个元素关联了一个整数,并且可以使用这个整数快速的获取到该元素,但是 items 数组中的元素 顺序是随机的并不是堆有序的

    • 增加一个数组 int[] pq,来 保存每个元素在 items 数组中的索引,pq 数组需要 堆有序

      例如:pq[1] 对应的数据元素 items[pq[1]] 要小于等于 pq[2] 和 pq[3] 对应的数据元素 items[pq[2]] 和 items[pq[3]]

在这里插入图片描述

  • 步骤三

    • 通过步骤二的分析,可以发现,其实通过上浮和下沉做堆调整的时候,调整的是 pq 数组

      如果需要对 items 中的元素进行修改,比如让 items[0]=“H”,那么很显然,需要对 pq 中的数据做堆调整,而且是调整 pq[9] 中元素的位置。但现在就会遇到一个问题,修改的是 items 数组中 0 索引处的值,如何才能快速的知道需要挑中 pq[9] 中元素的位置呢?

    • 最直观的想法就是遍历 pq 数组,拿出每一个元素和 0 做比较,如果当前元素是0,那么调整该索引处的元素即可,但是效率很低

    • 可以 另外增加一个数组 int[] qp,用来存储 pq 的 逆序

      例如:
      在pq数组中:pq[1] = 6;那么在qp数组中,把6作为索引,1作为值,结果是:qp[6]=1;

    • 当有了 qp 数组后,如果修改 items[0]=“H”,那么就可以先通过索引 0,在 qp 数组中找到 qp 的索引:qp[0]=9, 那么直接调整 pq[9] 即可

在这里插入图片描述

  • 总结

    • 得到 items 队列中的最小元素:items[pq[1]]
    • 得到 items 队列中的最 k 个元素:items[pq[k]]

    在这里插入图片描述

    • 修改 items 数组中的最 k 个元素为 t :

      1、items[k] = t

      2、通过 qp 逆序数组,找到 pq 对应位置:pq[qp[k]],并调整 pq 堆

      3、更新 qp 逆序数组

      在这里插入图片描述

2)API设计

在这里插入图片描述

3)代码实现

package chapter07;

/**
 * @author 土味儿
 * Date 2021/9/9
 * @version 1.0
 * 索引优先队列
 * 以最小堆方式实现
 */
public class IndexMinPriorityQueue<T extends Comparable<T>> {
    /**
     * 存储元素的数组
     */
    private T[] items;
    /**
     * 保存 items 中元素的索引
     */
    private int[] pq;
    /**
     * pq数组 的逆序
     * pq中的值为qp的索引,pq的索引为qp的值
     */
    private int[] qp;
    /**
     * 元素的数量
     */
    private int n;

    /**
     * 构造器
     *
     * @param capacity
     */
    public IndexMinPriorityQueue(int capacity) {
        this.items = (T[]) new Comparable[capacity + 1];
        this.pq = new int[capacity + 1];
        this.qp = new int[capacity + 1];
        this.n = 0;

        // 初始化qp为-1
        for (int i = 0; i < qp.length; i++) {
            qp[i] = -1;
        }
    }

    /**
     * 元素数量
     *
     * @return
     */
    public int size() {
        return n;
    }

    /**
     * 队列是否为空
     *
     * @return
     */
    public boolean isEmpty() {
        return n < 1;
    }
    
     /**
     * 获取队列 pq 中索引i对应元素的值
     *
     * @param i
     * @return
     */
    public T get(int i) {
        if (i < 0 || i > pq.length) {
            return null;
        }
        return items[pq[i]];
    }

    /**
     * 判断堆中索引i处元素是否小于j处
     *
     * @param i
     * @param j
     * @return
     */
    private boolean less(int i, int j) {
        // 实际比较的是items中元素的值,而堆中存的是items元素的索引
        return items[pq[i]].compareTo(items[pq[j]]) < 0;
    }

    /**
     * 交换堆中索引i处与j处的值
     *
     * @param i
     * @param j
     */
    private void exch(int i, int j) {
        // i = 1; j = 3;
        // 交换前
        // pq[1] = 5   --->   qp[5] = 1
        // pq[3] = 7   --->   qp[7] = 3

        // 交换后
        // pq[1] = 7   --->   qp[7] = 1  =>  qp[pq[1]] = 1  =>  qp[pq[i]] = i
        // pq[3] = 5   --->   qp[5] = 3  =>  qp[pq[3]] = 3  =>  qp[pq[j]] = j

        // 交换pq
        int temp = pq[i];
        pq[i] = pq[j];
        pq[j] = temp;

        // 交换qp
        qp[pq[i]] = i;
        qp[pq[j]] = j;
    }

    /**
     * 删除队列中最小的值
     *
     * @return 最小值对应的索引
     */
    public int delMin() {
        // 获取最小元素关联的索引
        int minIndex = minIndex();

        // 交换 pq 中索引1处和最大索引处的值
        exch(1,n);

        // 删除 qp 中对应的值
        qp[pq[n]] = -1;

        // 删除 items 中对应的值
        items[pq[n]] = null;

        // 删除 pq 中最大索引处的新元素值
        pq[n] = -1;

        // 元素数量减 1
        n--;

        // 通过下沉调整 pq 堆
        sink(1);

        return minIndex;
    }

    /**
     * 向队列中插入一个元素t,对应索引i
     *
     * @param i
     * @param t
     */
    public void insert(int i, T t) {
        // 索引0处舍弃不用
        // 参数有效性检查 1 <= i < items.length
        if (i < 1 || i >= items.length) {
            return;
        }
        
        // 判断i是否已被关联,如果已被关联,则不允许插入
        if(contains(i)){
            return;
        }

        // 把数据存到 items 数组的 i 索引处
        items[i] = t;

        // 元素数量加 1
        n++;

        // 把 i 存到 pq 中
        pq[n] = i;

        // 用 qp 记录 pq 中的 i
        qp[i]=n;

        // 通过上浮调整 pq 堆
        swim(n);
    }

    /**
     * 上浮堆中索引k位置的元素,使处于合适的位置
     *
     * @param k
     */
    private void swim(int k) {
        // 循环比较索引k处元素和它父结点元素,如果父结点大,就交换位置
        while (k > 1) {
            // 比较当前结点与父结点
            if (less(k, k/2)) {
                // 父比子大,交换位置
                exch(k, k/2);
            }
            k = k / 2;
        }
    }

    /**
     * 下沉堆中索引k位置的元素,使处于合适的位置
     *
     * @param k
     */
    private void sink(int k) {
        // 循环比较当前结点k 和 min(左子结点2k,右子结点2k+1)的值,如果当前结点大,就交换位置

        // 循环条件 2*k <= n :表示存在 左子结点
        while (2 * k <= n) {
            // 获取当前结点中的较小 子结点

            // 记录较小 子结点索引:默认为 左子结点
            int min = 2 * k;
            // 如果右结点存在,且左大于右,改变min
            if (2 * k + 1 <= n && less(2 * k +1, 2 * k)) {
                min = 2 * k + 1;
            }

            if (!less(min, k)) {
                // 当前结点不大于子结点,退出循环
                break;
            }

            // 当前结点大于子结点,交换位置
            exch(k, min);

            // 交换k值,继续循环
            k = min;
        }
    }

    /**
     * 判断k对应的元素是否存在
     *
     * @param k
     * @return
     */
    public boolean contains(int k) {
        return qp[k] != -1;
    }

    /**
     * 把索引i对应的元素修改为t
     *
     * @param i
     * @param t
     */
    public void changeItem(int i, T t) {
        // 修改 items 数组中索引 i 处的元素为 t
        items[i] = t;

        // 得到 items 中索引 i 在 pq 中的位置 k
        int k = qp[i];

        // 堆调整:修改后不确定是该下沉还是上浮,所以下沉和上浮都要做
        // 下沉
        sink(k);

        // 上浮:k处的元素和下沉时并不一定相同
        swim(k);
    }

    /**
     * 得到最小元素对应的索引
     *
     * @return
     */
    public int minIndex() {
        // pq是有序最小堆:1处最小
        return pq[1];
    }

    /**
     * 删除索引i对应的元素
     *
     * @param i
     */
    public void delete(int i) {
        // 得到 items 的索引 i 在 pq 中的位置
        int k = qp[i];
        // 交换 pq 中索引 k 处和最大处 n 的值
        exch(k,n);

        // 删除 qp 中相应的值
        qp[i] = -1;

        // 删除 items 中相应的值
        items[i] = null;

        // 删除 pq 中交换后的最大索引值(即原来k处的值)
        pq[n] = -1;

        // 元素数量减 1
        n--;

        // 调整堆
        // -----------这个分析是错误的----------------
        // pq 中 k 处的新元素下沉
        // 只需下沉即可:因为 删除前 堆就是有序的,子结点大于父结点
        // 下沉后交换上来的结点,肯定也大于 k 的父结点,不需要上浮
        //
        // ----------下沉和上浮都要做--------------
        // 比如堆中元素是 【2,5,3,6,8,4】,如果删除6,那么6和4交换后,4要上浮的
        // 下沉
        sink(k);
        
        // 上浮
        swim(k);
        
    }
}
 @Test
    public void testIndexMinPriorityQueue() {
        IndexMinPriorityQueue<String> queue = new IndexMinPriorityQueue<>(10);
        queue.insert(0, "B");
        queue.insert(1, "D"); // delete
        queue.insert(2, "E");
        queue.insert(3, "C"); // F
        queue.insert(4, "A");

		System.out.println("总数量:" + queue.size()); // 5
        for (int i = 1; i < queue.size()+1; i++) {
            System.out.print(queue.get(i) + " ");
        }
        System.out.println("\n-------------");

        queue.changeItem(3, "F");
        System.out.println("修改后数量:" + queue.size()); // 5
        for (int i = 1; i < queue.size()+1; i++) {
            System.out.print(queue.get(i) + " ");
        }
        System.out.println("\n-------------");

        queue.delete(1);
        System.out.println("删除后数量:" + queue.size()); // 4
        for (int i = 1; i < queue.size()+1; i++) {
            System.out.print(queue.get(i) + " ");
        }
        System.out.println("\n-------------");

        while (!queue.isEmpty()) {
            System.out.print(queue.delMin() + " "); // 4 0 2 3  即:A B E F
        }

        System.out.println("\n数量:" + queue.size()); // 0
    }
总数量:5
A B E D C 
-------------
修改后数量:5
A B E D F 
-------------
删除后数量:4
A B E F 
-------------
4 0 2 3 
数量:0
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

土味儿~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值