数据结构与算法学习——二叉排序树

简介: 数据结构与算法学习——二叉排序树

数据结构与算法学习——二叉排序树


目录

博主介绍

简介

二叉排序树的生成与节点插入

1、生成

二叉树的前中后序遍历

1、递归实现

1.2、非递归实现

二叉排序树节点的删除

1、编写用于搜索待删除节点和待删除节点父节点的方法

2、删除叶子节点

3、删除有一棵子树的节点

4、删除有两棵树的节点

5、二叉排序树中根据节点数据删除节点的方法

使用递归获取二叉树深度

测试

1、测试二叉排序树的生成和插入

2、测试二叉排序树的删除


💫点击直接资料领取💫


目录

博主介绍

💂 个人社区:CSDN全国各地程序猿


🤟作者介绍:中国DBA联盟(ACDU)成员,CSDN全国各地程序猿(媛)聚集地管理员。目前从事工业自动化软件开发工作。擅长C#、Java、机器视觉、底层算法等语言。2019年成立柒月软件工作室,2021年注册苏州凯捷智能科技有限公司


💬如果文章对你有帮助,欢迎关注、点赞、收藏(一键三连)和C#、Halcon、python+opencv、VUE、各大公司面试等一些订阅专栏哦


💅 有任何问题欢迎私信,看到会及时回复


👤 微信号:stbsl6,微信公众号:苏州程序大白


简介

二叉排序树亦称二叉查找树,是树形数据结构的一种,在一般情况下,二叉排序树的查找效率要高于普通链表,它要么是一棵空树,要么具有以下性质:


若它的左子树不为空,则左子树上所有结点的值均小于它的根结点的值;


若它的右子树不为空,则右子树上所有结点的值均大于它的根结点的值;


它的左、右子树分别为二叉排序树。


下面是一棵标准的二叉排序树。


bab7d98b4590468ca8b0decba68a3019.png


二叉排序树的生成与节点插入


1、生成

1、创建Node类和Tree类 :


创建一个Node类作为二叉排序树的节点类,这里省略getter、setter和toString方法。


public class Node {
    // 节点的值
    int data;
    // 当前节点的左子节点
    Node left;
    // 当前节点的右子节点
    Node right;
}    


创建一个Tree类,这个类包含一个Node类型的root属性。


public class Tree {
    // 当前树的根节点
    Node root;
}    


2、生成思路:


既可以在创建二叉树对象时直接使用有参构造函数传入根节点对象,也可以在添加节点时才插入root节点。


注:当一棵树root节点为空时,第一个插入该树的节点就是根节点。


2、节点插入


1、解题思路:


在Tree类中添加一个addNode方法,如果当前树的根节点为空,那么将要添加到二叉排序树的节点设置为根节点,否则就调用root节点对象的add方法,在root对象的add方法中:


如果传入要添加的节点node为空,那么直接返回,不做添加。


如果传入要添加的节点node的数值小于当前节点的数值,那么进行判断,如果当前节点的左子树为空,那么直接让当前节点的左子树为要添加的节点node。否则向左进行递归添加,判断待添加节点node的数据与当前左子节点数据的关系,重复以上操作。


如果传入要添加的节点node的数值大于等于当前节点的数值,这种情况需要尽量避免,这个时候进行判断,如果当前节点右子树为空,那么令当前节点右子树等于要添加的节点node。否则向右进行递归添加,判断待添加节点node的数据与当前右子节点数据的关系,重复以上操作。


2、插入节点–Tree


public void addNode(Node node) { 
    if(this.root == null) { 
        this.root = node; 
    } else { 
        this.root.add(node); 
    } 
}


3、节点的比较与插入–Node


比较节点树的静态方法如下:


public static boolean compare(Node node1,Node node2) { 
    return node1.data > node2.data; 
} 


Node类中插入节点的方法如下:


public void add(Node node) { 
    if(node == null) { 
        return; 
    } 
    if(compare(this,node)) { 
        if(this.left == null) { 
            this.left = node; 
        } else { 
            this.left.add(node); 
        } 
    } else { 
        if(this.right == null) { 
            this.right = node; 
        } else { 
            this.right.add(node); 
        } 
    } 
}


二叉树的前中后序遍历


前序遍历的顺序:根节点–左子节点–右子节点。
中序遍历的顺序:左子节点–根节点–右子节点。
后序遍历的顺序:左子节点–右子节点–根节点。


