Structure de données - tas (implémentation en langage C)

qu'est-ce que le tas

Un tas est une structure de données spéciale, qui est un arbre binaire complet et satisfait la propriété de tas, c'est-à-dire que la valeur d'un nœud parent est toujours supérieure ou inférieure à la valeur de ses nœuds enfants. Si la valeur du nœud parent est toujours supérieure à la valeur du nœud enfant, alors on l'appelle un grand tas racine ; à l'inverse, si la valeur du nœud parent est toujours inférieure à la valeur du nœud enfant, alors on appelle c'est un petit tas de racines. Dans un tas, le nœud racine a la plus grande valeur (grand tas racine) ou la plus petite valeur (petit tas racine), il est donc également appelé le sommet du tas. Les tas sont souvent utilisés dans des scénarios tels que le tri et les problèmes topK.
insérez la description de l'image ici

implémentation du tas

Cet article est implémenté en langage C et séparé des fichiers d'en-tête et des fichiers sources.Il présentera également progressivement les idées d'implémentation de chaque interface et fournira des codes de référence.

définition de la structure de tas

La définition de la structure du tas est en fait une table de séquence spéciale, qui est similaire à la pile. Il est donc nécessaire d'utiliser un pointeur pour pointer sur la mémoire ouverte dynamiquement, une variable pointant sur la position courante de l'indice, et une capacité à enregistrer la mémoire dynamique courante.
insérez la description de l'image ici

interface d'initialisation de tas

L'idée d'implémentation de l'interface d'initialisation du tas est la suivante : Tout d'abord, pour changer un tas, nous devons transmettre son adresse. La partie paramètre doit donc être écrite sous la forme Hp*. Au début de l'interface, jugez de la légalité du pointeur. Ouvrez ensuite la mémoire dynamique et jugez de l'efficacité de la mémoire dynamique. Enfin, initialisez les membres de la structure.
insérez la description de l'image ici

interface de destruction de tas

Nous devrions développer la bonne habitude de libérer l'espace pour une application dynamique et de le vider à temps après la libération. Enfin, définissez la taille et la capacité sur zéro.
insérez la description de l'image ici

interface de données d'insertion de tas

L'idée d'implémentation de l'interface d'insertion de tas est la suivante. Assert juge la validité du pointeur. C'est une bonne habitude de programmation. Il est recommandé de développer également cette habitude en temps ordinaire. Déterminez d'abord si la capacité est pleine, et si elle est pleine, augmentez la capacité. Ensuite, la logique d'insertion de données directement en dessous est en fait similaire à une table de séquence. Insérez directement les données dans la position de l'indice de taille, utilisez simplement ++size. Enfin, appelez l'interface de création de tas d'ajustement vers le haut pour conserver la structure du tas inchangée.
insérez la description de l'image ici

Ajustez l'interface de tas vers le haut

Tout d'abord, la position en indice du nœud parent doit être déduite en fonction de la position en indice du nœud enfant. Ensuite, commencez à ajuster vers le haut. Le processus d'ajustement vers le haut est un processus cyclique. La condition d'itération de la boucle est que lorsque l'enfant est supérieur à l'indice du nœud racine, la boucle continuera à être prise en charge. La boucle se termine lorsque le nœud enfant est plus petit que le nœud parent. Si le nœud parent est plus petit que le nœud enfant, effectuez l'échange de données de l'indice correspondant, puis itérez l'indice du nœud enfant et l'indice du nœud parent.
insérez la description de l'image ici

insérez la description de l'image ici

Vérifier si le tas est vide

L'idée de juger si le tas est vide est relativement simple, similaire à l'idée de juger du vide de la table de séquence.Lorsque le prochain indice pouvant être inséré dans les données est 0, cela signifie un tas vide.
insérez la description de l'image ici

Interface de suppression de tas de données

