最後のリンク: ヒープ+ヒープソートの実装
コンテンツ
前の記事の分析ヒープソート
前回の記事では、バイナリツリーノートを紹介し、最後に単純なヒープソートを実装しました。
アイデア
最初にヒープを作成し、ヒープの上部のプロパティを使用します。最大または最小を選択します
ヒープの一番上の要素を削除するプロパティを使用します。次に大きいまたは次に小さい要素を見つけます
配列を並べ替える
+
時間と空間の複雑さ
挿入と削除の時間計算量はO(logN)であり、最悪の場合は二分木の高さです。
順番に挿入および削除されるため、ノードの数に関連しているため、並べ替えアルゴリズムの時間計算量はO(N * logN)です。
ヒープを最初に作成する必要があり、配列データが挿入され、サイズが配列のサイズに関連しているため、スペースの複雑さはO(N)です。
void HeapSort(int* a,int size)
{
HP hp;
HeapInit(&hp);
//时间复杂度O(N*logN)
for (int i = 0;i<size;i++)
{
HeapPush(&hp,a[i]);
}
HeapPrint(&hp);
int j = 0;
//时间复杂度O(N*logN)
while (!HeapEmpty(&hp))
{
a[j] = HeapTop(&hp);
j++;
HeapPop(&hp);
}
HeapDestroy(&hp);
}
最適化されたヒープソート
最適化の目標:時間計算量O(N * logN)
スペースの複雑さO(1)
以前は、最初にヒープを作成してから配列を挿入していましたが、今回は配列内に直接ヒープを構築して配列をヒープにしたため、ヒープソートアルゴリズムのスペースの複雑さはO(1)になりました。
アレイにヒープを構築するには、次の2つの方法があります。ヒープを調整する
ヒープビルドダウンを調整する
ヒープを構築する
説明を簡単にするために、ここではヒープの上方調整を示します。例として小さなヒープを作成します。
分析する
配列サイズに直接含まれるのは、配列要素の数です。
上向きに調整するときは、開始ノードで終わるツリーがヒープでなければならないことを確認してください
最初の数字はヒープの一番上で、2番目の数字から始まり、上に向かって調整されます。
前から後ろへ、上向きに順番に調整します
ダイアグラム
時間計算量
コード
//向上调整
//建小堆为例
void Up(HPDataType* a,size_t child)
{
size_t 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 HeapSort(int* a,int size)
{
//向上调整建堆
//分析后是件复杂度为O(N*logN)
for (int i = 1;i<size;i++)
{
Up(a,i);
}
}
ヒープビルドダウンを調整する
分析する
下向きに調整するときは、開始ノードがヒープの最上部であるツリーの左側のサブツリーと右側のサブツリーがヒープであることを確認してください。
ヒープの上部として、最初の非リーフノードから下向きに調整します
後ろから前に調整
ダイアグラム
時間計算量
コード
//建小堆为例
void Down(HPDataType* a, size_t parent, size_t size)
{
size_t child = parent * 2 + 1;
while (child < size)
{
if (child + 1 <size && a[child+1] < a[child])
{
child++;
}
if (a[child] < a[parent])
{
swap(&a[child],&a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a,int size)
{
//向下调整建堆
//分析后是件复杂度为O(N+logN)=O(N)
for (int i = (size-1-1)/2; i>=0; i--)
{
Down(a,i, size);
}
}
要約する
ヒープを調整する
時間計算量はO(N * logN)です。ヒープを構築するための下方調整
、時間計算量はO(N)ですしたがって、ヒープを構築するために下方調整を使用する方が効率的です。
選別
昇順でヒープを構築する
降順で小さなヒープを構築します
思考分析
1.注:配列のサイズを上下に変更してヒープにします
ヒープの最上位要素の削除、ヒープへの挿入などの機能インターフェイスは作成されません。
したがって、HeapTopを配列に追加し、HeapPopを使用してヒープの先頭を削除することはできません。
2.一部の友人は、これら2つの関数インターフェースを構築して、それらを再び使用できると言うかもしれません。
いいえ、これを行うと、必然的に新しい配列を開いて、ヒープの最上位要素を配置します。
スペースの複雑さはO(N)になります
最初に構築されたヒープは、ヒープの先頭と末尾で交換および削除されます
最適化の目標を達成していません
3.次に、スペースの複雑さをO(1)にするには、元の配列で並べ替える必要があります
アイデアの並べ替え
1.ヒープのトップノードとエンドノードの要素を交換します
2.ヒープの最初のn-1ノードについて、ヒープの上部から下向きに調整します
3.この時点で、ヒープの一番上の要素は最大または最小の要素です
4.ヒープの一番上の要素をn-1番目の要素と交換します
5.上記のプロセスを繰り返して、昇順または降順を完了します
注:エンドノードの添え字が更新されます
降順で小さなヒープを構築していることの証明
同じ分析は、昇順で大きなヒープを構築することによって実行できます。
結論:昇順で大きなヒープを構築し、降順で小さなヒープを構築します
コード
最初にヒープを調整します(高効率)
iは最初の非リーフノードの添え字です
最後のデータ添え字の終わりを記録します
end = 1の場合、スワップを終了して調整します
注:関数パラメーターを調整します
aは配列の開始アドレスです
parentは、親ノードの添え字です
サイズは調整する要素の数です
void Down(HPDataType* a, size_t parent, size_t size);
次のコードでは、endの意味に注意してください
before whileは、最後の要素の添え字を表します
whileは、調整する要素の数を表します
void HeapSort(int* a,int size) { //升序建大堆 //降序建小堆 for (int i = (size-1-1)/2; i>=0; i--) { Down(a,i, size); } //最后一个数据的下标 size_t end = size - 1; while (end>0) { swap(&a[0],&a[end]); Down(a,0,end); end--; } }
これは、ヒープソートを実行する方法です。
昇順または降順を決定するには、大きなヒープまたは小さなヒープを作成します
小さなヒープは大きなヒープになります。
ヒープを構築するときに、大なり記号または小なり記号を変更できます。
子と子+1、子と親の比較記号
TOP-K問題
TOP-Kの問題とは何ですか?
つまり、データの組み合わせで上位K個の最大要素または最小要素を見つけるには、一般に、データ量が比較的多くなります。
例:プロのトップ10、世界のトップ500、豊富なリスト、ゲームのトップ100のアクティブなプレーヤーなど。
Top-Kの問題について、私が考えることができる最も単純で最も直接的な方法は、ソートすることです。
データ量が非常に多い場合(数十G)、ソートは望ましくなく、メモリは非常に大きくなり、効率は非常に低くなります。
最良の方法は、ヒープソートを使用して解決することです
アイデア
1.データセットの最初のK個の要素を使用して、ヒープの最初のk個の最大要素を作成し、次に
小さなヒープの
最初のk個の最小要素を
作成し、次に大きなヒープを作成します。2.残りのNK要素を使用して比較します。ヒープの上位要素が順番にヒープが小さい場合、ヒープの上部よりも大きい要素がヒープの上部に置き換わります。
ヒープの構造を確認するために調整します
大きなヒープの場合は、ヒープの上部をヒープの上部よりも小さい要素に置き換えます
ヒープの構造を確認するために調整します
順番に比較した後、ヒープはすべてのデータの中で最大または最小の上位K要素です
一度トラバースするだけ
時間計算量
ヒープはKとして設定され、最悪の場合に残っているNKの数を調整する必要があります
調整回数はlogK*(NK)回です
O(K + logK *(NK))
Kのサイズは不確実であり、省略できません
スペースの複雑さ
ヒープを構築するためにKスペースを開く必要があるだけです
わかった)
コード
//TOP-K问题
void PrintTopK(int* a, int n, int k)
{
// 1. 建堆--用a中前k个元素建堆
int* kHeap = (int*)malloc(sizeof(int)*k);
if (kHeap == NULL)
{
printf("malloc fail\n");
exit(-1);
}
//将前k个数插入数组kHeap中
for (int i = 0;i<k;i++)
{
kHeap[i] = a[i];
}
//在数组里面建小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
Down(a, i, k);
}
// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
for (int i = k;i<n;i++)
{
if (a[i]>kHeap[0])
{
kHeap[0] = a[i];
Down(kHeap,0,k);
}
}
// 3. 打印最大或最小的前k个
for (int j = 0;j<k;j++)
{
printf("%d ",kHeap[j]);
}
printf("\n");
free(kHeap);
}
ランダムデータテスト
100000以内の乱数を生成し、100000内の10個の乱数位置を100000より大きい数値に変換します
10,000個の数字から最大の10個の数字を見つける
コードを実行する
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[2335] = 1000000 + 6; a[9999] = 1000000 + 7; a[76] = 1000000 + 8; a[423] = 1000000 + 9; a[3144] = 1000000 + 10; PrintTopK(a, n, 10); } int main() { TestTopk(); return 0; }
演算結果:
最大10個の番号を取得できますが、順序付けされていません
ソーターを追加する
//TOP-K问题
void PrintTopK(int* a, int n, int k)
{
// 1. 建堆--用a中前k个元素建堆
int* kHeap = (int*)malloc(sizeof(int)*k);
if (kHeap == NULL)
{
printf("malloc fail\n");
exit(-1);
}
//将前k个数插入数组kHeap中
for (int i = 0;i<k;i++)
{
kHeap[i] = a[i];
}
//在数组里面建小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
Down(a, i, k);
}
// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
for (int i = k;i<n;i++)
{
if (a[i]>kHeap[0])
{
kHeap[0] = a[i];
Down(kHeap,0,k);
}
}
// 3. 排序
//最后一个数据的下标
size_t end = k - 1;
while (end>0)
{
swap(&kHeap[0], &kHeap[end]);
Down(kHeap, 0, end);
end--;
}
// 4. 打印排序后的前k个
for (int j = 0;j<k;j++)
{
printf("%d ",kHeap[j]);
}
printf("\n");
free(kHeap);
}
運転結果
ヒープソートとTOP-Kの問題に関するメモはここにあります。すべてのパートナーは、コメント領域でコメントを交換できます。!!