数据结构与算法--死磕二叉树

死磕二叉树
  • 近一年都比较关注算法相关的知识,也刷了不少题,之前的文章中大多也是算法相关的文章,但是感觉每次遇到树相关的题型都不能应对自如,因此还是有必要在相关知识上下功夫,因此有此次总结,以下是所有树相关的文章

数据结构与算法–面试必问AVL树原理及实现

数据结构与算法–二叉树的深度问题

数据结构与算法–二叉堆(最大堆,最小堆)实现及原理

数据结构与算法–二叉查找树转顺序排列双向链表

数据结构与算法-- 二叉树中和为某一值的路径

数据结构与算法-- 二叉树后续遍历序列校验

数据结构与算法-- 广度优先打印二叉树

数据结构与算法–解决问题的方法- 二叉树的的镜像

数据结构与算法–重建二叉树

数据结构与算法–二叉查找树实现原理

数据结构与算法–二叉树实现原理

数据结构与算法–B树原理及实现

数据结构与算法–数字在排序数组中出现次数

数据结构与算法–死磕二叉树

数据结构与算法–二叉树第k个大的节点

  • 本文中树,二叉树的实现都用自己的实现方式,在以上列举的最后两篇文中有详细的说明
    数据结构与算法--死磕二叉树_第1张图片

  • 二叉树,或者是说树经常用于大量的输入数据的场景下。大部分的操作运行时间平均是O(logN)。

  • 二叉树的变种题型多如牛毛,还是要掌握方法,多看不同题型,训练知识迁移的能力,如下题:

题目:输入一棵二叉树和他的两个节点,求他们的最低公共祖先。
  • 最低公共祖先的定义:给定一个有根树T 时候,对于任意两个节点 U, V,找到一个离根最远的节点X, 使得X同时 是U , V 的祖先,那么X便是 U,V的最近公共祖先。
最简模式

数据结构与算法--死磕二叉树_第2张图片

  • 上题中并没有明显给出树的特性,只是强调了一棵二叉树,那么我们用二叉搜索树为案例来分析如下:
    • 需要找到最低公共祖先,也就是找父节点,二叉树节点的特性,父节点比左节点大,比右节点小
    • 那么会有几种情况,如果两个节点分布在多个分支,那么我们需要找的父节点大小必然介于 V,U之间,情况一
    • 如果二叉树是一个单链,那么我们需要找到 V,U的其中一个父节点,此时改父节点要不不UV都打,要么比UV都小,情况二
    • 用如下图表示:
      数据结构与算法--死磕二叉树_第3张图片

数据结构与算法--死磕二叉树_第4张图片

  • 如上所示的一颗二叉搜索树,当输入的是6, 8 时候,公共祖先就是7 ,介于6, 8 之间

  • 如果输入的是3, 4,公共祖先就是2, 比3,4 都要小,或者反过来都在左子树,那么比输入值都大

  • 经过如上分析,那么我们直接中序遍历树,每次得到节点与输入节点比较,如果介于 UV之间,则返回得到我们需要的节点

  • 如果写范问节点同时大于U,V,并且是U,或者V的父节点,那么该节点也是我们需要的节点

  • 如上分析有如下代码:

/**
 * 输入两个树的节点node1, node2,找到他们最低公共祖先.
 * 最近公共祖先的定义为:对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。
 *
 * @author liaojiamin
 * @Date:Created in 16:31 2021/7/9
 */
public class FindCommonNode {

    public static void main(String[] args) {
        BinaryNode node = new BinaryNode(null, null, null);
        BinarySearchTree searchTree = new BinarySearchTree();
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            node = searchTree.insert(random.nextInt(100), node);
        }
        BinaryNode node1 = new BinaryNode(29, null, null);
        node = searchTree.insert(node1, node);
        BinaryNode node2 = new BinaryNode(45, null, null);
        node = searchTree.insert(node2, node);
        BinaryNode result = findBinarySearchTree(node, node1, node2);
        System.out.println(result.getElement());
    }

    /**
     * 二叉排序树解法
     */
    public static BinaryNode findBinarySearchTree(BinaryNode tree, BinaryNode node1, BinaryNode node2) {
        if (tree == null || node1 == null || node2 == null) {
            return null;
        }
        // node1< tree < node2
        if (node1.compareTo(node2) < 0 && tree.compareTo(node1) > 0 && tree.compareTo(node2) < 0) {
            return tree;
        }
        // node2< tree < node1
        if (node1.compareTo(node2) > 0 && tree.compareTo(node1) < 0 && tree.compareTo(node2) > 0) {
            return tree;
        }
        if (tree.compareTo(node1) > 0 & tree.compareTo(node2) > 0) {
            if (tree.getLeft() == node1 || tree.getLeft() == node2) {
                return tree;
            }
            return findBinarySearchTree(tree.getLeft(), node1, node2);
        }
        if (tree.compareTo(node1) < 0 & tree.compareTo(node2) < 0) {
            if (tree.getRight() == node1 || tree.getRight() == node2) {
                return tree;
            }
            return findBinarySearchTree(tree.getRight(), node1, node2);
        }
        return null;
    }
}
困难模式

