【ADT】第六章 堆—优先队列

之前在链表ADT中涉及到了队列,LInkedList通过双链表实现了队列先进先出的功能
此次提出一个优先队列的概念,它依旧满足队列一端入队一端出队的原则,唯一的区别在于出列的工作是找出、返回并删除优先队列中的最小值
优先队列中的操作仅仅限于插入和最小值,不能排序、也不能find(非最小值)
一次insert(入队)的平均为O(1),删除最小元(出队)平均时间O(logN)

对优先队列的实现:

1 不使用链表:链表在表头以O(1)执行插入操作,遍历链表删除最小元将花费O(N)的时间
2 不使用二叉查找树:二叉查找树插入删除都花费O(logN)的时间,但是如果删除最小元必在左子树,每次都删除左子树会破坏树的平衡,但是最重要的是优先队列操作仅仅对最小值的操作,二叉树提供的确实对整个数值的排序,因此使用二叉树有些大材小用。

为了实现优先队列,使用堆ADT



1 二叉堆

二叉堆普遍使用于优先队列的实现

1.1 二叉堆结构

二叉堆是一棵被完全填满的二叉树,这就意味着只有底层可能存在单节点,其他上层都是两个子节点

二叉堆

如此我们可知二叉堆的性质:

1 一棵高为h的二叉堆的节点总数量为 2 h ~ 2 h+1-1,深为i(自上向下第i层)父节点数量为2i
2 一颗节点数为N的二叉堆的高位O(logN)
3 我们设定根节点的下标为1,则对于下标为i的元素其左儿子下标为2i , 右儿子下标为 2i+1,父节点下标为 i/2;如果我们设定根节点的下标为0,则对于下标为i的元素其左儿子下标为2i+1 , 右儿子下标为 2i+2,父节点下标为 (i-1)/2
4 我们设定根节点下标为1,则所有父节点的取值范围 [1 , N/2];如果我们设定根节点下标为0,则所有父节点的取值范围 [0 , N-1/2]

除此之外由于我们想快速找出最小元,应该让最小元处于根位置上(如果找最大元,就让最大元在根位置上),考虑到任意一个子树都是一个二叉堆,我们有堆排性质:

5 为快速找出最小元,要求对二叉堆中的每一个节点X,X的值应当小于或等于自己子节点的值(如果要找最大值,则大于等于)

依据5条性质,实现二叉堆的优先队列的操作,首先规定二叉堆的根节点下标由1开始,使用一个数组容器容纳二叉堆中的节点

1.2 插入

1.2.1 实现

插入元素T
因为基于数组的实现,所以不需要查找,一开始要插入的位置就是数组[length+1]的节点A位置
在此节点A上,如果父节点的值小于T,则可以直接插入
如果父节点的值大于T,则说明T应该上移到较上层的位置,此时将父节点移动到A的位置
再次基于这个父节点,比较它的父节点是否小于T。。。直到找到一个节点O,O的父节点小于T,或者O的父节点为null,此时O即为元素T的位置
(对于根的父节点我们可以用下标控制,下标>=1)

这个过程叫做 上滤
我个人对上滤的理解:上层结构是满足条件的,从底层向上遍历,每次和父节点作比较

    public void enqueue(T t) {
        //从下角标1 开始,父节点 i/2
        //入队,上滤
        // 上滤:从底层向上层查找适合t的位置,找父节点
        //如果t的父节点小于t 直接插入
        //否则把t的父节点拉下来,在t的父节点的位置上查找
        this.size++;//先加1 这时size表示的是底层要插入的空穴i
        int i = this.size;
        for(this.heap[0] = t;i >= 1; i = i/2) { // i是父节点,向上范围 >=1
            if(((T)this.heap[i/2]).compareTo(t) > 0){ //父节点比较大,下移
                this.heap[i] = this.heap[i/2];
            }else{
                break;
            }
        }
        this.heap[i] = t;
    }

1.2.2 时间复杂度

一次insert的平均为O(1)
最差情况:插入元素是最小值,应该上滤到堆顶,经过logN层(堆的高)比较,时间复杂度为O(logN)

1.3 删除最小元

1.3.1 实现

出队的操作就是删除最小元,最小元位于堆顶,即数组下标为1的元素
将根删除之后要重新调整结构,同时一个元素被删除,队列size-1,那么原来在数组[size]的元素必然要移动(即最后一个元素)
既然如此,我们令数组[1]=数组[size],将删除问题转换为 为数组[1]堆顶元素T不符合性质,为其寻找合适位置 的问题

首先在比较元素T和根的子节点大小,如果元素T比较小,那么这个位置就是T的位置
如果元素T比较大,在子节点之间选择最小的元素将他赋值到根节点,此时有一个子节点的位置空闲
继续比较T和这个子节点的子节点的大小,还是和上面情况一样,如果T小,那么这个位置就是元素T的位置
如果T大,在子节点之间选择一个较小者代替父节点,空出较小子节点位置。。。。
继续比较,直至T比子节点的值小,或T的子节点为null
(子节点为null,同样就是叶节点,用数组下标控制,2i<=size)
这个过程叫做下滤

