数据结构 - 堆(PriorityQueue源码分析)

目录

1、堆的特点

2、堆的操作

1、堆化(添加元素)

2、移除堆顶元素

3、Java中的堆(PriorityQueue、PriorityBlockingQueue)


    堆的定义:
    1、堆是一个完全二叉树
    2、堆的每个节点都大于等于(或者小于等于)左右子节点,即大于等于左子树和右子树。节点大于等于所有子节点叫做大顶堆,小于等于所有子节点叫做小顶堆。

1、堆的特点

    首先堆是一个完全二叉树,再回顾一下完全二叉树的特点,下面的操作都会基于这些特点。堆一般也是使用数组进行表示,我们一般会将数组下标为0的位置空着,从下标1开始存储数据。并且所有节点都满足,如果父节点的下标为 i :

左子节点的下标为 2 * i右子节点的下标为 2 * i + 1; 如果我们从数组下标为0开始存储数据的话,左子节点会增加一次加法运算,左子节点的下标为:2 * i + 1,右子节点的下标为 2 * i + 2。 即时间换空间的思想(或反之)。

    并且如果完全二叉树的节点个数为N,那么叶子节点的个数为 N/2。并且叶子节点都存储在数组的后半部分,入下图:

   

并且一个堆可以有不同的存在形式,只要满足上面的特点即可,比如 1,3,4,5四个数,可以组成多种不同形式的堆:

 

2、堆的操作

    最为数据结构是堆,其实本身就是优先级队列,在java中就是PriorityQueue和PriorityBlockingQueue,那么使用场景就是所有优先级队列的场景,还有就是经常用于处理Top K的问题(链接地址:)。数据结构的存在都是为了解决现实中的问题,那么数据结构堆需要对外提供添加一个元素、移除堆顶元素(查看),并且在添加和删除后任然要保证是堆。即满足上面的两点:完全二叉树、节点大于等于(或小于等于)所有子节点。

1、堆化(添加元素)

    往堆中添加一个元素的操作,我们叫做堆化,堆化的过程可以从上往下堆化,也可以从下往上堆化。添加是我们一般使用从下往上堆化的方式,先将添加的元素放到数组剩余的第一位,这种方式可以保证添加节点后肯定还是完全二叉树。此时需要从下往上判断是否满足堆的特性,如果是大顶堆就判断父节点值是否大于等于子节点。判断的路径就是当前元素的下标比如为 i,判断路径就是 i / 2, i / 4, i / 2² 。过程如下图:

用代码实现,上面图的打印结果:

 [0, 6, 5, 1, 3, 4, 0]
 [0, 6, 5, 2, 3, 4, 1]

public class Heap {

    public static void main(String[] args) {
        Heap heap = new Heap(6);

        int[] arr = {6, 5, 1, 3, 4,};
        for (int i = 0; i < arr.length; i++) {
            heap.insert(arr[i]);
        }
        System.out.println(Arrays.toString(heap.arr));
        heap.insert(2);
        System.out.println(Arrays.toString(heap.arr));
        // [0, 6, 5, 1, 3, 4, 0]
        // [0, 6, 5, 2, 3, 4, 1]
    }

    /** 用数组存储堆元素 */
    public int[] arr;
    /** 堆的最大值,即数组的长度,当前不考虑扩容问题 */
    private int maxLength;
    /** 当前堆已经存储的个数 */
    private int currentLength;

    public boolean insert(int value) {
        if (currentLength >= maxLength) {
            return false;
        }
        // 先赋值到空闲数组的最后一位,此时一定满足完全二叉树
        ++currentLength;
        arr[currentLength] = value;
        int index = currentLength;
        // 判断还没有到堆顶下标为1, 并且当前值大于 index/2 为父节点的值
        while(index/2 > 0 && arr[index] > arr[index / 2]) {
            // 交换元素
            HeapSort.swap(arr, index, index/2);
            // 下标缩小一半,继续
            index = index / 2;
        }
        return true;
    }

    public Heap(int capacity) {
        // 留出下标为0的位置
        this.arr = new int[capacity + 1];
        maxLength = capacity;
        this.currentLength = 0;
    }

}

 

2、移除堆顶元素

    删除元素右要防止结构发生变化,那么类似添加操作,只使用数组有值的最后一个下标元素顶替堆顶元素【下标为1的元素】,再往下进行堆化。从堆顶开始判断左右子节点是否比自己大,将最大的进行交换。如果交换了,就将指针指向交换了的元素,继续前面的判断(与自己的左右子节点进行对比)。

代码实现入下:

    public Boolean remove() {
        if (currentLength == 0) {
            return false;
        }
        // 将最后一个叶子节点放到堆顶位置,计数器减1
        arr[1] = arr[currentLength];
        --currentLength;
        heapify(arr, currentLength, 1);
        return true;
    }

    private void heapify(int[] a, int currentLength, int i) { // 自上往下堆化
        while (true) {
            // 从堆顶1,开始往下堆化, maxPos记录要替换的节点
            int maxPos = i;
            // 左子下标在数组有值范围内判断, 如果当前节点值小于左子节点
            if (i*2 <= currentLength && a[i] < a[i*2]) {
                maxPos = i*2;
            }
            // 右子下标在数组有值的范围内判断,如果上面的父节点或者左子节点的最大值 比右子节点小
            if (i*2+1 <= currentLength && a[maxPos] < a[i*2+1]) {
                maxPos = i*2+1;
            }
            // 上面两个判断都没进,即父节点大于左右子节点,停止
            if (maxPos == i) {
                break;
            }
            // 将父节点与左子或右子替换
            HeapSort.swap(arr, i, maxPos);
            // 将堆化的指针指向刚替换过的左子或右子,看看其下面是否还需要替换
            i = maxPos;
        }
    }

 

