[Estrutura de Dados] Os conceitos de árvores e árvores binárias e a estrutura sequencial e implementação de árvores binárias

Prefácio:

Antes aprendemos as estruturas de dados de listas sequenciais, listas vinculadas, pilhas e filas, mas essas estruturas de dados são todas lineares (um para um). Em seguida, precisamos aprenderestrutura de dados não lineares - árvore (árvore binária). Comparada com a anterior, a estrutura da árvore é mais complexa. Não muito para dizer: vamos direto ao ponto.

1. Conceito e estrutura de árvore

1.O conceito de árvore

Uma árvore é umaestrutura de dados não linear, que é um para muitos ( também é possível que seja um relacionamento um-para-um). É um conjunto de relacionamentos hierárquicos compostos por n (n>=0) nós limitados. Chama-se árvore porque se parece com uma árvore invertida, ou seja, tem asraízes apontando para cima eAs folhas estão voltadas para baixo.

possui um nó especial, chamado de nó raiz. O nó raiz não possui nós predecessores
exceto o nó raiz. < a i=3>Os nós restantes são divididos em M (M>0) conjuntos disjuntos T1, T2,..., Tm, onde cada conjunto Ti (1<= i <= m) é outra subárvore com estrutura semelhante à de uma árvore. O nó raiz de cada subárvore tem um e apenas um predecessor e pode ter 0 ou mais sucessores, então a árvore é Recursãodefinida.

Árvores na realidade e árvores em estruturas de dados, como:
Insira a descrição da imagem aqui

Nota: Na estrutura em árvore não pode haver interseção entre subárvores, caso contrário não será uma estrutura em árvore.
Insira a descrição da imagem aqui

2. Conceitos relacionados às árvores

O conceito relacionado de árvore refere-se à conexão entre as raízes, galhos, folhas, etc. da árvore, que é semelhante ao relacionamento entre humanos.
Insira a descrição da imagem aqui

Grau do nó: O número de subárvores contidas em um nó é chamado de grau do nó; Como mostrado na figura acima: A é um par de 6, então o grau de A é 6< /span> A altura ou profundidade da árvore < /span>: Uma coleção de m (m>0) árvores disjuntas é chamada de floresta; Floresta de todos os nós: qualquer nó na subárvore com um nó como raiz é chamado de descendente deste nó. Como mostrado acima: todos os nós são descendentes de Adescendente: da raiz ao ramo passado pelo nó Todos os nós; como mostrado na figura acima: A é o ancestralO ancestral do nó : Ambos os pais estão na mesma Os nós na camada são primos um do outro; como mostrado na figura acima: H e I são nós primos um do outroNó primo: O nível máximo do nó na árvore; como mostrado acima: a altura da árvore é 4: A partir da definição da raiz, a raiz é o 1º nível, os nós filhos da raiz são o 2º nível e assim por diante. nó O nível de : Em um árvore, o grau do maior nó é chamado de grau da árvore; Como mostrado na figura acima: o grau da árvore é 6O grau da árvore: Nós com o mesmo nó pai são chamados de nós irmãos; como mostrado acima: B e C são nós irmãosNó irmão : O nó raiz da subárvore contida por um nó é chamado de nó filho do nó; conforme mostrado na figura acima: B é o nó filho de ANó filho ou nó filho: Se um nó contém nós filhos, este nó é chamado de nó pai de seu nó filho; conforme mostrado na figura acima: A é o nó pai de BNó pai ou nó pai: Um nó com grau diferente de 0; Como mostrado na figura acima: D, E, F, G... e outros nós são nós ramificados Nó não terminal ou nó ramificado: Um nó com grau 0 é chamado de nó folha; Conforme mostrado na figura acima: Nós como B, C, H, I... são folhas. Nó
Nó folha ou nó terminal










