Ltd的算法大抄·二叉树II

leetcode:104.543.102.107.116.226.114

Posted by Ltd on Monday, March 27, 2023

这是算法记录的第12天,主要内容是二叉树的遍历

对应leetcode题目为104.543.102.107.116.226.114

遇到二叉树问题的通用思考

1、是否可以通过遍历一遍得到结果,那么就可以通过traverse函数实现

2、是否可以定义递归函数,通过解决子问题来获取原问题结果

重要的是需要明白在前中后序位置需要干什么,即根据实际题目的要求进行变化就行

104.二叉树的最大深度

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例: 给定二叉树 [3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7

返回它的最大深度 3 。

思路

基于上面的两种思路框架,

1、遍历-回溯:深度可以在每次进入下一层的时候不断增加,返回上一层的时候减少,最后遍历到叶节点的时候就能够计算出当前叶节点的深度,最后求出所有叶节点的最大的深度就是题目所要求的,而深度计算的位置就是在进入下一层到退出下一层中任意位置都行

2、分解-动规:求最大深度就是求左右子树的最大深度然后+1就行,所以原问题就分解成了不断求子问题的解,最后再将子问题的解合并得到原问题的解

代码

// 遍历思想
func maxDepth(root *TreeNode) int {
	max, dep := 0, 0
	var traverse func(node *TreeNode)
	traverse = func(node *TreeNode) {
		if node == nil {
			return
		}
		//进入一个节点
		dep++
		//若是叶子节点就计算比较最大深度
		if node.Left == nil && node.Right == nil && dep > max {
			max = dep
		}
		//继续左右遍历
		traverse(node.Left)
		traverse(node.Right)
		dep--
	}
	traverse(root)
	return max
}

// 分解问题
func maxDepth1(root *TreeNode) int {
	if root == nil {
		return 0
	}
	//分别求解
	leftMax := maxDepth1(root.Left)
	rightMax := maxDepth1(root.Right)
	//通过子树的最大值求
	if leftMax > rightMax {
		return leftMax + 1
	} else {
		return rightMax + 1
	}
}

543.二叉树的直径

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。

示例 : 给定二叉树

          1
         / \
        2   3
       / \     
      4   5    

返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。

注意: 两结点之间的路径长度是以它们之间边的数目表示。

思路

题目要求得到直径,即当前节点左右子树最大深度之和,基于上面的代码有两种思路:

1、遍历:这道题使用遍历的方式,由于是自顶向下的方向,计算直径需要得到最大深度,即在traverse中需要对每个子节点求出最大深度,复杂度就是O(N^K),最坏情况就是O(N^2),所以运行时间很长;

2、分解:上面的问题是由于前序位置没法直接得到子节点的最大深度,而分解为子问题的话将直径的计算放到后序位置那么子节点的最大深度是可以通过递归直接获取的,所以解法更优。

这里注意传入的是直径的指针,便于直接比较修改

代码

func diameterOfBinaryTree(root *TreeNode) (max int) {
	diameter := 0
	//函数求的是最大深度,但直径可以通过深度来计算
	depMax(root, &diameter)
	return diameter
}

func depMax(node *TreeNode, diameter *int) int {
	if node == nil {
		return 0
	}
	//求左右子树的最大深度
	leftMax := depMax(node.Left, diameter)
	rightMax := depMax(node.Right, diameter)
	//通过深度求当前节点的直径
	diameterTmp := leftMax + rightMax
	//获取全局最大直径
	if diameterTmp > *diameter {
		*diameter = diameterTmp
	}
	//返回最大的深度
	if leftMax > rightMax {
		return leftMax + 1
	} else {
		return rightMax + 1
	}
}

102.二叉树的层序遍历

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。(即逐层地,从左到右访问所有节点)。

示例 1:

img

输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]

示例 2:

输入:root = [1]
输出:[[1]]

示例 3:

输入:root = []
输出:[]

提示:

  • 树中节点数目在范围 [0, 2000]
  • -1000 <= Node.val <= 1000

思路

层序遍历相对好理解,就是一层一层的得到值并将其左右子节点存起来用于下一层的遍历。

具体实现的话使用一个切片(也可以说是队列,因为是先进先出的),每一层遍历之前需要先记录下这一层的大小,也就是这个切片的前多少个是这一层的,避免后续子节点加入后混乱了。

代码

func levelOrder(root *TreeNode) (ans [][]int) {
	if root == nil {
		return ans
	}
	//用来存放每一层的所有节点,其实相当于一个队列
	q := []*TreeNode{root}

	for len(q) > 0 {
		//获取当前层的大小固定的值
		levelSize := len(q)
		//用来存放这一层的所有节点值
		var thisLevel []int
		//遍历完这一层里的所有节点
		for i := 0; i < levelSize; i++ {
			cur := q[0]
			thisLevel = append(thisLevel, cur.Val)
			//将左右子树放进q中等待下一层遍历
			if cur.Left != nil {
				q = append(q, cur.Left)
			}
			if cur.Right != nil {
				q = append(q, cur.Right)
			}
			//删除这个节点,相当于出队
			q = q[1:]
		}
		//一层遍历完了加入结果中
		ans = append(ans, thisLevel)
	}
	return ans
}

107.二叉树的层序遍历II

给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)

示例 1:

img

输入:root = [3,9,20,null,null,15,7]
输出:[[15,7],[9,20],[3]]

示例 2:

输入:root = [1]
输出:[[1]]

示例 3:

输入:root = []
输出:[]

思路

这个和上一个完全一样,只是在加入结果的时候加到前面就可以了

代码

