文章简要概述
- 本文主要进行树相关的算法题刷题题解记录,记录树相关算法以及如何解。
- 这文一共有5道题,主要介绍leetcode中
路径总和、从前序与中序遍历序列构造二叉树、完全二叉树的节点个数、二叉搜索树的第k大节点、和树的子结构的解题思路。
树相关概念
树
树(英语:tree)是一种抽象数据类型或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
①每个节点有零个或多个子节点;
②没有父节点的节点称为根节点;
③每一个非根节点有且只有一个父节点;
④除了根节点外,每个子节点可以分为多个不相交的子树;
关于树的术语:度,叶子节点,根节点,父节点,子节点,深度,高度。
二叉树 每个节点最多含有两个子树的树称为二叉树(我们一般在书中试题中见到的树是二叉树,但并不意味着所有的树都是二叉树)
满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点。也可以这样理解,除叶子结点外的所有结点均有两个子结点。节点数达到最大值,所有叶子结点必须在同一层上
完全二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~(h-1)层) 的结点数都达到最大个数,第h层所有的结点都连续集中在最左边,这就是完全二叉树。
二叉树的遍历方式
前序遍历:先根节点->遍历左子树->遍历右子树
中序遍历:遍历左子树->根节点->遍历右子树
后序遍历:遍历左子树->遍历右子树->根节点
深度优先搜索(DFS)与广度优先搜索(BFS)
实现:bfs=队列,入队列,出队列 一次访问一条路径;dfs=栈,压栈,出栈 一次访问多条路径
关系:用DFS解决的问题都可以用BFS解决。DFS易于编写(递归),时间消耗较少但是容易发生爆栈,而BFS可以控制队列的长度。
动态查找树
二叉查找树(英语:Binary Search Tree),也称为二叉搜索树、有序二叉树(ordered binary tree)或排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:
1.若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
2.若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
3.任意节点的左、右子树也分别为二叉查找树;
4.没有键值相等的节点。
特点:在于查找、插入的时间复杂度较低为 O ( log n ) 。二叉查找树是基础性数据结构,用于构建更为抽象的数据结构,如集合、多重集、关联数组等。
平衡二叉树(AVL树):当且仅当任何节点的两棵子树的高度差不大于1的二叉树。
红黑树:是一种自平衡的二叉查找树。
1.每个结点要么是红的要么是黑的。(红或黑)
2.根结点是黑的。 (根黑)
3.每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。 (叶黑)
4.如果一个结点是红的,那么它的两个儿子都是黑的。 (红子黑)
5.对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。(路径下黑相同)
如图就是一棵典型的红黑树。保证红黑树满足它的基本性质,就是在调整数据结构自平衡。
而红黑树自平衡的调整操作方式就有旋转和变色两种。
哈夫曼树(Huffman Tree) 是一种带权路径长度最短的二叉树,也称为最优二叉树。
一般可以按下面步骤构建:
1,将所有左,右子树都为空的作为根节点。
2,在森林中选出两棵根节点的权值最小的树作为一棵新树的左,右子树,且置新树的附加根节点的权值为其左,右子树上根节点的权值之和。注意,左子树的权值应小于右子树的权值。
3,从森林中删除这两棵树,同时把新树加入到森林中。
4,重复2,3步骤,直到森林中只有一棵树为止,此树便是哈夫曼树。
大家可能更多听说的是哈夫曼编码,其实就是哈夫曼树的应用。即如何让电文中出现较多的字符采用尽可能短的编码且保证在译码时不出现歧义。
多路查找树
B树(英语:B-tree)是一种自平衡的树,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。B树,概括来说是一个一般化的二叉查找树(binary search tree),可以拥有最多2个子节点。与自平衡二叉查找树不同,B树适用于读写相对大的数据块的存储系统,例如磁盘。
1.根结点至少有两个子女。
2.每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m
3.每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m
4.所有的叶子结点都位于同一层。
5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。
如图所示就是一颗符合规范的B树,由于相比于磁盘IO的速度,内存中的耗时几乎可以省略,所以只要树的高度足够低,IO次数足够小,就可以提升查询性能。
B树的增加删除同样遵循自平衡的性质,有旋转和换位。
B树的应用是文件系统及部分非关系型数据库索引。
B+树是一种树数据结构,通常用于关系型数据库(如Mysql)和操作系统的文件系统中。B+ 树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。B+ 树元素自底向上插入,这与二叉树恰好相反。
在B树基础上,为叶子结点增加链表指针(B树+叶子有序链表),所有关键字都在叶子结点 中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中。
b+树的非叶子节点不保存数据,只保存子树的临界值(最大或者最小),所以同样大小的节点,b+树相对于b树能够有更多的分支,使得这棵树更加矮胖,查询时做的IO操作次数也更少。
B*树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针
在B+树基础上,为非叶子结点也增加链表指针,将结点的最低利用率从1/2提高到2/3。
与树相关算法
112. 路径总和
112. 路径总和 -- leetcode
题目大意:
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
叶子节点 是指没有子节点的节点。
示例
输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
解释:等于目标和的根节点到叶节点路径如上图所示。
解题思路:
- 采用递归的方式,首先判断当前根节点是否存在
- 判断当前根节的值与目标值是否相等
- 递归左子树和右子树,目标值减去当前根节点的值
代码:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} targetSum
* @return {boolean}
*/
function hasPathSum (root, targetSum) {
if(!root) return false;
if (!root.left && !root.right) {
return root.val == targetSum;
}
return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val);
};
222. 完全二叉树的节点个数
题目大意:
给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。
完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。
示例:
输入: root = [1,2,3,4,5,6]
输出: 6
解题思路:
- 暴力递归法,遍历每一层,每个有值的节点加1
- 二分查找+位运算的方式。利用完全二叉树的特性计算节点个数。
- 完全二叉树的最左边的节点一定位于最底层,假设完全二叉树的最大层数为 h,最底层包含的节点数最少为 1,最多为 2^h。
- 最大层数为 hh 的完全二叉树,节点个数一定在 [2^h,2^{h+1}-1] 的范围内
- 如果第 kk 个节点存在,则节点个数一定大于或等于 kk,如果第 kk 个节点不存在,则节点个数一定小于 kk,由此可以将查找的范围缩小一半,直到得到节点个数。
- 通过位运算判断第 k 个节点是否存在。
代码:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
// 直接递归求解
var countNodes = function(root) {
if (!root) return 0;
return 1 + countNodes(root.left) + countNodes(root.right);
};
// 二分查找 + 位运算
const exists = (root, level, k) => {
let bits = 1 << (level - 1) ;
let node = root;
while (node !== null && bits > 0) {
if (!(bits & k)) {
node = node.left;
} else {
node = node.right;
}
bits >>= 1;
}
return node !== null;
}
var countNodes = function(root) {
if (root === null) {
return 0;
}
let level = 0;
let node = root;
while (node.left !== null) {
level++;
node = node.left;
}
let low = 1 << level, high = (1 << (level + 1)) - 1;
while (low < high) {
const mid = Math.floor((high - low + 1) / 2) + low;
if (exists(root, level, mid)) {
low = mid;
} else {
high = mid - 1;
}
}
return low;
};
剑指 Offer 54. 二叉搜索树的第k大节点
剑指 Offer 54. 二叉搜索树的第k大节点 -- leetcode
题目大意:
给定一棵二叉搜索树,请找出其中第 `k` 大的节点的值。
示例:
输入: root = [3,1,4,null,2], k = 1
3 / \ 1 4 \ 2输出: 4
解题思路:
- 二叉搜索树的中序遍历为 递增序列
- 二叉搜索树的 中序遍历倒序 为 递减序列
代码:
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @param {number} k
* @return {number}
*/
function kthLargest(root, k) {
let res;
function dsf(root) {
if (!root || k <= 0) return;
dsf(root.right);
if (--k === 0) res = root.val;
dsf(root.left)
}
dsf(root);
return res
};
剑指 Offer 26. 树的子结构
题目大意:
输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值。
例如:
给定的树 A:
3
/ \
4 5
/ \
1 2
给定的树 B:
4
/
1
返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。
示例:
输入: A = [1,2,3], B = [3,1]
输出: false
解题思路:
- 采用递归的思路,先从根节点开始比较
- 判断根节点的左子树与对应的左子树,根节点的右子树与对应的右子树是否相等,递归判断。
- 如果与根节点不相等,就判断与根节点的左子树,右子树是否相等
代码:
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} A
* @param {TreeNode} B
* @return {boolean}
*/
function compare (A, B) {
if (!B) return true;
if (!A || A.val !== B.val) return false;
return compare(A.left, B.left) && compare(A.right, B.right);
}
var isSubStructure = function(A, B) {
if (!A || !B) return false;
return compare(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B);
};
105. 从前序与中序遍历序列构造二叉树
105. 从前序与中序遍历序列构造二叉树 -- leetcode
题目大意:
给定一棵树的前序遍历 `preorder` 与中序遍历 `inorder`。请构造二叉树并返回其根节点。
示例:
Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
Output: [3,9,20,null,null,15,7]
解题思路:
- 前序数组的
左子树部分+根节点是1,2,4,5,中序数组的左子树部分+根节点是4,2,5,1。这两者的数组长度是一样的。 - 根据中序数组的中间位置
1,来确定前序数组的左右部分,由于前序数组第一个是根节点 - 递归以上操作, 直到前序和中序数组为空
代码:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} preorder
* @param {number[]} inorder
* @return {TreeNode}
*/
var buildTree = function(preorder, inorder) {
if(!preorder.length || !inorder.length) return null;
const rootVal = preorder[0];
const root = new TreeNode(rootVal);
const i = inorder.indexOf(rootVal);
root.left = buildTree(preorder.slice(1, i + 1), inorder.slice(0, i));
root.right = buildTree(preorder.slice(i + 1), inorder.slice(i + 1))
return root;
};
结束语
数据结构与算法相关的练习题会持续输出,一起来学习,持续关注。当前是树部分。后期还会有其他类型的数据结构,题目来源于leetcode。往期文章:
数据结构与算法-栈一 数据结构与算法-栈二 数据结构与算法-队列一
数据结构与算法-队列二 数据结构与算法-链表一 数据结构与算法-链表二
有兴趣的可以一起来刷题,如果文章对你有帮助,感谢点赞👍,关注!