目录
第1关:二叉搜索树的介绍与构建
任务描述
顾名思义,一棵二叉搜索树是以一棵二叉树来组织的,但是在组织的时候需要满足一些特定的性质。
本关任务:构建一棵二叉搜索树,向其中添加结点。
相关知识
二叉搜索树
什么是二叉搜索树
二叉搜索树
(binary search tree)
,又称二叉排序树,它或者是空树,或者是满足如下性质的二叉树:
- 若它的左子树非空,则左子树上所有结点的值均小于根结点的值;
- 若它的右子树非空,则右子树上所有结点的值均大于根结点的值;
- 左、右子树本身又各是一棵二叉排序树。
简而言之,二叉搜索树中任一结点
x
,其左(右)子树中任一结点y
(若存在)的关键字必小(大)于x
的关键字。下图是两棵二叉搜索树:
可以看到,对于树中的任何结点,其值大于左结点,小于右结点。 如果我们以中序遍历遍历一棵二叉搜索树,所得到的中序序列是一个递增有序序列。
上图二叉搜索树的中序遍历结果均为
2 3 4 5 7 8
。二叉搜索树的构建
构建一棵二叉搜索树,实际是构建一棵二叉树,但在添加结点时,需要满足二叉搜索树的性质。 以输入序列
5 3 7 2 4 8
为例,构建一棵二叉搜索树。如下图所示:
(1)
二叉搜索树为空,5
为根结点;
(2)
输入值3
比根结点5
小,且此时根结点的左结点为空,所以3
添加为根结点的左结点;
(3)
输入值7
比根结点5
大,添加为根结点的右结点;
(4)
输入值2
比根结点5
小,应添加至根结点的左结点,但此时根结点的左结点是3
,2
与3
比较,小于,所以添加为3
的左结点;
(5)
输入值4
比根结点5
小,应添加至根结点的左结点,但此时根结点的左结点是3
,4
与3
比较,大于,所以添加为3
的右结点;
(6)
输入值8
比根结点5
大,应添加至根结点的右结点,但此时根结点的左结点是7
,8
与7
比较,大于,所以添加为7
的右结点。
循环中小于当前需要置入结点放左边,大于当前置入结点放右边
代码:
package step1;
/**
* Created by zengpeng on 2018/3/3.
*/
public class BSTree {
private TreeNode root;//根结点
public BSTree() {
root = null;
}
/**
* 向树root中插入key
*
* @param key 要插入的值
*/
public void insert(int key) {
/********** Begin *********/
//二叉搜索树,严格要求左子树的值小于中间结点,右子树的值大于中间结点
TreeNode x = root;//创建一个结点x,储存根结点root
TreeNode p = null;//创建一个结点p,初始化为空
while (x != null) {//如果x不为空则继续,找到需要插入的结点的前一个位置
p = x;//用p储存x结点
if (key < x.item) {//如果值小于当前结点的值,则进入左子树
x = x.leftChild;//将左子树储存x
} else {//如果值大于当前结点的值,则进入右子树
x = x.rightChild;//将右子树储存x
}
}
//找到位置后再进行判断,应该构建左还是右
if (null == p) {//如果p为空,代表没有根结点
root = new TreeNode(key);//根结点储存需要构建的当前结点
} else if (key < p.item) {//如果值小于p结点的值
p.leftChild = new TreeNode(key);//p的左子树构建当前结点
} else {//如果值大于p结点的值
p.rightChild = new TreeNode(key);//p的右子树构建当前结点
}
/********** End *********/
}
public void preOrder() {
preOrder(root);
}
public void inOrder() {
inOrder(root);
}
public void postOrder(){
postOrder(root);
}
private void preOrder(TreeNode node) {
if (node != null) {
System.out.print(node.item + " ");
preOrder(node.leftChild);
preOrder(node.rightChild);
}
}
private void inOrder(TreeNode node) {
if (node != null) {
inOrder(node.leftChild);
System.out.print(node.item + " ");
inOrder(node.rightChild);
}
}
private void postOrder(TreeNode node) {
if (node != null) {
postOrder(node.leftChild);
postOrder(node.rightChild);
System.out.print(node.item + " ");
}
}
public static class TreeNode {
private TreeNode leftChild;
private TreeNode rightChild;
private int item;
public TreeNode(int item) {
this(null, null, item);
}
public TreeNode(TreeNode leftChild, TreeNode rightChild, int item) {
this.leftChild = leftChild;
this.rightChild = rightChild;
this.item = item;
}
}
}
以下是测试样例:
测试输入:
3 2 1 0 21
预期输出:
前序遍历: 3 2 1 0 21
中序遍历: 0 1 2 3 21
后序遍历: 0 1 2 21 3
第2关:二叉搜索树的删除
任务描述
在上一关,我们构建了一棵二叉搜索树,并且可以向其添加结点数据,本关将实现在二叉搜索树的删除功能。
本关任务是:实现二叉搜索树的删除功能。
相关知识
后继结点
给定一棵二叉搜索树中的一个结点,有时候需要按中序遍历的次序查找它的后继。如果所有的关键字互不相同,则一个结点
x
的后继结点是大于x
的最小关键字的结点。上图中,结点
66
的后继结点为68
。20
的后继结点为66
。二叉搜索树的删除
从一棵二叉搜索树
T
中删除一个结点z
的整个策略可分为三个基本情况:
- 如果
z
没有孩子结点,那么只是简单地将它删除,并修改它的父结点;- 如果
z
只有一个孩子结点,那么将这个孩子提升到树中z
的位置上,并修改z
的父结点,使其指向z
的孩子结点;- 如果
z
有两个孩子,那么找z
的后继结点y
,并让y
占据树中z
的位置。z
的原来右子树部分成为y
的新的右子树,并且z
的左子树成为y
的新的左子树。(根据二叉搜索树的性质,后继结点y
一定在z
的右子树中)。下列是几种情况的图示:
(1)
删除没有子结点的结点;因为结点
150
没有孩子结点,所以直接把其删掉。
(2)
删除有一个子结点的结点;结点
50
有一个孩子结点,我们只需把其孩子结点提升到结点50
的位置即可。
(3)
删除有两个子结点的结点;对于有两个子结点的结点
50
,在其右子树中找到其后继结点66
,使后继结点相应的替换结点50
。
删除的方法是如果有左子树,以左子树中最大值作为新的树根,否则,以右子树最小值作为树根。
代码:
package step2;
/**
* Created by zengpeng on 2018/3/14.
*/
public class BSTree {
private TreeNode root;//根结点
public BSTree() {
root = null;
}
/**
* 向树root中插入a
*
* @param key 要插入的值
*/
public void insert(int key) {
TreeNode x = root;
TreeNode p = null;//始终指向x的父结点
while (x != null) {
p = x;
if (key < x.item) {
x = x.leftChild;
} else {
x = x.rightChild;
}
}
if (null == p) {//空树
root = new TreeNode(key);
} else if (key < p.item) {
p.leftChild = new TreeNode(key);
} else {
p.rightChild = new TreeNode(key);
}
}
/**
* 在树root中删除结点key
*
* @param key
* @return
*/
public void delete(int key) {
root = delete(root, key);
}
private TreeNode delete(TreeNode x, int key) {
/********** Begin *********/
if (x == null)//x为根结点,如果根结点为空
return null;//返回空
//递归实现
if (key < x.item) {//如果删除的值小于当前结点
x.leftChild = delete(x.leftChild, key);//再次进入delete方法,往左子树继续搜寻需要删除的结点
} else if (key > x.item) {
x.rightChild = delete(x.rightChild, key);//再次进入delete方法,往右子树继续搜寻需要删除的结点
} else {//如果等于,由于是二叉搜索树需要严格满足条件
if (x.leftChild == null) return x.rightChild;//如果左子树为空,返回右子树的结点
if (x.rightChild == null) return x.leftChild;//如果右子树为空,返回左子树的结点
TreeNode t = x;//用t储存x结点
x = min(t.rightChild);//寻找t右子树的最小值结点,并用x储存,需要用最小结点代替要被删除的结点的位置
x.rightChild = deleteMin(t.rightChild);//删除找到的最小结点
x.leftChild = t.leftChild;//让最小结点的左子树变为要删除的结点的左子树
}
return x;//返回
/********** End *********/
}
/**
* 删除树x中的最小结点
*
* @param x
* @return
*/
private TreeNode deleteMin(TreeNode x) {
if (x.leftChild == null) return x.rightChild;
x.leftChild = deleteMin(x.leftChild);
return x;
}
/**
* 查找树x中的最小结点
*
* @param x
* @return
*/
private TreeNode min(TreeNode x) {
TreeNode p = x;
while (p.leftChild != null) {
p = p.leftChild;
}
return p;
}
public void preOrder() {
preOrder(root);
}
private void preOrder(TreeNode node) {
if (node != null) {
System.out.print(node.item + " ");
preOrder(node.leftChild);
preOrder(node.rightChild);
}
}
public void inOrder() {
inOrder(root);
}
private void inOrder(TreeNode node) {
if (node != null) {
inOrder(node.leftChild);
System.out.print(node.item + " ");
inOrder(node.rightChild);
}
}
public void postOrder() {
postOrder(root);
}
private void postOrder(TreeNode node) {
if (node != null) {
postOrder(node.leftChild);
postOrder(node.rightChild);
System.out.print(node.item + " ");
}
}
public static class TreeNode {
private TreeNode leftChild;
private TreeNode rightChild;
private int item;
public TreeNode(int item) {
this(null, null, item);
}
public TreeNode(TreeNode leftChild, TreeNode rightChild, int item) {
this.leftChild = leftChild;
this.rightChild = rightChild;
this.item = item;
}
}
}
以下是测试样例:
测试输入:
3 2 1 0 21
预期输出:
前序遍历: 3 2 1 0 21
中序遍历: 0 1 2 3 21
后序遍历: 0 1 2 21 3
前序遍历: 21 2 1 0
中序遍历: 0 1 2 21
后序遍历: 0 1 2 21
第3关:二叉搜索树的查找
任务描述
我们经常需要查找一个存储在二叉搜索树中的关键字,本关卡将讨论并实现查找功能。
本关任务:实现二叉搜索树的查找功能,如果指定的元素
x
存在树中则返回true
,否则返回false
。相关知识
二叉搜索树的查找
给定一个关键字
key
,我们要判断key
是否在指定的二叉搜索树T
中,如果存在则返回true
,不存在则返回false
。这个查找过程从树根结点开始,并沿着这棵树中的一条简单路径向下进行,如下图所示。对于遇到的每个结点
x
,比较key
和x
。如果两个关键字相等,查找终止。如果key
小于x
,查找在x
的左子树中继续,因为二叉搜索树的性质意味着key
不可能被存储在x
的右子树中。对称地,如果key
大于x
,查找在右子树中继续。为了查找上图中的关键字为
13
的结点,从树根开始沿着15->6->7->13
路径进行查找。
代码:
package step3;
/**
* Created by zengpeng on 2018/3/14.
*/
public class BSTree {
private TreeNode root;//根结点
public BSTree() {
root = null;
}
/**
* 向树root中插入a
*
* @param key 要插入的值
*/
public void insert(int key) {
TreeNode x = root;
TreeNode p = null;//始终指向x的父结点
while (x != null) {
p = x;
if (key < x.item) {
x = x.leftChild;
} else {
x = x.rightChild;
}
}
if (null == p) {//空树
root = new TreeNode(key);
} else if (key < p.item) {
p.leftChild = new TreeNode(key);
} else {
p.rightChild = new TreeNode(key);
}
}
/**
* 判断树root中是否包含key,包含则返回true,不包含返回false
*
* @param key
* @return
*/
public boolean search(int key) {
/********** Begin *********/
TreeNode p=root;//用p结点储存根结点
while(p!=null&&key!=p.item)//如果p不为空,并且要查找的值不为p结点的值
{
if(key<p.item)//如果要查找的值小于p结点的值
{
p=p.leftChild;//让p储存为p的左子树,因为小于p结点值的,结点都在左边
}
else//如果要查找的值大于p结点的值
{
p=p.rightChild;//让p储存为p的大子树,因为大于p结点值的,结点都在右边
}
}
if(p==null)//如果p为空则表示没找到
return false;//返回false
else//找到了
return true;//返回true
/********** End *********/
}
/**
* 在树root中删除结点key
*
* @param key
* @return
*/
public void delete(int key) {
root = delete(root, key);
}
private TreeNode delete(TreeNode x, int key) {
if (x == null) {
return null;
}
if (key < x.item) {
x.leftChild = delete(x.leftChild, key);
} else if (key > x.item) {
x.rightChild = delete(x.rightChild, key);
} else {
if (x.leftChild == null) return x.rightChild;
if (x.rightChild == null) return x.leftChild;
TreeNode t = x;
x = min(t.rightChild);
x.rightChild = deleteMin(t.rightChild);
x.leftChild = t.leftChild;
}
return x;
}
/**
* 删除树x中的最小结点
* @param x
* @return
*/
private TreeNode deleteMin(TreeNode x) {
if (x.leftChild == null) return x.rightChild;
x.leftChild = deleteMin(x.leftChild);
return x;
}
/**
* 查找树x中的最小结点
*
* @param x
* @return
*/
private TreeNode min(TreeNode x) {
TreeNode p = x;
while (p.leftChild != null) {
p = p.leftChild;
}
return p;
}
public void preOrder() {
preOrder(root);
}
public void inOrder() {
inOrder(root);
}
public void postOrder() {
postOrder(root);
}
private void preOrder(TreeNode node) {
if (node != null) {
System.out.print(node.item + " ");
preOrder(node.leftChild);
preOrder(node.rightChild);
}
}
private void inOrder(TreeNode node) {
if (node != null) {
inOrder(node.leftChild);
System.out.print(node.item + " ");
inOrder(node.rightChild);
}
}
private void postOrder(TreeNode node) {
if (node != null) {
postOrder(node.leftChild);
postOrder(node.rightChild);
System.out.print(node.item + " ");
}
}
public static class TreeNode {
private TreeNode leftChild;
private TreeNode rightChild;
private int item;
public TreeNode(int item) {
this(null, null, item);
}
public TreeNode(TreeNode leftChild, TreeNode rightChild, int item) {
this.leftChild = leftChild;
this.rightChild = rightChild;
this.item = item;
}
}
}
以下是测试样例:
测试输入:
1 5 4 3 2 6
预期输出:
删除前树中的结点:1 5 4 3 2 6
删除后树中的结点:5 4 3 2
false true true true true false
注意:二叉搜索树中,如果存在左右子树,则左子树上的结点都小于根结点,右结点子树上的结点都大于根结点,严格要求左子树小于根结点,右子树大于根结点