3、Java中的堆(PriorityQueue源码分析)

    堆其实就是优先队列,只是叫法不同而已,解决的都是相同的问题使用场景。在Java中提供了非线程安全的PriorityQueue和线程安全的PriorityBlockingQueue(前面分析过该类型为阻塞队列+优先级队列【堆】)。现在只是对数据结构进行分析,并且查看Java中堆的实现过程和堆化(从上往下和从下往上)的过程。所以下面对非线程安全的PriorityQueue源码分析,如下:

public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable {

	/** 默认的初始化数组的长度 */
	private static final int DEFAULT_INITIAL_CAPACITY = 11;
	/**
	 * 该队列从下表0开始存储,父节点下标[n]的左子节点下标[2*n+1]和右子节点下标[2*(n+1)] ,
	 * 默认是小顶堆,存储的泛型对象,左右子节点需要比父节点大,就是使用Comparator回调进行比较
	 * 没有使用private 修饰就是为了简化内部类的访问
	 */
	transient Object[] queue;
	/** 队列的总容量 */
	private int size = 0;
	/** 计算使用的个数,也是:没有使用private 修饰就是为了简化内部类的访问 */
	transient int modCount = 0; // non-private to simplify nested class access
	/** 比较器,如果比较器为 Null则使用自然排序 */
	private final Comparator<? super E> comparator;

	/******************************* 添加元素,从下往上堆化的过程 *********************************/
	/** 添加元素,使用的add方法兼容AbstractQueue接口 */
	public boolean add(E e) {
		return offer(e);
	}

	public boolean offer(E e) {
		if (e == null)
			throw new NullPointerException();
		modCount++;
		int i = size;
		// 判断是否扩容,需要则扩容为原来的二倍
		if (i >= queue.length)
			grow(i + 1);
		size = i + 1;
		// 如果堆为空,添加到堆顶(即下标为0的位置)
		if (i == 0)
			queue[0] = e;
		else // 否则与我们自己写的一样,往上堆化
			siftUp(i, e);
		return true;
	}

	/** 往上堆化,使用比较器或自然排序只是判断值大小时使用,其他都一样,下面只分析比较器的从下往上堆化 */
	private void siftUp(int k, E x) {
		if (comparator != null)
			siftUpUsingComparator(k, x);
		else
			siftUpComparable(k, x);
	}

	/** 添加元素时,从下往上heapify的过程 */
	private void siftUpUsingComparator(int k, E x) {
		while (k > 0) {
			// 带符号右移,获取父节点的下标
			int parent = (k - 1) >>> 1;
			// 获取父节点的值,并且使用比较器的回调方法比较
			Object e = queue[parent];
			if (comparator.compare(x, (E) e) >= 0)
				break; // while的退出条件就是已经满足堆的大小比较
			// swap父子节点的值,只是这样就一次性将X直接赋值到最终位置上 queue[k] = x;
			queue[k] = e;
			k = parent;
		}
		queue[k] = x;
	}

	/****************************** 移除堆顶元素,从上往下堆化的过程 *******************************/
	/** 移除堆顶元素 */
	public E poll() {
		if (size == 0)
			return null;
		// 获取叶子节点的最后一个值的下标
		int s = --size;
		modCount++;
		// 获取堆顶元素
		E result = (E) queue[0];
		// 获取叶子节点的最后一个值
		E x = (E) queue[s];
		queue[s] = null;
		if (s != 0)
			// 与上图一样,将最后一个叶子节点的值替换堆顶元素,
			// 再从上往下进行堆化
			siftDown(0, x);
		return result;
	}

	/** 同样,比较器只是用于判断值的大小,下面只分析带比较器的堆化过程 */
	private void siftDown(int k, E x) {
		if (comparator != null)
			siftDownUsingComparator(k, x);
		else
			siftDownComparable(k, x);
	}

	/** 从上往下堆化的过程 */
	private void siftDownUsingComparator(int k, E x) {
		// 完全二叉树本身后半部分是叶子节点不需要堆化
		int half = size >>> 1;
		while (k < half) {
			// 获取子节点的下标
			int child = (k << 1) + 1;
			// 获取左子元素
			Object c = queue[child];
			// 获取右子节点下标
			int right = child + 1;
			// 判断右子下标必要越过总长度,并且他先比较了左右子节点的最大值,
			if (right < size &&
					comparator.compare((E) c, (E) queue[right]) > 0)
				c = queue[child = right];
			// 那
			if (comparator.compare(x, (E) c) <= 0)
				break;
			// 同样现在只是内部调整,计算出堆顶元素X 最后应该去的位置k
			queue[k] = c;
			k = child;
		} 
		// 最后一步直接将x放置到最后应该去的位置上
		queue[k] = x;
	}

	/** 扩容,扩容为原来的两倍 */
	private void grow(int minCapacity) {
		int oldCapacity = queue.length;
		// Double size if small; else grow by 50%
		int newCapacity = oldCapacity + ((oldCapacity < 64) ?
				(oldCapacity + 2) :
				(oldCapacity >> 1));
		// overflow-conscious code
		if (newCapacity - MAX_ARRAY_SIZE > 0)
			newCapacity = hugeCapacity(minCapacity);
		queue = Arrays.copyOf(queue, newCapacity);
	}
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值