二分搜索树
一、 二叉树
二叉树特点: 二叉树具有天然的递归结构—>每个节点的左子树也是二叉树;每个节点的右子树也是二叉树
- 二分搜索树
- 二分搜索树的初始化
package cn.itcast.day5;
// 由于二分搜索树必须进行比较,因此对于天然不具备比较的数据。因此需要令泛型E实现Comparable接口,
// 并通过compareTo方法来进行大小的比较
public class BST<E extends Comparable<E>>{
private class Node{
public E e;
public Node left,right;
public Node(E e) {
this.e = e;
left = null;
right = null;
}
}
private Node root;
private int size;
public BST() {
root = null;
size = 0;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
}
- 二分搜索树添加新元素,添加两个方法:add(E e)和add(Node node,E e)
public void add(E e) {
if(root == null) {
root = new Node(e);
size ++;
}else {
add(root,e);
}
}
//向以node为根的二分搜索树中插入元素E,递归算法
private void add(Node node,E e) {
if(e.equals(node.e)) { //终止条件较为复杂,有三种情况需要进行判断
return;
}else if(e.compareTo(node.e)<0 && node.left == null) { //不是基础类型,因此不能直接用大于号或者小于号进行比较
node.left = new Node(e);
size ++;
return;
}else if(e.compareTo(node.e)>0 && node.right == null) {
node.right = new Node(e);
size ++;
return;
}
if(e.compareTo(node.e)<0) {
add(node.left,e);
}else {
add(node.right,e);
}
}
- 改写递归函数
//返回插入新节点后二分搜索树的根
private Node add(Node node,E e) {
if(node == null) {
size ++;
return new Node(e);
}
if(e.compareTo(node.e)<0) {
node.left = add(node.left,e);
}else if(e.compareTo(node.e)>0) {
node.right = add(node.right,e);
}
return node;
}
- 二分搜索树查询元素:添加方法contains(E e)和contains(Node node,E e)
//看二分搜索树中是否包含元素e(用户调用)
public boolean contains(E e) {
return contains(root,e);
}
//看以node为根的二分搜索树中是否包含元素e,递归算法(由公有的contains进行调用)
private boolean contains(Node node,E e) {
if(node == null) {
return false;
}
if(e.compareTo(node.e)==0) {
return true;
}else if(e.compareTo(node.e)<0) {
return contains(node.left,e);
}else {
return contains(node.right,e);
}
}
二、二分搜索树前向遍历
- 前序遍历:先访问根节点,再分别访问左节点、右节点
- 代码实现
//二分搜索树的前序遍历(用户调用的方法)
public void preOrder() {
preOrder(root);
}
//程序执行时实际的调用
public void preOrder(Node node) {
if(node == null) {
return;
}
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
}
// 前序遍历的打印
@Override
public String toString() {
StringBuilder res = new StringBuilder();
generateBSTString(root,0,res); //需要打印的树、深度
return res.toString();
}
//生成以node为根节点,深度为depth的描述二叉树的字符串
private void generateBSTString(Node node,int depth,StringBuilder res) {
if(node == null) {
res.append(generateDepthString(depth)+"null\n");
return;
}
res.append(generateDepthString(depth)+node.e+"\n");
generateBSTString(node.left,depth+1,res);
generateBSTString(node.right,depth+1,res);
}
private String generateDepthString(int depth) {
StringBuilder res = new StringBuilder();
for(int i=0;i<depth;i++) {
res.append("--");
}
return res.toString();
}
三、二分搜索树中、后序遍历
- 中序遍历
public void inOrder() {
inOrder(root);
}
//程序执行时实际的调用
public void inOrder(Node node) {
if(node == null) {
return;
}
inOrder(node.left);
System.out.println(node.e);
inOrder(node.right);
}
- 后序遍历
public void postOrder() {
postOrder(root);
}
//程序执行时实际的调用
public void postOrder(Node node) {
if(node == null) {
return;
}
postOrder(node.left);
postOrder(node.right);
System.out.println(node.e);
}
四、前序遍历的非递归实现(使用栈)
- 思路:利用栈的后进先出特性,每当访问到一个节点时,打印出该节点的元素后,将该节点的右子树先入栈,再将左子树入栈(由于栈先进先出,因此左子树后入栈时,出栈比右子树提前)。下图中的执行逻辑是:先将根节点数值28入栈,28为栈顶,读取28节点的左右子树,将右子树30入栈,然后16入栈;下一步执行出栈,此时16出栈,读取16的左右子树,则22先入栈,再将13入栈;下一步执行出栈。。。。以此类推
- 代码
//二分搜索树的非递归前序遍历
public void preOrderNR() {
Stack<Node> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()) {
Node cur = stack.pop();
System.out.println(cur.e);
if(cur.right!=null) {
stack.push(cur.right);
}
if(cur.left!=null) {
stack.push(cur.left);
}
}
}
五、二分搜索树的层序遍历
- 使用队列完成操作
思路:利用队列先进先出的规特性,每当访问到一个节点时,打印出该节点的数值后,再将其左右节点入队。由于先进先出,因此先将左节点入对,再将右节点入队。如下例:初始时根节点为28,进行出队操作,出队后,先将16进行入队,再让30入队;此时,令16出队(30成为队首),然后防卫16的左右节点,并按顺序将13、22进行入队操作。完成13、22的入队操作后,令队首元素(30)出队,然后访问其左右节点,再按照顺序将29、42进行入队操作,以此类推。。。
- 代码实现
//二叉树的层序遍历
public void levelOrder() {
Queue<Node> queue = new LinkedList<>(); //由于java提供的Queue是接口,应该使用其实现类来进行操作
queue.add(root);
while(!queue.isEmpty()) {
Node cur = queue.poll();
System.out.println(cur.e);
if(cur.left!=null) {
queue.add(cur.left);
}
if(cur.right!=null) {
queue.add(cur.right);
}
}
}
- 广度优先遍历的意义
六、二分搜索树删除节点 - 从简单开始,删除二分搜索树的最小值和最大值
要执行删除操作,首先需要找到最小值和最大值。最小值:在二分查找树的最左端;最大值:在二分查找树的最右端。
//寻找二分搜索树的最小元素(由用户调用)
public E minimum() {
if(size == 0) {
throw new IllegalArgumentException("错误");
}
return minimum(root).e;
}
private Node minimum(Node node) {
if(node.left == null) {
return node;
}
return minimum(node.left);
}
//寻找二分搜索树的最大元素(由用户调用)
public E maxmum() {
if(size == 0) {
throw new IllegalArgumentException("错误");
}
return maxmum(root).e;
}
private Node maxmum(Node node) {
if(node.right == null) {
return node;
}
return maxmum(node.right);
}
找到最小最大值后,要进行删除操作。如下,22是最小的节点,删除掉22这个节点后,只需要将22的右子树变为41的左子树即可。对于最大值,同理。
- 代码实现
//从二分搜索树中删除最小值所在的节点,返回最小值(用户端的执行)
public E removeMin() {
E ret = minimum();
root = removeMin(root); //用于得到所删除的节点的右子树的信息
return ret;
}
//删除操作时程序的实际执行操作
//删除以node为根的二分搜索树中的最小节点,返回删除节点后新的二分搜索树的根
private Node removeMin(Node node) {
if(node.left == null) { //当节点已经不存在左节点时已经到达树的最左端(即要删的最小值)
Node rightNode = node.right; //保存需要删除的最小值节点的右子树的信息
node.right = null; //删除此最小值节点
size --;
return rightNode;
}
node.left = removeMin(node.left); //将删除的节点的右子树挂到新节点的左子树上
return node; //只会在程序最后一步执行
}
//从二分搜索树中删除最大值所在的节点,返回最小值(用户端的执行)
public E removeMax() {
E ret = maxmum();
root = removeMax(root); //用于得到所删除的节点的右子树的信息
return ret;
}
//删除操作时程序的实际执行操作
//删除以node为根的二分搜索树中的最大节点,返回删除节点后新的二分搜索树的根
private Node removeMax(Node node) {
if(node.right == null) { //当节点已经不存在左节点时已经到达树的最右端(即要删的最大值)
Node leftNode = node.left; //保存需要删除的最大值节点的左子树的信息
node.left = null; //删除此最大值节点
size --;
return leftNode;
}
node.right = removeMin(node.right);
return node; //只会在程序最后一步执行
}
七、二分搜索树删除任意节点
- 删除只有右孩子的节点操作
- 删除左右都有孩子的节点:删除此节点后,需要找到一个节点来替代该节点的位置。如下图,删除58,则找的节点应该时58这个节点的后继(找到58的左右孩子中,离58最近的同时比58大的节点来取代58的位置,即59------>58的右子树中的最小值节点) 。下一步是在d的右子树中删除这个最小的值,再让剩下的子树成为这个d后继的孩子节点。如下图
最后,再让d原本的左子树成为59这个节点的左子树,然后将d删除。
除后继外,也可以使用前驱(左子树中最大的节点)来替代待删除的节点
- 代码实现
//删除以node为根的二分搜索树中值为e的节点,递归算法
//返回删除节点后新的二分搜索树的根
private Node remove(Node node,E e) {
if(node == null) {
return null;
}
if(e.compareTo(node.e)<0) {
node.left = remove(node.left,e);
return node;
}
else if(e.compareTo(node.e)>0) {
node.right = remove(node.right,e);
return node;
}
else { //e == node.e
//开始删除
//待删除节点的左子树为空的情况
if(node.left == null) {
Node rightNode = node.right;
node.right = null;
size --;
return rightNode;
}
//待删除节点的右子树为空的情况
if(node.right == null) {
Node leftNode = node.left;
node.left = null;
size --;
return leftNode;
}
//待删除节点的左右子树均不为空的情况
//找到比待删除节点大的最小节点,即待删除节点右子树的最小节点
//用这个节点顶替待删除节点的位置
Node successor = minimum(node.right); //node的后继节点
//removeMin中进行了size--操作,因此不用再继续维护size
successor.right = removeMin(node.right); //将node.right中的最小节点去掉后,整个树作为后继的右节点
successor.left = node.left;
node.left = node.right = null;
return successor;
}
}
八、完整代码
package cn.itcast.day5;
import java.util.Stack;
import java.util.LinkedList;
import java.util.Queue;
public class BST<E extends Comparable<E>>{
private class Node{
public E e;
public Node left,right;
public Node(E e) {
this.e = e;
left = null;
right = null;
}
}
private Node root;
private int size;
public BST() {
root = null;
size = 0;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public void add(E e) {
if(root == null) {
root = new Node(e);
size ++;
}else {
add(root,e);
}
}
//向以node为根的二分搜索树中插入元素E,递归算法
/* private void add(Node node,E e) {
if(e.equals(node.e)) {
return;
}else if(e.compareTo(node.e)<0 && node.left == null) { //不是基础类型,因此不能直接用大于号或者小于号进行比较
node.left = new Node(e);
size ++;
return;
}else if(e.compareTo(node.e)>0 && node.right == null) {
node.right = new Node(e);
size ++;
return;
}
if(e.compareTo(node.e)<0) {
add(node.left,e);
}else {
add(node.right,e);
}
}*/
//返回插入新节点后二分搜索树的根
private Node add(Node node,E e) {
if(node == null) {
size ++;
return new Node(e);
}
if(e.compareTo(node.e)<0) {
node.left = add(node.left,e);
}else if(e.compareTo(node.e)>0) {
node.right = add(node.right,e);
}
return node;
}
//看二分搜索树中是否包含元素e(用户调用)
public boolean contains(E e) {
return contains(root,e);
}
//看以node为根的二分搜索树中是否包含元素e,递归算法(由公有的contains进行调用)
private boolean contains(Node node,E e) {
if(node == null) {
return false;
}
if(e.compareTo(node.e)==0) {
return true;
}else if(e.compareTo(node.e)<0) {
return contains(node.left,e);
}else {
return contains(node.right,e);
}
}
//二分搜索树的前序遍历(用户调用的方法)
public void preOrder() {
preOrder(root);
}
//程序执行时实际的调用
public void preOrder(Node node) {
if(node == null) {
return;
}
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
}
public void inOrder() {
inOrder(root);
}
//程序执行时实际的调用
public void inOrder(Node node) {
if(node == null) {
return;
}
inOrder(node.left);
System.out.println(node.e);
inOrder(node.right);
}
public void postOrder() {
postOrder(root);
}
//程序执行时实际的调用
public void postOrder(Node node) {
if(node == null) {
return;
}
postOrder(node.left);
postOrder(node.right);
System.out.println(node.e);
}
//二分搜索树的非递归前序遍历
public void preOrderNR() {
Stack<Node> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()) {
Node cur = stack.pop();
System.out.println(cur.e);
if(cur.right!=null) {
stack.push(cur.right);
}
if(cur.left!=null) {
stack.push(cur.left);
}
}
}
//二叉树的层序遍历
public void levelOrder() {
Queue<Node> queue = new LinkedList<>(); //由于java提供的Queue是接口,应该使用其实现类来进行操作
queue.add(root);
while(!queue.isEmpty()) {
Node cur = queue.poll();
System.out.println(cur.e);
if(cur.left!=null) {
queue.add(cur.left);
}
if(cur.right!=null) {
queue.add(cur.right);
}
}
}
//寻找二分搜索树的最小元素(由用户调用)
public E minimum() {
if(size == 0) {
throw new IllegalArgumentException("错误");
}
return minimum(root).e;
}
private Node minimum(Node node) {
if(node.left == null) {
return node;
}
return minimum(node.left);
}
//寻找二分搜索树的最大元素(由用户调用)
public E maxmum() {
if(size == 0) {
throw new IllegalArgumentException("错误");
}
return maxmum(root).e;
}
private Node maxmum(Node node) {
if(node.right == null) {
return node;
}
return maxmum(node.right);
}
//从二分搜索树中删除最小值所在的节点,返回最小值(用户端的执行)
public E removeMin() {
E ret = minimum();
root = removeMin(root); //用于得到所删除的节点的右子树的信息
return ret;
}
//删除操作时程序的实际执行操作
//删除以node为根的二分搜索树中的最小节点,返回删除节点后新的二分搜索树的根
private Node removeMin(Node node) {
if(node.left == null) { //当节点已经不存在左节点时已经到达树的最左端(即要删的最小值)
Node rightNode = node.right; //保存需要删除的最小值节点的右子树的信息
node.right = null; //删除此最小值节点
size --;
return rightNode;
}
node.left = removeMin(node.left); //将删除的节点的右子树挂到新节点的左子树上
return node; //只会在程序最后一步执行
}
//从二分搜索树中删除最大值所在的节点,返回最小值(用户端的执行)
public E removeMax() {
E ret = maxmum();
root = removeMax(root); //用于得到所删除的节点的右子树的信息
return ret;
}
//删除操作时程序的实际执行操作
//删除以node为根的二分搜索树中的最大节点,返回删除节点后新的二分搜索树的根
private Node removeMax(Node node) {
if(node.right == null) { //当节点已经不存在左节点时已经到达树的最右端(即要删的最大值)
Node leftNode = node.left; //保存需要删除的最大值节点的左子树的信息
node.left = null; //删除此最大值节点
size --;
return leftNode;
}
node.right = removeMin(node.right);
return node; //只会在程序最后一步执行
}
// 从二分搜索树中删除元素为e的节点(用户进行调用)
public void remove(E e) {
root = remove(root,e);
}
//删除以node为根的二分搜索树中值为e的节点,递归算法
//返回删除节点后新的二分搜索树的根
private Node remove(Node node,E e) {
if(node == null) {
return null;
}
if(e.compareTo(node.e)<0) {
node.left = remove(node.left,e);
return node;
}
else if(e.compareTo(node.e)>0) {
node.right = remove(node.right,e);
return node;
}
else { //e == node.e
//开始删除
//待删除节点的左子树为空的情况
if(node.left == null) {
Node rightNode = node.right;
node.right = null;
size --;
return rightNode;
}
//待删除节点的右子树为空的情况
if(node.right == null) {
Node leftNode = node.left;
node.left = null;
size --;
return leftNode;
}
//待删除节点的左右子树均不为空的情况
//找到比待删除节点大的最小节点,即待删除节点右子树的最小节点
//用这个节点顶替待删除节点的位置
Node successor = minimum(node.right); //node的后继节点
//removeMin中进行了size--操作,因此不用再继续维护size
successor.right = removeMin(node.right); //将node.right中的最小节点去掉后,整个树作为后继的右节点
successor.left = node.left;
node.left = node.right = null;
return successor;
}
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
generateBSTString(root,0,res); //需要打印的树、深度
return res.toString();
}
//生成以node为根节点,深度为depth的描述二叉树的字符串
private void generateBSTString(Node node,int depth,StringBuilder res) {
if(node == null) {
res.append(generateDepthString(depth)+"null\n");
return;
}
res.append(generateDepthString(depth)+node.e+"\n");
generateBSTString(node.left,depth+1,res);
generateBSTString(node.right,depth+1,res);
}
private String generateDepthString(int depth) {
StringBuilder res = new StringBuilder();
for(int i=0;i<depth;i++) {
res.append("--");
}
return res.toString();
}
}
- 测试代码
package cn.itcast.day5;
import java.util.ArrayList;
import java.util.Random;
public class MAIN {
public static void main(String[] args) {
BST<Integer> bst = new BST<>();
int n = 500;
Random random = new Random();
for(int i=0;i<n;i++) {
bst.add(random.nextInt(10000));
}
ArrayList<Integer> nums = new ArrayList<>();
while(!bst.isEmpty()) {
nums.add(bst.removeMin());
}
System.out.println(nums);
}
}