1、递归实现


1、前序遍历


先输出当前节点,然后判断当前节点的左子树是否为空,如果不为空,就向左递归进行前序遍历。然后判断当前节点的右子树是否为空,若不为空,向右递归进行前序遍历。


//Tree类 
public void preOrder() { 
    if(this.root != null) { 
        this.root.preOrder(); 
    } else { 
        System.out.println("二叉树为空,无法遍历!"); 
    } 
} 
//Node类 
public void preOrder() { 
    System.out.println(this); 
    if(this.getLeft() != null) { 
        this.getLeft().preOrder(); 
    } 
    if(this.getRight() != null) { 
        this.getRight().preOrder(); 
    } 
}


2、中序遍历


先判断当前节点左子树是否为空,若不为空,向左递归进行中序遍历,然后输出当前节点;最后判断当前节点的右子树是否为空,若不为空,向右递归进行中序遍历。


//Tree类 
public void infixOrder() { 
    if(this.root != null) { 
        this.root.infixOrder(); 
    } else { 
        System.out.println("二叉树为空,无法遍历!"); 
    } 
} 
//Node类 
public void infixOrder() { 
    if(this.getLeft() != null) { 
        this.getLeft().infixOrder(); 
    } 
    System.out.println(this); 
    if(this.getRight() != null) { 
        this.getRight().infixOrder(); 
    } 
}


3、后序遍历


先判断当前节点左子树是否为空,若不为空,向左递归进行后序遍历;然后判断当前节点的右子树是否为空,若不为空,向右递归进行后序遍历。最后输出当前节点。


//Tree类 
public void postOrder() { 
    if(this.root != null) { 
        this.root.postOrder(); 
    } else { 
        System.out.println("二叉树为空,无法遍历!"); 
    } 
} 
//Node类 
public void postOrder() { 
    if(this.getLeft() != null) { 
        this.getLeft().postOrder(); 
    } 
    if(this.getRight() != null) { 
        this.getRight().postOrder(); 
    } 
    System.out.println(this); 
}


1.2、非递归实现

我们需要使用到栈这一数据结构来解决问题。


1、前序遍历


如果当前节点不为空,先输出当前节点信息,然后将该节点压入栈,并将指针移动到当前节点的左子节点,此时如果该左子树为空,就退出循环,此时如果栈不为空,就弹出栈顶数据,将指针移动到当前结点右子树,循环,直到栈空或者当前节点为空。


public void preOrder(Node node) { 
    Stack<Node> nodeStack = new Stack<>(); 
    while(node != null || !nodeStack.empty()) { 
        while(node != null) { 
            System.out.println(node); 
            nodeStack.push(node); 
            node = node.getLeft(); 
        } 
        if(!nodeStack.empty()) { 
            node = nodeStack.pop(); 
            node = node.getRight(); 
        } 
    } 
}


2、中序遍历


如果当前节点不为空,将当前节点压入栈中,然后将指针指向当前节点左子树,直到左子树为空,此时栈不为空,将栈顶元素弹出并输出后,将指针移动到当前结点右子树,循环,直到栈空或者当前节点为空。


public void midOrder(Node node) { 
    Stack<Node> nodeStack = new Stack<>(); 
    while(node != null || !nodeStack.empty()) { 
        while(node != null) { 
            nodeStack.push(node); 
            node = node.getLeft(); 
        } 
        if(!nodeStack.empty()) { 
            node = nodeStack.pop(); 
            System.out.println(node); 
            node = node.getRight(); 
        } 
    } 
}


3、后序遍历


需要利用到一个辅助栈用于输出结果,由于栈具有先进后出的特点,而后序遍历的顺序是左右根,所以压入栈顺序为根、右、左。最后使用辅助栈输出。


public void postOrder(Node node) { 
    if(node == null) { 
        System.out.println("要遍历的二叉树为空!"); 
        return; 
    } 
    Stack<Node> stack = new Stack<>(); 
    //辅助栈 
    Stack<Node> assistStack = new Stack<>(); 
    while(node != null || !stack.isEmpty()) { 
        while(node != null) { 
            stack.push(node); 
            assistStack.push(node); 
            node = node.getRight(); 
        } 
        if(!stack.isEmpty()) { 
            node = stack.pop(); 
            node = node.getLeft(); 
        } 
    } 
    while(!assistStack.isEmpty()) { 
        System.out.println(assistStack.pop()); 
    } 
}