数据结构与算法--死磕二叉树_第5张图片

  • 如果不是二叉排序树,只是一个普通的树或者二叉树,并且树中没有指向父节点的指针

  • 分析如下:

    • 不能用比较的方式找父节点,那么用遍历,两个节点都出现在某个节点的子节点,或者直接在某个节点下,如情况一:

数据结构与算法--死磕二叉树_第6张图片

  • 6, 8 都出现在了7 节点下,但是同时也都出现在了5 节点的子节点下

  • 我们需要求解的是最低公共祖先,那么离根节点越远的父节点才是我们需要求解的

  • 我们可以从根遍历一棵树,每次遍历一个节点,判断输入节点是否在他子树中

  • 如果在子树中,则分别遍历他所有子节点,并判断两个输入节点是否他们子树中,

  • 这样从上到下遍历,直到找到这样一个节点,他自己的子树中同时包含两个输入的节点,但是他的任何一个子节点都不会同时拥有这两个节点,那么这就是公共祖先
    数据结构与算法--死磕二叉树_第7张图片

  • 我们举例说明,如上图:

    • 第一种情况,当输入的是如上图中65, 26 时候,还是中序遍历,
    • 根节点中判断是否存在有两个节点,存在与否的判断依然是用递归,如果存在,标记U,V中count=1即可
    • 遍历21 ,依然存在,在遍历24,依然存在,接着65 不存在,26 不存在,说明 24 是我们需要求解的值
    • 第二中情况,当输入的是一个单链上的 32,77 时候,判断就稍微不同
    • 依然中序遍历当遍历到21 时候,我们判断都在右子树中,接着遍历31
    • 此时有不同情况,如果依然继续遍历判断,我们会直接干到 32,发现都存在,到77,发现不存在,那么返回的是32
    • 显然不是我们需要的,此处我们需要判断31 的子节点是否是输入节点,如果是,那么当前节点就是需要求解的
  • 综上也就三种情况,记录 validateLeft为都存在left中, validateRight为都存在right中

    • 当遍历到节点 N, 发现validateLeft = false && validateRight = false,说明一个在左,一个在右,那么得到解 N
    • 当遍历到N 发现validateLeft = true,此时判断 N 的left节点是否是输入节点,如果是,那么N就是我们求解的,否则我们就遍历N的left节点
    • 剩下的就是N 的validateRight = true情况,还是一样,判断right节点是否是输入节点,那么N就是我们求解,否则我们遍历N的right节点
  • 经如上分析有如下代码:

/**
 * 输入两个树的节点node1, node2,找到他们最低公共祖先.
 * 最近公共祖先的定义为:对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。
 *
 * @author liaojiamin
 * @Date:Created in 16:31 2021/7/9
 */
public class FindCommonNode {

