了解二叉查找树之前,先来看看折半查找法,也叫二分查找法。
在一个有序的整数数组中(假如是从小到大排序的),如果查找某个元素,返回元素的索引。
如下:
int[] arr = new int[]{1,3,4,6,8,9};
在 arr 数组中查找6这个元素,查到返回对应的索引,没有找到就返回-1
思想很简单:
1 先找到数组中间元素target与6比较
2 如果target比6大,就在数组的左边查找
3 如果target比6小,就在数组的右边查找
java实现代码如下:
private static int binarySearch(int[] data, int target) {
int l = 0;
int r = data.length - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (data[mid] > target) {
r = mid - 1;
} else if (data[mid] < target) {
l = mid + 1;
} else {
return mid;
}
}
return -1;
}
测试代码如下:
public static void main(String[] args) {
int[] data = new int[]{1,3,4,6,8,9};
System.out.println(binarySearch(data, 6));
}
输出
3
折半查找的关键是数组必须有序,一次过滤掉一半的数据,时间复杂度为O(logN)。
上面是以2为底的,N为数组的元素个数.
折半查找和下面的要讲的二分搜索树是有一样的思想
二分搜索树定义双叫二分查找树,其定义如下:
1 若它的左子树不为空,则左子树上所有的节点的值均小于根结点的值
2 若它的右子树不为空,则右子树上所有的节点的值均大于根结点的值
3 它的左右子树也分别为二分搜索树
由二叉搜索树的定义可知,它前提是二叉树,并且采用了递归的定义方式。再得,它的节点满足一定的关系,左子树的节点一定比父节点的小,右子树的节点一定比父节点的大。
构造一棵二叉搜索树的目的,其实目的不是为了排序,是为了提高查找,删除,插入关键字的速度。
下面我们用图和代码来解释二叉树的查找,插入,和删除。比如下图就是一个二叉搜索树
2.0 二叉搜索树的定义和节点的定义
二叉搜索树中存放的都是key。先看下二叉树的定义
public class QBST<K extends Comparable<K>, V> {
...
}
二叉树中节点的定义
class QNode {
K key;
V value;
QNode left;
QNode right;
QNode(K key, V value) {
this.key = key;
this.value = value;
this.left = null;
this.right = null;
}
QNode(QNode node){
this.key = node.key;
this.value = node.value;
this.left = node.left;
this.right = node.right;
}
}
类的定义和类中节点的定义都有了。
二分搜索树的定义如下:
public class QBST<K extends Comparable<K>, V> {
class QNode {
K key;
V value;
QNode left;
QNode right;
QNode(K key, V value) {
this.key = key;
this.value = value;
this.left = null;
this.right = null;
}
QNode(QNode node){
this.key = node.key;
this.value = node.value;
this.left = node.left;
this.right = node.right;
}
}
private QNode root;
private int count;
public QBST() {
root = null;
count = 0;
}
public int size() {
return count;
}
public boolean isEmpty() {
return count == 0;
}
}
2.1 二叉搜索树的插入
1 如果这棵树为空,新建一个节点,作为根
2 如果要插入的key比根节点大,就插入到右子树中
3 如果要插入的key比根节点小,就插入到左子树中
4 如果要插入的key和根节点相等,就更新当前节点的value
代码如下:
public void insert(K key, V value) {
root = insert(root, key, value);
}
private QNode insert(QNode node, K key, V value) {
checkNotNull(key,"key is null");
if (node == null) {
count++;
return new QNode(key, value);
}
if (key.compareTo(node.key) == 1) {
node.right = insert(node.right, key, value);
} else if (key.compareTo(node.key) == -1) {
node.left = insert(node.left, key, value);
} else {
node.value = value;
}
return node;
}
2.2 二叉搜索树的查找
和上面向一棵二叉搜索树插入一个节点一样。
向一棵二叉搜索树中查找一个节点也是类似
1 如果根节点为空,不用查找了,返回null
2 如果key比根节点的key要大,在右子树中查找
3 如果key比根节点的key要小,在左子树中查找
4 如果key和根节点的key相等,返回根节点
代码实现如下:
public V search(K key){
return search(root,key);
}
private V search(QNode node,K key){
checkNotNull(key,"key is null");
if(node == null){
return null;
}
if(key.compareTo(node.key) == 1){
return search(node.right,key);
}else if(key.compareTo(node.key) == -1){
return search(node.left,key);
}else {
return node.value;
}
}
2.3 二叉搜索树的遍历
二叉树的遍历有前序遍历,中序遍历,后序遍历,层序遍历(也叫做广度优先遍历)
如下图的二叉搜索树。
根据根节点的访问顺序,可以把遍历分为前序遍历,中序遍历,后序遍历
前序遍历:先访问根节点,再前序遍历左右子树
中序遍历:先中序遍历左子树,再访问根节点,后中序遍历右子树
后序遍历:先后序遍历左子树,再后序遍历右子树,再访问根节点
代码实现分别如下:
public void preOrder(){
preOrder(root);
}
private void preOrder(QNode node){
if(node != null){
System.out.println(node.key);
preOrder(node.left);
preOrder(node.right);
}
}
public void middleOrder(){
middleOrder(root);
}
private void middleOrder(QNode node){
if(node != null){
middleOrder(node.left);
System.out.println(node.key);
middleOrder(node.right);
}
}
public void postOrder(){
postOrder(root);
}
private void postOrder(QNode node){
if(node != null){
postOrder(node.left);
postOrder(node.right);
System.out.println(node.key);
}
}
其中层序遍历就是一层一层的从左到右遍历
上图中层序遍历的结果是 13 6 15 3 7 10 18
代码实现需要借助队列,代码实现如下:
public void levelOrder(){
if(root == null){
return;
}
LinkedList<QNode> queue = new LinkedList<>();
queue.addLast(root);
while (!queue.isEmpty()){
QNode node = queue.removeLast();
System.out.println(node.key);
queue.addLast(node.left);
queue.addLast(node.right);
}
}
2.4 二叉搜索树的删除
二叉搜索树最麻烦的就是删除节点,删除任意二叉树中的节点之前,我们来先删除特殊的节点。
删除二叉搜索树中最小的节点
删除二叉搜索树中最大的节点
查找二叉搜索树中最小的节点
查找二叉搜索树中最大的节点
我们先来实现这些操作。
如下图
根据二叉搜索树的定义,可以得出以下结论
在一个二叉搜索树中,最小的节点一定是最左边的节点,也就是图中的节点 3
在一个二叉搜索树中,最大的节点一定是最右边的节点,也就是图中的节点 18
总之:
最小节点去左子树中找,直到节点的左孩子为空,则当前节点就是最小节点
最大节点去右子树中找,直到节点的右孩子为空,则当前节点就是最大节点
1 先来实现查找二叉搜索树中最小的节点
如下代码
public K minimum(){
checkNotNull(root,"the tree is empty");
QNode minNode = minimum(root);
return minNode.key;
}
private QNode minimum(QNode node){
if(node.left == null){
return node;
}
return minimum(node.left);
}
同理,查找最大节点也是一样
2 实现查找二叉搜索树中最大的节点
代码如下:
public K maximum(){
checkNotNull(root,"the tree is empty");
QNode maxNode = maximum(root);
return maxNode.key;
}
private QNode maximum(QNode node){
if(node.right == null){
return node;
}
return maximum(node.right);
}
上面实现了查找最小节点和最大节点,下面我们再来实现删除最小节点和删除最大节点
3 实现删除二叉搜索树中最小的节点
一直往左孩子中删除,当某一个节点node没有左孩子时,说明当前节点就是最小节点
这时候分两种情况
当前节点有右孩子
如果是这种情况,直接把右孩子返回,作为当前节点
当前节点没有右孩子
如果是这种情况,直接返回null。此时返回右孩子也行,因为右孩子也是null
代码实现如下
public void removeMin(){
if(root != null){
root = removeMin(root);
}
}
private QNode removeMin(QNode node){
if(node.left == null){
QNode rightNode = node.right;
node = null;
count--;
return rightNode;
}
node.left = removeMin(node.left);
return node;
}
同理,删除二叉搜索树中最大的节点的代码如下:
public void removeMax(){
if(root != null){
root = removeMax(root);
}
}
private QNode removeMax(QNode node){
if(node.right == null){
QNode leftNode = node.left;
count--;
node = null;
return leftNode;
}
node.right = removeMax(node.right);
return node;
}
下面来分析一下删除任意一个节点。
删除任意一个节点node,那么可以分为以下几种情况
node 没有孩子
node 只有一个孩子
node 有两个孩子
如下图一棵二叉搜索树,我们来分析
第一种情况:node没有孩子
这种情况最简单,直接删除就行了,剩下的还是一棵二叉搜索树
比如图中的 节点5,节点13,节点27,节点50
,删除任意一个节点之后
剩下的还是满足一棵二叉搜索树
第二种情况:node只有一个孩子
这种情况又分两种
node节点有一个左孩子
node节点有一个右孩子
上面两种情况其实不影响,比如图中的节点10,节点45
,分别有一个左孩子和一个右孩子。
也好办,节点10删除后,它的左孩子节点5,放在节点10的位置
同理知,节点45删除后,它的右孩子节点50,放在节点45的位置
这样一来,剩下的节点还是一棵二叉搜索树
第三种情况:node有两个孩子
还是上图为准,以节点17
为例,节点17
有左右两个孩子,分别是10,19
要删除节点17
,怎么办呢?
或者说节点17
删除 后,哪个节点应该放在节点17
的位置上呢?
我们节点17
满足两个性质 :
17大于它的左孩子10
17小于它的右孩子19
那么我们找到一个这样的节点,只要满足上面这两条性质,不就是可以了吗。
so easey
我们就来先找一个大于10而且小于19的节点
大于 10 的节点,只要在 17 的右子树
也就是以 19 为根节点的树中找不就行了吗
因为17的右子树中所有的节点都比 17 大
小于 19 的节点,只要在以 19 为根的树中找左孩子不就得了吗
经过上面的分析,这样的节点就是 13 啊,将17删除 ,把13放到17的位置 ,如图
其实,把10放到17的位置也是可以的。如下图
10和13两个节点都满足条件,所以我们可以得出结论
删除一个有两个孩子节点,可以找这个节点左子树中的最大节点,或者右子树中的最小节点来放到当前位置
伪代码:
删除左右都 有孩子的节点 d
找到 s = min(d.right)
s 可以叫作 d 的后继
s.right = deledeMin(d->right)
s.left = d.left;
删除 d, s 是新的子树的根
翻译成代码如下:
public void remove(K key) {
root = remove(root, key);
}
private QNode remove(QNode node, K key) {
if (node == null) {
return null;
}
if (key.compareTo(node.key) == -1) {
node.left = remove(node.left, key);
return node;
} else if (key.compareTo(node.key) == 1) {
node.right = remove(node.right, key);
return node;
} else {
if (node.left == null) {
QNode rightNode = node.right;
count--;
node = null;
return rightNode;
}
if (node.right == null) {
QNode leftNode = node.left;
count--;
node = null;
return leftNode;
}
QNode min = minimum(node.right);
QNode s = new QNode(min);
s.right = removeMin(node.right);
s.left = node.left;
return s;
}
}
同过上面的分析,我们了解了二叉搜索树的性质,以及插入,查找,查找最大节点,查找最小节点,删除最大节点,删除最小节点,以及最后分析出来删除一个任意节点。
下面我们粘出完整代码 。如下
public class QBST<K extends Comparable<K>, V> {
class QNode {
K key;
V value;
QNode left;
QNode right;
QNode(K key, V value) {
this.key = key;
this.value = value;
this.left = null;
this.right = null;
}
QNode(QNode node) {
this.key = node.key;
this.value = node.value;
this.left = node.left;
this.right = node.right;
}
}
private QNode root;
private int count;
public QBST() {
root = null;
count = 0;
}
public int size() {
return count;
}
public boolean isEmpty() {
return count == 0;
}
public void insert(K key, V value) {
root = insert(root, key, value);
}
private QNode insert(QNode node, K key, V value) {
checkNotNull(key, "key is null");
if (node == null) {
count++;
return new QNode(key, value);
}
if (key.compareTo(node.key) == 1) {
node.right = insert(node.right, key, value);
} else if (key.compareTo(node.key) == -1) {
node.left = insert(node.left, key, value);
} else {
node.value = value;
}
return node;
}
public boolean contain(K key) {
return contain(root, key);
}
private boolean contain(QNode node, K key) {
checkNotNull(key, "key is null");
if (node == null) {
return false;
}
if (key.compareTo(node.key) == 1) {
return contain(node.right, key);
} else if (key.compareTo(node.key) == -1) {
return contain(node.left.key);
} else {
return true;
}
}
public V search(K key) {
return search(root, key);
}
private V search(QNode node, K key) {
checkNotNull(key, "key is null");
if (node == null) {
return null;
}
if (key.compareTo(node.key) == 1) {
return search(node.right, key);
} else if (key.compareTo(node.key) == -1) {
return search(node.left, key);
} else {
return node.value;
}
}
public void preOrder() {
preOrder(root);
}
private void preOrder(QNode node) {
if (node != null) {
System.out.println(node.key);
preOrder(node.left);
preOrder(node.right);
}
}
public void middleOrder() {
middleOrder(root);
}
private void middleOrder(QNode node) {
if (node != null) {
middleOrder(node.left);
System.out.println(node.key);
middleOrder(node.right);
}
}
public void postOrder() {
postOrder(root);
}
private void postOrder(QNode node) {
if (node != null) {
postOrder(node.left);
postOrder(node.right);
System.out.println(node.key);
}
}
public void levelOrder() {
if (root == null) {
return;
}
LinkedList<QNode> queue = new LinkedList<>();
queue.addLast(root);
while (!queue.isEmpty()) {
QNode node = queue.removeLast();
System.out.println(node.key);
queue.addLast(node.left);
queue.addLast(node.right);
}
}
public void destroy() {
destroy(root);
}
private void destroy(QNode node) {
if (node != null) {
destroy(node.left);
destroy(node.right);
node = null;
count--;
}
}
public K minimum() {
checkNotNull(root, "the tree is empty");
QNode minNode = minimum(root);
return minNode.key;
}
private QNode minimum(QNode node) {
if (node.left == null) {
return node;
}
return minimum(node.left);
}
public K maximum() {
checkNotNull(root, "the tree is empty");
QNode maxNode = maximum(root);
return maxNode.key;
}
private QNode maximum(QNode node) {
if (node.right == null) {
return node;
}
return maximum(node.right);
}
public void removeMin() {
if (root != null) {
root = removeMin(root);
}
}
private QNode removeMin(QNode node) {
if (node.left == null) {
QNode rightNode = node.right;
node = null;
count--;
return rightNode;
}
node.left = removeMin(node.left);
return node;
}
public void removeMax() {
if (root != null) {
root = removeMax(root);
}
}
private QNode removeMax(QNode node) {
if (node.right == null) {
QNode leftNode = node.left;
count--;
node = null;
return leftNode;
}
node.right = removeMax(node.right);
return node;
}
public void remove(K key) {
root = remove(root, key);
}
private QNode remove(QNode node, K key) {
if (node == null) {
return null;
}
if (key.compareTo(node.key) == -1) {
node.left = remove(node.left, key);
return node;
} else if (key.compareTo(node.key) == 1) {
node.right = remove(node.right, key);
return node;
} else {
if (node.left == null) {
QNode rightNode = node.right;
count--;
node = null;
return rightNode;
}
if (node.right == null) {
QNode leftNode = node.left;
count--;
node = null;
return leftNode;
}
QNode min = minimum(node.right);
QNode s = new QNode(min);
s.right = removeMin(node.right);
s.left = node.left;
return s;
}
}
private <E> void checkNotNull(E e, String message) {
if (e == null) {
throw new IllegalArgumentException(message);
}
}
}
推荐阅读
阿里、腾讯、百度、华为、京东最新面试题汇集
一个故事讲完HTTPS
HashMap是如何工作的?