树、二叉树、二叉搜索树(二叉查找树)

1 树

1.1 定义

file
结合图看,可以比较直观地发现,树(Tree)是元素的集合,每棵树由多个节点(node)组成,用以储存元素。某些节点之间存在着一定的关系,用连线表示,连线称为边(edge)或者链接。边的上端点成为父节点,下端称为子节点。最上边的节点为根节点(S节点)。没有子节点的节点则称为叶子节点或叶节点(A、R、X 节点)。父节点是一个节点,所以它们被称为兄弟节点(E、X节点)。

每个节点可以有多个子节点,而该节点则是相应子节点的父节点。但是每个节点只能有一个父节点(只有一个例外,也就是根节点,它没有父节点)。

  • 树是元素的集合
  • 该集合可以为空。此时树中没有元素,称之为空树(empty tree)。
  • 如果该集合不为空,那么该集合至少含有一个根节点以及 0 个或多个子树。根节点与它的子树的根节点用一个边(edge)或链接相连。

1.2 特征

关于树,有三个比较相似,容易搞混的概念:高度(Height)、深度(Depth)、层(Level)。它们的定义如下。
file

2 二叉树

2.1 定义

二叉树是一种特殊的数据结构,顾名思义,二叉树只有两个叉,也就是两个子节点:左子节点和右子节点。其中,左子节点是左子树的根节点,右子节点是右子树的根节点。当然,这并不是说,二叉树一定要求每个节点都必须有两个子节点,有的节点只有左子节点,而有的节点只有右子节点。
file
编号 2 的二叉树中,叶节点全都在最底层,除了叶节点之外,每个节点都于左右两个子节点,这种二叉树就叫满二叉树。编号为 3 的二叉树中,叶子结点在最底下两层,其中,最后一层的节点都靠左排列,并且,除了最后一层,其他层的节点数都要达到最大,即没有单一节点。这种二叉树叫做完全二叉树

2.2 二叉树的遍历方法

即:本身的位置

  • 前序遍历(本身 -> 左节点 -> 右节点):若二叉树为空,则空操作,否则,对于二叉树中的任意节点,先访问这个节点,然后再访问它的左子树,最后打印它的右子树。
  • 中序遍历(左节点 -> 本身 -> 右节点):若二叉树为空,则空操作,否则,对于二叉树中的任意节点,先访问它的左子树,然后再访问这个节点本身,最后访问它的右子树。
  • 后序遍历(左节点 -> 右节点 -> 本身):若二叉树为空,则空操作,否则,对于二叉树中的任意节点,先访问它的左子树,然后访问它的右子树,最后访问这个节点本身。
    file
遍历方式遍历结果
前序遍历S -> E -> B -> A -> C -> P -> O -> R -> X
中序遍历A -> B -> C -> E -> O -> P -> R -> S -> X
后序遍历A -> C -> B -> O -> R -> P -> E -> X -> S

2.3 树与二叉树的区别

  • 二叉树只能有两个子节点,而树没有限制。
  • 二叉树具有有序性。中两个节点被称为左子树和右子树,即使只有一个子节点,也要只能是左还是右。
  • 树不能为空,至少含有一个节点,而一棵二叉树可以为空。

3 二叉查找树(二叉搜索树)

3.1 定义

二叉查找树(Binary Search Tree,BST)是一种特殊的二叉树,一棵二叉搜索树(BST)是一棵二叉树,其中,每个节点的值都要大于其左子树中任意节点的值而小于右子树中任意节点的值

3.2 基本操作

3.2.1 查找

例如在下面标中查找R:
file

  1. 如果二叉搜索树为空则直接返回。
  2. 如果二叉搜索树根节点为R,直接返回。
  3. 如果节点小于R节点,则向右边递归查找。
  4. 如果节点大于R节点,则向左边递归查找。

查好顺序为:S -> E -> P -> R。

3.2.2 插入

在二叉树中插入一个节点,一般都是插入到叶节点上,所以只需从根结点开始,依次遍历比较要插入的数据和节点的大小关系。
file
具体步骤:

  1. 如果树是空的,则直接将新节点插入,否则,执行下面步骤。
  2. 要插入的数据比根节点数据大,则到右子树中插入新数据,如果右子树为> 空,则将新数据直接插入到右子节点的位置;不为空,则继续遍历右子树,查找插入位置。
  3. 要插入的数据比根节点数据小,则到左子树中插入数据,如果左子树为空,则直接将新数据插入到左子节点的位置;不为空,则继续遍历左子树,查找插入的位置。
3.2.3 删除
情况1:没有子节点

如果要删除的节点没有子节点,直接将父节点指向要删除节点的指针指向 null。
file
这种情况是最简单的,我们只需要删除该节点和父节点的关系即可。删除的时候需要先判断自己和父节点的关系是左侧还是右侧,判断方式很简单,如下:

//这里忽略了父节点不存在的情况,最后会巧妙的处理这种情况
if(node.parent.left == node){
    node.parent.left = null;
} else {
    node.parent.right = null;
}   

如果父节点的左节点是自己,就清左侧,否则就是右侧。删除后如下图所示:
file

情况2:存在左节点或者右节点时

如果要删除的节点只有一个节点,即只有左子节点或右子节点,则将父节点指向要删除节点的指针指向要删除节点的子节点即可。
file

情况3:同时存在左右子节点

如果要删除的节点有两个子节点,则需要先找到这个节点右子树中的最小节点或者左子树中的最大节点,将其替换到要删除的节点上。然后删除这个右子树中的最小节点或左子树中的最大节点。
file
删除之后:
file

