Data structure (C language implementation) - the concept of binary tree and the realization of binary tree sequence structure and chain structure (heap sort + TOP-K problem + chain binary tree related operations)

1 Introduction

Previously, we have learned several structures of linear structures in data structures, such as sequential lists, linked lists, stacks and queues, etc. Today we will learn a non-linear data structure-tree. Since the binary tree is an important and difficult point in the data structure, this article focuses on introducing the related concepts and properties of the binary tree, as well as the application of the binary tree.

2. The concept and structure of the tree

2.1 The concept of a tree

A tree is a non-linear data structure, which is a set of hierarchical relationships composed of n (n>=0) finite nodes. It is called a tree because it looks like an upside-down tree, which means it has the roots pointing up and the leaves pointing down.
Note:
1. The tree is defined recursively
2. In the tree structure, there cannot be intersection between subtrees, otherwise it is not a tree structure

insert image description here

2.2 Related concepts of trees

If there is a tree as shown in the figure below:
insert image description here
then it has the following concepts:

Degree of a node : The number of subtrees contained in a node is called the degree of the node; As shown in the figure above: A is a 6-
leaf node or terminal node : a node with a degree of 0 is called a leaf node; As shown in the figure above: B, C, Nodes such as H, I... are leaf nodes,
non-terminal nodes or branch nodes : nodes whose degree is not 0; as shown in the above figure: nodes such as D, E, F, G... are branch nodes.
Parent nodes or parent nodes : if a node contains children node, then this node is called the parent node of its child node; as shown in the figure above: A is the parent node of B child node
or child node : the root node of the subtree contained in a node is called the child node of the node; as shown in the figure above: B It is a child node of A.
Brother nodes : nodes with the same parent node are called sibling nodes; as shown in the figure above: B and C are the
degree of the sibling node tree : in a tree, the degree of the largest node is called the degree of the tree; as above Figure: The level of the tree is 6
nodes : starting from the definition of the root, the root is the first level, the child nodes of the root are the second level, and so on; the
height or depth of the tree : the maximum level of nodes in the tree; as above Figure: The height of the tree is 4
Cousin nodes :
the nodes whose parents are on the same layer are cousins ; Figure: A is the ancestor and descendant of all nodes
: any node in the subtree rooted at a certain node is called the descendant of the node. As shown in the figure above: All nodes are descendants of A
Forest : A collection of m (m>0) disjoint trees is called a forest;

2.3 Tree Representation

When the tree is stored, it is necessary to save both the data and the relationship between the nodes. In practice, there are many representation methods for the tree, such as: parent representation, child representation, child parent representation, and child siblings notation etc. Below we introduce the most commonly used child brother notation.

typedef int TreeDataType;
struct TreeNode
{
    
    
	TreeDataType data;//结点的数据域
	struct TreeNode* FirstChild;//指向其第一个孩子结点
	struct TreeNode* NextBrother;//指向其下一个兄弟结点
};

insert image description here

3. The concept of binary tree

A binary tree is a finite set of nodes, the set:
1. Or empty
2. It consists of a root node plus two binary trees called left subtree and right subtree respectively

As shown in the figure below, it is a binary tree:
insert image description here
From the figure above, we can see that:

1. There is no node with a degree greater than 2 in a binary tree
2. The subtrees of a binary tree are divided into left and right, and the order cannot be reversed, so a binary tree is an ordered tree

3.1 Special binary tree

There are also two special binary trees in the binary tree:

1. Full binary tree : a binary tree, if the number of nodes in each layer reaches the maximum value, then this binary tree is a full binary tree. That is to say, if a binary tree has K layers and the total number of nodes is 2^k - 1, then it is a full binary tree.

insert image description here

2. Complete binary tree : A complete binary tree is a very efficient data structure, and a complete binary tree is derived from a full binary tree. For a binary tree with a depth of K and n nodes, it is called a complete binary tree if and only if each node has a one-to-one correspondence with the nodes numbered from 1 to n in the full binary tree with a depth of K. It should be noted that a full binary tree is a special kind of complete binary tree.

insert image description here

3.2 Properties of Binary Trees

1. If the number of layers of the root node is specified as 1, then there are at most 2^(i-1) nodes on the i-th layer of a non-empty binary tree. 2. If the number of layers of the
root node is specified as 1, the depth is The maximum number of nodes in the binary tree of h is 2^h-1
3. For any binary tree, if the number of leaf nodes is n0 with degree 0, and the number of branch nodes with degree 2 is n2, then there is n0 = n2 + 1
4. If the number of layers of the root node is specified as 1, the depth of a full binary tree with n nodes, h = log2(n+1)
5. For a complete binary tree with n nodes, if according to the above All nodes are numbered starting from 0 in the order of the array from left to right,
then for the node with the serial number i:
(1) If i > 0, the parental serial number of the node at i position: (i - 1) / 2; If i = 0, then i is the root node number, no parent node
(2) if 2i + 1 < n, the left child number: 2i + 1, if 2i + 1 >= n, there is no left child
(3) if 2i + 2 < n, right child number: 2i + 2, if 2i + 2 >= n, there is no right child

