笔记_二叉树1

1.存储形式与逻辑

是一种调用格式

是一种存储格式

1:作为数组来说,在完全二叉树时,子节点和父节点的下标有关系,可以通过下标跳转调用的方式(2*n/2*n+1)访问子节点。

2.作为链表来说,它的存储逻辑是“一个父节点最多可拥有两个子节点”。

二叉树是由父节点延伸出子节点,而后子节点再作为父节点去进行延伸。这是一种递归的表述方式。

二叉树的本质属于递归。

数组和链表两种调用方式在二叉树叙述上的优劣:

  1. 数组形式:

优:用下标来访问更加方便直接,不需要什么预处理等等……

劣:仅适用于完全二叉树,在不完全的、接近链式二叉树的情况下无法存储(数组下标需求太大,内存危机),且在进行递归访问的时候需要确定边界范围以防数组越界

     2.链表形式:

优:用链表来访问,实打实的凭借数据间的关系而构建,在递归时不用考虑范围,在面对接近链式的不完全二叉树的时候能进行存储

劣:链表的通病,在访问单点数据时不便,且在存储时必须要清晰给出父子节点的关系

2.题目中的二叉树

二叉树貌似更多作为高级数据结构的基础,作为纯自身的应用比较稀少或者过于特意,但是有不少专门考察二叉树各个方面的题;

考题更多是侧重在考察对于二叉树这一结构的理解和操作,如给出数据让你构造、整理、返回二叉树,

常见的就是给出父子节点关系,让你返回并操作一颗初始二叉树,或在前中后续遍历上做文章。

例1

对于节点间的关系的理解

题目:

有一个 n (n≤10^{6}) 个结点的二叉树。给出每个结点的两个子结点编号(均不超过 n),建立一棵二叉树(根节点的编号为 11),如果是叶子结点,则输入 0 0

建好这棵二叉树之后,请求出它的深度。二叉树的深度是指从根节点到叶子结点时,最多经过了几层

解题思路:

知道父子节点关系后,可以以数组构造链表的形式来存储

构造数组结构体,结构体里面的元素存放左右子节点

数组的下标作为父节点,数组的内容为左右子节点,

再之后的求二叉树深度中,从根节点开始往下找子树节点,每向下找一层,二叉树深度加1;而后子树节点又可以作为父节点,重复此过程。叶子节点的子节点都为0,进行一下特殊判断即设置边界。

递归完成。

相比传统链表,方便简洁,易操作易理解。

#include<stdio.h>
#include<iostream>
using namespace std;

struct p{
  int lc,rc;  
}tree[1000009];


int dfs(int po)
{
    if(po==0) return 0;
    return max(dfs(tree[po].lc),dfs(tree[po].rc))+1;
}


main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    scanf("%d%d",&tree[i].lc,&tree[i].rc);
    printf("%d",dfs(1));
}

例2

对于二叉树三种遍历方式的理解

题目:

给出一颗二叉树的前序和中序遍历(节点以字母表示),求它的后序遍历

分析:解决这类题目的关键是领会二叉树的三种遍历方式的序列分别意味着什么。

可以先去分析三种遍历的序列规律。

前序遍历序列:根 左子树 右子树;

中序遍历序列:左子树 根 右子树;

后序遍历序列:左子树 右子树 根;

观察以上式子,很明显,在前序遍历与后序遍历中,树的根都是明确知晓的;而当你试图通过前序和后序遍历复原二叉树,你会发现你所遇到的最大的、也是唯一的障碍,就是它们的左右子树无法找到分界标志去分开而混淆在一起了。

而再看中序遍历,乍一看,根本没有任何信息,左右子树与根又没有什么分界点,即看不出左右子树也找不出根。但是,再观察一下便能发现一个关键点,中序遍历的左右子树有分界线,这条分界线就是根

换言之,只要知到根,凭借中序遍历,左右子树的就能完全分开了。而题目前序/后序遍历正好提供这个条件。

找到根→分开左右子树;而分开后的子树也可以同样当作根,进行同样的操作,

——又是递归。

#include<bits/stdc++.h>
using namespace std;
string a,b;

void dfs(int al,int ar,int bl,int br)
{
	for(int i=al;i<=ar;i++)
	{
		if(a[i]==b[bl])
		{
			dfs(al,i-1,bl+1,bl+i-al);
			dfs(i+1,ar,bl+i-al+1,br);
			cout<<b[bl];
			return;
		}
	}
}

main()
{
	cin>>a>>b;
	int n=a.size();
	dfs(0,n-1,0,n-1);
}

例三

二叉搜索树

题目

需要写一种数据结构,来维护一些数( 都是绝对值 10^{9}以内的数字)的集合,最开始时集合是空的。其中需要提供以下操作,操作次数 q 不超过 10^{4}

  1. 查询 x 数的排名(排名定义为比当前数小的数的个数 +1。若有多个相同的数,应输出最小的排名)。
  2. 查询排名为 x 的数。
  3. 求 x 的前驱(前驱定义为小于 x,且最大的数)。若未找到则输出 −2147483647。
  4. 求 x 的后继(后继定义为大于 x,且最小的数)。若未找到则输出 2147483647。
  5. 插入一个数 x。

