二叉树(二)

上篇文章提到,位于堆顶的元素是一个堆中最大或者最小的,那么我们可以根据这一特性进行选数,让数据从大到小或者从小到大排序。
根据我们所学的,我们可以先创建堆,然后在把要排序的数组中的元素一个个插入堆中,接着我们在依次取出栈顶元素放入要排序的数组,然后在Pop栈顶元素,循环操作,直到堆为空停止,就排好了。

堆排序(非最优)


//升序打印——小堆
void Test1()
{
	HP hp;
	HeapInit(&hp);
	int a[] = { 27, 45, 86, 32, 16, 84, 63, 70, 55, 12 };
	for (int i = 0; i < sizeof(a) / sizeof(int); ++i)
	{
		HeapPush(&hp, a[i]);
	}

	while(!HeapEmpty(&hp))
	{
		printf("%d ", HeapTop(&hp));
		HeapPop(&hp);
	}

	printf("\n");
}

//堆排序
//升序——建大堆
//降序——建小堆

void HeapSort(int* a, int n)
{
	建堆 1
	//for (int i = 0; i < n; i++)
	//{
	//	AdjustUp(a, i);
	//}

	//建堆 2
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)            //i是倒数第一个非叶子节点,-1求下标,再-1求父亲
	{
		AdjustDown(a, n, i);
	}

	int end = n - 1;
	while (end > 0)
	{
		//将最大的放到最下面,然后将他排除堆外,循环
		HeapSwap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		printf("%d ", a[end]);
		end--;

	}
}

下面是打印结果:

image.png

我们可以看出,在逻辑上是二叉树。并且也是降序排列。但是并不是最优解,原因如下:

  1. 需要先有一个堆 这样的数据结构,才能使用这种办法。

2.在排序过程中,额外开辟了一个数字,空间复杂度为O(N)。

堆排序(实用)

建堆方式:

1.向上建堆

在堆中那么多函数接口中,其实最关键的是向上调整和向下调整算法,我们可以只使用这两个函数,就完成建堆,然后在进行排序。
image.png
image.png
image.png
image.png
image.png
我们调整后就是一个堆了,但是我们如何把这个堆变为有序呢?首先把第一个元素和最后一个元素交换下位置,不考虑最后一个元素,把第一个元素向下调整,我们就可以得到一个新的堆,如此反复,就可以得到这个数组的降序。

void HeapSwap(HPDataType* n1, HPDataType* n2)
{
	HPDataType* tmp = *n1;
	*n1 = *n2;
	*n2 = tmp;
}


void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] < a[parent])
		{
			HeapSwap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

2.向下建堆

我们向下调整建堆的思路和向上调整不太类似,我们先从倒数第一个非叶子节点向上调整,调整到根结束。
image.png

最后我们按照图示方法建堆结果为:
image.png
也符合堆的性质,然后后面的思路和向上建堆就一样了,把第一个元素和最后一个元素交换,然后不考虑最后一个元素,让第一个元素向下调整在变为一个堆就好了,重复过程即可

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])
		{
			HeapSwap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

大堆小堆应用

排升序建的大堆,如果我们要排的是降序,我们依然建的大堆,那怎么办?
我们可以找到最大的数据,应该把它放在第一位,然后不考虑它继续找次大的,但是我们的父子关系全乱了,需要重新建堆了,这时间复杂度就变为O(N)*O(N)了,不划算。所以我们选择排降序时建小堆,我们找到其中最小的,然后把最小的放最后,不考虑它,然后使用向下调整,这样时间复杂度就降低为log(N)*O(N),这才是堆排序快的地方。



堆排序——TOP-K问题

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

  1. 用数据集合中前K个元素来建堆
    前k个最大的元素,则建小堆
    前k个最小的元素,则建大堆
  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
    比特就业课将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

image.png

我们有10000个数,求出最大的前10个数据。我们使用时间戳的概念,用srand函数生成随机数,然后我们对他们取模10000,然后我们在随机给上10个数,把它们的值加上10000,然后判断打印的数据是否大于10000.
代码和运行结果如下

void PrintTopK(int* a, int n, int k)
{
	// 1. 建堆--用a中前k个元素建堆
	int* KMinHeap = (int *)malloc(sizeof(int)*k);
	assert(KMinHeap);
	for (int i = 0; i < k; i++)
	{
		KMinHeap[i] = a[i];
	}
 
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDwon(KMinHeap, k, i);
	}
 
	// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
	for (int j = k; j < n; j++)
	{
		if (a[j]>KMinHeap[0])
		{
			KMinHeap[0] = a[j];
			AdjustDwon(KMinHeap, k, 0);
		}
	}
 
	for (int i = 0; i < k; i++)
	{
		printf("%d ", KMinHeap[i]);
	}
 
	printf("\n");
 
}
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() % 10000;
	}
	a[6] = 10000 + 1;
	a[12] = 10000 + 2;
	a[645] = 10000 + 3;
	a[3333] = 10000 + 4;
	a[1222] = 10000 + 5;
	a[459] = 10000 + 6;
	a[9999] = 10000 + 7;
	a[8778] = 10000 + 8;
	a[5897] = 10000 + 9;
	a[44] = 10000 + 10;
	PrintTopK(a, n, 10);
}