Observação: ao calcular o nível do nó, a contagem do nó superior para baixo pode começar de 0 ou 1, mas é mais recomendado começar de 1.
Como a árvore pode ser uma árvore vazia, uma árvore com apenas um nó ou uma árvore com vários nós, se o cálculo começar em 0, então o nível do nó (ou a altura da árvore ) deve ser 0 É difícil distinguir se é uma árvore vazia ou uma árvore com apenas um nó, então para facilitar a contagem da altura da árvore é melhor começar em 1.

Insira a descrição da imagem aqui

3. Armazenamento de árvores

A estrutura em árvore é mais complicada que a tabela linear e é mais difícil armazená-la e representá-la. É necessário salvar tanto o intervalo de valores quanto o relacionamento entre os nós. . Relacionamento, na verdade, existem muitas maneiras de representar árvores, como representação de pai, representação de filho, representação de pai filho e representação de irmão filho, etc. Aqui entenderemos brevemente a representaçãode irmão filho mais comumente usada. (também conhecido como notação filho esquerdo irmão direito)

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

Um nó pode apontar para vários nós arbitrariamente. O ponteiro firstchild deste nó aponta para o seu primeiro nó filho (se não, aponta para um ponteiro nulo), e o ponteiro nextbrother aponta para o seu nó irmão (se não, aponta para um nó irmão). Ponteiro nulo).

Insira a descrição da imagem aqui
Encontre primeiro o primeiro filho (por exemplo, B é o primeiro filho de A) e depois encontre o primeiro filho neste momento para encontrar seus irmãos até terminar com um ponteiro nulo.

TreeNode* Anode é considerado o nó A
Encontre o nó B, o primeiro filho de A: TreeNode* child = Anode->firstchild;
while(child) até o vazio terminar
{ ………… filho = filho->próximo irmão; }


Insira a descrição da imagem aqui

4. Aplicação de árvores na prática

O diretório que normalmente usamos no sistema de arquivos do computador é uma estrutura em árvore. Quando você abre este computador, há uma unidade D, uma unidade C, etc., e cada camada possui vários arquivos.
Insira a descrição da imagem aqui

2. Conceito e estrutura da árvore binária

1. Conceito

Uma árvore binária é umconjunto finito de nós.
1. Até dois nós, ou 1 ou 0
2. Um nó raiz mais dois aliases é chamado < A i=5>esquerdo subárvore e subárvore direita são compostas de árvores binárias

Insira a descrição da imagem aqui
Como pode ser visto na figura acima, não há nó com grau maior que 2 em uma árvore binária, e as subárvores de uma árvore binária podem ser divididas em subárvores esquerda e direita, e a ordem não pode ser invertida , então a árvore binária é ordenada Tree

Nota: Qualquer árvore binária é composta pelas seguintes situações:
Insira a descrição da imagem aqui
Árvore binária real:
Insira a descrição da imagem aqui

2. Árvore binária especial

(1) Árvore binária completa

Conceito: Uma árvore binária, seo número de nós em cada camada atingir o valor máximo , então Esta árvore binária é uma árvore binária completa. Ou seja, se o número de níveis de uma árvore binária for h, e o número total de nós for 2^h - 1, então é uma árvore binária completa.
Insira a descrição da imagem aqui

Cada camada está cheia: 2^(i-1) nós (i é a posição de uma determinada camada)
F(h) = 2 ^ 0 + 2 ^ 1 + ……+2 ^ (h-2) + 2 ^ (h-1)Soma da sequência geométrica

Suponha que esta árvore binária tenha N nós
N = 2 ^ h - 1 => h = log(N+1) (log é baseado em 2)

(2) Árvore binária completa

Conceito: Uma árvore binária completa é modificada com base em uma árvore binária completa. Suponha que sua altura seja h, então sua primeira camada h-1 está cheia, e a última camada pode estar cheia ou insatisfeita e sua a última camada deve ser contínua da esquerda para a direita. Uma árvore binária completa é um tipo especial de árvore binária completa.

Insira a descrição da imagem aqui
Portanto, o número total de nós em uma árvore binária completa possui um intervalo. Quando é uma árvore binária completa, ou seja, quando o número total de nós é maior, seu número total de nós é 2^h - 1; quando a última camada possui apenas um nó, ou seja, quando o número total de nós nós é o menor, seu número total de nós é 2 ^ (h-1), então o intervalo de nós é: [2 ^ (h-1), 2 ^ h - 1].

