提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
提示:以下是本篇文章正文内容,下面案例可供参考
// 这就是最基础的二叉树问题了,用分解子问题的方式解决
func maxDepth(root *TreeNode) int {
if root == nil {
return 0
}
left := maxDepth(root.Left)
right := maxDepth(root.Right)
if left > right {
return left + 1
}
return right + 1
}
func preorderTraversal(root *TreeNode) []int {
res := make([]int, 0, 10)
core(root, &res)
return res
}
func core(root *TreeNode, res *[]int) {
if root == nil {
return
}
*res = append(*res, root.Val)
core(root.Left, res)
core(root.Right, res)
}
先给一个错误答案,其实逻辑上没啥问题,但是在leetcode中,不能用全局变量,否则上一组测试用例的结果会影响下一组的结果。应该用指针传递。
// labuladong有说法,二叉树包括两种解法。一种是递归,一种是分解问题
// 递归不在乎返回值,用traverse(),
// 分解子问题,需要依赖子问题的返回值
// 根据lbld的说法,这种应该是分解问题的思路,也就是后续遍历,
// 先看子树到当前节点的深度,然后深度相加,更新直径,直到找到最大直径
var diameter = 0
func diameterOfBinaryTree(root *TreeNode) int {
getDeep(root)
return diameter
}
func getDeep(root *TreeNode) int {
if root == nil {
return 0
}
leftDeep := getDeep(root.Left)
rightDeep := getDeep(root.Right)
if leftDeep+rightDeep > diameter {
diameter = leftDeep + rightDeep
}
if leftDeep > rightDeep {
return leftDeep + 1
}
return rightDeep + 1
}
正确解法
// labuladong有说法,二叉树包括两种解法。一种是递归,一种是分解问题
// 递归不在乎返回值,用traverse(),
// 分解子问题,需要依赖子问题的返回值
// 根据lbld的说法,这种应该是分解问题的思路,也就是后续遍历,
// 先看子树到当前节点的深度,然后深度相加,更新直径,直到找到最大直径
func diameterOfBinaryTree(root *TreeNode) int {
diameter := 0
getDeep(root, &diameter)
return diameter
}
func getDeep(root *TreeNode, diameter *int) int {
if root == nil {
return 0
}
leftDeep := getDeep(root.Left, diameter)
rightDeep := getDeep(root.Right, diameter)
if leftDeep+rightDeep > *diameter {
*diameter = leftDeep + rightDeep
}
if leftDeep > rightDeep {
return leftDeep + 1
}
return rightDeep + 1
}
首先给出我自己的解法
// 分解子问题,每棵树要返回尾节点,因为root就是头节点
func flatten(root *TreeNode) {
if root == nil {
return
}
core(root)
}
func core(root *TreeNode) *TreeNode {
if root.Left == nil && root.Right == nil {
return root
}
right := root.Right
if root.Left != nil {
tail := core(root.Left)
root.Right = root.Left
root.Left = nil// 最初漏掉了这一句
if right == nil {
return tail
}
tail.Right = right
}
return core(right)
}
再看labuladong的解法
func flatten(root *TreeNode) {
// base case
if root == nil {
return
}
// 利用定义,把左右子树拉平
flatten(root.Left)
flatten(root.Right)
/**** 后序遍历位置 ****/
// 1、左右子树已经被拉平成一条链表
left := root.Left
right := root.Right
// 2、将左子树作为右子树
root.Left = nil
root.Right = left
// 3、将原先的右子树接到当前右子树的末端
p := root
for p.Right != nil {
p = p.Right
}
p.Right = right
}
非常暴力,完全不考虑优化,但是真的好理解。可以看到,性能其实和我的比差了不少。
我的:
> 2023/07/18 10:16:30
Success:
Runtime:0 ms, faster than 100.00% of Go online submissions.
Memory Usage:2.7 MB, less than 94.23% of Go online submissions.
labuladong:
> 2023/07/18 10:21:17
Success:
Runtime:4 ms, faster than 48.28% of Go online submissions.
Memory Usage:2.7 MB, less than 94.23% of Go online submissions.
问题就在于步骤3对拉直的左子树进行了遍历。所以如果可以返回尾节点,同时又可以很容易理解呢?
修改之后如下,突然就理解了问题分解的核心
是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。
我需要做的就是考虑在一个独立的节点上,我需要做什么
func flatten(root *TreeNode) {
core(root)
}
func core(root *TreeNode) *TreeNode {
if root == nil {
return nil
}
if root.Left == nil && root.Right == nil {
return root
}
tailL := core(root.Left)
tailR := core(root.Right)
right := root.Right
if root.Left != nil {
root.Right = root.Left
root.Left = nil
if right == nil {
return tailL
}
tailL.Right = right
}
return tailR
}
这一题,labuladong给出了一个非常巧妙的算法,把二叉树看成三叉树,这种做法过于巧妙了,以至于我都觉得他算是神仙题.
func connect(root *Node) *Node {
if root == nil {
return nil
}
traverse(root.Left, root.Right)
return root
}
func traverse(node1, node2 *Node) {
if node1 == nil || node2 == nil {
return
}
node1.Next = node2
traverse(node1.Left, node1.Right)
traverse(node1.Right, node2.Left)
traverse(node2.Left, node2.Right)
}
也许有一天我会突然认同,但现在我还是倾向于用层序遍历的方式。
首先给出考研书上的层序遍历的go实现。但是既然用go实现并且不用环形队列,那么rear可以用len(queue)代替
func level(p *Node) int {
front, rear := 0, 0 // 书中使用了一个环形队列,我没有用环形队列,但是用了书中的下标表示方法
queue := make([]*Node, 0, 10)
if p != nil {
rear++
queue = append(queue, p)
for front != rear {
q := queue[front] // 1,之前1、2写反了
front++ //2
visit(q)
if q.Left != nil {
rear++
queue = append(queue, q.Left)
}
if q.Right != nil {
rear++
queue = append(queue, q.Right)
}
}
}
return 0
}
然后给出labuladong的层序遍历实现
// 输入一棵二叉树的根节点,层序遍历这棵二叉树
func levelTraverse(root *TreeNode) {
if root == nil {
return
}
q := make([]*TreeNode, 0)
q = append(q, root)
// 从上到下遍历二叉树的每一层
for len(q) > 0 {
sz := len(q)
// 从左到右遍历每一层的每个节点
for i := 0; i < sz; i++ {
cur := q[0]
q = q[1:]
// 将下一层节点放入队列
if cur.Left != nil {
q = append(q, cur.Left)
}
if cur.Right != nil {
q = append(q, cur.Right)
}
}
}
}
然后我想到的答案是:
func connect(root *Node) *Node {
front := 0
queue := make([]*Node, 0, 10)
if root != nil {
//rear++
queue = append(queue, root)
first := queue[0].Left
for front != len(queue) {
q := queue[front]
front++
// visit(q)
if queue[front] != first {
q.Next = queue[front]
} else {
first = first.Left
}
if q.Left != nil {
//rear++
queue = append(queue, q.Left)
}
if q.Right != nil {
//rear++
queue = append(queue, q.Right)
}
}
}
return root
}
数组越界,gpt给的修改意见是:
if front < len(queue)-1 && queue[front] != first {
q.Next = queue[front+1]
} else {
first = first.Left
}
gpt还给出一种方案:
func connect(root *Node) *Node {
if root == nil {
return root
}
curLevel := root
nextLevel := curLevel.Left
for curLevel != nil {
for curLevel != nil {
// 处理当前节点的左子节点
curLevel.Left.Next = curLevel.Right
// 判断当前节点的右子节点和下一层的起始节点是否存在
if curLevel.Next != nil {
curLevel.Right.Next = curLevel.Next.Left
}
// 移动到下一个节点
curLevel = curLevel.Next
}
// 将指针移动到下一层
curLevel = nextLevel
if curLevel != nil {
nextLevel = curLevel.Left
}
}
return root
}
这些全都跑不起来。
找到一个能跑起来的答案:
func connect(root *Node) *Node { // 层序遍历,填充下一个右侧节点,全部遍历,依次指向
if root == nil {
return root
}
res := [][]*Node{}
queue := list.New()
queue.PushBack(root)
for queue.Len() > 0 {
size := queue.Len()
tmpArr := []*Node{}
for i := 0; i < size; i++ {
node := queue.Remove(queue.Front()).(*Node)
if node.Left != nil {
queue.PushBack(node.Left)
}
if node.Right != nil {
queue.PushBack(node.Right)
}
tmpArr = append(tmpArr, node)
}
res = append(res, tmpArr)
} // 每层节点指向右侧节点, 最后节点本身就是nil
for i := 0; i < len(res); i++ {
for j := 0; j < len(res[i])-1; j++ {
res[i][j].Next = res[i][j+1]
}
}
return root
}
// 很显然,对于反转而言,需要返回反转子树后的根节点,
// 所以对于二叉树的两种情况,递归和问题分解来讲,这就是问题分解的情况
func invertTree(root *TreeNode) *TreeNode {
if root == nil {
return nil
}
left := invertTree(root.Left)
right := invertTree(root.Right)
root.Right = left
root.Left = right
return root
}
其实这个题也可以用递归的思路来解决,我把当前节点的左右子树翻转,然后递归的去处理左右子树。
所以可以理解,递归和分解问题的思路其实差异点在于什么时候去处理某个点。