java实现(1)-二叉查找树

时间:2021-03-04 20:17:23

引言

在这个模块中,主要是用自己的代码来实现一些底层的源码。不像底层源码那样难理解,会用更加通俗的方式让每个人都能看得懂,比如说不会在if中声明变量和赋值,即使if只有一句话也不会省略花括号,不适用移位等等。在本篇博文中,会用自己的方式来实现二叉查找树,同时把它转变为更加通用的泛型。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290

技术点

1、泛型
在jdk1.5中引入,目标是为了更好的支持代码的重用。考虑这么一种情况,在除去对象的类型之外,实现方法是相同的,那么我们就可以用泛型实现。比如说排序当中,我们可以对int排序,也可以对double,也可以对String排序,甚至可以对我们自己写的对象进行排序,排序的方式是相同的,都是拿两者进行比较,大的放前面,小的放后面,这个时候就可以用泛型实现。比如说一个List对象,你可以是放Integer,也可以放String,这就是泛型。

2、树
一棵树是节点的集合,在不是空集的情况下,树就是由节点以及0个或者多个非空的子集树构成,这些子集树的都来自总集合树的一条有向的所连接。比如说下面就是一棵树:
java实现(1)-二叉查找树
A是B,C,D,E,F,G,H的父亲,反之它们是A的儿子;同理,F是K,L的父亲,K,L是F的儿子;B,C,D,E,F,G,H之间可以称为兄弟;甚至,可以称A为I,J,K,L的祖父,反之它们是A的孙子。其中如果某一个节点不再有儿子,那么我们称这个节点为树叶
那么,我们可以知道,树其实是由N个节点和N-1条边组成,为什么是N-1条边呢?因为除却总集合的根节点外,每个节点都会有一条边与父节点相连,所以“-1”是总集合根节点没有父节点造成的。
接着我们说说树的深度和高,对于任意一个节点n,从根到这个n节点所有边的和称为节点n的深度,比如说A的深度为0,B的深度为1,K的深度为2,M的深度为3。对于任意一个节点n,从这个节点到一片树叶所有的边长的和称为高。比如说A的高为3,B的高为0,F的高为2,K的高为1。

3、二叉查找树
分为两部分来分为,二叉树是指其中任意一个节点都不能有多余两个儿子。查找的意思就是它建立在二叉树的基础上多了一个节点大小的规则。对于任意一个节点,它的左儿子肯定比它自身小,它的右儿子肯定比它自身大,如下图就是一个二叉查找树:
java实现(1)-二叉查找树
今天,我们主要是来实现二叉查找树的创建,插入,查找最大,查找最小和删除等。同时在Integer实现的情况下改成更通用的泛型实现。

代码实现查找二叉树

package com.brickworkers;

/**
*
* @author Brickworker
* data:2017年4月20日下午2:21:13
* 关于类SearchBinaryTree.java的描述:一个二叉查找树存储Integer数据
* Copyright (c) 2017, brcikworker All Rights Reserved.
*/