3. Propriedades das árvores binárias

1. Se o número de níveis do nó raiz for 1, então o h-ésimo nível de uma árvore binária não vazia tem no máximo 2^(h-1) nós ;
2. Se o nível do nó raiz for 1, entãoUma árvore binária com profundidade h tem no máximo 2^h-1 nós;< /span>A profundidade de uma árvore binária completa com n nós, h=log(n+1 ) 4. Se o número de níveis do nó raiz for 1, n0=n2+ 1;
3. Para qualquer árvore binária, se o grau for 0, o número de nós folha for n0 e o número de nós ramificados com grau 2 for n2, então a fórmula será satisfeita

4. Armazenamento de árvores binárias

Os métodos de armazenamento de árvores binárias são divididos em armazenamento sequencial e armazenamento em cadeia .

(1)Armazenamento sequencial

O armazenamento de estrutura sequencial éusar matrizes para armazenar. Geralmente, as matrizes são adequadas apenas para representar árvores binárias completas, porque se não estiverem completas árvores binárias, haverá um desperdício de espaço. O armazenamento sequencial de árvores binárias é fisicamente um array e logicamente uma árvore binária.

Existe uma regra ao usar o armazenamento em array:
Insira a descrição da imagem aqui

O pai ou o filho podem ser encontrados subscritos em qualquer posição
Observação: esta regra só pode ser usada se uma árvore binária completa ou uma árvore binária completa for necessária

Insira a descrição da imagem aqui
Portanto, para árvores binárias não completas, é mais adequado usar armazenamento de estrutura encadeada.

Resumo: Árvores binárias completas ou árvores binárias completas são adequadas para armazenamento de array

(2)Armazenamento em cadeia

A estrutura vinculada de uma árvore binária é representada por uma lista vinculada, que é dividida em três escopos (domínio de dados e domínios de ponteiro esquerdo e direito) , os ponteiros esquerdo e direito são usados ​​para armazenar os endereços dos filhos esquerdo e direito.
Insira a descrição da imagem aqui

3. Estrutura sequencial e implementação de árvore binária

1. Estrutura sequencial da árvore binária

Árvores binárias comuns não são adequadas para armazenamento em arrays, porque pode haver muito espaço desperdiçado. Uma árvore binária completa é mais adequada para armazenamento de estrutura sequencial. Na realidade, geralmente armazenamos o heap (uma árvore binária) usando um array de estrutura sequencial. O que precisa ser observado é o heap e o sistema operacional virtual endereço do processo aqui. O heap no espaço é duas coisas diferentes. Uma é uma estrutura de dados e a outra é uma segmentação de área no sistema operacional que gerencia a memória.

2. Conceito e estrutura de heap

Heap: Uma árvore binária com estrutura não linear, mais precisamente uma árvore binária completa. Adequado para armazenamento de array.

Os heaps são divididos em dois tipos: o heap com o maior nó raiz é chamado de heap máximo ou heap raiz grande, e o heap com o menor nó raiz é chamado de heap mínimo ou heap raiz pequeno.
Pilha pequena: Qualquer pai na árvore é <= filho (a comparação é o valor do nó)
Pilha grande: Qualquer pai na árvore é > =Criança

Insira a descrição da imagem aqui

3. Implementação de heap

(1) Inicialize o heap

Existem duas maneiras de inicializar o heap.
O primeiro método: Deixe o array apontado pela estrutura vazio (sem elementos) e defina o número efetivo e a capacidade como 0

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

O segundo tipo: recebe o tamanho n do array externo, abre dinamicamente um espaço de tamanho n e copia os dados para o array apontado pela estrutura.

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 é um algoritmo de ajuste ascendente, que será analisado a seguir.

(2) Destrua a pilha

Escusado será dizer que a destruição do heap é o mesmo que a destruição da tabela de sequência.

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

(3) Inserção na pilha