image.png

二叉树链式结构

先简单的手动创建一颗二叉树

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

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

  1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
  2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
  3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后
前序遍历:

我们都知道二叉树我们可以分为 根 左子树 右子树,这三个部分,我们先序遍历,就是先访问二叉树的根,在访问左子树,最后访问右子树,如果访问到空树我们使用 # 代替,这里用到分治思想:

//前序遍历
void PreoOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("# ");
		return;
	}
	printf("%d ", root->data);
	PreoOrder(root->left);
	PreoOrder(root->right);
}

image.png
image.pngimage.png

中序遍历和后续遍历

思路同上image.png

//中序遍历
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);
}

二叉树节点个数

我们这里用两种思路,一种是遍历,另一种是分治。

  1. 遍历
int size;
void TreeSize1(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
 
	size++;
	TreeSize1(root->left);//访问左子树
	TreeSize1(root->right);//访问右子树
}
 
 
int main()
{
	BTNode* root = CreatBinaryTree();
	size = 0;
	TreeSize1(root);
	printf("TreeSize = %d\n", size);
 
	return 0;
}

但是每次调用之前需要把count置为空,有点麻烦。

2.分治
将一棵树分解,看成无数个子树,转化为根节点+左子树+右子树。

int TreeSize2(BTNode* root)
{
	return root == NULL ? 0 : TreeSize2(root->left) + TreeSize2(root->right)+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);

}

image.png

二叉树第k层节点个数

求第k层节点个数,转化为求第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);
}

image.png

求二叉树的深度

分治的思想,我们先求出左子树的高度,再求出右子树的高度,进行对比,比较时不要忘了自身也是有高度的,最后把二叉树拆到最小的空树时返回0

int TreeDepth(BTNode* root)
{
	if (root == NULL)//空树返回0
	{
		return 0;
	}
	int Leftdepth = TreeDepth(root->left);//求出左边高度
	int Rightdepth = TreeDepth(root->right);//求出右边高度
 
	return Leftdepth > Rightdepth ? Leftdepth + 1 : Rightdepth + 1;//返回时加上自身返回
}

image.png

二叉树查找值为x的节点

这里注意当找到该节点时,建立一个节点保存,便于我们直接返回

BTNode* TreeFind(BTNode* root, BTDataType x)//查找二叉树中值为x的节点
{
	if (root == NULL)//为空返回空
	{
		return NULL;
	}
 
	if (root->data == x)//相等就返回节点
	{
		return root;
	}
 
	BTNode* RetLeft = TreeFind(root->left, x);//保存左节点值,方便直接返回
	if (RetLeft)
	{
		return RetLeft;
	}
	
	BTNode* RetRight = TreeFind(root->right, x);//保存右节点值,方便直接返回
	if (RetRight)
	{
		return RetRight;
	}
 
	return NULL;//都找不到返回NULL
}

image.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

裙下的霸气

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值