旁白:对于二叉树相信都已经非常熟悉了,这里不介绍基本概念。
》为什么使用二叉树?
旁白:这个话题很犀利,我以前没注意到。书中做了如下解释:
我们前面聊的都是排序算法,见过数组和链表。有序数组通过二分查找可以将效率提升到O(logN),但是插入的时候需要移动很多元素,效率过低。而链表则正好相反,插入的时候效率很高,改一下节点指向就好,但是查找的时候只能顺藤摸瓜,效率很低。使用二叉树,可以结合两者的优势,插入时依照链式存储的特点,改下指向就行,查找时可以使用二分查找的特性。
》二叉搜索树
一个二叉树所有的左子节点的关键字值小于这个节点,右子节点的关键字值大于或等于这个节点。这种树就称为二叉搜索树。
》二叉搜索树的随机性
旁白:很明显,根据二叉搜索树的定义,我们可以将数据有序的保存下来,然后通过中根遍历得到有序的数列。然而这种树根据插入时的顺序不一样,得到的树并不唯一。
比如:2,1,3,我们依次插入会得到以下树状结构
2
/ \
1 3
如果是:1,2,3,我们依次插入会得到以下树状结构
1
\
2
\
3
然而根据中根遍历最后的结果都是:1,2,3。第二种树称为非平衡树。
平衡树:即平衡二叉树,又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡树,左子节点与右子节点对称。
当序列基本有序,更容易出现非平衡二叉树
》二叉搜索树的java实现
旁白:当前代码是我手打出来的,未经验证,如果发现有问题可以联系我,请勿直接使用。
1)首先要存储每个节点,实际中的节点可能是很复杂的,可以通过定义Data类
class Data{
int sortKey; //比如依据这个关键字的值来比较大小
... //其他数据
}
当然也可以通过实现Comparable接口来实现类对象的比较,这个java基础不讲了。
同时,每个节点有左子节点和右子节点的引用,当没有时指向null,所以Node的结构如下
class Node{
Data mData; //数据部分
Node leftChild; //左子节点
Node rightChild; //右子节点
Node parent; //当然也可以保存父节点,视具体需要
//提供get方法方便调用
getData
getLeftChild
getRightChild
getParent
}
2)其次,我们要实现二叉树的算法:
对于一颗树,我们要维护一个根节点,通过根节点顺藤摸瓜就能得到整个树。
class Tree{
Node root; //指向树的根节点
//其他方法需要有
//1.根据Node数组或者其他集合构建树
createTree(Node[])
//2.增
addNode(Node)
//3.删
delNode(int key)
//4.改
updateNode(Node)
//5.查
findNode(int key)
//6.打印整颗树
printTree()
//其他方法根据具体需要
}
2.1) 构建树
void createTree(Node[] nodeArr){
//对数组遍历
for(Node n:NodeArr){
if(root==null){ //第一个节点作为根节点
root = n;
}else{ //后面的直接调用添加节点
addNode(n);
}
}
}
2.2)添加节点
//依次比较,二叉搜索过程:从根开始,先比较当前节点,如果比当前小,则找到当前节点的左子节点,否则找到当前节点的右子节点。然后循环,直到找不到下一个节点,如果比当前小,则将其插入左子节点,否则插入右子节点。
void addNode(Node node){
//保存当前节点
Node current = root;
int key = node.getData().getKey();
while(true){
//缓存父节点
Node parent = current;
int currentKey = current.getData().getKey();
if(key<currentKey){
current = current.getLeftChild();
if(current==null){ //结束战斗
parent.setLeftChild(node);
return;
}
}else{
current = current.getRightChild();
if(current==null){ //结束战斗
parent.setRightChild(node);
return;
}
}
}
}
旁白:从这个插入过程可以看出我这个排序过程是稳定的,比如3(1),3(2),3(3),由于大于等于的时候都会去找右子节点,所以排序后中根遍历结果仍是3(1),3(2),3(3)。
2.3)删除节点
旁白:删除节点过程比较复杂,需要多点讨论
2.3.1)删除节点无子节点,这个好办直接将其父节点的对应引用置空就行
2.3.2)删除节点仅有一个子节点,这个也好办,子承父业,让它顶了原来它爹的位置
2.3.3) 删除节点有两个子节点,涉及到两个问题,谁来顶上,顶上之后,另外一个子节点怎么接上。
考虑下面这颗树:
a
b c
d e f g
加入删除a,谁来顶上?b?or c?其实都可以,想想我们说的树的结构是随机的,谁顶上都可以通过后期调整成二叉搜索树,但是最佳答案是e或者f。为啥呢?考虑b顶上的话,节点e的位置将重新调整这个过程有点复杂。但是如果我把a的左子树中的最大一个,也就是e顶上,那么,a的左子树和右子树完全可以按照原来的结构放在e下面,不用调整。结构变为
e
b c
d f g
同理,f也是可以的。结构变为:
f
b c
d e g
总结:有两个子树的情况下,选取左子树中的最大节点,或者右子树的最小节点顶替,如此结果最佳,选择一种就行。用代码来实现就是遍历左子树从根开始找到右子节点的右子节点的…直到最后一个。下面来实现一下:
void delNode(int key){
//首先找到关键字的值为key的节点
Node current = root;
while(true){
//缓存父节点
Node parent = current;
int currentKey = current.getData().getKey();
int isLeftChild = 0; //0为根节点,1为左,2为右子节点
if(key==currentKey){ //找到了
//该节点即将被删除,缓存两个子节点
Node leftChild = current.getLeftChild();
Node rightChild = current.getRightChild();
//1.两子节点都为空 2.两子节点有一个为空
if(leftChild==null || rightChild==null){
Node child = (leftChild==null)?rightChild:leftChild;
switch(isLeftChild){
case 0: //当前是根节点
root = child;
break;
case 1: //当前是父节点的左子节点
parent.setLeftChild(child);
break;
case 2: //当前是父节点的右子节点
parent.setRightChild(child);
break;
}
}else{//3.两个子节点都不为空
Node max = leftChild;
Node maxParent = current; //缓存其父节点
boolean isRightChild = false;
//循环查找左子树中的最大节点
while(max.getRightChild()!=null){
maxParent = max;
max = max.getRightChild();
isRightChild = true;
}
//断开原来的联系
if(isRightChild){
maxParent.setRightChild(null);
}else{
maxParent.setLeftChild(null);
}
//将节点放入删除节点位置
max.setLeftChild(leftChild);
max.setRightChild(rightChild);
switch(isLeftChild){
case 0: //当前是根节点
root = max;
break;
case 1: //当前是父节点的左子节点
parent.setLeftChild(max);
break;
case 2: //当前是父节点的右子节点
parent.setRightChild(max);
break;
}
}
}else if(key<currentKey){ //未找到,继续左子树查找
current = current.getLeftChild();
isLeftChild=1;
if(current==null){ //不存在,结束战斗
return;
}
}else{ //未找到,继续左子树查找
current = current.getRightChild();
isLeftChild=2;
if(current==null){ //不存在,结束战斗
return;
}
}
}
2.4)2.5)更新和查找的代码其实最终依赖查找。而查找我们在上述过程中其实已经实现了,这里不写了。
2.6) 打印树,这里实现一个中根遍历,简单点用递归,打印左子树,打印中根,打印右子树,完成。
void printTree(Node node){
//打印左子树
printTree(node.getLeftChild());
//打印中根
if(node!=null){
syso(" "+node.getData().toString());
}
//打印右子树
printTree(node.getRightChild());
}
至此,二叉搜索树的java实现基本完成。
》二叉搜索树的效率:很明显对于查找的效率,因为树的结构是随机的,导致查找的效率也是随机的。如果是满二叉树可能还好点o(logN),像上面提到的一边倒的树,其实就是链表,效率很低o(N)。但是对于插入来说,效率还是很高的,没有什么移动成本。
》当然树也可以用其他结构存储,比如数组。所以表面上是一个数组,实际上是一个树并不奇怪,还记得我们前面第三节讲优先级队列的时候里面有一段神秘代码吗?其实就是一个树。