目录
一、简介
堆是计算机科学中一类特殊的数据结构的统称,堆通常可以被看做是一棵完全二叉树的数组对象。
堆的特性:
- 它是完全二叉树,除了树的最后一层结点不需要是满的,其它的每一层从左到右都是满的,如果最后一层结点不是满的,那么要求左满右不满。
- 它通常用数组来实现。并且从
索引 1
开始存储,即索引 0 直接废弃。具体方法就是将二叉树的结点按照层级顺序放入数组中,根结点在位置 1,它的子结点在位置 2 和 3,而子结点的子结点则分别在位置 4, 5 , 6 和 7,以此类推。
如果一个结点的位置为k
,则它的父结点(根节点没有父结点)的位置为[k/2]
,而它的两个子结点的位置则分别为2k
和2k+1
。这样,在不使用指针的情况下,我们也可以通过计算数组的索引在树中上下移动。
- 每个结点都(大于或小于)等于它的两个子结点。这里要注意堆中仅仅规定了每个结点(大于或小于)等于它的两个子结点,但这两个子结点的顺序并没有做规定,跟二叉查找树是有区别的。
- 上述提到的结点需要(大于或小于)等于它的两个子节点,是根据堆的类别来判断的。将根节点最大的堆叫做最大堆或大根堆,结点需要大于等于它的两个子结点;根节点最小的堆叫做最小堆或小根堆,结点需要小于等于它的两个子结点。
二、堆的实现
堆的API设计
public class Heap<T extends Comparable<T>> {
// 存储堆中的元素
private T[] items;
// 记录堆中元素的个数
private int N;
public Heap(int capacity) {
this.items = (T[]) new Comparable[capacity + 1];
this.N = 0;
}
// 判断堆中索引i处的元素是否小于索引j处的元素
private boolean less(int i, int j){
return items[i].compareTo(items[j]) < 0;
}
// 交换堆中i索引和j索引处的值
private void exch(int i, int j){
T temp = items[i];
items[i] = items[j];
items[j] = temp;
}
// 往堆中插入一个元素
public void insert(T t){
}
// 使用上浮算法,使索引k处的元素能在堆中处于一个正确的位置
private void swim(int k){
}
// 删除堆中最值并返回
public T delFirst(){
}
// 使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置
private void sink(int k){
}
}
(1)堆的插入
堆是用数组完成数据元素的存储的,我们往数组中从索引 1 处开始,依次往后存放数据,但是堆中对元素的顺序是有要求的,每一个结点的数据要(大于或小于)等于它的两个子结点的数据,所以每次插入一个元素,都会使得堆中的数据顺序变乱,这个时候我们就需要通过一些方法让刚才插入的这个数据放入到合适的位置。
以下图例根据大根堆为例
所以,如果往堆中新插入元素,我们只需要不断的比较新结点 a[k] 和它的父结点 a[k/2] 的大小,然后根据结果完成数据元素的交换,就可以完成堆的有序调整。这里就设计到堆的上浮操作,等会再细谈。
// 往堆中插入一个元素
public void insert(T t){
// 先将新元素加入堆
items[++ N] = t;
// 再对这个元素进行上浮操作
swim(N);
}
(2)删除根节点
由堆的特性我们可以知道,索引1处的元素,也就是根结点。当我们把根结点的元素删除后,堆的顺序就乱了,那么我们应该怎么删除呢?
思路:
- 交换根节点与最后一个元素
- 把末尾的根节点删除
- 对新的根节点进行下沉操作,使之处于正确的位置
这里又提到了一个下沉操作,我们还是以大根堆为例,如图所示。
所以,当需要删除最值时,只需要将最后一个元素放到索引 1 处,并不断的拿着当前结点 a[k] 与它的子结点 a[2k]和 a[2k+1] 中的(较大者或较小者,根据大根堆。小根堆判断)交换位置,即可完成堆的有序调整。
// 删除堆中最值并返回
public T delFirst(){
T first = items[1];
// 交换索引1处的元素和最大索引处的元素,
// 让完全二叉树中最右侧的元素变为临时根结点
exch(1, N);
// 最大索引处的元素删除掉
items[N] = null;
// 元素个数-1
N --;
// 通过下沉调整堆,让堆重新有序
sink(1);
return first ;
}
(3)上浮操作
① 大根堆的上浮
思路:
- 确定需要上浮元素的下标
k
- 当
k > 1
时,比较item[k]
与item[k / 2]
的大小
- 若
item[k] > item[k / 2]
,交换两者位置,k = k / 2
- 若
item[k] <= item[k / 2]
,上浮结束
// 使用上浮算法,使索引k处的元素能在堆中处于一个正确的位置
private void swim(int k){
// 通过循环,不断的比较当前结点的值和其父结点的值
while(k > 1){
// 如果发现父结点的值比当前结点的值小,则交换位置
if (less(k / 2, k)){
exch(k / 2, k);
}
k /= 2;
}
}
② 小根堆的上浮
思路:
- 确定需要上浮元素的下标
k
- 当
k > 1
时,比较item[k]
与item[k / 2]
的大小
- 若
item[k] < item[k / 2]
,交换两者位置,k = k / 2
- 若
item[k] >= item[k / 2]
,上浮结束
这里实际就以大根堆的上浮操作是差不多的,只是比较当前结点与父结点的标准不同罢了。
// 使用上浮算法,使索引k处的元素能在堆中处于一个正确的位置
private void swim(int k){
// 通过循环,不断的比较当前结点的值和其父结点的值
while(k > 1){
// 如果发现当前结点的值比父结点的值小,则交换位置
if (less(k, k / 2)){
exch(k, k / 2);
}
k /= 2;
}
}
(4)下沉操作
① 大根堆的下沉
思路:
- 确定需要下沉元素的下标
k
- 当
k * 2 <= N
(N 为堆中元素个数)时,比较item[k]
与max{ item[k * 2],item[k * 2 + 1]}
的大小,并记录item[k * 2]
,item[k * 2 + 1]
较大值的下标maxIndex
- 若
item[k] < item[maxIndex]
,交换两者位置,k = maxIndex
- 若
item[k] >= maxIndex
,下沉结束
// 使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置
private void sink(int k){
/*
通过循环不断的对比当前 k 结点和
其左子结点 2 * k 以及右子结点 2 * k + 1
处中的较大值的元素大小
*/
while(2 * k <= N){
// 获取当前结点的子结点中的较大结点
// 记录较大结点所在的索引
int maxIndex;
// 判断是否有右孩子结点
if (2 * k + 1 <= N){
maxIndex = less(2 * k,2 * k + 1) ? 2 * k + 1 : 2 * k;
}else {
maxIndex = 2 * k;
}
// 比较当前结点和较大结点的值
if (!less(k, maxIndex)){
break;
}
// 交换 k 索引处的值和 maxIndex 索引处的值
exch(k, maxIndex);
// 变换k的值
k = maxIndex;
}
}
② 小根堆的下沉
思路:
- 确定需要下沉元素的下标
k
- 当
k * 2 <= N
(N 为堆中元素个数)时,比较item[k]
与min{ item[k * 2],item[k * 2 + 1]}
的大小,并记录item[k * 2]
,item[k * 2 + 1]
较小值的下标minIndex
- 若
item[k] > item[maxIndex]
,交换两者位置,k = minIndex
- 若
item[k] <= maxIndex
,下沉结束
// 使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置
private void sink(int k){
/*
通过循环不断的对比当前 k 结点和
其左子结点 2 * k 以及右子结点 2 * k + 1
处中的较小值的元素大小
*/
while(2 * k <= N){
// 获取当前结点的子结点中的较小结点
// 记录较小结点所在的索引
int minIndex;
// 判断是否有右孩子结点
if (2 * k + 1 <= N){
minIndex = less(2 * k,2 * k + 1) ? 2 * k : 2 * k + 1;
}else {
minIndex = 2 * k;
}
// 比较当前结点和较小结点的值
if (less(k, minIndex)){
break;
}
// 交换 k 索引处的值和 minIndex 索引处的值
exch(k, minIndex);
// 变换k的值
k = minIndex;
}
}
(5)堆的构造
堆的构造,最直观的想法就是另外再创建一个新数组,然后从左往右遍历原数组,每得到一个元素后,添加到新数组中,并通过上浮,对堆进行调整,最后新的数组就是一个堆。
上述的方式虽然很直观,也很简单,但是我们可以用更聪明一点的办法完成它。创建一个新数组,把原数组[0 ~ length -1]
的数据拷贝到新数组的 [1 ~ length]
处,再从新数组长度的一半
处开始往 1
索引处扫描(从右往左),然后对扫描到的每一个元素做下沉
调整即可。
以下代码以构建大根堆为例
private static Comparable[] createHeap(Comparable[] source) {
Comparable[] heap = new Comparable[source.length + 1];
// 把source中的元素拷贝到heap中,heap中的元素就形成一个无序的堆
System.arraycopy(source,0,heap,1,source.length);
// 对堆中的元素做下沉调整(从长度的一半处开始,往索引1处扫描)
for (int i = (heap.length)/2;i>0;i--){
sink(heap,i,heap.length-1);
}
return heap;
}
// 在heap堆中,对target处的元素做下沉,范围是0~range
private static void sink(Comparable[] heap, int target, int range){
while(2 * target <= range){
// 找出当前结点的较大的子结点
int maxIndex;
if (2*target+1<=range){
if (less(heap,2*target,2*target+1)){
maxIndex = 2*target+1;
}else{
maxIndex = 2*target;
}
}else{
maxIndex = 2*target;
}
//2.比较当前结点的值和较大子结点的值
if (!less(heap,target, maxIndex)){
break;
}
exch(heap,target, maxIndex);
target = maxIndex;
}
}
(6)堆排序
这里以实现数据的升序为例
实现步骤:
- 构造堆
- 得到堆顶元素,这个值就是最大值
- 交换堆顶元素和数组中的最后一个元素,此时所有元素中的最大元素已经放到合适的位置
- 对堆进行调整,重新让除了最后一个元素的剩余元素中的最大值放到堆顶
- 重复2~4这个步骤,直到堆中剩一个元素为止
对于堆的构造,上述已经谈到,对构造好的堆,我们只需要做类似于堆的删除操作,就可以完成排序。
- 将堆顶元素和堆中最后一个元素交换位置;
- 通过对堆顶元素下沉调整堆,把最大的元素放到堆顶(此时最后一个元素不参与堆的调整,因为最大的数据已经到了数组的最右边)
- 重复1~2步骤,直到堆中剩最后一个元素。
public class HeapSort {
// 对source数组中的数据从小到大排序
public static void sort(Comparable[] source) {
// 构建堆
Comparable[] heap = createHeap(source);
// 定义一个变量,记录未排序的元素中最大的索引
int N = heap.length - 1;
// 通过循环,交换1索引处的元素和排序的元素中最大的索引处的元素
while(N != 1){
// 交换元素
exch(heap,1, N);
// 排序交换后最大元素所在的索引,让它不要参与堆的下沉调整
N --;
// 需要对索引1处的元素进行对的下沉调整
sink(heap,1, N);
}
// 把heap中的数据复制到原数组source中
System.arraycopy(heap,1, source,0, source.length);
}
// 根据原数组source,构造出堆heap
private static Comparable[] createHeap(Comparable[] source) {
Comparable[] heap = new Comparable[source.length + 1];
// 把source中的元素拷贝到heap中,heap中的元素就形成一个无序的堆
System.arraycopy(source,0,heap,1,source.length);
// 对堆中的元素做下沉调整(从长度的一半处开始,往索引1处扫描)
for (int i = (heap.length)/2;i>0;i--){
sink(heap,i,heap.length-1);
}
return heap;
}
// 在heap堆中,对target处的元素做下沉,范围是0~range
private static void sink(Comparable[] heap, int target, int range){
while(2 * target <= range){
// 找出当前结点的较大的子结点
int maxIndex;
if (2*target+1<=range){
if (less(heap,2*target,2*target+1)){
maxIndex = 2*target+1;
}else{
maxIndex = 2*target;
}
}else{
maxIndex = 2*target;
}
//2.比较当前结点的值和较大子结点的值
if (!less(heap,target, maxIndex)){
break;
}
exch(heap,target, maxIndex);
target = maxIndex;
}
}
// 判断heap堆中索引i处的元素是否小于索引j处的元素
private static boolean less(Comparable[] heap, int i, int j) {
return heap[i].compareTo(heap[j]) < 0;
}
// 交换heap堆中i索引和j索引处的值
private static void exch(Comparable[] heap, int i, int j) {
Comparable tmp = heap[i];
heap[i] = heap[j];
heap[j] = tmp;
}
}
三、优先队列
普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在某些情况下,我们可能需要找出队列中的最大值或者最小值,例如使用一个队列保存计算机的任务,一般情况下计算机的任务都是有优先级的,我们需要在这些计算机的任务中找出优先级最高的任务先执行,执行完毕后就需要把这个任务从队列中移除。普通的
队列要完成这样的功能,需要每次遍历队列中的所有元素,比较并找出最大值,效率不是很高,这个时候,我们就可以使用一种特殊的队列来完成这种需求,优先队列。
优先队列按照其作用不同,可以分为以下两种:
- 最大优先队列:
可以获取并删除队列中最大的值
- 最小优先队列:
可以获取并删除队列中最小的值
(1)最大优先队列实现
这里我们以实现构建最大优先队列为例,最大优先队列就是以大根堆实现的。
public class MaxPriorityQueue<T extends Comparable<T>> {
// 存储堆中的元素
private T[] items;
// 记录堆中元素的个数
private int N;
public MaxPriorityQueue(int capacity) {
this.items = (T[]) new Comparable[capacity+1];
this.N = 0;
}
// 获取队列中元素的个数
public int size() {
return N;
}
// 判断队列是否为空
public boolean isEmpty() {
return N == 0;
}
// 判断堆中索引i处的元素是否小于索引j处的元素
private boolean less(int i, int j) {
return items[i].compareTo(items[j]) < 0;
}
// 交换堆中i索引和j索引处的值
private void exch(int i, int j) {
T tmp = items[i];
items[i] = items[j];
items[j] = tmp;
}
// 往堆中插入一个元素
public void insert(T t) {
items[++N] = t;
swim(N);
}
// 删除堆中最大的元素,并返回这个最大元素
public T delMax() {
T max = items[1];
exch(1, N);
N --;
sink(1);
return max;
}
// 使用上浮算法,使索引k处的元素能在堆中处于一个正确的位置
private void swim(int k) {
while(k > 1){
if (less(k / 2, k)){
exch(k / 2, k);
}
k = k / 2;
}
}
// 使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置
private void sink(int k) {
while(2 * k <= N){
int maxIndex;
if (2 * k + 1 <= N){
if (less(2 * k,2 * k + 1)){
maxIndex = 2 * k + 1;
}else{
maxIndex = 2*k;
}
}else {
maxIndex = 2*k;
}
if (!less(k, maxIndex)){
break;
}
exch(k, maxIndex);
k = maxIndex;
}
}
}
最小优先队列就是以小根堆实现,只需更改上述代码的上浮与下沉代码,更改判断结点与父结点、子结点的标准即可。