Complete Binary Search Tree

Complete Binary Search Tree

A Binary Search Tree (BST) is recursively defined as a binary tree which has the following properties:

  • The left subtree of a node contains only nodes with keys less than the node's key.
  • The right subtree of a node contains only nodes with keys greater than or equal to the node's key.
  • Both the left and right subtrees must also be binary search trees.

A Complete Binary Tree (CBT) is a tree that is completely filled, with the possible exception of the bottom level, which is filled from left to right.

Now given a sequence of distinct non-negative integer keys, a unique BST can be constructed if it is required that the tree must also be a CBT. You are supposed to output the level order traversal sequence of this BST.

Now given a sequence of distinct non-negative integer keys, a unique BST can be constructed if it is required that the tree must also be a CBT. You are supposed to output the level order traversal sequence of this BST.

Input Specification:

Each input file contains one test case. For each case, the first line contains a positive integer N (≤). Then N distinct non-negative integer keys are given in the next line. All the numbers in a line are separated by a space and are no greater than 2000.

Output Specification:

For each test case, print in one line the level order traversal sequence of the corresponding complete binary search tree. All the numbers in a line must be separated by a space, and there must be no extra space at the end of the line.

Sample Input:

10
1 2 3 4 5 6 7 8 9 0

Sample Output:

6 3 8 1 5 7 9 0 2 4

 

解题思路

  这题有点难度,当时自己想了快一个小时也没有想出来正确的解法。因为题目中的树是完全二叉树,所有当时很自然就想到堆,但是我所知道的堆操作都是基于最大堆或最小堆,而题目中的树同时又是颗二叉搜索树,所以不可以直接套用堆的算法。所以当时尝试模拟插入节点来构造这颗CBT,结果根本找不到规律。最后看别人的博客还有姥姥讲解这题,发现解法和我的思路完全不一样,emmm...。

  首先第一个问题是,我们用什么样的数据结构来存储这颗树?用链表,还是数组?因为CBT是一颗完全二叉树,所以当然是用数组啦。当时完全没有现过这个问题,直接无脑链表,以后应该尽量避免这种固定思维。

  同时,因为用的是数组,所以最后要求的层次遍历打印输出CBT就变得很简单了。

  然后第二个问题是,我们怎么样去生成这颗CBT?我们先根据Sample Output来画出题目给的CBT:

  对这颗CBT进行中序遍历,就会发现序列的数字大小是按升序排列好的。当然,这也说明CBT的其中一个性质:比根节点小的值都在左子树,比根节点大的值都在右子树。

  所以,我们可以根据下面这个方法来找到CBT的根节点。

  首先,我们要把输入的数字按升序进行排序,然后算出这颗CBT左子树的节点个数LTNum。那么从数组最左边开始数LTNum个数,则下一个元素就是要找根节点。

  找到了根节点后,构造CBT的左子树,右子树,就完成了整颗CBT的构造了。

  再根据CBT的性质,CBT的左子树和右子树也是CBT,所以构造左子树和右子树的方法和上面是一样的,因此我们可以用递归来解决构造CBT这个问题。

  递归函数的定义如下:根据所要构造的CBT的节点个数,计算出CBT左子树的节点个数,从而找到根节点,把根节点存放到CBT数组对应的下标位置中。然后递归去构造同样是CBT的左子树,递归去构造同样是CBT的右子树。

 1 void solve(int *a, int *CBT, int aLeft, int aRight, int root) {     // a是排好序的数组,CBT数组用来存放我们构造的CBT,aLeft是a数组最左边的位置,aRight是a数组最右边的位置,root是BTC根节点的位置 
 2     int n = aRight - aLeft + 1;                                     // n是CBT的节点的个数 
 3     
 4     if (n == 0) return;                                             // 递归终止条件就是CBT的节点个数为零 
 5     else { 
 6         int leftNum = getLeftNum(n);                                // getLeftNum函数就是用来算出左子树节点的个数 
 7         CBT[root] = a[aLeft + leftNum];                             // 在a数组找到根节点的位置,并把它存放到CBT对应的位置中,也就是CBT根的位置 
 8     
 9         solve(a, CBT, aLeft, aLeft + leftNum - 1, 2 * root + 1);    // 递归解决左子树,a数组的最左边的下标仍然是aleft,最右边的下标变成了aLeft + leftNum - 1,也就是根节点的上一个元素,左子树的根就是2 * root + 1 
10         solve(a, CBT, aLeft + leftNum + 1, aRight, 2 * root + 2);   // 递归解决右子树,a数组的最左边的下标变成了aLeft + leftNum + 1,也就是根节点的下一个元素,最右边的下标仍然是aRight,右子树的根就是2 * root + 2 
11     }
12 }

  解释一下,我们找到了根节点的位置后,那么根节点左边的元素都在左子树,右边的元素都在右子树。就像上图中,6是根节点,所以左边也就是绿色的部分就位于左子树,右边也就是白色的部分就位于右子树,所以递归传入的a数组下标会因为这个而改变。

  那么左右子树的根节点呢?首先我们是从0开始编号的,再根据完全二叉树的性质,左子树的根节点下标就是根节点的下标的2*root+1,右子树的根节点就是根节点的下标的2*root+2。当时我还想了很久,怎么确定那个root传入什么,不知道为什么完全想不起二叉树这个性质。

  很自然的,第三个问题是,怎么样算出左子树的节点个数?

  这里给出两种方法,先给出姥姥讲的方法。

  首先我们可以把CBT的节点分为两个部分,第一个部分是CBT中最大的满二叉树,第二部分就是最后一层剩下的节点。

  假设CBT的节点个数为n,则有公式 2H - 1 + X = n ,所以有 H = ⌊log2(n+1-X) 。

  在这里X的取值范围是 X ∈ [0, 2H-1] 。当X为0,就意味着这颗CBT是一颗满二叉树。当X小于2H-1,就表明最后一层的节点并不满,也就是有上图红色的那部分的节点存在。而X不可能是2H,是因为这意味着最后一层的节点满了,按理来说这颗CBT应该是满二叉树,X应该为0才对。

  所以,上面的H可以写成 H = ⌊log2(n+1)⌋ ,H是最大的满二叉树的高度,不是CBT的高度。

  因此知道,满二叉树的节点个数是2H-1,可是我们是要左子树的高度啊,也就是蓝色这个部分加上红色这部分。

  对于蓝色的部分,很简单,可以用满二叉树的节点个数,减去1,也就是根节点,再除以2。 leftNum = (2H - 1 - 1) / 2 = 2H-1 - 1 。又或是按照归纳的方法,既然H高度的满二叉树的节点个数是2H - 1,又因为满二叉树的左子树的也是满二叉树,只不过高度为H -1,所以蓝色的部分的节点个数就是2H-1-1。

  红色部分更简单,直接用总节点个数减去满二叉树节点个数,也就是 X = n - (2H - 1) = n - 2H +1 ,对于上图的CBT,这样确实没有错。可是真的是这样子吗?

  如果是这颗树呢?

  很明显,我们X应该是红色那部分的节点个数。如果按照上面的计算结果,那么会把右边那两个也计进来。

  所以,真正的剩余节点个数应该是 min(X, 2H-1) 。这里的2H-1是最后一层最大节点数的一半, 2H+1-1 / 2 = 2H-1 ,也就是说,如果X比最后一层一半的节点还多,那么多出来的那部分节点不要了,就直接取2H-1

  最后,左子树的节点个数就是蓝色的部分加上真正剩余节点个数。

  所以我们计算左子树的函数是:

