数据结构与算法-二叉堆

422 阅读4分钟

本文完整代码

定义

堆是一种树状的数据结构,堆有很多种,比如本次的主题二叉堆,对叉堆,索引堆,二项堆,斐波那契堆...... 二叉堆,又叫完全二叉堆,顾名思义它是由完全二叉树的形式组成的. 提供三个接口: 1:添加元素 2:获取最大值 3:删除最大值


特性

  1. 逻辑结构由完全二叉树组成
  2. 物理结构一般由数组组成
  3. 任意节点的值,都大于等于(小于等于)子节点的值
  4. 由特性二决定了它的每个节点必须具有可比较性

1.示例图

2.结构图

规律总结

规律一: 索引i和节点总数n之间的规律

结合图2,我们来看看,索引i和节点总数n之间的规律,这个很重要,后面进行节点的添加会用到这些,floor()向下取整:

  • 1:如果i=0,则它是根节点
  • 2:如果i>0,它的父节点索引为:floor((i-1)/2)
  • 3:如果2i<=n-2,它的左子树索引为:2i+1
  • 4:如果2i>n-2,它无左子树
  • 5:如果2i<=n-3,它的右子树索引为:2i+2
  • 6:如果2i>n-3,它无右子树

规律二: 非叶子节点的数量==第一个叶子节点的索引==floor(n/2)


接口实现

添加元素O(logn)

3:添加元素

从上图我们可以总结添加元素的流程是个循环执行过程:node(待插入元素)

  • 1:添加元素的第一步是把node插入到数组尾部
  • 2:node的值>父节点值,与父节点交换位置,继续循环
  • 3:如果node<=父节点的值或者node没有父节点,则终止循环 整个过程叫做上滤
/**
	 * 添加元素
	 *
	 * @param element
	 */
	private void add(int element) {
		//添加到数组尾部
		this.elements[size++] = element;
		//上滤
		siftUp(size - 1);
	}

	/**
	 * 对index位置的元素进行上滤操作
	 *
	 * @param index 索引位置
	 */
	private void siftUp(int index) {
		//获取index位置元素的值
		int value = elements[index];
		while (index > 0) {
			//获取父节点索引值:见==规律一第2条==
			int parentIndex = (index - 1) >> 1;
			//获取父节点的值
			int parentValue = elements[parentIndex];
			//如果父节点的值大,则循环结束
			if (parentValue >= value) {
				break;
			}
			//来到这里,说明父节点的值小,进行交换
			int tmp = elements[index];
			elements[index] = elements[parentIndex];
			elements[parentIndex] = tmp;

			//交换之后,index要指向父节点,继续循环
			index = parentIndex;
		}
	}
添加元素的优化

上面的siftUp(int index) 方法中,我们判断如果父节点的值小,我们就进行交换操作,其实这里可以进行优化,我们可以先把要添加的元素的值保存起来,等到最后确定了要添加元素的最终位置,才把要添加的元素放上去. 4:添加元素的优化

/**
	 * 对index位置的元素进行上滤操作
	 *
	 * @param index 索引位置
	 */
	private void siftUp2(int index) {
		//保存新添加元素的值
		int value = elements[index];
		while (index > 0) {
			//获取父节点索引
			int parentIndex = (index - 1) >> 1;
			//获取父节点值
			int parentValue = elements[parentIndex];
			//如果父节点的值大,则循环结束
			if (parentValue > value) {
				break;
			}
			//来到这里,说明父节点的值小,父节点值下挪
			elements[index] = parentValue;
			// 重新赋值index
			index = parentIndex;
		}
		//循环结束,确定了最终位置就是index
		elements[index] = value;
	}

删除最大值(大顶堆)O(logn)

由大顶堆的性质,可以看出,删除最大值,其实就是把数组头元素删除了,是不是就这么简单,我们来看删除过程. 5:删除最大值 总结删除的过程如下:

  • 1:用尾部的值替换头部的值
  • 2:删除尾部的值
  • 3:如果node<最大子节点的值,与最大子节点的值交换
  • 4:如果node>=最大子节点的值,或者没有子节点,退出循环

整个过程叫做下滤,

  • 第一:如果是叶子节点的话,就不需要进行下滤操作,比如上图的38,47,21,14因为没有子节点,所以不需要下滤.
  • 第二:第一个叶子节点之后的所有节点都不需要下滤.
  • 第三: 那问题来了我们怎么获取第一个叶子节点的索引呢?请看规律二.
  • 第四: 需要下滤的节点,要么只有左子节点,要么左右子节点都有,所以,肯定有左子节点,看节点72,43,68
/**
	 * 删除最大值
	 *
	 * @return 最大值
	 */
	private int remove() {
		//头部元素,最大值
		int maxValue = elements[0];
		//尾部索引
		int lastIndex = --size;
		//把尾部的值赋值给头部
		elements[0] = elements[lastIndex];
		//删除尾部元素
		elements[lastIndex] = 0;
		//下滤操作
		siftDown(0);
		return maxValue;
	}
/**
	 * 对index位置数据进行下滤操作
	 *
	 * @param index 索引位置
	 */
	private void siftDown(int index) {
		//获取头部元素的值
		int value = elements[index];
		//获取第一个叶子节点的索引,规律二==floor(n/2)
		int firstIndex = size >> 1;
		//只有是非叶子节点才进行下滤
		while (index < firstIndex) {
			//index节点有二种情况,1:只有左子节点 2:左右子节点都有
			//因为肯定有左子节点,看规律一第3点
			int leftIndex = (2 << 1) + 1;
			//获取左子节点数值
			int leftValue = elements[leftIndex];

			//右子节点索引=左子节点索引+1
			int rightIndex = leftIndex + 1;
			//只有右子节点左右<整个数组大小,说明index也有右子节点
			if (rightIndex < size) {
				int rightValue = elements[rightIndex];
				//获取左右子节点最大的那一个
				if (rightValue > leftValue) {
					leftValue = rightValue;
					leftIndex = rightIndex;
				}
			}
			//如果节点数值>=左右子节点数值,退出循环
			if (value >= leftValue) {
				break;
			}
			//将子节点数值赋值给index位置数值
			elements[index] = leftValue;
			//重新设置index
			index = leftIndex;
		}
		//确定最终的位置
		elements[index] = value;
	}

获取最大值(大顶堆)O(1)

最大值=数组头元素


批量建堆

就是给定一个数组,里面的元素是随机的,怎么把这个数组变成一个二叉堆

自上而下的上滤(O(nlogn))

自上而下的上滤 第一个节点不需要上滤

      /**
	 * 批量建堆
	 */
	private void heapify() {
		// 自上而下的上滤,从第二个元素开始
		for (int i = 1; i < size; i++) {
			siftUp(i);
		}
	}

自下而上的下滤(O(n))

自下而上的下滤

从第一个非叶子节点开始往前一个个下滤

/**
	 * 批量建堆
	 */
	private void heapify() {
		// 自下而上的下滤,第一个叶子节点的索引=size/2,所以往前推一个就是第一个非叶子节点
		for (int i = (size >> 1) - 1; i >= 0; i--) {
			siftDown(i);
		}
	}

结论:平时我们在批量建堆的过程中还是选择自下而上的下滤

记得点赞哦!