3.12二叉树与堆
3.12二叉树与堆
二叉树
树的定义:树 (Tree) 是 n(n>=0) 个结点的有限集。n=0 时称为空树。
在任意一颗非空树中:
(1) 有且仅有一个特定的称为根 (root) 的结点;
(2) 当 n>1时,其余结点可分为 m(m>0) 个互不相交的有限集 T1、T2、......、Tn,
其中每一个集合本身又是一棵树,并且称为根的子树。
此外,树的定义还需要强调以下两点:
(1) n>0 时根结点是唯一的,不可能存在多个根结点,数据结构中的树只能有一个根结点。
(2) m>0 时,子树的个数没有限制,但它们一定是互不相交的。
对于任意一棵包含n节点树来说,都有且只有 n-1 条边。
二叉树是每个结点最多有两个子树的树结构。
通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。
二叉树常被用于实现二叉查找树和二叉堆。
二叉树是递归定义的,其结点有左右子树之分,逻辑上二叉树有五种基本形态:
- 空二叉树
- 仅有根结点的二叉树
- 只有左子树的二叉树
- 只有右子树的二叉树
- 左右子树都有的二叉树
二叉树的类型
(1) 满二叉树——除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。
从图形形态上看,满二叉树外观上是一个三角形。
从数学上看,满二叉树的各个层的结点数形成一个首项为 1,公比为 2 的等比数列。
因此由等比数列的公式,满二叉树满足如下性质。
- 一个层数为 k 的满二叉树总结点数为:\(2^k−1\)。因此满二叉树的结点数一定是奇数个。
- 第 \(i\) 层上的结点数为:\(2^{i−1}\).
- 一个层数为 k 的满二叉树的叶子结点个数(也就是最后一层):\(2^{k−1}\).
(2) 完全二叉树——若设二叉树的高度为 h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。
- 所有的叶结点都出现在第 k 层或 k-1 层(层次最大的两层)
- 对任一结点,如果其右子树的最大层次为 L,则其左子树的最大层次为 L 或 L+1。
树的一些名词
节点的层次:从根开始定义起,根为第 1 层,根的子节点为第 2 层,以此类推;
节点的度:一个节点含有的子树的个数称为该节点的度;
树的高度或深度:树中节点的最大层次;
树的度:一棵树中,最大的节点的度称为树的度;
叶节点或终端节点:度为 0 的节点称为叶节点;
非终端节点或分支节点:度不为 0 的节点;
节点的祖先:从根到该节点所经分支上的所有节点;
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
兄弟节点:具有相同父节点的节点互称为兄弟节点;
堂兄弟节点:双亲在同一层的节点互为堂兄弟;
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
森林:由m(m>=0)棵互不相交的树的集合称为森林;
二叉树的遍历分为以下几种:
- 先序遍历:遍历顺序规则为【根左右】
- 中序遍历:遍历顺序规则为【左根右】
- 后序遍历:遍历顺序规则为【左右根】
- 层序遍历:遍历顺序规则为【上下左右】
【根左右】就是先遍历根,再遍历左子树,最后遍历右子树;所谓的先中后都是针对根节点的顺序。
【层序遍历的实现是通过队列 BFS 实现的】
将当前节点的左右子节点入队后依次出队,将出队节点的左右子节点继续入队,直到遍历完成。
如上图的三种遍历,答案如下:
先序遍历:ABDECFG
中序遍历:DBEAFCG
后序遍历:DEBFGCA
层序遍历:ABCDEFG
//定义树的节点
typedef struct Node{
char data;
struct Node *lch, *rch;
}Node, *Tree;
//先序遍历
void preorder(Tree node){
if(node==NULL) return;
cout<<node->data;
preorder(node->lch);
preorder(node->rch);
}
//中序遍历
void inorder(Tree node){
if(node==NULL) return;
inorder(node->lch);
cout<<node->data;
inorder(node->rch);
}
//后序遍历
void postorder(Tree node){
if(node==NULL) return;
postorder(node->lch);
postorder(node->rch);
cout<<node->data;
}
1336:【例3-1】找树根和孩子
题目描述
给定一棵树,输出树的根root,孩子最多的结点max以及他的孩子。
输入格式
第一行:n(结点个数≤100),m(边数≤200)。
以下m行:每行两个结点x和y,表示y是x的孩子(x,y≤1000)。
输出格式
第一行:树根:root;
第二行:孩子最多的结点max;
第三行:max的孩子(按编号由小到输出)。
输入样例
8 7
4 1
4 2
1 3
1 5
2 6
2 7
2 8
输出样例
4
2
6 7 8
参考程序
#include<bits/stdc++.h>
using namespace std;
const int N=110;
int n,m,x,y, root=0, max_v=0, max_root=0;
int father[N];// father[i]表示 i的父节点
int main(){
cin>>n>>m;
for(int i=1; i<=m; i++){
cin>>x>>y; father[y]=x;//x是y的父节点
}
for(int i=1; i<=n; i++){
if(father[i]==0){//没有父亲,则为根节点
root=i; break;
}
}
for(int i=1; i<=n; i++){
int sum=0;
for(int j=1; j<=n; j++){
if(father[j]==i) {
sum++;
}
}
if(sum>max_v){//孩子节点最多的节点
max_v=sum, max_root=i;
}
}
cout<<root<<endl<<max_root<<endl;
for(int i=1; i<=n; i++){
if(father[i]==max_root){//max_root的孩子节点
cout<<i<<" ";
}
}
return 0;
}
P1305 新二叉树
题目描述
输入一串二叉树,输出其前序遍历。
输入格式:
第一行为二叉树的节点数 n。( 1≤n≤26)
后面 n 行,每一个字母为节点,后两个字母分别为其左右儿子。
空节点用 * 表示
输出格式: 二叉树的前序遍历。
输入样例:
6
abc
bdi
cj*
d**
i**
j**
输出样例:
abdicj
【参考程序 1】
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
char tree[N][2], root;
void dfs(char x){
if(x<'a' || x>'z') return ;
cout<<x;
dfs(tree[x][0]);
dfs(tree[x][1]);
}
int main(){
int n; cin>>n;
for(int i=1; i<=n; i++){
char a,b,c; cin>>a>>b>>c;
if(i==1) root=a;//确定根节点
tree[a][0]=b, tree[a][1]=c;
}
dfs(root);
return 0;
}
【参考程序 2】
#include<cstdio>
using namespace std;
const int N=30;
bool vis[N], isNotRoot[N];
char s[5];
struct Node{
int lch=-1, rch=-1;
}tree[N];
void build(int p, int l, int r){
vis[p] = true;
if(l>=0){
tree[p].lch = l;
vis[l] = true;
isNotRoot[l] = true;
}
if(r>=0){
tree[p].rch = r;
vis[r] = true;
isNotRoot[r] = true;
}
}
void preorder(int r){
if(r<0) return ;
printf("%c", char(r+'a'));
preorder(tree[r].lch);
preorder(tree[r].rch);
}
int main(){
int n; scanf("%d", &n);
for(int i=1; i<=n; i++){
scanf("%s",s);
build(s[0]-'a', s[1]-'a', s[2]-'a');
}
for(int i=0; i<26; i++){
if(vis[i] && !isNotRoot[i]){
preorder(i);break;
}
}
return 0;
}
根据先序遍历建树
【题目描述】
输入一串先序遍历字符串,根据此字符串建立一个二叉树。
例如如下的先序遍历字符串:ABC##DE#G##F###
其中 '#' 表示的是空格,空格字符代表空树。
建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。
【输入格式】
输入包括 1 行字符串,长度不超过 100。
【输出格式】
可能有多组测试数据,对于每组数据,
输出将输入字符串建立二叉树后中序遍历的序列,每个字符后面都有一个空格。
每个输出结果占一行。
【样例输入】abc##de#g##f###
【样例输出】c b e g d f a
【参考程序】
#include<bits/stdc++.h>
using namespace std;
const int N=110;
char a[N];
int cnt=0;
typedef struct Node{
char data;
struct Node *lch, *rch;
}Node, *Tree;
//递归建树
struct Node* build(){
struct Node *root=NULL;
if(a[cnt++]!='#'){
root = (struct Node*)malloc(sizeof(struct Node));
root->data = a[cnt-1];
root->lch = build();
root->rch = build();
}
return root;
}
//中序遍历
void inorder(struct Node *root){
if(root!=NULL){
inorder(root->lch);
printf("%c ", root->data);
inorder(root->rch);
}
}
//销毁二叉树
void del(struct Node* &root){
if(root!=NULL){
del(root->lch);
del(root->rch);
delete root;
root = NULL;
}
}
int main(){
while(scanf("%s", a)!=EOF){
cnt=0;
struct Node *root=build();
inorder(root);
del(root);
printf("\n");
}
return 0;
}
根据层序遍历和中序遍历建树
【题目描述】
给一棵二叉树的层序遍历序列和中序遍历序列,
求这棵二叉树的先序遍历序列和后序遍历序列。
【输入格式】
第一行一个正整数N(1<=N<=30),代表二叉树的结点个数(结点编号为1~N)。
接下来两行,每行N个正整数,分别代表二叉树的层序遍历序列和中序遍历序列。
数据保证序列中1~N的每个数出现且只出现一次。
【输出格式】
输出一行,包含N个正整数,代表二叉树的先序遍历序列。
每行末尾不输出额外的空格。
【样例输入】
7
3 5 4 2 6 7 1
2 5 3 6 4 7 1
【样例输出】
3 5 2 4 6 7 1
【参考程序】
根据前序遍历和中序遍历建树
【题目描述】
已知一棵二叉树的前序遍历和中序遍历,求二叉树的后序遍历。
【输入格式】
输入数据有多组,第一行是一个整数t (t<1000),代表有t组测试数据。
每组包括两个长度小于50 的字符串,
第一个字符串表示二叉树的先序遍历序列,
第二个字符串表示二叉树的中序遍历序。
【输出格式】
每组第一行输出二叉树的后序遍历序列,第二行输出二叉树的层次遍历序列
【样例输入】
2
abdegcf
dbgeafc
xnliu
lnixu
【样例输出】
dgebfca
abcdefg
linux
xnuli
【参考程序】
#include<stdio.h>
#include<string.h>
#define N 110
char pre[N] = "abcdefg";
char in[N] = "cbdaegf";
struct Node{
char data;
struct Node *lch, *rch;
};
Node* build(int pl,int pr,int il,int ir){
if(pl>pr) return NULL;
Node* root = new Node;
root->data = pre[pl];
int k=il;
while(pre[pl]!=in[k] && k<=ir) k++;
root->lch = build(pl+1, pl+k-il, il, k-1);
root->rch = build(pl+k-il+1, pr, k+1, ir);
return root;
}
void postorder(Node* root){
if(root==NULL) return;
postorder(root->lch);
postorder(root->rch);
printf("%c", root->data);
}
void levorder(Node* root){
int head=0, rear=-1;
Node* que[110]; que[++rear]=root;
while(head <= rear){
Node* node = que[head];
if(node->lch!=NULL) que[++rear] = node->lch;
if(node->rch!=NULL) que[++rear] = node->rch;
++head;
printf("%c", node->data);
}
}
int main(){
freopen("data.in", "r", stdin);
int n; scanf("%d", &n);
while(~scanf("%s%s",pre,in)){
int len = strlen(pre)-1;
Node* tree = build(0,len,0,len);
postorder(tree); printf("\n");
levorder(tree); printf("\n");
memset(pre, 0, sizeof(pre));
memset(in, 0, sizeof(in));
}
return 0;
}
堆
堆的定义:堆(英语:heap)是计算机科学中一类特殊的数据结构的统称。
堆通常是一个可以被看做一棵树的数组对象,总是满足下列性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
将根节点最大的堆叫做最大堆或大根堆,
根节点最小的堆叫做最小堆或小根堆。
注意:堆内的元素并不一定数组下标顺序来排序的!!
- 由于优先队列实现了堆,所以我们可以直接用优先队列来完成堆的操作。
优先队列: priority_queue
priority_queue<int> q; // 定义,默认大顶堆
priority_queue<int, vector<int>, less<int> > q; // 大顶堆
priority_queue<int, vector<int>, greater<int> > q; // 小顶堆
q.push(x); // 入堆
q.pop(); // 出堆
q.top(); // 堆顶
q.size(); // 元素个数
具体操作:(我们以小根堆为例)
-
上浮
从当前结点开始,和它的父亲节点比较,若是比父亲节点来的小,就交换,
然后将当前询问的节点下标更新为原父亲节点下标;否则退出。 -
下沉
让当前结点的左右儿子(如果有的话)作比较,哪个比较小就和它交换,
并更新询问节点的下标为被交换的儿子节点下标,否则退出。 -
插入
每次插入的时候呢,我们都往最后一个插入,让后使它上浮。 -
弹出
把堆顶元素弹掉,让根节点元素和尾节点进行交换,然后让当前的根元素下沉就可以了。
堆插入和删除,每次O(logN)
程序实现:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int heap[N], size=0, a[N];
//建立大根堆,上浮
void put_less(int x){
heap[++size]=x;
int father, son=size;
while(son>1){
father = son/2;//大根堆,父亲大于儿子
if(heap[father]>=heap[son]) break;
swap(heap[father], heap[son]);
son = father;
}
}
//得到大根堆堆顶元素,下沉
int get_less(){
int father=1, son, ans=heap[1];
heap[1]=heap[size--];
while(father*2<=size){
son = father*2; //找寻最大的儿子
if(son<size && heap[son]<heap[son+1]) son++;
if(heap[father]>=heap[son]) break;
swap(heap[father], heap[son]);
father = son;
}
return ans;
}
//建立小根堆,上浮
void put_greater(int x){
heap[++size]=x;
int father, son=size;
while(son>1){
father=son/2;
if(heap[father]<=heap[son]) break;
swap(heap[father], heap[son]);
son = father;
}
}
//得到小根堆堆顶元素,下沉
int get_greater(){
int father=1, son, ans=heap[1];
heap[1]=heap[size--];
while(father*2<=size){
son = father*2; //找寻最小的儿子
if(son<size && heap[son]>heap[son+1]) son++;
if(heap[father]<=heap[son]) break;
swap(heap[father], heap[son]);
father = son;
}
return ans;
}
int main(){
freopen("data.in", "r", stdin);
int n; cin>>n;
for(int i=1; i<=n; i++) cin>>a[i];
memset(heap, 0, sizeof(heap)); //初始化
for(int i=1; i<=n; i++) put_less(a[i]); //建立大根堆
while(size) cout<<get_less()<<" "; cout<<endl;
memset(heap, 0, sizeof(heap)); //初始化
for(int i=1; i<=n; i++) put_greater(a[i]);//建立小根堆
while(size) cout<<get_greater()<<" "; cout<<endl;
return 0;
}
1369:合并果子(fruit)
【题目描述】
在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。
多多决定把所有的果子合成一堆。
每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。
可以看出,所有的果子经过 n 次合并之后,就只剩下一堆了。
多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。
假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,
你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。
例如有3种果子,数目依次为 1,2,9。可以先将 1、2 堆合并,新堆数目为 3,耗费体力为 3。
接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12,耗费体力为 12。
所以多多总共耗费体力=3+12=15。可以证明 15 为最小的体力耗费值。
【输入】
两行,第一行是一个整数n(1≤n≤30000),表示果子的种类数。
第二行包含 n 个整数,用空格分隔,第 i 个整数 ai(1≤ai≤20000)是第 i 种果子的数目。
【输出】
一行,这一行只包含一个整数,也就是最小的体力耗费值。
输入数据保证这个值小于 2^31。
【输入样例】
3
1 2 9
【输出样例】
15
【样例2输入】
10
3 5 1 7 6 4 2 5 4 1
【样例2输出】
120
【参考程序】
#include<bits/stdc++.h>
#define ll long long
using namespace std;
priority_queue<ll, vector<ll>, greater<ll> > que;
int main(){
ll n,x,ans=0; cin>>n;
for(int i=1; i<=n; i++){
cin>>x; que.push(x);
}
while(que.size()>1){
ll a=que.top(); que.pop();
ll b=que.top(); que.pop();
que.push(a+b);
ans+=a+b;
}
cout<<ans<<endl;;
return 0;
}
1370:最小函数值(minval)
【题目描述】
有 n 个函数,分别为 F1,F2,...,Fn。定义 Fi(x) = Ai * x^2 + Bi * x + Ci (x∈N+)。
给定这些 Ai、Bi 和 Ci,请求出所有函数的所有函数值中最小的 m 个(如有重复的要输出多个)。
【输入】
第一行输入两个正整数 n 和 m。
以下 n 行每行三个正整数,其中第 i 行的三个数分别位 Ai、Bi 和 Ci。
输入数据保证 Ai<=10,Bi<=100,Ci<=10000。
【输出】
将这 n 个函数所有可以生成的函数值排序后的前 m 个元素。
这 m 个数应该输出到一行,用空格隔开。
【输入样例】
3 10
4 5 3
3 4 5
1 7 1
【输出样例】
9 12 12 19 25 29 31 44 45 54
【数据规模】n,m≤10000。
【参考程序】
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;
int heap[N], size=0, ans[N];
struct T{
int a,b,c;
}t[N];
//建立大根堆,上浮
void put_less(int x){
heap[++size]=x;
int father, son=size;
while(son>1){
father = son/2;//大根堆,父亲大于儿子
if(heap[father]>=heap[son]) break;
swap(heap[father], heap[son]);
son = father;
}
}
//得到大根堆堆顶元素,下沉
int get_less(){
int father=1, son, ans=heap[1];
heap[1]=heap[size--];
while(father*2<=size){
son = father*2; //找寻最大的儿子
if(son<size && heap[son]<heap[son+1]) son++;
if(heap[father]>=heap[son]) break;
swap(heap[father], heap[son]);
father = son;
}
return ans;
}
int main(){
int n,m, a,b,c; cin>>n>>m;
for(int i=1; i<=n; i++) cin>>t[i].a>>t[i].b>>t[i].c;
memset(heap, 0, sizeof(heap));
for(int i=1; i<=m; i++) {
for(int j=1; j<=n; j++){
int temp = t[j].a*i*i+t[j].b*i+t[j].c;
if(size<m) put_less(temp); //不足 m 个,直接加入
else if(temp<heap[1]) get_less(), put_less(temp);
}
}
for(int i=1; i<=m; i++) ans[i]=get_less();
for(int i=m; i>=1; i--) cout<<ans[i]<<" ";
return 0;
}
1371:看病
【题目描述】
有个朋友在医院工作,想请BSNY帮忙做个登记系统。
具体是这样的,最近来医院看病的人越来越多了,因此很多人要排队,只有当空闲时放一批病人看病。
但医院的排队不同其他排队,因为多数情况下,需要病情严重的人优先看病,
所以希望BSNY设计系统时,以病情的严重情况作为优先级,判断接下来谁可以去看病。
【输入】
第一行输入 n,表示有 n个操作。
对于每个操作,首先输入 push 或 pop。
push 的情况,之后会输入 ai 和 bi,分别表示患者姓名和患者病情优先级。
pop 后面没有输入,但需要你输出。
【输出】
对于 pop 的操作,输出此时还在排队人中,优先级最大的患者姓名和优先级。
表示他可以进去看病了。
如果此时没人在排队,那么输出”none”,具体可见样例。
【输入样例】
7
pop
push bob 3
push tom 5
push ella 1
pop
push zkw 4
pop
【输出样例】
none
tom 5
zkw 4
【数据规模和约定】
1≤n≤100000,每个人的优先级都不一样,0≤优先级≤2000000000。
姓名都是小写字母组成的,长度小于20。
【参考程序】
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
struct T{
string name;
int priority;
} heap[N], a;
int size=0;
//建立大根堆,上浮
void put_less(T x){
heap[++size]=x;
int father, son=size;
while(son>1){
father = son/2;//大根堆,父亲大于儿子
if(heap[father].priority>=heap[son].priority) break;
swap(heap[father], heap[son]);
son = father;
}
}
//得到大根堆堆顶元素,下沉
T get_less(){
int father=1, son;
T ans=heap[1];
heap[1]=heap[size--];
while(father*2<=size){
son = father*2; //找寻最大的儿子
if(son<size && heap[son].priority<heap[son+1].priority) son++;
if(heap[father].priority>=heap[son].priority) break;
swap(heap[father], heap[son]);
father = son;
}
return ans;
}
int main(){
int n; cin>>n;
for(int i=1; i<=n; i++){
string opt; cin>>opt;
if(opt=="push") {
cin>>a.name>>a.priority; put_less(a);
}else if(opt=="pop"){
if(size==0) cout<<"none\n";
else{
T temp=get_less();
cout<<temp.name<<" "<<temp.priority<<endl;
}
}
}
return 0;
}