二叉树,堆的学习

一    二叉树

1.概念与结构

在树形结构中,我们最常⽤的就是⼆叉树,⼀棵⼆叉树是结点的⼀个有限集合,该集合由⼀个根结点 加上两棵别称为左⼦树和右⼦树的⼆叉树组成或者为空。

从上图可以看出⼆叉树具备以下特点:

 1. ⼆叉树不存在度⼤于 2 的结点

2. ⼆叉树的⼦树有左右之分,次序不能颠倒,因此⼆叉树是有序树

注意:对于任意的⼆叉树都是由以下⼏种情况复合⽽成的

2.特殊二叉树

(1)满二叉树

⼀个⼆叉树,如果每⼀个层的结点数都达到最⼤值,则这个⼆叉树就是满⼆叉树。也就是说,如果⼀ 个⼆叉树的层数为 K ,且结点总数是2^k-1 他就是满二叉树

(2)完全二叉树

完全⼆叉树是效率很⾼的数据结构,完全⼆叉树是由满⼆叉树⽽引出来的。对于深度为 K 的,有 n 个 结点的⼆叉树,当且仅当其每⼀个结点都与深度为K的满⼆叉树中编号从 1 ⾄ n 的结点⼀⼀对应时称 之为完全⼆叉树。要注意的是满⼆叉树是⼀种特殊的完全⼆叉树。

人话:倒数第一层可以不满 但是顺序必须和编号一样 然后上面层全是满的。

二叉树的三个性质:

(1):若根节点层数为1 则⼀棵⾮空⼆叉树的第i层上最多有2^(i-1)个节点

(2):若规定根结点的层数为 1 ,则深度为 h 的⼆叉树的最⼤结点数是 2^h − 1

(3):若规定根结点的层数为 1 ,具有 n 个结点的满⼆叉树的深度 h = log 2(n+1)  以2为底, n+1 为对数)

3.二叉树的存储结构

(1)顺序结构

顺序结构存储就是使⽤数组来存储,⼀般使⽤数组只适合表⽰完全⼆叉树,因为不是完全⼆叉树会有 空间的浪费,完全⼆叉树更适合使⽤顺序结构存储。

现实中我们通常把堆(⼀种⼆叉树)使⽤顺序结构的数组来存储

(2)链式结构

⼆叉树的链式存储结构是指,⽤链表来表⽰⼀棵⼆叉树,即⽤链来指⽰元素的逻辑关系。通常的⽅法 是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别⽤来给出该结点左孩⼦和右孩⼦所在的链结点的存储地址。

二    二叉树顺序结构-堆

1.堆的概念与结构

2.堆的实现

3.向上调整算法

(1)入堆

入堆实际上就是给数组尾插一个数据。

但是需要进行向上调整才能继续保持小堆

所以这个15(子节点)需要和它的父节点对比大小 这里15比40小 所以它们两个交换一下

一直这样操作直到遇到父节点比孩子小  或者是15走到了根节点循环结束