4. Sequential storage of binary trees

Sequential structure storage is to use arrays for storage. Generally, arrays are only suitable for representing complete binary trees, because not complete binary trees will waste space.
Binary tree sequential storage is physically an array and logically a binary tree.
In reality, only the heap will use arrays for storage.

4.1 The concept of heap

All elements are stored in a one-dimensional array in the order of a complete binary tree. The heap with the largest root node is called the largest heap or large root heap, and the heap with the smallest root node is called the smallest heap or small root heap.
The value of a node in the heap is always not greater than or not less than the value of its parent node;
the heap is always a complete binary tree.

4.2 Implementation of the heap

4.2.1 Heap node definition

typedef int HPDataType;

typedef struct Heap
{
    
    
	HPDataType* a;
	int size;
	int capacity;
}HP;

4.2.2 Heap printing and destruction

Print:

void HeapPrint(HP* php)
{
    
    
	assert(php);
	int i = 0;
	for (i = 0; i < php->size; i++)
	{
    
    
		printf("%d ", php->a[i]);
	}
	printf("\n");
}

destroy:

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

4.2.3 Heap insertion

First insert a piece of data to the end of the array, and then perform an upward adjustment algorithm until the large root heap or small root heap is satisfied.
Upward adjustment algorithm: Take the small root heap as an example, start to find the parent node from this node, if the node is smaller than the parent node, exchange the node with the parent node, and continue to adjust upward until the small root is satisfied heap.

insert:

void HeapPush(HP* php, HPDataType x)
{
    
    
	assert(php);
	if (php->size == php->capacity)
	{
    
    
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
    
    
			perror("malloc");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}
	php->a[php->size] = x;
	php->size++;
	AdjustUp(php->a, php->size - 1);
}

Algorithm for upward adjustment:

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

exchange:

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

4.2.4 Heap deletion

Deleting the heap is to delete the data at the top of the heap, replace the data at the top of the heap with the last data, then delete the last data in the array, and then perform the downward adjustment algorithm.
Downward adjustment algorithm: Take the small root heap as an example, start from the node to find the child node downwards, if the child node is smaller than the node, exchange the child node with the node, and then continue to adjust downwards, until the small root pile is satisfied

delete:

void HeapPop(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);
}

Adjust the algorithm down:

void AdjustDown(HPDataType* a, int size, int parent)
{
    
    
	int child = parent * 2 + 1;
	while (child < size)
	{
    
    
		if (child + 1 < size && (a[child + 1] < a[child]))
		{
    
    
			child++;
		}
		if (a[child] < a[parent])
		{
    
    
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
    
    
			break;
		}
	}
}

4.2.5 Get the top data of the heap

The top element of the heap is the element whose subscript is 0

HPDataType HeapTop(HP* php)
{
    
    
	assert(php);
	assert(php->size > 0);
	return php->a[0];
}

4.2.6 Heap Empty Judgment

size is 0 is empty

bool HeapEmpty(HP* php)
{
    
    
	assert(php);
	return php->size == 0;
}

4.2.7 Number of data in the heap

The size of size is the number of data

int HeapSize(HP* php)
{
    
    
	assert(php);
	return php->size;
}

4.3 Application of the heap

4.3.1 Heap sort

Heap sorting is to use the idea of ​​heap to sort. It is divided into two steps:
1. Build a heap
Ascending order: build a large heap
Descending order: build a small heap
2. Use the heap to delete the idea to sort

void HeapSort(int* arr, int size)
{
    
    
	//排升序建大堆,排降序建小堆
	//向上调整建堆O(N*logN)
	//int i = 0;
	//for (i = 1; i < size; i++)
	//{
    
    
	//	AdjustUp(arr, i);
	//}

	//向下调整建堆O(N)
	int i = 0;
	for (i = (size - 1 - 1) / 2; i >= 0; i--)
	{
    
    
		AdjustDown(arr, size, i);
	}

	int end = size - 1;
	while (end > 0)
	{
    
    
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, end, 0);
		end--;
	}
}

int main()
{
    
    
	int arr[10] = {
    
     23,45,48,123,12,49,80,15,5,35 };
	HeapSort(arr, 10);
	int i = 0;
	for (i = 0; i < 10; i++)
	{
    
    
		printf("%d ", arr[i]);
	}
	return 0;
}

4.3.2 TOP-K problem