    public static void main(String[] args) {
        BinaryNode node = new BinaryNode(null, null, null);
        BinarySearchTree searchTree = new BinarySearchTree();
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            node = searchTree.insert(random.nextInt(100), node);
        }
        BinaryNode node1 = new BinaryNode(29, null, null);
        node = searchTree.insert(node1, node);
        BinaryNode node2 = new BinaryNode(45, null, null);
        node = searchTree.insert(node2, node);
        BinaryNode result2 = findBinaryTree(node, node1, node2);
        System.out.println(result2.getElement());


    }
    /**
     * 非二叉排序树
     */
    public static BinaryNode findBinaryTree(BinaryNode tree, BinaryNode node1, BinaryNode node2) {
        if (tree == null || node1 == null || node2 == null) {
            return null;
        }
        //递归判断修改状态,所以每次都先初始化数量为0
        node1.setCount(0);
        node2.setCount(0);
        boolean left = validateNode(tree.getLeft(), node1, node2);
        node1.setCount(0);
        node2.setCount(0);
        boolean right = left ? false : validateNode(tree.getRight(), node1, node2);
        //情况一
        if (!left && !right) {
            return tree;
        }
        //情况二
        if (left) {
            //特殊情况二叉树为单条链的情况
            if (tree.getLeft() == node1 || tree.getLeft() == node2) {
                return tree;
            }
            return findBinaryTree(tree.getLeft(), node1, node2);
        }
        //情况三
        if (right) {
            if (tree.getRight() == node1 || tree.getRight() == node2) {
                return tree;
            }
            return findBinaryTree(tree.getRight(), node1, node2);
        }
        return null;
    }

    /**
     * 判断节点是否在二叉树中
     */
    public static boolean validateNode(BinaryNode tree, BinaryNode node1, BinaryNode node2) {
        if (tree == null) {
            return false;
        }
        if (tree.compareTo(node1) == 0) {
            node1.setCount(2);
        }
        if (tree.compareTo(node2) == 0) {
            node2.setCount(2);
        }
        if (node1.getCount() == 2 && node2.getCount() == 2) {
            return true;
        }
        boolean leftIn = validateNode(tree.getLeft(), node1, node2);
        boolean rightIn = validateNode(tree.getRight(), node1, node2);
        return leftIn || rightIn;
    }
}
地狱模式

数据结构与算法--死磕二叉树_第8张图片

  • 在以上方案中,当输入是65, 26 时候,在判断完都在13节点下时候,我们其实已经遍历过21, 24 节点了,但是在之后的遍历中,我们任然需要在遍历21, 24,这种思路会出现很多重复的遍历,更快速的解决方案还是有的

  • 之前文章数据结构与算法–两个链表中第一个公共节点给我们启发,如下图
    数据结构与算法--死磕二叉树_第9张图片

  • 上图其实就是一颗二叉树,只不过是斜的,之前用双指针,求第一个公共节点,或者用栈空间求第一个相同的节点接口

  • 受以上启发,如果我们将两个输入节点U, V 的范问路径分别放到两个链表中,不就将二叉树的问题转为以上链表的问题。

  • 还是如上图数据结构与算法--死磕二叉树_第10张图片
    分析如下:

    • 还是先根遍历,当遍历到13 节点,我在13节点对象中定义一个链表,用来存放已经走过的路径,也就是 父节点路径+ 本子节点,得到本节点路径,
    • 那么遍历13,将13 添加进去
    • 遍历21,将21 添加到路径 得到 13 ->21
    • 遍历24,将24 添加到路径 得到 13 ->21 ->24
    • 遍历65,将65 添加到路径 得到 13 ->21 ->24 ->65
    • 遍历26 ,将26 添加到路径 得到 13 ->21 ->24 ->26
    • 此时两个节点都已经完成路径的查询,直接返回,接着分析两个链表
    • 此处我们需要求的是最后一个公共节点,那么我们用栈,分别将两个链表导入两个栈,接着依次导出,求第一个非输入节点,并且相同的节点 得到我们的解,
  • 如上分析有如下代码


/**
 * 输入两个树的节点node1, node2,找到他们最低公共祖先.
 * 最近公共祖先的定义为:对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。
 *
 * @author liaojiamin
 * @Date:Created in 16:31 2021/7/9
 */
public class FindCommonNode {

