[데이터 구조] 이진 트리 체인 구조의 구현 및 공통 작업

목차

1. 이진 트리를 손으로 문지르기

2. 이진 트리 순회

2.1 선주문, 중위, 후위 순회

2.2 이진 트리의 레벨 순회

3. 이진 트리의 일반적인 작업

3.1 이진 트리 노드 수 찾기

3.2 이진 트리의 리프 노드 수 찾기

3.3 이진 트리의 k번째 계층에서 노드 수 찾기

3.3 이진 트리의 깊이 찾기

3.4 이진 트리에서 값이 x인 노드 찾기

4. 이진 트리의 파괴


1. 이진 트리를 손으로 문지르기

이진 트리의 기본 작업을 배우기 전에 관련 기본 작업을 배우기 전에 이진 트리를 만들어야 합니다. 우리는 이진 트리 구조에 대한 깊은 이해가 없기 때문에 학습 비용을 줄이기 위해 간단한 이진 트리를 만들고 빠르게 이진 트리 연산 학습에 들어가는 빠른 매뉴얼입니다. 거의 이해했다면 돌아가서 이진 트리의 실제 생성을 연구할 것입니다.

 이진 트리의 노드를 정의합니다.

typedef int BTDataType;
typedef struct BinaryTreeNode
{
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
	BTDataType data;
}BTNode;

위의 이진 트리 구조에 따른 필기 이진 트리:

BTNode* BuyNode(BTDataType data)
{
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	assert(node);
	node->data = data;
	node->left = node->right = NULL;
	return node;
}

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

이런 식으로 간단한 이진 트리를 작성했습니다.

2. 이진 트리 순회

2.1 선주문, 중위, 후위 순회

이진 트리 구조를 배우는 가장 쉬운 방법은 순회하는 것입니다. 소위 이진 트리 순회(Traversal)는 특정 특정 규칙에 따라 이진 트리의 노드에서 차례로 해당 작업을 수행하는 것이며 각 노드는 한 번만 작동합니다. 액세스 노드가 수행하는 작업은 특정 애플리케이션 문제에 따라 다릅니다. 순회는 이진 트리에서 가장 중요한 작업 중 하나이며 이진 트리에서 다른 작업의 기초이기도 합니다.

 규칙에 따라 이진 트리의 순회에는 다음이 포함됩니다. 전위/중위/후위 재귀 구조 순회:

  1. Preorder Traversal(Preorder Traversal, preorder traversal이라고도 함) - 루트 노드를 방문하는 작업은 왼쪽 및 오른쪽 하위 트리를 순회하기 전에 발생합니다.
  2. Inorder Traversal (Inorder Traversal) - 루트 노드를 방문하는 작업은 왼쪽 및 오른쪽 하위 트리(가운데)를 순회하면서 발생합니다.
  3. 후위 순회 - 루트 노드를 방문하는 작업은 왼쪽 및 오른쪽 하위 트리를 순회한 후에 발생합니다.

방문한 노드는 특정 하위 트리의 루트여야 하므로 N(노드), L(왼쪽 하위 트리) 및 R(오른쪽 하위 트리)은 루트, 루트의 왼쪽 하위 트리, 루트의 오른쪽 하위 트리로 해석할 수 있습니다. . NLR, LNR, LRN은 각각 first-root traversal, middle-root traversal, back-root traversal이라고도 합니다.

이진 트리 순회 코드는 매우 간단하게 작성할 수 있지만 초보자에게는 다소 이해하기 어려운 순회 코드는 다음 세 가지이며 먼저 살펴볼 수 있습니다.

선주문 순회

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

중위 순회

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

사후 순회

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

코드를 읽은 후 이 세 순회가 매우 유사하다고 생각하십니까? 컴파일러에서 세 순회의 코드를 실행해 보겠습니다.

int main()
{
	BTNode* root = CreatBinaryTree();
	PreOrder(root);
	printf("\n");
	InOrder(root);
	printf("\n");
	PostOrder(root);
	return 0;
}

작업 결과:

 선주문 순회의 재귀적 확장 그래프:

 중위 순회 및 후속 순회는 이 다이어그램과 유사합니다.

2.2 이진 트리의 레벨 순회

이진 트리의 레벨 순회는 너비 우선 검색(BFS) 방법입니다. 즉, 루트 노드에서 시작하여 먼저 첫 번째 계층 노드를 순회한 다음 두 번째 계층 노드를 순회하는 식으로 모든 계층이 순회될 때까지 계층 순서로 계층별로 이진 트리를 순회합니다.

