leetcode-labuladong-go语言实现

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 第一章 手把手刷数据结构
    • 手把手刷二叉树算法
      • 东哥带你刷二叉树(纲领篇)
        • 104. 二叉树的最大深度
        • 144. 二叉树的前序遍历
        • 543. 二叉树的直径
      • 东哥带你刷二叉树(思路篇)
        • 114. 二叉树展开为链表
        • 116. 填充每个节点的下一个右侧节点指针
        • 226. 翻转二叉树
  • 总结


前言


提示:以下是本篇文章正文内容,下面案例可供参考

第一章 手把手刷数据结构

手把手刷二叉树算法

东哥带你刷二叉树(纲领篇)

104. 二叉树的最大深度

// 这就是最基础的二叉树问题了,用分解子问题的方式解决
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
}

144. 二叉树的前序遍历

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)
}

543. 二叉树的直径

先给一个错误答案,其实逻辑上没啥问题,但是在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
}

东哥带你刷二叉树(思路篇)

114. 二叉树展开为链表

首先给出我自己的解法

// 分解子问题,每棵树要返回尾节点,因为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
}

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

这一题,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
}

226. 翻转二叉树

// 很显然,对于反转而言,需要返回反转子树后的根节点,
// 所以对于二叉树的两种情况,递归和问题分解来讲,这就是问题分解的情况
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
}

其实这个题也可以用递归的思路来解决,我把当前节点的左右子树翻转,然后递归的去处理左右子树。
所以可以理解,递归和分解问题的思路其实差异点在于什么时候去处理某个点。


总结

你可能感兴趣的:(leetcode,golang,算法)