堆(heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
- 堆中某个结点的值总是
不大于或不小于其父结点的值; - 堆总是一棵
完全二叉树。
将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。
数组实现堆
对于一个完全二叉树来说,用数组来存储是非常节省存储空间的。因为我们不需要存储指向左右子节点的指针,单纯的通过数组下标就可以找到一个节点的左右子节点和父节点。如下图所示。
对于数组中下标为 n 的节点来说, 这里的 n 从 1 开始算, 那么:
- 左子节点的索引是 2n
- 右子节点的索引是 2n+1
- 父节点的索引满足 n/2
建堆
建一个大顶堆
我们从最后一个非叶子节点开始,从上往下进行调整、堆化:
插入数据
- 让新插入的节点(也就是当前数组插入的最后一个节点)和其父节点对比大小。如果满足子节点大于等于父节点,就互换两个节点。一直重复这个过程,直到满足要求。
删除堆顶元素
- 首先把堆的最后一个元素和堆顶元素互换位置,然后删除最后一个元素。
- 然后从堆顶开始, 从上往下调整,直到父子节点满足大小关系为止
插入数据和删除堆顶数据的主要逻辑就是堆的调整,所以往堆里插入一个元素和删除堆顶元素的时间复杂度是O(logN)。
class Heap {
/// 堆: 实现大根堆
int _capacity = 0; // 堆的最大容量
late List _list; // 存储元素的数组
int _length = 0; // 长度
Heap({required int capacity}) {
_capacity = capacity;
_list = List.filled(capacity, null);
}
/// 添加元素, 并堆化
/// 为了最大性能的存储堆, 并保证堆中节点与数组下标的关系, 从下标1开始存储数据, 所以这里是 ++_length, 0位置不存储数据
/// 时间复杂度: O(logN)
add(item) {
if (isFull()) throw StackOverFlowException();
/// 从数组末尾插入, 然后开始堆化(heapify), 这里是最大堆,我们从下往上堆化
_list[++_length] = item;
_bubbleUp(_length);
}
/// 上浮算法, 使索引k处的元素处于一个正确的位置
void _bubbleUp(int index) {
/// 从下往上堆化
/// 循环, 如果当前节点元素 < 两个子节点元素中的较大值, 那么交换
/// 堆化的过程就是:用当前节点元素与父节点元素做比较, 大于父节点元素就交换,直到小于父节点元素为止
/// 到根节点截止, 根节点的索引是1
while (index > 1) {
/// 父节点的索引
var parentIndex = index ~/ 2;
/// 当前节点元素与父节点元素做比较, 大于父节点元素就交换
if (moreThan(index, parentIndex)) {
/// 交换
exchange(index, parentIndex);
index = parentIndex;
} else {
break;
}
}
}
/// 删除、返回堆顶元素, 并重新排列堆
/// 时间复杂度: O(logN)
removeFirst() {
if (isEmpty()) throw StackEmptyException();
/// 获取堆顶元素
var result = _list[1];
/// 交换索引1处元素和最大索引处的元素, 让完全二叉树最右侧的元素变为临时根结点
/// 交换后, 数组最后一个元素, 就是交换前的堆顶元素
exchange(1, _length);
/// 删除交换后的最大索引处的元素
_removeLast();
/// 通过下沉算法,重新排列堆
_bubbleDown(1);
return result;
}
/// 下沉算法: 从上往下调整
_bubbleDown(int index) {
/// 循环比较当前k节点和其 (左子节点2*k 以及 右子节点2*k+1处的较大值的) 元素大小
/// 当前节点元素小,则交换与子节点元素中最大值的位置
var leftChildIndex = index * 2;
/// 左子节点索引: k * 2
/// 右子节点索引: k * 2 + 1
while (leftChildIndex <= _length) {
/// 记录当前节点的左右子节点元素中的最大值索引
int maxChildIndex = 0;
int rightChildIndex = 2 * index + 1;
if (rightChildIndex <= _length) {
/// 当前k节点有右叶子节点, 取左右子节点元素中的较大者索引
maxChildIndex = moreThan(leftChildIndex, rightChildIndex)
? leftChildIndex
: rightChildIndex;
} else {
/// 当前k节点只有左子节点
maxChildIndex = leftChildIndex;
}
// 当前k节点的值大于 (左右子节点中最大节点的值), 则不需要交换, 循环结束
if (moreThan(index, maxChildIndex)) {
break;
}
/// k节点的值小于maxChildIndex的值,则需要继续交换
exchange(index, maxChildIndex);
/// 交换k的值, 重新标记左子节点
index = maxChildIndex;
leftChildIndex = index * 2;
}
}
/// 移除、返回最后一个元素/节点
_removeLast() {
if (isEmpty()) throw StackEmptyException();
var last = _list[_length];
_list[_length] = null;
/// 索引减1
_length--;
return last;
}
/// 排序算法: 从小到大排序
/// 时间复杂度: O(NlogN)
sort() {
/// 循环交换1索引处和排序的元素中最大的索引处的元素
while (_length != 1) {
/// 交换元素
exchange(1, _length);
/// 索引减1
_length--;
/// 堆索引1处的元素继续下沉
_bubbleDown(1);
}
return _list;
}
/// 判断索引i处的元素 < 索引j处的元素
bool moreThan(int i, int j) {
/// print(1.compareTo(2)); // => -1
/// print(2.compareTo(1)); // => 1
/// print(1.compareTo(1)); // => 0
return _list[i].compareTo(_list[j]) > 0;
}
/// 交换数组, 下标为 i 和 j 的元素
void exchange(int i, int j) {
var temp = _list[i];
_list[i] = _list[j];
_list[j] = temp;
}
toList() {
return _list;
}
bool isEmpty() {
/// 判断队列是否为空
return _length == 0;
}
bool isFull() {
/// 判断队列是否满
return _length == _capacity;
}
}
class StackOverFlowException implements Exception {
const StackOverFlowException();
String toString() => 'StackOverFlowException';
}
class StackEmptyException implements Exception {
const StackEmptyException();
String toString() => 'StackEmptyException';
}
void main() {
Heap _queue = Heap(capacity: 18);
/// 添加数据
_queue.add(17);
_queue.add(21);
_queue.add(13);
_queue.add(15);
_queue.add(33);
_queue.add(9);
_queue.add(2);
_queue.add(16);
_queue.add(5);
_queue.add(7);
_queue.add(3);
_queue.add(6);
_queue.add(8);
_queue.add(1);
// print('HeapQueue, 堆顶, first: ${_queue.first}');
// print('HeapQueue, _queue: $_queue');
_queue.add(35);
_queue.add(12);
// print('HeapQueue, _queue: $_queue');
_queue.add(3);
print('HeapQueue, toList: ${_queue.toList()}\n');
print('HeapQueue, go remove first');
/// 移除堆顶元素, 重新堆化
var first = _queue.removeFirst();
print('HeapQueue, first: $first');
print('HeapQueue, toList: ${_queue.toList()}\n');
first = _queue.removeFirst();
print('HeapQueue, first: $first');
print('HeapQueue, toList: ${_queue.toList()}\n');
/// 堆排序
var sortList = _queue.sort();
print('HeapQueue, sortList: ${sortList}\n');
}
堆排序
- 将
堆顶的数据与堆尾数据进行交换,这样就达到排好最大值的效果 - 将排好的数字不再纳入
建堆中【即建堆的目的就是选出最大值,所以已经选出来的,就无需要再参加】 - 并将交换后产生新的树进行重新
建堆【因为此时只有根节点发生变化,而左、右子树的关系并没有改变,所以可以利用向下调整算法进行建堆】 - 重复上述步骤即可,直至
堆里只剩下一个元素为止
时间复杂度: O(NlogN)
/// 排序算法: 从小到大排序
sort() {
/// 循环交换1索引处和排序的元素中最大的索引处的元素
while (_length != 1) {
/// 交换元素
exchange(1, _length);
/// 索引减1
_length--;
/// 堆索引1处的元素继续下沉
_bubbleDown(1);
}
return _list;
}