数据结构之二分搜索树(Binary Search Tree)

Posted 敲代码的人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构之二分搜索树(Binary Search Tree)相关的知识,希望对你有一定的参考价值。

先了解二叉树

  1. 和链表一样,动态的数组结构

  2. 具有唯一的根节点

  3. 二叉树每个节点最多有两个孩子

  4. 每个节点最多有一个父亲

特性

  1. 二叉树不一定是满的

  2. 一个节点,甚至一个null,也是一个二叉树

  3. 具有天然的递归性,每一个孩子的左子树,和又子树也是一个二叉树

数据结构之二分搜索树(Binary Search Tree)


二分搜索树

  1. 二分搜索树也是一颗二叉树

  2. 二分搜索树每个节点的值,大于其左子树所有的值,小于其又子树所有的值

  3. 每一棵子树也是二分搜索树

  4. 这样存储的元素就具有可比较性


设计:

内部类节点的设计

 private class Node { private E e; // 定义两个节点,左右孩子 private Node left, rigth; public Node(E e) { this.e = e; left = null; rigth = null; } }


基础框架及基础方法的搭建

//元素的E需要实现Compareable接口,//因为需要可比较性public class BST<E extends Comparable<E>> {  //内部节点类  //.................  private Node root;//定义二分搜索树的根节点 private int size;//定义元素的个数  public BST(){ root = null; size = 0; }  //获取元素的个数 public int getSize(){ return size; }  //判断元素是否为空 public boolean isEmpty(){ return size == 0; }}


向二分搜索树中添加元素

思路分析:

  1. 如果二分搜索树为空,插入进来的节点作为根节点

  2. 如果不为空,如果小于根节点,和根节点的左子树比较,此时左子树可以看成一个新的二分搜索树,,也有根,和左右孩子,然后一层一层往下,知道找一个一个空的节点挂上

  3. 如果大于,往又子树去比较

  4. 遍历寻找空节点,就是自己的位置


注:这里忽略相同情况,如果是相同元素,不插入(可以设计插入做节点或者又节点中)


简单图示

数据结构之二分搜索树(Binary Search Tree)

数据结构之二分搜索树(Binary Search Tree)

 //向二分搜索树中添加元素 public void add(E e){ //判断是否为空 if(isEmpty()){ //创建根节点 root = new Node(e); }else{ //递归查找需要插入的位置 //传入一个子树的根节点,和要插入的元素 add(root,e); } }  //设计向以node为根的二分搜索树中插入元素E,递归算法 private void add(Node node,E e){ //递归的终止条件, //1,小于正在比较的此二分搜索树根节点,且此根节点的左孩子为null //2.大于正在比较的此二分搜索树根节点,且此根节点的又孩子为null //先处理相同的情况 if(e.equals(node.e)){ return; }else if(e.compareTo(node.e) < 0 && node.left == null){//1. //创建节点,元素为e,挂上 node.left = new Node(e); size++; return; }else if(e.compareTo(node.e) > 0 && node.rigth == null){ node.rigth = new Node(e); size++; return; }  //一步一步处理递归逻辑,变成更小的逻辑 //如果此节点小于根节点,就去左孩子去查找 if(e.compareTo(node.e) < 0){ add(node.left,e); }else { add(node.rigth, e); }  }

实现思路二

  1. 对于添加一个节点,不管左还是右,最终都是添加到null的位置

  2. 所以我们对于处理最小问题(递归终止条件),让这个二分搜索树node节点为空,就是插入位置

  3. 所以每递归一次,我们的左子树,或者右子树就有可能发生变化,所以需要接住发生变化的孩子

 // 设计向以node为根的二分搜索树中插入元素E,递归算法 private void add(Node node, E e) { // 递归的终止条件, // 1,小于正在比较的此二分搜索树根节点,且此根节点的左孩子为null // 2.大于正在比较的此二分搜索树根节点,且此根节点的又孩子为null // 先处理相同的情况 if (e.equals(node.e)) { return; } else if (e.compareTo(node.e) < 0 && node.left == null) {// 1. // 创建节点,元素为e,挂上 node.left = new Node(e); size++; return; } else if (e.compareTo(node.e) > 0 && node.rigth == null) { node.rigth = new Node(e); size++; return; }
// 一步一步处理递归逻辑,变成更小的逻辑 // 如果此节点小于根节点,就去左孩子去查找 if (e.compareTo(node.e) < 0) { add(node.left, e); } else { add(node.rigth, e); } }
public void add2(E e) { root = add2(root, e); }
private Node add2(Node node, E e) { //递归终止条件传入节点为null if(node == null){ //创建节点并返回 size++; return new Node(e); } //处理递归函数 //去左子树,此时左子树会发生变化,需要接住 //但是根节点还是node if(e.compareTo(node.e) < 0){ node.left = add2(node.left,e); }else if(e.compareTo(node.e) > 0){ node.rigth = add2(node.rigth,e); } //最终返回的就是已node为根节点的二分搜索树 return node; }


判断是否包含元素的节点

思路:

  1. 查看以node为节点的树,

  2. 终止条件,当递归后到最后node为空是,返回false

  3. 如果元素小于当前的节点,去左孩子找

  4. 大于,去又孩子找

  5. 等于,说明包含,直接返回true

 private boolean contains(Node node, E e) { // 如果传入的节点为空,说明递归到底了, // 还是没有查到,所以返回false if (node == null) { return false; }
if (e.compareTo(node.e) < 0) { return contains(node.left, e); } else if (e.compareTo(node.e) > 0) { return contains(node.rigth, e); } else { return true; } }


前序遍历

先遍历根节点,再遍历左孩子,最后又孩子

 //遍历已node为节点的二分搜索树 private void perOrder(Node node){ //递归终止 /* if(node == null){ return; }  //为了方便直接打印 System.out.println(node.e); perOrder(node.left); perOrder(node.rigth);*/   //可以写成 if(node != null){ System.out.println(node.e); perOrder(node.left); perOrder(node.rigth); } }

图解,每个节点返回三次,蓝色点访问的时候取值

数据结构之二分搜索树(Binary Search Tree)

中序遍历

先访问左孩子,在访问节点,最后访问又子树

 private void inOrder(Node node){ if(node == null){ return; } inOrder(node.left); System.out.println(node.e); inOrder(node.rigth); }

图解,三次返回的顺序和前序一样,蓝色点取值

数据结构之二分搜索树(Binary Search Tree)


后序遍历

先左后又,最后节点

 public void postOrder(){ postOrder(); }
private void postOrder(Node node){ if(node == null){ return; } postOrder(node.left); postOrder(node.rigth); System.out.println(node.e); }

图解,同理,蓝色点取值

数据结构之二分搜索树(Binary Search Tree)


前序遍历的非递归写法

思路:需要借助栈这种数据结构,

  1. 把要操作的当前节点压入栈

  2. 弹栈(就是遍历),就是刚才压入栈的节点,操作此节点

  3. 如果右节点不为空,压栈,左节点不为空,压栈(先进后出),总体来说弹一次,压入两次

  4. 再弹栈(遍历),操作弹出栈的节点

  5. 循环步骤,最后弹出栈中所有元素,遍历完成

图解:

数据结构之二分搜索树(Binary Search Tree)


 public void perOrderNR(){ Stack<Node> stack = new Stack<>(); //先压入根节点 stack.push(root); //如果栈不为空,弹出 if(!stack.isEmpty()){ Node cur = stack.pop(); System.out.println(cur.e);  //压栈,压入弹出节点的左右子树 //先压入左子树 if(cur.left != null){ stack.push(cur.left); } if(cur.rigth != null){ stack.push(cur.rigth); } } }

层序遍历,一层一层,从左向又遍历

思路:借助队列

  1. 把根节点入队

  2. 把之前的入队的节点出队(遍历)

  3. 操作刚才出队的,如果左孩子不为空,入队,又孩子不为空入队(先进先出)

  4. 出队,循环出队与入队

  5. 当队列为空时,所有出队(遍历)完成

图解:

数据结构之二分搜索树(Binary Search Tree)

 public void levelOrder(){ Queue<Node> queue = new LinkedList<>(); //根节点入队 queue.add(root);  //如果队列不为空,循环出队,与入队 if(!queue.isEmpty()){ //出队 Node cur = queue.poll(); System.out.println(cur.e); //入队 if(cur.left != null){ queue.add(cur.left); }  if(cur.rigth != null){ queue.add(cur.rigth); } } }

删除最小值

思路分析

  1. 根据二分搜索树的定义,最小节点在左子树上

  2. 一路往左子树递归,当这个节点的左子树为空时,说明当前节点就是最小的

  3. 找到最小节点之后删除,并返回一个删除之后的新的节点

  4. 如果删除节点没有又孩子,返回节点就是空(也是一个节点),如果有又孩子,返回节点就是又孩子

  5. 把返回节点(新的二叉树),挂接到删除节点上一个节点的左子树上

图解

数据结构之二分搜索树(Binary Search Tree)

 //寻找最小位置 public E minimun(){ //判断元素的个数 if(size == 0){ throw new IllegalArgumentException("BST is empty!"); } return minimun(root).e; }  //返回以node的为根的二分搜索树最小值所在的节点 private Node minimun(Node node){ //递归终止条件.节点的左孩子为空 if(node.left == null){ return node; }  //处理递归函数 return minimun(node.left); }  //删除最小值,并返回 public E removeMin(){ E ret = (E) minimun(root); removeMin(root); return ret; }  //删除以node为根的二分搜索树中的最小节点 //返回删除节点后新的二分搜索树的根 private Node removeMin(Node node){ //递归终止条件,节点左孩子为空 if(node.left == null){ //先暂存一下这个删除节点的右孩子 Node rightNode = node.rigth; size--; return rightNode; }  //处理递归函数 //没有到递归结束时,说明还有左孩子,需要继续递归 //递归一次,左孩子就会发生变化,需要重新接住左孩子 node.left = removeMin(node.left); return node; }


删除最大值.同理

public E maximun(){ if(size == 0){ throw new IllegalArgumentException("BST is empty!"); } return maximun(root).e; }
private Node maximun(Node node){ if(node.rigth == null){ return node; } return maximun(node.rigth); }
public E removeMax(){ E ret = maximun(); removeMax(root); return ret; }
private Node removeMax(Node node){ if(node.rigth == null){ Node leftNode = node.left; node.left = null; size--; return leftNode; } node.rigth = removeMax(node.rigth); return node; }

删除任意位置

思路:如果节点只有右子树,或者只有左子树,逻辑和删除最大最小差不多

  1. 找到删除节点的后继(就是离删除节点最近,比这个节点所有左孩子都大,右孩子都小)

  2. 这个后继,就是右孩子的最小值

  3. 这个后继脱离,替换待删除节点,删除节点脱离整个二叉树

//从二分搜索树中删除元素为e的节点 public void remove(E e){ root = remove(root, e); }
//删除以node为根二分搜索树中值为e的节点,递归算法 //返回删除节点后新的二分搜索树的根 private Node remove(Node node, E e){ //如果node为空,直接返回null 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.rigth = remove(node.rigth, e); return node; } else{ //e == node.e //删除位置的左孩子为空 if(node.left == null){ //逻辑通删除最小节点差不多 Node rightNode = node.rigth; node.rigth = null; size--; return rightNode; } //删除位置的又孩子为空 if(node.rigth == null){ Node leftNode = node.left; node.left = null; size--; return leftNode; } //待删除节点左右都不为空 //找到比待删除节点最小的节点,及待删除节点又子树的最小节点 //使用minimun();返回就是上述所需要的节点 Node succeror = minimun(node.rigth);//所谓的后继S //后继的又孩子,接上removeMin(node.right)这个返回新的二分搜索树的根 //顺带着node.right及succeror也脱离了 succeror.rigth = removeMin(node.rigth); size++;//实际succeror指向着,所以需要++ //后继左子树指向待删除元素的左子树 succeror.left= node.left; //把待删除节点脱离 node.left = node.rigth = null; size--; //返回删除后的节点 return succeror; } }


以上是关于数据结构之二分搜索树(Binary Search Tree)的主要内容,如果未能解决你的问题,请参考以下文章

[LeetCode] Closest Binary Search Tree Value 最近的二分搜索树的值

132.Find Mode in Binary Search Tree(二分搜索树的众数)

[LeetCode] Closest Binary Search Tree Value II 最近的二分搜索树的值之二

#14 二分查找(Binary Search)

数据结构05红-黑树基础----二叉搜索树(Binary Search Tree)

Algorithms - Data Structure - Binary Search Tree - 数据结构之二叉搜索树