func levelOrderBottom(root *TreeNode) (ans [][]int) {
	if root == nil {
		return ans
	}
	//用来存放每一层的所有节点,相当于一个队列
	q := []*TreeNode{root}

	for len(q) > 0 {
		//获取当前层固定的值
		levelSize := len(q)
		//用来存放这一层的所有节点值
		var thisLevel []int
		//遍历完这一层里的所有节点
		for i := 0; i < levelSize; i++ {
			cur := q[0]
			thisLevel = append(thisLevel, cur.Val)
			//将左右子树放进q中等待下一层遍历
			if cur.Left != nil {
				q = append(q, cur.Left)
			}
			if cur.Right != nil {
				q = append(q, cur.Right)
			}
			//删除这个节点,相当于出队
			q = q[1:]
		}
		//一层遍历完了加入结果的前面就是从下往上的顺序了
		ans = append([][]int{thisLevel}, ans...)
	}
	return ans
}

116.填充节点的下一个右侧节点指针

给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:

struct Node {
  int val;
  Node *left;
  Node *right;
  Node *next;
}

填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL

初始状态下,所有 next 指针都被设置为 NULL

示例 1:

img

输入:root = [1,2,3,4,5,6,7]
输出:[1,#,2,3,#,4,5,6,7,#]
解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化的输出按层序遍历排列,同一层节点由 next 指针连接,'#' 标志着每一层的结束。

示例 2:

输入:root = []
输出:[]

提示:

  • 树中节点的数量在 [0, 212 - 1] 范围内
  • -1000 <= node.val <= 1000

进阶:

  • 你只能使用常量级额外空间。
  • 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。

思路

由于需要连接不同父节点之间的间隙,可以将原二叉树转化为三叉树,这样两两组合成一个节点,就会包含不同父节点的子节点关联。而这道题利用遍历的思想即可从上往下连接节点,分解为子问题的话始终需要父节点的信息才能够连接起来,所以好像没有比较好的方法。

代码

func connect(root *Node) *Node {
	//存在用例
	if root == nil {
		return nil
	}
	traverse1(root.Left, root.Right)
	return root
}
func traverse1(node1, node2 *Node) {
    //完美二叉树似乎判断一个也够了
	if node1 == nil || node2 == nil {
		return
	}
	//连接当前两个节点
	node1.Next = node2
	//连接相同父节点的两子节点
	traverse1(node1.Left, node1.Right)
	traverse1(node2.Left, node2.Right)
	//连接不同父节点的两个子节点
	traverse1(node1.Right, node2.Left)
}

226.翻转二叉树

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

示例 1:

img

输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]

示例 2:

img

输入:root = [2,1,3]
输出:[2,3,1]

示例 3:

输入:root = []
输出:[]

思路

遍历?

对于交换左右子节点,遍历看着是可行的,可以使用traverse遍历每一个节点,交换其子节点位置,在前中后序实现都行。其子节点再交换自己的子节点,这样从上往下就能将所有节点实现翻转交换。

分解?

分解也可以实现,就是先将底层的左右节点进行交换,再不断向上对每一层的进行翻转,最后就将整个二叉树进行了翻转。

代码

//遍历的思想
func connect(root *Node) *Node {
	//存在用例
	if root == nil {
		return nil
	}
	traverse1(root.Left, root.Right)
	return root
}
func traverse1(node1, node2 *Node) {
	if node1 == nil || node2 == nil {
		return
	}
	//连接当前两个节点
	node1.Next = node2
	//连接相同父节点的两子节点
	traverse1(node1.Left, node1.Right)
	traverse1(node2.Left, node2.Right)
	//连接不同父节点的两个子节点
	traverse1(node1.Right, node2.Left)
}

114.二叉树展开为链表

给你二叉树的根结点 root ,请你将它展开为一个单链表:

  • 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null
  • 展开后的单链表应该与二叉树 先序遍历 顺序相同。

示例 1:

img

输入:root = [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]

示例 2:

输入:root = []
输出:[]

示例 3:

输入:root = [0]
输出:[0]

提示:

  • 树中结点数在范围 [0, 2000]
  • -100 <= Node.val <= 100

进阶: 你可以使用原地算法(O(1) 额外空间)展开这棵树吗?

思路

这道题初看发现最后的结果就是前序遍历的结果,那么就直接考虑前序遍历,然后顺便构建一个链表就行了,但仔细一看发现函数没有返回值,说明没法用新的数据空间,只能在root原地修改了,这样一来没法通过简单的遍历来解决问题。

分解?

可以。先将最下面的节点拉伸为直线,再不断向上层拉伸,也就是子问题合并成原问题的解。具体而言,就是对节点x,将其原左节点放到右节点的位置,原右节点放到当前右节点的最右端(也就是原左节点的最右端)

代码

// 分解:先将最底层的节点拉伸为直线,再不断向上拉伸
func flatten(root *TreeNode) {
	if root == nil {
		return
	}
	flatten(root.Left)
	flatten(root.Right)
	//后序位置,意味着底层的节点已经交换完了,只需考虑当前层的操作
	//为了更直观选择先保存下之前的节点
	left := root.Left
	right := root.Right
	//将左节点放到右节点的位置
	root.Right = left
	//置空左节点
	root.Left = nil
	p := root
	//到达当前右边的最末端
	for p.Right != nil {
		p = p.Right
	}
	//将之前的right放到现在的节点的最右端
	p.Right = right
}

为何要使用循环到达当前右边的最末端?

这是因为由于递归,原来的左子树已经拉伸好了,那么若直接放在左节点上,左子树的剩余节点就都没了,所以应该放在拉伸之后的最右端。