目次
多くの場合、私たちの競争相手は他人ではなく、私たち自身です。
1. 二分木の逐次構造
2. ヒープの概念と構造
理解:ヒープは大ヒープと小ヒープに分けられます。大ヒープ/大ルート ヒープ: ツリー内の親のデータが子以上です。小ヒープ/小ルート ヒープ: ツリー内の親のデータです。ツリーは子以下です
ヒープによって解決される問題: ヒープソート、TOP-K
3、ヒープの実装
heap.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
size_t size;
size_t capacity;
}HP;
void HeapInit(HP* php);
void HeapDestory(HP* php);
void HeapPrint(HP* php);
void Swap(HPDataType* pa, HPDataType* pb);
void HeapPush(HP* php, HPDataType x);
void HeapPop(HP* php);
bool HeapEmpty(HP* php);
size_t HeapSize(HP* php);
HPDataType HeapTop(HP* php);
heap.c
#include "heap.h"
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
void HeapDestory(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
//按数组打印
void HeapPrint(HP* php)
{
assert(php);
for (size_t i = 0; i < php->size; ++i)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
void Swap(HPDataType* pa, HPDataType* pb)
{
HPDataType tmp = *pa;
*pa = *pb;
*pb = tmp;
}
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
//多少个数据
size_t HeapSize(HP* php)
{
assert(php);
return php->size;
}
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
void AdjustUp(HPDataType* a, size_t child)
{
size_t parent = (child - 1) / 2;
//这个比较取决于大小堆
//小堆
//最后一次比较,是parent是0,进行比较,当再次进行调整后。就不需要进行了,此时的child等于0,parent也是0[因为size_t是正整数】
//-1/2还是等于0
while (child > 0)
{
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;//跳出循环
}
}
}
void HeapPush(HP* php, HPDataType x)
{
assert(php);
数据插入数组后
//先判断是否有地方进行扩容
if (php->size == php->capacity)
{
size_t newCapacity = php->capacity == 0 ? 4 : (2 * (php->capacity));
//开辟空间,要有一个临时变量进行开辟,否则如果开辟失败,里面的数据就都找不到了
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
(php->size)++;//先插入,后size++,此时size这个下标的位置并没有值
向上调整的算法,成为堆
size_t child = (php->size) - 1;
AdjustUp(php->a, child);
}
ヒープ挿入: まず配列の末尾に数値を挿入します [この数値を挿入すると、ヒープの概念が満たされなくなる可能性があります]。次に、ヒープが満たされるまで上方調整アルゴリズムを実行します。
void AdjustDown(HPDataType* a, size_t root, size_t size)
{
//找出小的
//注意:可能没有右孩子
size_t parent = root;
size_t child = parent * 2 + 1;
while (child < size)
{
//避免越界
if (child + 1 < size && a[child] > a[child + 1])
{
child++;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;//跳出循环
}
}
}
void HeapPop(HP* php)
{
assert(php);
//当删除数据的时候,要判断有没有值
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, 0, php->size);
}
ヒープの削除: ヒープの削除は、ヒープの先頭のデータ[最小または最大のデータ] を削除し、ヒープの先頭のデータを最後のデータと交換し、配列内の最後のデータを削除します。その後、下方調整アルゴリズムを実行します。[最初に送信し、後で削除し、アルゴリズムを下方調整する]
下方調整アルゴリズム: 2 つの子ノードのうち小さい (大きい) 方を見つけて、それを親ノードと比較して交換します。親ノードのデータは常に以下 (以上) です。子ノード、そして交換された子ノードから下方に比較]
ヒープの挿入と削除の時間計算量は O(logN) です
4 番目に、ヒープの適用
4.1 ヒープソート
ヒープソートは ヒープの考え方を 使用してソートすることであり、次の 2 つのステップに分かれています。1. ヒープを構築します (配列上にヒープを構築すると、ヒープ ソートの空間計算量は O(1) になります)昇順: 大きな山を構築する降順: 小さなヒープを構築します2. ヒープ削除の考え方を利用してソートする
4.1.1 ヒープの構築
ヒープを構築するには 2 つの方法があります。 (1) 上方に調整してデータを挿入してヒープを構築するというアイデアを使用します。新しい配列へのデータの挿入は、連続挿入のプロセスでソートを実現するために上方調整を行うことです [コード 1] (2) 下方調整を使用します [最後から 2 番目の非リーフ ノード、つまり最後のノードの父親から開始します。 (サイズ -1-1)/2] [親ノードを見つけて下方向にソートし、次に親ノードを 1 つ減らして [それぞれの小さなヒープを見つけ]、それを 1 つずつソートすると、ヒープになります。】【コード2】
[ヒープの構築後、配列をヒープにすることができます]
コード 1 は次を示します。
void Swap(HPDataType* pa, HPDataType* pb)
{
HPDataType tmp = *pa;
*pa = *pb;
*pb = tmp;
}
void AdjustUp(HPDataType* a, size_t child)
{
size_t parent = (child - 1) / 2;
//这个比较取决于大小堆
//小堆
//最后一次比较,是parent是0,进行比较,当再次进行调整后。就不需要进行了,此时的child等于0,parent也是0[因为size_t是正整数】
//-1/2还是等于0
while (child > 0)
{
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;//跳出循环
}
}
}
void HeapSort(int* a, int n)
{
//升序,建大堆,向上
size_t i = 0;
for (i = 1; i < n; ++i)
{
AdjustUp(a, i);
}
}
int main()
{
int a[] = { 4, 3, 10 , 2, 5, 9 };
HeapSort(a, sizeof(a) / sizeof(int));
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
コード 2 は次を示します。
void HeapSort(int* a, int n)
{
//升序,建堆,向上
/*int i = 0;
for (i = 1; i < n; ++i)
{
AdjustUp(a, i);
}*/
//向下
int i = 0;
for (i = (n - 2) / 2; i >= 0; --i)
{
AdjustDown(a, i, n);
}
}
ヒープ構築の時間計算量:
ヒープの構築: まず、各層のノードの数は 2^(h-1) です。ヒープの構築は 2 番目の層からデータを挿入することであり、2 番目の層には 2^(2-1) 個のノードがあります。ヒープとなり上昇 最悪の調整数は 2^(2-1)*1、第 3 層には 2^(3-1) 個のノードがあり、ヒープとなり、上方調整数は 2^ になります(3-1)*2;… ...; 次に、累積ヒープ数を 2^(2-1)*1+2^(3-1)*2+2^(4-1)* まで調整します。 3+...+2^(h-1) *(h-1)。これは等差数列 * 等比数列です。転位減算を使用すると、計算回数は 2^h*(h-2)+2 になり、最終的な時間計算量は O(N*logN) になります。
ヒープを下方向に構築します: まず、各層のノードの数は 2^(h-1) です; ヒープは (最後から 2 番目の非リーフ ノードから開始して) から構築されます [この非リーフ ノードは必ずしも最後のノードではありません最後から 2 番目のレイヤー 1 ですが、この時点では、ヒープはフルレベルのバイナリ ツリーと見なすことができます [2 つの時間計算量には大きな違いはありません]。このときの非リーフ ノードは、次のレイヤー 1 の最後のノードになります。 [最後から 2 番目の層] 最後から 2 番目の層は下方調整を開始し、最初の層の下方調整が終了するまで、各層は 2^(h-1) 個のノードを持ち、各ノードと下部がヒープになり、最悪の数のノードが存在します。各ノードの下方調整は 2^(h -1)*(h); 次に、ヒープ構築の累積数を 2^0*(h-1)+2^1*(h-2)+2^ に下方調整します。 2*(h-2)+…+ 2^(h-2)*1、これは等差数列 * 等比数列です。転位減算を使用すると、2^h-1=N であるため、回数は 2^h-1-h として計算でき、最終的な時間計算量は O(N) になります。
要約: ヒープを下方向に構築するのが最善です
ヒープの構築:大きなヒープは昇順で構築し、小さなヒープは降順で構築します。[小さなヒープを昇順に構築する場合、最小の数値はすでに最初の位置にあり、次に最小の数値を継続的に構築して選択する必要があります。この場合、合計の時間計算量は O(N^2) になります。この場合、選択範囲を直接走査する方が良いため、時間計算量も O(N^2) になります] [昇順で大きな山を構築する必要があります]
4.1.2 ヒープ削除アイデアを使用したソート
昇順、大規模ヒープの例: 大規模ヒープを構築した後、最大値が先頭になり、最大値と最後の値[添え字はn-1]が入れ替わり、その後、関係なくヒープが構築されます。添字 n-1 の最大値を最後の値と再度交換します [添字は n-2]。配列は、添字 0 の要素が添字 1 の要素と交換されるまでソートされます。[時間計算量: O(N*logN)]
void HeapSort(int* a, int n)
{
//升序,建堆,向上
/*int i = 0;
for (i = 1; i < n; ++i)
{
AdjustUp(a, i);
}*/
//向下
int i = 0;
for (i = (n - 2) / 2; i >= 0; --i)
{
AdjustDown(a, i, n);
}
size_t end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, 0, end);
--end;
}
}
4.2 TOP-K問題
N 個の数値で最大/最小の上位 K 個を検索します
時間計算量は O(K+logK*(NK))、空間計算量は O(K) です。
void PrintTopK(int* a, int n, int k)
{
// 建堆--用a中前k个元素建堆
int* kminHeap = (int*)malloc(sizeof(int) * k);
if (kminHeap == NULL)
{
printf("malloc fail \n");
exit(-1);
}
//前k个元素,放在数组里面
for (int i = 0; i < k; ++i)
{
kminHeap[i] = a[i];
}
// 建小堆
for (int j = (k - 2) / 2; j >= 0; --j)
{
AdjustDown(kminHeap, j, k);//k指的是下标,数组最后元素的下标,为了方便找到父节点
}
// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
for (int i = k; i < n; ++i)
{
if (a[i] > kminHeap[0])
{
kminHeap[0] = a[i];
AdjustDown(kminHeap, 0, k);
}
}
for (int j = 0; j < k; ++j)
{
printf("%d ", kminHeap[j]);
}
printf("\n");
free(kminHeap);
}
void TestTopk()
{
int n = 10000;
int* a = (int*)malloc(sizeof(int) * n);
srand(time(0));
for (size_t i = 0; i < n; ++i)
{
a[i] = rand() % 1000000;
}
a[5] = 1000000 + 1;
a[1231] = 1000000 + 2;
a[531] = 1000000 + 3;
a[5121] = 1000000 + 4;
a[115] = 1000000 + 5;
a[2305] = 1000000 + 6;
a[99] = 1000000 + 7;
a[76] = 1000000 + 8;
a[423] = 1000000 + 9;
a[0] = 1000000 + 1000;
PrintTopK(a, n, 10);
}