3.1 二叉堆

       现实中总存在这样一类需求:从一个集合中每次取出的数据都是最小值或者最大值。比如说股市的连续竞价阶段的撮合交易,需要每次取出卖方的最低价和买方的最高价进行撮合交易。如果用数组列表,从尾部插入时进行排序,然后取的时候从尾部取出。这个想法似乎不错。但是1964年,J.W. J. Williams发明了一种新的数据结构,将这个问题用更好的算法和数据结构解决。这个数据结构就是堆。

http://www.dsc.ufcg.edu.br/~pet/jornal/maio2013/images/materias/historia_da_computacao/J.W.J.%20Williams.jpg

       堆,逻辑上它是一棵满二叉树,而物理上它是一个数组。所谓的满二叉树,形象地说,就是除最下面一层外,其他层都是满的。如图所示:

       那么怎么物理上是数组呢?其实很简单,就是按层扫描这棵树,然后按顺序塞入数组。数组就是这个样子:

       因为第一层是1个元素,第二层是两个元素,第三层是4个。又到了找规律的阶段了。我找到了一个规律是每层有2n个元素。根据这个下一层数量是上一层两倍的关系,我又计算出来了数组元素a[n]的左子为a[2n+1],右子为a[2n+2]。

       那么每个元素,获取parent的公式就是(x-1)/2,比如(2n+1 -1)/2 = n (2n+2 -1)/2 = n

       上面的堆是大顶堆,所以还有小顶堆。大顶堆要求左子和右子都比parent小。小顶堆要求左子和右子都比parent大。

一 堆添加元素

       堆从尾部添加元素。尾部添加元素后需要将堆进行调整,以保证堆顶是最大或者最小值。其与parent对比。比如小顶堆

       添加了新元素1之后,因为1比parent小,所以需要和parent交换位置。

       而且这是个循环到root的过程,因为新加的元素可能是整个数组中最小的元素。因为一直往上比较,所以这个过程叫sift up。但是不要忘了扩容。

二 堆删除元素

       因为顶部是最大或最小的元素,也就是业务需要的元素。所以堆只从顶部删除元素。

       堆删除元素的逻辑是这样的,先删除索引顶部的元素,先再把最后一个元素移动到顶部。也就是数组的最后一个元素,放到索引0这个位置。因为这从数组末尾移动过来的元素,并不是最大值或者最小值,所以需要进行sift down过程对堆进行调整,以保证堆顶是最大值或者最小值。

       以小顶堆为例子,讲一下siftdown过程。设新移动到堆顶的元素为e。

       Siftdown过程就是从堆顶开始,将两个子元素比较出一个最小值(如果只有一个子元素那么这个子元素就是最小值)。

       将e和子元素中的最小值比较。如果e比子元素的最小值大,那么交换位置。

       循环下去,直到不能找到子元素或者e比最小的子元素都小。

       一级一级往下比较的目的是要保证每个子树都是小顶堆。

       以下是我用java写的简单小顶堆实现。

package com.youngthing.heaps.binary;

/**
 * 小顶堆
 * created at 12/01/2022
 *
 * @author 俞建波
 * <a href="mailto://yujianbo@chtwm.com">yujianbo@chtwm.com</a>
 * @since 1.0.0
 */
public class MinHeap<T extends Comparable> {

    private Comparable[] elements;
    private int index;

    public MinHeap() {
        elements = new Comparable[8];
    }

    public void add(T t) {
        // 先进行扩容
        if (index == elements.length - 1) {
            final Comparable[] array = new Comparable[(elements.length + 1) << 1];
            System.arraycopy(elements, 0, array, 0, elements.length);
            elements = array;
        }
        elements[index++] = t;
        siftUp(t, index - 1);
    }

    private void siftUp(T t, int index) {
        while (index > 0) {
            int parent = (index - 1) >> 1;
            T parentElement = (T) elements[parent];
            if (parentElement.compareTo(t) > 0) {
                // 交换
                elements[index] = parentElement;
                elements[parent] = t;
                index = parent;
            } else {
                break;
            }
        }
    }

    public T remove() {
        if (index == 0) {
            return null;
        }
        T returnValue = (T) elements[0];
        elements[0] = elements[index - 1];
        siftDown();
        elements[--index] = null;
        return returnValue;
    }

    private void siftDown() {
        // i 代表当前null值位置
        int i = 0;
        /*
         *  一直循环往后跳
         * case 1 如果i的左子节点超出了边界,那么跳出,这时候右子树一定超出边界
         * case 2 如果i的左子节点未超出边界,但是右子节点超出了边界,比较并交换左子节点
         * case 3 i的左右子节点均未超出边界,比较最小者,与i交换
         */
        Comparable parent = elements[i];
        while ((i << 1) + 1 < index) {

            int leftIndex = (i << 1) + 1;
            int rightIndex = leftIndex + 1;
            if (rightIndex == index) {
                // 交换 parent和left
                if (parent.compareTo(elements[leftIndex]) > 0) {
                    swap(i, leftIndex);
                }
                break;
            }

            Comparable left = elements[leftIndex];
            Comparable right = elements[rightIndex];
            // left right 比较 小的值往上放树顶
            int smallerIndex;
            if (left.compareTo(right) > 0) {
                smallerIndex = rightIndex;
            } else {
                smallerIndex = leftIndex;
            }
            // 和最小的进行交换
            if (parent.compareTo(elements[smallerIndex]) > 0) {
                swap(i, smallerIndex);
                i = smallerIndex;
            } else {
                // 如果已经是最小,那么没必要比较了
                break;
            }
        }

    }

    private void swap(int i, int smallerIndex) {
        Comparable temp = elements[i];
        elements[i] = elements[smallerIndex];
        elements[smallerIndex] = temp;
    }
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

醒过来摸鱼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值