【数据结构】堆(下)+ 二叉树

发布于:2025-07-19 ⋅ 阅读:(25) ⋅ 点赞:(0)

上期回顾:【数据结构】树(堆)·上

一.堆的应用

1.1堆排序(向下调整在上一期)

向上调整算法建堆:

首先先回顾一下向上调整算法

void AdjustUP(HPDataType* arr, int child)
{
	int parent = (child - 1) / 2;//找到父结点的位置
	while (child > 0)//循环,确保父结点一定小于(大于)子结点
	{
		        //小堆:<
		        //大堆:>
				//     |
		        //     V
		if (arr[child] > arr[parent])
		{
			//调整位置
			Swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
			break;
	}
}

在上一期我们也学过了向下调整算法建堆,其是通过由下向上循环,通过确保父结点下方的是堆结构,而向上算法建队=堆就刚好反过来,是由上到下进行循环。

//堆排序
void HeapSort(int *arr,int n)
{
	//建堆--向下调整
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDOWN(arr, i, n);
	}
	//建堆--向上调整
	for (int i = 0; i < n; i++)
	{
		AdjustUP(arr, i);
	}
	//排序
	int end = n - 1;
	while (end > 0)
	{
		Swap(&arr[end], &arr[0]);
		AdjustDOWN(arr, 0, end);
		end--;
	}
}

1.2.TOP-K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

通过前面的学习可了解到,小堆的堆顶是数据的最小值,所以若想通过小堆找最小,就需要遍历所有的数据,在进行堆排序,这对于大数据肯定是不行的,

那么换一种思路,用小堆找最大数据:将前K个数据直接建小堆,然后再遍历剩下的n-k个数据,将其与堆顶进行比较,若比堆顶大,则替换堆顶,来回重复,最后构成一个新堆,该堆就是数据中前K大的数。

而找前K小的数据道理相同,所以就可以总结出:

找最大的前K个数据,建小堆

找最小的前K个数据,建大堆

实践:

先通过代码生成10万个数据,并保存在data.txt文件中

随后实现topK函数:

void TopK()
{
	int k =0;
	printf("请输入k:");
	scanf("%d", &k);
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen fail");
		exit(1);
	}
	//找最大的前K个数据--建小堆
	//找最小的前K个数据--建大堆
	int* minHeap = (int*)malloc(sizeof(int) * k);
	if (minHeap == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &minHeap[i]);
	}
	//minHeap -- 向下调整建堆
	for (int i = (k - 2) / 2; i >=- 0; i--)
	{
		//找最大的前K个数据--建小堆
		//找最小的前K个数据--建大堆
		AdjustDOWN(minHeap, i, k);
	}
	//遍历剩下的n-k个数据,跟堆顶比较,堆顶小替换堆顶数据
	int x = 0;
	while (fscanf(fout, "%d", &x) != EOF)
	{
	  //找最大>
      //找最小<
		if (x > minHeap[0])
		{
			minHeap[0] = x;
			AdjustDOWN(minHeap, 0, k);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", minHeap[i]);
	}
	printf("\n");
	fclose(fout);
}

先读取文件数据,取前K个保存在数组中,并构成堆,随后遍历剩下的n-k个数据,进行比较替换。

最后成功打印

二.实现链式结构二叉树

二叉树的相关知识需要用到递归知识,所以建议先学习递归后再来看以下文章!!!


2.1二叉树的插入与删除

数据的插入和删除可以在二叉树的任何位置,所以此时暂时不写文章,在后期学习到平衡树,红黑树等在进行编写。

2.2二叉树的遍历

二叉树的遍历有前序/中序/后序的遍历的递归结构遍历:

1)前序遍历:访问根节点的操作在遍历其左右子树之间

 访问顺序为:根结点、左子树、右子树

2)中序遍历:访问根结点的操作发生在遍历其左右子树之中(间)

访问顺序为:左子树、根结点、右子树

3)后序遍历:访问根结点的操作发生在遍历其左右子树之后

访问顺序为:左子树、右子树、根结点

遍历的实现:

通过函数的递归实现遍历,需要先判断递归条件,当目前根节点为NULL时,递归中止,拿前序遍历为例,在目前根节点时,先访问根节点的data,随后根据前序遍历的顺序,分别访问左子树,右子树,进行递归。那么中序遍历与后序遍历也就很好写出来了。

构造文章上文的二叉树例子,然后进行遍历输出:

2.3二叉树的结点个数

1.运用二叉树遍历的知识,通过递归遍历每一个结点,然后size++,那么结果会是什么呢?

int BinaryTreeSize(BTNode* root)
{
	int size = 0;
	if (root == NULL)
	{
		return;
	}
	//结点非空,+1
	size++;
	BinaryTreeSize(root->left);
	BinaryTreeSize(root->right);

	return size;
}

运行后可以发现,不管调用几次,size都为1,原因是因为,每次递归size都被初始化为0

