前置知识:
什么是二叉搜索树?
是二叉树的一种,任意一个结点的值都大于左子树的所有值,任意一个结点的值都小于右子树所有的值。
二叉搜索树存储的元素类型,必须是可以比较的,并且不能为 null 。
二叉搜索树的接口
既然是用于搜索功能,应该有增删查功能。 另外还可以提供一些便捷的接口。当前树的大小,是否为空等。
//添加一个元素public void add(E element) ;//删除一个指定的元素public void remove(E element);//查找一个元素public boolean constains(E element);//获取当前二叉搜索树大小public int size();//清空public void clear();//当前二叉树是否为空public boolean isEmpty();
二叉搜索树接口的实现
类设计
在实现接口之前,首先分析一下二叉搜索树应该有哪些结构、元素。
树是由结点组成的,所以须要有结点这个类。结点里面肯定有一个元素(或者叫做值)来存储内容。结点存储的元素的类型是未知的,所以使用泛型。由于是二叉搜索树,所以结点需要有左右子节点,并且除了根结点以外的结点都有父节点。
创建结点的时候,必须要传结点的内容和父节点才可以创建成功。所以构造函数要求传这两个参数。除了根结点不需传父节点以外。
综合来看,结点类如下:
public class Node<E> {/*** 元素*/E element;/*** 左子节点*/Node<E> left;/*** 右子节点*/Node<E> right;/*** 父节点*/Node<E> parent;/*** 创建结点除了根结点没有父节点外,其他结点都有父节点和结点元素对象。左右子节点创建的时候不一定有。*/public Node(E element, Node<E> parent) {this.element = element;this.parent = parent;}}
二叉搜索树类,首先必须有一个大小的属性来记录当前树内有多少个元素。然后必须有一个根结点,不然对这棵树无从下手。结点类是为了二叉搜索树而存在的,其它类不需要用到它,所以可以作为二叉搜索树的内部类。
再加上之前分析的需要的一些方法。综合上述,二叉搜索树类如下:
/*** 二叉搜索树*/public class BinarySearchTree<E> {private int size;private E element;/*** 返回当前大小* @return size*/public int size() {return size;}/*** 返回当前是否为空* @return 是否为空*/public boolean isEmpty() {return size == 0 ;}/*** 清空所有元素*/public void clear() {root = null;size = 0;}/*** 添加一个元素* @param element 元素*/public void add(E element) {}/*** 删除一个特定元素* @param element 需要删除的元素*/public void remove(E element) {}/*** 判断元素是否存在* @param element 需要判断的元素* @return 判断结果*/public boolean contains(E element) {return false;}private static class Node<E>{E element;Node<E> left;Node<E> right;Node<E> parent;public Node(E element, Node<E> parent) {this.element = element;this.parent = parent;}}}
方法实现
那么,主要实现的方法有3个,添加,删除和查找
添加方法
添加一个结点比较简单,首先看是否添加的是根结点,如果是添加的是根结点,则直设置当前结点为根结点,然后就可以结束方法了。 代码如下:
/*** 添加一个元素*/public void add(E element) {//如果根结点为空,则先完成根结点的添加if (root == null) {root = new Node<>(element,null);size++;return;}}
非根节点的添加思路:
- 找到新增节点的父节点。
从根节点开始对比,如果比根节点大,则去右子树找,如果比根节点小,则去左子树找。一直找到空的位置。如果新增的节点和父节点相等,不处理,或者替换,建议进行替换。因为新元素和旧元素是不一样的。如果没有处理,则丢失了新增的节点了。 - 确定新增节点的位置。
确定父节点后,也就确定了新增节点的位置,是父节点的左节点还是右节点,还是和父节点一样。 然后新增加一个节点,添加进树里面。 然后设置新增的节点为父节点的左子树或者右子树,或者替换掉父节点。
代码如下:
/*** 添加一个元素*/public void add(E element) {//确保元素是非空的elementNotNullCheck(element);//如果根结点为空,则先完成根结点的添加if (root == null) {root = new Node<>(element,null);size++;return;}//当前结点从根结点开始Node<E> currentNode = root;//保存父节点,创建新节点的时候需要用到Node<E> parentNode = currentNode;//保存比较结果,用于判断新增的是左子节点还是右子节点int compareResult = 0;while (currentNode != null) {parentNode = currentNode;compareResult = ((Comparable)currentNode.element).compareTo(element);if (compareResult > 0) {currentNode = currentNode.left;} else if (compareResult < 0) {currentNode = currentNode.right;} else {//两个元素相等的情况下,用新的元素覆盖掉旧的元素currentNode.element = element;return;}}//将新节点加入树:新节点和树的结点建立引用关系Node<E> newNode = new Node<>(element,parentNode);if (compareResult > 0 ) {parentNode.left = newNode;} else if (compareResult < 0) {parentNode.right = newNode;}size++;}private void elementNotNullCheck(E element) {if (element == null) {throw new IllegalArgumentException("元素不能为null");}}
这个时候,一般的添加算法就可以算作是完成了。但是值得优化的地方是,如果需要对添加进树的元素进行自定义的比较的话,当前是满足不了的。 因为在比较两个元素的时候,只是单纯的将元素转换成 comparable 接口来进行比较 :
compareResult = ((Comparable)currentNode.element).compareTo(element);
虽然如果元素没有实现 comparable 接口的话就会报错,类型转换异常,相当于强制要求元素实现 comparable 接口,但是这样的实现方式也限制了比较的逻辑。
例如 Car 类,默认实现了 compareble 接口,按照价格比较。
public class Car implements Comparable {private int price ;private int size;@Overridepublic int compareTo(Object o) {Car carO = (Car)o;return carO.price - this.price;}}
这时候,将 Car 对象放入搜索树的话,只能按照价格来排序。但是这时候希望,Car 对象可以按照尺寸来进行排序。或者说希望有2棵二叉搜索树,都是装 Car 类型的对象,但是1棵是按照 Car 的价格排序,另外1棵按照 Car 类型的尺寸排序。
要实现这样的定制化比较需求的话,可以在二叉搜索树内设计一个比较器对象。然后将比较的逻辑抽取出来形成一个方法。这个比较方法,如果二叉搜索树有比较器的话,就优先使用比较器进行比较,如果没有的话,就使用 comparable 进行比较。
增加构造器和抽取 compareTo 方法:
public class BinarySearchTree<E> {//增加比较器属性private Comparator<E> comparator;//在构造函数里面将构造器传进来public BinarySearchTree(Comparator<E> comparator) {this.comparator = comparator;}//提供默认构造函数public BinarySearchTree() {}/*** 比较两个元素的大小* @param e1 当前元素* @param e2 需要比较的元素* @return e1 > e2 返回正数, e1=e2 返回0 , e1 < e2 返回负数*/private int compareTo(E e1 , E e2) {if (comparator != null) {return comparator.compare(e1,e2);}return ((Comparable)e1).compareTo(e2);}}
完成添加接口后,完整的类如下:
public class BinarySearchTree<E> {private Comparator<E> comparator;public BinarySearchTree(Comparator<E> comparator) {this.comparator = comparator;}public BinarySearchTree() {}/*** 树的大小*/private int size;/*** 根结点*/private Node<E> root;/*** 添加一个元素*/public void add(E element) {elementNotNullCheck(element);//如果根结点为空,则先完成根结点的添加if (root == null) {root = new Node<>(element,null);size++;return;}//当前结点从根结点开始Node<E> currentNode = root;//保存父节点,创建新节点的时候需要用到Node<E> parentNode = currentNode;//保存比较结果,用于判断新增的是左子节点还是右子节点int compareResult = 0;while (currentNode != null) {parentNode = currentNode;compareResult = compareTo(currentNode.element,element);if (compareResult > 0) {currentNode = currentNode.left;} else if (compareResult < 0) {currentNode = currentNode.right;} else {//两个元素相等的情况下,用新的元素覆盖掉旧的元素currentNode.element = element;return;}}//将新节点加入树:新节点和树的结点建立引用关系Node<E> newNode = new Node<>(element,parentNode);if (compareResult > 0 ) {parentNode.left = newNode;} else if (compareResult < 0) {parentNode.right = newNode;}size++;}private void elementNotNullCheck(E element) {if (element == null) {throw new IllegalArgumentException("元素不能为null");}}/*** 比较两个元素的大小* @param e1 当前元素* @param e2 需要比较的元素* @return e1 > e2 返回正数, e1=e2 返回0 , e1 < e2 返回负数*/private int compareTo(E e1 , E e2) {if (comparator != null) {return comparator.compare(e1,e2);}return ((Comparable)e1).compareTo(e2);}/*** 删除一个指定的元素*/public void remove(E element){}/*** 查找一个元素*/public boolean contains(E element){return false;}/***获取当前二叉搜索树大小*/public int size(){return this.size;}/*** 清空*/public void clear(){root = null;size = 0;}/*** 当前二叉树是否为空*/public boolean isEmpty(){return sizs == 0;}private static class Node<E>{E element;Node<E> left;Node<E> right;Node<E> parent;public Node(E element, Node<E> parent) {this.element = element;this.parent = parent;}}}
遍历方法
进行查找编写查找方法之前,首先进行遍历方法的编写。因为查找的时候会用到。对二叉搜索树的遍历,一般有4种方式,前序遍历,中序遍历,后序遍历,层级遍历。
前序遍历:先访问根结点,再访问子结点。 根结点 -> 左子结点 -> 右子结点 或者 根结点 -> 右子节点 -> 左子节点 都可以,一般默认先左再右。根结点在最先访问。
中序遍历:先访问左子结点,再访问根结点,再访问右子节点。 左子结点 -> 根结点 -> 右子结点 或者 右子结点 -> 根节点 -> 左子节点 都可以,一般默认先左再右。 根结点放在中间访问。
后序遍历:先访问左子结点,再访问右子结点,再访问根节点。 左子结点 -> 右子结点 -> 根结点 或者 右子结点 -> 左子节点 -> 根节点 都可以,一般默认先左再右。 根结点放在最后访问。
层序遍历:按照二叉树,一层一层的遍历。

例如上面的二叉树,
前序遍历结果是:7 4 2 1 3 5 9 8 11 10 12
中序遍历结果是:1 5 3 4 5 7 8 9 10 11 12
后续遍历结果是:1 3 2 5 4 8 10 12 11 9 7
层序遍历的结果是:7 4 9 2 5 8 11 1 3 10 12
使用递归的方式进行遍历的话,遍历的实现比较简单,调用的时候使用传入根结点即可:
/*** 递归的方式进行前序遍历*/public void preOrderByRecursive(Node node) {if (node == null) {return;}System.out.print(node.element + " ");preOrderByRecursive(node.left);preOrderByRecursive(node.right);}/*** 递归的方式进行中序遍历*/public void inOrderByRecursive(Node node) {if (node == null) {return;}inOrderByRecursive(node.left);System.out.print(node.element + " ");inOrderByRecursive(node.right);}/*** 递归的方式进行后序遍历*/public void afterOrderByRecursive(Node node) {if (node == null) {return;}afterOrderByRecursive(node.left);afterOrderByRecursive(node.right);System.out.print(node.element + " ");}
层序遍历稍微复杂一些,需要和队列配合使用。首先将根结点放入队列,然后循环遍历出列,在出列的同时,进行结点的访问,并且将结点的左右子节点放入队列。一直循环将所有元素都出列,这样就完成了层序遍历。
/*** 循环的方式进行层序遍历*/public void levelOrderByLoop(Node node) {if (node == null) {return;}//根结点入列Queue<Node> queue = new LinkedList();queue.offer(node);//循环出列,然后访问元素并且将子结点入列while (!queue.isEmpty()) {Node n = queue.poll();System.out.print(n.element + " ");if (n.left != null) {queue.offer(n.left);}if (n.right != null) {queue.offer(n.right);}}}
这样的写法,还是有一个缺点,就是在遍历的时候,对结点的操作是固定的,只能是进行输出元素。如果希望将元素+1 ,或者 -1 这样的操作,就满足不了。所以,可以像增加比较器那样,增加一个访问器,遍历的时候,根据访问器的逻辑,对结点进行访问。由于访问器是在二叉搜索树内部使用的,所以定义一个内部类即可。
public static interface Visitor<E> {/*** 访问方法*/void visit(E element);}
然后将遍历的方法稍加修改,加入访问器, 修改后的遍历方法如下:
/*** 使用递归的方式进行前序遍历* @param node 前序遍历的结点*/public void preOrderByRecursive(Node<E> node ,Visitor<E> visitor) {if (node == null) {return;}if (visitor != null) {visitor.visit(node.element);}preOrderByRecursive(node.left,visitor);preOrderByRecursive(node.right,visitor);}/*** 递归的方式进行中序遍历*/public void inOrderByRecursive(Node<E> node,Visitor<E> visitor) {if (node == null) {return;}inOrderByRecursive(node.left,visitor);if (visitor != null) {visitor.visit(node.element);}inOrderByRecursive(node.right,visitor);}/*** 递归的方式进行后序遍历*/public void afterOrderByRecursive(Node<E> node,Visitor<E> visitor) {if (node == null) {return;}afterOrderByRecursive(node.left,visitor);afterOrderByRecursive(node.right,visitor);if (visitor != null) {visitor.visit(node.element);}}/*** 循环的方式进行层序遍历*/public void levelOrderByLoop(Node<E> node,Visitor<E> visitor) {if (node == null) {return;}//根结点入列Queue<Node> queue = new LinkedList();queue.offer(node);//循环出列,然后访问元素并且将子结点入列while (!queue.isEmpty()) {Node<E> n = queue.poll();if (visitor != null) {visitor.visit(n.element);}if (n.left != null) {queue.offer(n.left);}if (n.right != null) {queue.offer(n.right);}}}
高度计算
接下来再来看看二叉树的高度的计算。二叉树的高度=根结点的高度。
根结点的高度=高度更高的子节点的高度+1。只需要递归的不断找到更高的子结点的高度,然后+1,就可以得到根结点的高度了。 使用递归的方式求树的高度实现如下:
/*** 使用递归的方式返回结点的高度* @return 结点的高度*/public int heightByRecursive(Node node) {if (node == null) {return 0;}return Math.max(heightByRecursive(node.left),heightByRecursive(node.right)) + 1;}
使用循环的方式来计算树的高度也比较简单,树的高度其实也就是等于树的层数。那么只需要计算出树有多少层即可。
计算层数,使用层序遍历,是比较简单的方式。只需要在遍历的时候,遍历完一层的时候,高度+1,一直遍历完所有的层,那么树的层数也就知道了。
经过观察发现,每遍历完一层的时候,队列中剩下的元素的数量就等于下一层元素的数量。例如在上图的二叉树中,结点7出列后,队列中就只剩下结点4,9。结点9出列后,队列中只剩剩下结点2,5,8,11。
那么,就可以加一个变量,用来记录当前层还没有遍历的元素:levelSize 。在遍历的过程中 levelSize = 0,那么就代表,这一层已经遍历完了,就可以进行高度的累加了。累加完的同时,也是可以知道下一层的元素的数量的,那么重新将 levelSize 的大小设置为下一层的元素的数量,然后开始遍历下一层。这样遍历完成后,就可以得到树的层数,也就是高度了。 代码实现如下:
/*** 使用递归的方式返回树的层数也就是高度* @return 树的高度*/public int heightByLoop(Node node) {if (node == null) {return 0;}//根结点入列Queue<Node> queue = new LinkedList();queue.offer(node);//当前层,还没有遍历的元素个数//还没有开始遍历,所以,还没有遍历的层是第1层,还没有遍历的元素是1个。int levelSize = 1;//初始高度为0;int height = 0;//循环出列,然后访问元素并且将子结点入列while (!queue.isEmpty()) {Node n = queue.poll();levelSize --;if (n.left != null) {queue.offer(n.left);}if (n.right != null) {queue.offer(n.right);}//levelSize == 0 说明已经遍历完这一层的元素if (levelSize == 0) {//累加高度height ++;//赋值下一层的未遍历的元素个数levelSize = queue.size();}}return height;}
前驱和后继结点
中序遍历时的前一个结点称为前驱结点,中序遍历时的后一个结点称为后继结点。
例如上面的二叉树,结点8的前驱是7,后继是9。结点2的前驱是1,后继是3。结点7的前驱是6,后继是8。结点1没有前驱,后继是2。结点10的前驱是9,后继是11。结点12的前驱是11,没有后继
根据观察总结可以分析得出,求一个结点的前驱的时候,可以分为以下2种情况:
- 结点有左子树。那么,前驱一定是该结点的左子节点或者是该结点的左子树的最深的右子树。例如结点8有左子树,它的前驱就是左子节点4的右子结点6的右子节点7。结点4的前驱是左子结点2的右子结点3。结点2的前驱是左子节点1。
- 结点没有左子树,那么它的前驱一定是比它小的第1个父节点或者祖父结点。如果找不到比它小的父节点或者祖父结点,那么,它就没有前驱。例如:结点3没有左子树,就找它的父结点2,2比3小,所以2就是3的前驱节点。结点11,第1个比它小的祖父结点是10,所以10是前驱。结点1,没有左子树,而且父节点和祖父结点里面没有找到比1小的,找到8以后,就没有父节点了。所以,1没有前驱。
后继结点的情况刚好和前驱相反。
根据观察总结可以分析得出,求一个结点的后继的时候,可以分为以下2种情况:
- 结点有右子树。那么,后继一定是它的右子节点或者是它的右子树的最深的左子树。例如结点8有右子树,它的后继就是右子节点13的右子结点10的右子节点9。结点4的后继是右子结点6的左子结点5。结点6的后继是右子节点7。
- 结点没有右子树,那么它的后继一定是比它大的第1个父节点或者祖父结点。如果找不到比它大的父节点或者祖父结点,那么,它就没有后继。例如:结点3没有右子树,就找它的父结点2,2比3小,继续找祖父结点4,比3大,所以4就是3的后继点。结点12,第1个比它大的祖父结点是13,所以13是后继。结点13,没有右子树,而且父节点和祖父结点里面没有找到比13大的,找到8以后,就没有父节点了。所以,13没有后继。
根据以上思路,代码实现如下:
/*** 求前驱结点*/public Node<E> predecessor(Node<E> node) {if (node == null) {return null;}//存在左子树的情况Node<E> p = node.left;if (p != null) {while (p.right != null) {p = p.right;}return p;}//不存在左子树的情况,在父节点或者祖父结点找while (node.parent != null && node == node.parent.left) {node = node.parent;}return node.parent;}/*** 求后继结点*/public Node<E> successor(Node<E> node) {if (node == null) {return null;}//存在右子树的情况Node<E> p = node.right;if (p != null) {while (p.left != null) {p = p.left;}return p;}//不存在右子树的情况. 在父节点或者祖父结点找while (node.parent != null && node == node.parent.right) {node = node.parent;}return node.parent;}
删除
因为二叉树只有3种结点,叶子结点,度为1的结点,度为2的结点。所以删除按照这3种结点进行分类删除:
删除叶子结点。删除叶子结点比较简单,例如上面的二叉树。删除结点1,和7和11,就是删除叶子结点。删除1的时候,只需要 1.parent = null 和 1.parent.left = null 就可以了。删除7就是 7.parent = null 和7.parent.right = null。 删除11 , 11.parent = null 和 11.parent.left = null。 如果只有1个根结点,那么根结点就是叶子结点,那么直接 root = null 就可以了。
删除父节点为空的结点。只有1种情况,就是该结点是根结点。 只需要 root = null。 就可以了。
删除度=1 的结点。例如13和12 ,只需要将父结点的子,指向它的子。它的子的父节点指向,指向它的父即可。删除13,13.parent.right = 13.left 和 13.left.parent = 13.parent。 删除12 : 12.parent.right = 12.left 和12.left.parent = 12.parent.
删除度=2 的结点。可以选择,使用它的前驱或者后继结点来替代。这样可以保持二叉树的整体结构不变。过程是,使用前驱或者后继结点的元素覆盖要删除的结点的元素。 然后删除前驱或者后继结点即可。前驱结点或后继结点一定是度为0或1的结点。
例如结点4,8,10。删除j结点4,结点4.element = 结点4.predecessor.element 和 删除结点4.predecessor。删
除结点8,结点8.element = 结点8.predecessor.element 和 删除结点8.predecessor。 删除结点10,结点
10.element = 结点10.predecessor.element 和 删除结点10.predecessor。这3个例子用的是前驱结点替代删
除的结点,使用后继结点来替代删除的结点也可以。
因为提供对外的删除接口是的参数是元素,不是结点,但是进行删除的时候真正删除的是结点。所以需要写2个方法中转一下,根据元素找到节点和删除结点。
/*** 根据元素返回对应的结点* @param element 元素* @return 元素所在的结点*/public Node<E> node(E element) {Node<E> node = root;while (node != null) {int cmp = compareTo(element, node.element);if (cmp == 0) {return node;}if (cmp > 0) {node = node.right;}else {node = node.left;}}return null;}/*** 删除一个指定的元素*/public void remove(E element){remove(node(element));}/*** 删除一个指定的结点*/private void remove(Node<E> node) {//删除结点的实现}
根据上面的删除的思路,删除结点的方法实现如下:
/*** 删除一个指定的结点*/private void remove(Node<E> node) {if (node == null) {return;}size --;//结点度=2的情况 , 使用前驱来替代的方案if (node.hasTowChildren()) {//找到前驱Node<E> predecessor = predecessor(node);node.element = predecessor.element;//删除前驱. if语句块的外面,就是删除度=0或1的情况。所以直接赋值,在外面就可以删除了。node = predecessor;}//先假设是度=1的情况Node<E> child = node.left == null ? node.right : node.left;if (child != null) {//绝对是度=1 的情况,因为有子结点if (node.parent == null) {//根结点的情况root = child;} else {child.parent = node.parent;if (node == node.parent.left) {//删除的结点是左结点的情况node.parent.left = child;} else {node.parent.right = child;//删除的结点是右结点的情况}}} else {//没有子节点,证明度=0,是叶子结点if (node.parent == null) {//根结点root = null;} else {if (node == node.parent.left) {node.parent.left = null;} else {node.parent.right = null;}}}}
最后补充一下contains接口:
/*** 查找一个元素* @param element 需要查找的元素* @return 存在返回 true ,否则false*/public boolean contains(E element){return node(element) != null;}
导致为止,二叉搜索树就完全实现了。
二叉搜索树的退化
在理想状态下,二叉树的添加、删除、查找结点的算法复杂度都是 O(logn)。
例如上面的二叉树:搜索结点2,9,11,由于是有序的,所以就是搜索的时候相当于二分搜索,所以,算法复杂度是O(logn)。 添加和删除结点也是相同的情况。
但是,如果是从小到大添加结点的话,二叉树可能就会变成如下的情况:

这时候二叉树就变成了链表的形状,搜索、添加、和删除结点,时间复杂度都变成了O(n)。 还有一种情况是,本身二叉树是相对正常的二叉树,但是由于删除结点,将二叉树变成单链的结构。

这些二叉树变成链表的情况,称为二叉树的退化。由树结构退化成了链表结构。
要避免这种情况的话,就要在添加,删除结点的时候,保持二叉搜索树的平衡,左右子树的高度差不能相差太远。保持平衡的二叉树搜索树,就称为平衡二叉搜索树。经典的平衡二叉搜索树有:AVL树,红黑树。
