喜欢我的文章,点击上方“编程三分钟”关注,不加班时更新。
回复“资源”,获取一份专属大礼包。
真爱,加个“星标” 或者点个“在看”。
对于接触编程的人员来说,堆这个词经常会听到,经常和一群名次混合堆区,栈区,静态区等等,面试的时候可能经常也会遇到一个算法,堆排,今天小编主要和大家一起来看看堆这个数据结构。
学习堆要有两个前提 1:你学过二叉树,不需要很精通,但是基本的结构你要知道 2:你学过线型表,随便一种(链表 顺序表)都可以,为了理解容量这个概念。
堆实际上是在满二叉树基础上做的一个延拓,我们来看看满二叉树
如果你第一次接触堆我们就来看个图
线型表转换成树无非就是将这个线型表的元素依次放入这个完全二叉树中,相反树转换成线性表就是将这个二叉树的节点层次放入这个表中
首先我们将这个堆的节点构造出来
typedef struct Heap
{
DataType *heap;
size_t capacity;//容量
size_t size;//实际大小
}Heap;
老规矩还是先写出最简单的函数,判断堆满还是空。
bool HeapEmpty(Heap *php)
{
return php->size == 0;
}
bool HeapFull(Heap *php)
{
return php->size >= php->capacity;
}
然后就是我们堆的初始化销毁
void HeapInit(Heap *php, int sz)
{
php->capacity = sz;
php->size = 0;
php->heap = (DataType*)malloc(sizeof(DataType) * sz);
}
void HeapDestroy(Heap *php)
{
php->capacity = php->size = 0;
free(php->heap);
php->heap = NULL;
}
接下来就是我们堆的核心,前面所讲都是为了接下来的操作
我们补充几个知识点: 一个满二叉树
父节点(序号数)\* 2 = 左子节点;
父节点(序号数)\* 2 +1= 右子节点;
在堆中,涉及一个调整,向上调整和向下调整,也就是我们常说的最小堆和最大堆
来看百度百科给出的定义:
最大堆:根结点的键值是所有堆结点键值中最大者; 最小堆:根结点的键值是所有堆结点键值中最小者。
我们设计向下调整和向上调整这两个函数就是为了实现这个最小堆和最大堆,也就是我们所谓的堆排。
在上代码前我们规定所有的下标都是从 0 开始 ,我们需要传入一个调整的点作为初始调整点
void _AdjustUp(Heap *php, int start)
{
int j = start;
int i = (j - 1) / 2;//父节点
while (j > 0)//j没到根节点,j是当前节点
{
if (php->heap[j] < php->heap[i])
{
Swap(&(php->heap[j]),&(php->heap[i]));
j = i;
i = (j - 1) / 2;
}
else
break;
}
}
向上调整无非就是和父节点相比然后将小的放上去 来看向下调整 这时候我们需要注意的是我们先要找到堂兄关系节点中的最小的那个和 父节点相比较。
void _AdjustDown(Heap *php, int start)
{
size_t i = start;
size_t j = 2 * i + 1;
while (j < php->size)
{
if (j + 1 < php->size && php->heap[j] > php->heap[j + 1])//找到最小的
j++;
if (php->heap[j] < php->heap[i]) //把小的换上去
{
Swap(&(php->heap[j]), &(php->heap[i]));
i = j;
j = 2 * i + 1;
}
else
break;
}
}
我们再做出来插入函数
bool HeapInsert(Heap *php, DataType x)
{
if (HeapFull(php))
return false;
php->heap[php->size] = x;
_AdjustUp(php, php->size);
php->size++;
return true;
}
然后再做出销毁函数和展示函数
**void HeapDestroy(Heap *php)
{
php->capacity = php->size = 0;
free(php->heap);
php->heap = NULL;
}
**
void HeapShow(Heap *php)
{
int i;
for (i = 0; i < php->size; ++i)
{
printf("%d ", php->heap[i]);
}
printf("\n");
}
然后就是取堆顶元素
DataType HeapTop(Heap *php)
{
if (!HeapEmpty(php))
{
return php->heap[0];
}
return -1;
}
前面说这么多呢是为了干啥?堆排,对就是这个很多人哭着看完的算法。
堆排的原理就将是通过堆的调整,找到最大(或最小的)数,之后把他和最后的数交换,并让这个堆大小减一,那么这个最大(或最小的数)就不再参与堆的调整了.我们为啥做这么多函数呢?
就是为了实现这个堆排过程中的移动。当然还需要一个函数就是将这个堆顶取出来然后将堆最后的元素补上去再向下调整。
bool HeapRemove(Heap *php)
{
if (HeapEmpty(php))
return false;
php->heap[0] = php->heap[php->size - 1];
php->size--;
_AdjustDown(php, 0);
return true;
}
我们先来看这个堆是怎么构建出来的 :上代码
int ar[] = { 27, 15,19, 18, 28};
int n = sizeof(ar) / sizeof(int);
Heap hp;
int i;
HeapInit(&hp, n);
for (i = 0; i < n; ++i)
{
HeapInsert(&hp, ar[i]);
}
打印出来
我们画图看下 我们是不是期待这样一个二叉树:
认为这就是我们那个堆,但是 show 出来就不一样,因为我们堆这个堆在插入的时候做出了调整,也就是_AdjustUp,每插入一个数都会向上调整。
来看过程
最后就调整成为了 这个情况,当然我们还需要看个步骤就是取一次堆顶会产生什么现象
HeapRemove(&hp);
HeapShow(&hp);
继续看图
看到这堆排的所有能力就具备了,我们直接来看代码
为了我们代码能够实现多个接口,我们多放几个参数,一个记录起始位置,一个记录结束位置,对着一段数进行排序。
我们构建个小堆调整
void _HeapAdjustDown(int *ar, int left, int right, int start)
{
int n = right - left + 1;
int i = start;
int j = 2 * i + 1 + left; //+left
while (j < n)
{
if (j + 1 < n && ar[j] < ar[j + 1])
j++;
if (ar[i] < ar[j])
{
Swap(&ar[i], &ar[j]);
i = j;
j = 2 * i + 1;
}
else
break;
}
}
void HeapSort(int * ar,int left, int right)
{
int n = right - left+ 1;
int cur = n / 2 - 1 + left; //+left
while (cur>=0)
{
_HeapAdjustDown(ar, left, right, cur);
cur--;
}
int end = right;
while (end > left)
{
Swap(&ar[left], &ar[end]);
end--;
_HeapAdjustDown(ar, left, end, 0);
}
}
来看主函数怎么调用
int ar[] = { 27,15,19,89,85,1,4,85,6,8,9,12};
int n = sizeof(ar) / sizeof(int);
HeapSort(ar, 0, n-1);
for (int i = 0; i < n-1; ++i)
{
printf("%d ", ar[i]);
}
这就 O 了。。。。。
堆排的使用一般在数据量很大的时候其算法复杂度是小于 2n|log 2 n|。看完这你是否掌握了呢?
最后,发起一个长期活动,每15天 (倒计时5天)留言 最多的人送上一本书或企鹅公仔。今日问题:看完这你是否掌握了呢?