程序员的内功心法,你不来看看吗?

326 阅读6分钟
最近这阵工作,我越来越感受到基础知识能力的重要性。之前都是在赶进度、做业务逻辑,逃避自己基础薄弱的事实。最近下定决心,加强之前薄弱的基础。先从自己之前一直似是而非的搜索树的内容开始吧。

在生活中我们经常会使用到搜索的功能。在我们数据量不大的情况下,可以使用每次遍历全部数据,查询我们的目标数据。当数据量增加时,我们遍历的方式就有些力不从心了;也可以将数据的数据排序,使用比较高效的二分查找方式,但是在插入或删除数据时,数组表现就会很慢。所以我们可以结合二分查找查询的高效 + 链表添加删除的高效性来实现高效搜索(符号表)的情况

下面我将列举一些树的内容定义(后续所有的代码使用Java语言实现)

  • 树由节点构成,每个节点都有一个父节点(根结点不存在父节点)

  • 节点包含的链接可以指向不存在的NULL或者其他真实存在的节点

  • 每个节点都可以包含多个子链接,将子链接的个数称为;树的度是指所有的子节点中最大的度(将度为2的树称为二叉树、度为3的树称为三叉树等)。如图1~3示

  • 叶节点:没有子节点的节点 如图-1的B、C、D节点

  • 父节点:有子树的节点是其子树的根节点的父节点 图-1的A节点是B、C、D节点的父节点

  • 子节点:若A节点是B节点的父节点,那么B是A的子节点,子节点也可以成为孩子节点 图-3的A节点是B、C、D的父节点,同时也是H节点的子节点

  • 兄弟节点:具有相同一个父节点的个节点彼此是兄弟节点 图-1的B、C、D

二叉搜索树

定义

  • 每个节点只指向一个父节点,最多包含左右两个子链接

  • 左子边节点的Key小于父节点、右子节点的Key大于父节点 如图-4示

//二叉搜索树的节点定义public class TreeNode<T extends Comparable<T>>{    T data;    TreeNode<T> left;    TreeNode<T> right;    int size;}//二叉搜索树的定义public class BinarySearchTree<T extends Comparable<T>> {    private TreeNode<T> root;}

查找

//查询算法public TreeNode<T> find(T element, TreeNode<T> node){    if (Objects.isNull(node)) {        return null;    }    T val = node.data;    int res = val.compareTo(element);   //和node节点比较    if (res == 0) {                     //等于node的值,表示查询到        return node;    }    if (res < 0) {                      //节点的值小于要查询的值,向右递归        return find(element, node.getRight());    }    return find(element, node.getLeft());   //节点的值大于查询的值,向左递归}

查询极值(极大/极小值)

根据查找二叉树的特性,极值存在于叶节点或者只包含一个子节点的父节点中

//查询极小值,一直向左查询,如果没有左节点,则认为当前节点最小 例子:A节点public TreeNode<T> findMin(TreeNode<T> node){    if (Objects.isNull(node.getLeft())) {        return node;    }    return findMin(node.getLeft());}//查询极大值,一直向右查询,如果没有右节点,则认为当前节点最大 例子:Z节点public TreeNode<T> findMax(TreeNode<T> node){    if (Objects.isNull(node.getRight())) {        return node;    }    return findMax(node.getRight());}

插入

图-7展示了插入Z的作为F右子节点的情况(插入到左子节点的情况类似,不再赘叙)

图-8展示了被插入节点存在的情况。

