4 堆

本文深入讲解堆数据结构的概念、特点及应用,包括堆的插入、删除等关键算法,并通过示例程序展示了堆如何用于实现优先队列,以及如何在操作系统中进行多进程抢占调度。

堆的概念

堆上的算法

堆的打印

CPU优先级抢占模型

 

 

堆是一种数组对象,与之前几章所讨论的内容稍有不同的是,堆中的元素项具有特殊的顺序结构。在本书后面将介绍树数据结构,事实上属于一种特殊的树,即完全二叉树。完全二叉树中除了最后一层之外树的每一层都填满。之所以将堆数据结构提前介绍,一方面是因为堆也是一种基本的数据结构。另一方面,堆的一个重要功能是能实现优先队列。上一章中我们刚讨论过队列,所以紧接着在这一章介绍堆以及利用堆实现优先队列。

 

堆的概念

假设r[0], r[1], …, r[n-1]是一序列元素,可以看作是数组结构。如果对于任意r[i],同时满足条件r[i] ≥ r[2*i+1]和r[i] ≥ r[2*i+2],则称该序列是一个堆,也称为最大化堆,如果把不等式中的大于号≥换成小于号≤,称为最小化堆。在下面的讨论中,所有的堆指的是最大化堆。

可以把序列r[0],r[1],…, r[n-1]看成是按数组方式存储的一棵满二叉树,元素r[0]为树根,其它元素为树中的中间节点或叶子节点(具体定义参见树数据结构)。考虑一棵按数组方式存储的满二叉树,元素r[2*i+1]和元素r[2*i+2]是元素r[i]的左孩子和右孩子,因此在一个堆中,对于任一元素r[i],其大小一定小于等于其左右两个孩子的键值r[2*i+1]和r[2*i+2]。

 

 

 

最大堆的示例

上图为二叉树表示的堆结构,将堆中元素用数组形式存放中,其存储顺序:

 

 

 

最大堆用数组存储结构

根据堆的定义,可以得出堆的一个重要性质:如果一个按数组存放的满二叉树是堆,那么堆顶元素一定是堆中所有元素中最大的元素。

该性质可以根据堆二叉树的层数利用数学归纳法证明:

当堆二叉树的层数为1时,堆二叉树中只有一个元素r[0],因此堆顶元素是堆中所有元素中最大的元素。

假设对于所有的层数小于n的堆二叉树,堆顶元素的值是堆中所有元素中最大的元素。当堆二叉树的层数为n时,堆顶元素r[0]的左右子树一定是层数小于n的堆二叉树,左子树的堆顶元素是r[1],右子树的堆顶元素是r[2]。根据假设可知,r[1]是左子树中最大的元素,r[2]是右子树中最大的元素;再根据堆的定义:对于任意元素r[i]满足r[i]≥r[2* i+1],r[i] ≥r[2*i+2],可知r[0]≥r[1],r[0]≥r[2],因此,堆顶元素r[0]一定是堆中所有元素中键值最大的元素。

堆的性质使得堆很适合从一组元素中选择最值元素(最大值或最小值)的场合,只要把所有可供选择的元素组织成一个堆,堆顶元素即为需要选择的最值元素。所以在操作系统中,常常采用堆数据结构选择优先数最小线程(通常优先数越小,对应的优先级越高),在本章的示例程序中我们将讨论并仿真这一过程。

 

堆上的算法

元素项的值发生变化时对堆的调整

假设在堆中存在n个元素r[0], r[1], … , r[n-1],这些元素之间全部满足条件r[i]≥r[2*i+1]和r[i]≥r[2*i+2],由于某种原因,其中某个元素r[k]的值发生改变,这可能会破坏堆的结构,需要对元素序列进行相应的调整,把元素序列恢复为一个堆。

元素r[k]的值的改变有两种可能:r[k]减小或r[k]增大。在堆中,元素r[k]的值小于等于其双亲的值r[INT((k-1)/2)],大于等于左右孩子的值r[2*i+1]和r[2*i+2],如果键值r[k]增大,它仍然大于等于左右孩子的值,但可能会大于其双亲的值,这样就破坏了堆的结构。通过在序列中把r[k]和其双亲结点相互交换,对应的操作就是在二叉树中把r[k]向上移动,直至把序列调整为一个堆,我们常把这一过程称为自低向上的堆化过程。

 

 

 

 

图 自低向上堆化过程

如图,(b)中第3层中第三个元素从3变为7,并且大于该节点的父节点的值4。将其上移一层,具体的做法就是将其与上一层元素互换位置,如图(c)。但此时第2层中的新改变的的节点的元素值7比它的父节点的值6还大,则还需要进一步上移,如图(d)。

与自低向上堆化过程相对应的是第顶向下对堆化过程,。如果键值r[k]减小,它仍然小于等于其父节点的值,但可能会小于其双亲的值,这样也会破坏堆的结构。通过在序列中把r[k]和其左右节点点相互交换,对应的操作就是在二叉树中把r[k]向下移动,直至把序列调整为一个堆,这一过程就称为自顶向下堆化过程。

 

 

 

