「基本データ構造」におけるヒープソートのまとめと考え方

1.この章の焦点 

  • ヒープ
  • 調整する
  • 調整する
  • ヒープソート

第二に、ヒープ

2.1ヒープの概要(3つのポイント)

1.物理構造は配列です

2.論理構造は完全な二分木です

3.大きなヒープ:すべての親ノードが子ノード以上、小さなヒープ:すべての親ノードが子ノード以下です。

2.2上方調整

概念:小さい/大きいヒープがあり、配列の最後に要素を挿入し、ヒープを調整してヒープをまだ小さい/大きいようにします。

使用条件:配列の最初のn-1要素がヒープを形成します。

例として大きなヒープを取り上げます。

ロジックの実装:

新しく挿入された最後の要素を子として扱い、それを親と比較します。子が親よりも大きい場合は、それらを交換し、親を子として扱い、子が0に等しくなるまで順番に比較します。調整。

子供が真ん中の父親よりも小さい場合、子供はループから飛び出して調整を終了します。

参照コード:

void AdjustUp(HPDataType* a, 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;
		}
	}
}

アプリを調整する

big \ smallヒープに新しい要素を追加した後も、big\smallヒープはbig\smallヒープのままです。

2.3下方修正

概念:ルートノードの左右のサブツリーはすべて大/小ヒープです。下向きに調整することにより、完全なバイナリツリー全体が大/小ヒープで構成されます。

使用条件:ルートノードの左右のサブツリーはすべて大きい\小さいヒープです。

 図に示すように、ルートは23で、その左右のサブツリーは大きなヒープですが、完全な二分木全体はヒープではありません。下向きに調整することで、完全な二分木全体をヒープにすることができます。

ロジックの実装:

ルートの左右の子の大きい方の子を選択し、ルートと比較します。ルートよりも大きい場合は交換し、そうでない場合は調整を終了します。

参照コード:

