树上的每一个元素所在的节点
最顶部的节点,如1节点,一棵树最多只有一个根节点
一个节点上一层的与该节点连接的节点
一个节点下一层的节点
同一层的节点
连接父节点和子节点的连线
一个节点子树的个数
所有节点度中最大的值
度为0的节点
根节点在第一次,根节点的子节点在第二层,以此类推
从根节点到当前节点唯一路径所经过的节点总数
从当前节点到最远叶子节点所经历的节点总数
所有节点深度中的最大值
所有节点高度中的最大值
一般来说树的高度等于树的深度。
一棵树可以没有任何节点。
假设要在n个动态的整数中搜索某一个数,应该用什么方式?
可以将数据存放在动态数组中,遍历数组进行查找,此时平均时间复杂度为O(n)如果使用二分查找对数组进行查找,此时最坏时间复杂度为O(logn),但是添加和删除元素的平均时间复杂度是O(n)此时需要使用二叉搜索树,添加、删除、查找的最坏时间复杂度都可以优化到O(logn)私有类节点的构造方法只需要传入元素和父节点即可,因为在添加元素时会判断左右节点,给相应的位置赋值。
往二叉搜索树里添加元素时,每一个元素都需要进行比较。这里有两种比较方式:
第一种用到了比较器,可以自定义比较规则,用来比较添加元素的与树中已有元素的大小;第二种则是让相互比较的两个元素都实现Comparable接口,调用该接口的compareTo方法进行比较。在二叉树中的比较方法如下:
public int compare(E e1, E e2){ if (comparator == null){ //如果比较器为null,则说明按照元素自己的比较规则来进行比较 return ((Comparable<E>)e1).compareTo(e2); } //程序到此处说明比较器不为空,则按照比较器中的规则来进行比较 return comparator.compare(e1, e2); }如果使用无参构造创建二叉搜索树,则将传入的元素强转为Comparable类型进行比较(前提是这个元素的类必须实现该接口)。
如果使用有参构造创建二叉搜索树,则需要传入比较器为参数,调用该比较器进行比较,比较器可以使用匿名内部类的方式传入。如下,自定义比较方法,按照名称来进行比较。
BinarySearchTree<User> tree = new BinarySearchTree<>(new Comparator() { @Override public int compare(Object o1, Object o2) { return ((User)o2).getAge() - ((User)o1).getAge(); } });添加的元素不能为空,因此首先调用checkElement方法进行元素检查
然后判断要添加的节点是否为根节点,如果是根节点则让root指向新创建的节点。
如果不是根节点,则使用while循环让该元素与树中节点中的元素进行比较
先声明两个变量parent和node
parent用来保存while循环中的父节点,以用于后面给父节点的left或者right赋值。每次循环中的都是parent都与node相等
node用来表示树中用于比较的节点。
当node为空时,说明该位置没有子树了,元素应该被放在这个位置,此时while循环结束。
新建一个元素,指定parent为循环中保存的parent
还应声明一个result变量,用来记录要往左子树还是右子树添加元素。
分析:
分为三种情况,删除度为0,1,2的节点。
先说删除度为0的节点,也就是叶子节点
只需要让该节点的父节点的right或者left指向null即可。
注意:如果父节点为null,则说明删除的是根节点,也是叶子节点,只需要让root=null即可。
删除度为1的节点
先判断该节点是父节点的左节点还是右节点,然后让该节点的父节点的right或者left指向该节点的子节点。
删除度为2的节点
一个节点的度为2,在二叉搜索树中,该节点左边的所有节点的值都小于该节点,右边的所有节点的值都大于该节点,因此只要找到该节点的前驱节点或者后继节点,将前驱/后继节点中的值赋给要删除的节点,然后删除前驱/后继节点即可。
前驱节点:中序遍历中,一个节点的前一个节点
后继节点:中序遍历中,一个节点的后一个节点
前驱节点怎么获得:
将该节点前面的所有节点都遍历到,其中遍历到的最后一个节点就是前驱节点。
如果左子树不为空,那么左子树中最大的节点就是左子树中最右边的节点。
node.left.right.right…
如果左子树为空,那么就找比他小的父节点
如本图中,找9的前驱节点,9是10的左子树,10是13的左子树,证明这些数字都比9大,因此当遇到比9小的父节点时就是9的前驱节点,13的父节点为8,13是8的右子树,因此当某一个祖父节点是其父节点的右子树时,这个父节点就是要找的前驱节点。
代码:
private Node<E> predesessor(Node<E> node){ //节点为空直接返回null if (node == null) return null; //第一种情况,左子树不为空 if (node.left != null){ node = node.left; while (node.right != null){ node = node.right; } return node; } while (node.parent != null && node != node.parent.right){ node = node.parent; } return node.parent; }后继节点的获得方式刚好于前驱节点相反
private Node<E> successor(Node<E> node){ if (node == null) return null; if (node.right != null){ node = node.right; while (node.left != null){ node = node.left; } return node; } while (node.parent != null && node != node.parent.left){ node = node.parent; } return node.parent; }删除方法的代码实现
public void remove(E element){ if (element == null) return; size--; remove(node(element)); }对外提供的方法,在内部调用了私有的删除方法,因为删除涉及到节点,节点对外界来说是不可见的。
private Node<E> node(E element){ Node<E> node = root; while (node != null){ int result = compare(element, node.element); if (result == 0) { return node; }else if (result < 0){ node = node.left; }else { node = node.right; } } return null; }在这里先根据传入的元素获取节点,根据元素大小的比较来找到节点。
private void remove(Node<E> node){ if (node == null) return; if (node.right != null && node.left != null){ //说明要删除度为2的节点 //找到前驱或者后继节点 Node<E> pre = predesessor(node); node.element = pre.element; node = pre; } //程序走到这里说明有两种可能: //1.要删除的节点度为1或者0 //2.要删除的节点度为2,前面已经将要删除的节点替换为前驱节点,这里只需要删除前驱节点即可,前驱节点的度只能为1或者0 //因此代码可以放到一起 //这里用来判断应该用要删除的节点到底存在左节点还是右节点,又或者是没有节点, //如果有左或者右节点,那么用左/右节点取代原来的节点如果没有节点,那么不管三目运算符取哪一个结果,这个替换节点都为null Node<E> replacement = node.right == null ? node.left : node.right; if (replacement == null){ //说明要删除的节点为叶子节点 if (node.parent == null){ //说明要删除的节点为根节点,并且是叶子节点 root = null; }else if (node == node.parent.right){ node.parent.right = null; }else { node.parent.left = null; } }else { //说明度为1,让替换节点的parent指向要删除节点的parent replacement.parent = node.parent; if (node.parent == null){ //说明该节点为根节点 root = replacement; }else if(node == node.parent.left){ node.parent.left = replacement; }else { node.parent.right = replacement; } } }先访问根节点的值,再访问左右子树
public void preorderTraver(Visitor visitor){ preorderTraver(root, visitor); } /** * 前序遍历的实现,使用递归 * @param node */ private void preorderTraver(Node<E> node, Visitor visitor){ if (node == null || visitor == null) return; //前序首先访问根节点 visitor.visit(node.element); //访问左右子树 preorderTraver(node.left, visitor); preorderTraver(node.right, visitor); }先访问左子树上的节点,再访问根节点,最后访问右节点
/** * 中序遍历,按照从小到大的顺序 */ public void inorderTraver(Visitor visitor){ inorderTraver(root, visitor); } private void inorderTraver(Node<E> node, Visitor visitor){ if (node == null || visitor == null) return; //中序遍历首先访问左子树 inorderTraver(node.left, visitor); //然后访问自己 visitor.visit(node.element); //最后访问右子树 inorderTraver(node.right, visitor); }先访问左右子树,然后再访问根节点
public void postorderTraver(Visitor visitor){ postorderTraver(root, visitor); } /** * 后序遍历的实现,使用递归 * @param node */ private void postorderTraver(Node<E> node, Visitor visitor){ if (node == null || visitor == null) return; //前序首先访问左右树 postorderTraver(node.left, visitor); postorderTraver(node.right, visitor); //再访问根节点 visitor.visit(node.element); }按照从上到下从左到右一层一层的方式进行遍历。
这里需要使用到队列,用到了先进先出的原则。
首先让根节点入队,然后进行访问时让根节点出队,获取根节点的值,最后让根节点的左右节点入队,使用循环的方式循环此操作,每当一个节点出队后就将该节点的左右节点加入到队列中去。
public void LevelOrderTraver(Visitor visitor){ if (visitor == null || root == null) return; //使用while循环以及队列来访问每一层的元素 Node<E> node = root; Queue<Node<E>> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()){ Node<E> eNode = queue.poll(); visitor.visit(eNode.element); if (eNode.left != null) queue.offer(eNode.left); if (eNode.right != null) queue.offer(eNode.right); } }如果要想让外界使用遍历的结果,我们在这里应该给外界定义一个接口使用,让外界自定义遍历结果的处理方法。
/** * 对外提供遍历的接口,支持前中后层四种遍历方式 */ public static interface Visitor<E>{ //外界调用者可以自定义对遍历结果的处理方式 void visit(E element); }当外界需要进行遍历时,需要实现该接口,自己实现方法来自定义处理方式。
每一种遍历方式都有一个visitor参数
public void LevelOrderTraver(Visitor visitor) public void postorderTraver(Visitor visitor) public void inorderTraver(Visitor visitor) public void preorderTraver(Visitor visitor)将每一个遍历结果交给visit方法进行处理。
visitor.visit(eNode.element);需求:
有时候只需要遍历到某一个元素就停止遍历,这时候不需要遍历所有元素,该如何实现这个需求呢?
将Visitor接口变为抽象类定义一个布尔类型成员变量stop,并且将visit的返回值类型变为boolean在重写visit方法时可以根据需求改变返回值为true或者false在每种前中后序三种遍历方法中使用成员变量stop接收visit方法的返回值在每次执行visit方法前根据stop的值判断是否停止遍历。在每次进行递归调用前根据stop的值判断是否停止递归。改进之后的代码:
public static abstract class Visitor<E>{ //定义一个成员遍历,当stop为true时,停止遍历 boolean stop; //外界调用者可以自定义对遍历结果的处理方式 abstract boolean visit(E element); } /** * 前序遍历对外提供的方法 */ public void preorderTraver(Visitor visitor){ preorderTraver(root, visitor); } /** * 前序遍历的实现,使用递归 * @param node */ private void preorderTraver(Node<E> node, Visitor visitor){ if (node == null || visitor == null || visitor.stop) return; //前序首先访问根节点 visitor.stop = visitor.visit(node.element); //访问左右子树 preorderTraver(node.left, visitor); preorderTraver(node.right, visitor); } /** * 中序遍历,按照从小到大的顺序 */ public void inorderTraver(Visitor visitor){ inorderTraver(root, visitor); } private void inorderTraver(Node<E> node, Visitor visitor){ if (node == null || visitor == null || visitor.stop) return; //中序遍历首先访问左子树 inorderTraver(node.left, visitor); if (visitor.stop) return;//如果上一个递归中stop变为了true,那么下面就不应该再进行元素的打印了 //然后访问自己 visitor.stop = visitor.visit(node.element); //最后访问右子树 inorderTraver(node.right, visitor); } /** * 后序遍历对外提供的方法 */ public void postorderTraver(Visitor visitor){ postorderTraver(root, visitor); } /** * 后序遍历的实现,使用递归 * @param node */ private void postorderTraver(Node<E> node, Visitor visitor){ if (node == null || visitor == null || visitor.stop) return; //前序首先访问左右树 postorderTraver(node.left, visitor); postorderTraver(node.right, visitor); //再访问根节点 if (visitor.stop) return;//如果上一个递归中stop变为了true,那么下面就不应该再进行元素的打印了 visitor.stop = visitor.visit(node.element); } /** * 层序遍历,使用迭代的方式 * @param visitor */ public void LevelOrderTraver(Visitor visitor){ if (visitor == null || root == null) return; //使用while循环以及队列来访问每一层的元素 Node<E> node = root; Queue<Node<E>> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()){ Node<E> eNode = queue.poll(); boolean stop = visitor.visit(eNode.element); if (stop) return; if (eNode.left != null) queue.offer(eNode.left); if (eNode.right != null) queue.offer(eNode.right); } }