1.定义
树其实是链表的变种,树是一种非线性的数据结构,相对于线性的数据结构(链表、数组)而言,树的平均运行时间更短(往往与树相关的排序时间复杂度都不会高)
分类
二叉树: 树中节点的度不大于2的有序树。
满二叉树: 就是所有叶子结点都是满的,即用每个节点都有两个子节点。
完全二叉树:只要保证最后一个节点之前都是齐全就ok。
二叉查找树:当前节点的左边一定小于当前,右边一定大于当前。
二叉堆:是一种完全的二叉树,包含 最大堆 和 最小堆
-
最大堆:任何一个父节点都大于等于左右两个字节点的值。
-
最小堆:任何一个父节点都小于等于左右两个字节点的值。
二叉堆
最大堆的顶堆是整个堆中最大的元素。
最小堆的顶堆是整个堆中最大的元素。
操作
堆的自我调整:把不复合堆特性的完全二叉树调整为 一个堆。
插入
插入的位置从二叉树最后一个位置开始,如下图,在5下面插入0,然后发现0比5小,替换位置, 插入元素0
删除
先从最后一个元素替换堆顶元素,然后删除最后元素,堆顶元素 重现排序调整。
删除顶堆1
构建
就是把当前无序的完全二叉树调整为 二叉堆,让所有非叶子结点依次下沉。从倒数第二开始下沉。
代码实现
最大堆 递归方法
图一中 2 作为父节点小于子节点,很显然不符合最大堆性质。maxHeapify 函数可以把每个不符合最大堆性质的节点调换位置,从而满足最大堆性质的数组。
调整步骤:
- 调整分支节点 2 的位置(不满足最大堆性质)
- 获取父节点 2 的左右节点 ( 12 , 5 ) ,从 ( 2 , 15 , 5 ) 中进行比较
- 找出最大的节点与父节点进行交换,如果该节点本身为最大节点则停止操作
- 重复 step2 的操作,从 2 , 4 , 7 中找出最大值与 2 做交换(递归)
堆的数组关系 核心逻辑:
左子节点: 2* parent +1
右子节点: 2* parent +2
同理
父节点 : (左子节点 - 1) /2
父节点 : (右子节点 - 2) /2
/**
* 最大堆
*/
function left(i) {
return (i * 2) + 1;
}
function right(i) {
return (i * 2) + 2;
}
function swap(A, i, j) {
const t = A[i];
A[i] = A[j];
A[j] = t;
}
class Heap {
constructor(arr) {
this.data = [...arr];
this.size = this.data.length;
this.rebuildHeap = this.rebuildHeap.bind(this);
this.isHeap = this.isHeap.bind(this);
this.sort = this.sort.bind(this);
this.insert = this.insert.bind(this);
this.delete = this.delete.bind(this);
this.maxHeapify = this.maxHeapify.bind(this);
}
//1.找到所有分支节点 Math.floor( N / 2 )(不包括叶子节点)
//2.将找到的子节点进行 maxHeapify 操作
/**
* 重构堆,形成最大堆
*/
rebuildHeap() {
const L = Math.floor(this.size / 2);
for (let i = L - 1; i >= 0; i--) {
this.maxHeapify(i);
}
}
// 生成一个升序的数组
//1.swap 函数交换首尾位置
//2.将最后一个从堆中拿出相当于 size - 1
//3.执行 maxHeapify 函数进行根节点比较找出最大值进行交换
//4.最终 data 会变成一个升序的数组
sort() {
for (let i = this.size - 1; i > 0; i--) {
swap(this.data, 0, i);
this.size--;
this.maxHeapify(0);
}
}
isHeap() {
const L = Math.floor(this.size / 2);
for (let i = L - 1; i >= 0; i--) {
const l = this.data[left(i)] || Number.MIN_SAFE_INTEGER;
const r = this.data[right(i)] || Number.MIN_SAFE_INTEGER;
const max = Math.max(this.data[i], l, r);
if (max !== this.data[i]) {
return false;
}
return true;
}
}
//插入方法 做上浮操作
//Insert 函数作为插入节点函数,首先
//1.往 data 结尾插入节点
//2.因为节点追加,size + 1
//3.因为一个父节点拥有 2 个子节点,我们可以根据这个性质通过 isHeap 函数获取第一个叶子节点,可以通过第一个叶子节点获取新插入的节点,然后进行 3 个值的对比,找出最大值,判断插入的节点。如果跟父节点相同则不进行重构(相等满足二叉堆性质),否则进行 rebuildHeap 重构堆
insert(key) {
this.data[this.size++] = key;
if (this.isHeap()) {
return;
}
this.rebuildHeap();
}
delete(index) {
if (index >= this.size) {
return;
}
this.data.splice(index, 1);
this.size--;
if (this.isHeap()) {
return;
}
this.rebuildHeap();
}
/**
// 下沉
* 交换父子节点位置,符合最大堆特征
* @param {*} i
*/
maxHeapify(i) {
let max = i;
if (i >= this.size) {
return;
}
// 求左右节点中较大的序号
const l = left(i);
const r = right(i);
if (l < this.size && this.data[l] > this.data[max]) {
max = l;
}
if (r < this.size && this.data[r] > this.data[max]) {
max = r;
}
// 如果当前节点最大,已经是最大堆
if (max === i) {
return;
}
swap(this.data, i, max);
// 递归向下继续执行 //这里找到的最大值就是下一个要比较的值
return this.maxHeapify(max);
}
}
module.exports = Heap;
//测试代码
const arr = [15, 12, 8, 2, 5, 2, 3, 4, 7];
const fun = new Heap(arr);
fun.rebuildHeap(); // 形成最大堆的结构
fun.sort();// 通过排序,生成一个升序的数组
console.log(fun.data) // [2, 2, 3, 4, 5, 7, 8, 12, 15]
优先队列-二叉堆实现
优先队列:有别于队列,是哪个值最大就优先出列,可以直接是用二叉堆的插入和删除实现,实现入队和出队。
- 入队: 从尾部插入然后上浮。
- 出队:从头部删除,然后叶子节点替换再下沉。
结构
树是逻辑结构,依赖与“物理结构”的来实现。(链表与数组都属于物理结构)
树属于逻辑结构,可以使用多种物理结构,如:链式结构,数组。
链表
使用结构
node : {
data:xxx
left:node1,
right:node2
}
数组
根据父节点找子节点:
左节点: 2* parent + 1
右节点: 2* parent + 2
根据子节点找父节点:
根据左节点找父: leftChild -1 /2
根据右节点找父:rightChild -1 /2
2.遍历
广度遍历:前,中,后,序遍历
- 前序遍历:先访问根节点,然后访问左节点,最后访问右节点(根->左->右)
- 中序遍历:先访问左节点,然后访问根节点,最后访问右节点(左->根->右)
- 后序遍历:先访问左节点,然后访问右节点,最后访问根节点(左->右->根)
前,中,后, 都是按根节点的访问顺序确定。
深度遍历:层序遍历
一层一层横向遍历各个节点。
先遍历根节点,root插入数组。推出数组第一个root元素,同时插入root的左右两边节点。 继续循环,推出数组第一个元素,再查两边插入,一直循环。
//代码实现
function levelOrder(root) {
let queue = [];
queue.push(root)
while(queue.length > 0) {
let item = queue.pop()
console.log(item.data)
if(item.left) {
queue.push(item.left)
}
if(item.right) {
queue.push(item.right)
}
}
}
3.leetcode刷题
100. 相同的树
给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
示例 1:
输入: p = [1,2,3], q = [1,2,3]
输出: true
示例 2:
输入: p = [1,2], q = [1,null,2]
输出: false
答题:
/**
* 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} p
* @param {TreeNode} q
* @return {boolean}
*/
//递归判断
var isSameTree = function(p, q) {
if(p == null && q == null) {
return true
}else
if(p == null || q == null) {
return false
}else
if(p.val != q.val) {
return false
}else {
return isSameTree(p.left,q.left) && isSameTree(p.right,q.right)
}
};
226. 翻转二叉树
翻转一棵二叉树。 示例: 输入:
4
/ \
2 7
/ \ / \
1 3 6 9
输出:
4
/ \
7 2
/ \ / \
9 6 3 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 {TreeNode} root
* @return {TreeNode}
*/
var invertTree = function(root) {
if(root == null) {
return root
}
//这里递归 一直赋值,注意最后一个如:3下面是左右都是null ,会做两个null的对换
root.left = invertTree(root.right)
root.right = invertTree(root.left)
return root
};
//优化代码
var invertTree = function(root) {
if(root == null) {
return root
}
[root.left,root.right] = [invertTree(root.right),invertTree(root.left)]
return root
};
144. 二叉树的前序遍历
给你二叉树的根节点 root ,返回它节点值的 前序遍历。
示例 1:
输入: root = [1,null,2,3]
输出: [1,2,3]
示例 2:
输入: root = []
输出: []
答题:
/**
* 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[]}
*/
//递推 + 双 while + stack
var preorderTraversal = function(root,arr = []) {
let res = []
let stack = []
let cur = root
while (cur || stack.length > 0 ) {
while (cur) {
res.push(cur.val)
stack.push(cur)
cur = cur.left
}
cur = stack.pop()
cur = cur.right
}
return res
};
// 递归
var preorderTraversal = function(root,arr = []) {
if(root != null) {
arr.push(root.val)
preorderTraversal(root.left,arr)
preorderTraversal(root.right,arr)
}
return arr
};
145. 二叉树的后序遍历
/**
* 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[]}
*/
// 后序 遍历 关系是 左 右 根 则我们只需要求出 根 右 左,再使用 reverse 反转
var postorderTraversal = function(root,arr = []) {
let res = []
let stack = []
let cur = root
while (cur || stack.length > 0 ) {
while (cur) {
res.push(cur.val)
stack.push(cur)
cur = cur.right
}
cur = stack.pop()
cur = cur.left
}
res.reverse()
return res
};
// 递归
var postorderTraversal = function(root,arr = []) {
if(root != null) {
postorderTraversal(root.left,arr)
postorderTraversal(root.right,arr)
arr.push(root.val)
}
return arr
};
94. 二叉树的中序遍历
/**
* 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[]}
*/
//while + stack
var inorderTraversal = function(root) {
let res = []
let stack = []
let cur = root
while (cur || stack.length > 0 ) {
while (cur) {
stack.push(cur)
cur = cur.left
}
cur = stack.pop()
res.push(cur.val) //这里就是所有节点加入的关键时间,包括左中右
cur = cur.right
}
return res
};
//递归
var inorderTraversal = function(root,arr = []) {
if(root != null) {
inorderTraversal(root.left,arr)
arr.push(root.val)
inorderTraversal(root.right,arr)
}
return arr
};
while+stack 迭代逻辑分析
98. 验证二叉搜索树
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
- 节点的左子树只包含 小于 当前节点的数。
- 节点的右子树只包含 大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入: root = [2,1,3]
输出: true
示例 2:
输入: root = [5,1,4,null,null,3,6]
输出: false
解释: 根节点的值是 5 ,但是右子节点的值是 4 。
答题
/**
* 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 {boolean}
*/
//中序遍历 + 迭代 + 实时判断
var isValidBST = function(root) {
let stack = [];
let inorder = -Infinity;
while (stack.length || root !== null) {
while (root !== null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
// 如果中序遍历得到的节点的值小于等于前一个 inorder,说明不是二叉搜索树
if (root.val <= inorder) {
return false;
}
inorder = root.val;
root = root.right;
}
return true;
};
//中序遍历+ 递归 + 判断是否是从小到大排序
var isValidBST = function(root ,arr = []) {
if(root != null) {
isValidBST(root.left,arr)
arr.push(root.val)
isValidBST(root.right,arr)
}
//判断当前的顺序是否正确
for (var i = 0 ; i < arr.length - 1; i ++) {
if(arr[i] >= arr[i+1]){
return false
}
}
return true
};
//递归判断, 这里一直左右两边递归,直到子节点都为null,就为true,
//-Infinity ,和 Infinity处理固定最小值和最大值 左右两边的判断,
//
const helper = (root, lower, upper) => {
if (root === null) {
return true;
}
if (root.val <= lower || root.val >= upper) {
return false;
}
//helper(root.left, lower, root.val) 判断左边节点 是否大于中节点,是则返回否,因为是至上而下,所以每次只需要处理
//helper(root.left, lower, root.val) 判断右边节点 是否小于中节点,是则返回否
return helper(root.left, lower, root.val) && helper(root.right, root.val, upper);
}
var isValidBST = function(root) {
return helper(root, -Infinity, Infinity);
};
104. 二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
答题
/**
* 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 maxDepth = function(root) {
if(!root) {
return null
}
return Math.max(maxDepth(root.left),maxDepth(root.right)) + 1
};
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, 因为根据定义最近公共祖先节点可以为节点本身。
答题
利用排好的顺序,左边小,右边大的特点。是中心节点不断移动判断的逻辑。 判断中心点 都在 两个节点都在的左边,则中心点右移 判断中心点 都在 两个节点都在的右边,则中心点左移 直到不同时在左边,并且不同时在右边,证明找到,否则要么为空
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @param {TreeNode} p
* @param {TreeNode} q
* @return {TreeNode}
*/
//递推 迭代方式
var lowestCommonAncestor = function(root, p, q) {
while(root) {
let prev = root
if(root.val < p.val && root.val < q.val) {//中心点都两点的左边,进行右移
root = root.right
}else if(root.val > p.val && root.val > q.val) {//中心点都两点的右边,进行左移
root = root.left
}
else { //即不在两个节点左边,也不在两个节点右边 , 则找到了
return root
}
}
return null
};
//递归
var lowestCommonAncestor = function(root, p, q) {
if(!root) {
return root
}
if(root.val < p.val && root.val < q.val) {
return lowestCommonAncestor(root.right,p,q)
}else if(root.val > p.val && root.val > q.val) {
return lowestCommonAncestor(root.left,p,q)
}else {
return root
}
};
236. 二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
这次不是二叉搜索树,所有树是没有排序的。
示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3 。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
问题分析
分四种情况
- root是null或者root等于p或q,说明root是p,q的公共祖先,
- 递归左右子树,如果左右子树递归函数返回的都不为空,则root就是p,q的公共祖先
- 左子树递归函数返回的值为空,则p,q都在右子树,
- 右子树递归函数返回的值为空,则p,q都在左子树
答题
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @param {TreeNode} p
* @param {TreeNode} q
* @return {TreeNode}
*/
// 递归 判断左边没有就去右边找
var lowestCommonAncestor = function(root, p, q) {
if(root == null || root == p || root == q) { // 确定递归终止条件
return root
}
let left = lowestCommonAncestor(root.left,p,q) //递归单层 左边逻辑
let right = lowestCommonAncestor(root.right,p,q)//递归单层 右边边逻辑
if(left == null) { //如果左子树没找到就说明p,q都在右子树
return right //这里是递归过的右边集合
}
if(right == null) {//如果右子树没找到就说明p,q都在左子树
return left //这里是递归过的左边集合
}
//能执行到这里,证明left和right都不为空,如果在某一个节点的左右子树都能找到p和q说明这个节点就是公共祖先
return root
};
257. 二叉树的所有路径
给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。
示例 1:
输入: root = [1,2,3,null,5]
输出: ["1->2->5","1->3"]
示例 2:
输入: root = [1]
输出: ["1"]
答题
/**
* @param {TreeNode} root
* @return {string[]}
*/
var binaryTreePaths = function(root) {
let arr = [] //全局返回数组
let getNodeStr = (node,str) => {//该方法只深度递归,不返回信息
if(!node) {
return ""
}
str += node.val //每次都把上一次的内容,与当前累加
if(node.left == null && node.right == null) {
arr.push(str) //如果是叶子页面直接加入数组
}else {
let path = str + "->" //不是叶子,递归嵌套两个左右节点
getNodeStr(node.left,path)
getNodeStr(node.right,path)
}
}
getNodeStr(root,"")
return arr
};
617. 合并二叉树
给你两棵二叉树: root1 和 root2 。其实就是把对应所有节点相加
返回合并后的二叉树。
注意: 合并过程必须从两个树的根节点开始。
示例 1:
输入: root1 = [1,3,2,5], root2 = [2,1,3,null,4,null,7]
输出: [3,4,5,5,4,null,7]
示例 2:
输入: root1 = [1], root2 = [1,2]
输出: [2,2]
答题
/**
* @param {TreeNode} root1
* @param {TreeNode} root2
* @return {TreeNode}
*/
//递归
var mergeTrees = function(root1, root2) {
if(!root1) return root2
if(!root2) return root1
root1.val = root1.val + root2.val
root1.left = mergeTrees(root1.left,root2.left)
root1.right = mergeTrees(root1.right,root2.right)
return root1
};