Pour supprimer les données du tas, devez-vous supprimer les données en haut du tas ou les données en bas du tas ? La réponse est de supprimer les données en haut du tas, car la suppression des données en bas du tas n'a que peu de valeur. Et la suppression du haut du tas peut générer une certaine valeur, comme le tri ou la collecte de certaines données K supérieures. Par exemple, lorsque nous voulons choisir un ordinateur dans l'application d'achat, nous pouvons le trier par volume de ventes, ce qui est également un scénario pour les applications de tas. De retour au sujet, l'idée de mise en œuvre de la suppression du haut du tas est la suivante : nous échangeons les données du haut du tas avec les dernières données, puis utilisons la taille - pour obtenir l'effet de suppression des données sur le haut du tas, et améliorer considérablement l'efficacité. Enfin, ajustez le tas vers le bas.
insérez la description de l'image ici

insérez la description de l'image ici

Ajuster l'interface du tas vers le bas

L'idée de mise en œuvre de la construction de tas d'ajustement à la baisse est la suivante : premièrement, le processus d'ajustement à la baisse est un cycle et sa condition de terminaison est parent > taille. À l'intérieur du corps de la boucle se trouve l'idée centrale de l'ajustement vers le bas. Le parent est plus grand (plus petit) que les enfants gauche et droit. Cet article prend la réalisation d'un gros tas comme exemple. Un concept plus important est introduit ici : étant donné que la couche inférieure du tas utilise un stockage de table séquentiel, les enfants gauche et droit du même père sont stockés de manière adjacente. Autrement dit, l'indice de l'enfant gauche + 1 est l'indice de l'enfant droit. Laissez le père comparer avec le plus grand des enfants gauche et droit, et si le père est plus petit que l'enfant, échangez la position, puis itérez. Remarque : La condition d'ajustement vers le bas est que les sous-arborescences gauche et droite doivent être des tas.
insérez la description de l'image ici

Obtenez des données de haut de tas

En fait, c'est le premier élément de la table de séquence d'accès. Cependant, fournir une interface de cette manière est très cohérent avec l'interface et améliore grandement la lisibilité du code.
insérez la description de l'image ici

Obtenir le nombre de données valides dans le tas

Puisque notre taille commence à partir de 0, renvoyez simplement la taille directement.
insérez la description de l'image ici

Code d'implémentation complet

//Heap.h文件
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

//默认起始容量
#define DefaultCapacity 4

//存储的数据类型
typedef int HpDataType;

typedef struct Heap
{
    
    
	HpDataType* data;
	int size;//可以插入数据的下标
	int capacity;//容量
}Hp;


//初始化
void HpInit(Hp* pHp);

//堆的销毁
void HpDestroy(Hp* pHp);

//插入数据
void HpPush(Hp* pHp, HpDataType x);

//向上调整建堆
void AdjustUp(HpDataType* data, int child);

//判断是否为空
bool HpEmpty(Hp* pHp);

//删除数据
void HpPop(Hp* pHp);

//向下调整建堆
void AdjustDown(HpDataType* data,int size, int parent);

// 取堆顶的数据
HpDataType HpTop(Hp* pHp);

// 堆的数据个数
int HpSize(Hp* pHp);
// Heap.c文件
#include"Heap.h"

//初始化
void HpInit(Hp* pHp) 
{
    
    
	//判断合法性
	assert(pHp);

	//开辟动态空间
	HpDataType* tmp = (HpDataType*)malloc(sizeof(HpDataType) * DefaultCapacity);
	if (tmp == NULL)//判断合法性
	{
    
    
		perror("malloc fail");
		return;
	}

	//初始化
	pHp->data = tmp;
	pHp->size = 0;
	pHp->capacity = DefaultCapacity;
}

//堆的销毁
void HpDestroy(Hp* pHp)
{
    
    
	//判断合法性
	assert(pHp);

	//释放内存和清理
	free(pHp->data);
	pHp->data = NULL;
	pHp->size = pHp->capacity = 0;

}


