"Manual de cultivo de estructura de datos"----La implementación del árbol binario encadenado

1. Instrucciones preliminares

¿Por qué construir un árbol binario encadenado?

Para almacenar una estructura de árbol binario incompleta, es decir, una estructura de árbol binario irregular, los nodos intermedios no pueden almacenar elementos. P.ej:

imagen-20220409151527822

Nota: No tiene sentido agregar, eliminar, verificar y modificar un árbol binario común, si es solo para almacenar datos, es mejor usar la estructura del árbol binario de la tabla de secuencia. ?

P: Entonces, ¿por qué aprender árboles binarios encadenados?

Respuesta: Para poder controlar mejor su estructura, sentar las bases para el posterior aprendizaje de árboles binarios de búsqueda más complejos. Además, muchos problemas OJ de árboles binarios están en árboles binarios ordinarios.

Simplemente cree un árbol binario encadenado:

typedef int BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType data;//节点存储的数据
	struct BinaryTreeNode* left;//存储左子节点的地址
	struct BinaryTreeNode* right;//存储右子节点的地址
}BTNode;
BTNode* BuyNode(BTDataType x)//开辟一个新节点
{
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	assert(newnode);
	newnode->data = x;
	newnode->left = NULL;
	newnode->right = NULL;
	return newnode;
}
BTNode* CreatBinaryTree()
{
	BTNode* node1 = BuyNode(1);
	BTNode* node2 = BuyNode(2);
	BTNode* node3 = BuyNode(3);
	BTNode* node4 = BuyNode(4);
	BTNode* node5 = BuyNode(5);
	BTNode* node6 = BuyNode(6);

	node1->left = node2;
	node1->right = node4;
	node2->left = node3;
	node4->left = node5;
	node4->right = node6;
	return node1;
}

Nota: El código anterior no es la forma de crear un árbol binario, la forma real de crear un árbol binario se explica en detalle después de la secuencia.

Antes de ver las operaciones básicas de un árbol binario, repasemos el concepto de un árbol binario.Un árbol binario es:

  1. arbol vacio
  2. No vacío: el nodo raíz, el subárbol izquierdo del nodo raíz y el subárbol derecho del nodo raíz.

imagen-20220409155057833

Como se puede ver en el concepto, la definición de un árbol binario es recursiva, por lo que las operaciones básicas del orden posterior se implementan básicamente de acuerdo con este concepto.

2. Recorrido del árbol binario

2.1 Recorrido en preorden, en orden y postorden (recorrido primero en profundidad (DFS))

El recorrido del árbol binario (Traversal) consiste en realizar las operaciones correspondientes en los nodos del árbol binario a su vez de acuerdo con una determinada regla, y cada nodo solo se opera una vez . Las operaciones realizadas por el nodo de acceso dependen del problema específico de la aplicación. El recorrido es una de las operaciones más importantes en un árbol binario y también es la base para otras operaciones en un árbol binario.

imagen-20220409155849494

imagen-20220409160040183

De acuerdo con las reglas, el recorrido del árbol binario incluye: preorder/inorder/postorder estructura recursiva transversal :

  1. Recorrido de preorden (también conocido como recorrido previo a la raíz): la operación de visitar el nodo raíz se produce antes de atravesar sus subárboles izquierdo y derecho. Es decir, el orden de acceso es: nodo raíz - subárbol izquierdo - subárbol derecho

    Tomando el diagrama de árbol binario anterior como ejemplo, el recorrido de preorden se muestra en la figura:

    imagen-20220409161302347

  2. Recorrido en orden (también conocido como recorrido de raíz media): el acceso al nodo raíz se produce durante el recorrido (medio) de sus subárboles izquierdo y derecho. Es decir, el orden de acceso es: subárbol izquierdo - nodo raíz - subárbol derecho

    El recorrido en orden se muestra en la figura:

    imagen-20220409165050924

  3. Recorrido posterior al pedido (también conocido como recorrido posterior a la raíz): el acceso a la raíz se produce después de atravesar sus subárboles izquierdo y derecho. Es decir, el orden de acceso es: subárbol izquierdo - subárbol derecho - nodo raíz

    El recorrido posterior al pedido se muestra en la figura:

    imagen-20220409171418202

2.2 Implementación de tres recorridos

2.2.1 Implementación del recorrido previo al pedido

Código:

void PreOrder(BTNode*root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	printf("%d ", root->data);//遍历根节点
	PreOrder(root->left);//遍历左子树节点
	PreOrder(root->right);//遍历右子树节点
}

Ilustración: Los números entre ( ) son el orden de ejecución del código

imagen-20220409200219445

2.2.2 Implementación del recorrido en orden

Código:

void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	InOrder(root->left);
	printf("%d ", root->data);
	InOrder(root->right);
}

Icono:

imagen-20220409210540857

2.2.3 Implementación del traspaso posterior al pedido

Código:

void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	InOrder(root->left);
	InOrder(root->right);
	printf("%d ", root->data);
}