public class SearchBinaryTree {

//定义一个节点
private static class BinaryNode{
Integer data;//存储数据
BinaryNode left;//左孩子
BinaryNode right;//右孩子

public BinaryNode(Integer data) {//构造函数,生成一个没有儿子的节点
this(data, null, null);
}

public BinaryNode(Integer data, BinaryNode lt, BinaryNode rt) {//构造函数,生产一个有两个儿子的节点(lt,rt不为空)
this.data = data;
left = lt;
right = rt;
}
}


//定义一个根节点,当二叉查找树初始化的时候必须要有一个根节点,哪怕是个空的根节点
private BinaryNode root;

//构造函数,构造一颗空的查找二叉树
public SearchBinaryTree() {
root = null;
}

//判断是否包含某个节点
public boolean contains(Integer t){
return contains(t, root);
}

//寻找最小的节点
public Integer findMin(){
if(root == null)//如果跟节点是空的,表示二叉查找树是空的,不必寻找最小了
throw new IllegalArgumentException("该树是空的");
else
return findMin(root).data;
}

//寻找最大节点
public Integer findMax() {
if (root == null)
throw new IllegalArgumentException("该树是空的");
else
return findMax(root).data;
}


//插入节点
public void insert(Integer t){
root = insert(t, root);
}


//删除节点
public void remove(Integer t){
root = remove(t, root);
}




//按顺序打印树
public void printTree(){
if(root == null)
throw new IllegalArgumentException("该树是空的");
else
printTree(root);
}


//返回true或者false
// 搜索树,查找是否包含某个节点
private boolean contains(Integer t, BinaryNode node) {
if (node == null) {// 如果搜索到null了还没有找到,那么就返回false
return false;
}
// 如果要查找的数据比目前节点的小,根据二叉查找树的大下原则,往左边继续递归查找
if (t < node.data) {
return contains(t, node.left);
}
// 如果要查找的数据比目前节点的大,根据二叉查找树的大下原则,往右边继续递归查找
else if (t > node.data) {
return contains(t, node.right);
} else{//两者相等,说明找到了目标值
return true;
}
}

//返回一个节点
//寻找最小节点
//在二叉查找树要寻找最小,根据二叉查找树的性质,其实就是找树的最左边的节点,也就是最左边的叶子
private BinaryNode findMin(BinaryNode node) {
// 如果这个节点不为空,且没有左儿子,就表示这个节点是最小的了
if (node.left == null) {
return node;
} else {// 否则继续递归寻找
return findMin(node.left);
}
}


//返回一个节点
// 寻找最大节点
//在二叉查找树要寻找最大,根据二叉查找树的性质,其实就是找树的最右边边的节点,也就是最右边的叶子
private BinaryNode findMax(BinaryNode node) {
// 如果这个节点不为空,且没有右儿子,就表示这个节点是最大的了
if (node.right == null) {
return node;
} else {//否则继续递归寻找
return findMax(node.right);
}
}

//返回一个节点,为什么要返回一个节点呢?因为在我们新建一个节点的时候,必须要把它关联到上一个节点的做儿子或者右儿子上。比如下面的node.right = insert(t, node.right);
//插入节点
//在二叉查找树插入中,根据它的原则,比节点小的要往左边插入,比节点大的要往右边插入
private BinaryNode insert(Integer t, BinaryNode node){
//如果这个节点为null,就表示已经找到当前要插入的位置,并用t创建一个新的节点返回
if(node == null){
return new BinaryNode(t);
}
//如果要插入的数据比当前节点大,那么根据二叉查找树的原则,需要往当前节点的右侧开始查找
if(t > node.data){
node.right = insert(t, node.right);
}
//如果要插入的数据比当前节点小,那么根据二叉查找树的原则,需要往当前节点的左侧开始查找
else if(t < node.data){
node.left = insert(t, node.left);
}else{//当两者相同的时候,禁止插入(当然,你也可以设计成能插入的,具体插入到左边还是右边自己定)
throw new IllegalArgumentException("不允许插入已存在的数据,t:" +node.data);
}
return node;

}

//返回一个节点
//删除节点
//在二叉查找树插入中,删除节点要分为3中情况来讨论,①要删除的节点本身是树叶,那么直接删除。②如果要删除的节点
//存在一个儿子,那么直接把它儿子顶替它的位置;③如果要删除的节点有两个儿子,那么就需要把它左边最大,或者右边最小
//的来顶替它的位置。因为只有左边最大和右边最小放在它的位置才能符合二叉查找树的规则。
private BinaryNode remove(Integer t, BinaryNode node){
//如果要删除的节点比当前节点小,那么根据二叉查找树的规则,往左边继续查找
if(t < node.data){
node.left = remove(t, node.left);
}
//如果要删除的节点比当前节点大,那么根据二叉查找树的规则,往右边继续查找
else if(t > node.data)
node.right = remove(t, node.right);
//如果已经找到要删除的节点,但是这个节点有两个儿子
else if(node.left != null && node.right != null){
node.data = findMin(node.right).data;//找到右边最小的顶替目前节点的位置
node.right = remove(node.data, node.right);//然后继续向右遍历删除用于顶替的最小的那个节点
}else{//如果要删除的节点没有或者只有一个儿子
node = (node.left != null)? node.left: node.right;
}
return node;
}

//按顺序打印树
private void printTree(BinaryNode node){
//当前节点不为空
if(node != null){
//优先遍历左边,从小到大输出
printTree(node.left);
System.out.print(node.data + " ");
/遍历完左边再遍历右边,从小到大输出
printTree(node.right);
}
}

}