二叉排序树节点的删除


二叉排序树中的节点可以分为以下三种:
叶子节点
有一棵子树的节点
有两棵子树的节点
我们需要判断待删除节点为什么类型,然后根据节点类型进行删除操作。


1、编写用于搜索待删除节点和待删除节点父节点的方法


1、搜索待删除节点的方法


搜索待删除节点,首先判断当前二叉排序树是否为空,若为空,直接返回,否则调用当前二叉排序树根节点的search方法。


如果当前传入的数据data的值刚好等于当前节点的data值,那么当前节点就是待删除节点,直接返回即可。


如果当前传入的数据data的值小于当前节点的值且当前节点的左子树为空,证明当前二叉排序树中没有要删除的节点;如果当前传入数据data值小于当前节点值且当前节点左子树不为空,那么调用当前节点左子树的search方法,向左递归查询。


同理,如果当前传入的数据data值大于当前节点值且当前节点右子树为空,证明当前二叉排序树没有要删除节点,此刻只能返回null;如果当前传入数据data值大于当前节点且当前节点右子树不为空,那么调用当前右子树的search方法,向右递归查询。


代码如下:


/*** 
 * 根据节点的data数据搜索Node节点 
 * @param data 目标节点的data值 
 * @return 如果找到符合条件的node节点,那么返回该node节点 
 *         否则返回null 
 */ 
public Node search(int data) { 
    if(data == this.data) { 
        return this; 
    } else if(data < this.data) { 
        if(this.left == null) { 
            return null; 
        } 
        return this.left.search(data); 
    } else { 
        if(this.right == null) { 
            return null; 
        } 
        return this.right.search(data); 
    } 
}


2、搜索待删除节点父节点的方法


搜索待删除节点的父节点的方法,同理先判断当前二叉排序树的根节点是否为空,若为空,返回null,否则调用当前二叉排序树的根节点的搜索待删除节点父节点(searchParent)的方法。


如果当前节点的左子树不为空且当前节点左子树的data值等于用户传入的要检索的data值或者当前节点右子树不为空且当前节点右子树的data值等于用户传入的要检索的data值,那么证明当前节点就是待检索节点,返回当前节点。


如果传入的data值小于当前节点值且当前左子树不为空,那么调用当前左子节点的searchParent的方法。


同理,如果传入data值大于等于当前节点的data值且当前节点右子树不为空,那么调用当前节点右子节点的searchParent方法。


如果程序走到此处,证明没有找到待删除数据的父节点,此时返回null。


代码如下:


/*** 
 * 查找要删除节点的父节点 
 * @param data 要删除的节点的数据 
 * @return 待删除节点的父节点 
 */ 
public Node searchParent(int data) { 
    if((this.left != null && this.left.data == data) || (this.right != null && this.right.data == data)) { 
        return this; 
    } else { 
        //如果要查找的值小于当前节点值,且当前节点左子节点不为空 
        //递归向左 
        if(data < this.data && this.left != null) { 
            return this.left.searchParent(data); 
        } else if(data >= this.data && this.right != null) { 
            return this.right.searchParent(data); 
        } else { 
            return null; 
        } 
    } 
}


2、删除叶子节点


对于叶子节点,我们需要先找到待删除节点target和待删除节点的父节点parent,然后判断待删除叶子节点是其父节点的左子树还是右子树,如果为左子树,那么令parent.left = null,否则让parent.right = null。


二叉排序树中判断待删除节点是否为叶子节点的静态方法与Node类中判断传入节点为当前左子树/右子树的方法:


/*** 
 * 判断node节点是否为叶子节点 
 * @param node 
 * @return 
 */ 
public static boolean isLeaf(Node node) { 
    return node.left == null && node.right == null; 
} 
/*** 
 * 判断传入节点是否为当前节点左子节点的方法 
 * @param target 
 * @return 
 */ 
public boolean isLeft(Node target) { 
    return this.left != null && this.left.data == target.data; 
} 
/*** 
 * 判断传入节点是否为当前节点右子节点的方法 
 * @param target 
 * @return 
 */ 
public boolean isRight(Node target) { 
    return this.right != null && this.right.data == target.data; 
}


3、删除有一棵子树的节点


对于有一棵子树的节点的删除,我们需要找到待删除节点和待删除节点的父节点,然后判断两个条件:1(待删除节点是其父节点的左子树还是右子树)、2(待删除节点有左子树还是右子树)。


