目录
0 二分搜索
1 二分搜索树
2 插入元素
3 查找元素
4 二分搜索树的遍历---深度优先
5 二分搜索树的层序遍历---广度优先
6 二分搜索树删除一个节点
1 二分搜索
这部分代码已经在数组中给出了详细的介绍,此处只给出实现代码:
//在一个有效整数数组中根据二分法查找一个整数 public int binarySearch(int[] arr,int target){ //在[left...r]的区间范围内寻找target int left = 0,r = arr.length-1; //当left==r时,区间仍然是有效的只不过此时区间中只有一个元素 while (left <= r){ //防止溢出的求中值 int mid = left+(r-left)/2; if (arr[mid] == target){ return mid; } if (target > arr[mid]){ //target在[mid+1...r]中 left = mid + 1; }else{ //target在[left...mid-1]中 r = mid - 1; } } //当搜索不到时就返回-1 return -1; }
在此处给出二分查找的递归版本,递归版本要比循环的性能上要稍微差一点,但整体上还是一个级别的时间复杂度的,都是O(logn)级别:
// 二分查找法的递归版本 public static int find(Comparable[] arr, Comparable target) { return find(arr, 0, arr.length-1, target); } private static int find(Comparable[] arr, int left, int r, Comparable target){ if (left > r){ //当查不到元素时就返回-1 return -1; } int mid = left + (r-left)/2; if (arr[mid].compareTo(target) == 0){ return mid; }else if (arr[mid].compareTo(target) > 0){ return find(arr,left,mid - 1,target); }else { return find(arr,mid+1,r,target); } }
注:上面的查找都是考虑到数组中没有重复元素的情况,如果有重复元素那么返回的数值索引可能会不一样,因此有另外两个函数floor和ceil分别查找第一次出现的索引和最后一次出现的索引。
2 树的基本概念
(1)二叉树
每个结点至多拥有两棵子树(即二叉树中不存在度大于2的结点),并且,二叉树的子树有左右之分,其次序不能任意颠倒。
(2)二叉树的性质
(1)若二叉树的层次从0开始,则在二叉树的第i层至多有2^i个结点(i>=0)。
(2)高度为k的二叉树最多有2^(k+1) - 1个结点(k>=-1)。 (空树的高度为-1)
(3)对任何一棵二叉树,如果其叶子结点(度为0)数为m, 度为2的结点数为n, 则m = n + 1。
(3)完美二叉树(满二叉树)
一个深度为k(>=-1)且有2^(k+1) - 1个结点的二叉树称为完美二叉树。
(4)完全二叉树
完全二叉树从根结点到倒数第二层满足完美二叉树,最后一层可以不完全填充,其叶子结点都靠左对齐。
3 二分搜索树
二分搜索树一般查用于查找表的实现,即字典数据结构:
优势:不仅可查找数据;还可以高效地插入,删除数据,还可以方便维护数据之间的关系。那么对于查找表这种数据结构,其底层采用不同的实现形式,所带来的性能也是不同的,具体性能如下图所示:
二分搜索树的定义:是一个二叉树,每个节点的键值大于左孩子,小于右孩子。二分搜索树与堆不同,不一定是完全二叉树,因此底层不容易直接用数组表示故采用链表来实现二分搜索树。对于具体二分搜索树可以看如下图所示:
对于实际问题中,需要存储的还有value值,但是在分析中一般更重要的是分析key值,在上图中也是只存储了key值并不体现value值。在二分搜索树中对于前中后序其本质在于位置的不同,与遍历的次数是没有关系的,在一次遍历中也可以有多种遍历方式。
2 插入元素
如下图中,想要把元素60插入树中,首先要比较的就是根节点,发现元素比根节点大则应插入在根节点的右侧
继续比较发现58比60小那么应该在58的右侧,因为58右侧无节点便可以直接插入了。
当插入元素小时如下图,当插入28时,因为28小,所以应该在41的左节点侧
因为28又比22大因此会在22右子树中,继续比较28与33,因为28小所以应该在33的左子树中
33无子节点了,因此便可以插入了28,最终插入效果如下:
在插入节点时还有一种特殊情况即插入的元素和树中的值相等,此时应该用插入的元素直接覆盖掉旧元素即可:
代码如下:
// 二分搜索树 // 由于Key需要能够进行比较,所以需要extends Comparable<Key> public class BST<Key extends Comparable<Key>, Value> { // 树中的节点为私有的类, 外界不需要了解二分搜索树节点的具体实现 private class Node { private Key key; private Value value; private Node left, right; public Node(Key key, Value value) { this.key = key; this.value = value; left = right = null; } } private Node root; // 根节点 private int count; // 树种的节点个数 // 构造函数, 默认构造一棵空二分搜索树 public BST() { root = null; count = 0; } // 返回二分搜索树的节点个数 public int size() { return count; } // 返回二分搜索树是否为空 public boolean isEmpty() { return count == 0; } // 向二分搜索树中插入一个新的(key, value)数据对 public void insert(Key key, Value value){ root = insert(root, key, value); } // 向以node为根的二分搜索树中, 插入节点(key, value), 使用递归算法 // 返回插入新节点后的二分搜索树的根 private Node insert(Node node, Key key, Value value){ if( node == null ){ count ++; return new Node(key, value); } //如果相等便更新数据 if( key.compareTo(node.key) == 0 ){ node.value = value; } else if( key.compareTo(node.key) < 0 ){ node.left = insert( node.left , key, value); } else { // key > node->key node.right = insert( node.right, key, value); } return node; } }
3 查找元素
对于二分搜索树的查找元素其实和插入部分元素相同的,其思路是基本一致的,在这里直接给出代码:
// 查看二分搜索树中是否存在键key public boolean contain(Key key){ return contain(root, key); } // 在二分搜索树中搜索键key所对应的值。如果这个值不存在, 则返回null public Value search(Key key){ return search( root , key ); } // 查看以node为根的二分搜索树中是否包含键值为key的节点, 使用递归算法 private boolean contain(Node node, Key key){ //如果最终查不到返回false if( node == null ) { return false; } // 查找到了 if( key.compareTo(node.key) == 0 ){ return true; } else if( key.compareTo(node.key) < 0 ){ // 当前key小于节点便在左侧查找 return contain( node.left , key ); } else{// key > node->key // 当前key小于节点便在右侧查找 return contain( node.right , key ); } } // 在以node为根的二分搜索树中查找key所对应的value, 递归算法 // 若value不存在, 则返回NULL private Value search(Node node, Key key){ if( node == null ){ return null; } if( key.compareTo(node.key) == 0 ){ return node.value; } else if( key.compareTo(node.key) < 0 ){ return search( node.left , key ); } else {// key > node->key return search( node.right, key ); } }
4 二分搜索树的遍历---深度优先
对树中一个节点的遍历其图示如下(前序遍历),可以看出相当于父节点其实是访问了三次的,第一次来到父节点,然后访问左子树,再回到父节点,访问右子树,再回到父节点。
对于前中后序遍历其代码如下:
// 二分搜索树的前序遍历 public void preOrder(){ preOrder(root); } // 二分搜索树的中序遍历 public void inOrder(){ inOrder(root); } // 二分搜索树的后序遍历 public void postOrder(){ postOrder(root); } // 对以node为根的二叉搜索树进行前序遍历, 递归算法 private void preOrder(Node node){ if( node != null ){ // 输出遍历的节点值 System.out.println(node.key); preOrder(node.left); preOrder(node.right); } } // 对以node为根的二叉搜索树进行中序遍历, 递归算法 private void inOrder(Node node){ if( node != null ){ inOrder(node.left); System.out.println(node.key); inOrder(node.right); } } // 对以node为根的二叉搜索树进行后序遍历, 递归算法 private void postOrder(Node node){ if( node != null ){ postOrder(node.left); postOrder(node.right); System.out.println(node.key); } }
从上面的代码中也可以看出,三种遍历方式其本质都是一样的,只不过在遍历时其输出的语句的值的位置不同而已。
5 二分搜索树的层序遍历---广度优先
层序遍历需要借助另一种数据结构即:队列
在树中遍历到节点时便把元素入队如28,当队列中元素不为空时便弹出对首元素,在弹出后把该节点的左右子节点入队。
继续弹出队首元素16,然后再把16的左右子节点入队,此时整个队列如上图所示。接着把30出队,再把30的左右子节点入队,依次进行下去即可。在遍历到13这些叶子节点时其实还是重复的上面的操作,只不过是叶子节点没有子节点了,所以不会有新元素入队。其层序遍历如下图所示:
// 二分搜索树的层序遍历 public void levelOrder(){ // 队列的底层采用链表实现 Queue<Node> q = new LinkedList<Node>(); q.add(root); while( !q.isEmpty() ){ // 当队列不为空时首先队首元素出队 Node node = q.remove(); // 输出节点值 System.out.println(node.key); // 左右子节点入队 if( node.left != null ){ q.add( node.left ); } if( node.right != null ){ q.add( node.right ); } } }
层序遍历有一个非常好的优点便是其时间复杂度为O(n)。
6 二分搜索树删除一个节点
找到一个节点,然后删除这是简单的,问题的关键在于当删除了节点后如何还能保持二分搜索树的性质。直接讨论删除比较困难,先从最简单的入手如:删除二分搜索树中的最大值和最小值节点。因为二分搜索树的性质为每个节点值大于左孩子小于右孩子,因此不断地进行左侧递归遍历,直至再无左子树便是整个二分搜索树中最小值;对于最大值也是类似,便是不断进行右侧递归遍历。对于如下图所示的二分搜索树很简单,只需要直接删除最小元素即可了。
当但一个二分搜索树为下图情况时,便不是直接删除节点22了,删除之后如何处理剩下的元素。
根据二分搜索树的性质,虽然22的右子树节点一定比22大,但也一定比22的父亲节点要小,因此可以直接把右子树直接向上移动即可。
同样的删除最大值也是类似的思路,如下图中删除最大值,则需要把左子树节向上移动填充即可。
代码如下:
// 返回以node为根的二分搜索树的最小键值所在的节点 private Node minimum(Node node){ if( node.left == null ){ return node; } // 不断进行左侧递归遍历 return minimum(node.left); } // 返回以node为根的二分搜索树的最大键值所在的节点 private Node maximum(Node node){ if( node.right == null ){ return node; } // 不断进行右侧递归遍历 return maximum(node.right); } // 删除掉以node为根的二分搜索树中的最小节点,返回删除节点后新的二分搜索树的根 private Node removeMin(Node node){ // 当再无左节点时便可以删除了 if( node.left == null ){ // 返回的是当前节点的右节点 Node rightNode = node.right; // 让当前节点的右节点为空,相当于释放了内存,便于回收 node.right = null; count --; return rightNode; } node.left = removeMin(node.left); return node; } // 删除掉以node为根的二分搜索树中的最大节点,返回删除节点后新的二分搜索树的根 private Node removeMax(Node node){ if( node.right == null ){ Node leftNode = node.left; node.left = null; count --; return leftNode; } node.right = removeMax(node.right); return node; }
从上面的删除最大最小值可以看出,最大值只有左节点,最小值只有右节点。因此可以进行推广删除一个只有左节点或者只有右节点的节点。那么如何删除一个有左右子节点的节点,其图示如下:
待删除的节点是58,那么应该找一个节点去填充位置,因为二分搜索树的性质右子树的节点都大于左子树的节点,父节点要大于左子树小于右子树,所以填充的节点应该是右子树中的最小值,即59。 删除部分代码如下:
// 二分搜索树 // 由于Key需要能够进行比较,所以需要extends Comparable<Key> public class BST<Key extends Comparable<Key>, Value> { // 树中的节点为私有的类, 外界不需要了解二分搜索树节点的具体实现 private class Node { private Key key; private Value value; private Node left, right; public Node(Key key, Value value) { this.key = key; this.value = value; left = right = null; } // 进行节点的赋值,为了解决successor中指向为空的问题 public Node(Node node){ this.key = node.key; this.value = node.value; this.left = node.left; this.right = node.right; } } private Node root; // 根节点 private int count; // 树种的节点个数 // 构造函数, 默认构造一棵空二分搜索树 public BST() { root = null; count = 0; } // 删除掉以node为根的二分搜索树中键值为key的节点, 递归算法,返回删除节点后新的二分搜索树的根 Node remove(Node node, Key key){ if( node == null ){ return null; } if( key.compareTo(node.key) < 0 ){ node.left = remove( node.left , key ); return node; } else if( key.compareTo(node.key) > 0 ){ node.right = remove( node.right, key ); return node; } else{ // 前面的两个判断其实就是一个寻找的过程 // 待删除节点左子树为空的情况,与删除最小值相同 // 当该节点左右孩子都为空时其实是进入了这个if中 if( node.left == null ){ Node rightNode = node.right; node.right = null; count --; return rightNode; } // 待删除节点右子树为空的情况,与删除最大值相同 if( node.right == null ){ Node leftNode = node.left; node.left = null; count--; return leftNode; } // 待删除节点左右子树均不为空的情况 // 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点,用这个节点顶替待删除节点的位置 Node successor = new Node(minimum(node.right)); // 多了一个新节点所以count要自增 count ++; // 删除节点右子树中的最小值。在此处要注意上面是把待删除节点右子树的最小值赋值给sccessor,当删除最小值后suceessor也就指向空了 // 因此要用new 而不是直接赋值 successor.right = removeMin(node.right); successor.left = node.left; // 让node的左右节点都为空 node.left = node.right = null; // 此时node已经被删除了,因此count要自减 count --; return successor; } } }
注:要注意当数据是递增的时候二分搜索树就会退化成一个链表,此时时间复杂度会很高,因此才有了后面的红黑树、平衡二叉树等
0