在上面的实现中,我写了大量的注释,希望对大家有所帮助,但是在这里,我还是要把删除再解释一遍。在二叉查找树的删除分为3中情况,以下面这种图为例子:
java实现(1)-二叉查找树

不存在儿子,比如说4,7,9,13。那么在删除这些节点的时候可以直接删除。
存在一个儿子,比如说5,删除之后应该是这样的:
java实现(1)-二叉查找树

存在两个儿子,比如说10,6,8。在上图中以删除6为例子,删除之后应该是这样的:
java实现(1)-二叉查找树

同理,如果用6的左边最大的来替换6节点也是一样的道理。

其他的,如果一句句看上面的代码应该是能看懂的。上面这个二叉查找树只能使用于Integer的存储,那如果我要存储String,甚至是自己定义的对象怎么办?接下来,我们一起研究一下,如何把它转变成一个更通用的泛型对象。

把特殊二叉查找树转化成一般二叉查找树

我们先进行上面特殊的二叉查找树进行分析:
①有一个重要的静态内部类BinaryNode, 它主要是用于存储数据,所以在这里我们就要用泛型来表示数据的类型,也就是说data应该是anytype的。
②二叉查找树的原则是要根节点比它左节点大,比右节点小。所以每个节点之前必然存在着一个比较的行为,在上面的例子中Integer可以用>或者< 来比较,那一般对象怎么办呢?在以往的博客中有介绍过Comparable接口,这个接口可以实现使用compareto来进行比较。

所以接下来我们对上面的特殊类进行转化成泛型类:

package com.brickworkers;

/**
*
* @author Brickworker
* data:2017年4月20日下午2:21:13
* 关于类SearchBinaryTree.java的描述:一个二叉查找树存储Integer数据
* Copyright (c) 2017, brcikworker All Rights Reserved.
*/