图 堆顶元素值减小后自顶向下堆化过程

   如图,图(b)中堆顶元素项变为4,此时小于它的两个字节点的值,需要将其下移一层。下移的具体过程也是与子节点进行互换,但是该节点有有两个子节点,应该选择哪一个节点?如果选择较小的一个子节点,即值为6的节点,将其与堆顶节点互换后新的堆顶值将比它值为7的右子节点的值小,这不满足堆的定义。事实上,选择用来进行交换的子节点应为两个子节点中值较大的一个,即这里的值为7的右子节点,交换后如图(c)。交换后第二层上的新节点的值依然小于它的左子节点的值,所以需要继续下移,此时只有一个子节点,所以直接交换即可,如图(d),此时即能满足堆的定义。

 

堆中插入元素

   跟前几章讨论的数据结构类似,堆上的算法还包括向堆中插入新的元素项和删除任意位置上的元素项。

由于堆具有特定的结构,在插入元素项时,需要选择合适的位置插入。在具体的实现时,首先在原堆的最后添加这个元素项Insert_E(此时的顺序不满足堆的性质)并将堆的大小增加一,然后将该新元素项Insert_E逐次上移,直至满足堆的性质。其过程类似上面讨论的自底向上堆化的过程。

 

 

 

 

 

    如上图,在堆上插入新的值为8的新元素项,首先在原堆的最后添加新元素项,如图(b),此时不满足堆的性质。将新元素项上移两次后到达堆的第1层,此时满足堆的性质,则完成元素插入过程。

    插入元素的操作通常应用于建堆。在建堆时,首先添加第一个元素项,然后利用上述插入过程逐次见新的元素项插入。

    类似的,删除元素时,首先将堆的最后一个元素项End_E和待删除的元素项互换位置(此时的顺序不满足堆的性质)并将堆的大小减小一,然后逐次调整End_E的位置,直至满足堆的性质。这个过程类似于上面讨论的自顶向下堆化过程。

 

 

 

图 堆中删除元素项操作过程

    如上图,将堆中第一层值为7的元素项删除,首先将该元素项与堆的最后一个元素项,即值为3的元素项交换位置(此时的顺序不满足堆的性质)并将堆的大小减小一,如图(b)。交换后的顺序不满足堆的性质,需要将节点继续下移,即图(c)~(d)。在这个例子中待删除的节点跟最后一个元素项交换后进行的是自顶向下堆化过程。

 

 

 

 

 

图 堆中删除元素项操作过程

   再看一个例子,上图中,将值为3的元素项删除,首先将该元素项与堆的最后一个元素项,即值为6的元素项交换位置(此时的顺序不满足堆的性质)并将堆的大小减小一,如图(b)。交换后的顺序不满足堆的性质,需要将节点上移,即图(c)。

从上面两个例子可以发现,在任意的位置删除元素项时,在待删除元素项与最后一个元素项交换位置后,为保证堆的性质,需要进行的操作可能是自顶向下堆化或者是自低向上堆化。原堆的最后一个元素项在交换位置后,其值如果大于新位置的父节点的值,则向上堆化,如图;反之,如果其值小于其父节点的值,且小于其子节点的值,那么需要将节点向下堆化,如图。一个特殊的情况,在删除堆顶处元素项时,交换位置后如果需要,则进行的操作一定是自顶向下的堆化过程。

 

打印堆

在前几章中,打印线性表、栈和队列中元素项时,元素项一般是顺序打印出来的。堆具有二维结构,而且具有特殊的顺序,在打印时应将堆的结构也显示出来。在后面章节中讨论树数据结构时我们会详细介绍树的遍历算法,这里简单介绍如何打印堆中元素并显示出堆的结构性。

在利用System.out.println函数打印字符串时,必须遵循从左到右、从上到下的顺序。可以采用层序遍历算法遍历打印每个元素项,但是难以控制各层元素之间的间隔。这里介绍基于中序遍历的算法打印堆逆时针旋转90度后的结果。这里中序遍历与我们通常采用的方法有一点不同,遍历的顺序是右子节点→父节点→左子节点。

如图,中序遍历堆中元素得到的元素项的输出顺序为4→7→5→9→4→6→3。如果将堆旋转90度进行观察,各个元素项可以看成分别处于不同层上,可以逐行将各元素项打印出来。当然,在打印时只要根据元素项所处的层数控制打印位置。例如第一个元素项4,处于第二层,则打印在第一行第二个制表位(一个制表位即一个TAB间隔)。第九个元素项7,处于第零层,则打印在第三行第零个制表位。图中为打印结果显示。

 

 

 

图 中序遍历最大堆

 

 

 

 

 

最大堆ADT的设计

   根据上面对堆数据结构的讨论,堆的操作包括堆中元素项的插入和删除,自顶向下堆化和自低向上堆化等,堆的抽象数据类型设计如下:

 

