[データ構造] ツリーと二分木の概念、二分木の逐次構造と実装

序文:

シーケンス リスト、リンク リスト、スタック、キューのデータ構造について学習しましたが、これらのデータ構造はすべて線形 (1 対 1) です。次に、非線形データ構造 - ツリー (二分木)を学習する必要があります。前のものと比較して、ツリーの構造はより複雑です。早速本題に入りましょう。

1. ツリーの概念と構造

1.木の概念

ツリーは非線形データ構造であり、 1 対多 ( 1 対 1 の関係であることも可能です)。これは、n (n>=0) 個の限定されたノードで構成される階層関係のセットです。逆さまの木のように見えるため、つまり根が上を向いている 葉は下を向いています

には、 ルート ノードと呼ばれる 特別なノードがあります。ルート ノードには、ルート ノード以外の先行ノードがありません
。 < a i=3>残りのノードは M (M>0) 個の素なセット T1、T2、...、Tm に分割されます。、各セット Ti (1<= i <= m) は、ツリーと同様の構造を持つ別のサブツリーです。 各サブツリーのルート ノードには先行ノードが 1 つだけあり、0 個以上の後続ノードを持つことができるので、ツリーは再帰が定義されました。

現実のツリーとデータ構造内のツリー。次のようなものです。
ここに画像の説明を挿入します

注: ツリー構造では、サブツリー間に交差があってはなりません。交差しないと、ツリー構造になりません。
ここに画像の説明を挿入します

2. 樹木に関する概念

樹木の関連概念は、樹木の根、枝、葉などのつながりを指し、人間間の関係に似ています。
ここに画像の説明を挿入します

ノードの次数: ノードに含まれるサブツリーの数をノードの次数と呼びます。上の図に示すように、A は 6 のペアであるため、 A の次数は 6
リーフ ノードまたはターミナル ノード: 次数 0 のノードはリーフ ノードと呼ばれます; 上の図に示すように: B、C、H、I... は葉です。 ノード
非終端ノードまたは分岐ノード: 0 以外の次数を持つノード。上の図に示すように: D、E、F、G... およびその他のノードはブランチ ノードです
親ノードまたは親ノード: ノードに次のものが含まれる場合子ノードの場合、このノードはその子ノードの親ノードと呼ばれます。上の図に示すように: A は B の親ノード
子ノードまたは子ノードです。 : ノードに含まれるサブツリーのルート ノードは、ノードの子ノードと呼ばれます; 上の図に示すように: B は A の子ノード
兄弟ノード: 同じ親ノードを持つノードは兄弟ノードと呼ばれます。上に示すように: B と C は兄弟ノードです
ツリーの次数:ツリー、最大のノードの次数はツリーの次数と呼ばれます。上の図に示すように、ツリーの次数は 6
ノードです。
ツリーの高さまたは深さ < /span> の子孫です。m (m>0) 個の互いに素なツリーの集合はフォレストと呼ばれます。フォレスト のすべてのノード: ノードをルートとするサブツリー内の任意のノードは、このノードの子孫と呼ばれます。上に示すように、すべてのノードは A子孫: ノードによって渡されるルートからブランチまで すべてのノード。上の図に示すように: A は祖先ノードの祖先です。 : 両方の親が同じです レイヤー内のノードは互いにいとこです; 上の図に示すように: H と I は互いにいとこノードです
カズン ノード: ツリー内のノードの最大レベル。上に示すように: ツリーの高さは 4


: ノード レベルを計算する場合、最上位のノードから数えて 0 または 1 から開始できますが、1 から開始することをお勧めします。
ツリーは空のツリー、ノードが 1 つだけあるツリー、または複数のノードを持つツリーである可能性があるため、計算が 0 から始まる場合、ノード レベル (またはツリーの高さ) ) は 0 である必要があります。空の木なのか、ノードが 1 つしかない木なのかを区別するのは難しいため、木の高さを数えやすくするために、1 から開始する方が良いです。

ここに画像の説明を挿入します

