【数据结构】堆(下)+ 二叉树
上期回顾:【数据结构】树(堆)·上
一.堆的应用
1.1堆排序(向下调整在上一期)
向上调整算法建堆:
首先先回顾一下向上调整算法
void AdjustUP(HPDataType* arr, int child)
{int parent = (child - 1) / 2;//找到父结点的位置while (child > 0)//循环,确保父结点一定小于(大于)子结点{//小堆:<//大堆:>// |// Vif (arr[child] > arr[parent]){//调整位置Swap(&arr[child], &arr[parent]);child = parent;parent = (child - 1) / 2;}elsebreak;}
}
在上一期我们也学过了向下调整算法建堆,其是通过由下向上循环,通过确保父结点下方的是堆结构,而向上算法建队=堆就刚好反过来,是由上到下进行循环。
//堆排序
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;}//结点非空,+1size++;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;}//结点非空,+1size++;BinaryTreeSize(root->left);BinaryTreeSize(root->right);return size;
}
运行后发现,虽然第一次调用size的结果是正确的,但是经过多次调用,size的值就会越加越大。
3.这次换一种思路,直接把size设为参数进行调用。
int BinaryTreeSize(BTNode* root, int size)
{if (root == NULL){return;}//结点非空,+1size++;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;}//结点非空,+1return 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结点去掉后应为完全二叉树:符合预期