文章目录
- 堆
- 堆的时间复杂度
- 堆的分类
- 堆的存储
- 堆的操作
- 插入元素
- 删除堆顶元素
- 堆排序
- 建堆
- 排序
- 所有操作代码
堆
堆一般分为两种类型:最大堆和最小堆。在最大堆中,父节点的值总是大于或等于子节点的值,而在最小堆中,父节点的值总是小于或等于子节点的值。堆可以被用来进行排序,如堆排序,它的时间复杂度是 O(nlogn)。堆还可以被用来实现优先级队列、图算法中的最短路径算法、高效的计算中位数等等
堆一般是完全二叉树,但不都是完全二叉树,只是为了方便存储和索引,我们通常用完全二叉树的形式来表示堆,事实上,广为人知的斐波那契堆和二项堆就不是完全二叉树,它们甚至都不是二叉树。
堆的时间复杂度
相较于有序数组,堆的主要优势在于插入和删除数据效率较高。 当只关心所有数据中的最大值或最小值,存在多次取最大值或最小值,多次插入或删除数据时,就可以使用堆。
有序数组 | 堆 | |
---|---|---|
初始化 | O(nlog(n)) | O(n) |
查找最大(最小)值 | O(n) | O(1) |
插入(删除)数据 | O(n) | O(log(n)) |
堆的分类
堆分为 最大堆 和 最小堆。二者的区别在于节点的排序方式。
- 最大堆:堆中的每一个节点的值都大于等于子树中所有节点的值
- 最小堆:堆中的每一个节点的值都小于等于子树中所有节点的值
堆的存储
由于完全二叉树的优秀性质,利用数组存储二叉树即节省空间,又方便索引(若根结点的序号为 1,那么对于树中任意节点 i,其左子节点序号为
2*i
,右子节点序号为2*i+1
)。
Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
value | 23 | 18 | 20 | 9 | 5 | 1 |
堆的操作
堆的操作主要包括两种:插入元素和删除堆顶元素。
插入元素
- 将要插入的元素放到最后
- 从底向上,如果父结点比该元素小,则该节点和父结点交换,直到无法交换
// 向树中添加元素 public void add(int val) { this.size++; this.data[this.size] = val; // floatUp(); floatUp1(val); } // 上浮操作,交换操作 private void floatUp() { int curIndex = this.size; int paintIndex = getParentIndex(curIndex); while (curIndex > 1 && this.data[curIndex] > this.data[paintIndex]) { swap(curIndex, paintIndex); curIndex = paintIndex; paintIndex = getParentIndex(curIndex); } } // 上浮操作,替换操作 private void floatUp1(int ele) { int curIndex = this.size; int paintIndex = getParentIndex(curIndex); while (curIndex > 1 && ele > this.data[paintIndex]) { this.data[curIndex] = this.data[paintIndex]; curIndex = paintIndex; paintIndex = getParentIndex(curIndex); } this.data[curIndex] = ele; }
删除堆顶元素
自底向上堆化
- 将堆顶元素删除
- 然后比较左右子结点,将较大的元素填充到根节点的位置
- 一直循环填充空出的位置,直到堆的最底部
会出现空节点,浪费存储空间
自顶向下堆化
- 将堆顶元素删除
- 将堆尾元素放到堆顶
- 判断堆顶元素和左右子结点,将较大的元素与堆顶元素交换
- 一直循环直至堆的最底部
// 从堆中取出优先级最大的元素 public int removeMaxValueFromHeap() { int result = this.data[1]; this.data[1] = this.data[size]; this.size--; int curIndex = 1; int leftChildIndex = getLeftChild(curIndex); while (leftChildIndex <= this.size) { int rightChildIndex = leftChildIndex + 1; // 判断右子树是否存在并且右子树要大于左子树,否则就是左子树与父结点进行判断 if (rightChildIndex <= this.size && this.data[leftChildIndex] < this.data[rightChildIndex]) { swap(rightChildIndex, curIndex); curIndex = rightChildIndex; } else { if (this.data[leftChildIndex] > this.data[curIndex]) { swap(leftChildIndex, curIndex); } curIndex = leftChildIndex; } leftChildIndex = getLeftChild(curIndex); } return result; }
堆排序
- 建堆,将一个无序的数组建立为一个堆
- 排序,将堆顶元素取出,对剩余元素进行堆化,直到所有元素被取出
建堆
建堆的过程就是一个对所有非叶节点的自顶向下堆化的过程。非叶节点就是最后一个节点的父结点以及它之前的元素都是非叶节点。也就是,如果节点个数为n,我们需要对n/2到1
的节点进行自顶向下堆化。
public MaxHeap(int[] arr) {
this.data = new int[arr.length + 1];
this.size = arr.length;
for (int i = 0; i < arr.length; i++) {
this.data[i + 1] = arr[i];
}
for (int i = this.size / 2; i > 0; i--) {
sinkDown(i);
}
}
// 从节点为index处开始下沉
private void sinkDown(int index) {
int curIndex = index;
int leftChildIndex = getLeftChild(curIndex);
while (leftChildIndex <= this.size) {
int rightChildIndex = leftChildIndex + 1;
// 判断右子树是否存在并且右子树要大于左子树,否则就是左子树与父结点进行判断
if (rightChildIndex <= this.size && this.data[leftChildIndex] < this.data[rightChildIndex]) {
swap(rightChildIndex, curIndex);
curIndex = rightChildIndex;
} else {
if (this.data[leftChildIndex] > this.data[curIndex]) {
swap(leftChildIndex, curIndex);
}
curIndex = leftChildIndex;
}
leftChildIndex = getLeftChild(curIndex);
}
}
排序
由于堆顶元素是所有元素中最大的,所以我们重复取出堆顶元素,将这个最大的堆顶元素放至数组末尾,并对剩下的元素进行堆化即可。由于堆尾元素空出来了,我们就可以将取出的元素放在末尾,所以其实就是做了一次交换操作。
Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
value | 23 | 20 | 18 | 9 | 5 | 1 |
Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
value | 20 | 9 | 18 | 1 | 5 | 23 |
Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
value | 18 | 9 | 5 | 1 | 20 | 23 |
Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
value | 9 | 1 | 5 | 18 | 20 | 23 |
Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
value | 5 | 1 | 9 | 18 | 20 | 23 |
Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
value | 1 | 5 | 9 | 18 | 20 | 23 |
所有操作代码
package datastructure.heap;
import java.util.*;
public class MaxHeap {
private int[] data; // 保存树中的节点值
private int size; // 保存树中的节点个数
public MaxHeap() {
this.data = new int[500];
Arrays.fill(data, Integer.MIN_VALUE);
this.size = 0;
}
public MaxHeap(int[] arr) {
this.data = new int[arr.length + 1];
this.size = arr.length;
for (int i = 0; i < arr.length; i++) {
this.data[i + 1] = arr[i];
}
for (int i = this.size / 2; i > 0; i--) {
sinkDown(i);
}
}
// 判断堆是否为空
public boolean isEmpty() {
return this.size == 0;
}
// 获取堆中元素个数
public int getSize() {
return this.size;
}
// 根据当前节点所在数组的索引获取父结点的索引
public int getParentIndex(int index) {
if (index == 0) {
return -1;
}
return index / 2;
}
// 根据当前节点所在数组的索引获取左孩子节点的索引
public int getLeftChild(int index) {
return index * 2;
}
// 向树中添加元素
public void add(int val) {
this.size++;
this.data[this.size] = val;
// floatUp();
floatUp1(val);
}
// 上浮操作,交换操作
private void floatUp() {
int curIndex = this.size;
int paintIndex = getParentIndex(curIndex);
while (curIndex > 1 && this.data[curIndex] > this.data[paintIndex]) {
swap(curIndex, paintIndex);
curIndex = paintIndex;
paintIndex = getParentIndex(curIndex);
}
}
// 上浮操作,替换操作
private void floatUp1(int ele) {
int curIndex = this.size;
int paintIndex = getParentIndex(curIndex);
while (curIndex > 1 && ele > this.data[paintIndex]) {
this.data[curIndex] = this.data[paintIndex];
curIndex = paintIndex;
paintIndex = getParentIndex(curIndex);
}
this.data[curIndex] = ele;
}
// 获取堆中最大元素
public int getMaxValueFromHeap() {
if (isEmpty()) {
return -1;
}
return this.data[1];
}
// 从堆中取出优先级最大的元素
public int removeMaxValueFromHeap() {
int result = this.data[1];
this.data[1] = this.data[size];
this.size--;
int curIndex = 1;
sinkDown(curIndex);
return result;
}
// 从节点为index处开始下沉
private void sinkDown(int index) {
int curIndex = index;
int leftChildIndex = getLeftChild(curIndex);
while (leftChildIndex <= this.size) {
int rightChildIndex = leftChildIndex + 1;
// 判断右子树是否存在并且右子树要大于左子树,否则就是左子树与父结点进行判断
if (rightChildIndex <= this.size && this.data[leftChildIndex] < this.data[rightChildIndex]) {
swap(rightChildIndex, curIndex);
curIndex = rightChildIndex;
} else {
if (this.data[leftChildIndex] > this.data[curIndex]) {
swap(leftChildIndex, curIndex);
}
curIndex = leftChildIndex;
}
leftChildIndex = getLeftChild(curIndex);
}
}
// 交换的方法
private void swap(int curIndex, int changeIndex) {
int temp = this.data[curIndex];
this.data[curIndex] = this.data[changeIndex];
this.data[changeIndex] = temp;
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer("[");
for (int i = 0; i < this.size; i++) {
if (i == 0) {
sb.append(this.data[i + 1]);
} else {
sb.append(", " + this.data[i + 1]);
}
}
sb.append("]");
return sb.toString();
}
public static void main(String[] args) {
int[] arr = new int[]{23, 9, 20, 18, 5, 1};
System.out.println("数组中的元素:" + Arrays.toString(arr));
MaxHeap maxHeap = new MaxHeap(arr);
// Arrays.stream(arr).forEach(maxHeap::add);
// 将数组变为堆
System.out.println("创建堆:" + maxHeap.toString());
// 向堆中添加元素
maxHeap.add(35);
System.out.println("向堆中添加元素35:" + maxHeap.toString());
// 删除堆顶元素
maxHeap.removeMaxValueFromHeap();
System.out.println("删除堆顶元素:" + maxHeap.toString());
// 堆排序
for (int i = 0; i < arr.length; i++) {
arr[i] = maxHeap.removeMaxValueFromHeap();
}
System.out.println("堆排序:" + Arrays.toString(arr));
}
}