3. ツリーストレージ

ツリー構造は線形テーブルに比べて複雑で、保存や表現が面倒です。値の範囲とノード間の関係の両方を保存する必要があります。 . 関係、実際には、親表現、子表現、子親表現、子兄弟表現など、ツリーを表現する方法はたくさんあります。ここでは、 最も一般的に使用される子の兄弟表現について簡単に説明します。 (左子右兄弟表記とも呼ばれます)

struct TreeNode
{
    
    
	int val;
	struct TreeNode* firstchild;
	struct TreeNode* nextbrother;
};

ノードは複数のノードを任意に指すことができます。このノードの firstchild ポインタは最初の子ノードを指し (そうでない場合は null ポインタを指します)、nextbrother ポインタはその兄弟ノードを指します (そうでない場合は、ヌルポインタ)。

ここに画像の説明を挿入します
最初に最初の子を検索し (たとえば、B は A の最初の子です)、次にこの時点で最初の子を検索して、NULL ポインターで終わるまで兄弟を検索します。

TreeNode* アノードはノード A であると想定されます
A の最初の子であるノード B を見つけます: TreeNode* child = Anode->firstchild;
while(child) が空になるまで
{ ………… child = child->nextbrother; }


ここに画像の説明を挿入します

4. 実際の木の適用

私たちが普段コンピュータのファイルシステムで使用しているディレクトリはツリー構造になっており、このコンピュータを開くとDドライブ、Cドライブなどがあり、それぞれの階層に複数のファイルがあります。
ここに画像の説明を挿入します

2. 二分木の概念と構造

1.コンセプト

バイナリ ツリーはノードの有限セットです。
1. 最大 2 つのノード、または 1 または 0
2. ルート ノードと 2 つのエイリアスは < A i=5>left と呼ばれます。サブツリー右側のサブツリーは二分木で構成されています。

ここに画像の説明を挿入します
上の図からわかるように、二分木には次数が 2 より大きいノードは存在せず、二分木の部分木は左右の部分木に分割でき、順序を逆にすることはできません。したがって、バイナリ ツリーは順序付けされたツリーです。

注: バイナリ ツリーは次の状況で構成されます。
ここに画像の説明を挿入します
実際のバイナリ ツリー:
ここに画像の説明を挿入します

2. 特殊な二分木

(1) 完全なバイナリツリー

コンセプト: バイナリ ツリー (各層のノード数が最大値に達した場合) 、その後、このバイナリ ツリーは完全なバイナリ ツリーです。つまり、二分木のレベル数が h で、ノードの総数が 2^h - 1 であれば、それは完全な二分木になります。
ここに画像の説明を挿入します

各レイヤーはいっぱいです: 2^(i-1) ノード (i は特定のレイヤーの位置)
F(h) = 2 ^ 0 + 2 ^ 1 + ……+2 ^ (h-2) + 2 ^ (h-1)等比数列の和

このバイナリ ツリーには N 個のノードがあると仮定します
N = 2 ^ h - 1 => h = log(N+1) (log は 2 に基づきます)

(2) 完全な二分木

コンセプト: 完全なバイナリ ツリーは、完全なバイナリ ツリーに基づいて変更されます。高さが h であるとすると、 最初の h-1 層はいっぱい 、最後の層はいっぱいか満たされていない可能性があり、 その最後のレイヤーは左から右に連続している必要があります。完全なバイナリ ツリーは、特殊なタイプの完全なバイナリ ツリーです。

ここに画像の説明を挿入します
したがって、完全なバイナリ ツリー内のノードの総数には範囲があります。完全な二分木の場合、つまりノードの総数が最大の場合、ノードの総数は 2^h - 1 になります。最後の層にノードが 1 つしかない場合、つまり、ノードの総数が 2^h - 1 になります。ノードの総数は 2 ^ (h-1) であるため、ノード範囲は [2 ^ (h-1), 2 ^ h - 1] となります。

3. 二分木の性質