3.2.4 查找最大、最小节点。
  1. 如果二叉查找树为空,则返回空操作。
  2. 如果不为空,则判断是否只有一个节点(即只有根节点),如果是则返回根节点。
  3. 查找二叉查找树的最大节点时,则到右子树中递归查找。查找二叉查找树的最小节点时,则到左子树中递归查。

代码:

查找:

import java.util.ArrayList;
import java.util.List;

/**
 * @author weilai
 * @email 352342845@qq.com
 * @date 2020/2/14 4:55 下午
 */
public class BinarySearchTree {
    /**
     * 二叉查找树
     */
    private Node tree;

    /**
     * 演示遍历,用来存放数据的集合
     */
    private List<Node> each = new ArrayList<>();

    public void clear() {
        tree = null;
        each = new ArrayList<>();
    }

    public void clearEach() {
        each = new ArrayList<>();
    }

    public void clearTree() {
        tree = null;
    }

    public static class Node {
        private int data;
        private Node left;
        private Node right;

        public Node(int data) {
            this.data = data;
        }

        public int getData() {
            return data;
        }
    }

    /**
     * 前序遍历
     */
    public List<Node> dlrEach() {
        Node p = tree;
        if (p == null) {
            return null;
        }
        if (p.left == null && p.right == null) {
            each.add(p);
            return each;
        }
        dlrEach(p);
        return each;
    }

    private void dlrEach(Node node) {
        if (node != null) {
            each.add(node);
            dlrEach(node.left);
            dlrEach(node.right);
        }
    }

    /**
     * 中序遍历
     */
    public List<Node> ldrEach() {
        Node p = tree;
        if (p == null) {
            return null;
        }
        if (p.left == null && p.right == null) {
            each.add(p);
            return each;
        }
        ldrEach(p);
        return each;
    }

    private void ldrEach(Node node) {
        if (node != null) {
            ldrEach(node.left);
            each.add(node);
            ldrEach(node.right);
        }
    }

    /**
     * 后序遍历
     */
    public List<Node> lrdEach() {
        Node p = tree;
        if (p == null) {
            return null;
        }
        if (p.left == null && p.right == null) {
            each.add(p);
            return each;
        }
        lrdEach(p);
        return each;
    }

    private void lrdEach(Node node) {
        if (node != null) {
            lrdEach(node.left);
            lrdEach(node.right);
            each.add(node);
        }
    }

    /**
     * 查找
     */
    public Node find(int data) {
        Node p = tree;
        while (p != null) {
            if (data < p.data) {
                p = p.left;
            } else if (data > p.data) {
                p = p.right;
            } else {
                return p;
            }
        }
        return null; //没有找到
    }

    /**
     * 插入
     */
    public void insert(int data) {
        // 如果根节点,直接返回
        if (tree == null) {
            tree = new Node(data);
            return;
        }

        Node p = tree;
        for (;;){
            if (data > p.data) {
                if (p.right == null) {
                    p.right = new Node(data);
                    return;
                }
                p = p.right;
            } else { //data < p.data
                if (p.left == null) {
                    p.left = new Node(data);
                    return;
                }
                p = p.left;
            }
        }
    }

    /**
     * 删除
     */
    public void delete(int data) {
        Node p = tree; //p 指向要删除的结点,初始化指向根节点
        Node pp = null; //pp 记录的是 p 的父节点

        while (p != null && p.data != data) {
            pp = p;
            if (data > p.data) p = p.right;
            else p = p.left;
        }
        if (p == null) return; //没有找到

        //要删除的节点有两个子节点
        if (p.left != null && p.right != null) {//查找右子树中最小的节点
            Node minP = p.right;
            Node minPP = p; //minPP 表示 minP 的父节点
            while (minP.left != null) {
                minPP = minP;
                minP = p.left;
            }
            p.data = minP.data; //将 minP 的数据替换到 p 中
            p = minP; //下面就变成了删除 minP 了,要结合整个删除函数来看
            pp = minPP;
        }

        //删除的是叶子节点或者仅有一个子节点
        Node child; //p 的子节点
        if (p.left != null) child = p.left;
        else if (p.right != null) child = p.right;
        else child = null;

        if (pp == null) tree = child; // 删除的是根节点
        else if (pp.left == p) pp.left = child;
        else pp.right = child;
    }

    /**
     * 查找最小节点
     */
    public Node findMin() {
        if (tree == null) return null;
        Node p = tree;
        while (p.left != null) {
            p = p.left;
        }
        return p;
    }

    /**
     * 查找最大节点
     */
    public Node findMax() {
        if (tree == null) return null;
        Node p = tree;
        while (p.right != null) {
            p = p.right;
        }
        return p;
    }

}

使用示例:

public static void main(String[] args) {
    BinarySearchTree binarySearchTree = new BinarySearchTree();
    int[] arr = new int[]{50, 30, 80, 20, 36, 34, 32, 35, 40, 37, 48, 70, 75, 100};
    for (int value : arr) {
        binarySearchTree.insert(value);
    }
    List<BinarySearchTree.Node> nodeList = binarySearchTree.dlrEach();
    for (BinarySearchTree.Node node : nodeList) {
        System.out.print(node.getData() + ",");
    }
    System.out.println();
    binarySearchTree.clearEach();
    nodeList = binarySearchTree.ldrEach();
    for (BinarySearchTree.Node node : nodeList) {
        System.out.print(node.getData() + ",");
    }
    System.out.println();
    binarySearchTree.clearEach();
    nodeList = binarySearchTree.lrdEach();
    for (BinarySearchTree.Node node : nodeList) {
        System.out.print(node.getData() + ",");
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值