A inserção no heap adota a inserção final da tabela de sequência (inserção uma por uma), pois a inserção final do array é altamente eficiente (há também exclusão final, que será usada posteriormente). Ao inserir dados, a expansão deve ser considerada porque o tamanho do espaço era 0 durante a inicialização. Aqui você pode definir uma variável newcapacity e usar o operador condicional. Se a capacidade for 0, dê a capacidade inicial 4, caso contrário será o dobro da capacidade original. Após a expansão, novos dados são inseridos, o número de elementos é ++.

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);//向上调整
}

Basta inserir os dados externos um por um? Na verdade, não é o caso. Depois de inserirmos os dados, precisamos transformar o array apontado pela estrutura em um heap . Aqui nós apresentará um algoritmo.

Ajuste o algoritmo para cima

Comece no último nó folha e ajuste para cima. Tomando como exemplo a construção de um pequeno heap, insira novos dados como o último nó folha. A característica de um pequeno heap é que o nó pai <= nó filho, portanto, o nó pai deve ser encontrado primeiro.

Fórmula: pai = (filho - 1)/2

Sabemos que embora a implementação do heap seja uma árvore binária, seu método de armazenamento é essencialmente um array e o espaço é contínuo, de modo que o nó pai pode ser encontrado através do subscrito do nó filho. Neste momento, o nó filho pode ser comparado com o nó pai.Se o nó pai for maior que o nó filho, ele será trocado e, em seguida, o nó filho atingirá a posição do nó pai para cima e o nó pai original atingirá a posição de seu nó pai. Preste atenção ao controle do intervalo do nó filho. O nó filho não pode ser o elemento superior do heap, caso contrário ele sairá dos limites, então filho> 0. Se a relação de tamanho entre os nós pai e filho atender às características de um heap pequeno, o loop será interrompido e a matriz se tornará um heap pequeno.
Insira a descrição da imagem aqui

Pré-requisito para ajuste ascendente: o array é originalmente um heap pequeno ou um heap grande

Nesse caso, como saber se o array é um heap pequeno ou grande antes de inserir um dado? Na verdade, o array apontado pelo ponteiro aqui originalmente não tinha nada. Os dados foram inseridos um por um e eu os ajustei toda vez que inseri um dado. Isso equivale a dizer que quando eu quiser inserir os próximos dados, os dados anteriores foram ajustados para um pequeno heap. Ao inserir os próximos dados, apenas ajusto o relacionamento entre esses dados e seu nó pai.

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) Exclusão de heap

A exclusão de um elemento do heap usa a exclusão final da lista de sequências, mas deve-se notar aqui que não faz sentido excluir apenas o último elemento do heap. Se for um heap pequeno, então tem a característica de que o elemento superior do heap deve ser o menor entre todos os elementos, portanto, o elemento superior do heap deve ser excluído. No entanto, excluir o início da matriz requer mover os dados, o que é problemático, então aqui trocamos o elemento superior do heap pelo último elemento e, em seguida, excluímos o final, para que o menor elemento possa ser excluído. Mas após a exclusão, o tamanho do elemento no topo do heap é incerto e não é necessariamente um heap pequeno neste momento. Outro algoritmo é apresentado aqui.

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);
}

Ajuste o algoritmo para baixo

Comece a ajustar a partir do elemento superior do heap e ajuste para baixo. Como o prenúncio anterior alterou apenas o elemento superior do heap, o elemento superior do heap é o nó pai e seus nós filhos podem ser encontrados por meio da seguinte fórmula:

Filho esquerdo: filho = pai * 2 + 1
Filho direito: filho = pai * 2 + 2

