我们先了解有序数组和链表两种数据结构:有序数组,可以通过二分查找法快速的查询特定的值,时间复杂度为O(logN),可是插入删除时效率低,平均要移动N/2个元素,时间复杂度为O(N)。链表:查询效率低,平均要比较N/2个元素,时间复杂度O(N),插入和删除效率较高,O(1)。二叉树的特点是结合了有序数组和链表的优点,能像有序数组那样快速的查找,又能像链表那样快速的插入和删除。操作二叉搜索树的时间复杂度是O(logN)。
1.定义
二叉树:树中的每个节点最多只能有两个子节点,这样的树是二叉树。
二叉搜索树:一个节点的左子节点的关键字值小于这个父节点,右子节点的关键字值大于等于这个父节点。
平衡二叉搜索树:它是一颗裸空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两棵子树都是平衡二叉树。它的实现方式有:红黑树,AVL
2.结构图
二叉树由节点和边构成,每个节点包含的有关键字值,以及其他数据。接下来我们通过代码看如何对二叉搜索树进行插入,删除,查找,遍历等操作。
3.查找操作
先定义节点Node
public class Node { int key; //key 关键字值 double data; //存储的数据 Node leftChild; //左子节点的引用 Node rightChild; //右子节点的引用 //显示该节点内容 public void displayNode(){ System.out.println("key="+key+",data="+data); } }
定义二叉树Tree
public class Tree { //根节点 public Node root;
}
查找方法:将关键字值key与根节点的关键字值做比较,小于root节点的关键字值,进入root节点的左子树进行比较;否则如果大于,进入root节点的右子树进行比较。依次比较下去,直到key的值等于某个节点的关键字值,则将该节点Node返回。如果没有找到
符合条件的节点,那么返回null
//查询效率和有序数组中的二分查找法一样,时间复杂度为O(log2N) public Node find(int key){ Node current = root; while (current.key != key) { if(key < current.key) current = current.leftChild; else{ current = current.rightChild; } //表示没有找到符合条件的节点 if (null == current) return null; } return current; }
4.插入操作
插入操作,先要确定新节点要插入的位置,这个就是查找的过程;然后确定位置后,将新节点newNode作为父节点parent的的左子节点或者右子节点
//插入方法:查找最后一个不为null的节点parent作为要插入节点的父节点,newNode作为它的左子节点或者右子节点 public void insert(int key,double data){ Node newNode = new Node(); newNode.key = key; newNode.data = data; if(null == root){ root = newNode; }else{ Node current = root; Node parent; while(true){ parent = current; if(key < current.key){ //go left current = current.leftChild; if(null == current){ parent.leftChild = newNode; return; } }else{ //go right current = current.rightChild; if(null == current){ parent.rightChild = newNode; return; } } } } }
5.遍历操作
遍历操作,是指根据特定的顺序访问树的每一个节点。遍历方法有:中序遍历,前序遍历,后序遍历
中序遍历,会使所有节点按照关键字值的升序被访问到。遍历时,通常使用递归调用的方法,初始参数是树的根节点。中序遍历会有三个步骤:
* 调用自身来遍历该节点的左子树
* 访问这个节点
* 调用自身来遍历该节点的右子树
//中序遍历二叉树:按key值的升序排序 public void orderByMiddle(Node localRoot){ if(null != localRoot){ orderByMiddle(localRoot.leftChild); System.out.println(localRoot.data); orderByMiddle(localRoot.rightChild); } } //前序遍历二叉树 public void preOrder(Node localRoot){ if(null != localRoot){ System.out.println(localRoot.data); preOrder(localRoot.leftChild); preOrder(localRoot.rightChild); } } //后序遍历二叉树 public void postOrder(Node localRoot){ if(null != localRoot){ postOrder(localRoot.leftChild); postOrder(localRoot.rightChild); System.out.println(localRoot.data); } }
6.查找最大值和最小值
查找最大值,从根节点开始走向右子节点,然后一直走向右子节点,直到最后一个不为null的右子节点,就是最大值。查找最小值,是类似的,一直走向左子节点。
//查找最大值 public Node maxNum() { Node current,last = null; current = root; while (null != current) { last = current; current = current.rightChild; } return last; } //查找最小值 public Node miniNum() { Node current,last = null; current = root; while (null != current) { last = current; current = current.leftChild; } return last; }
7.删除操作
对于删除操作的逻辑如下:
* 要先判断被删除的节点的情况,然后分别处理,被删除节点可能是:是叶节点,只有一个子节点,有两个子节点
* 如果被删除节点有两个子节点:找到要删除节点的后继节点,然后让后继节点替换该节点。由于二叉搜索树要满足的特性是一个节点的左子节点的key值要比该节点小,右子节点的key值要比该节点大,所以,一个节点的后继节点就是比该节点key值大的最小的那个节点。也就是该节点的右子节点的左子节点,再左子节点,直到最后一个不是null的左子节点,就是该节点的后继节点。找到后继节点后,就将后继节点替换当前节点,涉及到各个节点引用的改变,有四个地方的引用改变,也就是代码中的abcd四个步骤。
如果要删除的节点有两个子节点,看图:
//删除方法 //1.要删除的节点是叶节点 2.只有一个字节点 3.有两个子节点 //步骤:1.先找到要删除的节点 public boolean delete(int key){ //1.先查找到要删除的节点 Node current = root; Node parent = root; //标识被删除的节点是否是左子节点 boolean isLeftChild = true; while(key != current.key){ parent = current; if(key < current.key){ current = current.leftChild; isLeftChild = true; }else{ current = current.rightChild; isLeftChild = false; } //没有找到要删除的节点 if(null == current){ return false; } } //2.如果该节点没有子节点 if(null == current.leftChild && null == current.rightChild){ //判断该节点是否是根节点 if(current == root){ root = null; }else if(isLeftChild) { parent.leftChild = null; }else{ parent.rightChild = null; } //3.如果该节点只有一个子节点:左子节点 }else if(null == current.rightChild){ //被删除的节点如果是根节点 if(current == root){ root = current.leftChild; }else if(isLeftChild){ parent.leftChild = current.leftChild; }else{ parent.rightChild = current.leftChild; } //4.如果该节点只有一个子节点:右子节点 }else if(null == current.leftChild){ if(current == root){ root = current.rightChild; }else if(isLeftChild){ parent.leftChild = current.rightChild; }else{ parent.rightChild = current.rightChild; } //5.如果要删除的节点有两个子节点:需要找到该节点的后继节点代替该节点,后继节点就是key值比当前节点大的那个最小的值 }else{ //先找到后继节点successor Node successor = getSuccessor(current); if(current == root){ root = successor; }else if(isLeftChild){ //c 将后继节点赋值给要删除节点的父节点的左子节点 parent.leftChild = successor; }else{ //c 将后继节点赋值给要删除节点的父节点的右子节点 parent.rightChild = successor; } //d 将要删除节点的左子节点赋值给后继节点的左子节点 successor.leftChild = current.leftChild; } return true; } //查找后继节点,并替换部分引用 a,b 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){ successorParent.leftChild = successor.rightChild; //a 把后继节点的右子节点赋值给后继节点的父节点的左子节点 successor.rightChild = delNode.rightChild; //b 把要删除节点的右子节点赋值给后继节点的右子节点 } return successor; }
8.用数组表示二叉树
其实,除了以上那种方式表示二叉树外,还可以用数组表示二叉树。用数组时,节点存储在数组中,节点再数组中的位置对应于它在树中的位置,通过下图可以理解它的存储方式。
9.重复关键字
由于二叉搜索树的特性是:右子节点的key值大于等于父节点,所以在insert操作中,关键字key值相同的节点插入到与它相同的节点的右子节点处。在查询操作时,获取到的是多个相同节点的第一个节点。
注:以上图片摘自《Java数据结构和算法》这本书