TOP-K problem: Find the top K largest elements or smallest elements in the data combination. Generally, the amount of data is relatively large.
For example: the top 10 professional players, the world's top 500, the rich list, the top 100 active players in the game, etc.
Ideas:
1. Use the first K elements in the data set to build a heap. Find
the first k largest elements, then build a small heap.
Find the first k smallest elements, then build a large heap
. 2. Use the remaining N - K elements in order Compare with the top element of the heap, if not satisfied, replace the top element of the heap
After comparing the remaining N - K elements with the top element of the heap in turn, the remaining K elements in the heap are the first K smallest or largest elements sought.

void PrintTopK(int* a, int n, int k)
{
    
    
	//1.建堆--用a中前k个元素建堆
	int* kMaxHeap = (int*)malloc(sizeof(int)*k);
	assert(kMaxHeap);
	int i = 0;
	for (i = 0; i < k; i++)
	{
    
    
		kMaxHeap[i] = a[i];
	}
	for (i = (k - 1 - 1) / 2; i >= 0; i--)
	{
    
    
		AdjustDown(kMaxHeap, k, i);
	}

	//2.将剩余n-k个元素依次与堆顶元素交换,不满则则替换
	int j = 0;
	for (j = k; j < n; j++)
	{
    
    
		if (a[j] > kMaxHeap[0])
		{
    
    
			kMaxHeap[0] = a[j];
			AdjustDown(kMaxHeap, k, 0);
		}
	}
	for (i = 0; i < k; i++)
	{
    
    
		printf("%d ", kMaxHeap[i]);
	}
}

void TestTopk()
{
    
    
	int n = 10000;
	int* a = (int*)malloc(sizeof(int) * n);
	srand(time(0));
	for (int i = 0; i < n; i++)
	{
    
    
		a[i] = rand() % 1000000;
	}
	a[5] = 1000000 + 1;
	a[1231] = 1000000 + 2;
	a[531] = 1000000 + 3;
	a[5121] = 1000000 + 4;
	a[115] = 1000000 + 5;
	a[2335] = 1000000 + 6;
	a[9999] = 1000000 + 7;
	a[76] = 1000000 + 8;
	a[423] = 1000000 + 9;
	a[3144] = 1000000 + 10;
	PrintTopK(a, n, 10);
}

int main()
{
    
    
	TestTopk();
	return 0;
}

5. Chain storage of binary tree

The linked storage structure of the binary tree means that a linked list is used to represent a binary tree, that is, a link is used to indicate the logical relationship of elements.
The usual method is that each node in the linked list is composed of three fields, the data field and the left and right pointer fields, and the left and right pointers are used to give the storage addresses of the link points where the left child and right child of the node are located.
The chain structure is further divided into two-fork chain and three-fork chain.

5.1 Node definition of chained binary tree

typedef int BTDataType;

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

5.2 Node Creation

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

Since the creation of a binary tree is more complicated, let's manually simulate a simple binary tree to facilitate the implementation of subsequent operations.

5.3 Simulation to create a binary tree

BTNode* CreatBinaryTree()
{
    
    
	BTNode* node1 = CreatBTNode(1);
	BTNode* node2 = CreatBTNode(2);
	BTNode* node3 = CreatBTNode(3);
	BTNode* node4 = CreatBTNode(4);
	BTNode* node5 = CreatBTNode(5);
	BTNode* node6 = CreatBTNode(6);

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

	return node1;
}

5.4 Binary tree traversal

The traversal of the binary tree includes: recursive structure traversal of preorder/inorder/postorder/level order:
1. Preorder traversal (Preorder Traversal, also known as preorder traversal) - the operation of visiting the root node occurs when traversing its left and right subtrees Before.
2. Inorder Traversal (Inorder Traversal) - the operation of visiting the root node occurs in traversing its left and right subtrees (between).
3. Postorder Traversal——The operation of visiting the root node occurs after traversing its left and right subtrees.
4. Level Traversal (Level Traversal) - Starting from the root node of the binary tree where it is located, first visit the root node of the first level, then visit the nodes on the second level from left to right, and then the third level By analogy, the process of visiting the nodes of the tree layer by layer from top to bottom and from left to right is layer order traversal.

5.4.1 Preorder traversal

void PreOrder(BTNode* root)
{
    
    
	if (root == NULL)
	{
    
    
		printf("# ");
		return;
	}

	printf("%d ", root->data);
	PreOrder(root->left);
	PreOrder(root->right);
}

5.4.2 Inorder traversal

void InOrder(BTNode* root)
{
    
    
	if (root == NULL)
	{
    
    
		printf("# ");
		return;
	}

	InOrder(root->left);
	printf("%d ", root->data);
	InOrder(root->right);
}

5.4.3 Postorder traversal

void PostOrder(BTNode* root)
{
    
    
	if (root == NULL)
	{
    
    
		printf("# ");
		return;
	}

	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->data);
}