复制代码
package Heap;

import Element.ElemItem;

/** 
 * 堆数据结构 Heap.java
 *
 
*/
public interface Heap {
    public boolean insert(ElemItem elem);//堆中合适的位置插入新的元素项
    public ElemItem remove(int position);//删除position位置的元素项
    public ElemItem removeMax();            //删除堆中最大或最小元素项
    public boolean exchange(int i, int j);//交换位置i和位置j处的两个元素
    public void shiftdown(int k);            //自顶向下堆化
    public void shiftup(int k);            //自底向上堆化
    public ElemItem topVal();                //返回堆顶元素项
    public int heapSize();                    //返回当前堆中元素项的个数
    public int leftchild(int position);    //获取position的左子节点位置
    public int rightchild(int position);    //获取position的右子节点位置
    public int parent(int position);        //获取position的父节点位置
    public boolean isleaf(int position);    //判断当前节点是否是叶节点
    public void printHeap();                //打印当前堆中所有元素
}
复制代码

 

其中removeMax删除堆顶元素项,并在删除后返回该元素项。Exchange交换两个位置上的元素项,若交换成功则返回ture,否则返回false。Shiftdown和shiftup分别是自顶向下和自低向上堆化操作。topVal返回堆顶最大元素项。isleaf判断节点是否是叶节点,若是则返回ture,否则返回false。

 

最大堆ADT的实现

根据上面的讨论对最大堆类的实现如下:

复制代码
package Heap;

import Element.ElemItem;

/**
 * 最大堆类,MaxHeap.java
 
*/
public class MaxHeap implements Heap{
    protected  ElemItem maxheapdata[];    //数组形式保存堆中元素项 
    protected  int maxSize;                //堆的最大大小,对应数组的最大长度
    protected  int currSize;            //堆当前有效大小
    public MaxHeap(int _MaxSize){        //构造函数
        maxSize = _MaxSize;                //最大元素个数
        currSize = 0;                    //当前有效元素个数
        maxheapdata = new ElemItem[maxSize];//新建数组,个数为最大元素个数
    }
    // 比较堆元素项大小的私有函数。
    protected int compare(int i, int j){
        return (maxheapdata[i]).compareTo(maxheapdata[j]);
    }
    
    public boolean insert(ElemItem elem) {
        if(currSize >= maxSize){
            System.out.println("堆已满!");
            return false;
        }
        else{
            int n = currSize++;        //n为堆的最后一个的位置
            maxheapdata[n] = elem;    //将元素插入
            shiftup(n);                //自底向上堆化
        }
        return true;
    }

    public ElemItem remove(int position) {
        if(position < 0 || position >= currSize){
            System.out.println("当前位置无效!");
            return null;
        }
        else{
            //将最后一个元素与position上的元素交换
            exchange(position, currSize - 1);
            //当前个数减1,currSize位置上元素即堆最后一个的元素项
            currSize--;    
            // 如果比父节点的值更大,则向上堆化
            if(1 == compare(position, parent(position))) 
                shiftup(position);
            else shiftdown(position);//向下堆化
        }
        return maxheapdata[currSize];//返回删除的元素
    }

    public ElemItem removeMax() {
        if(currSize <= 0){
            System.out.println("当前堆为空!");
            return null;
        }
        else{
            //将最后一个元素与0位置上的元素交换
            exchange(0, currSize - 1);
            //当前个数减1,则currSize位置上元素即堆最后一个的元素项
            currSize--;        
            if(currSize > 0) shiftdown(0);//从堆顶开始自顶向下堆化
        }
        return maxheapdata[currSize];
    }

    public boolean exchange(int i, int j) {
        if( i < 0 || i >= currSize || j < 0 || j >= currSize){
            System.out.println("待交换位置不合法");
            return false;
        }
        else{
            // 交换i和j位置上的两个元素项
            ElemItem tmp = maxheapdata[i];
            maxheapdata[i] = maxheapdata[j];
            maxheapdata[j] = tmp;
        }
        return false;
    }

    public void shiftdown(int k) {
        if(k < 0 || k >= currSize){
            System.out.println("当前位置" + k + "不合法");
            return;
        }
        while(!isleaf(k)){
            int l = leftchild(k);
            if(l <= currSize - 2 && compare(l, l + 1) == -1)
                l++;    // l是两个子节点中较大的元素项的标号
            if(compare(k, l) != -1) return;        //结束
            else{
                exchange(l, k);    //交换
                k = l;            //下移
            }
        }
    }

    public void shiftup(int k) {
        if(k < 0 || k >= currSize){
            System.out.println("当前位置" + k + "不合法");
            return;
        }
        int i;
        //k不是堆顶而且k处小于其父节点的值,则不断上移
        while(k >= 0 && compare(parent(k), k) == -1){
            exchange(k, parent(k));
            k = parent(k);
        }
    }

    
    public int heapSize() {
        return currSize;
    }