public void add(T element) {    if (element == null) {        throw new RuntimeException("数据不能为NULL");    }    TreeNode<T> node = new TreeNode<>();    node.data = element;    if (Objects.isNull(root)) {        root = node;        return;    }    addImpl(root, node);}private void addImpl(TreeNode<T> root, TreeNode<T> node) {    T val = root.data;    T element = node.data;    int sub = element.compareTo(val);    //包含要插入的值,不处理    if (sub == 0) {        return;    }    //插入的值大于根节点的值,将新节点作为根节点的右子节点    if (sub > 0) {        TreeNode<T> right = root.getRight();        if (Objects.isNull(right)) {            root.setRight(node);            return;        }        addImpl(right, node);    } else {        //插入的值小于根节点的值,将新节点作为根节点的左子节点        TreeNode<T> left = root.getLeft();        if (Objects.isNull(left)) {            root.setLeft(node);            return;        }        addImpl(root.getLeft(), node);    }}

删除

由于删除节点比较复杂,我们先看下删除极大值(极小值)的情况,为节点删除做好准备工作

删除最小值

由于二叉搜索树的特点二(左子边节点的Key小于父节点、右子节点的Key大于父节点)那么最小值节点要么是叶子节点或者包含右子节点的情况

  • 极小值节点是叶子节点,可以直接移除

  • 极小值节点有一个右子节点,将右子节点替换为父节点(如果还包含左子节点,那么当前节点非最小值)

//移除最小的节点,将返回的值作为根节点private TreeNode<T> deleteMin(TreeNode<T> node) {    if (Objects.isNull(node.getLeft())) {   //没有左子节点,返回右子节点        return node.getRight();    }    TreeNode<T> min = deleteMin(node.getLeft());    //递归左子树    node.setLeft(min);    return node;}

删除最大值

和删除最小值的情况相似。只不过递归的是右子树

  • 极大值节点是叶子节点,可以直接移除

  • 极大值节点有一个左子节点,将左子节点替换为父节点(如果还包含右子节点,那么当前节点非最大值)

//移除node节点的最大值,使用返回值替换原节点public TreeNode<T> deleteMax(TreeNode<T> node) {    if (Objects.isNull(node.getRight())) {        return node.getLeft();    }    TreeNode<T> max = deleteMax(node.getRight());    node.setRight(max);    return node;}

删除节点

我们将删除节点的情况归纳如下

  • 被删除节点是叶子节点,可以直接移除

  • 被删除节点只包含一个子节点(左子节点或者右子节点),我们需要需要将子节点替换到父节点

  • 被删除节点包含两个子节点,如果直接移除E节点,那么子节点D、F将会丢失。我们需要转换思路,将包含两个子节点的情况转换为上两种情况。下面我们介绍下如何处理(T.Hibbard 1962年提出的方法,膜拜巨佬

  • 我们使用前驱节点(后续节点)的值替换被删除节点,然后删除前驱节点(后继节点)

  • 前驱节点:当前节点的左子树中的最大值

  • 后继节点:当前节点的右子树中的最小值

//删除element的节点,返回根结点的引用public TreeNode<T> delete(T element, TreeNode<T> node){    if (Objects.isNull(node)) {        return null;    }    T val = node.data;    int res = val.compareTo(element);    if (res < 0) {          //被删除节点在node的右子树        TreeNode<T> rNode = delete(element, node.getRight());        node.setRight(rNode);    } else if (res > 0) {   //被删除节点在node的左子树        TreeNode<T> lNode = delete(element, node.getLeft());        node.setLeft(lNode);    } else {                //node为被删除节点        //包含一个子节点,使用子节点替换父节点        if (Objects.isNull(node.getLeft())) {               return node.getRight();        }        if (Objects.isNull(node.getRight())) {            return node.getLeft();        }        //左右节点均存在,使用后继节点代替,移除后继节点        TreeNode<T> tmp = node;        node = findMin(node.getRight());        TreeNode<T> rNode = deleteMin(tmp.getRight());        node.setRight(rNode);        node.setLeft(tmp.getLeft());    }    return node;}

至此,我们已经完成了二叉搜索树的增加、查询、删除的方法。我们发现二叉搜索树的实现并不困难,并且在大多数场景下也能正常运行。二叉搜索树在极端情况的性能也是不可忍受的。

后面我们将讲述一种在任何场景初始化,运行时间都将是对数级的(下一篇预告**平衡查找树**)

系列

《程序员内功心法——搜索二叉树》

《程序员内功心法——AVL树》

本文是在工作之余整理出来的,难免出现逻辑纰漏、排版错误,还请各位客官理解,同时欢迎各位留言和我交流分享!

▼更多精彩推荐,请关注我们▼