堆就是用数组实现的二叉树(完全二叉树),它没有父指针、左右子指针。
堆分为两种:大根堆(最大堆), 小根堆(最小堆),两者差别在于排序方式。
小根堆:父节点的值比每一个子节点的值都小
大根堆:父节点的值比每一个子节点的值都大
堆中用来存储数据的结构为一个 数组,大/小根堆 的根节点即为数组的第 0 个元素,则第0个元素即为该堆中的 最大/小元素。
小根堆的第0个元素为该堆中的最小值。
大根堆的第0个元素为该堆中的最大值。
如下小根堆:
数组中的排序为0 1 3 5 9 8 4 6 20 10 16
如果展示为二叉树形式如下
0
-------------------------------------------------
| |
1 3
------------------------- -------------------------
| | | |
5 9 8 4
------------- -------------
| | | |
6 20 10 16
从上图可以清晰的看到每个节点的值都小于其左右子节点的值(只要满足这一特征,小根堆排序即成立)
同样的数据来构建一个大根堆
如下大根堆:
数组中的排序为 20 16 6 8 10 3 1 0 4 5 9
如果展示为二叉树形式如下
20
-------------------------------------------------
| |
16 6
------------------------- -------------------------
| | | |
8 10 3 1
------------- -------------
| | | |
0 4 5 9
从上图可以清晰的看出每个节点的值均大于其左右子节点的值(只要满足这一特征,大根堆排序即成立)
堆中的存储结构只有一个数组,所以是没有如上图显示的树形结构指针的。
那么如何确认每个节点的父节点、左右子节点呢。它们的索引如下
第 i 个索引节点
父节点索引:parentIndex = ( i - 1) / 2
左节点索引:leftChIndex = i * 2 + 1
右节点索引: righChIndex = i * 2 + 2
取如上大根堆20 16 6 8 10 3 1 0 4 5 9
的一个节点 16
该节点在数组中的索引为 1,将 i = 1带入公式计算,
则其父节点 (i - 1) / 2 = (1 - 1) / 2 = 0
索引为 0,对应值为 20
左节点 (i * 2) + 1 = (1 * 2) + 1 = 3
索引为 3,对应的值为 8
右节点 (i * 2) + 2 = (1 * 2) + 2 = 4
索引为4, 对应的值为10
对照树形结构展示图,对应无误
当添加、删除、从一个无序数列构建堆时是如何排序的?
答:通过 上虑、下虑两个操作可以实现堆的排序
下面以大根堆为列
上虑:逆序(父节点<子节点)则互换父/子节点的值
下虑:index 位置的值,比子节点的值小,则互换自身与交大子节点的值
20 16 6 8 10 3 1 0 4 5 9
当删除根节点20后,将最后一个节点 9 替换掉20,9作为新的根节点
9 16 6 8 10 3 1 0 4 5
此时检查根节点 9的左子树为 16,9 < 16 不满足大根堆特性, 将9下虑, 9 跟16互换位置,
此时排序为
16 9 6 8 10 3 1 0 4 5
接下来依然检测节点9,节点9的右子树为节点10, 9<10 不满足大根堆特性,将9下虑,9跟10互换位置,此时排序为
16 10 6 8 9 3 1 0 4 5
到此排序已经满足堆特性,结束
当添加一个节点的时候使用上虑来调整排序,在此不详细说明了,下面将代码实现奉上
class Heap<T> where T : IComparable<T>
{
// 此处使用 List 是为了偷懒,因为涉及到 插入 insert 和删除 delet
// 如果使用数组首先开辟多大空间不确定假设开辟 N 个空间,则还需要
// 记录当前已经使用到哪个下标索引了记为 size。且当 size >= N 时
// 还需要手动再次开辟空间
public List<T> _list = new List<T>();
/// 是否大根堆
private bool _isBigHeap = true;
public Heap() { }
public void SetHeapType(bool isBigHeap)
{
_isBigHeap = isBigHeap;
}
private int ParentIndex(int index)
{
index = (index - 1) >> 1;
return index;
}
public void Insert(T value)
{
_list.Add(value);
PercolateUp(_list, _list.Count - 1);
}
public T GetRoot()
{
if (_list.Count <= 0)
{
return default(T);
}
return _list[0];
}
// 删除最大元素
public T DelRoot()
{
if (_list.Count <= 0)
{
return default(T);
}
T max = _list[0];
// 删除堆顶元素,将末元素填补到堆顶。
_list[0] = _list[_list.Count - 1];
_list.RemoveAt(_list.Count - 1);
// 对堆顶元素下虑
PercolateDown(_list, 0, _list.Count);
return max;
}
// 批量建堆
public void HeapCreate()
{
// 批量建堆思路为从最后一个非叶子节点开始下虑,一直到跟节点结束
// 所有非叶子节点执行完下虑堆自然而成
for (int i = (_list.Count / 2) - 1; i >= 0; --i)
{
PercolateDown(_list, i, _list.Count);
}
}
// 上虑
private void PercolateUp(List<T> dataList, int index)
{
if (index >= dataList.Count)
{
return;
}
// 直到抵达堆顶
while (0 < index)
{
// 获取 index 的父节点
int parentIndex = ParentIndex(index);
// 逆序(父节点<子节点)则互换父/子节点的值
if (Compare(dataList[parentIndex], dataList[index]) >= 0)
{
break;
}
T temp = dataList[parentIndex];
dataList[parentIndex] = dataList[index];
dataList[index] = temp;
index = parentIndex;
}
}
// 下虑
public void PercolateDown(List<T> dataList, int index, int length)
{
if (index >= dataList.Count)
{
return;
}
// 令 index 位置的值 为自身和子节点中最大者
int maxIndex = 0;
while (index != (maxIndex = ProperParent(dataList, index, length)))
{
// index 位置的值,比子节点的值小,则互换自身与较大子节点的值
T temp = dataList[maxIndex];
dataList[maxIndex] = dataList[index];
dataList[index] = temp;
// 互换位置,继续下虑
index = maxIndex;
}
}
// 自己和左右两个子节点中最大者
public int ProperParent(List<T> dataList, int index, int length)
{
int leftChildIndex = index * 2 + 1;
int rightChildIndex = index * 2 + 2;
if (length > leftChildIndex)
{
index = Compare(dataList[index], dataList[leftChildIndex]) >= 0 ? index : leftChildIndex;
}
if (length > rightChildIndex)
{
index = Compare(dataList[index], dataList[rightChildIndex]) >= 0 ? index : rightChildIndex;
}
return index;
}
public int Compare(T x, T y)
{
int compare = x.CompareTo(y);
return _isBigHeap ? compare : compare * -1;
}
}
测试代码如下
Heap<int> heap = new Heap<int>();
heap.SetHeapType(true);
heap.Insert(4);
heap.Insert(6);
heap.Insert(8);
heap.Insert(5);
heap.Insert(9);
heap.Insert(3);
heap.Insert(1);
heap.Insert(0);
heap.Insert(20);
heap.Insert(10);
heap.Insert(16);
Console.WriteLine();
// 获取跟
int root = heap.GetRoot();
// 删除跟
heap.DelRoot();
// 将数组中数据清除添加 N 个数值
heap._list.Clear();
for (int i = 0; i < 10; ++i)
{
heap._list.Add(i);
}
// 批量建堆将序列排序
heap.HeapCreate();
堆的原理以及实现