public class SearchBinaryTree <T extends Comparable<? super T>>{//类型 T 必须实现 Comparable 接口,并且这个接口的类型是 T 或 T 的任一父类,这样声明后,T 的实例之间,T 的实例和它的父类的实例之间,可以相互比较大小

private static class BinaryNode<T>{//用T表示节点的数据存储类型
T data;//用T表示数据类型
BinaryNode<T> left;//用T表示数据类型
BinaryNode<T> right;//用T表示数据类型

public BinaryNode(T data) {//用T表示数据类型
this(data, null, null);
}

public BinaryNode(T data, BinaryNode<T> lt, BinaryNode<T> rt) {//用T表示数据类型
this.data = data;
left = lt;
right = rt;
}
}


//一个支持任意类型的根节点
private BinaryNode<T> root;

public SearchBinaryTree() {
root = null;
}

public boolean contains(T t){//用T表示数据类型
return contains(t, root);
}

public T findMin(){//直接返回T类型
if(root == null)
throw new IllegalArgumentException("该树是空的");
else
return findMin(root).data;
}

public T findMax() {//直接返回T类型
if (root == null)
throw new IllegalArgumentException("该树是空的");
else
return findMax(root).data;
}


public void insert(T t){//用T表示数据类型
root = insert(t, root);
}


//删除节点
public void remove(T t){//用T表示数据类型
root = remove(t, root);
}




//按顺序打印树
public void printTree(){
if(root == null)
throw new IllegalArgumentException("该树是空的");
else
printTree(root);
}


private boolean contains(T t, BinaryNode<T> node) {// 用T表示数据类型
if (node == null) {
return false;
}

// 不能再用> < 来对T 类型的数据进行比较了,需要用compareTo方法
if (t.compareTo(node.data) < 0) {
return contains(t, node.left);
} else if (t.compareTo(node.data) > 0) {
return contains(t, node.right);
} else {
return true;
}

}

private BinaryNode<T> findMin(BinaryNode<T> node) {// 用T表示数据类型
if (node.left == null) {
return node;
} else {
return findMin(node.left);
}
}


private BinaryNode<T> findMax(BinaryNode<T> node) {// 用T表示数据类型
if (node.right == null) {
return node;
} else {
return findMax(node.right);
}
}

private BinaryNode<T> insert(T t, BinaryNode<T> node){// 用T表示数据类型
if(node == null){
return new BinaryNode<T>(t);
}
if(t.compareTo(node.data) > 0){
node.right = insert(t, node.right);
}
else if(t.compareTo(node.data) < 0){
node.left = insert(t, node.left);
}else{
throw new IllegalArgumentException("不允许插入已存在的数据,t:" +node.data);
}
return node;

}

private BinaryNode<T> remove(T t, BinaryNode<T> node){
if(t.compareTo(node.data) < 0){
node.left = remove(t, node.left);
}
else if(t.compareTo(node.data) > 0)
node.right = remove(t, node.right);
else if(node.left != null && node.right != null){
node.data = findMin(node.right).data;
node.right = remove(node.data, node.right);
}else{
node = (node.left != null)? node.left: node.right;
}
return node;
}

private void printTree(BinaryNode<T> node){
if(node != null){
printTree(node.left);
System.out.print(node.data + " ");
printTree(node.right);
}
}

}

我们可以用上面的测试类,给二叉查找树赋予一个类型,进行测试:

package com.brickworkers;
/**
*
* @author Brickworker
* Date:2017年4月20日下午2:33:19
* 关于类TreeTest.java的描述:二叉查找树测试
* Copyright (c) 2017, brcikworker All Rights Reserved.
*/

public class TreeTest {

public static void main(String[] args) {
SearchBinaryTree<Integer> searchBinaryTree = new SearchBinaryTree<Integer>();//用<Integer>来表示数据类型
//插入0~9的数据
for (int i = 0; i < 10; i++) {
searchBinaryTree.insert(i);
}
//打印情况
searchBinaryTree.printTree();
System.out.println();
//插入测试
searchBinaryTree.insert(11);
searchBinaryTree.insert(12);
searchBinaryTree.printTree();
System.out.println();

//插入10,看是否遵循顺序
searchBinaryTree.insert(10);
searchBinaryTree.printTree();
System.out.println();

//判断某节点是否存在
System.out.println(searchBinaryTree.contains(3));
System.out.println(searchBinaryTree.contains(15));

//查找最小
System.out.println("最小的数据为:" + searchBinaryTree.findMin());

//查找最大
System.out.println("最大的数据为:" + searchBinaryTree.findMax());

//删除某个节点:
searchBinaryTree.remove(3);
searchBinaryTree.printTree();
System.out.println();

//重复插入节点
searchBinaryTree.insert(2);

}
//输出结果:
//0 1 2 3 4 5 6 7 8 9
// 0 1 2 3 4 5 6 7 8 9 11 12
// 0 1 2 3 4 5 6 7 8 9 10 11 12
// 最小的数据为:0
// 最大的数据为:12
// 0 1 2 4 5 6 7 8 9 10 11 12
// Exception in thread "main" java.lang.IllegalArgumentException: 不允许插入已存在的数据,t:2
}

发现结果和上面特殊情况是一样的。这样就是把特殊转化成了一般。好了,关于查找二叉树,就写到这里了,希望对大家有所帮助,或者有所感悟也行哈。