该题需要快速的完成插入数据与排序两个操作,对于常规方式来说,每一次放入都得重新排序才能支持后续操作,但是,二叉树能够提供一种分类方式:

二叉树每个节点可以分出两个孩子节点,这其实可以看作是一个“0”和“1”,即某个事件真假的判断,而子节点可以记录对于判断的回答,存储某个事件的一项属性。而在树延伸的过程中,进行的判断越来越多,可以继续分出更多的、不同的事件。

而把每次分左右子树的判别看成“该数是否大于父节点的数”,便可得到二叉搜索树(这是我个人对它能完成这样功能的原因的浅显理解)。

二叉搜索是一棵空树或具有以下几种性质的树:

  1. 若左子树不空,则左子树上所有结点的值均小于它的根结点的值(判断分类)

  2. 若右子树不空,则右子树上所有结点的值均大于它的根结点的值(判断分类)

  3. 左、右子树也分别为二叉搜索树(满足递归)

  4. 没有权值相等的结点。(二叉树的每个节点都是一个不同的”事件“)

对于4,在数据中遇到多个相等的数时,由于上述对于二叉树分类功能的解释,很明显,同类的“事件”应该进行合并,即可用一个变量来记录这个“事件”出现的次数。

那么我们的每一个节点都包含以下几个信息:

  1. 当前节点的权值,也就是序列里的数

  2. 左孩子的下标和右孩子的下标,如果没有则为0

  3. 计数器,代表当前的值出现了几遍

  4. 子树大小和自己的大小的和

节点:

struct node{
	int val,ls,rs,cnt,siz;
}tree[500010];

其中 val 是权值,ls / rs 是左/右 孩子的下标,cnt 是当前的权值出现了几次,siz 是子树大小和自己的大小的和。

插入:

x 是当前节点的下标,v 是要插入的值。要在树上插入就要找到一个合适 v 的位置。

此时两种情况:

1.树的节点内已有代表 v 的值的节点,直接把该节点的计数器加 1 。

2.树的节点内无代表v的值的节点,则向下寻找,重复这个过程,直到找到叶子节点。此时直接从这个叶子节点连出一个儿子,代表 v 的节点。

void add(int x,int v)
{
	tree[x].siz++; //到这个节点来判断:左/右子树还是自身,无论如何排名必加1
	if(tree[x].val==v){
		//满足第四条件
		tree[x].cnt++;
		return ;
	}
	if(tree[x].val>v){//v在x的左子树里
		if(tree[x].ls!=0)//如果有左子树
		  add(tree[x].ls,v);//继续递归至子叶
		else{//如果没有左子树 则添加左子树
			cont++;//节点+
			tree[cont].val=v;
			tree[cont].siz=tree[cont].cnt=1;
			tree[x].ls=cont;
		}
	}
	else{//右子树与上文同理
		if(tree[x].rs!=0)
		  add(tree[x].rs,v);
		else{
			cont++;
			tree[cont].val=v;
			tree[cont].siz=tree[cont].cnt=1;
			tree[x].rs=cont;
		}
	}
}

按值找排名:

排名就是比这个值要小的数的个数再 +1,所以我们按值找排名,即找比这个值小的数的个数,最后加上 1 即可。

int queryval(int x,int val)
{
	if(x==0) return 0;//空节点 递归出口,返回 
	if(val==tree[x].val) return tree[tree[x].ls].siz;//找到排名
	if(val<tree[x].val) return queryval(tree[x].ls,val);//去左子树 
	return queryval(tree[x].rs,val)+tree[tree[x].ls].siz+tree[x].cnt;
    //去右子树,并且加上左子树中节点的个数与父节点出现的次数
}

按排名找值:

要去找排名为 n 的数,即在二叉搜索树上找树上第 n 靠左的数。

rk是要找的排名

int queryrk(int x,int rk)
{
	if(x==0) return INF; //到子叶节点
	if(tree[tree[x].ls].siz>=rk)//去左子树 
		return queryrk(tree[x].ls,rk); 
	if(tree[tree[x].ls].siz+tree[x].cnt>=rk)//左子树大小加上当前数恰好>=k 即要找的就是该节点
		return tree[x].val;//找到了,直接返回
	return queryrk(tree[x].rs,rk-tree[tree[x].ls].siz-tree[x].cnt);
    //否则就查右子树,同时要的名次减去左子树与父节点出现的次数
}

找前驱后继:

其实直接通过排名来找就行了,该数的前一名就是它的前驱,后一名就是它的后驱。即先查询该数的排名,再查询排名+1/-1所对应的数即可。

  • 14
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值