(一)树相关的知识
(a)为什么要有树结构?
1.树结构本身是一种天然的组织结构,计算机中的目录,公司的架构就是一种树结构
2.将数据使用结构存储后,出奇的高效
(b)二叉树:和链表一样,是一种动态数据结构
class Node{
E e;
Node left; 左孩子
Node right; 右孩子
}
(c)二叉树的特点:
(1)二叉树具有唯一的根节点
(2)二叉树中每个节点最多有两个孩子,二叉树每个节点最多有一个父亲
(3)叶子结点的左右孩子都为空
(4)二叉树具有天然的递归结构:每个节点的左子树也是二叉树,每个节点的右子树也是二叉树
(5)二叉树不一定是“满”的,满二叉树即为除了叶子结点外的其他节点都存在左右子树
(6)一个节点也是二叉树,空(null)也是二叉树。
(d)二分搜索树的特点
(1)二分搜索树是二叉树
(2)二分搜索树的每个节点的值
1.大于其左子树的所有节点的值
2.小于其右子树的所有节点的值
(3)二分搜索树的每一棵子树也是二分搜索树
(e)
1.什么是遍历操作:便利操作就是把所有节点都访问一遍
2.为什么要遍历:访问原因和业务相关
3.在线性结构下,遍历是极其容易的
二分搜索树的递归操作:
1.对于遍历操作,两棵子树都要顾及
function traverse(node)
if(node == null)
return;
1.(前序遍历)
访问该节点
traverse(node.left)(访问节点的左子树)
traverse(node.right)(访问节点的右子树)前序遍历是最自然的遍历方式,最常用的遍历方式
2.(中序遍历)
traverse(node.left)(访问节点的左子树)
访问该节点
traverse(node.right)(访问节点的右子树)
3.(后序遍历)
traverse(node.left)(访问节点的左子树)
traverse(node.right)(访问节点的右子树)
访问该节点后序遍历的应用:二分搜索树释放内存
二分搜索树的遍历:不论是哪一种遍历方式,每个节点都会被访问三次
因为每个节点都连接着左子树和右子树,访问左子树时得先访问该节点,才可以访问该节点的左子树
访问右子树也得先访问该节点才能够访问其右子树,加上访问节点本身,一共三次访问
二分搜索树遍历的非递归实现
1.二分搜索树的非递归实现,比递归实现要复杂,需要利用栈这种数据结构辅助完成
2.中序遍历和后序遍历的非递归实现更复杂
3.中序遍历和后序遍历的非递归实现,实际应用不广
二分搜索树的层序 遍历(广度优先遍历)
先遍历第0层,再遍历第1层,接着遍历第二层
这种方式逐层向下遍历,在广度上也有了拓展,故也称为广度优先遍历
1.根节点是第0层
广度优先遍历的意义:
1.更快的找到问题的解(相比于深度优先,深度优先容易一下从左子树就到达树的最深层,
在右子树上的解就难以快速找到)
2.常用于算法设计中-最短路径
3.图中的深度优先遍历和广度优先遍历
(二)二分搜索树的底层实现
package cn.data.Set;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
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;
}
/*
//向二分搜索树中添加新的元素e
public void add(E e){
//如果根为空,插入的元素即为根节点
if(root == null){
root = new Node(e);
size++;
}else{
//否则递归调用,将e作为子节点插入
add(root,e);
}
}
//向以node为根的二分搜索树中插入元素e,递归算法
//将元素e插入为node的左孩子或者右孩子。
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;
}
//如果要插入的值比节点元素的值要小,说明要插在node的左节点
//但左节点不为空的话,就递归调用,将其插给左子树的节点
if(e.compareTo(node.e)<0){
add(node.left,e);
}else{
//e.compareTo(node.e)>0
add(node.right,e);
}
}
*/
//向二分搜索树中添加新的元素
public void add(E e){
root = add(root,e);
}
//向以node为根的二分搜索树中插入元素e,递归算法
//返回插入新节点后二分搜索树的根
private Node add(Node node,E e){
//对于一个空的二叉树,插入一个节点后,这个节点就是二叉树本身
if(node == null){
size++;
//将其返回给调用者,进行挂接
return new Node(e);
}
if(e.compareTo(node.e)<0){
//node本身也是一种二叉树, node.left为根节点,插入元素e
//add方法返回的就是以node.left为根节点的二叉树,将其挂接在node的左子树上
node.left = add(node.left,e);
}else if(e.compareTo(node.e)>0){
node.right = add(node.right,e);
}
//上述判断语句没有考虑e.compareTo(node.e)=0的情况,对于这种情况不作任何处理
return node;
}
//看二分搜索树中是否包含元素e
//从二分搜索树的根开始,逐渐缩小二分搜索树的规模,在二分搜索树的子树中缩小问题的规模,
//知道找到元素e或者找不到元素e为止
public boolean contains(E e){
return contains(root,e);
}
//查看以node为根的二分搜索树中是否包含元素e,递归算法
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{//e.compareTo(node.e)>0
//在当前节点的右子树中寻找
return contains(node.right,e);
}
}
//二分搜索树的前序遍历(用户可以使用的方法)
public void preOrder(){
preOrder(root);
}
//前序遍历以node为根的二分搜索树,递归算法
private void preOrder(Node node){
//递归终止条件
if(node == null){
return;
}
//访问节点的形式在这里体现为打印出该节点的值
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
/*
* if(node != null){
* System.out.println(node.e);
* preOrder(node.left);
* preOrder(node.right);
* }
* */
}
//二分搜索树的非递归前序遍历
public void preOrderNR(){
//利用栈这种数据结构实现
Stack<Node> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
//cur表示当前要访问的节点
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 inOrder(){
inOrder(root);
}
//中序遍历以node为根的二分搜索树,递归算法
private void inOrder(Node node){
if(node == null){
return;
}
inOrder(node.left);
System.out.println(node.e);
inOrder(node.right);
}
//二分搜索树的后续遍历
public void postOrder(){
postOrder(root);
}
//后序遍历以node为根的二分搜索树,递归算法
private void postOrder(Node node){
if(node == null){
return;
}
postOrder(node.left);
postOrder(node.right);
System.out.println(node.e);
}
//二分搜索树的层序遍历
public void levelOrder(){
//借助队列Queue这种数据结构,但Java.utils中的Queue是一个接口
//因此在使用的时候必须选择一种具体的底层数据结构
Queue<Node> q = new LinkedList<>();
q.add(root);
//利用循环进行遍历操作
while(!q.isEmpty()){
//要出队的元素就是需要访问的元素
Node cur = q.remove();
//此处访问就用打印输出语句表示
System.out.println(cur.e);
//根据当前访问的节点,对其左右孩子进行操作
if(cur.left != null){
q.add(cur.left);
}
if(cur.right != null){
q.add(cur.right);
}
}
}
//删除二分搜索树中的节点
//从最简单的,删除二分搜索树的最小值和最大值开始
//删除最大值和最小值之前该做的就是找到二分搜索树的最大值和最小值
//二分搜索树的最小值为从根节点开始一直往左走,直到走不动为止,不一定是走到叶子结点
//最大值同理
//寻找二分搜索树的最小元素
public E minimum(){
if(size == 0)
throw new IllegalArgumentException("BST is empty!");
return minimum(root).e;
}
//返回以node为根的二分搜索树的最小值所在的节点
private Node minimum(Node node){
//递归终止条件,如果节点的左子树为空,则该节点就是最小值所在节点
if(node.left == null){
return node;
}
return minimum(node.left);
}
//寻找二分搜索树的最大元素
public E maximum(){
if(size == 0){
throw new IllegalArgumentException("BST is empty");
}
return maximum(root).e;
}
//返回以node为根节点的二分搜索树的最大值所在的节点
private Node maximum(Node node){
if(node.right == null){
return node;
}
return maximum(node.right);
}
//删除二分搜索树的最大值和最小值时,会遇到两种情况
//1.最大值节点或最小值节点为叶子结点,此时删除操作比较简单
//2.最大值节点或最小值节点为非叶子节点,此时删除操作比较复杂
//从二分搜索树中删除最小值所在的节点,返回最小值
//(此种方法有点不胜理解)
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);//返回以node.left为根节点的二分搜索树的根
return node;
}
//从二分搜索树中删除最大值所在节点
public E removeMax(){
E ret = maximum();
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 = removeMax(node.right);
return node;
}
//删除二分搜索树中的任意元素
/**
* 此时存在一种比较复杂的情况,那就是需要删除左右都有孩子的节点d
* 因此删完之后,应该找一个节点替代d的位置,找到s=min(d->right),
* 找到比d节点要大且距离d数值最近(不是挨着最近)的节点,也就是d的右子树中最小的节点(s)
* s是d的后继,然后将d的右子树中的s节点给删掉,s->right = delMin(d->right)
* 然后让s替代d的位置,并让原来d的左子树成为s的左子树,s->left = d->left
* 删除d,则s为新的子树的根
* */
//从二分搜索树中删除元素为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;
}
//待删除的节点左右节点均不为空的情况
//找到比待删除节点大的最小节点,即待删除节点右子树的最小节点
//用这个节点(后继节点successor)顶替待删除节点的位置
Node successor = minimum(node.right);//找到node的右子树中最小的节点
//删除node右子树中最小的节点,并返回最新子树根节点,将其挂接在successor的右子树上
successor.right = removeMin(node.right);
//successor的左子树就为被删除节点的左子树
successor.left = node.left;
//经过上述操作,node节点已经被替代了,故将node节点从树上脱离
node.left = node.right = null;
return successor;
//除了上述操作中用后继节点(node右子树中最小的节点)来替代被删除节点
//也可以使用前驱节点(predecessor)(p=max(d->left)待删除节点左子树中最大的节点)来替代被删除的节点
}
}
//覆盖Object中的toString方法
@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;
}
//若节点不为空,将节点的值添加到StringBuilder中
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();
}
}
(三)相关测试 代码
(1)Main
package cn.data.Set;
public class Main {
public static void main(String[] args) {
BST<Integer> bst = new BST<>();
int[] nums ={5,3,6,8,4,2};
for(int num:nums){
bst.add(num);
// 5
// / \
// 3 6
// / \ \
// 2 4 8
}
bst.levelOrder();
System.out.println("=======");
bst.preOrder();
/*
* 5
3
2
4
6
8
* */
System.out.println("=======");
bst.preOrderNR();
System.out.println();
System.out.println(bst);
bst.inOrder();
//二分搜索树的中序遍历结果是顺序的,相当于把二分搜索树中的元素进行了排列
//因此,有时候二分搜索树也被称为排序树
System.out.println();
bst.postOrder();
}
}
(2)testMoveMin
package cn.data.Set;
import java.util.ArrayList;
import java.util.Random;
public class testRemoveMin {
public static void main(String[] args) {
BST<Integer> bst = new BST<>();
Random random = new Random();
int n = 1000;
for(int i = 0;i<n;i++)
//添加1000个小于10000的随机数,但是有可能添加相等的元素
//但BST不存储重复的元素,因此bst中不一定有1000个元素
bst.add(random.nextInt(10000));
ArrayList<Integer> nums = new ArrayList<>();
while(!bst.isEmpty())
//将二分搜索树中的最小元素添加进nums中
nums.add(bst.removeMin());
System.out.println(nums);
//检验一下nums中的元素是否是由小到大排列
for(int i=1;i<nums.size();i++)
if(nums.get(i-1)>nums.get(i))
throw new IllegalArgumentException("Error");
System.out.println("removeMin test completed");
}
}
(四)二分搜索树的几个基本操作还是很好理解的,但对于其中的删除元素操作不胜理解,需要后续加强理解和运用。
对于非科班出身的人,数据结构每一章学下来,需要耗费巨量的时间,接下来的时间暂停数据结构的学习,开始将更多的时间笔试题做题,在做题过程中将所学知识进行运用。