目录
前言
一、堆的应用
1. 堆排序
1.1 排升序,建大堆
1.2 时间复杂度计算
2. Top k问题
二、 二叉树的链式实现
1. 二叉树的遍历
2. 二叉树基础OJ
3.DFS && BFS
总结
学习完堆的数据结构,我们要清楚,它虽然实现了排序功能,但是真正的排序函数应当是在给定的数组内,将数组排序,如果我们要用堆排序,那么我们不可能手写堆的数据结构,在堆内排序后再复制给给定的数组,这样不仅很麻烦,而且还要开辟另外的空间。
所以,我们又有了堆排序的方法。
我们可以将给定的数组看为一个完全二叉树,但它此刻还不是堆,因为堆是有顺序的,所以我们可以使用向上或向下调整函数进行堆排序,模拟堆插入,这就是一个建堆的过程。
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1.建堆
- 升序:建大堆
- 降序:建小堆
2.利用堆删除思想进行排序
方法一:先利用向上调整建大堆(在给定的数组内从下标为1的数据开始进行向上调整),然后再进行向下调整,完成升序排序
解释: 如果采用小堆,那么堆顶就是最小值,取走堆顶数据后,次小数上至堆顶,此时堆所表示的二叉树内父子兄弟关系就全乱了,不能再按顺序提出堆顶数据。
所以,排升序还是需要用大堆
//堆排升序 -- 建大堆
void HeapSort(int* a, int n)
{
// 1.建堆 -- 向上调整建堆--模拟插入的过程
for (int i = 1; i < n; ++i)
{
AdjustUp(a, i);
}
// 2.利用堆删除思想进行排序
int end = n - 1;
while (end > 0)
{
swap(&a[0], &a[end]);
AdjustDown(a, end - 1, 0);
end--;//end下标前移
}
}
int main()
{
int a[10] = { 2, 1, 5, 7, 6, 8, 0, 9, 4, 3 }; // 对数组排序
HeapSort(a, 10);
return 0;
}
方法二:先向下调整建大堆,再用向下调整完成升序排序
注意: 在向下调整时,不能从堆顶开始,因为数组最初是无序的,而向下调整的前提是其左右子树是大堆或小堆,才可以向下调整,
所以我们先将最后一个叶子节点的父节点开始向下调整,完成调整后,从父节点开始向前向下调整,直到堆顶完成向下调整后结束。
综上分析,方法二效率比方法一高,只用写一个向下调整函数即可
//堆排升序 -- 建大堆
void HeapSort(int* a, int n)
{
//1.建堆 -- 向下调整建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
// 2.利用堆删除思想进行排序
int end = n - 1;
while (end > 0)
{
swap(&a[0], &a[end]);
AdjustDown(a, end - 1, 0);
end--;//end下标前移
}
}
int main()
{
int a[10] = { 2, 1, 5, 7, 6, 8, 0, 9, 4, 3 }; // 对数组排序
HeapSort(a, 10);
return 0;
}
向下调整建堆的时间复杂度计算 O(N)
向上调整建堆的时间复杂度计算 O(N*logN)
最简单的解释:向下调整,节点最多的一层最坏情况只调整一次,节点最少的一层最坏情况调整h-1次,而向上调整相反,节点最少的一层最坏情况只调整一次,节点最多的一层最坏情况调整h-1次,显而易见,向上调整的时间复杂度大于向下调整.
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是,如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。
最佳的方式就是用堆来解决,基本思路下:
1. 用数据集合中前K个元素来建堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素,将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素
void PrintTopK(const char* file, int k)
{
// 1. 建堆--用a中前k个元素建小堆
int* topk = (int*)malloc(sizeof(int) * k);
assert(topk);
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen error");
return;
}
// 读出前k个数据建小堆
for(int i = 0; i < k; ++i)
{
fscanf(fout, "%d", &topk[i]);
}
for (int i = (k-2)/2; i >= 0; --i)
{
AdjustDown(topk, k, i);
}
// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
int val = 0;
int ret = fscanf(fout, "%d", &val);
while (ret != EOF)
{
if (val > topk[0])
{
topk[0] = val;
AdjustDown(topk, k, 0);
}
ret = fscanf(fout, "%d", &val);
}
for (int i = 0; i < k; i++)
{
printf("%d ", topk[i]);
}
printf("\n");
free(topk);
fclose(fout);
}
void CreateNDate()
{
// 造数据
int n = 10000000;
srand(time(0));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen error");
return;
}
for (size_t i = 0; i < n; ++i)
{
int x = rand() % 10000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
int main()
{
//int a[10] = { 2, 1, 5, 7, 6, 8, 0, 9, 4, 3}; // 对数组排序
//HeapSort(a, 10);
CreateNDate();
PrintTopK("data.txt", 10);
return 0;
}
二叉树本身增删查改没有什么实际意义,但当加上了一个特性例如:左子树小于根,右子树大于根后,即搜索二叉树,它就有了实际意义。
在看二叉树基本操作前,再回顾下二叉树的概念,二叉树是:
1. 空树
2. 非空:根节点,根节点的左子树、根节点的右子树组成的。
把每一个二叉树都分为三部分:根、左子树、右子树
理解函数栈帧,画图就可理解递归调用过程
二叉树结构体:
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
1.1 前序遍历:根、左子树、右子树 (先访问根,遇到每一个都作为根)
1 2 3 NULL NULL NULL 4 5 NULL NULL 6 NULL NULL
//前序遍历
void PreOrder(BTNode* root) {
if (root == NULL) {
printf("NULL ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
1.2 中序遍历:左子树、根、右子树 (遇到每一个都先访问左子树,直到NULL,所以访问的第一个一定为NULL)
NULL 3 NULL 2 NULL 1 NULL 5 NULL 4 NULL 6 NULL
//中序遍历
void InOrder(BTNode* root) {
if (root == NULL) {
printf("NULL ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
1.3 后序遍历:左子树、右子树、根 (左、右、根)
NULL NULL 3 NULL 2 NULL NULL 5 NULL NULL 6 4 1
//后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
1.4 层序遍历:一层从左往右
1 2 4 3 5 6
用队列实现,每出队一个根节点就把它的孩子入队,实现出上一层带入下一层
#include"Queue.h"
//队列的实现,并把data的类型改为树结点指针类型
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);//用树节点的指针类型接收队头数据(因为队头数据类型为树节点指针类型)
QueuePop(&q);
printf("%d ", front->data);
//如果左孩子不为空就入队
if(front->left)
QueuePush(&q, front->left);
//如果右孩子不为空就入队
if (front->right)
QueuePush(&q, front->right);
}
QueueDestroy(&q);
}
1.计算二叉树内结点个数
注意:
- 如果想要计算二叉树内节点个数,可以在传参时加上一个参数psize指针
- 不要在函数内使用static静态局部变量,因为它指挥在第一次调用时才执行,之后调用都不会再执行,也就是说第一次计算二叉树内节点个数是正确的,而再次计算时,size不能初始化为0,所以计算结果就会出错。
- 也尽量不使用全局变量,这样的话我们要在每一次调用计算函数前,自己手动初始化size全局变量为0,并不是很方便。
最简形式:
int TreeSize(BTNode* root) { return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1; }
2.计算树的高度
看山不是山,不考虑太多递归过程,看结果
注意:
- 一定要记录递归结果,因为如果不记录结果,那么程序就会递归两大遍次数
int TreeHeight(BTNode* root) { if (root == NULL) return 0; int leftHeight = TreeHeight(root->left); int rightHeight = TreeHeight(root->right); return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1; }
3.计算第k层节点数
k--直到k为1,就找到了要计算的第k层,如果此时root不为NULL,则return 1,如果为NULL,则返回0。这样就计算出了左右子树的第k-1层节点数,最后相加。
int TreeKLevel(BTNode* root, int k) { assert(k > 0); if (root == NULL) return 0; if (k == 1) return 1; return TreeKLevel(root->left, k - 1) + TreeKLevel(root->right, k - 1); }
4.二叉树查找值为x的结点
注意:
在函数多层调用时,返回root是不能直接返回到最外面的,只能返回上一个函数调用处
//二叉树查找值为x的结点 BTNode* BinaryTreeFind(BTNode* root, BTDataType x) { if (root == NULL) return NULL; if (root->data == x) return root; BTNode* lret = BinaryTreeFind(root->left, x); if (lret) return lret; BTNode* rret = BinaryTreeFind(root->right, x); if (rret) return rret; return NULL; }
2.1 965. 单值二叉树
bool isUnivalTree(struct TreeNode* root)
{
if(root == NULL)
return true;
if(root->left && root->val != root->left->val)
return false;
if(root->right && root->val != root->right->val)
return false;
return isUnivalTree(root->left) && isUnivalTree(root->right);
}
在写递归时,要写递归的出口!顾名思义,是在递归过程中特殊的结果,写能阻断递归的逻辑表达式。例如此题:第二三个if,只有在值不相等的时候返回false,终止递归,不能在判断中写相等的情况,因为那样的判断没有任何用处 。
bool isSameTree(TreeNode* p, TreeNode* q)
{
if(p == NULL && q == NULL)
return true;
if(p == NULL || q == NULL)
return false;
if(p->val != q->val)
return false;
return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}
2.3 144. 二叉树的前序遍历
由于要返回数组,所以要在函数内开辟一个数组空间,而开辟多少字节空间就要我们计算二叉树的结点数
int TreeSize(struct TreeNode* root)
{
return root == NULL? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}
void preorder(struct TreeNode* root, int*arr, int* pi)
{
if(root == NULL)
return;
a[(*pi)++] = root->val;//错误,因为i的值不会及时更新
preorder(root->left,arr,pi);
preorder(root->right,arr,pi);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize)
{
*returnSize = TreeSize(root);
int* arr = (int*)malloc(sizeof(int)*(*returnSize)));
int i = 0;
preorder(root,arr,&i);
}
注意前序遍历函数内数组下标是*pi,因为如果只传整形 i 是无法在递归中及时更新i的值的!
2.4 572. 另一棵树的子树
使左边的每一颗子树与右边的树比较,可以利用上相同的树函数
bool isSameTree(TreeNode* p, TreeNode* q)
{
//两个都为空
if(p == NULL && q == NULL)
return true;
//其中一个为空
if(p == NULL || q == NULL)
return false;
//可以退出的情况
if(p->val != q->val)
return false;
return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot)
{
if(root == NULL)
return false;
if(isSameTree(root,subRoot))
return true;
//递归一定要记录,不然就白递归了
//比如:
//isSubtree(root->left,subRoot);
//isSubtree(root->right,subRoot);
//return isSubtree(root->left,subRoot) || isSubtree(root->right,subRoot);
//递归了两大遍,是错误写法
return isSubtree(root->left,subRoot) || isSubtree(root->right,subRoot);
}
2.5 KY11 二叉树遍历
struct TreeNode
{
struct TreeNode* left;
struct TreeNode* right;
char val;
};
void InOrder(struct TreeNode* root)
{
if (root == NULL)
return;
InOeder(root->left);
printf("%c ", root->val);
InOeder(root->right);
}
struct TreeNode* CreateTree(char* a, int* pi)
{
if (a[*pi] == '#')
{
(*pi)++;
return NULL;
}
struct TreeNode* root = (struct TreeNode*)malloc(sizeof(struct TreeNode));
root->val = a[(*pi)++];
root->left = CreateTree(a, pi);
root->right = CreateTree(a, pi);
return root;
}
int main()
{
char a[100];
scanf("%s", a);
int i = 0;
InOrder(CreateTree(a, &i));
return 0;
}
2.6 判断二叉树是否是完全二叉树
思路:
- 采用层序遍历的方式,如果访问到NULL,那么后面全是NULL,因为完全二叉树的性质,非空结点都是连续的,空结点是最后一个节点。
- 遇到NULL,就把剩下的数据都取出来,判断是否为非空,如果是非空数据,那么就返回false,如果都是NULL,那么返回true
bool TreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front == NULL)
{
break;
}
else
{
QueuePush(root->left);
QueuePush(root->right);
}
}
//把剩下的数据都取出来,看是否右非空数据
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front)
{
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
return true;
}
2.7 销毁二叉树
思路:采用后序遍历,递归式销毁二叉树
//销毁二叉树
//思路:采用后序遍历的的方式,递归销毁二叉树
void TreeDestory(BTNode* root)
{
if (root == NULL)
return;
TreeDestory(root->left);
TreeDestory(root->right);
free(root);
//如果想要在里面置空,要么传二级指针,要么使用C++中的引用
//如果不在里面置空,那么要在外面手动或置空
root = NULL;
}
二叉树的前序遍历就是严格的深度优先遍历,一般使用递归实现,如果不考虑数据访问顺序,那么中序、后序遍历也可算为深度优先遍历。
3.2 BFS 广度优先遍历
二叉树的层序遍历就是广度优先遍历的一种,一般使用队列实现
二叉树这一数据结构包含了诸多的递归函数,本节学习了二叉树的遍历、计算二叉树的高度、第k层结点数、总结点数、如何在二叉树内查找数以及一些二叉树的OJ
如何正确书写递归函数,如何正确使用递归函数来计算二叉树的各项属性是学习二叉树的关键。
最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!