树是一种很有意思的数据结构,它既能像链表那样快速的插入和删除,又能像有序数组那样快速的查找,一般我们探究的树是一种特殊的树 -- 二叉树。如果树中每个节点最多只能有两个子节点,这样的树就成为“二叉树”,否则,就是多路树了。二叉搜索树是一种更特殊的树 -- 父节点的左子节点的关键字值小于父节点,右子节点的关键字的值大于或等于这个父节点。
一般树的节点包括三个部分:data、leftNode、rightNode。节点的数值,左子节点和右子节点。
public class Node {
int data;
Node leftChild;
Node rightChild;
}
1、 二叉搜索树的查找:
public Node find(int key) {
Node current = root;
while (current.data != key) {
if (current.data > key) {
current = current.leftChild;
}
if (current.data < key) {
current = current.rightChild;
}
if (current == null) {
return null;
}
}
return current;
}
2、二叉搜索树的插入新节点:
思路:1、如果没有节点,则待插入的节点直接插入到根节点;
2、寻找合适的位置以便插入节点:current、parent分别表示当前搜索到的节点及其父节点,如果待搜索节点的值大于current节点的值,则current = current.rightChild;,否则,current = current.leftChild;结束的条件是:current==null为true,则可以将待插入节点插入到parent的后面了。
/**
* @param key
* 待插入的节点的值 1、如果没有节点,则待插入的节点直接插入到根节点;
* 2、通过两个引用:current、parent分别表示当前搜索到的节点及其父节点
* 找到的条件是:current==null为true
*/
public void insert(int key) {
Node insertNode = new Node();
insertNode.data = key;
Node current, parent;
if (root == null) {
root = insertNode;
} else {
current = root;
while (true) {
parent = current;
if (current.data < key) {
current = current.rightChild;
} else {
current = current.leftChild;
}
if (current == null) {
if (parent.data < key) {
parent.rightChild = insertNode;
} else {
parent.leftChild = insertNode;
}
break;
}
}
}
}
3、二叉搜索树的遍历(前序、中序、后序遍历)
思路:
遍历适用于所有的二叉树,而不只是二叉搜索树。这个遍历的原理不关心节点的关键字值; 它只是看这个节点是否有子节点; 遍历树最简单的方式是用递归方法。用递归的方法遍历整棵树要用一个节点作为参数。初始化 时这个节点是根。这个方法只需要做三件事,以中序遍历为例:
1、调用自身来遍历节点的左子树; 2、访问这个节点; 3、调用自身来遍历节点的右子树;
/**
* 遍历适用于所有的二叉树,而不只是二叉搜索树。这个遍历的原理不关心节点的关键字值; 它只是看这个节点是否有子节点;
* 遍历树最简单的方式是用递归方法。用递归的方法遍历整棵树要用一个节点作为参数。初始化 时这个节点是根。这个方法只需要做三件事:
* 1、调用自身来遍历节点的左子树; 2、访问这个节点; 3、调用自身来遍历节点的右子树;
*/
// -------------------------------------------------------
// 前序遍历
public void preTravel(Node localRoot) {
if (localRoot != null) {
System.out.println(localRoot.data + "~~~~");
preTravel(localRoot.leftChild);
preTravel(localRoot.rightChild);
}
}
// 中序遍历<最常用> -- 中序遍历二叉搜索树会使所有的节点按照关键字升序被访问到,比如 root是10,左子节点是2,右子节点是
// 12.则中序遍历的结果是2,10,12.可以看到是按照升序被访问的
public void inTravel(Node localRoot) {
if (localRoot != null) {
inTravel(localRoot.leftChild);
System.out.println(localRoot.data + "^^^^");
inTravel(localRoot.rightChild);
}
}
// 后序遍历:要中序遍历一棵树的原因很清楚,但要通过前序或后序来遍历树的目的就不是那么清楚了。不过,如果要编写程序来解析
// 或分析袋鼠表达式,这两种遍历方法就很有用了。
public void postTravel(Node localRoot) {
if (localRoot != null) {
postTravel(localRoot.leftChild);
postTravel(localRoot.rightChild);
System.out.println(localRoot.data + "$$$$");
}
}
4、查找二叉搜索树的最大与最小值
思路:基于二叉搜索树的特征,最小值位于树的最左边的子节点是最小值节点,而最右边是最大值的节点。
// 二叉搜索树的特点,决定了最左边的子节点是最小值的节点,而最右边的子节点是最大值的节点
public Node findMin() {
Node current = root, parent = null;
if (current == null) {
return null;
}
while (current != null) {
parent = current;
current = current.leftChild;
}
return parent;
}
public Node findMax() {
Node current = root, parent = null;
if (current == null) {
return null;
}
while (current != null) {
parent = current;
current = current.rightChild;
}
return parent;
}
5、删除
删除操作是二叉搜索树中最复杂的操作,分三种情况:
1、该节点是叶节点(没有子节点);
2、该节点有一个子节点 ;
3、该节点有两个子节点;
第一种与第二种都比较简单,最复杂的是第三种情况。
首先在二叉搜索树中查找待删除的节点,如果有,则执行删除;否则,结束删除操作。
5.1、查找待删除节点
Node parent = root;
Node current = root;// 待删除的节点
boolean isLeftNode = false;// 待删除的子节点是左子节点还是右子节点,true:左子节点,false:右子节点
if (root == null || root.rightChild == null) {
return;
}
// 查找待删除的Node
while (current.data != key) {
parent = current;
if (current.data < key) {
isLeftNode = false;
current = current.rightChild;
} else {
isLeftNode = true;
current = current.leftChild;
}
if (current == null) {// 说明没有该节点,故结束删除操作
return;
}
}
5.2、待删除的节点是叶子节点
这种情况很简单,根据isLeftChild判断待删除的是左子节点还是右子节点,相应的将parent的子节点置为null。
// 第一种情况:待删除的节点是叶子节点
if (current.leftChild == null && current.rightChild == null) {
if (current == root) {
root = null;
} else if (isLeftNode) {
parent.leftChild = null;
} else {
parent.rightChild = null;
}
}
5.2、待删除的节点有一个子节点
这种情况也比较简单:直接将待删除的节点的子节点插入到parent节点下面即可。
// 第二种情况:待删除的节点有一个子节点
if (current.leftChild == null) {// 只有右子节点
if (current == root) {
root = current.rightChild;
} else {
if (isLeftNode) {
parent.leftChild = current.rightChild;
} else {
parent.rightChild = current.rightChild;
}
}
} else if (current.rightChild == null) {// 只有左子节点
if (current == root) {
root = current.leftChild;
} else {
if (isLeftNode) {
parent.leftChild = current.leftChild;
} else {
parent.rightChild = current.leftChild;
}
}
}
5.3、 待删除的节点有两个子节点
这种情况需要寻找后继节点,后继节点用于取代待删除的节点,后继节点位于待删除节点的右子树的最小值。
后继结点也能为两种情况:1、是左节点(即右子树的最左子节点);2、是右节点(右子树无左节点,则第一个右节点是后继结点);
第一种情况:
第二种情况:
// 获取后继节点 -- 后继节点用于取代待删除的节点,后继节点位于待删除节点的右子树的最小值
/**
* @param delNode
* 待删除的Node
* @return 待删除的Node的后继Node
*/
private Node getSuccessor(Node delNode) {
Node successorParent = delNode;// 后继节点的父节点
Node successor = delNode;// 后继节点
Node current = delNode.rightChild;
while (current != null) {
successorParent = successor;
successor = current;
current = current.leftChild;
}
if (successor != delNode.rightChild) {// 针对后继节点是待删除子节点的非直接右子节点的情况
// 调整successor所属的子树
successorParent.leftChild = successor.rightChild;
successor.rightChild = delNode.rightChild;
}
return current;
}
得到后继节点之后,替换待删除节点即可。
// 第三种情况:待删除的节点同时有左子节点和右子节点
Node successor = getSuccessor(current);
if (current == root) {
root = successor;
}
if (isLeftNode) {
parent.leftChild = successor;
} else {
parent.rightChild = successor;
}