如果待删除节点有左子树且为其父节点的左子树:此时让待删除节点的父节点的左子树等于待删除节点的左子树,即parent.left = target.left。


如果待删除节点有左子树且为其父节点的右子树:此时根据二叉排序树的性质,待删除节点的左子树的数据要全部大于(等于)其父节点的数据,所以令待删除节点父节点的右子树等于待删除节点的左子树,即parent.right = target.left。


如果待删除节点有右子树且为其父节点的左子树:此时让parent.left = target.right。


如果待删除节点有右子树且为其父节点的右子树,此时让parent.right = target.right。


Node类中判断传入节点是否为当前节点左/右子树的方法。


/*** 
 * 判断传入节点是否为当前节点左子节点的方法 
 * @param target 
 * @return 
 */ 
public boolean isLeft(Node target) { 
    return this.left != null && this.left.data == target.data; 
} 
/*** 
 * 判断传入节点是否为当前节点右子节点的方法 
 * @param target 
 * @return 
 */ 
public boolean isRight(Node target) { 
    return this.right != null && this.right.data == target.data; 
}


4、删除有两棵树的节点


对于有两棵子树的节点的删除:需要先取到待删除节点的右子树的最小节点的值,然后将数值最小的节点删除,最后将前面取到的最小节点的值赋值给待删除节点。


获取待删除节点右子树中最小节点的值并删除最小节点的方法。


/*** 
 * 1 返回以node为根节点的二叉排序树的最小节点的值 
 * 2 删除以node为根节点的二叉排序树的最小节点 
 * @param node 传入的节点(二叉排序树的根节点) 
 * @return 返回的以Node为根节点的二叉排序树的根节点的值 
 */ 
public int delTreeMin(Node node) { 
    Node target = node; 
    //循环的查找左节点,就能找到最小值 
    while(target.left != null) { 
        target = target.left; 
    } 
    //此时循环结束后,target指向最小节点 
    //删除最小节点 
    delNode(target.data); 
    return target.data; 
}


判断当前节点是否有两棵子树的方法。


/*** 
 * 判断传入节点是否有左右子树的方法 
 * @param node 
 * @return 
 */ 
public static boolean hasTwoSon(Node node) { 
    return node.left != null && node.right != null; 
}


5、二叉排序树中根据节点数据删除节点的方法


/*** 
 * 根据data删除节点 
 * @param data 要删除的节点的data 
 */ 
public void delNode(int data) { 
    if(root == null) { 
        return; 
    } else { 
        //1 先找到要删除的节点target 
        Node target = search(data); 
        //1.1 如果要删除的节点不存在,直接返回 
        if(target == null) { 
            return; 
        } 
        //1.2 如果当前树只有一个节点,且为待删除节点,那么直接置空 
        if(root.left == null && root.right == null) { 
            root = null; 
            return; 
        } 
        //2 去找到target的父节点 
        Node parent = searchParent(data); 
        //2.1 如果要删除的节点是叶子节点 
        if(isLeaf(target)) { 
            //a 判断target是父节点的左子节点还是有子节点 
            if(parent.isLeft(target)) { 
                //如果是左子节点 
                parent.left = null; 
            } else if(parent.isRight(target)) { 
                //如果是右子节点 
                parent.right = null; 
            } 
        } else if(hasTwoSon(target)) { 
            //2.2 如果要删除的节点有左右子树 
            //删除右子树最小节点,同时将最小节点的值取出来 
            int minData = delTreeMin(target.right); 
            target.data = minData; 
        } else { 
            //2.3 删除只有一棵子树的节点 
            //如果待删除节点有左子节点 
            if(target.left != null) { 
                //如果target是parent的左子节点 
                if(parent.isLeft(target)) { 
                    parent.left = target.left; 
                } else { 
                    //说明target是parent的右子节点 
                    parent.right = target.left; 
                } 
            } else { 
                //待删除节点有右子节点 
                //如果target是parent的左节点 
                if(parent.isLeft(target)) { 
                    parent.left = target.right; 
                } else { 
                    parent.right = target.right; 
                } 
            } 
        } 
    } 
}


使用递归获取二叉树深度


/*** 
 * 递归获取二叉树深度的方法 
 * 如果根为空:返回0 
 * 否则分别递归深入左右节点,返回深度 
 * @param root 二叉树的根节点 
 * @return 二叉树深度 
 */ 