void AdJustUp(int *arr,int n)//arr指向数组,n是插入元素的下标
{
    assert(arr)//防止传空
   int child=n,parent=(n-1)/2;   
   while(child>0)//或者parent>=0   当孩子走到根节点,父亲此时越界了 不可以再执行了,或者当父亲走到根节点 再执行一次就不可以了。
    {
      if(arr[child]>arr[parent])//小堆 所以当孩子比父亲大就进入
         {
            swap(&arr[child],&arr[parent);//注意传地址
              child=parent;
               parent=(child-1)/2;                      
         }
      else
       break;//如果孩子没父亲大 就不用参与后面的流程了 已经是小根堆了。
    } 
}
//ps 下方就直接用这个函数了

(2)向上调整建堆

向上调整还可以用于在一个乱序的数组中建堆

在数组上建堆就是 一个个尾插进堆 比如图上建小根堆 如果遇到小的数字就对它进行向上调整即可。调到整个堆是小根堆即可

n是数组长度 
for(int i=0;i<n,i++)
{
  AdJustUp(arr,i);
}

向上调整时间复杂度计算

4.向下调整算法

(1)出堆

出堆实际上是出堆顶 也就是根数据。 但是根出去之后需要一个新的根。

所以出堆需要 数组第一个元素(根)和最后一个元素交换一下 然后堆节点个数-1 

此时解决了根和出元素的问题 但是此时不是小堆了 所以需要进行向下调整来变小堆

void AdJustDown(int *arr,int k,int p) k是堆大小 arr指向数组,p是父节点下标
{
  int parent=p,child=2*parent+1
   while(child<k)//孩子如果大于k 就越界不能执行了 此处不适合用parent 
   {
    if(child+1<k&&arr[child]>arr[child+1]
    {
       child=child+1;     //右孩子没越界并且比左孩子小  所以孩子用右边的
    }
     if(arr[child]<arr[parent]
     {
       swap(&arr[child],&arr[parent]);
        parent=child;
        child=parent*2+1;
     }
     else
      break;
   } 
}

(2)向下调整建堆

 

向下调整需要下方有序  看例子 25会和10换 但是最下方20没办法处理 如果最下方是有序的 那就可以排成小堆了。

k是堆大小
for(int i=(k-1-1)/2;i>=0;i--)此时这个i是父节点 直接指向了25 然后开始循环遍历每个子树
{
  AdJustDown(arr,k,i);
}

向下调整时间复杂度计算

综上 向下调整建堆时间复杂度更小 更推荐。

5.堆的应用

(1)堆排序

版本一
基于已有数组建堆、取堆顶元素完成排序版本
 void HeapSort(int* a, int n)
 {
 HP hp;
 for(int i = 0; i < n; i++)
 {
 HPPush(&hp,a[i]);
 }
 int i = 0;
 while (!HPEmpty(&hp))
 {
 a[i++] = HPTop(&hp);
 HPPop(&hp);
 }
 HPDestroy(&hp);
 }
该版本有⼀个前提,必须提供有现成的数据结构堆

版本二
在数组上建堆(推荐向下调整建堆)

大堆出最大值
小堆出最小值
进行出堆操作即可。
注 出堆会首尾交换
大堆出来是升序 小堆出来是降序

(2)Top-K问题

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

1,把前K个元素建立大/小堆

2,用一个指针来遍历后面的元素(N-K个)和堆的根比较

大堆: 求前K个最小值  因为大堆内部都没根大  所以当遍历到比根小的就可以代替根 进行一次向下调整即可再次成大堆。

PS:如果求前k个最小值用小堆的话   根是最小 把比他大的元素放进去堆那就错了 (根最小的没了)

如果把比它小的元素放进去堆那也错了(堆里面的元素都比根小 根出去了 最小值还是求错了)

小堆:求前K个最大值 因为小堆内部都没根小 所以遍历到比根大的就可以代替根 进行一次向下调整即可再次成小堆。 

三    二叉树链式结构

1.三种遍历方式

(1)前序遍历-根在前

前序遍历是我们说的根左右遍历方式

先遍历根节点A 然后进入左子树B 此时B相当于是新的根 D是左子树 因为先遍历根 所以打印B 进入D 同理打印D 由于D的左子树为空 返回进入右边子树也为空 返回B 此时B该遍历右子树 为空 此时返回A 接下来就该遍历C了。

特点:第一个是根 ,第二个性质和后续特点差不多。

(2)中序遍历-根在中

中序遍历是我们说的左根右遍历方式

从A开始 先遍历左子树 进入B 此时B为新的根

遍历左子树D 此时D为新的根 遍历左边发现为空返回 然后接着遍历根 打印D

最后遍历右子树为空返回  然后返回B B此时左边遍历完毕 开始遍历根 打印B 然后遍历右边为空

返回至A ,A开始遍历根 打印A 然后遍历右子树进入C 然后C左为空 打印根 右为空 返回。

所以中序遍历是 D B A C

特点:可以联合后续或前序来找到根的左右节点是谁。

(3)后序遍历-根在后

后序遍历是我们说的左右根遍历方式

后续就略了 和前面类似

特点:最后一个是根 倒数第二个是根的左或者右边子树(通常是右子树 除非右子树是空。)

2.树的结点个数以及高度

int BTSize(BTNode* root)
{
  if(root==NULL)
    return 0;
return 1+BTSize(root->left)+BTSize(root->right)
}

int BTDepth(BTNode* root)
{
  if(root==NULL)
   return 0;

  int size=BTDepth(root->left);
  int right=BTDepth(root->right);

left>right?return 1+left:return 1+right;
}

节点个数 用前序遍历去走 走到叶子节点时开始返回。 返回值应是1+左右子树的节点数 所以是

1+左右子树返回值。

深度 和节点个数类似 只不过返回值是返回1+  左右子树大的一方  看图 返回至根节点时左子树返回2 右子树返回1 但是这棵树深度是3  所以返回值应该是1+左子树的2 。

3.层序遍历

void BinaryTreeLevelOrder(BTNode* root)
{
	Queue p;  //创建队列
	QueueInit(&p);//初始化队列
	QueuePush(&p, root);//入队列
	while (!QueueEmpty(&p)) 
	{
		BTNode *x=QueueFront(&p);//取队头
		QueuePop(&p);//出队列
		printf("%d ", x->x);
		if (x->left)
			QueuePush(&p, x->left);
		if (x->right)
			QueuePush(&p, x->right);
	}
	QueueDestroy(&p);
}

此时出队列的顺序就是层序遍历的结果。  注 队列保存元素此时保存的是树节点 所以队列保存类型是指针

4.判断是否为完全二叉树

bool BinaryTreeComplete(BTNode* root)
{
	Queue p;
	QueueInit(&p);
	QueuePush(&p, root);
	while (QueueFront(&p) != NULL)
	{
		BTNode* x = QueueFront(&p);
		QueuePush(&p, x->left);
		QueuePush(&p, x->right);
		QueuePop(&p);
	}
	while (!QueueEmpty(&p))
	{
		BTNode* x = QueueFront(&p);
		if (x != NULL)
		{
			QueueDestroy(&p);
			return false;
		}
		QueuePop(&p);
	}
	QueueDestroy(&p);
	return true;
}

此题也要用到队列, 

和上题是一样的 区别在于把空指针也加进来了 很明显 如果是完全二叉树 出现一次空后面就不会有正常元素了 全是空  所以遇到空就循环结束 再判断一下后面有没有正常元素即可

四   二叉树算法题

965. 单值二叉树 - 力扣(LeetCode)

bool isUnivalTree(struct TreeNode* root) {
   if(root==NULL)
   return true;

   if(root->left&&root->left->val!=root->val)
   return false;
   if(root->right&&root->right->val!=root->val)
   return false;
   return isUnivalTree(root->left)&&isUnivalTree(root->right); 
}

大致思路是遇到不为空节点就看它的左子树(1.不为空 2.值不等于它本身的val)就返回false 右子树也是一样  如果遇到空节点就返回true 因为空节点并不能说明他就不是单值二叉树了

返回值要写左右子树 用&&   只要有一个地方出现false 就不是单值二叉树了 所以要用&&。

100. 相同的树 - 力扣(LeetCode)

bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    if(p==NULL&&q==NULL)
    return true;
    else
    if(p==NULL&&q!=NULL||p!=NULL&&q==NULL)
    return false;
    if(p->val!=q->val)
    return false;
    return isSameTree(p->left,q->left)&&isSameTree(p->right,q->right);
}

当然返回值还是要用&&  因为左右子树只要出现一个false 那就不是一个相同的树了。

101. 对称二叉树 - 力扣(LeetCode)

 bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    if(p==NULL&&q==NULL)
    return true;
    else
    if(p==NULL&&q!=NULL||p!=NULL&&q==NULL)
    return false;
    if(p->val!=q->val)
    return false;
    return isSameTree(p->left,q->right)&&isSameTree(p->right,q->left);
}
bool isSymmetric(struct TreeNode* root) {

    return isSameTree(root->left,root->right);
}

本题要用到上一题判断相同树的函数 然后就很简单了 把根的左右子树给判断树相同函数去判断即可。  注意 因为他要判断的是对称 所以return isSameTree(p->left,q->right)&&isSameTree(p->right,q->left);是和上一题有区别的 需要改一下传入值。 p子树的左边和q子树的右边去比。p子树的右边和q子树的左边去比。

572. 另一棵树的子树 - 力扣(LeetCode)

 bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    if(p==NULL&&q==NULL)
    return true;
    else
    if(p==NULL&&q!=NULL||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;
    }
    return isSubtree(root->left,subRoot)||isSubtree(root->right,subRoot);
}