1. ルート ノードのレベル数が 1 の場合、空ではないバイナリ ツリーの h 番目のレベルには最大 2^(h-1) 個のノードがあります。 ;
2. ルート ノードのレベルが 1 の場合、深さ h のバイナリ ツリーには最大 2^h-1 個のノードがあります。< /span>完全なバイナリ ツリーの深さは次のようになります。 n ノード、h=log(n+1 ) 4. ルート ノードのレベル数が 1 の場合、n0=n2+ 1;
3. 任意の二分木で、次数が 0、葉ノードの数が n0、次数 2 の枝ノードの数が n2 であれば、式は満たされます。

4. バイナリツリーの保存

バイナリ ツリーの保存方法は、シーケンシャル ストレージチェーン ストレージに分けられます。 。

(1)順次保存

シーケンシャル構造ストレージは配列を使用して保存します。一般に、配列は完全なバイナリ ツリーを表す場合にのみ適しています。バイナリ ツリーを使用すると、スペースが無駄になります。 バイナリ ツリーの順次ストレージは、物理的には配列であり、論理的にはバイナリ ツリーです。

配列ストレージを使用する場合には次のルールがあります。
ここに画像の説明を挿入します

任意の位置に添え字を付けることで、父または子を見つけることができます。
注: このルールは、完全なバイナリ ツリーまたは完全なバイナリ ツリーが必要な場合にのみ使用できます。

ここに画像の説明を挿入します
したがって、不完全なバイナリ ツリーの場合は、連鎖構造ストレージを使用する方が適しています。

概要: 完全なバイナリ ツリーまたは完全なバイナリ ツリーは配列ストレージに適しています

(2)チェーンストレージ

バイナリ ツリーのリンク構造は リンク リストで表され、3 つのスコープ (データ ドメインと左右のポインタ ドメイン) に分割されます の場合、左右のポインタは、左右の子のアドレスを格納するために使用されます。
ここに画像の説明を挿入します

3. 二分木の逐次構造と実装

1. 二分木の逐次構造

通常のバイナリ ツリーは、無駄なスペースが大量に存在する可能性があるため、配列での保存には適していません。。完全なバイナリ ツリーは、順次構造の保存に適しています。 実際には、通常、シーケンシャル構造の配列を使用してヒープ (バイナリ ツリー) を保存します。 注意が必要なのは、ヒープとオペレーティング システムの仮想化です。プロセス アドレスはここにあります。空間内のヒープは 2 つの異なるものです。1 つはデータ構造で、もう 1 つはメモリを管理するオペレーティング システムの領域分割です。

2. ヒープの概念と構造

ヒープ: 非線形構造を持つバイナリ ツリー、より正確には完全なバイナリ ツリー。アレイストレージに適しています。

ヒープは 2 つのタイプに分類されます。最大のルート ノードを持つヒープは最大ヒープまたはラージ ルート ヒープと呼ばれ、最小のルート ノードを持つヒープは最小ヒープまたはスモール ルート ヒープと呼ばれます。
小さなヒープ: ツリー内のすべての父親は <= 子です (比較はノードの値です)
大きなヒープ: ツリー内のすべての父親は子です> =子供

ここに画像の説明を挿入します

3. ヒープの実装

(1) ヒープの初期化

ヒープを初期化するには 2 つの方法があります。
最初の方法: 構造体が指す配列を空 (要素なし) にし、実効数と容量を 0 に設定します。

void HPInit(HP* php)
{
    
    
	assert(php);
	php->a = NULL;
	php->capacity = 0;
	php->size = 0;
}

2 番目のタイプ: 外部配列のサイズ n を受け取り、サイズ n のスペースを動的に開き、構造体が指す配列にデータをコピーします。

