平衡树算法

一、平衡树用来干什么

您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:

  1. 插入 xxx 数
  2. 删除 xxx 数(若有多个相同的数,因只删除一个)
  3. 查询 xxx 数的排名(排名定义为比当前数小的数的个数 +1+1+1 )
  4. 查询排名为 xxx 的数
  5. xxx 的前驱(前驱定义为小于 xxx,且最大的数)
  6. xxx 的后继(后继定义为大于 xxx,且最小的数)

二、平衡树与二叉排序树区别

平衡树是二叉搜索树和堆合并构成的数据结构,它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡树的平均查找长度要小于等于二叉排序树的平均查找长度

平衡树是二叉排序树通过旋转来达到最优二叉排序树

平衡树本身就是二叉排序树

不懂二叉排序树的可以看一下:二叉排序树的构造 && 二叉树的先序、中序、后序遍历

三、二叉平衡树复杂度

前提:包含n个顶点的二叉平衡树(AVL树)

1、查找一个节点时间复杂度为O(lgn)

2、插入的时间复杂度O(lgn)

3、删除一个节点时间复杂度为O(lgn)

代码中会发现每一次操作之后都会进行旋转操作,这样导致平衡树的时间复杂度常数很大

四、平衡树的构造

参考链接:https://blog.csdn.net/a_comme_amour/article/details/79382104

1、变量声明

f[i]表示i的父结点,ch[i][0]表示i的左儿子,ch[i][1]表示i的右儿子,key[i]表示i的关键字(即结点i代表的那个数字),cnt[i]表示i结点的关键字出现的次数(相当于权值),sizes[i]表示包括i的这个子树的大小;sz为整棵树的大小,rt为整棵树的根。

注意:

sz代表的是不同节点种类数,比如你要插入5 5 4 3 4这些数,那么sz的大小是3

sizes代表的就是节点有几个,就不是种类个数了

几个小函数:

void clears(int x) //删除x点信息
{
    f[x]=cnt[x]=ch[x][0]=ch[x][1]=sizes[x]=key[x]=0;
}
bool get(int x) //判断x是父节点的左孩子还是右孩子
{
    return ch[f[x]][1]==x;  //返回1就是右孩子,返回0就是左孩子
}
void pushup(int x)  //重新计算一下x这棵子树的节点数量
{
    if(x)
    {
        sizes[x]=cnt[x];
        if(ch[x][0]) sizes[x]+=sizes[ch[x][0]];
        if(ch[x][1]) sizes[x]+=sizes[ch[x][1]];
    }
}

2、旋转操作

 怎么旋转看下面

 让4旋转到他的父亲位置2(我们代码中的旋转就是让一个节点移动到他的父亲节点位置)

首先我们要知道平衡树就是二叉排序树,那么二叉排序树儿子节点的特点就是左节点的值小于父节点的值,右节点的值大于父节点的值

那么4节点移动到了2节点(4节点的父亲节点)的位置,4节点会把它的右儿子(也就是9号节点)给2号节点充当左儿子,然后2号节点在作为4号节点的右儿子节点出现

 旋转操作代码:

 1 void rotates(int x)  //将x移动到他父亲的位置,并且保证树依旧平衡
 2 {
 3     int fx=f[x],ffx=f[fx],which=get(x);
 4     //x点父亲,要接受x的儿子。而且x与x父亲身份交换
 5     ch[fx][which]=ch[x][which^1];
 6     f[ch[fx][which]]=fx;
 7 
 8     ch[x][which^1]=fx;
 9     f[fx]=x;
10 
11     f[x]=ffx;
12     if(ffx) ch[ffx][ch[ffx][1]==fx]=x;
13 
14     pushup(fx);
15     pushup(x);
16 }

3、splay操作

这个就是把树上的一个节点旋转的树根位置,分为两种操作:

<一>、

一个点p和他的父亲(fa)同为一边的儿子(如图同为左儿子) 此时应先rotate(p.fa), 再rotate(p)

<二>、

一个点p和他的父亲不是同一边的儿子 此时两次rotate(p)即可

 1 void splay(int x)  //将x移动到数根节点的位置,并且保证树依旧平衡
 2 {
 3     for(int fx; fx=f[x]; rotates(x))
 4     {
 5         if(f[fx])
 6         {
 7             rotates((get(x)==get(fx))?fx:x);
 8             //如果祖父三代连城一条线,就要从祖父哪里rotate
 9             //至于为什么要这样做才能最快……可以去看看Dr.Tarjan的论文
10         }
11     }
12     rt=x;
13 }

