堆,完全二叉数的一种,分为大堆和小堆,如果父节点比子节点大,堆顶元素最大,则是大堆,反之为小堆,以小堆为例(后面演示所用皆为小堆),其结构如下图所示:
抽象的是,堆的实现靠的是顺序表(也难怪可以用它来排序啦),也就是数组,那么,堆是如何通过数组实现的呢?
首先记住一个公式,在堆的数组中,父下标和子下标的关系为:左子下标 = 父下表*2+1,右子下表在左子下表的基础上加一。一个堆的基本结构如下:
typedef int HPDataType;
typedef struct Heap
{
HPDataType* _a;
int _size;
int _capacity;
}Heap;
void HeapInit(Heap* php)
{
assert(php);
php->_a = NULL;
php->_size = 0;
php->_capacity = 0;
}
现在有了一个堆的基本结构,那么如何向其中插入数据呢?
这里介绍一个向上调整算法,以上图树的结构为例,若想插入数据,先将数据置于数组末尾,如插入一个4,就像这样:
然后让它与父节点5比较,如果它比父节点小,就和父节点交换位置,继续向上比较,直到父节点比它小或到堆顶为止
这样,就成功地向堆中插入了一个节点,具体代码如下:
void AdjustUp(HPDataType* a, int n)
{
//n代表要向上调整的数据下表
int child = n;
int parent = (n - 1) / 2;
while (parent >= 0)
{
if (a[child] < a[parent])
{
//交换父子
HPDataType temp = a[child];
a[child] = a[parent];
a[parent] = temp;
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void HeapPush(Heap* hp, HPDataType x)
{
//空间不足时记得扩容
if (hp->_capacity <= hp->_size)
{
hp->_a = realloc(hp->_a, (hp->_capacity == 0 ? 4 : 2 * hp->_capacity) * sizeof(HPDataType));
hp->_capacity = (hp->_capacity == 0 ? 4 : 2 * hp->_capacity);
}
hp->_a[hp->_size] = x;
//向上调整算法
AdjustUp(hp->_a, hp->_size);
hp->_size++;
}
然后就是在堆中删除数据,删除数据用的是向下调整算法,是向上调整算法的逆过程,
为了删除堆顶数据,我们先将堆顶元素和堆尾元素交换位置,然后堆的size,也就是堆的元素个数减一,忽略原堆顶元素,就相当于删了它,现在要用向下调整算法把原堆尾元素拉下来。
从堆顶开始,堆顶元素和其子元素比较,若子元素比它小,则和子元素交换位置,继续向下比较和交换,直到子元素比它大或到堆底为止,其具体代码如下:
void AdjustDown(HPDataType* a, int n, int size)
{
//n代表要向下调整的数据下标
//size代表整个数组元素长度
HPDataType parent = n;
HPDataType child = parent * 2 + 1;//得到父亲的左孩子,这里的child是最小的那个child,先假设是左孩子
while (child < size)
{
if (child + 1 < size && a[child] > a[child + 1])
{
child++;
}
if (a[child] < a[parent])
{
//交换
HPDataType temp = a[child];
a[child] = a[parent];
a[parent] = temp;
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapPop(Heap* hp)
{
//交换堆顶和堆底元素
HPDataType temp = hp->_a[0];
hp->_a[0] = hp->_a[hp->_size - 1];
hp->_a[hp->_size - 1] = temp;
hp->_size--;
AdjustDown(hp->_a,0,hp->_size);
}
那么两个堆的核心方法,堆的插入和删除,就成功实现了,还有一点其它的方法,比如堆的销毁,获取堆顶元素等,具体代码如下:
// 堆的销毁
void HeapDestory(Heap* hp)
{
assert(hp);
free(hp->_a);
hp->_a = NULL;
hp->_size = 0;
hp->_capacity = 0;
}
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
assert(hp);
assert(hp->_size > 0);
return hp->_a[0];
}
// 堆的数据个数
int HeapSize(Heap* hp)
{
assert(hp);
return hp->_size;
}
// 堆的判空
int HeapEmpty(Heap* hp)
{
return hp->_size == 0 ? 1: -1;
}
现在,堆的结构已经实现,下面就来研究堆排序吧!
现在给定一个数组,若要对它进行堆排序,先用向下或向上调整算法将其构建成堆,接下来的步骤和堆顶元素的删除类似,先把堆顶元素与堆底元素互换,再用向下调整算法维持堆结构,直到仅剩堆顶元素为止,具体代码如下:
//堆排序,时间复杂度为nlogn
void HeapSort(int* a, int n)
{
//n代表数组长度
//建堆,用向上调整算法,时间复杂度为nlogn
//降序,用小堆,升序,用大堆,这里演示降序
//for (int i = 0; i < n; i++)
//{
//AdjustUp(a, i);
//}
//建堆,用向下调整算法,时间复杂度为n
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, i ,n);
}
for (int i = n - 1; i > 0; i--)
{
//交换
int temp = a[0];
a[0] = a[i];
a[i] = temp;
AdjustDown(a, 0, i);
}
}