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
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/14586356.html