引入:
二叉搜索树是这样的一种二叉树:
(1)每个元素都有一个关键值,并且没有任意两个元素有相同的关键值
(2)根节点的左子树中任意元素的关键值小于根节点的关键值。
(3)根节点的右子树中任意元素的关键值大于根节点的关键值。
(4)根节点的左右子树也是二叉搜索树。
我们这里就用程序来实现这样一颗二叉搜索树。
分析:
从定义看出,二叉搜索树是一种特殊的二叉树,它给每个元素加上了序的概念,但又不同于最大最小堆,它总是 左<根<右的。我们分别看常用的几个操作。
查找:
关键值查找很简单,就是从根元素开始遍历,如果找到匹配的,则直接将其对应元素的值返回。
如果关键值比当前查找元素的关键值小,那么关键值一定在左子树中,所以递归的在左子树中查找。
如果关键值比当前查找元素的关键值大,那么关键值一定在右子树中,所以递归的在右子树中查找。
上述递归跳出的条件就是当前查找元素为空,一旦为空都找不到匹配,则说明该二叉搜索树中没有匹配值。
插入:
插入之前必须保证二叉搜索树的第一条定义,所以先找下是否有匹配元素,如果有,则不执行插入动作。
然后从根开始折半找,根据子元素的值来确定这个值插入左边还是右边,直到选择到一个精确的位置,使得待插元素的关键值刚好落在当前元素和其某个子元素之间(或者没有子元素)
删除:
删除复杂多了,先遍历找到要删除的元素,
分为3种情况:
1.如果该元素左,右子树都不为空,则吧该元素替换为其左子树中的最大元素或者右子树的最小元素(因为这2个元素刚好是与当前元素最近的2个元素)
2.如果该元素左,右子树都为空( 也就是叶节点),那么直接丢弃,当然也要考虑根节点的情况。
3.如果该元素左或者右子树不为空,那么需要找到该元素的父亲元素,然后把其不为空的这个分支的根元素设为父亲元素的直接子元素,当然也要考虑根节点的情况。
代码:
我们写了一个这样的类,很详细的注释,尤其删除部分,的确很复杂:
package com.charles.algo.binarysearchtree; import com.charles.algo.binarytree.BinaryTreeNode; /** * 这个类是二叉搜索树的定义 * @author charles.wang * */ public class BinarySearchTree{ //根节点 private BSTreeNode rootNode; public BinarySearchTree(){ rootNode = null; } /** * 返回关键字匹配的节点的值 */ public V search (int key){ //如果根节点为空,那么这个二叉搜索树为空,肯定无匹配值 if(rootNode==null) return null; //如果根节点不为空,那么从根开始比较,因为二叉搜索树都是保持了左<根<右的结构,所以很快能找到匹配值 BSTreeNode currentNode = rootNode; while (currentNode!=null){ //如果当前节点的key刚好和请求的key匹配,那么则返回当前节点的值 if( key==currentNode.getKey()) return currentNode.getValue(); //如果当前的节点的key和请求的key不匹配,那么进行比较 //如果传入的key比当前节点的key小,那么匹配的值(如果有),必定在当前节点的左子搜索树中 //吧待搜节点设为当前节点的左子节点 else if ( key< currentNode.getKey()) currentNode=currentNode.getLeftChild(); //如果传入的key比当前的节点的key大,那么匹配的值(如果有),必定在当前节点的右子搜索树中 //把待搜节点设为当前节点的右子节点 else currentNode=currentNode.getRightChild(); } //如果跳出了while循环还没找到匹配的值,说明在这个搜索树中找不到,返回null return null; } /** * 插入一个节点到二叉搜索树中 * @param key * @param value */ public void insertNode( int key, V value){ //先搜索二叉搜索树是否已经有相同的节点,如果有,则不执行插入动作 if (search(key)!=null) return; BSTreeNode newNode= new BSTreeNode (null,null,key,value); //假设是二叉搜索树还没有任何内容,那么该赋值给根节点 if(rootNode==null){ rootNode = newNode; } //假设二叉搜索树已经存在,那么从根开始,找到适合其插入的位置 //如果判断某个位置是否是其可以插入? //方法是,从根开始标记为当前节点,如果新节点的key比当前节点的key小,那么说明插在当前节点的左子树中 //这时候,如果左节点为空,那么新节点就作为当前节点的左边节点 // 如果左节点不为空,那么比较左节点的key和新节点的key, // 如果新节点的key大于左节点的key,那么吧新节点插入到左节点和当前节点之间,退出循环 // 如果新节点的key小于左节点的key,那么吧当前节点设为当前节点的左节点。 //右节点处理与上述类似 else{ BSTreeNode currentNode= rootNode; //比较新节点的key和当前节点的key while(currentNode!=null){ //插入到左子树中 if(key< currentNode.getKey()){ //当前节点的左节点为空,那么找到了合适位置 if(currentNode.getLeftChild()==null){ currentNode.setLeftChild(newNode); break; } //当前节点的左节点不为空,则比较左节点的key和新节点的key else{ //如果新节点key大于当前节点的左节点的key,则说明新节点的key刚好位于当前节点的左节点和当前节点之间 //这是一个合适的插入位置 if (key > currentNode.getLeftChild().getKey()){ newNode.setLeftChild(currentNode.getLeftChild()); currentNode.setLeftChild(newNode); break; } //如果新节点key比当前节点左节点还小,那么说明适合插入的位置位于当前节点的左搜索子树中,于是移动当前节点 //到左子节点作为新的比较的基准 else{ currentNode=currentNode.getLeftChild(); } } } //插入到右子树中 else { //如果当前节点的右节点为空,那么找到了合适的位置 if( currentNode.getRightChild()==null){ currentNode.setRightChild(newNode); } //如果当前节点的右节点不为空,那么比较右节点的key和新节点的key else{ //如果新节点的key小于当前节点的右节点的key,则说明新节点的key刚好位于当前节点和当前节点的右节点之间 //这是一个合适的插入位置 if(key < currentNode.getRightChild().getKey()){ newNode.setRightChild(currentNode.getRightChild()); currentNode.setRightChild(newNode); break; } //如果新节点的key比当前节点的右边节点还大,说明适合插入的位置位于当前节点的右搜索子树中,于是移动当前节点 //到右子节点作为新的比较的基准 else { currentNode = currentNode.getRightChild(); } } } } } } /** * 删除指定key的节点 * @param key */ public void deleteNode(int key){ //如果二叉搜索树中没有对应key的节点,那么不执行删除操作 if(search(key)==null) return; //否则,肯定有key节点,首先定位到这个节点 BSTreeNode currentNode = rootNode; BSTreeNode currentNodeParentNode= null; //当前节点的父亲节点 while( currentNode.getKey()!=key){ //如果要删除的节点key比当前节点的key小,那么说明要删除的节点在左子树中 if(key maxNode = currentNode.getLeftChild(); //这变量存放了要找的最大节点 BSTreeNode maxNodeParentNode= currentNode; //这变量存放了要找的最大节点的父节点 while(maxNode.getRightChild()!=null){ //向右移动 maxNodeParentNode=maxNode; maxNode= maxNode.getRightChild(); } //吧最大节点插入到currentNode中,从而维持了原有的结构不变 currentNode.setKey(maxNode.getKey()); currentNode.setValue(maxNode.getValue()); //删除左子树的原最大节点 maxNodeParentNode.setRightChild(null); return ; } //情况2,如果当前节点既没有左节点又没有右节点,则直接丢弃 if(( currentNode.getLeftChild()==null) && (currentNode.getRightChild()==null) ){ //如果当前节点为根节点,那么删除根节点 if(currentNode==rootNode){ rootNode=null; } //如果当前节点不为根节点,那么必定有父亲节点,这时候只要丢弃当前节点就可以了,判断是判断当前节点是父亲节点的左儿子还是右儿子 if(currentNode==currentNodeParentNode.getLeftChild()){ currentNodeParentNode.setLeftChild(null); } else currentNodeParentNode.setRightChild(null); return; } //情况3,如果当前节点只有左节点或者右节点中的1个,那么吧儿子提升上来 else { //如果只有左节点 if( currentNode.getLeftChild()!=null){ //如果当前节点为根节点,那么吧左节点提升上来当根节点 if(currentNode==rootNode){ rootNode=currentNode.getLeftChild(); } //如果当前节点不为根节点,那么必定有父节点,则判断当前节点是父亲节点的左儿子还是右儿子,并且用左节点值取代之 else if(currentNode==currentNodeParentNode.getLeftChild()){ currentNodeParentNode.setLeftChild(currentNode.getLeftChild()); } else currentNodeParentNode.setRightChild(currentNode.getLeftChild()); } //如果只有右节点 else{ //如果当前节点为根节点,那么吧右节点提升上来当根节点 if(currentNode==rootNode){ rootNode=currentNode.getRightChild(); } //如果当前节点不为根节点,那么必定有父节点,则判断当前节点是父亲节点的左儿子还是右儿子,并且用右节点值取代之 if(currentNode==currentNodeParentNode.getLeftChild()){ currentNodeParentNode.setLeftChild(currentNode.getRightChild()); } else currentNodeParentNode.setRightChild(currentNode.getRightChild()); } } } /** * 因为二叉搜索树中,左<根<右,所以我们可以用中序遍历的方法来打印出所有的节点 */ public void printAllNodes(){ midOrder(rootNode); } /** * 中序遍历某节点,它会先打印左边节点,再打印当前节点,最后打印出右边节点 * @param args */ private void midOrder(BSTreeNode currentNode){ //当前节点如果是null,则不打印出当前节点,并且退出递归 if(currentNode!=null){ midOrder(currentNode.getLeftChild()); System.out.print(currentNode.getValue()+" "); midOrder(currentNode.getRightChild()); } } }
下面我们来验证,我们先构造一组强度彼此不同的人,吧他们放入二叉搜索树,接着我们来演示查找还有删除操作是否每次删除都维持着二叉搜索树中顺序的关系:
public static void main(String[] args){ //构造一颗二叉搜索树,其中key为每个人的能力强度(假设每个人的个人能力都不同) ,value为每个人的名字 //Charles:1000 //Jack: 422 //Penny: 635 //Jude: 95 //John: 23 //Golden 768 //Bull: 79 BinarySearchTreebsTree = new BinarySearchTree (); bsTree.insertNode(1000, "Charles"); bsTree.insertNode(422,"Jack"); bsTree.insertNode(635, "Penny"); bsTree.insertNode(95, "Jude"); bsTree.insertNode(23, "John"); bsTree.insertNode(768, "Golden"); bsTree.insertNode(79, "Bull"); //测试二叉搜索树的搜索 System.out.println("测试在二叉搜索树中搜索."); int powerKey1 = 422; String powerValue1 = bsTree.search(powerKey1); if(powerValue1==null) System.out.println("找不到匹配强度为:"+powerKey1+"的人."); else System.out.println("强度为"+powerKey1+"的人名字叫:"+powerValue1); int powerKey2 = 420; String powerValue2 = bsTree.search(powerKey2); if(powerValue2==null) System.out.println("找不到匹配强度为:"+powerKey2+"的人."); else System.out.println("强度为"+powerKey2+"的人名字叫:"+powerValue2); System.out.println(); //测试打印出所有二叉搜索树的节点,升序 System.out.println("测试二叉搜索树中各个节点是否有序排列了"); System.out.print("所有人强度按照升序排名为:"); bsTree.printAllNodes(); System.out.println(); //测试二叉搜索树的删除,看每次删除是否还维持二叉搜索树的结构 System.out.println("删除 Jude:"); bsTree.deleteNode(95); System.out.println("删除后所有人强度升序排名为:"); bsTree.printAllNodes(); System.out.println(); System.out.println("删除 Charles:"); bsTree.deleteNode(1000); System.out.println("删除后所有人强度升序排名为:"); bsTree.printAllNodes(); }
最后根据结果,显然我们数据结构是正确的: