《算法》2.4优先队列

1优先队列

很多情况下,我们会收集一些元素,处理当前键值最大的元素,然后在收集更多的元素,再处理当前键值最大的元素,如此这般。

1.1定义

在上述情况下,一个合适的数据结构应该支持两种操作:删除最大元素插入元素

这种数据类型就是优先队列

1.2实现方式

实现方式有两种类型:

  • 初级方式:

    使用无序或有序的数组和链表,其一或两种操作能在线性时间内完成。

  • 高级方式:

    使用堆,两种操作都能在对数时间内完成。

为了保证灵活性,我们在实现中使用了泛型,将实现了Comparable接口的数据的类型作为参数Key

1.3一个应用问题

为了展现优先队列的抽象模型的价值,考虑一下问题:

  • 输入N个字符串,每个字符串都对应着一个整数,你的任务就是从中找出最大(或最小)的M个整数(及其对应的字符串)。

1.4初级实现

  1. 数组实现(无序)

    基于2.1节中的下压栈的代码。

  2. 数组实现(有序)

    参照插入排序。

  3. 链表表示法

    基于链表的下压栈的代码。

优先队列的各种实现在最坏情况下运行时间的增长数量级:

数据结构插入元素删除最大元素
有序数组N1
无序数组1N
logNlogN
理想情况11

2堆

数据结构二叉堆能够很好地实现优先队列的基本操作。

2.1定义

堆有序:

  • 当一棵二叉树的每个结点都大于或等于它的两个子结点时,它被称为堆有序。

如此一来,根结点就是堆有序的二叉树中的最大结点。

二叉堆表示法:

为了方便,我们使用完全二叉树,因此只需要使用数组,而不需要使用指针来表示。

具体方法:

  • 将二叉树的结点按照层次顺序放入数组中,根结点在位置1,它的子结点在位置2,3。
  • 而子结点的子结点则分别位于位置4,5,6,7中,以此类推。

因此:我们可以通过利用数组的索引在树种上下移动。

  • a[k]向上一层就令k等于k/2
  • 向下一层就令k等于2k2k+1

一棵大小为N的完全二叉树的高度为logN

2.2堆的算法

堆的有序化:

  • 堆的操作会首先进行一些简单的改动,打破堆的状态,然后再遍历堆,并按照要求将堆的状态恢复。

由下至上的堆有序化(上浮)

/**
* 上浮,当某个结点太大时
* @param k
*/
private void swim(int k) {
    while (k > 1 && less(k / 2, k)) {//该结点比父结点大
        exch(k / 2, k);//与父结点交换位置
        k = k / 2;
    }
}

由上至下的堆有序化(下沉)

/**
* 下沉,当某个结点太小时
* @param k
*/
private void sink(int k) {
    while (2 * k <= N) {//防止溢出
        int j = 2 * k;//指向左子结点
        if (j < N && less(j, j + 1)) {//左子结点小于右子结点
            j++;//指向右子结点
        }
        if (!less(k, j)) {//该节点大于左右子结点
            break;
        }
        exch(k, j);//该结点与子结点中较大者交换位置
        k = j;
    }
}

2.3基于堆的优先队列

完整实现:

/**
 * 基于堆的优先队列
 * @Author: AZhu
 * @Date: 2021/2/23 16:33
 */
public class MaxPQ<Key extends Comparable<Key>> {

    private Key[] pq;//基于堆的完全二叉树
    private int N = 0;//存储于pq[1..N]中,pq[0]没有使用

    public MaxPQ(int maxN) {
        pq = (Key[]) new Comparable[maxN + 1];
    }

    /**
     * 判断优先队列是否为空
     * @return
     */
    public boolean isEmpty() {
        return N == 0;
    }

    /**
     * 返回优先队列中的元素个数
     * @return
     */
    public int size() {
        return N;
    }

    /**
     * 插入一个元素
     * @param v
     */
    public void insert(Key v) {
        pq[++N] = v;//插入末尾
        swim(N);//上浮
    }

    /**
     * 删除最大的元素并返回
     * @return
     */
    public Key delMax() {
        Key max = pq[1];//从根结点得到最大的元素
        exch(1, N--);//和最后的结点交换位置
        pq[N + 1] = null;//防止对象游离
        sink(1);//下沉,恢复堆的有序性
        return max;
    }

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

    /**
     * 交换队列中索引为i和j的元素
     * @param i
     * @param j
     */
    private void exch(int i, int j) {
        Key t = pq[i];
        pq[i] = pq[j];
        pq[j] = t;
    }

    /**
     * 上浮,当某个结点太大时
     * @param k
     */
    private void swim(int k) {
        while (k > 1 && less(k / 2, k)) {//该结点比父结点大
            exch(k / 2, k);//与父结点交换位置
            k = k / 2;
        }
    }

    /**
     * 下沉,当某个结点太小时
     * @param k
     */
    private void sink(int k) {
        while (2 * k <= N) {//防止溢出
            int j = 2 * k;//指向左子结点
            if (j < N && less(j, j + 1)) {//左子结点小于右子结点
                j++;//指向右子结点
            }
            if (!less(k, j)) {//该节点大于左右子结点
                break;
            }
            exch(k, j);//该结点与子结点中较大者交换位置
            k = j;
        }
    }

}

分析:

对于一个含有N个元素的基于堆的优先队列:

  • 插入元素的操作只需要不超过lgN+1次比较,
  • 删除最大元素的操作需要不超过2lgN次比较。

基于堆的实现能够保证在对数时间内完成两种基本操作。

优化:

多叉堆:

  • 基于用数组表示的完全三叉树构造对并不困难,
  • 位置k的结点大于等于位于3k-13k3k+1的结点,小于等于位于(k+1)/3的结点。

调整数组的大小:

  • 可以添加一个无参构造器,
  • insert()中添加数组长度加倍的代码,
  • delMax()中添加将数组长度减半的代码。

元素的不可变性:

  • 优先队列存储了用例创建的对象,用例改变它们就可能会打破堆的有序性。
  • 可以做一个强制条件,但是代码会变复杂,降低性能。

索引优先队列:

  • 允许用例引用已经进入优先队列中的元素,
  • 给每个元素一个索引。

3堆排序

3.1定义

我们可以将任意优先队列变成一种排序方法。

  • 将所有元素插入一个查找最小元素的优先队列,然后再重复调用删除最小元素的操作将它们按顺序删去。

用无序数组实现的优先队列这么做就相当于进行了一次选择排序,

用堆实现的优先队列这么做就是堆排序

3.2堆排序算法

堆排序分为两个阶段:

  • 堆的构造阶段:

    将原始数组重新组织安排进一个堆中。

  • 下沉排序阶段:

    从堆中按递减顺序取出所有元素并得到排序结果。

我们在排序时直接将需要排序的数组本身作为堆,因此无需任何额外的空间。

堆的构造:

Q:如何高效地由N个给定的元素构造一个堆?

A:从右向左,用sink()方法构造子堆。

一个命题:

  • 用下沉操作由N个给定的元素构造一个堆,只需要少于2N次比较和少于N次交换。

实现:

//堆排序
public static void sort(Comparable[] a){
    int N = a.length;
    for(int k=N/2; k>=1; k--){//构造堆
        sink(a, k, N);//k在a[1..N]中下沉
    }
    while(N>1){//下沉排序
        exch(a, 1, N--);//在a[]中交换1和N
        sink(a, 1, N);//1在a[1..N]中下沉
    }
}

分析:

N个元素排序,堆排序只需要少于2NlgN+2N次比较(以及一半次数的交换)。

堆排序和选择排序的某些地方很像,但是堆排序的比较次数要少得多。

堆排序是我们所知的唯一能够同时最优地利用空间和时间的方法:

  • 在最坏的情况下它也能够保证使用 ~ 2NlgN次比较和恒定的额外空间。

优化:

大多数在下沉排序期间重新插入堆的元素会被直接加入到堆底。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
短作业优先调度算法(Short Job First Scheduling Algorithm)是一种经典的进程调度算法,在操作系统中得到广泛应用。该算法的核心思想是优先调度执行时间短的进程,以达到最小化平均等待时间的目的。 本次实验的目的是通过模拟实现短作业优先调度算法,并对其进行分析和评价。具体实验步骤如下: 1. 实验环境 本次实验使用Python编程语言,没有使用任何外部库。 2. 实验设计 为了模拟短作业优先调度算法,我们需要定义一个进程类,包含进程ID、到达时间、执行时间三个属性。并设计一个调度函数,实现短作业优先调度算法的逻辑。 具体代码如下: ```python class Process: def __init__(self, proc_id, arrival_time, burst_time): self.proc_id = proc_id self.arrival_time = arrival_time self.burst_time = burst_time def sjf_scheduling(processes): n = len(processes) # 按到达时间排序 processes.sort(key=lambda x: x.arrival_time) current_time, completed = 0, 0 waiting_time, turnaround_time = 0, 0 queue = [] # 循环处理进程 while completed < n: # 添加到达的进程到队列中 for i in range(n): if processes[i].arrival_time <= current_time and processes[i] not in queue: queue.append(processes[i]) # 如果队列为空,则时间跳转到下一个进程到达时间 if not queue: current_time = processes[completed].arrival_time else: # 按照执行时间排序,选择执行时间最短的进程 queue.sort(key=lambda x: x.burst_time) process = queue.pop(0) # 计算等待时间和周转时间 waiting_time += current_time - process.arrival_time turnaround_time += current_time - process.arrival_time + process.burst_time # 更新当前时间和已完成进程数 current_time += process.burst_time completed += 1 # 计算平均等待时间和平均周转时间 avg_waiting_time = waiting_time / n avg_turnaround_time = turnaround_time / n return avg_waiting_time, avg_turnaround_time ``` 3. 实验结果 我们使用如下数据进行测试: | 进程ID | 到达时间 | 执行时间 | | ------ | -------- | -------- | | P1 | 0 | 4 | | P2 | 2 | 3 | | P3 | 4 | 2 | | P4 | 5 | 4 | | P5 | 6 | 1 | 运行上述代码,得到的平均等待时间为2.4,平均周转时间为6.8。 4. 实验分析 从实验结果可以看出,短作业优先调度算法可以有效地减少平均等待时间和平均周转时间。这是因为该算法优先调度执行时间短的进程,避免了长时间等待导致的浪费。但是,该算法可能会导致长作业等待时间过长,进而导致长作业的延迟和性能下降。因此,在实际应用中,需要根据具体情况选择合适的调度算法,以达到最优的性能和效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值