Então a questão é: devemos escolher o filho da esquerda ou o filho da direita? Aqui podemos selecionar o filho esquerdo por padrão e, em seguida, adicionar um julgamento: se o valor do filho esquerdo for maior que o valor do filho direito, o subscrito do filho esquerdo será aumentado em 1 e se tornará o filho direito. Porque o nó a ser trocado ao ajustar para baixo para se tornar um nó pai de heap pequeno é o menor dos dois nós filhos. Em seguida, compare a relação de tamanho entre o nó pai e o nó filho.Se o nó pai for maior que o nó filho, troque-o (a troca do nó pai e do filho é para trocar com o nó filho menor), então o pai o nó atinge a posição do nó filho, e o nó filho a alcança.A posição do nó filho esquerdo. Se as condições não forem atendidas, significa que o heap é pequeno e o loop será interrompido.

Nota: Há um detalhe a ser controlado aqui: o intervalo de subscritos do nó filho é menor que o número de elementos. Ao mesmo tempo, se um nó pai tiver apenas um nó filho (apenas o filho esquerdo), adicionar 1 ao subscrito do filho esquerdo estará fora dos limites.
filho + 1 < n - Esta condição é verdadeira, indicando que há apenas um filho esquerdo, mas nenhum filho direito

Insira a descrição da imagem aqui

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) Obtenha o elemento superior da pilha/o número de elementos na pilha/julgamento vazio

//获取堆顶元素
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. Classificação de pilha

Precisamos classificar os dados no heap (tomando a ordem crescente como exemplo). Existem duas maneiras: uma é imprimi-los em ordem e a outra é ordenar o array no lugar.

(1) Classificação de heap versão 1

Insira os dados no heap um por um, pegue o elemento superior do heap, desde que a matriz não esteja vazia, e exclua o elemento superior do heap até que todos os elementos sejam impressos em ordem crescente.

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);
}

Este método tem desvantagens:
1. A expansão frequente causa consumo de complexidade de espaço
2. Primeiro deve haver uma estrutura de dados heap

(2) Heap sort versão 2 - classifique a matriz no lugar

Se um array for usado após a classificação do heap, não é apropriado apenas imprimi-lo e classificá-lo. Portanto, precisamos realizar a classificação heap no array para manter o conteúdo do array em ordem.

Por exemplo:
Matriz original: 65,100,70,32,50,60
Matriz classificada: 32,50,60,65 ,70,100

Precisamos construir um heap antes de classificar, então devemos construir um heap pequeno ou um heap grande?
Vamos construir um pequeno heap primeiro:
Tomando a ordem crescente como exemplo, imprimimos a classificação de heap anterior e usamos o método de ajuste ascendente para construir um pequeno pilha. O método de construção de um pequeno heap também é usado aqui. Depois que o pequeno heap é construído, o elemento no topo do heap é o menor, então este elemento é classificado em primeiro lugar na matriz; então o próximo elemento menor é classificado na frente para trás até que os elementos da matriz sejam Organize tudo da frente para trás em ordem crescente.

Mas o método de construir primeiro um heap pequeno tem falhas:
Análise: O elemento no topo do heap é o menor em o heap e corresponde à matriz em sequência de Na linha de trás, cada vez que um dado é enfileirado, os próximos dados menores devem ser encontrados nos dados subsequentes. No entanto, o problema é que os próximos dados após a linha não são necessariamente o segundo menor,então o heap deve ser reconstruído para os dados subsequentes.O elemento superior do heap reconstruído é o próximo menor. Reconstruir o heap a cada vez aumenta a complexidade do tempo, resultando em eficiência muito baixa.

A complexidade de tempo de percorrer um dado no método de ajuste ascendente é: logN
Existem N dados, então a complexidade de tempo de construção de um heap é: N * logN É equivalente a construir um pequeno heap N vezes< /span>
A complexidade de tempo de construção de um heap cada vez que um dado é enfileirado é: N * (N * logN)

Então aqui devemos construir um grande heap. O topo do grande heap é o maior elemento do heap, mas está classificado na primeira posição do array. Como fazer com que ele alcance a última posição do array?

