死磕二叉树
- 近一年都比较关注算法相关的知识,也刷了不少题,之前的文章中大多也是算法相关的文章,但是感觉每次遇到树相关的题型都不能应对自如,因此还是有必要在相关知识上下功夫,因此有此次总结,以下是所有树相关的文章
数据结构与算法–面试必问AVL树原理及实现
数据结构与算法–二叉树的深度问题
数据结构与算法–二叉堆(最大堆,最小堆)实现及原理
数据结构与算法–二叉查找树转顺序排列双向链表
数据结构与算法-- 二叉树中和为某一值的路径
数据结构与算法-- 二叉树后续遍历序列校验
数据结构与算法-- 广度优先打印二叉树
数据结构与算法–解决问题的方法- 二叉树的的镜像
数据结构与算法–重建二叉树
数据结构与算法–二叉查找树实现原理
数据结构与算法–二叉树实现原理
数据结构与算法–B树原理及实现
数据结构与算法–数字在排序数组中出现次数
数据结构与算法–死磕二叉树
数据结构与算法–二叉树第k个大的节点
-
本文中树,二叉树的实现都用自己的实现方式,在以上列举的最后两篇文中有详细的说明
-
二叉树,或者是说树经常用于大量的输入数据的场景下。大部分的操作运行时间平均是O(logN)。
-
二叉树的变种题型多如牛毛,还是要掌握方法,多看不同题型,训练知识迁移的能力,如下题:
题目:输入一棵二叉树和他的两个节点,求他们的最低公共祖先。
- 最低公共祖先的定义:给定一个有根树T 时候,对于任意两个节点 U, V,找到一个离根最远的节点X, 使得X同时 是U , V 的祖先,那么X便是 U,V的最近公共祖先。
最简模式
- 上题中并没有明显给出树的特性,只是强调了一棵二叉树,那么我们用二叉搜索树为案例来分析如下:
- 需要找到最低公共祖先,也就是找父节点,二叉树节点的特性,父节点比左节点大,比右节点小
- 那么会有几种情况,如果两个节点分布在多个分支,那么我们需要找的父节点大小必然介于 V,U之间,情况一
- 如果二叉树是一个单链,那么我们需要找到 V,U的其中一个父节点,此时改父节点要不不UV都打,要么比UV都小,情况二
- 用如下图表示:
-
如上所示的一颗二叉搜索树,当输入的是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 < node2if (node1.compareTo(node2) < 0 && tree.compareTo(node1) > 0 && tree.compareTo(node2) < 0) {return tree;}// node2< tree < node1if (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;}
}
困难模式
-
如果不是二叉排序树,只是一个普通的树或者二叉树,并且树中没有指向父节点的指针
-
分析如下:
- 不能用比较的方式找父节点,那么用遍历,两个节点都出现在某个节点的子节点,或者直接在某个节点下,如情况一:
-
6, 8 都出现在了7 节点下,但是同时也都出现在了5 节点的子节点下
-
我们需要求解的是最低公共祖先,那么离根节点越远的父节点才是我们需要求解的
-
我们可以从根遍历一棵树,每次遍历一个节点,判断输入节点是否在他子树中
-
如果在子树中,则分别遍历他所有子节点,并判断两个输入节点是否他们子树中,
-
这样从上到下遍历,直到找到这样一个节点,他自己的子树中同时包含两个输入的节点,但是他的任何一个子节点都不会同时拥有这两个节点,那么这就是公共祖先
-
我们举例说明,如上图:
- 第一种情况,当输入的是如上图中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;}//递归判断修改状态,所以每次都先初始化数量为0node1.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;}
}
地狱模式
-
在以上方案中,当输入是65, 26 时候,在判断完都在13节点下时候,我们其实已经遍历过21, 24 节点了,但是在之后的遍历中,我们任然需要在遍历21, 24,这种思路会出现很多重复的遍历,更快速的解决方案还是有的
-
之前文章数据结构与算法–两个链表中第一个公共节点给我们启发,如下图
-
上图其实就是一颗二叉树,只不过是斜的,之前用双指针,求第一个公共节点,或者用栈空间求第一个相同的节点接口
-
受以上启发,如果我们将两个输入节点U, V 的范问路径分别放到两个链表中,不就将二叉树的问题转为以上链表的问题。
-
还是如上图
分析如下:- 还是先根遍历,当遍历到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)
-
今天的代码分享就到这,之后还会有更多的练习,最后给一张神图
上一篇:数据结构与算法–这个需求很简单怎么实现我不管(发散思维)
下一篇:数据结构与算法–再来聊聊数组