void Swap(HpDataType* p1, HpDataType* p2)
{
    
    
	HpDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向上调整建堆
void AdjustUp(HpDataType* data, int child)
{
    
    
	//判断指针有效性
	assert(data);
	int parent = (child - 1) / 2;
	while (child > 0)
	{
    
    
		//向上调整呢
		if (data[child] > data[parent])
		{
    
    
			Swap(&data[child], &data[parent]);
		}
		else
		{
    
    
			break;
		}	
		//迭代
		child = parent;
		parent = (child - 1) / 2;
	}

}

//插入数据
void HpPush(Hp* pHp, HpDataType x)
{
    
    
	//判断指针有效性
	assert(pHp);

	//判断容量是否满了
	if (pHp->size == pHp->capacity)
	{
    
    
		HpDataType* tmp = (HpDataType*)realloc(pHp->data,sizeof(HpDataType) * pHp->capacity * 2);
		if (tmp == NULL)//判断空间合法性
		{
    
    
			perror("malloc fail");
			return;
		}
		//扩容后
		pHp->data = tmp;
		pHp->capacity *= 2;
	}

	//数据入堆
	pHp->data[pHp->size] = x;
	pHp->size++;

	//向上调整建堆
	AdjustUp(pHp->data, pHp->size - 1);

}
void AdjustDown(HpDataType* data, int size, int parent)
{
    
    
	//断言检查
	assert(data);

	int child = parent * 2 + 1;

	while (child < size)
	{
    
    
		//求出左右孩子较大的那个下标
		if (child + 1 < size && data[child + 1] > data[child])
		{
    
    
			child++;
		}
		//父亲比孩子小就交换位置
		if (data[child] > data[parent])
		{
    
    
			//交换
			Swap(&data[child], &data[parent]);
			//迭代
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
    
    
			break;
		}
	}

}

void HpPop(Hp* pHp)
{
    
    
	//断言检查
	assert(pHp);

	//删除数据
	Swap(&pHp->data[0], &pHp->data[pHp->size-1]);
	pHp->size--;

	//向下调整建堆
	AdjustDown(pHp->data,pHp->size-1,0);

}

//判断是否为空
bool HpEmpty(Hp* pHp)
{
    
    
	assert(pHp);
	
	return pHp->size == 0;
}

// 取堆顶的数据
HpDataType HpTop(Hp* pHp)
{
    
    
	assert(pHp);

	return pHp->data[0];
}

// 堆的数据个数
int HpSize(Hp* pHp)
{
    
    
	assert(pHp);

	return pHp->size;
}

résumé

Faire fonctionner la structure de données du tas revient à manger des gâteaux de femme. Vous mangez des gâteaux sucrés, mais il n'est pas certain que votre femme les ait faits. Cependant, lorsque vous le mangez, vous imaginez que le gâteau fait par votre femme a une saveur particulière. Sur la structure logique du tas, ce que vous exploitez est un arbre, et sur le stockage sous-jacent est une table de séquence. C'est un endroit relativement abstrait, qui doit tester notre capacité à dessiner des images et à lire le débogage du code.

tri en tas

Le tri de tas est en fait une utilisation courante de la structure de données de tas. L'idée centrale du tri de tas est d'utiliser l'idée de suppression de tas pour effectuer des opérations de tri. Le tri par tas est un tri instable de complexité temporelle O(N*logN). Quant à l'explication de la stabilité du tri, je vous la présenterai dans le blog suivant.

Implémentation du tri par tas

L'idée de mise en œuvre du tri par tas est la suivante : d'abord, déterminez l'ordre de tri et créez les données dans des tas, créez de grands tas dans l'ordre croissant et créez de petits tas dans l'ordre décroissant. Il est recommandé d'utiliser un ajustement vers le bas pour créer un tas. Étant donné que la complexité temporelle est O(logN), si vous utilisez un ajustement vers le haut pour créer le tas, la complexité temporelle est O(N*logN). Ce type de complexité temporelle est trop coûteux pour trouver les données du sommet du tas, il est donc préférable de le parcourir directement (complexité temporelle).
insérez la description de l'image ici

Ensuite, utilisez l'idée de suppression de tas pour trier. Voici un exemple de tri par ordre croissant.
insérez la description de l'image ici

//堆排序--排升序建大堆
void HeapSort(int* arr, int n)
{
    
    
	//向下建堆,效率更高
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
    
    
		AdjustDown(arr,n-1,i);
	}

	//排序
	//利用堆删除的思想进行排序
	int end = n - 1;
	while (end > 0)
	{
    
    
		//交换
		int tmp = arr[0];
		arr[0] = arr[end];
		arr[end] = tmp;
		//调整堆
		AdjustDown(arr, end-1, 0);
		end--;
	}
}

Analyse de la complexité temporelle de la construction de tas et du tri de tas