public static int getTreeDepth(Node root) { 
    return root == null ? 0 : (1 + Math.max(getTreeDepth(root.left), getTreeDepth(root.right))); 
}


测试


Tree tree = new Tree(null);
int[] arr = {7,8,19,-1,26,100,777,-1012,222,18};
for (int i = 0; i < arr.length; i++) {
    int data = arr[i];
    Node node = new Node(data);
    tree.addNode(node);
}
System.out.println("tree:");
Tree.show(tree.root);


运行结果:




2、测试二叉排序树的删除

1、删除叶子节点


System.out.println("---------------------------------删除叶子节点-1012,删除前:"); 
Tree.show(tree.root); 
tree.delNode(-1012); 
System.out.println("---------------------------------删除叶子节点-1012,删除后:"); 
Tree.show(tree.root);


删除前:



删除后:




2、删除有一棵子树的节点


System.out.println("---------------------------------删除有一棵树的节点-1,删除前:"); 
Tree.show(tree.root); 
tree.delNode(-1); 
System.out.println("---------------------------------删除有一棵树的节点-1,删除后:"); 
Tree.show(tree.root);


删除前:



删除后:



3、删除有两棵子树的节点


System.out.println("---------------------------------删除有两棵子树的节点7,删除前:"); 
Tree.show(tree.root); 
tree.delNode(7); 
System.out.println("---------------------------------删除有两棵子树的节点7,删除后:"); 
Tree.show(tree.root);


删除前:




删除后:





相关文章
|
23天前
|
存储 算法 数据安全/隐私保护
【Python学习篇】Python实验小练习——高级数据结构(五)
【Python学习篇】Python实验小练习——高级数据结构(五)
34 1
|
13天前
|
存储 算法 Go
算法学习:数组 vs 链表
算法学习:数组 vs 链表
17 0
|
17天前
|
机器学习/深度学习 算法 PyTorch
【从零开始学习深度学习】43. 算法优化之Adam算法【RMSProp算法与动量法的结合】介绍及其Pytorch实现
【从零开始学习深度学习】43. 算法优化之Adam算法【RMSProp算法与动量法的结合】介绍及其Pytorch实现
|
17天前
|
机器学习/深度学习 算法 PyTorch
【从零开始学习深度学习】40. 算法优化之AdaGrad算法介绍及其Pytorch实现
【从零开始学习深度学习】40. 算法优化之AdaGrad算法介绍及其Pytorch实现
|
17天前
|
机器学习/深度学习 算法 PyTorch
【从零开始学习深度学习】38. Pytorch实战案例:梯度下降、随机梯度下降、小批量随机梯度下降3种优化算法对比【含数据集与源码】
【从零开始学习深度学习】38. Pytorch实战案例:梯度下降、随机梯度下降、小批量随机梯度下降3种优化算法对比【含数据集与源码】
|
2天前
|
存储 NoSQL Redis
Redis系列学习文章分享---第十六篇(Redis原理1篇--Redis数据结构-动态字符串,insert,Dict,ZipList,QuickList,SkipList,RedisObject)
Redis系列学习文章分享---第十六篇(Redis原理1篇--Redis数据结构-动态字符串,insert,Dict,ZipList,QuickList,SkipList,RedisObject)
10 1
|
2天前
|
存储 NoSQL 安全
Redis系列学习文章分享---第十五篇(Redis最佳实践--设计优雅的key+合适的数据结构+持久化如何配置+慢查询问题解决)
Redis系列学习文章分享---第十五篇(Redis最佳实践--设计优雅的key+合适的数据结构+持久化如何配置+慢查询问题解决)
7 1
|
13天前
|
算法 搜索推荐 JavaScript
算法学习:快速排序
算法学习:快速排序
13 1
|
2天前
|
存储 消息中间件 缓存
Redis系列学习文章分享---第十七篇(Redis原理篇--数据结构,网络模型)
Redis系列学习文章分享---第十七篇(Redis原理篇--数据结构,网络模型)
7 0
|
2天前
|
算法 Java
《JVM由浅入深学习【八】 2024-01-12》JVM由简入深学习提升分(JVM的垃圾回收算法)
《JVM由浅入深学习【八】 2024-01-12》JVM由简入深学习提升分(JVM的垃圾回收算法)
5 0