注意:void splay(int x)  这个x是这个节点在树上的位置

假设上面节点1、fa、3的cnt值都为1 

那么这个fa这个节点在树上的位置就是2,3这个节点在树上的位置就是3

4、void inserts(int x),插入一个节点(这个x是你要插入值的大小)

代码实现

 1 void inserts(int x)
 2 {
 3     if(rt==0)
 4     {
 5         sz++;
 6         key[sz]=x;
 7         rt=sz;
 8         cnt[sz]=sizes[sz]=1;
 9         f[sz]=ch[sz][0]=ch[sz][1]=0;
10         return;
11     }
12     int now=rt,fx=0;
13     while(1)
14     {
15         if(x==key[now])
16         {
17             cnt[now]++;
18             pushup(now);
19             pushup(fx);
20             splay(now);  //splay的过程会rotates now点的所有祖先节点,这个时候它们所有子树权值也更新了
21             return;
22         }
23         fx=now;
24         now=ch[now][key[now]<x];
25         if(now==0)
26         {
27             sz++;
28             sizes[sz]=cnt[sz]=1;
29             ch[sz][0]=ch[sz][1]=0;
30             ch[fx][x>key[fx]]=sz;  //二叉查找树特性”左大右小“
31             f[sz]=fx;
32             key[sz]=x;
33             pushup(fx);
34             splay(sz);
35             return ;
36         }
37     }
38 }

注意:插入完之后要把这个刚插入到树上的点给通过splay函数旋转到树根,至于为什么要这样做。大家可以这样想,假设插入之前的树是一颗最优二叉排序树,那么插入一个点之后可能就不最优了,所以我们要旋转这棵树,以保证这棵树还是最优的

5、int rnk(int x)  //查询x的排名

这个函数就是插入x这个值,他在平衡树上的位置

代码实现

 1 /*
 2 有人问:
 3 很想知道为什么rnk操作也要splay操作呢?如果del要用的话直接splay(x)是不是就可以了
 4 
 5 原博客答:
 6 呃不不不这个貌似不是随便splay以下就可以的 首先find之后的splay就是将找到的这个点转到根,
 7 当然你不加这个应该是也可以,只不过这道题加上的话对于这一堆操作来说比较方便,不过一般来说转一转splay的
 8 平衡性会好一点(当然也不要转得太多了就tle了...) 但是del之前直接splay(x)要视情况而定,关键在于分清楚
 9 “点的编号”和“点的权值”这两个概念。如果你已经知道了该转的点的编号,当然可以直接splay(x),但是如果你只
10 知道应该splay的点的权值,你需要在树里find到这个权值的点的编号,然后再splay 其实最后splay写起来都是非
11 常灵活的,而且有可能一个点带若干个权之类的。对于初学者的建议就是先把一些最简单的情况搞清楚,比如说一
12 个编号一个权的这种,然后慢慢地多做题就能运用得非常熟练了。最好的方法就是多画画树自己转一转,对之后
13 复杂题目的调试也非常有益
14 
15 我说:
16 我在洛谷上得模板题上交了一下rnk里面不带splay(now)的,一共12个样例,就对了两个样例。错了一个样例,其他全TLE
17 
18 我解释:
19 为什么作者解释可以删去,但是删过之后还错了。因为它的代码中函数之前是相互联系的
20 就比如它调用rnk(x)函数之后就已经认为x为平衡树树根,然后直接对它进行下一步操作(这个假设在del函数里面)
21 
22 如果你光删了rnk(x)里面的splay(),你肯定还要改其他地方代码。。。。。。
23 */
24 int rnk(int x)  //查询x的排名
25 {
26     int now=rt,ans=0;
27     while(1)
28     {
29         if(x<key[now]) now=ch[now][0];
30         else
31         {
32             ans+=sizes[ch[now][0]];
33             if(x==key[now])
34             {
35                 splay(now); //这个splay是为了后面函数的调用提供前提条件
36 //就比如pre函数的前提条件就是x(x是我们要求谁的前驱,那个谁就是x)已经在平衡树树根
37                 return ans+1;
38             }
39             ans+=cnt[now];  //cnt代表now这个位置值(key[now])出现了几次
40             now=ch[now][1];
41         }
42     }
43 }