void HPInitArray(HP* php, HPDataType* a, int n)
{
    
    
	assert(php);
	assert(a);
	php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (php->a == NULL)
	{
    
    
		perror("malloc fail");
		exit(-1);
	}
	php->size = n;
	php->capacity = n;
	memcpy(php->a, a, sizeof(HPDataType) * n);
	for (int i = 1; i < n; i++)
	{
    
    
		AdjustUp(php->a, i);
	}

AdjustUp は上方調整アルゴリズムであり、以下で分析します。

(2) ヒープを破壊する

言うまでもなく、ヒープの破壊はシーケンステーブルの破壊と同じである。

void HPDestroy(HP* php)
{
    
    
	assert(php);
	free(php->a);
	php->a = NULL;
	php->capacity = php->size = 0;
}

(3) ヒープへの挿入

ヒープへの挿入は、配列の末尾挿入の方が効率が良いため、配列表の末尾挿入(1つずつ挿入)を採用しています(後ほど使用する末尾削除もあります)。初期化時の領域サイズは0であったため、データを挿入する場合は拡張を考慮する必要があります。ここでは、変数 newcapacity を設定し、条件演算子を使用できます。容量が 0 の場合は、初期容量に 4 を与えます。それ以外の場合は、元の容量の 2 倍になります。展開後、新しいデータが挿入され、要素数は ++ です。

void HPPush(HP* php, HPDataType x)
{
    
    
	assert(php);
	if (php->size == php->capacity)
	{
    
    
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* ptr = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
		if (ptr == NULL)
		{
    
    
			perror("realloc fail");
			exit(-1);
		}
		php->a = ptr;
		php->capacity = newcapacity;
	}
	php->a[php->size] = x;
	php->size++;
	AdjustUp(php->a, php->size - 1);//向上调整
}

外部データを 1 つずつ挿入するだけで十分ですか?実際にはそうではありません。データを挿入した後、 構造体が指す配列をヒープ に変換する必要があります。アルゴリズムを紹介します。

アルゴリズムを上方調整する

最後のリーフ ノードから開始して上向きに調整します。小さなヒープを構築する場合を例として、最後のリーフ ノードとして新しいデータを挿入します。小さなヒープの特徴は、親ノード <= 子ノードであるため、最初に親ノードを見つける必要があります。

公式:parent = (child - 1) / 2

ヒープの実装はバイナリ ツリーですが、その格納方法は基本的に配列であり、空間は連続しているため、子ノードの添え字を通じて親ノードを見つけることができることがわかります。このとき、子ノードと親ノードを比較することができ、親ノードが子ノードより大きい場合は交換され、子ノードは親ノードの上位の位置に達し、元の親ノードは次の位置に達します。親ノードの位置。子ノードの範囲の制御に注意してください。子ノードをヒープの最上位要素にすることはできません。そうでない場合は、範囲外になるため、child>0 となります。親ノードと子ノードの大小関係が小ヒープの特性を満たす場合、ループが飛び出し、配列は小ヒープになります。
ここに画像の説明を挿入します

上方調整の前提条件: 配列はもともと小さいヒープまたは大きいヒープです

この場合、データを挿入する前に、配列が小さいヒープであるか大きいヒープであるかをどのように判断すればよいでしょうか?実はここでポインタが指す配列にはもともと何もなく、1つずつデータを挿入し、データを挿入するたびに調整していたのですが、これは次のデータを挿入したいときに、前のデータは小さなヒープに調整されています。次のデータを挿入するときに、このデータとその親ノードの関係を調整するだけです。

void AdjustUp(HPDataType* a, int child)
{
    
    
	int parent = (child - 1) / 2;
	while (child > 0)
	{
    
    
		if (a[parent] > a[child])
		{
    
    
			Swap(&a[parent], &a[child]);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
		{
    
    
			break;
		}
	}
}

(4) ヒープの削除

ヒープの要素を削除するには、シーケンスリストの末尾削除を使用しますが、ここで注意しなければならないのは、ヒープの最後の要素を削除するだけでは意味がありません。小さなヒープの場合は、ヒープの先頭要素が全要素の中で最も小さくなければならないという特性があるため、ヒープの先頭要素を削除する必要があります。ただし、配列の先頭を削除するにはデータを移動する必要があり面倒なので、ここではヒープの先頭要素と最後の要素を入れ替えて末尾を削除することで最小の要素を削除できるようにしています。ただし、削除後のヒープの先頭にある要素のサイズは不確実であり、現時点では必ずしも小さなヒープであるとは限りません。ここでは別のアルゴリズムを紹介します。

void HPPop(HP* php)
{
    
    
	assert(php);
	assert(php->size > 0);
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;
	AdjustDown(php->a, php->size, 0);
}

アルゴリズムを下方調整する

ヒープの最上位要素から調整を開始し、下方向に調整します。前の予告ではヒープの最上位要素のみが変更されたため、ヒープの最上位要素が親ノードであり、その子ノードは次の式で見つけることができます。

左の子: 子 = 親 * 2 + 1
右の子: 子 = 親 * 2 + 2

そこで問題は、左の子を選ぶべきか右の子を選ぶべきかということです。ここでは、デフォルトで左の子を選択し、判定を追加します。左の子の値が右の子の値より大きい場合、左の子の添字が 1 増加して右の子になります。小さいヒープの親ノードになるように下方調整するときに交換されるノードは、2 つの子ノードのうち小さい方であるためです。次に、親ノードと子ノードの大小関係を比較し、親ノードが子ノードより大きい場合は交換(親ノードと子の交換とは、小さい方の子ノードと交換すること)し、親ノードを交換します。ノードが子ノードの位置に到達し、子ノードが到達する 左の子ノードの位置。条件が満たされない場合はヒープが小さいことを意味し、ループが飛び出します。

注: ここでは詳細を制御する必要があります。子ノードの添え字の範囲は要素の数よりも小さくなります。同時に、親ノードに子ノードが 1 つだけ (左の子のみ) ある場合、左の子の添字に 1 を追加することは範囲外になります。
child + 1 < n - この条件は true で、左側の子のみが存在し、右側の子が存在しないことを示します

ここに画像の説明を挿入します

void AdjustDown(HPDataType* a, int n, int parent)
{
    
    
	int child = parent * 2 + 1;
	while (child < n)
	{
    
    
		if (child + 1 < n && a[child + 1] < a[child])
		{
    
                             
			child++;
		}
		if (a[parent] > a[child])
		{
    
    
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
    
    
			break;
		}
	}
}

(5) ヒープの先頭要素の取得/ヒープ内の要素数/空判定

//获取堆顶元素
HPDataType HPTop(HP* php)
{
    
    
	assert(php);
	assert(php->size > 0);
	return php->a[0];
}
//获取堆的元素个数
int HPSize(HP* php)
{
    
    
	assert(php);
	return php->size;
}
//堆的判空
bool HPEmpty(HP* php)
{
    
    
	assert(php);
	return php->size == 0;
}

4. ヒープソート

ヒープ内のデータを昇順に並べ替える必要があるのですが、順番に出力する方法と、配列をそのまま並べ替える方法があります。

(1) ヒープソートバージョン1

データを 1 つずつヒープに挿入し、配列が空でない限りヒープの先頭要素を取得し、すべての要素が昇順で出力されるまでヒープの先頭要素を削除します。

void HeapSort(int* a, int n)
{
    
    
	HP hp;
	HPInit(&hp);
	int i = 0;
	for (i = 0; i < n; i++)
	{
    
    
		HPPush(&hp, a[i]);
	}
	HPPrint(&hp);

	while (!HPEmpty(&hp))
	{
    
    
		printf("%d ", HPTop(&hp));
		HPPop(&hp);
	}
	HPDestroy(&hp);
}

この方法には次のような欠点があります。
1. 頻繁な拡張によりスペースの複雑さが消費される
2. 最初にヒープ データ構造が必要である

(2) ヒープソートバージョン 2 - 配列を所定の位置にソートします

ヒープのソート後に配列を使用する場合、それを単に出力してソートするのは適切ではありません。したがって、配列の内容を正しい順序に保つために、配列に対してヒープ ソートを実行する必要があります。

例:
元の配列: 65,100,70,32,50,60
並べ替えられた配列: 32,50,60,65 ,70,100

並べ替えの前にヒープを構築する必要があります。では、小さなヒープを構築するべきですか、それとも大きなヒープを構築すべきですか?
最初に小さなヒープを構築しましょう:
昇順を例として、前のヒープの並べ替えを出力し、上方調整方法を使用して小さなヒープを構築しました。ヒープ。ここでも小さなヒープを構築する方法が使用されています。小さなヒープが構築された後、ヒープの先頭にある要素が最も小さいため、この要素が配列内で最初にランク付けされ、次に小さい要素が前からランク付けされます。配列内の要素が整うまで、前から後ろにすべてを昇順に並べます。

しかし、最初に小さなヒープを構築する方法には欠陥があります。
分析: ヒープの先頭にある要素は、ヒープ内の最小の要素です。後列では、データがキューに入れられるたびに、後続のデータから次に小さいデータを見つけなければなりませんが、問題は、その行の次のデータが見つからないことです。必然的に 2 番目に小さい要素となるため、後続のデータに対してヒープを再構築する必要があります。再構築されたヒープの最上位の要素は次に小さいものになります。ヒープを毎回再構築すると時間の複雑さが増大し、効率が非常に低くなります。

上方調整方法で 1 つのデータを走査する時間計算量は次のとおりです: logN
N 個のデータがあるため、ヒープ構築の時間計算量は次のようになります: N * logN これは、小さなヒープを N 回構築することに相当します< /span>
データがキューに入れられるたびにヒープを構築する時間計算量は次のとおりです: N * (N * logN)

ここで大きなヒープを構築する必要があります。大きなヒープの先頭はヒープ内で最大の要素ですが、配列の最初の位置にランクされます。それを配列の最後の位置に到達させるにはどうすればよいでしょうか?

このステップでは、ヒープ削除のアイデアを使用できます。ヒープの先頭にある要素を最後の要素と交換します。この時点で、最大の要素が本来あるべき場所に配置されます。次のステップは非常に重要です。最後の要素を実際に削除しない場合は、最大の要素が削除されます。は最後にランクされます。ここで、配列の最後の要素を指す変数 end を定義できます。交換が完了すると、下方に調整されます。調整範囲は 0 (最初の要素) から end までの有効な数値です。。次に、end が 1 減って、end>0 ループを制御します。これは、最後に要素が 1 つだけあり、調整が必要ないため、これが最小になります。 end が指す要素は調整範囲内にないことに注意してください
ここに画像の説明を挿入します

//建堆
	for (int i = 1; i < n; i++)
	{
    
    
		AdjustUp(a, i);
	}
	//调整
	int end = n - 1;
	while (end > 0)
	{
    
    
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}

(3) 最適化ヒープソートバージョン2と時間計算量の比較

ヒープを構築する上方調整方法と比較して、下方調整方法を使用したヒープ構築の時間計算量は優れています。以前は上方調整方法を使用してヒープを構築しましたが、下方調整方法を使用してヒープを構築するにはどうすればよいでしょうか?

上方調整方法を使用してヒープを構築すると、最後のノードから開始し、その親ノードを見つけて、それを比較してヒープの構築を完了することがわかっています。

実行された調整の合計数: 各レイヤーのデータ数 * 上下に移動したレイヤーの数

上方調整法の時間計算量の計算
図に示すように:
ここに画像の説明を挿入します
下方調整法のヒープ構成:
これまでの下方調整方法は、ヒープの先頭から下方に調整し、順番に比較する方法でした。山の一番上から始めるのではなく、一番下から調整してください。最後の親ノードを見つけて (親ノードに子ノードが必要である限り)、比較して調整し、次に前の親ノードに移動して前の操作を実行し、最後にヒープを構築します。

最後の親ノードを検索します:
最後の子ノード: child = n-1
子がある場合は、父親を検索します: ( child-1)/ 2
したがって、最後の親ノードは次のようになります: (n - 1 - 1) / 2

下方調整方法の時間計算量の計算
図に示すように:
ここに画像の説明を挿入します
比較: 上方調整の総数は、下方調整の合計数 2 ^ (h-1)。これは、計算のもう 1 つの層 (最後の層) があることを意味します。最終層が全体の約半分を占めるため、魔法のスタックを下方に調整するのが良いです。

void HeapSort(int* a, int n)
{
    
    
	//建堆
	//选择向上调整——O(N*log(N))
	/*for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}*/
	//选择向下调整——O(N)
	int i = 0;
	for ( i = (n - 1 - 1) / 2; i >= 0; i--)
	{
    
    
		AdjustDown(a, n, i);
	}
	//调整
	int end = n - 1;
	while (end > 0)
	{
    
    
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}

(4)TopK問題

データが N 個あると仮定して、最大の上位 K 個を見つけます

1. ファイルの最初の K データを読み取り、メモリ配列に小さなヒープを構築します。 2. 次のデータを順番に読み取ります。データ はヒープの先頭要素と比較されます。データがヒープの先頭要素より大きい限り、ヒープの先頭要素をヒープに置き換えます < a i=4> その後、下方に調整します 3. すべてのデータを読み取った後、ヒープ内のデータは上位 K 個のデータになります

この方法は非常に賢いものです。ヒープの先頭にある要素がヒープ内で最小になるように小さなヒープを構築します。それより大きい要素はヒープにスワップされます。大きなデータはヒープに入り、ヒープの一番下に沈みます。ヒープ内の上位 K 個の最大データのみが交換されません。ヒープ内に上位 K 個の最大要素がある場合、ヒープは小さいため、ヒープの先頭はヒープ内で最小の要素ですが、ヒープ内にない要素よりも大きくなります。ヒープの先頭に既に K-1 個の要素が存在する場合でも、すべての要素のうち最初の K 個の最大要素の範囲内で、ヒープを小さく保つために下方調整する場合は、ヒープの先頭に要素が存在する必要があります。この要素は後続のデータよりも小さいです。次の要素を読み取った後、それを交換して下方に調整します。上位 K 個の最大の要素が見つかります。
大きなヒープを構築している場合、大きなヒープの最上位の要素がヒープ内で最大になります。それよりも大きい場合にのみヒープに追加できます。ヒープが構築されると、ヒープの最上位要素はすべての最大の要素になります。これではすべての要素がブロックされませんか?

void PrintTopK(const char* file, int k)
{
    
    
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
    
    
		perror("fopen fail");
		return;
	}
	int* minheap = (int*)malloc(sizeof(int) * k);
	if (minheap == NULL)
	{
    
    
		perror("minheap fail");
		return;
	}
	int i = 0;
	for (i = 0; i < k; i++)
	{
    
    
		fscanf(fout, "%d", &minheap[i]);
	}
	for (i = (k - 2) / 2; i >= 0; i--)
	{
    
    
		AdjustDown(minheap, k, i);
	}
	int x = 0;
	while (fscanf(fout, "%d", &x) != EOF)
	{
    
    
		if (minheap[0] < x)
		{
    
    
			minheap[0] = x;
			AdjustDown(minheap, k, 0);
		}
	}
	for (i = 0; i < k; i++)
	{
    
    
		printf("%d ", minheap[i]);
	}
	free(minheap);
	fclose(fout);
}
void CreateNDate()
{
    
    
	// 造数据
	int n = 10000;
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
    
    
		perror("fopen error");
		return;
	}
	for (int i = 0; i < n; ++i)
	{
    
    
		int x = rand() % 1000000;
		fprintf(fin, "%d\n", x);
	}
	fclose(fin);
}
int main()
{
    
    
	CreateNDate();
	PrintTopK("data.txt", 5);
	return 0;
}

ここに画像の説明を挿入します
ご覧いただきありがとうございます~

おすすめ

転載: blog.csdn.net/2301_77459845/article/details/132777524