此题也要用到相同树的函数  随便来个遍历 然后加一句判断两个树是否相同即可 左右子树只要有相同的就说明这棵树里面有另一个子树 所以此题用||  不用&&了。

注意 不能用先判断左边节点和右边根相同再判断是否为子树 因为这样的话 1 1 这颗树和1这另一个子树  出的答案就是错的。 它进入函数后在判断是否相同发现结构不同直接返回false了 不会再进入左子树了。 所以走不到根左边直接返回了。

144. 二叉树的前序遍历 - 力扣(LeetCode)

int BTSize(struct TreeNode* root)
 {
    if(root==NULL)
    return 0;
    return 1+BTSize(root->left)+BTSize(root->right);
 }
 void BTFront(struct TreeNode* root,int* i,int* p)
 {
    if(root==NULL)
    return;
    *(p+(*i))=root->val;
    (*i)++;
    BTFront(root->left,i,p);
    BTFront(root->right,i,p);
 }
int* preorderTraversal(struct TreeNode* root, int* returnSize) {
    *returnSize=BTSize(root);
    int *p=(int*)malloc(sizeof(int)*(*returnSize));
    //if~~~exit~~~略
    int i=0;
    BTFront(root,&i,p);
    return p;
}

本题在于他需要把前序遍历放到数组里面 但是没给开辟空间 所以开辟个空间就行。