    public static void main(String[] args) {
        BinaryNode node = new BinaryNode(null, null, null);
        BinarySearchTree searchTree = new BinarySearchTree();
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            node = searchTree.insert(random.nextInt(100), node);
        }
        BinaryNode node1 = new BinaryNode(29, null, null);
        node = searchTree.insert(node1, node);
        BinaryNode node2 = new BinaryNode(45, null, null);
        node = searchTree.insert(node2, node);
        BinaryNode result = findBinarySearchTree(node, node1, node2);
        System.out.println(result.getElement());
        BinaryNode result2 = findBinaryTree(node, node1, node2);
        System.out.println(result2.getElement());
        BinaryNode result3 = buildBinaryLink(node, node1, node2);
        System.out.println(result3.getElement());

    }

    /**
     * 构造两个单向链表
     */
    public static BinaryNode buildBinaryLink(BinaryNode tree, BinaryNode node1, BinaryNode node2) {
        if (tree == null || node1 == null || node2 == null) {
            return null;
        }

        buildListNode(tree, node1, node2);
        ListNode node1List = node1.getLinkedList();
        ListNode node2List = node2.getLinkedList();
        MyStack<BinaryNode> stack1 = new MyStack<>();
        MyStack<BinaryNode> stack2 = new MyStack<>();
        while (node1List != null) {
            if (node1List.getBinaryNode() != null) {
                stack1.push(node1List.getBinaryNode());
            }
            node1List = node1List.getNext();
        }
        while (node2List != null) {
            if (node2List.getBinaryNode() != null) {
                stack2.push(node2List.getBinaryNode());
            }
            node2List = node2List.getNext();
        }
        //去掉node1, node2 节点,可能出现单链情况的树,也就是node1, node2,同时出现在一个链中
        BinaryNode stackNode1 = stack1.pop();
        while (!stack1.isEmpty() && (stackNode1.compareTo(node1) == 0 || stackNode1.compareTo(node2) == 0)) {
            stackNode1 = stack1.pop();
        }
        BinaryNode stackNode2 = stack2.pop();
        while (!stack2.isEmpty() && (stackNode2.compareTo(node1) == 0 || stackNode2.compareTo(node2) == 0)){
            stackNode2 = stack2.pop();
        }
        do {
            if(stackNode1.compareTo(stackNode2) == 0){
                return stackNode1;
            }
            if(stack1.size() > stack2.size() && !stack1.isEmpty()){
                stackNode1 = stack1.pop();
            }else if(stack1.size() < stack2.size() && !stack2.isEmpty()){
                stackNode2 = stack2.pop();
            }else if(!stack2.isEmpty() && !stack1.isEmpty()){
                stackNode1 = stack1.pop();
                stackNode2 = stack2.pop();
            }else {
                return  null;
            }
        }while (true);
    }

  /**
     * 构造节点路径
     * */
    public static void buildListNode(BinaryNode tree, BinaryNode node1, BinaryNode node2) {
        if (tree == null) {
            return;
        }
        //初始化根节点路径
        if (tree.getLinkedList() == null) {
            ListNode treeList = new ListNode(tree);
            tree.setLinkedList(treeList);
        }
        if (tree.getLeft() != null) {
            //将父节点路径复制到子节点
            ListNode leftList = new ListNode();
            ListNode header = tree.getLinkedList();
            while (header != null) {
                ListNode newNode = new ListNode(header.getBinaryNode());
                MyLinkedList.addToTail(leftList, newNode);
                header = header.getNext();
            }
            //添加子节点本身,得到节点最终路径
            MyLinkedList.addToTail(leftList, new ListNode(tree.getLeft()));
            tree.getLeft().setLinkedList(leftList);
        }
        if (tree.getRight() != null) {
            //将父节点路径复制到子节点
            ListNode rightList = new ListNode();
            ListNode header = tree.getLinkedList();
            while (header != null) {
                ListNode newNode = new ListNode(header.getBinaryNode());
                MyLinkedList.addToTail(rightList, newNode);
                header = header.getNext();
            }
            //添加子节点本身,得到节点最终路径
            MyLinkedList.addToTail(rightList, new ListNode(tree.getRight()));
            tree.getRight().setLinkedList(rightList);
        }
        //当输入节点路径都不为空,则表示已经查找完毕
        if (node1.getLinkedList() != null && node2.getLinkedList() != null) {
            return;
        }
        buildListNode(tree.getLeft(), node1, node2);
        buildListNode(tree.getRight(), node1, node2);
    }
}

  • 时间复杂度分析,因为从开始到输入的两个节点的路径,只需要依次遍历,每次遍历复杂度是O(n),但是每个节点的路径负责还需要额外的开销,每个节点路径其实就是二叉树的深度 O(logn) 那么最终的世界复杂度是O(n)

  • 空间复杂度此处我们用额额外的链表存储路径,并且在分析链表时候用来额外的栈空间,链表只需要存储路径上的节点,也就是深度,那么空间复杂度O(logn),栈同样,O(logn)

  • 今天的代码分享就到这,之后还会有更多的练习,最后给一张神图

数据结构与算法--死磕二叉树_第11张图片

上一篇:数据结构与算法–这个需求很简单怎么实现我不管(发散思维)
下一篇:数据结构与算法–再来聊聊数组

你可能感兴趣的:(数据结构,算法,数据结构,算法)