    public int leftchild(int position) {
        // 位置不合法:小于0或者没有子节点
        if(position < 0 || position >= currSize / 2)
            return -1;
        return 2 * position + 1;
    }

    public int rightchild(int position) {
        //位置不合法:小于0或者没有子节点
        if(position < 0 || position >= (currSize - 1) / 2)
            return -1;
        return 2 * position + 2;
    }

    public int parent(int position) {
        //位置不合法
        if(position < 0)
            return -1;
        return (int)((position - 1) / 2);
    }
    //判断是否是子节点
    public boolean isleaf(int position) {
        return position >= 0 && position >= currSize / 2;
    }

    public ElemItem topVal() {
        if(currSize > 0)return maxheapdata[0];
        return null;
    }
    
    //在特定高度打印一个元素项
    protected void printnode(int ps, int h){
        for(int i = 0; i < h; i++)
            System.out.print("\t");
        System.out.println(maxheapdata[ps].getElem());
    }
    //中序遍历堆中元素项
    protected void iterative_show(int pos, int h){
        if(pos < 0 || pos >= currSize) return;
        //先访问右子节点
        iterative_show(rightchild(pos), h + 1);
        //访问父节点
        printnode(pos, h);
        //访问左子节点
        iterative_show(leftchild(pos), h + 1);
    }
    
    public void printHeap() {
        System.out.println("堆中元素旋转90度分层打印:");
        //从第一个元素开始打印,它处于第0层
        iterative_show(0, 0);
    }
}
复制代码

 

    最大堆中访问当前元素项的左子节点、右子节点和父节点的操作都是复杂度为O(1)的操作。判断节点是否是叶节点的依据是,position是否大于或等于当前堆中元素项总个数的一半,这一结论可以根据后面章节中二叉树的性质很容易得到:如果堆中有n个内节点,则一定有n或n+1个外节点,即叶节点。

    通过对最大堆的实现,我们可以分析出两种堆化算法和插入删除算法的复杂度。后两种算法实质上也涉及了两种堆化算法。堆化是不断将某元素项与其子节点或父节点交换位置的过程,其交换次序最多为堆的层数,则这四个函数的复杂度都是Θ(logn)。建堆实际上是n次插入过程,则其复杂度为Θ(nlogn)

    下面通过示例程序来进一步说明对数据结构:

 

复制代码
package Heap;

import Element.ElemItem;

/**
* 最大堆的测试实例代码,ExampleMaxHeap.java
 
*/
public class ExampleMaxHeap {
    public static void main(String args[]){
        MaxHeap mheap = new MaxHeap(20);
        //建堆
        for(int i = 0; i < 12; i++){
            mheap.insert(new ElemItem<Integer>(i));
        }
        mheap.printHeap();
        //插入元素项值为20
        mheap.insert(new ElemItem<Integer>(20));
        System.out.println("插入元素项20后:");
        mheap.printHeap();
        ElemItem e = mheap.removeMax();
        System.out.println("删除最大项后:");
        System.out.println("删除的堆顶为:" + e.getElem());
        mheap.printHeap();
        mheap.insert(new ElemItem<Integer>(8));
        System.out.println("插入元素项8后:");
        mheap.printHeap();
        System.out.println("删除位置4上的元素项后:");
        mheap.remove(4);
        mheap.printHeap();
    }
}
复制代码

 

   本实例程序中首先建立最大容量为20的最大堆,并将整型常量0~11插入到堆中完成建堆过程。在此基础上先后插入元素项20,删除堆中最大元素项,此时堆中元素项与建堆时相同。最后插入元素项8,删除第4个位置上的元素项。本示例程序的运行结果如下:


复制代码
堆中元素旋转90度分层打印:
        4
    10
        5
            1
11
            7
        8
            2
    9
            3
        6
            0
插入元素项20后:
堆中元素旋转90度分层打印:
        4
    11
            5
        10
            1
20
            7
        8
            2
    9
            3
        6
            0
删除最大项后:
删除的堆顶为:20
堆中元素旋转90度分层打印:
        4
    10
        5
            1
11
            7
        8
            2
    9
            3
        6
            0
插入元素项8后:
堆中元素旋转90度分层打印:
        4
    10
            5
        8
            1
11
            7
        8
            2
    9
            3
        6
            0
删除位置4上的元素项后:
堆中元素旋转90度分层打印:
        4
    10
        8
            1
11
            5
        7
            2
    9
            3
        6
            0
复制代码

 

      优先队列是堆的一个具体应用,实际上说成“堆是优先队列的一种自然实现方法”更能体现出问题是实质。为每个元素项设定特定的优先级,并将它们建立成堆结构。每次操作都删除并返回堆中最大的元素项,对其进行相关处理。在删除的同时还会有新的元素项不断插入堆中。

在上一章中我们讨论了生产者-消费者模型,并用顺序队列对这个模型进行了仿真说明。这里我们将讨论操作系统中的另一个重要的问题,即多进程间的抢占