用求树大小的函数来求树整体大小 再开辟这样大的空间 把前序遍历放进去即可。

二叉树遍历_牛客题霸_牛客网

#include <stdio.h>
#include<stdlib.h>
typedef struct BTNode
{
    char x;
    struct BTNode*left;
    struct BTNode*right;
}BTNode;
BTNode* BuyNewnode(char a)
{
  BTNode* node=(BTNode*)malloc(sizeof(BTNode));
  node->x=a;
  node->left=NULL;
  node->right=NULL;
  return node;
}
BTNode* BTCreate(char* arr,int *pi)//返回值BTNode*是为了给左右孩子赋值。//创建树过程
{
  if(arr[(*pi)++]=='#')                //如果为# 就返回空 如果不为# 就返回node 
  {
    return NULL;
  }
  BTNode* node=BuyNewnode(arr[(*pi)-1]);
  node->left=BTCreate(arr,pi);
  node->right=BTCreate(arr,pi);
  return node;

}
void BTzhong(BTNode* root)
{
    if(root==NULL)
    return;
    BTzhong(root->left);
    printf("%c ",root->x);
    BTzhong(root->right);
}
int main() {
   char arr[100]={0};
   scanf("%s",arr);  //字符数组保存字符串
   int i=0;
  BTNode* root= BTCreate(arr,&i);
  BTzhong(root);

    return 0;
}

五   二叉树选择题

(1)二叉树性质

根据树里面的两个公式(树的性质)

1 结点数=边+1

2 总度=边      

设度为0的节点为n0 度1为n1 度2为n2

n1+n2+n0=边+1

n1+2*n2=边       得出公式n0=n2+1  

直接套用公式 n0=200 选B。。

注意题目 2n说明是偶数个节点 他又要满足完全二叉树 说明度为1的节点有一个。

n0=n2-1  n1+n2+n0=2n  得出n0=n

选A

套用公式  h=log2(n+1) 直接秒sa 选B

767个节点 是奇数节点 说明n1=0

只有n0和n2了  n0+n2=767  n2=n0-1  得出n0=384  选B

(2)链式二叉树遍历选择题

选A 参照画出来的树

两个遍历就可以得到一个完整的树

先序和中序步骤

1.看先序找到根 此题为E  中序中找到E E的左边HFI 右边JKG  说明E左子树节点有HFI 右子树节点有JKG  此时再根据先序来进行遍历就能得到一个完整的树(有多种可能)。

但是此题只说要找根 所以选A

后续遍历也是可以找到根 a是根 从中序遍历得到 b是a的左子树 dce在a的右子树

所以得到了a和b的位置 接下来看dce 

后序遍历中找到了c 并且c还是右子树的节点 根据后序遍历特点左右根 所以c是a的右第一个节点   再看中序遍历 ab已经排了 看de d是c的左子树 e是c的右子树

所以此题选D

后序是ABCDEF

中序是ABCDEF

根据后续遍历 F是根节点  再看中序 ABCDE均为F的左子树 F右子树为空

再看E 因为没右边子树 所以E只能是左边子树第一个节点了

然后看中序 E也是只有左子树

以此类推此树为

选A

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值