我个人对上滤的理解:下层结构是满足条件的,从堆顶向下遍历,每次和子节点作比较

 private void downSort(int index) {
        // index位置的元素不符合二叉堆特性, 下滤查找适合他的位置并排序
        //下滤,从顶层向下查找适合当前元素的位置,找子节点
        //下层是符合排序的
        // i的子节点 2i 2i+1
        // 如果i的子节点比i要小,把最小的子节点上移,在子节点的位置继续向下查找 直到i的子节点不比i小
        T t = (T)this.heap[index];//拿到这个元素
        //有两个子节点,先找最小的子节点
        int i = index;
        int child ;//第一个子节点
        for(; i <= this.size / 2; i = child){ // i是父节点,向下 i <= size/2
            child = 2 * i;
            if(child != this.size && ((T)this.heap[child]).compareTo((T)this.heap[child+1]) > 0) { //找最小的子节点,如果只有一个节点那就是2i
                child++;
            }
            if(((T)this.heap[child]).compareTo(t) < 0) {
                //把child上移
                this.heap[i] = this.heap[child];
            }else{
                break; //否则跳出,子节点大,i就是要放的位置
            }
        }
        this.heap[i] = t;
    }

如上定义了一个通用方法,用来对index位置的元素寻找它的正确位置,删除操作就变成了

    public T dequeue() {
        //出队,返回删除最小元素(root),把最后一个元素拿到root处,下滤排序
        T min = (T)this.heap[1];
        this.heap[1] = this.heap[this.size];
        this.size--;
        downSort(1);
        System.out.println(min);
        return min;
    }

1.3.2 时间复杂度

删除最小元最差情况:放在根的元素需要下滤到最底层,经过logN层比较,时间复杂度O(logN)
而实际上,被放到根上的元素几乎都下滤到最底层(它来自的那一层)
因此平均时间O(logN)

1.4 构建堆

构建堆,是在构造函数时接收一组数组,将这组数组作为堆的元素构建

首先我们想到的是将这组N个元素一一insert,每个inset平均花费O(1),那构建N个元素的堆平均花费O(N)

一般情况下的思路为:将N项以任意顺序放入树中,从最大的父节点开始上滤 执行 下滤操作

    public void buildHeap(T[] list) {
        //利用下滤,对每个i位置上不满足序列的排序
        // 下滤要求下层是排好序的
        // 所以要从底到顶
        this.heap = new Object[this.size+DEFALUT_HEAP_SIZE];
        int i = 1;
        for(T t : list){
            this.heap[i++] = t;
        }
        for(int j = this.size/2; j >= 1; j--) {// 父节点开始--
            downSort(j);
        }
    }

父节点的取值范围为[1,size/2],从最后一个父节点开始一一向上遍历,每一个父节点执行在此节点上的下滤操作,为当前的父节点寻找合适的插入位置,直至根节点完成,二叉堆也就构建完成
此操作的时间复杂度的求解:
根节点操作O(logN) * 1
第一层父节点(O(logN)-1) * 2
……
第i层父节点 (O(logN)-i) * 2i
由此求和为O(N)

构建堆的时间复杂度为O(N)

1.5 扩展 d-堆

d-堆是对二叉堆的简单扩展,它的规则与二叉堆一样,只是所有的父节点都有d个儿子
二叉堆也可以叫做2-堆

在这里对时间复杂度分析:
d-堆比二叉堆要浅,d-堆的高度为O(logdN)
因此一次insert的平均运行时间为O(logdN)
但是对于删除操作,虽然堆变浅了,但是原先只需要在两个子节点之间找最小值,比较一次;现在需要在d个子节点之间找最小值,比较d-1次
删除操作平均花费O(d logdN),依旧为O(logN)层次

对d的讨论:
d的改变对父节点、子节点的位置发生影响:(第一个节点下标为1)
第i个节点,它的第一个子节点就是i*d-d+1,最后一个子节点为i*d+1
第i个节点,它的父节点为(i-2+d)/d

为找到父子节点,会经过复杂的计算,尤其是除法运算占据较大的运行时间,这样会增大运行时间
除非d是2的幂,可以通过移位运算实现除法

那么d-堆一般的应用场景:
在优先队列太大以至于不能完全装入主存时,可以考虑使用d-堆降低深度

2 JavaCollection实现

在Java1.5之后才实现了对优先队列的支持:泛型类PriorityQueue

在该类中调用
add方法实现入队
element实现返回最小元素但不删除
remove实现出队,返回并删除最小元素

3 堆的合并

除了不能排序、不能find之外,堆ADT的一个明显缺点是:很难将两个堆ADT合并成一个堆ADT(这种合并操作merge)

为了实现堆的合并,引进堆ADT的优化结构:左式堆、斜堆、二项队列

3.1 左式堆

3.2 斜堆

3.3 二项队列

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值