Nesta etapa, você pode usar a ideia de exclusão de heap. Troque o elemento no topo do heap pelo último elemento. Neste momento, o maior elemento estará onde deveria estar. A próxima etapa é muito crítica. Se você realmente não excluir o último elemento, então o maior elemento será classificado em último lugar. Foi? Então aqui podemos definir uma variável end, que aponta para o último elemento do array. Depois que a troca for concluída, ela será ajustada para baixo. O intervalo de ajuste é o número efetivo de 0 (o primeiro elemento) até o final, . Então end diminui em 1 e controla o loop end>0, pois só existe um elemento no final e nenhum ajuste é necessário, ele é o menor. Observe que o elemento apontado por end não está dentro da faixa de ajuste
Insira a descrição da imagem aqui

//建堆
	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) Comparação da versão 2 de classificação de heap otimizada e complexidade de tempo

Em comparação com o método de ajuste ascendente para construir um heap,a complexidade de tempo de construir um heap usando o método de ajuste descendente é melhor. Anteriormente usávamos o método de ajuste ascendente para construir uma pilha, então como usamos o método de ajuste descendente para construir uma pilha?

Sabemos que usar o método de ajuste ascendente para construir um heap começa no último nó, encontra seu nó pai e então o compara para concluir a construção do heap.

O número total de ajustes realizados: o número de dados em cada camada * o número de camadas movidas para cima ou para baixo

Cálculo da complexidade de tempo do método de ajuste ascendente
Conforme mostrado na figura:
Insira a descrição da imagem aqui
Construção de heap do método de ajuste descendente:
O método de ajuste para baixo usado anteriormente é ajustar para baixo a partir do topo da pilha e compará-los em sequência. Em vez de começar no topo da pilha, ajuste a partir de baixo. Encontre o último nó pai (desde que o nó pai deva ter nós filhos), compare e ajuste; depois vá para o nó pai anterior para realizar as operações anteriores e, finalmente, construa um heap.

Encontre o último nó pai:
O último nó filho: child = n-1
Se houver um filho, encontre o pai: ( child-1)/ 2
Portanto, o último nó pai é: (n - 1 - 1) / 2

Cálculo da complexidade de tempo do método de ajuste para baixo
Conforme mostrado na figura:
Insira a descrição da imagem aqui
Em comparação: o número total de ajustes para cima é maior que o número total de ajustes para baixo 2 ^ (h-1), o que significa que há mais uma camada (a última camada) de cálculo. A última camada representa cerca de metade do total, por isso é melhor ajustar a pilha mágica para baixo.

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)Problema TopK

Suponha que haja N dados, encontre os K maiores

1. Leia os primeiros K dados do arquivo e crie um pequeno heap na matriz de memória 2. Leia os seguintes dados em sequência Os dados, são comparados com o elemento superior do heap. Contanto que sejam maiores que o elemento superior do heap, substitua o elemento superior do heap pelo heap < a i=4> e depois ajuste para baixo 3. Depois de ler todos os dados, os dados no heap serão os K do topo

Este método é muito inteligente. Construa um pequeno heap de modo que o elemento no topo do heap seja o menor do heap. Contanto que seja maior do que ele, ele será trocado no heap. Dados grandes entram no heap e vão para a parte inferior do heap, apenas os K maiores dados do heap não são trocados. Quando há os K maiores elementos no topo do heap, por ser um heap pequeno, o topo do heap é o menor elemento do heap, mas é maior que os elementos que não estão no heap. Mesmo que já existam K-1 elementos no heap no início, dentro do intervalo dos primeiros K maiores elementos de todos os elementos, se você ajustar para baixo para manter o heap pequeno, deve haver um elemento no topo do heap e este elemento é menor do que alguns dados subsequentes. Depois de ler o elemento a seguir, troque-o e ajuste-o para baixo. Os K maiores são encontrados.
Se você estiver construindo um heap grande, o elemento superior do heap grande será o maior do heap. Somente se for maior do que ele poderá ser adicionado ao heap. Se o valor inicial heap é construído, o elemento superior do heap será todo o maior dos elementos, isso não bloquearia todos os elementos?

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;
}

Insira a descrição da imagem aqui
Obrigado por assistir~

Acho que você gosta

Origin blog.csdn.net/2301_77459845/article/details/132777524
Recomendado
Clasificación