博主收集的资料,连载中。
转载请标明出处:New Young
本文介绍 堆—-一种特殊的完全二叉树.
如果完全二叉树中,所有的
父亲结点
的值都比其左右孩子结点
的值大或者小,这种完全二叉树称为堆
,
- 其中将父亲结点大于孩子结点的称为大堆.
- 父亲结点小于孩子结点的称为小堆.
- 堆是一种
特殊的
完全二叉树- 根结点的值是最值.
- 堆中某个节点的值总是不大于或不小于其父节点的值 ,即大的值或者小的值总是沉到更下层.
堆是一种
完全二叉树
,为了避免不必要的空间浪费,一般用==顺序存储(数组)==的形式来存储数据.typedef int HPDataType; typedef struct Heap { HPDataType* _a; int _size; int _capacity; }Heap;
- 堆的很多功能(堆的创建,堆排序,TopK问题等)都需要向上或者向下调整算法,因此先介绍这2个,再介绍其它功能。
//向上调整。 void Adjustup(HPDataType* a, int child) { assert(a); int parent = (child - 1) / 2;//无论左右孩子,都可以. while (child>0) { if (a[child] < a[parent]) { Swap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } }
- 从根结点向下调整前,
左右子树
必须满足堆要求—-堆中某个节点的值总是不大于或不小于其父节点的值。//向下调整 void Adjustdown(HPDataType* a, int n, int parent) { assert(a); int child = 2 * parent + 1; while (child < n)//完全二叉树的特点,当无左子树时,代表达到领界或者越界。 { if ( child+1<n && (a[child+1] < a[child])) { child ++; } if (a[child] < a[parent]) { Swap(&a[child], &a[parent]); parent = child; child = 2 * parent + 1; } else { break; } } }
算法 | 特点 | 使用的范围 | 联系 |
---|---|---|---|
向上调整 | 从堆的尾开始的调整,结点总是在从根结点到该结点的路径上移动 | 堆的插入(Push),堆的创建(Create), | 二者没然的联系,主要看我们的使用角度 |
向下调整 | 从堆的头开始的调整,调整前,根结点的左右子树必须满足堆的要求。 | 堆的删除(Pop),堆的创建(Create),TopK问题,堆排序 | |
堆的插入从
尾部
进行插入。// 堆的插入 void HeapPush(Heap* hp, HPDataType x) { assert(hp); HeapIsExpandCapacity(hp); hp->_a[hp->_size++] = x; //因为插入元素前,仍是一个堆,只需要要向上调整,就可以让插入的元素走到正确的位置 Adjustup(hp->_a,hp->_size-1); }
堆的删除方式很特殊:
先将根结点与尾结点进行交换,之后从根结点进行向下调整,直到尾结点前
/ 堆的删除,堆的删除删的是跟结点 void HeapPop(Heap* hp) { assert(hp); Swap(&hp->_a[0], &hp->_a[hp->_size - 1]); --hp->_size; Adjustdown(hp->_a, hp->_size, 0); }
//是否扩容 void HeapIsExpandCapacity(Heap* hp) { if (hp->_size == hp->_capacity) { hp->_capacity = hp->_capacity == 0 ? 4 : 2 * (hp->_capacity); HPDataType* tmp = (HPDataType*)realloc(hp->_a, sizeof(HPDataType)*hp->_capacity); if (tmp == NULL) { printf("Expand Is Failure\n"); exit(-1); } hp->_a = tmp; } }
堆的创建有2种主要形式:
- 通过向上调整的算法,–直接在给定数组上进行建队,
- 通过向下调整的算法,通过给定数组的数据,动态规划,重新建立堆。
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
assert(hp);
//方式一:不直接在数组上操作,重新构建一个堆
for (int i = 0; i < n; ++i)
{
HeapPush(hp, a[i]);
}
printf("向上调整结果\n");
HeapPrint(hp->_a, hp->_size);
}
- 只有一个结点的堆也堆,因此我们从下标1开始插入堆,然后向上调整,重复这个过程直到建堆完成。
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
assert(hp);
思路:只有一个结点也是堆,通过插入,向上调整,建堆。
for (int i = 1; i < n; ++i)
{
Adjustup(a, i);
}
HeapPrint(a, n);
}向下调整法
因为堆是一种递归结构,这表明如果我们再向下调整整个堆前,可以先调整它的左右子树,通过这种递归,最终我们需要从非叶结点的父亲结点回推
//思路:只有一个结点也是堆,通过插入,向下调整,建堆。
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
assert(hp);
for (int parent = (n - 1 - 1) / 2; parent >= 0; --parent)
{
Adjustdown(a, n, parent);
}
}
void RecDown(HPDataType* a, int n, int parent)
{
assert(a);
int child = 2 * parent + 1;
if (child >= n )
{
Adjustdown(a, n, parent-1);
return;
}
else
{
RecDown(a, n, parent + 1);
Adjustdown(a, n, parent);
}
}
我们再删除堆操作中,先将头尾交换,之后对头向下调整。再结合堆的根结点是
最值
.如果我们每次都将堆的根结点与尾交换,然后将尾前面就行向下调整,这样就可以完成排序。如果想
降序
,因为最大值
在最后,所以要求第一个根结点
是最大值,这就要求我们建大堆,让根结点
成为最大值。反之,升序,建小堆
。
// 降序,建小堆,跟结点必然是最小结点,
//所以首尾交换后,跟的左右子树仍是关系未破坏的堆,
//所以可以通过向下调整的方式,再次选出最小的结点,
//重复改过程。
void HeapSort(HPDataType* a, int n)
{
assert(a);
//建堆
//
//
//
//方式一:向上调整算法
//for (int i = 1; i < n; ++i)
//{
// Adjustup(a, i);
//}
//HeapPrint(a, n);
//方式二,非递归向下调整算法。
//方式二:在数组上操作
//思路:只有一个结点也是堆,通过插入,向下调整,建堆。
//for (int parent = (n - 1 - 1) / 2; parent >= 0; --parent)
//{
// Adjustdown(a, n, parent);
//}
//printf("向下调整结果\n");
//HeapPrint(a, n);
方式三,递归向下调整算法建堆
//
//递归法建堆
printf("递归向下调整结果\n");
RecDown(a, n, 0);
for (int i = 0; i < n-1; ++i)
{
Swap(&a[0], &a[n -1- i ]);
Adjustdown(a, n-1-i, 0);
}
}
问题:如果有10亿个数,找出其中最大的前10。
首先10亿个数,只能将他们先存到文件中,之后取和适数量放到内存中。
这里为了方便,假设文件存放了N(我实际假设环境是100个数)个数,一次取t个数存入内存,需要K次。
如果我们用排序的思想—-这里用选择法,时间复杂度为:0(K*N^2)
发现很大,效率不高。
我们观察
堆
,发现,堆的根结点是最值
,根以下的结`点`都大于或者小于根结点,如果我们将除了==建堆以外==的数据与==根结点==比较,如果
大于
根结点,就替换
根结点,同时进行向下调整,这样的话大数
就沉到下面了,而最小的,次小等等都会因为根结点而被替换调。如果要找最大的前K个,那么必须建小堆,如果建大堆,如果根结点恰好是最大的一个,这就会Creash。但是建小堆就不用担心,根结点是最小的,不会Creash。
int main ()
{
srand(time(NULL));
FILE* pf = fopen("C:\\Users\\Administrator\\Desktop\\test.txt","w");
assert(pf);
for (int i = 0; i < 100; ++i)
{
fprintf(pf, "%d ", rand()%100);
}
return 0;
}
//找最大的 k个,则建立小堆,因为大堆可能会导致最大的丢失。
//先去文件中的前N个放到数组中,建成小堆,之后继续将数据重新放到数组中,然后依次比较堆根结点。直到EOF。
void TopK( int n, int k)
{
FILE* pf = fopen("C:\\Users\\Administrator\\Desktop\\test.txt", "r");
assert(pf);
Heap hp;
HeapInit(&hp);
int* a = (int*)calloc(k ,sizeof(int));
assert(a);
//建小堆
for (int i = 0; i < k; ++i)
{
if (feof(pf))
{
break;
}
else
{
fscanf(pf, "%d ", &a[i]);
}
}
HeapPrint(a, k);
//这里不能在原数组上建堆。动态建堆
HeapCreate(&hp, a, k);
printf("建的小堆:");
HeapPrint(hp._a, hp._size);
while (!feof(pf))
{
int i = 0;
for ( i = 0; i < k; ++i)
{
if (feof(pf))
{
break;
}
else
{
fscanf(pf, "%d ", &a[i]);
}
}
for (int j = 0; j < i; ++j)
{
if (a[j] > HeapTop(&hp))
{
hp._a[0] = a[j];
Adjustdown(hp._a, k, 0);
}
}
}
HeapPrint(hp._a, hp._size);
HeapDestory(&hp);
free(a);
a = NULL;
}