void AdjustDown(HPDataType* a, int size, int root)
{
	int parent = root;
	int child = parent * 2 + 1;//左孩子
	while (child < size)
	{
		if (child + 1 < size && a[child] < a[child + 1])//如果左孩子小于右孩子,则选右孩子
		{
			//务必加上child+1,因为当child=size-1时,右孩子下标是size,对其接引用会越界访问。
            child++;//右孩子的下标等于左孩子+1
		}
		if (a[child] > a[parent])//让较大的孩子与父亲比较,如果孩子大于父亲,则将它们交换。
		{
			Swap(&a[child], &a[parent]);
            //迭代过程
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

2.4ヒープの構築(2つの方法)

1つ目:ヒープを構築するための上方調整(時間計算量はO(N * logN)、空間計算量はO(1))

アイデアは、2番目の配列要素から最後の配列要素まで順番に上向きに調整することです。

参照コード:

for (int i = 1; i < n; i++)
{
	AdjustUp(a, i);
}

 時間計算量の計算:

完全な二分木で計算する

最悪の場合の実行ステップ数は次のとおりです。T=(2 ^ 1)* 1 +(2 ^ 2)* 2 +(2 ^ 3)* 3 + .... + 2 ^(h-1)*(h -1)

最終的な簡略化は次のとおりです。T=2^ h *(h-2)+2

そして(2 ^ h)-1 = N

したがって、h = log(N + 1)

T =(N + 1)*(logN-1)+2を持ち込んだ後

したがって、その時間計算量は次のとおりです。O(N * logN)

2番目:ヒープを構築するための下方調整(時間計算量はO(N)、空間計算量はO(1))

最後の非リーフノード(最後の配列要素の親)から最初の配列要素まで下向きに調整します。

参照コード:

//n代表数组元素个数,j的初始值代表最后一个元素的父亲下标
for (int j = (n - 1 - 1) / 2; j >= 0; j--)
{
	AdjustDown(a, n, j);
}

時間計算量の計算:

完全な二分木で計算する

最悪の実行数:

T = 2 ^(h-2)* 1 + 2 ^(h-3)* 2 + 2 ^(h-4)* 3 + ..... + 2 ^ 3 *(h-4)+ 2 ^ 2 *(h-3)+ 2 ^ 1 *(h-2)+ 2 ^ 0 *(h-1)

同時2^h-1 = N

T = N-log(N + 1)を取得するために単純化する

Nが大きい場合、log(N + 1)は無視できます。

したがって、その時間計算量はO(N)です。

したがって、通常、ヒープを構築するために下方調整方法を使用します。

3.ヒープソート

現在の最良のソートアルゴリズムは、O(N * logN)の時間計算量を持っています

ヒープソートの時間計算量はO(N * logN)です。

ヒープソートはヒープをソートすることであるため、配列をソートするときは、最初に配列をヒープに構築してから、それをソートします。

最初に知っておくべきことは次のとおりです。

配列を昇順させるには、配列を大きなヒープに構築する必要があります。

配列を降順にするには、配列を小さなヒープに構築する必要があります。

どうしてこれなの?

これには、大きなヒープと小さなヒープの違いを理解する必要があります。大きなヒープの上部が最大の数であり、小さなヒープの上部が最小の数です。

初めてヒープを構築するとき、大きなヒープを構築すると最初の最大数を取得し、それを配列の最後の要素と交換できます。次回は、ヒープの上部の数を調整するだけで済みます。再び下に移動すると、配列を再度変更できます。大きなヒープになり、配列の最後から2番目の要素と交換されます。それ以降、2つの要素は、必要な場所に格納されるように配置されています。次に、番号がフェッチされ、順番に調整されます。

そして、それが小さなヒープである場合、ヒープが最初に構築されたときに、最小の数を取得し、それを配列の最初の位置に配置することができます。次に、それまたは小さなヒープを保持したい場合は、どうすればよいですか。 ?ヒープは2番目の要素から始めて下からしか構築できず、ヒープ構築の時間計算量はO(N)です。ヒープを常に再構築する必要があります。ヒープソートの最終的な時間計算量はO(N * N)です。 )賢明です。

または、小さなヒープを構築した後、次のようにします。

n個の配列のスペースを開くときは、最初の最小数を選択して新しく開いた配列スペースに保存し、ヒープの上部の番号を削除してから、ヒープの上部の番号を調整してから、ヒープの新しいトップ番号は、新しい配列の2番目の位置に配置されます......。

そのような時間計算量はO(N * logN)ですが。

しかし、そのような空間の複雑さはO(N)です。

また、これは最適なヒープソート方法でもありません。

大きなヒープを構築する利点は、選択した番号を最後に配置することです。これにより、ヒープの上部を下向きに調整できるため、まだ大きなヒープであり、下向きの調整の時間計算量はO(logN )、最終ヒープソートの時間計算量はO(N * logN)です。

ヒープソートの中心的な意味:

大きなヒープまたは小さなヒープを構築することにより、ヒープ内の最大または最小の数を選択し、それらを後ろから前に配置します。

参照コード:

    int end = n - 1;//n代表数组元素的个数
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}

ヒープソート全体のコード:

void Swap(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

void AdjustUp(int* a, 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 AdjustDown(int* a, int n, int root)
{
	int child = 2 * root + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child] < a[child+1])
		{
			child++;
		}
		if (a[child] > a[root])
		{
			Swap(&a[child], &a[root]);
			root = child;
			child = 2 * root + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
	//建大堆(向上调整)
	//for (int i = 1; i < n; i++)
	//{
	//	AdjustUp(a, i);
	//}

	//建大堆(向下调整)
	for (int j = (n - 1 - 1) / 2; j >= 0; j--)
	{
		AdjustDown(a, n, j);
	}
	//升序
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}
void printarr(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

int main()
{
	int arr[10] = { 9,2,4,8,6,3,5,1,10 };
	int size = sizeof(arr) / sizeof(arr[0]);
	HeapSort(arr, size);
	printarr(arr, size);
	return 0;
}

おすすめ

転載: blog.csdn.net/m0_62171658/article/details/124004650