6、int kth(int x)

这个是查找树上面第x大的数是多少

 1 int kth(int x)
 2 {
 3     int now=rt;
 4     while(1)
 5     {
 6         if(ch[now][0] && x<=sizes[ch[now][0]])
 7         {
 8             //满足这个条件就说明它在左子树上
 9             now=ch[now][0];
10         }
11         else
12         {
13             int temp=sizes[ch[now][0]]+cnt[now];
14             if(x<=temp) //这个temp是now左子树权值和now节点权值之和
15                 return key[now]; //进到这个判断里面说明他不在左子树又不在右子树,那就是now节点了
16             x-=temp;
17             now=ch[now][1];
18         }
19     }
20 }

7、求根节点的前驱节点和后继结点在树上的位置

int pre()//由于进行splay后,x已经到了根节点的位置
{
    //求x的前驱其实就是求x的左子树的最右边的一个结点
//为什么呢,因为这是平衡树(带有二叉排序树特点),根据二叉排序树中序遍历结果我么可以知道,一个数的前序就在
//x的左子树的最右边的一个结点
    int now=ch[rt][0];
    while(ch[now][1]) now=ch[now][1];
    return now;
}
int next()
{
    //求后继是求x的右子树的最左边一个结点
//为什么呢,因为这是平衡树(带有二叉排序树特点),根据二叉排序树中序遍历结果我么可以知道,一个数的前序就在
//x的右子树的最左边一个结点
    int now=ch[rt][1];
    while(ch[now][0])  now=ch[now][0];
    return now;
}

8、删除一个值

 1 /*
 2 删除操作是最后一个稍微有点麻烦的操作。
 3 step 1:随便find一下x。目的是:将x旋转到根。
 4 step 2:那么现在x就是根了。如果cnt[root]>1,即不只有一个x的话,直接-1返回。
 5 step 3:如果root并没有孩子,就说名树上只有一个x而已,直接clear返回。
 6 step 4:如果root只有左儿子或者右儿子,那么直接clear root,然后把唯一的儿子当作根就可以了(f赋0,root赋为唯一的儿子)
 7 剩下的就是它有两个儿子的情况。
 8 step 5:我们找到新根,也就是x的前驱(x左子树最大的一个点),将它旋转到根。然后将原来x的右子树接到新根的
 9 右子树上(注意这个操作需要改变父子关系)。这实际上就把x删除了。不要忘了update新根。
10 */
11 void del(int x)
12 {
13     rnk(x);
14     if(cnt[rt]>1)//如果这个位置权值大于1,那就不用删除这个点
15     {
16         cnt[rt]--;
17         pushup(rt);
18         return;
19     }
20     if(!ch[rt][0] && !ch[rt][1]) //这个就代表平衡树只有一个节点
21     {
22         clears(rt);
23         rt=0;
24         return;
25     }
26     if(!ch[rt][0]) //只有左儿子,树根只有左儿子那就把树根直接删了就行
27     { //然后左儿子这棵子树变成新的平衡树
28         int frt=rt;
29         rt=ch[rt][1];
30         f[rt]=0;
31         clears(frt);
32         return;
33     }
34     else if(!ch[rt][1]) //只有右儿子,和上面差不多
35     {
36         int frt=rt;
37         rt=ch[rt][0];
38         f[rt]=0;
39         clears(frt);
40         return;
41     }
42     int frt=rt;
43     int leftbig=pre();
44     splay(leftbig); //让前驱做新根
45     ch[rt][1]=ch[frt][1];  //这个frt指向的还是之前的根节点
46     /*
47     看着一点的时候就会发现,数在数组里面的位置一直没有改变,平衡树旋转改变的是根节点儿子数组ch[x][]指向的值
48     */
49     f[ch[frt][1]]=rt;
50     clears(frt);
51     pushup(rt);
52 }

五、例题

P3369 【模板】普通平衡树

题目描述

