darlingtangli 2019-06-28
二叉查找树(Binary Search Tree),也称为二叉搜索树、有序二叉树(ordered binary tree)或排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:
二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低,为O(log n)。中序遍历二叉查找树可得到一个关键字的有序序列,一个无序序列可以通过构造一棵二叉查找树变成一个有序序列,构造树的过程即将无序序列中元素逐个插入到二叉查找树的过程。每次插入的新的结点都是二叉查找树上新的叶子结点,在进行插入操作时,不必移动其它结点,只需改动某个结点的指针,由空变为非空即可。搜索、插入、删除的复杂度等于树高,期望 O(log n),最坏O(n)(数列有序,树退化成线性表)。
下面构造的二叉查找树因为要实现选择(select)和排名(rank)两个操作,所以在Node中新增加了一个int类型字段size,表示该结点所作为根节点所代表的树当中所有结点的个数(包括其本身),则对任意结点总有size(x)=size(x.left)+size(x.right)+1
(1代表的是x结点本身),当我们后面在进行插入、删除相关操作的都要考虑到了对N的进行更新。还设置了size函数来返回size,如下所示
public int size() { return size(root); } // return number of key-value pairs in BST rooted at x private int size(Node x) { if (x == null) return 0; else return x.size; }
//Returns the value associated with the given key. public Value get(Key key) { if (key == null) throw new IllegalArgumentException("calls get() with a null key"); return get(root, key); } private Value get(Node x, Key key) { if (x == null) return null; int cmp = key.compareTo(x.key); if (cmp < 0) return get(x.left, key); else if (cmp > 0) return get(x.right, key); else return x.val; }
查找操作的递归实现:如果树是空的,则查找未命中;如果被查找的键与根节点的键相等,查找命中。如果被查找的键较小就选择在左子树中(递归地)继续查找,较大则选择右子树。
public void put(Key key, Value val) { if (key == null) throw new IllegalArgumentException("calls put() with a null key"); root = put(root, key, val); } private Node put(Node x, Key key, Value val) { if (x == null) return new Node(key, val, 1); int cmp = key.compareTo(x.key); if (cmp < 0) x.left = put(x.left, key, val); else if (cmp > 0) x.right = put(x.right, key, val); else x.val = val; //下面两行时总会执行的 x.size = 1 + size(x.left) + size(x.right); return x; }
函数功能:在node作为根节点所表示的二叉查找树当中,将key和val生成新结点插入进去,已有相同key时则是更新对应的值为val,然后将根节点返回。其实这个函数当中并不会有下面在删除操作中存在的根节点被更换的问题,而插入操作本身也不需要像查找(要返回查找到的结点)或者选择(返回排名为k的结点)、排名(返回key对应结点在树中的排名)、取整(要返回一个key取整在树中能够找到的结点)的要有一个返回值,之所以返回根节点只是为了更新结点当中的size,在Node不需要size属性时,完全可以将返回值设置成为void即可代码留待后面写下
传入参数:node、key和val。
返回值:返回一个根节点,该根节点可能是只是根据参数key找到了子树上某个结点更新了值或者根据key在该根节点子树上的相应位置插入了一个新结点。
其中1234主要是逻辑实现,0是主要为了逻辑实现与完善递归。
public Key floor(Key key) { Node x = floor(root, key); if (x == null) return null; return x.key; } private Node floor(Node x, Key key) { if (x == null) return null; int cmp = key.compareTo(x.key); if (cmp == 0) return x; if (cmp < 0) return floor(x.left, key); Node t = floor(x.right, key); if (t != null) return t; else return x; }
向下取整是得到键相比给定的key变小了一点的结点,向上取整是得到键相比给定的key变大了一点的结点
向下取整:如果给定键的key小于二叉树的根节点的键,那么小于等于key的最大键floor(key)一定在根节点的左子树当中,这是因为本来就小于了你再变小一点那就只能是根节点左子树当中的结点了,所以在左子树当中;如果给定键的key大于二叉树的根节点的键,那么只有当根节点右子树当中存在小于等于key的结点时,小于等于key的最大键才会出现在右子树当中,这是因为本来是大于的,如果变小了一点,那么可能会变成根节点或者根节点左子树中的结点。并且当小于的时候向下取整可能不存在,而当大于的时候向下取整一定存在。
函数功能:在根节点x所代表的二叉查找树当中找到 键值等于向下取整后的key 的结点,找不到则返回null
传入参数:二叉查找树根节点x和要进行向下取整的键key
返回值:在根节点x代表的树中找到的key向下取整的结点或者null(未找到)
向下取整函数的递归逻辑(向上取整类似就不在重复,上下互换、左右互换、大小互换即可):
public Key select(int k) { if (k < 0 || k >= size()) { throw new IllegalArgumentException("argument to select() is invalid: " + k); } Node x = select(root, k); return x.key; } // Return key of rank k. private Node select(Node x, int k) { if (x == null) return null; int t = size(x.left); if (t > k) return select(x.left, k); else if (t < k) return select(x.right, k-t-1); else return x; } public int rank(Key key) { if (key == null) throw new IllegalArgumentException("argument to rank() is null"); return rank(key, root); } // Number of keys in the subtree less than key. private int rank(Key key, Node x) { if (x == null) return 0; int cmp = key.compareTo(x.key); if (cmp < 0) return rank(key, x.left); else if (cmp > 0) return 1 + size(x.left) + rank(key, x.right); else return size(x.left); }
函数功能:在根节点所代表的二叉查找树当中找到名次为k的结点,然后将其返回,如找不到则返回的是null
传入参数:排名名次k,二叉查找树根节点x
返回值:找到的排名为k的键,找不到则返回null
方法思路:假设我们想找到排名为k的键(即对于该键而言在树中正好有k个小于它的键)。如果左子树中的结点数t大于k,那么我们就继续(递归地)在左子树中查找排名为k的键;如果t等于k,我们就返回根节中的键;如果t小于k,我们就(递归地)在右子树中查找排名为(k-t-1,因为要除去原本的根节点及其左子树上的结点个数之和)的键。
传入参数:要进行排名的键值Key(如果输入的不是二叉树中存在的结点的键那么就会返回0,其实改成-1应该更好些)
返回值:给定键的排名
方法思路:实现与select()类似:如果给定键和根节点的键相等,我们返回左子树的结点总数t;如果给定的键小于根节点,我们会返回该键在左子树中的排名(递归计算);如果给定的键大于根节点,我们会返回t+1(根节点)加上它在右子树当中的排名(递归计算)。
对于delMin()方法(删除最小键所对应的键值对),递归方法接收一个指向结点的引用,并返回指向一个结点的引用,这样的话我们就能将这个返回值赋给被作为参数传进来的那个引用变量,就能将对树结构进行的删除正确地反映。
什么意思呢,我们在删除的过程中可能会删除掉根节点,也就是说根节点换了,但函数外部那个原本的根节点的引用变量还是指向被删除的那个根节点,怎么办?就是只能将新的根节点(当然根节点也可能没被删掉,还是旧的,被删掉的情况是更少的但是是必须考虑的)返回赋值给那个根节点引用变量,如下面的root = deleteMin(root),这样就能正确地将删除操作反映出来了。
//Removes the smallest key and associated value from the symbol table. public void deleteMin() { if (isEmpty()) throw new NoSuchElementException("Symbol table underflow"); root = deleteMin(root); assert check(); } private Node deleteMin(Node x) { if (x.left == null) return x.right; x.left = deleteMin(x.left); x.size = size(x.left) + size(x.right) + 1; return x; }
函数功能:删除根节点x代表的二叉查找树中键值最小的结点,然后将“新”的根节点x返回
传入参数:根节点x
返回值:进行删除键值最小的结点之后的"新"的根节点
删除最大键与删除最小键的原理和实现均相似,在此不再赘述。
对于删除指定键而言,如果指定键对应的结点x只有一个子结点或者没有子结点,我们就可以采用和上面删除最大键最小键的方式一样来删除。但对于有两个子结点的结点x,要采用在删除结点x后用它的后继结点填补它的位置。因为x有一个右子结点,因此它的后继结点就是其右子树中的最小结点,这样的替换仍能保证树的有序性,因为x.key和它的后继结点之间不存在其他的键。我们能用四个简单的步骤完成将x替换为它的后继结点的任务
ps:对于左右子树任一或者同时不存在的情况已经用删除最大最小键的方法解决,在这里不用再考虑了,一定是有右子树的。
//Removes the specified key and its associated value from this symbol table public void delete(Key key) { if (key == null) throw new IllegalArgumentException("calls delete() with a null key"); root = delete(root, key); assert check(); } private Node delete(Node x, Key key) { if (x == null) return null; int cmp = key.compareTo(x.key); if (cmp < 0) x.left = delete(x.left, key); else if (cmp > 0) x.right = delete(x.right, key); else { if (x.right == null) return x.left; if (x.left == null) return x.right; Node t = x; x = min(t.right); x.right = deleteMin(t.right); x.left = t.left; } x.size = size(x.left) + size(x.right) + 1; return x; }
找到要删除的结点后的操作:
以上就是在找到指定键值相应节点后的操作,可以看出是一个顺序化流程,并不涉及递归,但在找到这个相应结点的过程中还是用到了递归,并在找到后对结点的N进行了更新,删除操作总体流程如下:
函数功能:对指定根结点x所表示的树,删除指定key在树中对应的结点,然后将该树“新”的根节点返回
传入参数:根节点x,要删除的结点的指定键key
返回值:进行删除之后的"新"的根节点(也可能并没有进行任何删除操作,在没找到指定键的情况下)