二叉排序树
*以此谨记自己java学习心得
昨天总结了霍夫曼树,今天来总结一下二叉排序树,首先二叉排序树概念,二叉排序树任意一个非叶子结点,它的左结点值比它(当前的非叶子结点)小,它的右结点比它(当前的非叶子结点)大。
如图:
这个二叉排序树的优点就是当我们在查找某一个值时,先和中间的根结点进行比较,若小于根节点则再向根节点的左结点遍历寻找,反之则向根结点的右结点遍历寻找,类似于之前的二分查找,只需要找一半就行,并不需要全部找过去。还有如果现在对上图还要进行插入,比如说插入2,我们就可以先和根结点7进行比较,发现小于根结点,那么再与根结点的左结点3比较,发现小于3,则再与3的左结点,发现大于1,则将要插入的新结点2放在1的右结点上。
如图:
以下是代码:
这是结点类的代码
class Node {
public int number;
public Node left;
public Node right;
public Node(int number) {
this.number = number;
}
@Override
public String toString() {
return "Node{" +
"number=" + number +
'}';
}
public void add(Node node) {
if (this.right == null && node.number >= this.number) {
this.right = node;
} else if (this.left == null && node.number < this.number) {
this.left = node;
} else if (this.right != null && node.number > this.number) {
this.right.add(node);
} else if (this.left != null && node.number < this.number) {
this.left.add(node);
}
}
public void midselect(Node node) {
if (node.left != null) {
midselect(node.left);
}
System.out.println(node);
if (node.right != null) {
midselect(node.right);
}
}
以上结点类中编写了add方法于中序遍历方法。add方法思路就是让要插入的值与当前根结点比较,比根结点小就往左插入,比根结点大就往右插入。里面用了递归。
下面是二叉排序树的类:
class BinarySortTree {
public Node root;
public BinarySortTree() {
}
public BinarySortTree(Node root) {
this.root = root;
}
@Override
public String toString() {
return "BinarySortTree{" +
"root=" + root +
'}';
}
public void add(Node node) {
if (root == null) {
root = node;
} else {
root.add(node);
}
}
public void midselect(Node node) {
if (root == null) {
System.out.println("树中无信息");
} else
root.midselect(node);
}
再是主函数:
public static void main(String[] args) {
int array[] = new int[]{7, 3, 10, 12, 5, 1, 9};
BinarySortTree tree = new BinarySortTree();
for (int i = 0; i < array.length; i++) {
Node node = new Node(array[i]);
tree.add(node);
}
tree.midselect(tree.root);
注意,此时已将main函数中的int 数组特意写成{7, 3, 10, 12, 5, 1, 9},如果是其他顺序,根结点可能不是7。这里有一个坑,如果数组写成是[1,2,3,4,5,6,7],则1会作为根结点,而其他的都是前一位数字的右结点,相当于一个树只有右结点有值,相当于是一个单链表,这个坑,由平衡二叉树来填,后面再提及。
我们现在创建的二叉排序树,中序遍历结果如下
现在我们已经完成了添加和遍历操作。再来进行删除操作。这个删除操作步骤有点多,有3种情况,1、删除叶子结点。2、删除非叶子结点,但此非叶子结点有左结点与右结点。3、删除非叶子结点,但此非叶子结点只有一个左结点或只有一个右结点。
进行编写删除代码前,首先要完成查找功能,也就是要找到当前要删除的结点,又因为这是个树结构,在删除前还要找到要删除结点的父结点。不然若是直接令当前结点=null,这样是不会对树造成影响的,应该让父结点的left或者父结点的right=null,才能对树结构造成影响。
接下来是查找要删除结点 与查找要删除结点的父结点
public Node selectTarget(int value) {
if (this.number == value) {
return this;
} else if (this.number > value) {
if (this.left == null) {
return null;
}
return this.left.selectTarget(value);
} else {
if (this.right == null) {
return null;
}
return this.right.selectTarget(value);
}
}
public Node selectParentNode(int value) {
if ((this.left != null && this.left.number == value) ||
(this.right != null && this.right.number == value)) {
return this;
} else if (this.number > value) {
if (this.left != null) {
return this.left.selectParentNode(value);
}
if (this.left == null) {
return null;
}
} else if (this.number < value) {
if (this.right != null) {
return this.right.selectParentNode(value);
}
if (this.right == null) {
return null;
}
}
return null;
}
老样子还是通过二分查找法,来查找结点。
接下来讨论一下删除叶子结点,如果要删除叶子结点,则需要令父结点.left或者父结点.right=null
代码如下:
public void delete(int value) {
Node parentNode = selectParentNode(value);
Node target = selectTarget(value);
if (target != null) {
if (target.left == null && target.right == null && target == parentNode.right) {
parentNode.right = null;//并不能直接使target为Null,因为返回的target只是取出一个复制的对象而已,并不能真正对树结构进行操作。
System.out.println("删除成功");
} else if (target != null && target.left == null && target.right == null && target == parentNode.left) {
parentNode.left = null;//并不能直接使target为Null,因为返回的target只是取出一个复制的对象而已,并不能真正对树结构进行操作。
System.out.println("删除成功");
}
其次是非叶子结点,但它左右结点都不为空,我为了方便说明,将这个非叶子结点暂且称之为A结点,A结点有左结点B,A结点有右结点C。当我们要删除A时,需要让A的子树中的一个值来代替A,我这边用A的右子树中的最小值来代替,如图:
代码如下:(已将上面删叶子结点放一个方法中)
public void delete(int value) {
Node parentNode = selectParentNode(value);
Node target = selectTarget(value);
if (target != null) {
if (target.left == null && target.right == null && target == parentNode.right) {
parentNode.right = null;//并不能直接使target为Null,因为返回的target只是取出一个复制的对象而已,并不能真正对树结构进行操作。
System.out.println("删除成功");
} else if (target != null && target.left == null && target.right == null && target == parentNode.left) {
parentNode.left = null;//并不能直接使target为Null,因为返回的target只是取出一个复制的对象而已,并不能真正对树结构进行操作。
System.out.println("删除成功");
}
else if (target.right != null && target.left != null) {
int min = selectMin(target.right);
target.number = min;
}
其中selecMin方法就是寻找该结点右子树中最小的值,
代码如下:
public int selectMin(Node node) {
Node tar = node;
while (tar.left != null) {
tar = tar.left;
}
delete(tar.number);
return tar.number;
}
接下来就是删除非叶子结点,但只有左结点或者右结点有值,这个就是令要删除的父结点的左结点或者右结点等于要删除结点的左结点或者右结点,代码如下(将删除叶子结点,非叶子结点以合在同一个delete方法中):
public void delete(int value) {
Node parentNode = selectParentNode(value);
Node target = selectTarget(value);
if (target != null) {
if (target.left == null && target.right == null && target == parentNode.right) {
parentNode.right = null;//并不能直接使target为Null,因为返回的target只是取出一个复制的对象而已,并不能真正对树结构进行操作。
System.out.println("删除成功");
} else if (target != null && target.left == null && target.right == null && target == parentNode.left) {
parentNode.left = null;//并不能直接使target为Null,因为返回的target只是取出一个复制的对象而已,并不能真正对树结构进行操作。
System.out.println("删除成功");
}
//第二种情况删除非叶子结点并且有左右两个结点值
//思路:寻找右侧最小的值代替该值
else if (target.right != null && target.left != null) {
int min = selectMin(target.right);
target.number = min;
}
//第三种情况 删非叶子结点 但是此结点只有一个左或右子结点
else {
if (target.left != null && target == parentNode.left) {
parentNode.left = target.left;
} else if (target.left != null && target == parentNode.right) {
parentNode.right = target.left;
} else if (target.right != null && target == parentNode.left) {
parentNode.left = target.right;
} else if (target.right != null && target == parentNode.right) {
parentNode.right = target.right;
}
}
} else {
System.out.println("并不存在这个值");
}
}
到此为止,二叉排序树的添加、查找、删除已经完成。
平衡二叉树
现在来填坑,在上面已经说到,刚开始主函数中的数组已经规定了要按[7, 3, 10, 12, 5, 1, 9]来排序,如果按照[1,2,3,4,5,6]来进行二叉排序树的添加,会造成以下情况
现在引入新的概念平衡二叉树(AVL树),平衡二叉树就是以根结点为基,它的左子树高度和右子树高度之差绝对值要小于等于1。
那么怎么要能使任意的数组形成平衡二叉树呢,我们首先需要统计左子树高度与右子树高度,如果右子树高度大于左子树高度则进行左旋转,具体算法思路如下:
如果右子树高度小于左子树高度则进行右旋转,如图
代码如下,首先计算左子树与右子树高度
//返回左子树高度
public int leftHeight() {
if (left == null) {
return 0;
}
return left.height();
}
// 返回右子树的高度
public int rightHeight() {
if (right == null) {
return 0;
}
return right.height();
}
// 返回 以该结点为根结点的树的高度
public int height() {
return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}
接下来就是左旋转代码
/**左旋转
* @param root 传入树的根结点,
*/
public void leftSpin(Node root) {
Node newnode = new Node(root.number);
newnode.left = this.left;//这里的this 值代树的根结点
newnode.right = this.right.left;
this.number = this.right.number;
this.left = newnode;
this.right = this.right.right;
}
右旋转代码
/**
* 右旋转
* @param root 树的根结点
*/
public void rightSpin(Node root) {
Node newnode = new Node(root.number);
newnode.right = this.right;
newnode.left = this.left.right;
this.number = this.left.number;
this.left = this.left.left;
this.right = newnode;
}
在这里有一个注意点,如果当我们的左子树的高度大于右子树高度,但是当目光聚焦于左子树时,却发现左子树的子树的左子树高度小于左子树的子树的右子树高度,如下图
这时,我们要进行双旋转,也就是先时左侧的子树进行左旋转,再对整体进行右旋转,思路是,在进行旋转之前,先对根结点的左子结点进行左子树和右子树高度之差,如果之差大于1,则需要进行双旋转
代码如下
//双旋转
public void converToSpin() {
int rigthHeight = root.rightHeight();
int leftHeight = root.leftHeight();
if (rigthHeight - leftHeight > 1) {//进行左旋转
if (root.right != null && (root.right.leftHeight()>root.right.rightHeight()) ){
root.right.rightSpin(root.right);
root.leftSpin(root);
} else
root.leftSpin(root);
} else if (leftHeight - rigthHeight > 1) {//进行右旋转
if (root.left != null && (root.left.leftHeight()<root.left.rightHeight())) {
root.left.leftSpin(root.left);
root.rightSpin(root);
} else
root.rightSpin(root);
}
}
到此为止,平衡二叉树也已经创建完成。