2.3 Recorrido de orden de nivel (recorrido primero en amplitud (BFS))

Recorrido por orden de capas: recorra directamente capa por capa, comenzando desde el nodo principal y recorriendo hacia atrás uno por uno. El orden de recorrido en la figura anterior es: 1-2-4-3-5-6

Ideas:

  1. Primero coloque la raíz en la cola, con la ayuda de la naturaleza de la cola de primero en entrar, primero en salir.
  2. Cuando el nodo de la capa anterior se apague, traer los nodos secundarios izquierdo y derecho de la siguiente capa en

Icono:

imagen-20220415154604840

realizar:

Nota: Use los dos archivos Queue.c y Queue.h, y defina el tipo de datos almacenado en la cola como BinaryTreeNode*, ¡preste atención a la inclusión del archivo de encabezado y la declaración de la estructura en el archivo Queue!

Modificaciones en el archivo Queue.h:

imagen-20220415152436909

Modificaciones en el archivo Test.c

imagen-20220415152615070

Código:

void LevelOrder(BTNode* root)
{
	Queue q;//队列的创建
	QueueInit(&q);
	if (root)//判断是否为空二叉树
	{
		QueuePush(&q, root);//将root节点push到队列中
	}
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);//拿到队列首元素
		QueuePop(&q);//将出队列的节点从队列中删掉
		if (front->left)
		{
			QueuePush(&q, front->left);//将左子节点push到队列中
		}
		if (front->right)
		{
			QueuePush(&q, front->right);//将右子节点push到队列中
		}
		printf("%d ", front->data);//打印出队列的数据
	}
	//对列的销毁
	QueueDestory(&q);
}

2.4 La aplicación del recorrido de orden de nivel----evaluación del árbol binario completo

Ideas:

  1. Recorrido de orden de nivel, los nodos vacíos también se ponen en cola.Nota: antes de ingresar a la cola, verifique si el frente está vacío, de lo contrario, habrá un error de desreferenciación de un puntero nulo.
  2. Después de salir a un nodo vacío, todos los datos en la cola de salida, si todos están vacíos, son un árbol binario completo, si no están vacíos, no lo están.

Icono:

imagen-20220415170651476

Código:

//判断一个二叉树是否是完全二叉树
bool BTreeComplete(BTNode* root)
{
	Queue q;//队列的创建
	QueueInit(&q);
	if (root)//判断是否为空二叉树
		QueuePush(&q, root);//将root节点push到队列中
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front == NULL)//出到空的时候退出,后面再进行判断
		{
			break;
		}
		//为什么要放到后面push呢?为了防止对空指针进行解引用
		QueuePush(&q, front->left);//此时front一定不为空
		QueuePush(&q, front->right);
	}
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		//空后面出现非空,就说明不是完全二叉树
		if (front)
		{
			return false;
		}
		QueuePop(&q);
	}
    //队列的销毁
	QueueDestory(&q);
	return true;
}

Nota : Considere la siguiente situación:

imagen-20220415172625693

imagen-20220415172901775

3. Número de nudos y altura, etc.

3.1 Número de nodos del árbol binario

Dos métodos:

método uno

Idea: Poligonal + Cuenta

Código:

int count = 0;
void BTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	count++;
	BTreeSize(root->left);
	BTreeSize(root->right);
}

Por supuesto, las variables estáticas locales también se pueden usar para almacenar la cantidad de nodos, pero no se recomienda. El código es el siguiente:

int BTreeSize(BTNode* root)
{
    static int count = 0;//只会在第一次进入时初始化一次
    if (root == NULL)
	{
		return count;
	}
	count++;
	BTreeSize(root->left);
	BTreeSize(root->right);
    return count;
}

Por supuesto, ninguno de los dos métodos anteriores es bueno, y ninguno de ellos aprovecha al máximo las propiedades estructurales de la definición recursiva de los árboles binarios encadenados, por lo que no se recomiendan los dos métodos anteriores. El segundo de los dos métodos anteriores es particularmente malo, especialmente cuando el número de elementos se calcula varias veces. Si el conteo no se reinicializa en el segundo cálculo, el valor del cálculo anterior aún se conservará. Si se usa Las variables globales también se pueden reasignar después de cada conteo, pero el segundo método no puede cambiar el valor residual después del último conteo.

Las variables globales tendrán problemas cuando se utilicen subprocesos múltiples. Por ejemplo, habrá problemas en situaciones de subprocesos múltiples: varios subprocesos utilizan el recuento de variables globales al mismo tiempo y se producirán problemas de seguridad de subprocesos en este momento.

Por supuesto, también puede definir una variable de conteo en la función de llamada y luego pasar la dirección de conteo al llamar, luego la función BTreeSize debe definirse así:

int BTreeSize(BTNode* root,size_t *count)
{
    static int count = 0;//只会在第一次进入时初始化一次
    if (root == NULL)
	{
		return count;
	}
	(*count)++;
	BTreeSize(root->left);
	BTreeSize(root->right);
    return count;
}

Método 2 (recomendado)