在操作系统中多个进程常常会同时向CPU发出处理请求,但是CPU一次只能处理一个进程。操作系统中不同进程的优先级是不同的,在进程池中等待CPU处理的进程并不一定是按照先来先处理的准则接受CPU的处理的。现代操作系统一般都支持进程之间的抢占,即后来的进程如果优先级比较高,则先处理。根据这一准则可以将进程池用优先队列结构(最大堆)来表示,优先队列的元素项即为各个优先级不同进程。CPU每隔一段时间处理进程池中的一个进程,这对应着删除优先队列顶部元素的操作。在CPU处理的同时会有新的进程添加到进程池中,新添加的进程需要在进程池中进行位置的调整,对应的优先队列中元素项的插入和自低向上堆化的过程。

根据讨论我们将利用最大堆仿真上述的多进程抢占过程。首先需要设计进程类,这里主要讨论类的优先级,不考虑进程的具体功能。类的成员变量包括进程号、进程优先级和进程所需的CPU处理时间。其中最后一项进程所需的CPU处理时间是专为本仿真过程设定的成员变量,实际的进程肯定是没有这个属性的,因为处理时间只有在CPU完成该进程的处理任务之后才能得知。由于进程之间存在优先级的比较,所以进程类在实现Comparable接口的函数compareTo函数时比较两个进程的优先级。

进程类的设计如下:

 

复制代码
    package Heap;
    
    /**
    * CPU 进程类,
     
*/
    public class Process implements Comparable{
        int id;            // 进程的ID
        int priory;        //进程的优先级
        int processtime;//进程的处理时间
        int cometime;    //进程的到达时间
        public Process(){}
        
        // 比较进程的优先级
        public int compareTo(Object o) {
            Process p =(Process)(o);
            // 较高,返回1
            if(this.priory > p.priory) return 1;
            // 相同,返回0
            else if(this.priory == p.priory) return 0;
            // 较低,返回-1
            else return -1;
        }
        
    }
复制代码

 

 

      首先设定进程的有限级分为三级:0,1,2,数值越大其优先级越高。实际操作系统中一般采用数值越小优先级越高的限定。这里之所以这样设定,主要是因为考虑到优先队列是基于最大堆实现的。

下面将对CPU的处理速度以及新进程进入进程池的到达时间建立模型。CPU处理速度也可以用处理时间来描述,这里假设CPU对进程的处理时间是随机的,假设为8,9,10个时间单位,且均匀分布。

进程到达进程池的时间间隔可以建模成泊松分布,即

 

 

 

其中λ表示平均到达时间间隔,P(X=k)表示实际到达时间间隔为k的概率。

这里假设进程到达时间间隔平均为5个时间单位,小于进程处理时间。所以进程池中处于等待状态的进程数量将逐渐增多。我们关心的问题是不同优先级的进程的等待时间之间的差异。

进程池中进程在各个时间点既有删除也有插入,这是一个动态变化的过程。对这类仿真问题通常采用离散时间仿真方法。具体流程如图。

在流程图中多处涉及到CPU的状态,CPU的状态可以用一个变量processcnt表示,其含义是CPU正在处理的进程距离进程结束还需要处理的时间单位个数,即“CPU正在处理的进程的处理时间”。该变量为正值时表示CPU正在处理中,为零时表示当前进程处理结束,为负值表示当前CPU处于空闲状态。当进程池中有进程被删除(被CPU调用)时,Processcnt的值被更新为该进程的处理时间。

仿真最终得到的结果是三种优先级各自的等待时间的变化情况。每当CPU处理完一个进程则打印当前三种优先级的等待时间。

 

 

 

 

 

图 利用优先队列仿真多进程抢占CPU过程

本示例程序代码如下:

 