ajuster la construction vers le bas

Dans l'implémentation précédente du tri en tas, il est mentionné que l'ajustement vers le bas du tas est plus efficace, car la complexité temporelle de l'ajustement vers le bas du tas est O(N). Ensuite, je vous amènerai à analyser brièvement la complexité temporelle de l'ajustement du tas vers le bas.
insérez la description de l'image ici

Ajuster l'accumulation

La complexité temporelle de la construction du tas d'ajustement vers le haut est O(N*logN). Examinons le problème de complexité temporelle de l'ajustement à la hausse.
insérez la description de l'image ici

tri en tas

La complexité temporelle du tri par tas est O(N logN). La complexité de l'ajustement du tas vers le bas est O (N), qui a été analysée ci-dessus. La partie tri est O(N logN) combinée avec l'ajustement du tas vers le bas à partir du premier nœud non-feuille .
·

résumé

Pour la complexité temporelle de la construction d'un tas et la complexité du tri de tas décrites ci-dessus, il suffit en fait d'écrire une conclusion. Bien sûr, du point de vue de la mise en œuvre, il n'est pas difficile d'analyser l'écart d'efficacité approximatif entre l'ajustement à la hausse et l'ajustement à la baisse de la construction de pieux. Étant donné que l'ajustement à la baisse commence à partir du premier nœud non feuille, le pire des cas consiste à ajuster la moitié des nœuds en moins que l'ajustement à la hausse. Cela a déjà beaucoup gagné en termes d'efficacité.

Introduction aux problèmes TOPK

Le problème TOPK fait référence au problème consistant à trouver les K premières données les plus grandes ou les plus petites dans un ensemble de données. Les solutions courantes incluent le tri par tas, le tri rapide, le tri par fusion, etc. Ce problème se pose souvent dans des domaines tels que l'analyse de données et l'apprentissage automatique. Bien sûr, il existe un scénario spécial où il est très merveilleux d'utiliser le tas pour le dépistage TOK. En supposant qu'il y a maintenant 10 milliards d'entiers et que les 50 premiers nombres sont requis, nous pouvons construire un petit tas, et tant que les données traversées sont plus grandes que les données supérieures du tas, remplacez-les dans le tas (ajuster vers le bas) , et enfin obtenir le plus grand nombre de top 50. Prenons un exemple simple pour le ressentir.

void AdjustDownSH(HpDataType* data, int size, int parent)
{
    
    
	//断言检查
	assert(data);

	int child = parent * 2 + 1;

	while (child < size)
	{
    
    
		//求出左右孩子较大的那个下标
		if (child + 1 < size && data[child + 1] < data[child])
		{
    
    
			child++;
		}
		//父亲比孩子小就交换位置
		if (data[child] < data[parent])
		{
    
    
			//交换
			Swap(&data[child], &data[parent]);
			//迭代
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
    
    
			break;
		}
	}

}

void PrintTopK(const char* file, int k)
{
    
    
	// 1. 建堆--用a中前k个元素建小堆
	int* topk = (int*)malloc(sizeof(int) * k);
	assert(topk);

	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
    
    
		perror("fopen error");
		return;
	}

	// 读出前k个数据建小堆
	for (int i = 0; i < k; ++i)
	{
    
    
		fscanf(fout, "%d", &topk[i]);
	}

	for (int i = (k - 2) / 2; i >= 0; --i)
	{
    
    
		AdjustDownSH(topk, k, i);
	}

	// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
	int val = 0;
	int ret = fscanf(fout, "%d", &val);
	while (ret != EOF)
	{
    
    
		if (val > topk[0])
		{
    
    
			topk[0] = val;
			AdjustDownSH(topk, k, 0);
		}

		ret = fscanf(fout, "%d", &val);
	}

	for (int i = 0; i < k; i++)
	{
    
    
		printf("%d ", topk[i]);
	}
	printf("\n");

	free(topk);
	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 (size_t i = 0; i < n; ++i)
	{
    
    
		int x = rand() % 10000;
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}

int main()
{
    
    
	CreateNDate();
	PrintTopK("data.txt", 10);

	return 0;
}

Je suppose que tu aimes

Origine blog.csdn.net/m0_71927622/article/details/131070174
conseillé
Classement