树
, 是一种常见的数据结构, 有不少的应用场景, 也是面试中的常客
。前端
好比:树的遍历, 分层打印, 平摊的数据转成树, 等等。node
这就须要咱们对树这种数据结构有个基础的认识,今天咱们就再回顾一下这种数据结构。面试
今天的内容主要包括:segmentfault
树
二叉树
二叉搜索树
讲树以前, 咱们先回顾下链表。数据结构
实际上链表和树, 图,都是有一些联系的。学习
先看一个单链表的示意图:spa
每一个结点都有个value
和一个next
指向后续结点, 一直向后,串成一个链。3d
这种结构很方便, 可是也有必定的局限。指针
好比想一想访问中间某个结点的时候,或者倒数第几个结点 就只能从头日后一个一个查, 效率不高。code
为解决这种问题,应运而生的方法有不少, 好比双向链表
, 每一个结点不光有后继结点
, 还有前继结点
。
其实再观察一下, 不难发现, 若是每一个结点的next有两个, 会是怎么样?
就变成了咱们所说的树
。
这是一个普通的二叉树的结构, 每一个结点有两个next指针, 即左右孩子。
二叉树的一种代码表示:
这个特殊的链表
的第一个结点, 就是咱们说的树的根结点
。
树也是分层的, 所谓的层, 就是距离根结点的距离
,如上图所示。
若是每个结点都有两个孩子结点, 这样的树, 就是满二叉树
。
再观察一下, 发现, 若是结点还能指回到根结点,或者其余结点, 这个树会变成什么样?
没错, 就变成了图
。
图在咱们的生活中也有不少类似的案例, 好比你要走到什么地方, 怎么走最短等等。
简单总结一下:
链表, 就是特殊化的树。
树, 就是特殊化的图。
二叉搜索树, 是一种特殊的二叉树。
它能够是一颗空树
, 或者是具备下列性质
的二叉树:
好比:
左孩子上的结点都是小于27的, 后孩子上的结点都是大于27的。
这种结构的好处在于, 好比咱们要查找一个元素的时候, 只须要和根比较。
大于根, 就在右子树, 小于就在左子树, 每次搜索, 都能减小一半的数据量。
和链表相比, 查找一个元素, 链表是O(N), 二叉搜索树每次都是减一半, 就变成了O(log2(N)), 效率得以提高。
最后献上一个老生常谈的比较图:
二叉搜索树在最坏的状况下,会退化成O(N)的, 好比, 只有右子树, 没有左子树, 就是一条长长的链。
为了改善这种状况, 后面又发展出了各类各样的树, 好比下面的
这三种也叫平衡二叉搜索树, 在最坏状况下, 也能保持O(log(n))的时间复杂度
在Java, C++ 的标准库里面,二叉搜索树都是用红黑树来实现的。
对红黑树有兴趣的同窗,能够看一下维基百科: https://zh.wikipedia.org/wiki...
理论大概就是这么些, 下面咱们就进入到实战环节。
这是leetcode 的第98题, medium 难度。
给定一个二叉树,判断其是不是一个有效的二叉搜索树。
假设一个二叉搜索树具备以下特征:
节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
全部左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入: 2 / \ 1 3 输出: true 示例 2: 输入: 5 / \ 1 4 / \ 3 6 输出: false 解释: 输入为: [5,1,4,null,null,3,6]。 根节点的值为 5 ,可是其右子节点值为 4 。
我用了三种解法, 下面咱们一个一个看。
观察二叉搜索树
, 咱们不难发现, 若是是一个合法的二叉搜索数, 必定是左结点 < 根结点 < 右结点
这样获得的中序遍历必定是一个升序的
,能够用这种方式来验证。
简单回顾下二叉树的遍历:
好比这个树的中序遍历结果就是: 10 14 19 27 31 35 42
因此利用升序特性
, 咱们能够获得第一种解法:
var isValidBST = function (root) { var stack = []; // 中序遍历 function dfs(root) { if (!root) return; root.left && dfs(root.left) root && stack.push(root.val) root.right && dfs(root.right) } dfs(root) for (var i = 0; i < stack.length - 1; i++) { if (stack[i] >= stack[i + 1]) return false } return true; };
在观察一下 ,咱们不难发现, 左结点 < 根结点 < 右结点
, 根结点的值必定是夹在左右结点的值中间的
,
若是不在这个范围里, 也必定是不合法的。 因此, 根据这个思路,能够获得解法2.
var isValidBST = function (root) { function isValidBSTHelper(root, min, max) { if (root == null) return true; // 空树也是合法的 if (root.val <= min || root.val >= max) return false; // 不在范围内, 不合法 return isValidBSTHelper(root.left, min, root.val) && isValidBSTHelper(root.right, root.val, max); // 减小一半数据, 继续往下判断 } return isValidBSTHelper(root, -Infinity, Infinity) }
这种解法也很是容易理解。
第三种解法来自网友,也是利用大小的特性.
即: 任意节点的值必须大于其左子树的最右节点;同时小于右子树的最左节点。
从根节点开始检查,一旦发现不知足则返回false.
代码实现:
var isValidBST = function (root) { function dfs(root) { if (root == null) return true if (root.left) { if (root.left.val >= root.val) return false let rightest = getRightest(root.left) if (rightest && rightest.val >= root.val) return false } if (root.right) { if (root.right.val <= root.val) return false let leftest = getLeftest(root.right) if (leftest && leftest.val <= root.val) return false } return dfs(root.left) && dfs(root.right) } function getRightest(node) { while (node && node.right) node = node.right return node } function getLeftest(node) { while (node && node.left) node = node.left return node } return dfs(root) };
代码稍显繁琐, 理解一下思路便可。
这是leetcode 235题。
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x. 知足 x 是 p、q 的祖先且 x 的深度尽量大(一个节点也能够是它本身的祖先 例如,给定以下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
示例 1: 输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8 输出: 6 解释: 节点 2 和节点 8 的最近公共祖先是 6。 示例 2: 输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4 输出: 2 解释: 节点 2 和节点 4 的最近公共祖先是 2, 由于根据定义最近公共祖先节点能够为节点自己。 说明: 全部节点的值都是惟一的。 p、q 为不一样节点且均存在于给定的二叉搜索树中。
这道题我用了两种解法。
递归的思路也很是简单:
若是 p, q 都小于root, 说明解在左子树。
若是 p, q 都大于root, 说明解在右子树。
若是一个大于root, 一个小于root, 那root 就是最近的公共祖先。
按照这个思路, 实现代码:
var lowestCommonAncestor = function(root, p, q) { // p, q 都小于root, 说明解在左子树 if(p.val < root.val && q.val < root.val ) return lowestCommonAncestor(root.left, p, q) // p, q 都大于root, 说明解在右子树 if(p.val > root.val && q.val > root.val ) return lowestCommonAncestor(root.right, p, q) return root }
解法2是解法1的变种, 思路都是同样的, 只不过由递归
改为了非递归
。
代码实现:
var lowestCommonAncestor = function (root, p, q) { while (root) { if (p.val < root.val && q.val < root.val) { root = root.left } else if (p.val > root.val && q.val > root.val) { root = root.right } else { return root } } }
这篇文章, 咱们回顾了下几种树的概念, 并经过实战巩固了这几个概念。
但愿对你有所启发。
以为内容有帮助能够关注下个人公众号 「 前端e进阶 」,我整理了不一样的学习专题
。
你也能够联系我,加入咱们的学习群。