稍微介绍一下自己吧:
双非非科班(其实也沾点边)大一菜鸟,自学C++中,所以不要问咱太复杂的问题啊。
写博客都是记录学习,也是督促自己不要摸鱼,没啥想法,学到哪写到哪,有问题,指,都可以指。
没了。
菜鸡大学生门前有两棵树,一棵是高数,一棵是二叉树。
上学期在高强度预习下,大学生没挂在高数上面,真是万幸。
这学期我看悬。
总之在菜鸡大学生的讲解下,大家都不会挂在二叉树上面的!
我们开始吧!
树(左一)
毫无疑问这个是树,但不是我们要讲的树。
我们要讲的树是这个:
由于它长得像一棵倒挂的树所以就给它起名叫树。
先简单了解一下孩子兄弟表示法吧,毕竟我们主要讲的是二叉树,树啥的,目前…不重要。
typedef int DataType;
struct Node
{
struct Node* firstChild; // 第一个孩子结点
struct Node* NextBrother; // 指向其下一个兄弟结点
DataType data; // 结点中的数据域
};
实际表示是这样的。
这样可以既兼顾值和节点之间的关系,还是很不错的。
啥是二叉树,根据朴素的按照名字去推断的方法,二叉树有两个叉。
再说明白一点,就是一个节点最多有两个子节点。
还有一个特点,左右节点是有顺序的。比如上图左边的二叉树,将4,5两个节点换一个位置,就会变成一个新的二叉树。
满二叉树是一种特殊的完全二叉树。
啥意思?
先放一张满二叉树的图片:
图很好懂,用我的话说就是除了最后一层全是叶子节点其他所有节点都是度为二的节点。
完全二叉树呢?
我们先给这个满二叉树标个号。
如果一个二叉树有6个节点,且分别对应节点1,2,3,4,5,6就是完全二叉树。
其余任何情况都不是。
简单来说只要中间不断就是完全二叉树了。
我们根据第三条结论,可以直接算出n0=200。
A. 原因我们下面讲。
我们假设n2=x,n0就是x+1,n2+n0=2x+1由于完全二叉树,n1的个数不是1就是0,又因为2x+1肯定是奇数,所以n1=1,2x+1+1=2n,就可以算出n0=n了,选A。
2^9-1<531< 2^10-1高度是10,选B。
和第三题一样的思路,选B。
假设我们有一棵完全二叉树,
康康这完美的序号!这么漂亮的结构不用数组说不过去吧!
我们用数组的下标形式重新标一下:
amazing啊,我们可以发现下标和它们的父母孩子都是有关系的。
非完全二叉树也可以这么搞,但肯定没有完全二叉树实用不是吗?
那么我们顺势引入堆的概念。
堆的性质:
按照堆的定义可以把堆分成大堆或者小堆。
既然是数组就好操作了。
先把常规操作整了:
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
size_t size;
size_t capacity;
}HP;
// 堆的初始化
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
//堆的销毁
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->size = php->capacity = 0;
}
//交换函数
void Swap(HPDataType* pa, HPDataType* pb)
{
HPDataType tmp = *pa;
*pa = *pb;
*pb = tmp;
}
//堆的打印
void HeapPrint(HP* php)
{
assert(php);
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
//判断堆是否为空
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
//返回堆的大小
size_t HeapSize(HP* php)
{
assert(php);
return php->size;
}
//返回堆顶数据
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
为了保证堆的结构不被破坏,我们选择讲数据先插入到最后然后慢慢向上调整的方法,如图:
void AdjustUp(HPDataType* a, size_t child)
{
size_t 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;
}
}
}
// 插入x以后,保持他依旧是(大/小)堆
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = realloc(php->a, sizeof(HPDataType) * newCapacity);
if (tmp == NULL)
{
printf("realloc failed\n");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
php->size++;
// 向上调整,控制保持是一个(大/小)堆
AdjustUp(php->a, php->size - 1);
}
一般我们删除是删除堆顶的数据,但是,如果我们把堆顶数据删掉了,谁是堆顶呢?堆的结构是不是就乱了呢?
此时有一个天才般的方法,先把堆顶的数据和最后一个数据交换,删掉最后一个数据,然后将堆顶慢慢往下调整。
看图:
void AdjustDown(HPDataType* a, size_t size, size_t root)
{
size_t parent = root;
size_t child = root*2+1;
while (child < size)
{
// 1、选出左右孩子中小的那个
if (child + 1 < size && a[child] > a[child + 1])
++child;
// 2、如果孩子小于父亲,则交换,并继续往下调整
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
// 删除堆顶的数据。(最小/最大)
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);
}
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。
它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
我们先不管这些,我们可以先用上面的代码写一个粗糙的堆排序试试看。
由于大/小堆堆顶的数据是堆里面最大/小的数据,我们只要不停出堆顶的数据放入数组就可以保证数组有序了。
思路:
void HeapSort1(int* a, int size)
{
HP hp;
HeapInit(&hp);
for (int i = 0; i < size; i++)
{
HeapPush(&hp, a[i]);
}
int j = 0;
while (!HeapEmpty(&hp))
{
a[j++] = HeapTop(&hp);
HeapPop(&hp);
}
HeapDestroy(&hp);
}
这个方法不错是吧?可不可以优化呢?
由于我们额外建立了一个堆导致有O(n)的空间复杂度,我们是不是可以考虑直接在数组里面建堆呢?
可以。
再次强调: 排升序要建大堆,排降序建小堆。
如果是建小堆的话第一个就已经是最小的了,后面的数据还需要重新建堆,那么堆排序的意义就不存在了。
思路:
我们先不急着建堆,先把后面的代码写出来。
for (size_t i = n - 1; i > 0; i--)
{
Swap(&a[0], &a[i]);
AdjustDown(a, i, 0);
}
然后在尝试建堆康康。
我们可以采取向上建堆和向下建堆两种建堆方式。
向上建堆就类似于一个个向数组里面插入数据。
向下建堆从倒数第一个非叶子节点开始向下调整。
//建大堆
// 向上建堆
for (int i = 0; i < n; i++)
{
AdjustUp(a, i);
}
//向下建堆
for (int i = (n-1-1)/2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
既然有两种建堆方式我们就要比较一下效率了。
我们假设是满二叉树:
显然向下建堆效率更高。
将代码整合一下:
void HeapSort(int* a, int n)
{
//向下建堆
for (int i = (n-1-1)/2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
for (size_t i = n - 1; i > 0; i--)
{
Swap(&a[0], &a[i]);
AdjustDown(a, i, 0);
}
}
即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
如果数据量比较小的话我们可以使用排序,但是如果数据很大呢?
这个时候我们可以用堆解决:
用数据集合中前K个元素来建堆
用剩余的N-K个元素依次与堆顶元素来比较,决定是否替换:
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
代码:
void PrintTopK(int* a, int n, int k)
{
int* kminHeap = (int*)malloc(sizeof(int) * k);
assert(kminHeap);
for (int i = 0; i < k; ++i)
{
kminHeap[i] = a[i];
}
// 建小堆
for (int j = (k - 1 - 1) / 2; j >= 0; --j)
{
AdjustDown(kminHeap, k, j);
}
// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
for (int i = k; i < n; ++i)
{
if (a[i] > kminHeap[0])
{
kminHeap[0] = a[i];
AdjustDown(kminHeap, k, 0);
}
}
for (int j = 0; j < k; ++j)
{
printf("%d ", kminHeap[j]);
}
printf("\n");
free(kminHeap);
}
注:时间复杂度:O(K+logK*(N-K))) 空间复杂度:O(K)。
如果N非常大,K很小,时间复杂度就可以看作O(N)。
也就是说每个节点包含三个部分:
typedef int BTDataType;
typedef struct BinaryTreeNode {
struct BinaryTreeNode* left; //指向左孩子的指针
struct BinaryTreeNode* right; //指向右孩子的指针
BTDataType data; //数据
}BTNode;
二叉树增删查改没啥价值,我的评价是不如线性表。
二叉树的遍历主要可以分为前序遍历,中序遍历,后序遍历和层序遍历。
我们一个个来讲。
通过观察我们发现所谓的前中后其实就是根节点在顺序中的位置,
前序就是根在前,中序就是根在中间,后序就是根在最后。
明白了思想之后,我们就可以考虑写代码了。
在遍历的时候我们考虑使用递归的方法。
把每一个问题拆成更小的问题,直到我们可以轻松解决。
前中后序遍历唯一的区别就是打印根的时机,所以代码一并放出来:
//前序遍历
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return NULL;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
//中序遍历
void InOrder(BTNode* root) {
if (root == NULL) {
printf("NULL ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
//后序遍历
void PostOrder(BTNode* root) {
if (root == NULL) {
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
也许有好朋友没看懂。
这个时候就需要画图了,我们以前序为例:
递归题目看不懂的话就多画画递归展开图,会有奇效。
啥是层序遍历捏?
就是一层一层遍历。
以上图为例,层序遍历结果是:A,B,C,D,E,F。
解题思路:
//层序遍历
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);
}
}
printf("\n");
QueueDestory(&q);
}
做点题玩玩吧,会讲一些大致思路或者直接摆烂放代码。
递归这个东西很玄,要是绕不过来就画图吧。
树的节点个数等于左子树+右子树+1.
空节点直接返回0.
//树的节点个数
int BTreeSize(BTNode* root) {
return root == NULL ? 0 :
BTreeSize(root->left)
+ BTreeSize(root->right) + 1;
}
还是把它分成左子树和右子树。
只有左子树和右子树都是NULL是叶子节点。
//叶子节点个数
int BTreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)
return 1;
return BTreeLeafSize(root->left) + BTreeLeafSize(root->right);
}
// 第k层的节点的个数,k >= 1
int BTreeKLevelSize(BTNode* root, int k)
{
assert(k >= 1);
if (root == NULL)
return 0;
if (k == 1)
return 1;
return BTreeKLevelSize(root->left, k - 1)
+ BTreeKLevelSize(root->right, k - 1);
}
分治啊,分治。
累了,毁灭吧。
// 二叉树销毁
void BTreeDestory(BTNode* root)
{
if (root == NULL)
{
return;
}
BTreeDestory(root->left);
BTreeDestory(root->right);
free(root);
}
左子树的最大深度和右子树的最大深度中最大的那一个。
int maxDepth(struct TreeNode* root){
if(root==NULL)
return 0;
int left=maxDepth(root->left);
int right=maxDepth(root->right);
return left>right?left+1:right+1;
}
// 二叉树查找值为x的结点
BTNode* BTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BTNode* ret1 = BTreeFind(root->left, x);
if (ret1)
return ret1;
//return BTreeFind(root->right, x);
BTNode* ret2 = BTreeFind(root->right, x);
if (ret2)
return ret2;
return NULL;
}
链接:965. 单值二叉树 - 力扣(LeetCode)
如果二叉树每个节点都具有相同的值,那么该二叉树就是_单值_二叉树。
只有给定的树是单值二叉树时,才返回 true
;否则返回 false
。
这道题是的情况比不是的情况要多很多,所以我们只要在意不是的情况就行。
不是的情况很简单:父节点和子节点不一样。(子节点存在)
bool isUnivalTree(struct TreeNode* root){
if(root==NULL)
return true;
if(root->right&&root->val!=root->right->val)
return false;
if(root->left&&root->val!=root->left->val)
return false;
return isUnivalTree(root->left)&&isUnivalTree(root->right);
}
链接:100. 相同的树 - 力扣(LeetCode)
给你两棵二叉树的根节点 p
和 q
,编写一个函数来检验这两棵树是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
bool isSameTree(struct TreeNode* p, struct 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(q->right,p->right);
}
链接:101. 对称二叉树 - 力扣(LeetCode)
给你一个二叉树的根节点 root
, 检查它是否轴对称。
什么是对称?
简单来说,除了根节点,左子树的左子树等于右子树的右子树。
所以我们要先撇掉根节点再写。
思路与上一题一致。
bool _isSymmetric(struct TreeNode* p,struct TreeNode* q)
{
if(p==NULL&&q==NULL)
return true;
if(p==NULL||q==NULL)
return false;
if(p->val!=q->val)
return false;
return _isSymmetric(q->left,p->right)&&_isSymmetric(q->right,p->left);
}
bool isSymmetric(struct TreeNode* root){
if(root==NULL)
return true;
return _isSymmetric(root->left,root->right);
}
链接:226. 翻转二叉树 - 力扣(LeetCode)
给你一棵二叉树的根节点 root
,翻转这棵二叉树,并返回其根节点。
也和上一题有点像,二叉树的左节点和右节点互换就行,用分治就能换掉整棵树。
struct TreeNode* invertTree(struct TreeNode* root){
if(root==NULL)
return NULL;
invertTree(root->left);
invertTree(root->right);
struct TreeNode* tmp=root->left;
root->left=root->right;
root->right=tmp;
return root;
}
之前讲过了对吧,但是题目稍微变了一下。
链接:144. 二叉树的前序遍历 - 力扣(LeetCode)
它要求你把值放到数组里面了。
所以我们要先计算一下节点个数再去操作。
注意注释部分的代码。
int TreeSize(struct TreeNode* root)
{
if(root==NULL)
return 0;
return TreeSize(root->left)+TreeSize(root->right)+1;
}
void PreOrder(struct TreeNode* root, int* a,int* i)
{
if(root==NULL)
return ;
a[(*i)++]=root->val; //
PreOrder(root->left,a,i);
PreOrder(root->right,a,i);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize){
int size=TreeSize(root);
int* arr=(int*)malloc(sizeof(int)*size);
int i=0;
PreOrder(root,arr,&i); //
*returnSize=size;
return arr;
}
链接:572. 另一棵树的子树 - 力扣(LeetCode)
给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false 。
二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。
相同的树promax版。
我们只要康康它是不是有子树和subroot相等就行。
bool isSameTree(struct TreeNode* p, struct 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(q->right,p->right);
}
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){
if(root==NULL)
return NULL;
return isSameTree(root,subRoot)||isSubtree(root->left,subRoot)||isSubtree(root->right,subRoot);
}
这题和层序遍历有关。
我们只需要:
// 判断二叉树是否是完全二叉树
bool BTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front == NULL)
break;
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
// 空后面出到非空,那么说明不是完全二叉树
if (front)
{
QueueDestory(&q);
return false;
}
}
QueueDestory(&q);
return true;
}
链接:二叉树遍历_牛客网
编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储)。 例如如下的先序遍历字符串: ABC##DE#G##F### 其中“#”表示的是空格,空格字符代表空树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。
前序遍历力扣版本的变种。
typedef struct BinaryTreeNode
{
char data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
BTNode* BinaryCreate(char* s,int* i)
{
if(s[*i]=='#')
{
(*i)++;
return NULL;
}
BTNode* Node=(BTNode*)malloc(sizeof(BTNode));
Node->data=s[(*i)++];
Node->left=BinaryCreate(s, i);
Node->right=BinaryCreate(s, i);
return Node;
}
void InOrder(BTNode* root)
{
if(root==NULL)
return;
InOrder(root->left);
printf("%c ",root->data);
InOrder(root->right);
}
int main()
{
char arr[100];
scanf("%s",arr);
int len=strlen(arr);
int i=0;
BTNode* root=BinaryCreate(arr,&i);
InOrder(root);
return 0;
}
下一篇就是C++篇了。
向那些有着伟大浪漫主义色彩的事业致敬,劳动节快乐!