文章目录
对于一个给定序列的二叉查找树,有很多种,但是完全二叉查找树只有一种,因为树形是确定的,我们按照中根序列遍历的顺序也是按照数值大小顺序的,因此 按照树结点的中根遍历顺序 将 序列按照数值大小顺序 填入这颗树形确定的完全二叉树中,可以得到一颗唯一确定的完全二叉树。并且对于根结点,其左子树的所有结点都小于根结点,右子树的所有结点都大于等于根结点。
构建一颗 完全二叉查找树 (Complete Binary Search Tree,CBST)的算法关键在于如何保证二叉树的完全性质,同时维护二叉查找树(BST)的特性:对于任意节点,其左子树的所有节点的值小于该节点的值,其右子树的所有节点的值大于等于该节点的值。
以下是构建完全二叉查找树的一种有效算法:
1、步骤 1: 对给定数组排序
由于二叉查找树的中序遍历结果是一个有序数组,**首先要对输入的无序数组进行排序。**根结点的位置是可以唯一确定的。
2、步骤 2: 递归构建完全二叉查找树
- 选择中间元素作为树的根:从排序好的数组中选择中间的元素作为二叉查找树的根节点。如果有两个中间元素,选择它们中的任意一个。
- 递归构建左子树和右子树:将根节点左侧的元素递归地构建成左子树,将根节点右侧的元素递归地构建成右子树。
3、注意
- 完全二叉树的定义是除了最后一层外,每一层都是完全填满的,而最后一层从左向右填充。上述算法构建的是一棵高度平衡的二叉查找树,但不一定满足完全二叉树的严格定义。构建满足完全二叉树定义的二叉查找树需要更复杂的逻辑来确保所有层(除了可能的最后一层)都完全填满。
- 确保完全性的一个方法是通过计算给定元素数量所能构建的完全二叉树的最大高度,然后在构建过程中控制树的形态,使其满足完全二叉树的性质。
4、在有序数组中寻找根结点位置
完全二叉树的性质:
- 性质的数学公式换算:
- 树的结点个数n 取对数 可以得到树高:
Treeheight=log2(n)
- 通过树高 可以得到 除最后一层之外的右子树结点个数:
rightTree1=pow(2,Treeheight-1)-1
- 通过树高 以及 树的结点个数 可以得到树的最后一层结点个数:
lastLevelNodes=n-(pow(2,Treeheight)-1)
- 最后可以得到 右子树结点个数:
rightTree=rightTree1+((lastLevelNodes-pow(2,Treeheight-1))>0?(lastLevelNodes-pow(2,Treeheight-1)) :0)
- 树的结点个数n 取对数 可以得到树高:
- 直接求满了的层的总结点个数:
- 利用快速幂思想,即取出总结点数n的二进制位数的最高位。
- 然后计算即可得到右子树的个数
5、代码实现
- 数学性质
cpp
#include<bits/stdc++.h>
using namespace std;
struct TreeNode {
int val;
TreeNode *left, *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 构建完全二叉查找树
TreeNode* createTree(int start, int end, vector<int>& sortedValues) {
if (start > end) return nullptr;
// 计算完全二叉树的最后一层前的所有节点数和最后一层的节点数
int totalNodes = end - start + 1;
int fullLevels = log2(totalNodes + 1);
int lastLevelNodes = totalNodes - (pow(2, fullLevels) - 1);
int leftSubtreeNodes = pow(2, fullLevels - 1) - 1 + min((int)pow(2, fullLevels - 1), lastLevelNodes);
// 根据左子树的节点数确定根节点
int rootIndex = start + leftSubtreeNodes;
TreeNode* root = new TreeNode(sortedValues[rootIndex]);
root->left = createTree(start, rootIndex - 1, sortedValues);
root->right = createTree(rootIndex + 1, end, sortedValues);
return root;
}
// 层次遍历
void traversal(TreeNode* root) {
if (!root) return;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
TreeNode* current = q.front(); q.pop();
cout << current->val << " ";
if (current->left) q.push(current->left);
if (current->right) q.push(current->right);
}
}
int main() {
int n;
cin >> n;
vector<int> values(n);
for (int& value : values) {
cin >> value;
}
sort(values.begin(), values.end());
TreeNode* root = createTree(0, n - 1, values);
traversal(root);
return 0;
}
- 直接计算
cpp
#include<bits/stdc++.h>
using namespace std;
struct TreeNode{
TreeNode * left=nullptr;
TreeNode * right=nullptr;
int val;
TreeNode (int v=0):val(v) {}
};
TreeNode * createTree(int left,int right,vector<int> & baby){//O(nlogn)
if(left>right) return nullptr;
if(left==right) return new TreeNode(baby[left]);
TreeNode * root=nullptr;
int flag=right-left+2;
int b=1;
while(flag!=1){//O(logn) 每次建树logn,n个结点共nlogn
b*=2;
flag>>=1;
}
int rightTree_size=b/2-1;
if(rightTree_size+2-b-b/2>0){
rightTree_size+=rightTree_size+2-b-b/2;
}
root=new TreeNode(baby[right-rightTree_size]);
root->left=createTree(left,right-rightTree_size-1,baby);
root->right=createTree(right-rightTree_size+1,right,baby);
return root;
}
int main(void){
ios_base::sync_with_stdio(false);
cin.tie(0);
int n;
cin>>n;
vector<int> baby;
for(int i=0;i<n;++i){
int j;cin>>j;
baby.emplace_back(j);
}
sort(baby.begin(),baby.end());//O(nlogn)
createTree(0,n-1,baby);
return 0;
}
6、其他方法?
动态构建完全二叉查找树(CBST)比静态构建更为复杂,因为需要在插入和删除操作发生时持续保持树的完全二叉树性质。一种方法是在每次插入或删除后重建树,但这显然效率不高。另一种思路是通过维护额外的信息和采用特定的插入/删除策略来尽可能保持树的完全性。下面是一个较为高效的动态构建完全二叉查找树的策略:
基本思路
动态维护一个完全二叉查找树,主要挑战在于插入和删除操作。对于插入操作,需要找到树中最后一个节点并在适当的位置插入新节点以保持完全二叉树的性质。对于删除操作,则需要找到可以替换被删除节点的节点(通常是树中的最后一个节点)并调整树以保持其完全性质。
插入操作
- 计算完全二叉树的深度:基于树当前的节点数量,可以计算出树的深度。
- 定位插入点:根据完全二叉树的性质,新节点应当被插入为最后一个节点。可以通过深度和节点数定位到这个位置。
- 执行插入:在定位到的位置插入新节点。如果需要,调整树的结构以保持二叉查找树的性质。
删除操作
- 定位并删除目标节点:找到需要删除的节点,将其删除。如果这个节点不是最后一个节点,则需要找到最后一个节点。
- 用最后一个节点替换删除的节点(如果被删除的节点不是最后一个节点):将最后一个节点移动到被删除节点的位置。
- 调整树:可能需要对树进行调整,以保持二叉查找树的性质。
特别考虑
- 这种方法要求你能快速定位到树的最后一个节点,这可能需要维护额外的信息,如每层的节点数。
- 插入和删除操作可能导致需要重新平衡树,以保持二叉查找树的性质。
- 动态维护完全二叉树的复杂度主要在于定位最后一个节点和维护二叉查找树的性质。
动态地维护一个完全二叉查找树在实践中较为罕见,因为它要求在每次操作后都严格保持完全二叉树的性质,这可能导致较高的复杂性和成本。通常,人们会使用其他类型的自平衡二叉搜索树(如AVL树、红黑树)来获得较好的平均性能,尽管这些树不保证完全性质。