5.4.4 Level order traversal

Hierarchical traversal is a bit more complicated and requires a queue to implement. We will use the previously created queue here, and will not show it too much. The core idea is: first judge whether the node is empty, if not, enter the queue, and then judge Whether the team is empty, if you don’t first print out the team head element at this time, then delete the team head element, and then judge whether the left child and right child of the printed team head element are empty, and continue to join the team if they are not empty. In this way, loop iteration is layer sequence traversal.

void LevelOrder(BTNode* root)
{
    
    
	Queue q;
	QueueInit(&q);

	if (root)
	{
    
    
		QueuePush(&q, root);
	}
	while (!QueueEmpty(&q))
	{
    
    
		BTNode* front = QueueFront(&q);
		printf("%d ", front->data);
		QueuePop(&q);
		if (front->left)
		{
    
    
			QueuePush(&q, front->left);
		}
		if (front->right)
		{
    
    
			QueuePush(&q, front->right);
		}
	}
	QueueDestroy(&q);
}

5.5 Operations such as the number and height of binary tree nodes

5.5.1 The number of nodes in a binary tree

Because the left subtree and right subtree of a binary tree can be regarded as separate binary trees, the core idea is to recursively calculate the number of nodes in the left subtree and right subtree, and finally return and add 1 to the number of nodes in the binary tree number.

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

5.5.2 Number of leaf nodes in binary tree

The node whose left and right subtrees are both empty is the leaf node. Recursive thinking is also used here to divide the binary tree into left subtree and right subtree. The left subtree can also be divided into left subtree and right subtree. Recursive calculation And return the number of leaf nodes of the whole tree that can be found.

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

	return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

5.5.3 The number of nodes in the kth layer of the binary tree

The idea here is that finding the number of nodes in the kth layer of the root node can be converted into finding the number of nodes in the k-1th layer of the left child of the node + the kth of the right child of the node - The number of nodes in layer 1, recursively, and the number of nodes in the kth layer of the root node will be returned after the end.

int BinaryTreeLevelKSize(BTNode* root, int k)
{
    
    
	assert(k >= 1);
	if (root == NULL)
	{
    
    
		return 0;
	}
	if (k == 1)
	{
    
    
		return 1;
	}

	return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);

}

5.5.4 Find the node with the value x in the binary tree

The idea here is that if the node is the required node, return the node directly, if not, recursively search the left subtree and right subtree of the node, and return if found.

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

5.5.5 Computing the depth of a binary tree

The depth of the binary tree is the maximum depth of the left subtree and the right subtree of the binary tree plus 1. The idea here is to recursively calculate the depth of the left subtree and the depth of the right subtree. Each return is based on a certain node The depth of the binary tree as the root, and the last return is the depth of the binary tree.

int BinaryTreeDepth(BTNode* root)
{
    
    
	if (root == NULL)
	{
    
    
		return 0;
	}

	int LeftDepth = BinaryTreeDepth(root->left);
	int RightDepth = BinaryTreeDepth(root->right);

	return LeftDepth > RightDepth ? LeftDepth + 1 : RightDepth + 1;

}

5.5.6 Determine whether it is a complete binary tree

Queues are also used here. The core idea is to use the properties of a complete binary tree. The non-empty nodes of a complete binary tree must be connected together. Once an empty node appears, all nodes behind the empty node should be empty. If there are still non-empty nodes, it is not a complete binary tree.

int BinaryTreeComplete(BTNode* root)
{
    
    
	Queue q;
	QueueInit(&q);

	if (root)
	{
    
    
		QueuePush(&q, root);
	}
	while (!QueueEmpty(&q))
	{
    
    
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front)
		{
    
    
			QueuePush(&q, front->left);
			QueuePush(&q, front->right);
		}
		else
		{
    
    
			break;
		}
	}
	while (!QueueEmpty(&q))
	{
    
    
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front)
		{
    
    
			QueueDestroy(&q);
			return false;
		}
	}
	QueueDestroy(&q);
	return true;
}

6. Ending

So far, the concept of binary tree and the implementation and application of binary tree sequence structure and chain structure have been introduced. Binary tree, as the key and difficult points in data structure, needs repeated study and thorough understanding. It is a test of everyone's understanding ability and basic skills. I am also a For beginners, there are inevitably many mistakes and omissions in this article. This article is only for your study and reference. If this article is helpful for everyone to learn binary trees, the blogger is very honored.
Finally, I would like to thank you all for your patience and support. Friends who feel that this article is well written can follow and support it three times. If you have any questions or there are mistakes in this article, you can private message me or leave a message in the comment area Discussion, thanks again everyone.

Guess you like

Origin blog.csdn.net/qq_43188955/article/details/130223760