二叉查找树(Binary Search Tree),也称为二叉搜索树、有序二叉树(ordered binary tree)或排序二叉树(sorted binary tree),是指一棵空树或者具备下列性质的二叉树:node
二叉查找树相比于其余数据结构的优点在于查找、插入的时间复杂度较低,为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,以下所示less
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在该根节点子树上的相应位置插入了一个新结点。this
其中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(未找到)code
向下取整函数的递归逻辑(向上取整相似就不在重复,上下互换、左右互换、大小互换便可):排序
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(根节点)加上它在右子树当中的排名(递归计算)。ci
对于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
返回值:进行删除以后的"新"的根节点(也可能并无进行任何删除操做,在没找到指定键的状况下)