数据结构基础——堆的概念、结构及应用
目录
- 堆的概念
- 堆这种数据结构的代码实现
- 堆在排序算法上的应用
1.堆的概念
堆就是以二叉树的顺序存储方式来存储元素,同时又要满足父亲结点存储数据都要大于儿子结点存储数据(也可以是父亲结点数据都要小于儿子结点数据)的一种数据结构。堆只有两种即大堆和小堆,大堆就是父亲结点数据大于儿子结点数据,小堆则反之。
堆的性质: 堆中某个节点的值总是不大于或不小于其父节点的值; 堆总是一棵完全二叉树。
用图画展示就如下图所示:
2.堆这种数据结构的代码实现
了解了堆的概念之后,我们要实际写出一个堆来,先写个小堆为例。 物理结构是用一个数组来存储数据,先定义一个动态增长的数组来。
typedef int Datatype;
typedef struct
{
Datatype* a;
int size;
int capacity;
}heap;
在测试的源文件中创建一个堆
heap hp;
void heapinit(heap* hp);
void heapdestroy(heap* hp);
void heappush(heap* hp, Datatype x);
void heappop(heap* hp);
void heapadjustdown(int* a, int n, int x);
void heapadjustup(int* a, int n, int x);
void swap(int* a, int* b);
void print(heap* hp);
bool heapEmpty(heap* hp);
int heapSize(heap* hp);
Datatype heaptop(heap* hp);
然后在另一个源文件中定义这些函数,并加以详细说明。
void heapinit(heap* hp)
{
assert(hp);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
//往堆中加入数据,物理结构上是不断地在数组尾部加入数据,但加入数据后就要调整数据维持一个小堆或大堆的逻辑结构,要有一个向上调整的函数。如下图所示
void heappush(heap* hp, Datatype x)
{
assert(hp);
if (hp->capacity == hp->size)
{
int newcapacity = (hp->capacity == 0) ? 4 : 2 * hp->capacity;
Datatype* tem = (Datatype*)realloc(hp->a, newcapacity * sizeof(Datatype));
if (tem == NULL)
{
printf("realloc failed\n");
exit(-1);
}
hp->a = tem;
hp->capacity = newcapacity;
}
hp->a[hp->size] = x;
hp->size++;
heapadjustup(hp->a, hp->size, hp->size - 1);
}
void heapadjustup(int* a, int n, int child)
{
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 heappop(heap* hp)
{
assert(hp);
if (heapEmpty(hp))
{
printf("没有数据可删\n");
return;
}
swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
heapadjustdown(hp->a, hp->size, 0);
}
向下调整就跟向上调整有区别了,从根结点开始向下可能有不同的路径,我们要维持住小堆则最小的数在堆顶,大堆则最大的数在堆顶。具体看下图所示
void heapadjustdown(int* a, int n, int parent)
{
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 + parent, a + child);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
后面其它函数的实现
void swap(int* a, int* b)
{
int tem = *a;
*a = *b;
*b = tem;
}
void print(heap* hp)
{
assert(hp);
assert(hp->a);
for (int i = 0; i < hp->size; i++)
printf("%d ", hp->a[i]);
printf("\n");
}
bool heapEmpty(heap* hp)
{
assert(hp);
return hp->size == 0;
}
int heapSize(heap* hp)
{
assert(hp);
return hp->size;
}
Datatype heaptop(heap* hp) /获得堆顶的数据
{
assert(hp);
assert(!heapEmpty(hp));
return hp->a[0];
}
void heapdestroy(heap* hp)
{
assert(hp);
assert(hp->a);
free(hp->a);
hp->capacity = hp->size = 0;
}
3.堆在排序算法上的应用
用堆这种数据结构来找一组数据中最大的或者最小的几个数据是一种比较高效的算法,小堆就是比较大的数据都在堆的下面,如果要找出N个数中最大的K个数,我们可以先用N个数中的K个数建一个小堆,最小的数在堆顶,拿其余N-K个数依次与堆顶数据比较,大于堆顶数据,就拿它更新堆顶数据,在向下调整,比较完这N-K个数后,堆中的K个数就是这N个数中最大的K个数。同理在大堆中最大的数在堆顶,较小的数在堆下面,以同样的规律,比较数据,如果小于堆顶数据就更新堆顶数据,最后留在堆中的K个数就是N个数中最小的数据。 这里来找一下N个数中最大的K个数。
void printTok(int* a, int n, int k);
void testTok()
{
int N = 10000, k = 10;
int* a = (int*)malloc(sizeof(int) * N);
if (a == NULL)
{
printf("malloc fail\n");
exit(-1);
}
srand((size_t)time(0));
for (int i = 0; i < N; i++)
{
a[i] = rand() % 10000;
}
a[24] = 10000 + 5;
a[56] = 10000 + 7;
a[78] = 10000 + 6;
a[5] = 10000 + 12;
a[68] = 10000 + 9;
a[355] = 10000 + 15;
a[789] = 10000 + 1;
a[865] = 10000 + 8;
a[7900] = 10000 + 4;
a[5846] = 10000 + 3;
printTok(a, N, 10);
free(a);
}
void printTok(int* a, int n, int k)
{
int i;
heap hp;
heapinit(&hp);
for (i = 0; i < k; i++)
{
heappush(&hp, a[i]);
}
for (i = k; i < n; i++)
{
if (a[i] > heaptop(&hp))
{
hp.a[0] = a[i];
heapadjustdown(hp.a, hp.size, 0);
}
}
print(&hp);
heapdestroy(&hp);
}
最后代码没问题的话,效果如下图
|