实现一个简单的小顶堆(MinHeap)


Java实现小顶堆

二叉堆

二叉堆在逻辑结构上完全可以看成是一棵完全二叉树,完全二叉树的性质:

  • 非叶子节点层次尽可能排满
  • 叶子节点数量为:2^(h-1)个(h>=1),如果不满,则叶子节点要尽可能的靠左排列

按照排序的规则可划分为:

  • 小顶堆(小根堆)
  • 大顶堆(大根堆)

实现

使用ArrayList动态数组实现二叉堆

为什么不用链表?

数组和链表在入队和出队的操作上的时间复杂度都是O(logn),但是由于链表需要使用额外的空间来存储节点之间的父子关系,而数组的地址是连续且固定的,可以用简单的公式来表示父子关系,因此采用动态数组而不是链表

父子节点索引关系

父节点索引:parentIndex = (index - 1) / 2(index表示不管是左子节点还是右子节点都可以,计算机在运算时会默认向下取整)

左子节点索引:leftIndex = parentIndex * 2 + 1

右子节点索引:rightIndex = parentIndex * 2 + 2

代码

package Queue;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class MinHeap<E> {
    private List<E> list;
    private Comparator<E> comparator;

    /**
     * todo 构造器
     * 1.空参构造器
     * 2.指定比较器(Comparator)创建最小堆
     * 3.指定数组创建最小堆
     * 4.指定容量创建最小堆
     * 5.指定容量和比较器创建最小堆
     * @param <E>
     */

    /**
     * 空参构造器
     */
    public MinHeap() {
        list = new ArrayList<>();
    }

    /**
     * 指定比较器
     */
    public MinHeap(Comparator<E> comparator) {
        list = new ArrayList<>();
        this.comparator = comparator;
    }

    /**
     * 指定数组
     */
    public MinHeap(E[] array) {
        list = new ArrayList<>(Arrays.asList(array));
        // todo 定义一个方法,将数组转成堆,因为没有指定Comparator,所以使用自然排序
        heapify();
    }

    /**
     * 指定容量
     */
    public MinHeap(int capacity) {
        list = new ArrayList<>(capacity);
    }

    /**
     * 指定容量和比较器
     */
    public MinHeap(int capacity, Comparator<E> comparator) {
        list = new ArrayList<>(capacity);
        this.comparator = comparator;
    }

    /**
     * todo 定义常用方法
     * 1.返回堆中的元素个数
     * 2.判断堆是否为空
     * 3.获取当前节点的父节点索引
     * 4.获取当前节点的左子节点
     * 5.获取当前节点的右子节点
     *
     * 父节点索引和左右子节点索引的关系:
     *  leftIndex = parentIndex * 2 + 1;
     *  rightIndex = parentIndex * 2 + 2;
     *  下面这里不管是leftIndex还是rightIndex都可以,因为计算机会向下取整
     *  parentIndex = (leftIndex - 1) / 2;
     */

    /**
     * 返回最小堆中的元素个数
     * @return
     */
    public int size() {
        return list.size();
    }

    /**
     * 判断堆是否为空
     * @return
     */
    public boolean isEmpty() {
        return list.isEmpty();
    }

    /**
     * 获取当前节点的父节点索引
     * @param index
     * @return
     */
    public int parentIndex(int index) {
        if (index == 0) {
            throw new IllegalArgumentException();
        }
        return (index - 1) / 2;
    }

    /**
     * 获取当前节点的左子节点索引
     * @param index
     * @return
     */
    public int leftIndex(int index) {
        return index * 2 + 1;
    }

    /**
     * 获取当前节点的右子节点
     * @param index
     * @return
     */
    public int rightIndex(int index) {
        return index * 2 + 2;
    }

    /**
     * todo 添加元素的操作
     * 1.把元素添加到数组的末尾
     * 2.判断小顶堆是否失衡,失衡就进行siftUp操作让小顶堆保持平衡
     *
     * todo siftUp
     * 将元素和其父节点比较,比父节点小则跟父节点交换值,循环这种操作直至新元素无法交换
     */
    public void add(E e) {
        // 将新元素添加到数组末尾
        list.add(e);
        siftUp(list.size() - 1);
    }

    private void siftUp(int index) {
        // 将元素索引和其父节点索引比较,比父节点小则跟父节点交换值,循环这种操作直至新元素无法交换
        if (comparator != null) {
            // 指定了比较器
            while (index > 0 && comparator.compare(list.get(parentIndex(index)), list.get(index)) > 0) {
                E tempElement = list.get(index);
                list.set(index, list.get(parentIndex(index)));
                list.set(parentIndex(index), tempElement);
                // 子父节点交换值后要把当前索引赋值为父索引
                index = parentIndex(index);
            }
        } else {
            // 没有指定比较器
            while (index > 0 && ((Comparable) list.get(parentIndex(index))).compareTo(list.get(index)) > 0) {
                E tempElement = list.get(index);
                list.set(index, list.get(parentIndex(index)));
                list.set(parentIndex(index), tempElement);
                index = parentIndex(index);
            }
        }
    }

    /**
     * todo 弹出元素的操作
     * 1.弹出数组第一个元素
     * 2.将数组末端元素移到堆顶
     * 3.判断堆是否失衡,失衡就进行siftDown操作让堆保持平衡
     *
     * todo siftDown
     * 将当前节点的值和min(左子节点, 右子节点)比较,比子节点的值大就交换,否则就结束循环
     */
    public E poll() {
        if (list.isEmpty() || list == null) {
            return null;
        }
        E popElement = list.get(0);
        list.set(0, list.get(list.size() - 1));
        list.remove(list.size() - 1);
        siftDown(0);
        return popElement;
    }

    private void siftDown(int index) {
        if (comparator != null) {
            // 有指定的比较器
            while (leftIndex(index) < list.size()) {
                int tempIndex = leftIndex(index);
                // 如果右子节点存在且其值小于左子节点就替换掉tempindex的值
                if (rightIndex(index) < list.size() && comparator.compare(list.get(leftIndex(index)), list.get(rightIndex(index))) > 0) {
                    tempIndex = rightIndex(index);
                }
                // 如果当前节点的值小于等于子节点的值就退出
                if (comparator.compare(list.get(index), list.get(tempIndex)) <= 0) {
                    break;
                }
                E tempElement = list.get(tempIndex);
                list.set(tempIndex, list.get(index));
                list.set(index, tempElement);
                index = tempIndex;
            }
        } else {
            // 没有指定的比较器
            while (leftIndex(index) < list.size()) {
                int tempIndex = leftIndex(index);
                if (rightIndex(index) < list.size() && ((Comparable) list.get(leftIndex(index))).compareTo(list.get(rightIndex(index))) > 0) {
                    tempIndex = rightIndex(index);
                }
                if (((Comparable) list.get(index)).compareTo(list.get(tempIndex)) <= 0) {
                    break;
                }
                E tempElement = list.get(tempIndex);
                list.set(tempIndex, list.get(index));
                list.set(index, tempElement);
                index = tempIndex;
            }
        }
    }

    /**
     * 将数组转成堆
     */
    private void heapify() {
        // 将最后一个非叶子节点的元素往前遍历并使用siftDown操作插入到堆中
        for (int i = parentIndex(list.size() - 1); i >= 0; i--) {
            siftDown(i);
        }
    }

    /**
     * 查看堆顶元素
     * @return
     */
    public E peek() {
        if (list.isEmpty() || list == null) {
            return null;
        }
        return list.get(0);
    }

}

测试小顶堆:

  • 往小顶堆中添加1000_0000个元素
  • 定义一个长度为1000_0000的数组,将小顶堆中的元素弹出并保存到数组中
  • 判断数组中下一个元素的值是否大于上一个元素的值,大于则证明小顶堆错了
package Queue;

import java.util.concurrent.ThreadLocalRandom;

public class testMinHeap {
    public static void main(String[] args) {
        int n = 1000_0000;
        MinHeap<Integer> minHeap = new MinHeap<Integer>();

        // 往堆中随机存放一千万个元素
        for (int i = 0; i < n; i++) {
            int element = ThreadLocalRandom.current().nextInt(0, n);
            minHeap.add(element);
        }
        int[] arr = new int[n];

        // 将堆中的元素弹出并存到数组中
        for (int i = 0; i < n; i++) {
            arr[i] = minHeap.poll();
        }

        for (int i = 1; i < arr.length; i++) {
            if (arr[i - 1] > arr[i]) {
                throw new RuntimeException("error heap");
            }
        }
        System.out.println("小顶堆顺序正确!");
    }
}

image-20240508150718856


文章作者: Feliks
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Feliks !
评论
  目录