复制代码
    package Heap;
    
    import Element.ElemItem;
    
    /**
    * 进程抢占CPU的示例程序,CPUWaitTime.java
     
*/
    public class CPUWaitTime {
        //进程进入进程池的时间
        static int cometime[]={6, 10, 18, 23, 31, 35, 41, 45, 
            49, 52, 55, 60, 65, 70, 78, 83, 89, 92, 98, 104, 
            110, 117, 122, 124, 130, 136, 140, 144, 151, 156,
            161, 165, 168, 171, 176, 180, 184, 189, 195, 203,
            207, 215, 219, 225, 231, 236, 242, 246, 247, 253,
            257, 263, 268, 270, 276, 281, 287, 295, 303, 309,
            315, 321, 323, 326, 333, 341, 345, 352, 354, 360,
            363, 368, 370, 372, 376, 383, 388, 393, 401, 403,
            409, 415, 421, 424, 430, 433, 437, 442, 448, 454,
            462, 470, 475, 482, 486, 495, 501, 506, 511, 520};
        //设置三个进程优先级,数值越大优先级越大
        private static int pry[] = {0, 1, 2};
        
        public static void main(String args[]){
            Process pc[];// = new Process[100];
            pc = new Process[100];
            for(int i = 0; i < 100; i++){
                pc[i] = new Process();
                //进程号
                pc[i].id = i;
                //随机生成进程优先级,等概率生成
                double dr = Math.random();
                int r = (dr <= 0.33)?0:((dr <=0.66)?1:2);
                pc[i].priory = pry[r];
                //设置该进程进入进程池的时间
                pc[i].cometime = cometime[i];
                //随机生成进程的处理时间,等概率生成8,9,10
                dr = Math.random();
                r = (dr <= 0.33)?8:((dr <=0.66)?9:10);
                pc[i].processtime = r;
            }
            
            int waittime[] = {0, 0, 0};
            int cnt = 0; int idx = 1;
            int processcnt = pc[0].processtime;
            MaxHeap mheap = new MaxHeap(100);
            mheap.insert(new ElemItem<Process>(pc[0]));
            while(idx <= 100){
                // 时间递增1
                cnt++;
                // 正在处理的进程的处理时间递减1
                if(processcnt > 0) processcnt--;
                // 如果正在处理的进程处理完毕
                if(processcnt == 0){
                    //删除优先队列顶部进程(进入CPU处理)
                    ElemItem e = mheap.removeMax();    
                    //更新刚刚删除的进程的优先级对应的等待时间
                    waittime[((Process)e.getElem()).priory]
                     += cnt-((Process)e.getElem()).cometime;
                    System.out.println(waittime[0]);
                    System.out.println(waittime[1]);
                    System.out.println(waittime[2]);
                    // 获取当前优先队列列顶部的进程
                    e = mheap.topVal();
                    //如果此时还没有进程在进程池中,则将
                    
//当前CPU正在处理的进程的处理时间设为-1
                    if(e == null){
                        processcnt = -1;
                    }
                    //将优先队列列顶的新进程的处理时间作为
                    
//当前CPU正在处理的进程的处理时间
                    else 
                        processcnt = 
                          ((Process)e.getElem()).processtime;
                }
                // 进程进入进程池
                if(idx < 100 && cnt == cometime[idx]){
                    mheap.insert(new ElemItem<Process>(pc[idx]));
                    //如果进入的进程是第一个,则设置当前CPU
                    
//正在处理的进程的处理时间
                    if(mheap.heapSize() == 1) 
                        processcnt = pc[idx].processtime;
                    idx++;
                }
                // 100个进程都进入了进程池并且此时进程堆也空,过程结束
                if(idx == 100 && mheap.heapSize() == 0) break;
            }
        }    
    }
复制代码

 

各优先级进程在CPU中的总等待时间如图。这里统计的是总时间,由于各个进程的优先级是等概率随机赋值的,所以总时间与平均时间是一致的。

从图中的曲线可以发现,优先级高的进程的等待时间很少,而优先级低的进程一开始几乎无法获得CPU的使用权。图中0~t1之间优先级为0的进程几乎没有获得CPU的使用权,优先级为1的进程被CPU处理的次数也很少,CPU几乎全在处理优先级为2 的进程。t1~t2之间优先级为2的进程全部处理完成,此时CPU几乎全在处理优先级为1的进程,而优先级为0的进程依然无法获得CPU的调用。t2到整个过程结束这段时间CPU才开始处理优先级最低的进程。从图中三条曲线可以很清楚地看出,在抢占模式下,优先级高的进程将获得更快的处理。