레벨 순서 순회를 구현하는 일반적인 방법은 대기열을 사용하는 것입니다. 구체적인 아이디어는 다음과 같습니다.

  1. 빈 대기열을 만들고 루트 노드를 대기열에 넣습니다.
  2. 대기열이 비워질 때까지 다음 단계를 반복합니다.
  • 노드를 대기열에서 빼서 해당 값을 결과 목록에 저장합니다.
  • 노드에 왼쪽 자식이 있으면 왼쪽 자식을 대기열에 넣습니다.
  • 노드에 올바른 자식이 있으면 올바른 자식을 대기열에 넣습니다.

이와 같이 Queue가 비었을 때 순회 프로세스가 완료되고 계층적 순회 결과가 결과 목록에 저장됩니다.

 다음 코드는 큐를 손으로 쓰는 문제를 피하기 위해 C++STL의 큐를 사용합니다.

void LevelOrder(BTNode* root)
{
	assert(root);
	queue<BTNode*> a;
	a.push(root);
	while (!a.empty())
	{
		BTNode* front = a.front();
		a.pop();
		printf("%d ", front->data);
		if (front->left)
		{
			a.push(front->left);
		}
		if (front->right)
		{
			a.push(front->right);
		}
	}
}

int main()
{
	BTNode* root = CreatBinaryTree();
	LevelOrder(root);
	return 0;
}

출력 결과:

3. 이진 트리의 일반적인 작업

3.1 이진 트리 노드 수 찾기

방법 1:

전역 변수 수를 정의한 다음 각 노드를 순회합니다. 노드가 순회할 때마다 수는 1씩 증가합니다.

암호:

int count = 0;
void TreeSize1(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	count++;
	TreeSize1(root->left);
	TreeSize1(root->right);
}
int main()
{
	BTNode* root = CreatBinaryTree();
	count = 0;
	TreeSize1(root);
	printf("%d\n", count);
	return 0;
}

방법 2:

int TreeSize2(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	return TreeSize2(root->left) + TreeSize2(root->right) + 1;
}
int main()
{
	BTNode* root = CreatBinaryTree();
	printf("TreeSize2: %d\n", TreeSize2(root));
	return 0;
}

먼저 루트 노드가 비어 있는지 확인하고 비어 있으면 빈 트리임을 의미하며 직접 0을 반환합니다. 루트 노드가 비어 있지 않으면 자신을 재귀적으로 호출하여 왼쪽 및 오른쪽 하위 트리의 노드 수를 세고 왼쪽 하위 트리의 노드 수, 오른쪽 하위 트리의 노드 수 및 루트 수를 더합니다. 노드 자체(노드 1개), 최종적으로 결과를 반환합니다.

이 재귀 방식으로 이 함수는 이진 트리에 있는 모든 노드의 총 수를 계산할 수 있습니다.

3.2 이진 트리의 리프 노드 수 찾기

구체적인 아이디어:

먼저 루트 노드가 비어 있는지 확인하고 비어 있으면 빈 트리임을 의미하며 직접 0을 반환합니다. 다음으로, 루트 노드의 왼쪽 하위 트리와 오른쪽 하위 트리가 모두 비어 있는지를 판단하여 현재 노드가 리프 노드인지 여부를 판단합니다 . 리프 노드인 경우 1을 반환합니다. 리프 노드가 아니면 자신을 재귀적으로 호출하여 왼쪽 하위 트리와 오른쪽 하위 트리의 리프 노드 수를 계산하고 결과로 추가하여 반환합니다.