1 int getLeftNum(int n) {
2     int FBTHeight = (int)log2(n + 1);                       // 最大满二叉树的高度 
3     int partOfleft = (int)pow(2, FBTHeight - 1) - 1;        // 满二叉树左子树的节点个数 
4     
5     int x = n - (int)pow(2, FBTHeight) + 1;                 // 最后一层的节点个数,也就是剩余的节点 
6     int leftOver = std::min(x, (int)pow(2, FBTHeight - 1)); // 其中属于左子树的节点个数是X和最后一层最大节点数的一半中,最小的那个 
7     
8     return partOfleft + leftOver;                           // 返回左子树的节点数 
9 }

   下面给出这个题目的AC代码。

 1 #include <cstdio>
 2 #include <cmath>
 3 #include <algorithm>
 4 
 5 int getLeftNum(int n);
 6 void solve(int *a, int *CBT, int aLeft, int aRight, int root);
 7 
 8 int main() {
 9     int n;
10     scanf("%d", &n);
11     
12     int a[n], CBT[n];
13     for (int i = 0; i < n; i++) {
14         scanf("%d", &a[i]);
15     }
16     
17     std::sort(a, a + n);
18     solve(a, CBT, 0, n - 1, 0);
19     
20     for (int i = 0; i < n; i++) {
21         if(i) putchar(' ');
22         printf("%d", CBT[i]);
23     }
24     
25     return 0;
26 }
27 
28 void solve(int *a, int *CBT, int aLeft, int aRight, int root) {
29     int n = aRight - aLeft + 1;
30     
31     if (n == 0) return;
32     else {
33         int leftNum = getLeftNum(n);
34         CBT[root] = a[aLeft + leftNum];
35     
36         solve(a, CBT, aLeft, aLeft + leftNum - 1, 2 * root + 1);
37         solve(a, CBT, aLeft + leftNum + 1, aRight, 2 * root + 2);
38     }
39 }
40 
41 int getLeftNum(int n) {
42     int FBTHeight = (int)log2(n + 1);
43     int partOfleft = (int)pow(2, FBTHeight - 1) - 1;
44     
45     int x = n - (int)pow(2, FBTHeight) + 1;
46     int leftOver = std::min(x, (int)pow(2, FBTHeight - 1));
47     
48     return partOfleft + leftOver;
49 }

  下面的给出另外一种计算左子树节点个数的方法。这个方法是我自己想出来的,比较直接,好理解。

  既然知道了节点的个数,那么这颗CBT的高度就是 h = ⌊log2n⌋ + 1 ,那么h-1层之前的(包括第h-1层)节点数就为2h-1-1,这也对于最大满二叉树的节点个数。又因为我们要的是左子树的个数(也就是上图蓝色的那部分),所以要先减去根节点,再除以2。也就是 (2h-1 - 1 - 1) / 2 = 2h-2 - 1 个。

  接下来X也是同样的计算方法, X = n - (2h-1 - 1) = n - 2h-1 +1 。

  同样的,真正的剩余节点个数应该是 min(X, 2h-2) 。

  代码改变的部分如下:

 1 void solve(int *a, int *CBT, int aLeft, int aRight, int root) {
 2     int n = aRight - aLeft + 1;
 3     
 4     if (n == 0) return;
 5     else if (n == 1) CBT[root] = a[aLeft];
 6     else {
 7         int leftNum = getLeftNum(n);
 8         CBT[root] = a[aLeft + leftNum];
 9     
10         solve(a, CBT, aLeft, aLeft + leftNum - 1, 2 * root + 1);
11         solve(a, CBT, aLeft + leftNum + 1, aRight, 2 * root + 2);
12     }
13 }
14 
15 int getLeftNum(int n) {
16     int height = (int)log2(n) + 1;
17     int partOfleft = (int)pow(2, height - 2) - 1;
18     int leftOver = std::min(n - (int)pow(2, height - 1) + 1, (int)pow(2, height - 2));
19     
20     return partOfleft + leftOver;
21 }

  需要注意的是,这种方法不可以计算节点个数为1的情况,所以我们在递归函数中增加n == 1的情况。

  这种方法的AC代码如下。

 1 #include <cstdio>
 2 #include <cmath>
 3 #include <algorithm>
 4 
 5 int getLeftNum(int n);
 6 void solve(int *a, int *CBT, int aLeft, int aRight, int root);
 7 
 8 int main() {
 9     int n;
10     scanf("%d", &n);
11     
12     int a[n], CBT[n];
13     for (int i = 0; i < n; i++) {
14         scanf("%d", &a[i]);
15     }
16     
17     std::sort(a, a + n);
18     solve(a, CBT, 0, n - 1, 0);
19     
20     for (int i = 0; i < n; i++) {
21         if(i) putchar(' ');
22         printf("%d", CBT[i]);
23     }
24     system("pause");
25     return 0;
26 }
27 
28 void solve(int *a, int *CBT, int aLeft, int aRight, int root) {
29     int n = aRight - aLeft + 1;
30     
31     if (n == 0) return;
32     else if (n == 1) CBT[root] = a[aLeft];
33     else {
34         int leftNum = getLeftNum(n);
35         CBT[root] = a[aLeft + leftNum];
36     
37         solve(a, CBT, aLeft, aLeft + leftNum - 1, 2 * root + 1);
38         solve(a, CBT, aLeft + leftNum + 1, aRight, 2 * root + 2);
39     }
40 }
41 
42 int getLeftNum(int n) {
43     int height = (int)log2(n) + 1;
44     int partOfleft = (int)pow(2, height - 2) - 1;
45     int leftOver = std::min(n - (int)pow(2, height - 1) + 1, (int)pow(2, height - 2));
46     
47     return partOfleft + leftOver;
48 }

 

参考资料

  浙江大学——数据结构:https://www.icourse163.org/course/ZJU-93001?tid=1461682474

posted @ 2021-03-28 09:45  onlyblues  阅读(191)  评论(0编辑  收藏  举报
Web Analytics