(método recursivo)

Idea: Subproblemas

1. Árbol vacío, subproblema de escala mínima, el número de nodos devuelve 0

2. No vacío, el número de nodos del subárbol izquierdo + el número de nodos del subárbol derecho + 1 (uno mismo)

Código:

int BTreeSize(BTNode* root)
{
    return root==NULL ? 0 : 
    BTreeSize(root->left) + 
    BTreeSize(root->right) + 1;//1在最前面就是前序,在中间就是中序,在最后就是后序
}

Este método aprovecha al máximo las propiedades estructurales de la definición recursiva del árbol binario encadenado, porque la esencia del árbol binario encadenado se compone del nodo raíz y dos subárboles. Encontrar el subárbol completo es encontrar el número de nodos de el nodo raíz 1 más los nodos de los subárboles izquierdo y derecho.

como muestra la imagen:

imagen-20220410105756117

Nota: Los números morados en la figura representan el valor de retorno y los números negros representan el valor de retorno de la función correspondiente.

La idea del algoritmo utilizada :divide y conquistaras

Divide y vencerás: divide los problemas complejos en subproblemas de menor escala, y los subproblemas se dividen luego en subproblemas de menor escala... hasta que los subproblemas ya no se puedan dividir y los resultados se puedan obtener directamente .

3.2 Número de nodos secundarios del árbol binario

método uno

Ideas: (recorrido + conteo)

Es básicamente lo mismo que la idea anterior, excepto que se agrega una condición de juicio de un nodo de hoja delante de count++, para lograr el propósito de contar los nodos de hoja.

Código:

int count = 0;
void BTreeLeafSize(BTNode* root)
{
	if (root == NULL)
		return 0;
	if (root->left == NULL&&root->right==NULL)
	{
		count++;
	}
	BTreeLeafSize(root->left);
	BTreeLeafSize(root->right);
}

Método dos

Ideas: Adopta la idea de divide y vencerás.

Código:

int BTreeLeafSize(BTNode* root)
{
	if (root == NULL)
		return 0;
	if (root->left == NULL&&root->right==NULL)
	{
		return 1;
	}
	else
		return BTreeLeafSize(root->left) + BTreeLeafSize(root->right);
}

Por supuesto, el código anterior se puede acortar al siguiente código:

int BTreeLeafSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	return root->left == NULL && root->right == NULL ? 
		1 + BTreeLeafSize(root->left)+ BTreeLeafSize(root->right) : 
		0 + BTreeLeafSize(root->left) + BTreeLeafSize(root->right);
}

Sin embargo, en términos relativos, se recomienda más el no simplificado, porque es más fácil de entender.

3.3 Número de nodos en la k-ésima capa de un árbol binario

Nota: k>=1

Pensamiento:

  1. Árbol vacío, devuelve 0
  2. no nulo, devuelve 1
  3. no vacío, y k > 1, convertir a buscarEl número de nodos en el nivel k-1 del subárbol izquierdo + el número de nodos en el nivel k-1 del subárbol derecho
int BTreeKLevelSize(BTNode* root,int k)
{
	assert(k >= 1);
	if (root == NULL||k<)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}
	return BTreeKLevelSize(root->left, k - 1) + 
           BTreeKLevelSize(root->left, k - 1);
}

Icono:

imagen-20220410135435235

imagen-20220410141749616

3.4 Profundidad del árbol binario

Ideas: La idea de divide y vencerás

La altura del árbol binario = la altura del subárbol izquierdo y la altura del subárbol derecho, el mayor + 1.

int BTreeDepth(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	int leftDepth = BTreeDepth(root->left);//左子树的深度
	int rightDepth = BTreeDepth(root->right);//y
	return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;//1是因为根节点本身也算作是一层
}

3.5 Árbol binario para encontrar un nodo con valor x

Ideas:divide y conquistaras Árbol binario = nodo raíz + subárbol izquierdo + subárbol derecho

  1. Comprobar si el nodo raíz actual está vacío
  2. Determinar si el nodo actual es el valor que buscamos
  3. Determinar si el nodo que estamos buscando existe en el subárbol izquierdo
  4. Determinar si el nodo que estamos buscando existe en el subárbol derecho
  5. El nodo que buscamos no existe en el árbol binario actual
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	//节点为空的情况:直接返回NULL
	if (root == NULL)
	{
		return NULL;
	}
	//当前节点就是我们要查找的节点:返回当前节点的地址
	if (root->data == x)
	{
		return root;
	}
	//左子树
	BTNode* leftRet = BinaryTreeFind(root->left, x);
	if (leftRet)
	{
		return leftRet;
	}
	//右子树
	BTNode* rightRet = BinaryTreeFind(root->right, x);
	if (rightRet)
	{
		return rightRet;
	}
	//都找不到的情况下返回NULL,就是说当前二叉树中不存在存储该值的节点
	return NULL;
}

Supongo que te gusta

Origin blog.csdn.net/m0_57304511/article/details/124342419
Recomendado
Clasificación