int TreeLeafSize(BTNode* root)
{
	if (root == NULL) return 0;
	if (root->left == NULL && root->right == NULL) return 1;
	return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
int main()
{
	BTNode* root = CreatBinaryTree();
	printf("TreeLeafSize: %d\n", TreeLeafSize(root));
	return 0;
}

3.3 이진 트리의 k번째 계층에서 노드 수 찾기

아이디어:

먼저 루트 노드가 비어 있는지 확인하고 비어 있으면 빈 트리임을 의미하며 직접 0을 반환합니다. k가 1이면 현재 레이어가 대상 레이어이고 1을 반환한다는 의미입니다. k가 1보다 크면 자신을 재귀적으로 호출하여 왼쪽 및 오른쪽 하위 트리에서 k-1 수준의 노드 수를 세고 모두 더하여 결과로 반환합니다.

이 재귀 방식으로 함수는 이진 트리에서 k번째 수준 노드의 총 수를 계산할 수 있습니다.

int TreeKLevel(BTNode* root, int k)
{
	assert(k >= 1);
	if (root == NULL) return 0;
	if (k == 1) return 1;
	return TreeKLevel(root->left, k - 1) + TreeKLevel(root->right, k - 1);
}
int main()
{
	BTNode* root = CreatBinaryTree();
	printf("TreeKLevel: %d\n", TreeKLevel(root, 2));//第2层节点数量
	printf("TreeKLevel: %d\n", TreeKLevel(root, 3));//第3层节点数量
	printf("TreeKLevel: %d\n", TreeKLevel(root, 4));//第4层节点数量
	return 0;
}

함수의 재귀 확장 그래프:

3.3 이진 트리의 깊이 찾기

int TreeDepth(BTNode* root)
{
	if (root == NULL) return 0;
	int l = TreeDepth(root->left); //左子树的深度
	int r = TreeDepth(root->right); //右子树的深度
	return (l > r ? l : r) + 1; //返回左右子树深度的较大值加自身的深度1
}
int main()
{
	BTNode* root = CreatBinaryTree();
	printf("TreeDepth: %d\n", TreeDepth(root));
	return 0;
}

먼저 루트 노드가 비어 있는지 확인하고 비어 있으면 빈 트리임을 의미하며 깊이 0으로 바로 돌아갑니다. 다음으로 함수는 자신을 재귀적으로 호출하여 왼쪽 및 오른쪽 하위 트리의 깊이를 계산하고 결과를 변수 l 및 r에 각각 저장합니다. 그런 다음 l과 r의 크기를 비교하여 더 큰 값을 선택하고 여기에 전체 이진 트리의 깊이로 1(현재 노드의 깊이를 나타냄)을 더합니다.

이 재귀적인 방법으로 함수는 이진 트리의 깊이(높이)를 계산할 수 있습니다.

3.4 이진 트리에서 값이 x인 노드 찾기

이진 트리에서 값이 x인 노드를 찾을 때 사전 순회를 사용하여 효율성을 높이십시오.

BTNode* TreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL) return NULL;
	if (root->data == x) return root;
	BTNode* ret1 = TreeFind(root->left, x);
	if (ret1)
	{
		return ret1;
	}
	BTNode* ret2 = TreeFind(root->right, x);
	if (ret2)
	{
		return ret2;
	}
	return NULL;
}

int main()
{
	BTNode* root = CreatBinaryTree();
	BTNode* ret = TreeFind(root, 3);
	if (ret)
	{
		printf("找到了:%d\n", ret->data);
	}
	else
	{
		printf("找不到\n");
	}
	return 0;
}

먼저 루트 노드가 비어 있는지 확인하고 비어 있으면 빈 트리임을 의미하며 직접 NULL을 반환합니다. 다음으로 함수는 현재 노드의 데이터가 목표 값 x와 같은지 확인하고 같으면 목표 노드를 찾았다는 의미이며 현재 노드에 대한 포인터를 반환합니다. 같지 않으면 함수를 재귀적으로 호출하여 각각 왼쪽 하위 트리와 오른쪽 하위 트리에서 대상 값 x를 찾고, 반환된 포인터가 비어 있지 않으면 하위 트리에서 대상 노드를 찾았다는 의미이며 포인터를 직접 반환합니다. . 왼쪽 및 오른쪽 하위 트리에서 대상 노드를 찾을 수 없으면 NULL이 반환됩니다.

이 재귀를 통해 함수는 이진 트리에서 특정 값의 노드를 조회하고 해당 노드에 대한 포인터를 반환할 수 있습니다. 대상 값을 찾을 수 없으면 NULL을 반환합니다.

4. 이진 트리의 파괴

이진 트리의 파괴를 위해 우리는 전위 순회를 사용할 수 없는데, 왜냐하면 우리가 전위 순회를 사용하면 이진 트리의 루트 노드가 먼저 파괴되어 루트 노드의 왼쪽 및 오른쪽 하위 트리를 찾을 수 없기 때문입니다. 순회를 위해서는 노드의 왼쪽 하위 트리와 오른쪽 하위 트리를 먼저 저장해야 합니다. 하지만 post-order traversal을 사용하면 쉽게 해결할 수 있다.

사후 순회를 사용하여 왼쪽 자식, 오른쪽 자식 및 루트 노드의 순서로 이진 트리를 파괴합니다.

void TreeDestory(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	TreeDestory(root->left);
	TreeDestory(root->right);
	free(root);
}
int main()
{
	BTNode* root = CreatBinaryTree();
	PreOrder(root);
	TreeDestory(root);
	root = NULL;
}

추천

출처blog.csdn.net/m0_73648729/article/details/132289236