如果树中每个节点最多只能有两个子节点,这样的树就成为“二叉树”。两个子节点分别称为“左子节点”和“右子节点”,二叉树中的节点不是必须有两个子节点;它可以只有一个左子节点,或者只有一个右子节点,或者干脆没有子节点(为叶子节点)。
对于一个二叉树来说,如果任意一个节点的左子节点的关键字值小于这个节点的值,右子节点的关键字的值大于或等于这个节点的值,那么这个二叉树可以称为二叉搜索树。
下面就对二叉搜索树进行java的代码实现。
1. 定义节点类
节点里包含本身数据,左子节点引用和右子节点引用,对于复杂的数据,也可以将父节点的引用保存下来,便于向上遍历。
class Node {
/**
* 数据(为简化代码,本程序默认节点里存储一个int变量,实际程序里可以存储自定义类型变量,需要自己实现比较逻辑)
*/
private int data;
/**
* 左子节点
*/
private Node leftNode;
/**
* 右子节点
*/
private Node rightNode;
//getter setter...
}
定义树类
树中只存储一个对根节点的引用,所有的操作都要从根节点开始遍历,与链表类似。 对于一些项目也可以将树的层数,节点个数保存下来,根据实际情况自由设计。
class Tree {
/**
* root node
*/
private Node root;
/**
* add data
* @param data
*/
public void add(int data) {
}
/**
* 遍历打印
*/
public void print() {
}
/**
* 最大值
*/
public void getMax() {
}
/**
* 最小值
*/
public void getMin() {
}
/**
* 删除一个节点
*/
public void delete(int data) {
}
}
2.1 插入节点
要插入节点,必须要找到可以插入的地方,即遍历树,找到可以插入的位置,将父节点的子节点引用指向待插入节点即可。新的节点连接到父节点的左子节点或右子节点取决于新节点的值比父节点的值大还是小。
算法实现:从根节点开始遍历整个树,如果当前遍历到的节点的值大于待插入值,则待插入节点应位于此节点的左侧,检查此节点是否有左子节点,若没有,则待插入节点作为此节点的左子节点,若有值,继续从此节点的左子节点处开始遍历。 反之,如果当前遍历到的节点的值小于或等于待插入值,则待插入节点应位于此节点的右侧,检查此节点是否有右子节点,若没有,则待插入节点作为此节点的右子节插入,若有值,继续从此节点的右子节点处开始遍历。 代码如下:
public void add(int data) {
Node node = new Node();
node.setData(data);
if (root == null) {
root = node;
return ;
}
// find parent node and add node
Node parentNode = root;
while (true) {
if (parentNode.getData() > data) {
if (parentNode.getLeftNode() == null) {
parentNode.setLeftNode(node);
break;
}
parentNode = parentNode.getLeftNode();
continue;
}
if (parentNode.getData() <= data) {
if (parentNode.getRightNode() == null) {
parentNode.setRightNode(node);
break;
}
parentNode = parentNode.getRightNode();
continue;
}
}
}
2.2 遍历整个树
树中只存储了根节点的引用,所以如果需要遍历整个树,需要从根节点开始,查询子节点,子节点继续查询自身的子节点,递归调用查询子节点,最终查询到子节点为空的停止。代码如下:
public void print() {
if (root == null) {
System.out.println(" tree is empty. ");
return ;
}
inorder(root);
}
/**
* 中序遍历
* 从左至右遍历, 从小到大排序
* @param node
*/
private void inorder(Node node) {
if (node == null)
return ;
inorder(node.getLeftNode());
System.out.print(" " + node.getData() + " ");
inorder(node.getRightNode());
}
(1). 调用自身来遍历节点的左子树。
(2). 访问自身的值。
(3). 调用自身来遍历节点的右子树。
中序遍历遍历二叉搜索树会使所有的节点按照关键字值升序被访问到,如果希望顺序获得树中的值,这是一种方法;
对于上面提到的3个步骤来说,不同的执行顺序会有不同的效果。如果要编写程序来解析或分析代数表达式,会用到前序遍历和后序遍历,这里不贴代码了,附录会上传所有的代码。
2.3 获取最小值,最大值
由二叉搜索树的定义可知左子节点的值都小于父节点,右子节点的值都大于/等于父节点的值,所以如果查询一棵树中的最小值,从根节点开始,一直查询左子节点,查到左子节点穷尽位置,得到的节点就是“最左端”的节点,就是值最小的节点; 反之,求取最大值则循环查询右子节点即可。
/**
* 最小值
* 循环查找左子节点,直到左子节点为空
*/
public void getMin() {
if (root == null) {
System.out.println(" tree is empty. ");
return ;
}
Node currentNode = root;
while (true) {
if (currentNode.getLeftNode() == null) {
System.out.println(currentNode.getData());
break;
}
currentNode = currentNode.getLeftNode();
}
}
/**
* 最大值
* 循环查找右子节点,直到右子节点为空
*/
public void getMax() {
if (root == null) {
System.out.println(" tree is empty. ");
return ;
}
Node currentNode = root;
while (true) {
if (currentNode.getRightNode() == null) {
System.out.println(currentNode.getData());
break;
}
else {
currentNode = currentNode.getRightNode();
}
}
}
2.4 删除一个节点
删除一个节点主要可以分为以下三种情况:
(1). 删除的节点没有子节点,是一个叶子节点,无牵无挂,这种情况直接将其父节点的对它的引用置为空即可,没有引用指向这个对象,那么这个对象会被jvm自动回收。
(2). 删除的节点有1个子节点,直接用子节点代替它即可,假设删除的节点有一个左子节点,将其父节点的对它的引用置为对这个左子节点的引用。
后继节点: 大于删除节点的值的所有节点的最小值, 小于删除节点的值的所有节点的最大值
简单来说,对于以下数据: [ 10, 30, 40, 60, 70, 80, 90 ], 假设我们需要删除的数据是40,我们要找的后继节点是60,可以来顶替40。
例如: 我们需要得到位于大于等于40的节点的值,为40,60,70,80,90; 现在我们将40删除之后,需要一个值类顶替它,即大于x的值为40,60,70,80,90,40删除了,我们期望得到的结果为60,70,80,90, 所以x=60。 同理,30节点也可以作为40节点的后继节点。
对于大于删除节点的值的所有节点的最小值,程序找到待删除节点的右子节点,右子节点的值肯定比待删除节点值大,然后转到此右子节点的左子节点那里(如果有的话),然后找这个左子节点的左子节点,顺着左子节点的路径一直找下去,就和上文中所提到的获得最小值方法一样,只不过现在根节点变为待删除节点的右子节点。查询到的最后一个左子节点就是待删除节点的后继。 同理,我们也可以先获取待删除节点的左子节点,然后一直向下查找右子节点,查找到小于删除节点的最大值,也可作为后继节点,本文采用前者。
循环查找待删除节点的后继节点代码如下,完整代码请见附录
Node subsequentNode = deletedNode.getRightNode(); // 后继节点 deletedNode为待删除节点
Node subsequentParentNode = deletedNode; // 后继节点的父亲节点
while (true) {
if (subsequentNode.getLeftNode() == null) {
break;
}
else {
subsequentParentNode = subsequentNode;
subsequentNode = subsequentNode.getLeftNode();
}
}
查找: 二叉搜索树查找与二分查找算法类似,每次比较都可以排除一半的节点。当二叉搜索树左右子树趋于平衡时(乱序),这时查找的平均时间复杂度在O(logN)数量级上; 当树依次存入一个有序数组时,这时树的子节点全部为右子节点(升序)或者左子节点(降序),树变成了类似于链表的结构,平均查找长度为(n+1)/2,查找的平均时间复杂度在O(N)数量级上。
插入: 插入一个结点的代价与查找一个不存在的数据的代价完全相同。
删除: 首先查找到删除的节点,需要花费和查找一样的代价, 如果删除的节点为叶子节点或者只有一个子节点时,简单的改变引用的操作的代价仅为O(1),如果待删除节点有两个子节点,需要查询后继节点,代价为O(logN)(主要为查找过程代价,简单的赋值忽略不计)。
综上,查找最好时间复杂度O(logN),最坏时间复杂度O(N)。 插入删除操作算法简单,具体操作之前需要查找节点,所以时间复杂度与查找差不多。
附: 完整代码
package com.suning.tree.twoforkedtree;
/**
* 二叉树<br>
* 〈功能详细描述〉
*
* @author zhujk
* @see [相关类/方法](可选)
* @since [产品/模块版本] (可选)
*/
class Tree {
public static void main(String[] args) {
Tree tree = new Tree();
tree.add(100);
tree.add(110);
tree.add(75);
tree.add(62);
tree.add(87);
tree.add(77);
tree.add(93);
tree.add(79);
tree.add(78);
tree.add(80);
System.out.println();
tree.print(1);
System.out.println();
tree.getMin();
tree.getMax();
System.out.println();
tree.delete(75);
tree.print(1);
System.out.println();
tree.delete(87);
tree.print(1);
System.out.println();
tree.delete(93);
tree.print(1);
System.out.println();
tree.delete(62);
tree.print(1);
System.out.println();
tree.delete(75);
tree.print(1);
System.out.println();
tree.delete(100);
tree.print(1);
}
/**
* root node
*/
private Node root;
/**
* add data
* @param data
*/
public void add(int data) {
Node node = new Node();
node.setData(data);
if (root == null) {
root = node;
return ;
}
// find parent node and add node
Node parentNode = root;
while (true) {
if (parentNode.getData() > data) {
if (parentNode.getLeftNode() == null) {
parentNode.setLeftNode(node);
break;
}
parentNode = parentNode.getLeftNode();
continue;
}
if (parentNode.getData() <= data) {
if (parentNode.getRightNode() == null) {
parentNode.setRightNode(node);
break;
}
parentNode = parentNode.getRightNode();
continue;
}
}
System.out.println(data + "'s parent is " + parentNode.getData());
}
public void print(int flag) {
if (root == null) {
System.out.println(" tree is empty. ");
return ;
}
switch (flag) {
case 0:
inorder(root);
break;
case 1:
preorder(root);
break;
case 2:
postorder(root);
break;
default:
break;
}
}
/**
* 中序遍历
* 从小到大排序
* @param node
*/
private void inorder(Node node) {
if (node == null)
return ;
inorder(node.getLeftNode());
System.out.print(" " + node.getData() + " ");
inorder(node.getRightNode());
}
/**
* 前序遍历
* @param node
*/
private void preorder(Node node) {
if (node == null)
return ;
System.out.print(" " + node.getData() + " ");
preorder(node.getLeftNode());
preorder(node.getRightNode());
}
/**
* 后序遍历
* @param node
*/
private void postorder(Node node) {
if (node == null)
return ;
postorder(node.getLeftNode());
postorder(node.getRightNode());
System.out.print(" " + node.getData() + " ");
}
/**
* 最大值
* 循环查找右子节点,直到右子节点为空
*/
public void getMax() {
if (root == null) {
System.out.println(" tree is empty. ");
return ;
}
Node currentNode = root;
while (true) {
if (currentNode.getRightNode() == null) {
System.out.println(currentNode.getData());
break;
}
else {
currentNode = currentNode.getRightNode();
}
}
}
/**
* 最小值
* 循环查找左子节点,直到左子节点为空
*/
public void getMin() {
if (root == null) {
System.out.println(" tree is empty. ");
return ;
}
Node currentNode = root;
while (true) {
if (currentNode.getLeftNode() == null) {
System.out.println(currentNode.getData());
break;
}
currentNode = currentNode.getLeftNode();
}
}
/**
* 删除一个节点
* 这里只考虑没有重复值的情况, 如果有重复值,删除之后继续检查右子节点(右子节点大于等于父节点)。
*
* 情况1: 删除的是节点,左右子节点至少一个为空的
* 情况2: 删除的是中间节点(包括根节点),具有两个根节点,需要找后继节点顶替此节点。
*/
public void delete(int data) {
if (root == null) {
System.out.println(" tree is empty. ");
return ;
}
// 待删除节点
Node deletedNode = root;
// 待删除节点的父节点
Node parentNode = null;
// 遍历找出待删除节点
while (true) {
if (deletedNode.getData() == data) {
break;
}
else if (deletedNode.getData() > data) {
parentNode = deletedNode;
deletedNode = deletedNode.getLeftNode();
}
else {
parentNode = deletedNode;
deletedNode = deletedNode.getRightNode();
}
if (deletedNode == null) {
System.out.println(" not find : " + data);
return ;
}
}
// 1. 待删除节点,只有一个子节点,或者没有子节点
if (deletedNode.getLeftNode()==null || deletedNode.getRightNode()==null) {
// 叶子节点
if (deletedNode.getLeftNode()==null && deletedNode.getRightNode()==null) {
if (parentNode == null) {
root = null; // 删除的是根节点,直接将此树置空
}
else {
if (parentNode.getLeftNode() == deletedNode) // 确定待删除节点是父节点的左子节点还是右子节点
parentNode.setLeftNode(null);
else
parentNode.setRightNode(null);
}
}
// 只有一个子节点, 直接用子节点代替待删除节点即可
else {
if (deletedNode.getLeftNode() != null) {
if (parentNode == null) {
root = deletedNode.getLeftNode();
}
else {
if (parentNode.getLeftNode() == deletedNode)
parentNode.setLeftNode(deletedNode.getLeftNode());
else
parentNode.setRightNode(deletedNode.getLeftNode());
}
}
else {
if (parentNode == null) {
root = deletedNode.getRightNode();
}
else {
if (parentNode.getLeftNode() == deletedNode)
parentNode.setLeftNode(deletedNode.getRightNode());
else
parentNode.setRightNode(deletedNode.getRightNode());
}
}
}
}
// 2. 待删除节点,有两个子节点,需要找后继节点的
else {
// 找出后继节点,即: 大于待删除节点的最小节点 或者 小于待删除节点的最大节点, 两者都一样,本代码采用第一个。
// 大于待删除节点的最小节点: 待删除节点的右子节点的左子节点的左子节点的左子节点... 一直查找到最左端
// 待删除节点的后继节点顶替待删除节点, 后继节点的右子节点顶替后继节点, 后继节点没有左子节点。
Node subsequentNode = deletedNode.getRightNode(); // 后继节点
Node subsequentParentNode = deletedNode; // 后继节点的父亲节点
while (true) {
if (subsequentNode.getLeftNode() == null) {
break;
}
else {
subsequentParentNode = subsequentNode;
subsequentNode = subsequentNode.getLeftNode();
}
}
// 待删除节点的相关节点命名: parentNode deletedNode lChildNode rChildNode
Node lChildNode = deletedNode.getLeftNode();
Node rChildNode = deletedNode.getRightNode();
// 后继节点的相关节点命名: subsequentParentNode subsequentNode srChildNode
Node srChildNode = subsequentNode.getRightNode(); // 后继节点的右子节点
// 后继节点替代待删除节点
subsequentNode.setLeftNode(lChildNode); // 后继节点的左子节点指向待删除节点的左子节点
if (subsequentParentNode != deletedNode) {
subsequentNode.setRightNode(rChildNode);// 后继节点的右子节点指向待删除节点的右子节点, 特殊情况,待删除的右子节点就是后继节点
}
if (parentNode == null) {
root = subsequentNode;
} else {
if (parentNode.getLeftNode() == deletedNode)
parentNode.setLeftNode(subsequentNode);
else
parentNode.setRightNode(subsequentNode);
}
// 后继节点的右子节点顶替后继节点, 特殊情况,待删除的右子节点就是后继节点
if (subsequentParentNode != deletedNode) {
subsequentParentNode.setLeftNode(srChildNode);
}
}
}
}
package com.suning.tree.twoforkedtree;
/**
* 树节点实体类<br>
* 〈功能详细描述〉
*
* @author zhujk
* @see [相关类/方法](可选)
* @since [产品/模块版本] (可选)
*/
class Node {
/**
* 数据
*/
private int data;
/**
* 左子节点
*/
private Node leftNode;
/**
* 右子节点
*/
private Node rightNode;
/**
* @return the data
*/
public int getData() {
return data;
}
/**
* @param data the data to set
*/
public void setData(int data) {
this.data = data;
}
/**
* @return the leftNode
*/
public Node getLeftNode() {
return leftNode;
}
/**
* @param leftNode the leftNode to set
*/
public void setLeftNode(Node leftNode) {
this.leftNode = leftNode;
}
/**
* @return the rightNode
*/
public Node getRightNode() {
return rightNode;
}
/**
* @param rightNode the rightNode to set
*/
public void setRightNode(Node rightNode) {
this.rightNode = rightNode;
}
}
注: 本文内容参考于Java数据结构和算法第二版,代码纯手打,如有错误和疑问请留言,共同进步。