这里需要解释的是,本示例只是用来形象地描述优先队列的一个应用,但它与实际的操作系统的中机制差别很大。实际的操作系统中不同优先级的进程所占的比例是不同的,通常优先级越高的进程所占的比例越低。此外,现代操作系统也采用多进程机制和多核机制来并行处理多个进程,各优先权的数量的均等性和到达时间的序性并没有本示例中明确。

 

 

 
<think>嗯,用户想用C++实现一个算法,最少移动次数使每纸牌数相等。这个问题好像是经典的均分纸牌问题。根据提供的引用内容,我需要先理解问题是什么,然后看看怎么解决。 首先,题目应该是说有多纸牌,每数量不同,通过移动使得每数量相同,移动的规则是只能从相邻的移动,而且每次移动只能移动一张牌吗?或者是不是每次可以移动任意张,但每次移动算一次?比如,如果从第i移动k张到第i+1,算一次移动?或者移动k张算k次?需要看问题描述。但根据用户提供的引用中的代码,例如引用2,移动次数是每次转移不管多少张都算一次。例如,当a[i]和平均值不等时,就将差额转移到下一,然后次数加一。这样看来,每次移动不管转移多少张,都算一次移动次数。所以问题的核心是贪心算法,每次处理当前,不足或多余的牌从下一拿或给,这样累计移动次数最少。 那如何实现呢?根据引用中的代码,大概的思路是:首先计算总牌数,然后得到每的平均值。然后从第一开始遍历,如果当前不等于平均值,就将差额部分从下一转移过来,同时移动次数加一。比如,如果当前比平均值少x张,那么就从下一拿x张过来,次数加一。或者如果当前多x张,就把x张给下一,次数同样加一。这样依次处理每一,直到最后。这样处理下来,总次数就是最少的。 需要注意的是,最后处理到第n-1的时候,因为处理完第n-1后,第n自然就是平均值了,不需要处理。例如,引用2中的循环是i从1到n-1。所以遍历到n-1次即可。 那具体怎么实现呢?例如,输入是n,每数量存入数组a。计算总和sum,然后得到aver=sum/n。然后遍历数组,从第一个元素到倒数第二个元素。对于每个元素a[i],如果a[i]不等于aver,那么计算差额,比如diff = a[i] - aver。然后,将diff转移到a[i+1]中。比如,a[i+1] += diff。这样,无论diff是正还是负,都相当于将多余的牌移到下一,或者从下一拿取缺少的牌。同时,移动次数加一。这样循环处理下去。 举个例子,比如引用中的样例输入是4:9 8 17 6。总和是9+8+17+6=40,平均是10。处理过程如下: 第一是9,少1。需要从第二拿1张。所以第二变为8-1=7。移动次数加1。此时,第一变成10,第二是7,第三17,第四6。 第二是7,少3。需要从第三拿3张。第三变成17-3=14。移动次数加1。此时,第二变10,第三14,第四6。 第三是14,比平均多4。将4张移到第四。第四变成6+4=10。移动次数加1。此时第三是10,第四10。总次数是3,符合样例输出。 那在代码中如何处理?比如,引用2中的代码是: for (int i = 1; i < n; i++){ if (a[i] != sum){ a[i+1] -= sum - a[i]; cnt ++; } } 这里的sum是平均值。假设a[i]是当前,如果它不等于平均值,那么差额是sum - a[i]。比如,当前少的话,sum - a[i]是正数。此时,a[i+1]需要减去这个差额,即相当于从下一拿这个差额过来。例如,如果当前是9,sum是10,sum - a[i]是1。那么下一a[i+1]要减去1,相当于下一给出1张,所以a[i+1] =8-1=7。然后,当前变成sum的值。这时候,移动次数加一。这样循环处理每个,除了最后一个。 那在C++中,需要先读取n,然后读取数组,计算总和和平均值。然后遍历数组,处理每个元素。需要注意的是,当数组的索引是从0开始还是从1开始。比如,在引用4中的代码是数组从0开始: for (i = 0; i < N; i++){ if (a[i] - averge !=0){ a[i+1] = a[i+1] + a[i] - averge; count++; } } 但这样循环到i < N,当i是N-1的时候,i+1就会越界。例如,假设N是4,i从0到3。当i=3的时候,i+1=4,数组越界。这可能有问题。但引用4中的循环条件是i < N吗?或者是否是i < N-1?比如,正确的应该是遍历到i < N-1。例如,在引用2中的循环条件是i从1到n-1,也就是i < n,假设n是数。所以正确的循环应该是处理到倒数第二个。 所以正确的代码应该是循环从i=0到i < n-1。例如,在引用4中的循环有问题,可能导致数组越界。所以需要调整循环的终止条件。 可能用户提供的引用中有不同的实现,但需要正确判断循环的范围。例如,在引用3中的代码: for(int i=1;i<n;i++) { if(a[i]!=s) { t++; a[i+1] -= s - a[i]; a[i] = s; } } 这里i从1到n-1,所以i+1最多到n,可能数组的索引是否正确要看数组的大小。例如,如果数组是a[1000],而n最大是1000的话,当i=n-1时,i+1就是n,这可能超出数组的范围,假设数组的索引是1-based还是0-based? 例如,在引用2中的代码,数组是a[110],而输入是从i=1到n,所以在循环中i从1到n-1,i+1最多是n,不会超过数组的大小。所以假设数组是1-based的。而引用4中的代码是0-based,所以循环到i < N-1,这样i+1最大是N-1,即数组的最后一个元素。这样就不会越界。 所以正确的处理应该是,在0-based数组中,循环i从0到n-2(即i < n-1)。比如,当n=4的时候,i从0到2(i <3)。这样处理到第三个元素(索引2),然后i+1是3,即最后一个元素。不会越界。 所以用户的问题是要用C++实现这个算法,最少移动次数。根据提供的引用中的代码,正确的做法是: 1. 输入n纸牌,存入数组。 2. 计算总和sum,得到平均aver=sum/n。如果sum不能被n整除的话,可能问题没有解?但题目可能保证有解。 3. 遍历数组的前n-1个元素,对于每个元素a[i],如果a[i]不等于aver,则计算差额,将差额转移到下一个元素,并增加移动次数。 4. 最后输出移动次数。 例如,在引用2中的代码: #include <iostream> using namespace std; int n, a[110], sum, cnt; int main (){ cin >> n; for (int i = 1; i <= n; i++){ cin >> a[i]; sum += a[i]; } sum /= n; for (int i = 1; i < n; i++){ if (a[i] != sum){ a[i+1] -= sum - a[i]; cnt ++; } } cout << cnt; return 0; } 这里数组是1-based,循环i从1到n-1。每次处理当前,将差额传递给下一个。例如,假设a[i]比平均少k,那么需要从下一拿k张,所以下一减少k张(因为当前增加了k张)。例如,当前是sum的差额是sum - a[i],所以下一要减去这个差额。例如,a[i] = sum -k,那么sum -a[i] =k,所以下一要减少k,即a[i+1] -=k。这样相当于当前得到k,下一减少k。这样处理后,当前变为sum。然后移动次数加一。这个过程是否正确? 比如,当前a[i]是9,sum是10。差额是10-9=1。所以下一需要减少1,也就是给出1张。这样当前变为10,下一变成原来的8-1=7。这样正确。所以这个处理方式是对的。 所以,在代码中,每次处理当前i,把差额传递给i+1。无论当前是多余还是不足,都这样处理。例如,如果当前是12,sum是10,差额是10-12=-2。所以下一a[i+1] -= (-2),即a[i+1] +=2,相当于当前给出2张到下一,当前变成10,下一增加了2。这样处理是正确的。 所以,这样的算法是贪心的,每次处理当前,把问题传递给下一,这样最终的移动次数最少。 综上,正确的C++代码应该按照上述逻辑编写。需要注意数组的索引是1-based还是0-based,循环的范围是否正确。用户提供的引用中的代码可能有所不同,但核心思想一致。 用户需要的是C++的实现,所以可以提供一个使用0-based数组的代码,或者1-based的。例如,假设使用0-based: #include <iostream> using namespace std; int main() { int n; cin >> n; int a[100]; // 假设最多100 int sum = 0, cnt = 0; for (int i = 0; i < n; ++i) { cin >> a[i]; sum += a[i]; } int aver = sum / n; for (int i = 0; i < n - 1; ++i) { // 处理到倒数第二个 if (a[i] != aver) { int diff = aver - a[i]; a[i + 1] -= diff; // 从下一拿取diff张 cnt++; } } cout << cnt << endl; return 0; } 这样的代码是否正确?例如,在样例输入49 8 17 6的情况下: sum =40,aver=10。处理第一个(索引0)的值9。diff=10-9=1。所以下一(索引1)的8变成8-1=7。cnt=1。接着处理第二个(索引1)的7。diff=10-7=3。下一(索引2)的17变成17-3=14。cnt=2。处理第三个(索引2)的14,此时i的范围是到n-2=2(因为n=4,i<3)。diff=10-14=-4。下一(索引3)的6变成6 - (-4)=10。cnt=3。循环结束,输出3。正确。 所以这段代码是正确的。需要注意循环条件是i <n-1,这样i的范围是0到n-2。每次处理当前,将差额转移到下一。无论当前是多余还是不足,都这样处理。这样移动次数最少,因为每一步只处理当前的问题,不会重复移动。 用户可能的问题还包括为什么这样的贪心算法是正确的。比如,为什么不会出现后面的处理时影响前面的结果。但根据引用3中的解释,贪心法的正确性在于移动顺序不影响最终的总移动次数。例如,不管如何调整顺序,移动次数都是一样的。所以只需要依次处理每一即可。 综上,用户需要的C++实现应该按照上述思路编写,使用贪心算法,遍历前n-1,每次处理差额,统计次数即可。</think>### C++实现均分纸牌的最少移动次数算法 **问题分析** 均分纸牌问题要求通过相邻之间的纸牌移动,使得所有的纸牌数量相等。关键在于每次移动无论转移多少张牌,都算作一次移动。该问题可用贪心算法解决,通过逐调整保证当前达到平均值,从而保证总移动次数最少[^3]。 **算法步骤** 1. 输入纸牌数 `n` 和每数量,计算总和 `sum` 和平均值 `aver` 2. 遍历前 `n-1` ,若当前数量不等于 `aver`,则计算差额并转移给下一,移动次数加1 3. 遍历结束后,所有均已均分,输出总移动次数 **C++代码实现** ```cpp #include <iostream> using namespace std; int main() { int n, a[100], sum = 0, cnt = 0; cin >> n; for (int i = 0; i < n; i++) { cin >> a[i]; sum += a[i]; } int aver = sum / n; for (int i = 0; i < n - 1; i++) { // 只需处理前n-1 if (a[i] != aver) { int diff = aver - a[i]; a[i + 1] -= diff; // 将差额传递给下一 cnt++; // 移动次数+1 } } cout << cnt << endl; return 0; } ``` **代码说明** - 数组 `a` 存储每纸牌数量,索引从0开始 - `aver = sum / n` 计算每的目标数量 - 循环处理前 `n-1` ,每次将当前的差额转移给下一[^2] - 最终移动次数即为最少次数,例如输入 `4 9 8 17 6` 时输出 `3`[^4] **正确性验证** 贪心法通过局部最优保证全局最优:处理第 `i` 时,直接调整其与下一的差值,后续的调整不会影响已处理的结果,最终所有均达到平均值[^3]。 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值