您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:

  1. 插入 xxx 数
  2. 删除 xxx 数(若有多个相同的数,因只删除一个)
  3. 查询 xxx 数的排名(排名定义为比当前数小的数的个数 +1+1+1 )
  4. 查询排名为 xxx 的数
  5. xxx 的前驱(前驱定义为小于 xxx,且最大的数)
  6. xxx 的后继(后继定义为大于 xxx,且最小的数)

输入格式

第一行为 nnn,表示操作的个数,下面 nnn 行每行有两个数 opt ext{opt}opt 和 xxx,opt ext{opt}opt 表示操作的序号( 1≤opt≤6 1 leq ext{opt} leq 6 1opt6 )

输出格式

对于操作 3,4,5,63,4,5,63,4,5,6 每行输出一个数,表示对应答案

输入输出样例

输入 #1
10
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598
输出 #1
106465
84185
492737

说明/提示

【数据范围】
对于 100%100\%100% 的数据,1≤n≤1051le n le 10^51n105,∣x∣≤107|x| le 10^7x107

 
代码:
  1 /*
  2 注意:
  3 1、看平衡树之前你要注意,对于1 3 5 3 2这一组数据。sz的值是4,因为sz保存的是节点种类
  4    为什么要这样,因为sz涉及到要为几个点开空间
  5 
  6 2、sizes[x]保存的是以x为树根的子树上节点数量,比如x这颗子树所有节点为1,2,1.那么它的sizes[x]=3
  7    而且实际上不会有两个1放在树上。而是给1的权值数组cnt[1]加1
  8 
  9 3、对于1 3 5 3 2这一组数据。sz的值是4。那么1对应位置就是1,3对应位置就是2,5对应位置就是3,2对应位置就是4
 10    之后他们所对应的位置都不会改变
 11    在平衡树旋转的过程中只是每一个点的儿子节点ch[x][0]和ch[x][1]里面保存的值在改变
 12 */
 13 #include<stdio.h>
 14 #include<string.h>
 15 #include<algorithm>
 16 #include<iostream>
 17 using namespace std;
 18 const int maxn=1e5+10;
 19 int f[maxn],cnt[maxn],ch[maxn][2],sizes[maxn],key[maxn],sz,rt;
 20 /*
 21 f[i]:i节点的父节点,cnt[i]每个点出现的次数,ch[i][0/1]:0表示左孩子,
 22 1表示右孩子, size[i]表示以i为根节点的子树的节点个数
 23 key[i]表示点i代表的数的值;sz为整棵树的节点种类数,rt表示根节点
 24 */
 25 void clears(int x) //删除x点信息
 26 {
 27     f[x]=cnt[x]=ch[x][0]=ch[x][1]=sizes[x]=key[x]=0;
 28 }
 29 bool get(int x) //判断x是父节点的左孩子还是右孩子
 30 {
 31     return ch[f[x]][1]==x;  //返回1就是右孩子,返回0就是左孩子
 32 }
 33 void pushup(int x)  //重新计算一下x这棵子树的节点数量
 34 {
 35     if(x)
 36     {
 37         sizes[x]=cnt[x];
 38         if(ch[x][0]) sizes[x]+=sizes[ch[x][0]];
 39         if(ch[x][1]) sizes[x]+=sizes[ch[x][1]];
 40     }
 41 }
 42 void rotates(int x)  //将x移动到他父亲的位置,并且保证树依旧平衡
 43 {
 44     int fx=f[x],ffx=f[fx],which=get(x);
 45     //x点父亲,要接受x的儿子。而且x与x父亲身份交换
 46     ch[fx][which]=ch[x][which^1];
 47     f[ch[fx][which]]=fx;
 48 
 49     ch[x][which^1]=fx;
 50     f[fx]=x;
 51 
 52     f[x]=ffx;
 53     if(ffx) ch[ffx][ch[ffx][1]==fx]=x;
 54 
 55     pushup(fx);
 56     pushup(x);
 57 }
 58 void splay(int x)  //将x移动到数根节点的位置,并且保证树依旧平衡
 59 {
 60     for(int fx; fx=f[x]; rotates(x))
 61     {
 62         if(f[fx])
 63         {
 64             rotates((get(x)==get(fx))?fx:x);
 65             //如果祖父三代连城一条线,就要从祖父哪里rotate
 66             //至于为什么要这样做才能最快……可以去看看Dr.Tarjan的论文
 67         }
 68     }
 69     rt=x;
 70 }
 71 /*
 72 将x这个值插入到平衡树上面
 73 如果这个值在树上存在过,那就sz不再加1,更新一下权值即可
 74 如果这个值在树上不存在,那就sz加1,再更新一下权值
 75 
 76 sz是书上节点种类数
 77 sizes[x]是x这棵子树上有多少节点
 78 */
 79 void inserts(int x)
 80 {
 81     if(rt==0)
 82     {
 83         sz++;
 84         key[sz]=x;
 85         rt=sz;
 86         cnt[sz]=sizes[sz]=1;
 87         f[sz]=ch[sz][0]=ch[sz][1]=0;
 88         return;
 89     }
 90     int now=rt,fx=0;
 91     while(1)
 92     {
 93         if(x==key[now])
 94         {
 95             cnt[now]++;
 96             pushup(now);
 97             pushup(fx);
 98             splay(now);  //splay的过程会rotates now点的所有祖先节点,这个时候它们所有子树权值也更新了
 99             return;
100         }
101         fx=now;
102         now=ch[now][key[now]<x];
103         if(now==0)
104         {
105             sz++;
106             sizes[sz]=cnt[sz]=1;
107             ch[sz][0]=ch[sz][1]=0;
108             ch[fx][x>key[fx]]=sz;  //二叉查找树特性”左大右小“
109             f[sz]=fx;
110             key[sz]=x;
111             pushup(fx);
112             splay(sz);
113             return ;
114         }
115     }
116 }
117 /*
118 有人问:
119 qwq很想知道为什么find操作也要splay操作呢?如果del要用的话直接splay(x)是不是就可以了
120 
121 原博客答:
122 呃不不不这个貌似不是随便splay以下就可以的 首先find之后的splay就是将找到的这个点转到根,
123 当然你不加这个应该是也可以,只不过这道题加上的话对于这一堆操作来说比较方便,不过一般来说转一转splay的
124 平衡性会好一点(当然也不要转得太多了就tle了...) 但是del之前直接splay(x)要视情况而定,关键在于分清楚
125 “点的编号”和“点的权值”这两个概念。如果你已经知道了该转的点的编号,当然可以直接splay(x),但是如果你只
126 知道应该splay的点的权值,你需要在树里find到这个权值的点的编号,然后再splay 其实最后splay写起来都是非
127 常灵活的,而且有可能一个点带若干个权之类的。对于初学者的建议就是先把一些最简单的情况搞清楚,比如说一
128 个编号一个权的这种,然后慢慢地多做题就能运用得非常熟练了。最好的方法就是多画画树自己转一转,对之后
129 复杂题目的调试也非常有益
130 
131 我说:
132 我在洛谷上得模板题上交了一下rnk里面不带splay(now)的,一共12个样例,就对了两个样例。错了一个样例,其他全TLE
133 
134 我解释:
135 为什么作者解释可以删去,但是删过之后还错了。因为它的代码中函数之前是相互联系的
136 就比如它调用rnk(x)函数之后就已经认为x为平衡树树根,然后直接对它进行下一步操作(这个假设在del函数里面)
137 
138 如果你光删了rnk(x)里面的splay(),你肯定还要改其他地方代码。。。。。。
139 */
140 int rnk(int x)  //查询x的排名
141 {
142     int now=rt,ans=0;
143     while(1)
144     {
145         if(x<key[now]) now=ch[now][0];
146         else
147         {
148             ans+=sizes[ch[now][0]];
149             if(x==key[now])
150             {
151                 splay(now); //这个splay是为了后面函数的调用提供前提条件
152 //就比如pre函数的前提条件就是x(x是我们要求谁的前驱,那个谁就是x)已经在平衡树树根
153                 return ans+1;
154             }
155             ans+=cnt[now];  //cnt代表now这个位置值(key[now])出现了几次
156             now=ch[now][1];
157         }
158     }
159 }
160 int kth(int x)
161 {
162     int now=rt;
163     while(1)
164     {
165         if(ch[now][0] && x<=sizes[ch[now][0]])
166         {
167             //满足这个条件就说明它在左子树上
168             now=ch[now][0];
169         }
170         else
171         {
172             int temp=sizes[ch[now][0]]+cnt[now];
173             if(x<=temp) //这个temp是now左子树权值和now节点权值之和
174                 return key[now]; //进到这个判断里面说明他不在左子树又不在右子树,那就是now节点了
175             x-=temp;
176             now=ch[now][1];
177         }
178     }
179 }
180 int pre()//由于进行splay后,x已经到了根节点的位置
181 {
182     //求x的前驱其实就是求x的左子树的最右边的一个结点
183 //为什么呢,因为这是平衡树(带有二叉排序树特点),根据二叉排序树中序遍历结果我么可以知道,一个数的前序就在
184 //x的左子树的最右边的一个结点
185     int now=ch[rt][0];
186     while(ch[now][1]) now=ch[now][1];
187     return now;
188 }
189 int next()
190 {
191     //求后继是求x的右子树的最左边一个结点
192 //为什么呢,因为这是平衡树(带有二叉排序树特点),根据二叉排序树中序遍历结果我么可以知道,一个数的前序就在
193 //x的右子树的最左边一个结点
194     int now=ch[rt][1];
195     while(ch[now][0])  now=ch[now][0];
196     return now;
197 }
198 /*
199 删除操作是最后一个稍微有点麻烦的操作。
200 step 1:随便find一下x。目的是:将x旋转到根。
201 step 2:那么现在x就是根了。如果cnt[root]>1,即不只有一个x的话,直接-1返回。
202 step 3:如果root并没有孩子,就说名树上只有一个x而已,直接clear返回。
203 step 4:如果root只有左儿子或者右儿子,那么直接clear root,然后把唯一的儿子当作根就可以了(f赋0,root赋为唯一的儿子)
204 剩下的就是它有两个儿子的情况。
205 step 5:我们找到新根,也就是x的前驱(x左子树最大的一个点),将它旋转到根。然后将原来x的右子树接到新根的
206 右子树上(注意这个操作需要改变父子关系)。这实际上就把x删除了。不要忘了update新根。
207 */
208 void del(int x)
209 {
210     rnk(x);
211     if(cnt[rt]>1)//如果这个位置权值大于1,那就不用删除这个点
212     {
213         cnt[rt]--;
214         pushup(rt);
215         return;
216     }
217     if(!ch[rt][0] && !ch[rt][1]) //这个就代表平衡树只有一个节点
218     {
219         clears(rt);
220         rt=0;
221         return;
222     }
223     if(!ch[rt][0]) //只有左儿子,树根只有左儿子那就把树根直接删了就行
224     { //然后左儿子这棵子树变成新的平衡树
225         int frt=rt;
226         rt=ch[rt][1];
227         f[rt]=0;
228         clears(frt);
229         return;
230     }
231     else if(!ch[rt][1]) //只有右儿子,和上面差不多
232     {
233         int frt=rt;
234         rt=ch[rt][0];
235         f[rt]=0;
236         clears(frt);
237         return;
238     }
239     int frt=rt;
240     int leftbig=pre();
241     splay(leftbig); //让前驱做新根
242     ch[rt][1]=ch[frt][1];  //这个frt指向的还是之前的根节点
243     /*
244     看着一点的时候就会发现,数在数组里面的位置一直没有改变,平衡树旋转改变的是根节点儿子数组ch[x][]指向的值
245     */
246     f[ch[frt][1]]=rt;
247     clears(frt);
248     pushup(rt);
249 }
250 int main()
251 {
252     int n;
253     scanf("%d",&n);
254     for (int i=1; i<=n; i++)
255     {
256         int type,k;
257         scanf("%d%d",&type,&k);
258         if (type==1) inserts(k);
259         if (type==2) del(k);
260         if (type==3) printf("%d
",rnk(k));
261         if (type==4) printf("%d
",kth(k));
262         if (type==5)
263         {
264             inserts(k);
265             //插入操作中存在splay操作,这样的话插入之后平衡树树根就是k
266             printf("%d
",key[pre()]);
267             del(k);
268         }
269         if (type==6)
270         {
271             inserts(k);
272             printf("%d
",key[next()]);
273             del(k);
274         }
275     }
276     printf("%d %d %d %d
",sz,sizes[1],sizes[2],sizes[3]);
277     return 0;
278 }
原文地址:https://www.cnblogs.com/kongbursi-2292702937/p/12214388.html