2.那么把size设为全局变量后是不是就可以避免了?

int size = 0;
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	//结点非空,+1
	size++;
	BinaryTreeSize(root->left);
	BinaryTreeSize(root->right);

	return size;
}

运行后发现,虽然第一次调用size的结果是正确的,但是经过多次调用,size的值就会越加越大。

3.这次换一种思路,直接把size设为参数进行调用。

int BinaryTreeSize(BTNode* root, int size)
{
	if (root == NULL)
	{
		return;
	}
	//结点非空,+1
	size++;
	BinaryTreeSize(root->left,size);
	BinaryTreeSize(root->right,size);

	return size;
}

这一次发现结果又变为三个1,分析原因,当递归回溯的时候,size的值会返回到上一步的值,例如当递归到4结点时,size=3,但是回溯到2结点时,size又回到了2

4.要解决这个问题,把传值调用改为传址调用就可解决。

int BinaryTreeSize(BTNode* root,int*size)
{
	if (root == NULL)
	{
		return 0;
	}
	//结点非空,+1
	(*size)++ ;
	BinaryTreeSize(root->left,size);
	BinaryTreeSize(root->right,size);

	return *size;
}

这时结果就正确了,但是,每次调用参数,都需要把size初始化为0,有一点麻烦,在对函数进行改造

分析二叉树可知,结点数 = 根结点+左子树的结点数+右子树的结点数 

最终版:

//二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	//结点非空,+1
	return 	1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}

2.4二叉树叶子结点数

叶子结点数 = 左子树叶子节点数 + 右子树叶子结点数

结合刚才学到的结点数的知识,可以写出:

//二叉树叶子节点数
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);
}

2.5二叉树第K层结点个数

//二叉树第K层结点个数
int BinaryTreeLevelSize(BTNode* root, int k)
{
	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}
	return BinaryTreeLevelSize(root->left, k - 1) + BinaryTreeLevelSize(root->right, k - 1);
}

2.6二叉树的深度

深度 = 根结点 + max(左子树的深度+右子树的深度)

//二叉树的深度
int BinaryTreeDepth(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	return 1 + max(BinaryTreeDepth(root->left), BinaryTreeDepth(root->right));
	//return 1 + (BinaryTreeDepth(root->left) > BinaryTreeDepth(root->right) ? BinaryTreeDepth(root->left) : BinaryTreeDepth(root->right));
}

2.7二叉树查找值为x的结点

//二叉树查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDtatatype x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}
	BTNode*leftFind =  BinaryTreeFind(root->left, x);
	if (leftFind)
	{
		return leftFind;
	}
	BTNode*rightFind =  BinaryTreeFind(root->right, x);
	if (rightFind)
	{
		return rightFind;
	}
}

2.8二叉树的销毁

//二叉树的销毁
void BinaryTreeDestroy(BTNode** root)
{
	if (root == NULL)
	{
		return;
	}
	BinaryTreeDestroy(&((*root)->left));
	BinaryTreeDestroy(&((*root)->right));

	free(*root);
	*root = NULL;
}

二叉树的层序遍历

借助数据结构--队列

首先将之前所编写过的队列的.h和.c文件导入进来

注意:

1.要包含队列的头文件

2.修改队列的头文件:修改队列的保存类型,务必要加struct,为了告诉编译器BinaryTreeNode是个结构体。

2.9层序编列实现:

//层序遍历
void levelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		//取队头,出队头
		BTNode*top =  QueueFront(&q);
		QueuePop(&q);
		printf("%c ", top->data);
		//将左右孩子入队列
		if (top->left)
			QueuePush(&q, top->left);
		if (top->right)
			QueuePush(&q, top->right);
	}
    QueueDestrory(&q);
}

2.10判断是否为完全二叉树

前置知识:完全二叉树的特点:

1)最后一层节点个数不一定达到最大

2)结点从左到右依次排列

与二叉树的层序遍历相同,借助队列进行实现:

在第一个循环中,取队头,出对头,直到遇到第一个空结点出循环,并进入到第二个循环。

若在第二个循环中,从非空队列中取到了非空的队头结点,那么该二叉树一定不是完全二叉树,否则一定是完全二文树

//判断是否为完全二叉树
bool BinaryTreeComlete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		//取队头,出队头
		BTNode* top = QueueFront(&q);
		QueuePop(&q);
		if (top == NULL)
		{
			break;
		}
		QueuePush(&q, top->left);
		QueuePush(&q, top->right);
	}
	//队列不一定为空
	while(!QueueEmpty(&q))
	{
		BTNode* top = QueueFront(&q);
		QueuePop(&q);
		if (top != NULL)
		{
			return false;
		}
	}
	QueueDestrory(&q);
	return true;
}

以上图做例子,应不是完全二叉树:符合预期

把G H I结点去掉后应为完全二叉树:符合预期


网站公告

今日签到

点亮在社区的每一天
去签到