二叉查找树
二叉查找树是一棵二叉树,其结点含有Comparable的对象,并以如下方式组织。对于树的每一个结点:
- 结点中的数据大于结点的左子树中的数据
- 结点中的数据小于结点的右子树中的数据
二叉查找树的性质:对二叉查找树进行中序遍历,即可得到有序的数列。
二叉查找树的时间复杂度:它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是 平衡查找树设计的初衷。
二叉查找树的高度决定了二叉查找树的查找效率。
二叉查找树的时间复杂度:它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是 平衡查找树设计的初衷。
二叉查找树的高度决定了二叉查找树的查找效率。
查找与检索
二叉查找树的查找类似于进行数组的折半查找:在二叉查找树的其中一棵子树中继续查找,而折半查找则是在数组的其中一半中进行查找。
公有方法getEntry调用了一个私有递归方法findEntry来实现查找。
public T getEntry(T entry){
return findEntry(grtRootNode(), entry);
}
private T findEntry(BinaryNode<T> rootNode,T entry){
T result=null;
if(rootNode!=null){
T rootEntry=rootNode.getData();
if(entry.compareTo(rootEntry)==0)
result=rootEntry;
else if(entry.compareTo(rootEntry)<0)
result=findEntry((BinaryNode<T>) rootNode.getLeftChild(), entry);
else
result=findEntry((BinaryNode<T>) rootNode.getRightChild(), entry);
}
return result;
}
插入元素
二叉查找树的每次插入都为这棵树加上一个新的叶子结点。
插入过程如下:
- 若当前的二叉查找树为空,则插入的元素为根节点
- 若插入的元素值小于根节点值,则将元素插入到左子树中
- 若插入的元素值不小于根节点值,则将元素插入到右子树中
public T add(T newEntry){
T result=null;
if(isEmpty()) //设置根结点
setRootNode(new BinaryNode<T>(newEntry));
else
result=addEntry(grtRootNode(), newEntry);
return result;
}
private T addEntry(BinaryNode<T> rootNode,T newEntry){
if(newEntry==null)
return null;
T result=null;
int comparison=newEntry.compareTo(rootNode.getData());
if(comparison==0){
result=rootNode.getData();
rootNode.setData(newEntry);
}else if(comparison<0){
if(rootNode.hasLeftChild())
result=addEntry((BinaryNode<T>) rootNode.getLeftChild(), newEntry);
else
rootNode.setLeftChild(new BinaryNode<T>(newEntry));
}else{
if(rootNode.hasRightChild())
result=addEntry((BinaryNode<T>) rootNode.getRightChild(), newEntry);
else
rootNode.setRightChild(new BinaryNode<T>(newEntry));
}
return result;
}
删除元素
删除元素比插入元素还要棘手,需要考虑3种情况:
- 该结点没有孩子,即叶子结点
- 该结点有一个孩子
- 该结点有两个孩子
(1)删除叶子结点
若删除的是叶子结点,则将双亲的相应孩子的引用置为null即可。
(2)删除有一个孩子的结点
若删除的是有一个孩子C的结点N,若N是P的左孩子,则让C成为P的左孩子;类似地,若N是P的右孩子,则让C成为P的右孩子。
(3)删除有两个孩子的结点
假若
e是结点N的元素,由于删除的结点N有两个孩子,则
e就不可能是树中最小元素,也不可能是最大元素。假若元素按升序排序,则必有
...
a <
e <
b ...
a称为
e的
中序前驱,表示结点N左子树中最大的元素;
b称为
e的
中序后驱,表示结点N右子树中最小的元素。
假定删除了含有a的结点,并用a代替e,那么N的左子树中剩下的元素都小于a,而右子树中的元素都大于e>a,因此仍是二叉树。
寻找适合的元素a,a必须满足一下要求:
显然,a位于左子树最右侧的结点。
|
基于上述分析,可得出两套解决方案:
- 方案一:删除结点N时,先找出N的左子树中最右侧结点R,以结点R中的元素替代结点N中的元素,最后删除结点R
- 方案二:删除结点N时,先找出N的右子树中最左侧结点L,以结点L中的元素替代结点N中的元素,最后删除结点L
下面是删除元素为12的结点的例子。
public T remove(T entry){
ReturnObject oldEntry=new ReturnObject<T>(null);
BinaryNode<T> newRoot=removeEntry(grtRootNode(), entry, oldEntry);
setRootNode(newRoot);
return (T) oldEntry.getEntry();
}
private BinaryNode<T> removeEntry(BinaryNode<T> rootNode,T entry,ReturnObject oldEntry){
if(rootNode!=null){
T rootData =rootNode.getData();
int comparison=entry.compareTo(rootData);
if(comparison==0){
oldEntry.setEntry(rootData);
rootNode=removeFromRoot(rootNode);
}else if(comparison<0){
BinaryNode<T> leftChild=(BinaryNode<T>) rootNode.getLeftChild();
rootNode.setLeftChild(removeEntry(leftChild, entry, oldEntry));
}else{
BinaryNode<T> rightChild=(BinaryNode<T>) rootNode.getRightChild();
rootNode.setRightChild(removeEntry(rightChild, entry, oldEntry));
}
}
return rootNode;
}
/**
* Task:在指定树中找含最大元素节点
* @param rootNode
* @return
*/
private BinaryNode<T> findLargest(BinaryNode<T> rootNode){
if(rootNode.hasRightChild())
rootNode=findLargest((BinaryNode<T>) rootNode.getRightChild());
return rootNode;
}
/**
* Task:在指定树中删除含最大元素节点
* @param rootNode
* @return
*/
private BinaryNode<T> removelargest(BinaryNode<T> rootNode){
if(rootNode.hasRightChild()){
BinaryNode<T> root=removelargest((BinaryNode<T>) rootNode.getRightChild());
rootNode.setRightChild(root);
}else
rootNode=(BinaryNode<T>) rootNode.getLeftChild();
return rootNode;
}
/**
* Task:删除指定树根节点元素
* @param rootNode
* @return
*/
private BinaryNode<T> removeFromRoot(BinaryNode<T> rootNode){
if(rootNode.hasLeftChild() && rootNode.hasRightChild()){
BinaryNode<T> leftSubtreeRoot=(BinaryNode<T>) rootNode.getLeftChild();
BinaryNode<T> largestNode=findLargest(leftSubtreeRoot);
rootNode.setData(largestNode.getData());
rootNode.setLeftChild(removelargest(leftSubtreeRoot));
}else if(rootNode.hasRightChild())
rootNode=(BinaryNode<T>) rootNode.getRightChild();
else
rootNode=(BinaryNode<T>) rootNode.getLeftChild();
return rootNode;
}
private BinaryNode<T> removeEntry(BinaryNode<T> rootNode,T entry,ReturnObject oldEntry){
if(rootNode!=null){
T rootData =rootNode.getData();
int comparison=entry.compareTo(rootData);
if(comparison==0){
oldEntry.setEntry(rootData);
rootNode=removeFromRoot(rootNode);
}else if(comparison<0){
BinaryNode<T> leftChild=(BinaryNode<T>) rootNode.getLeftChild();
rootNode.setLeftChild(removeEntry(leftChild, entry, oldEntry));
}else{
BinaryNode<T> rightChild=(BinaryNode<T>) rootNode.getRightChild();
rootNode.setRightChild(removeEntry(rightChild, entry, oldEntry));
}
}
return rootNode;
}
操作的效率
插入、删除、查找操作都需要从树根开始查找。插入元素时,若该元素不在树中,则查找终止于叶子,否则,查找可以终止得更快。同样删除操作亦如此。
因此,每个操作所需要的比较次数与树的高度成正比,即O(h)。
但是,在某些极端的情况下(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,此时,其操作的时间复杂度将退化成线性的,即O(n)。
若结点的数目相同,则满二叉查找树是最矮的树,它的性能也是最高效的。含有n个结点的满二叉树的高度是
log
2
(
n
+1),因此满二叉查找树的操作都是O(log n)。