美文网首页
面试回顾与解析:在O(logN)时间复杂度下求二叉树中序后继

面试回顾与解析:在O(logN)时间复杂度下求二叉树中序后继

作者: 海之方 | 来源:发表于2019-11-28 22:15 被阅读0次

回顾

话说,一多个月前,那时我刚刚开始找工作,对猎头推的一家跨境电商w**h很感兴趣,大有志在必得的兴致。

猎头和我打过招呼,这家公司算法必考,无奈只有一周时间复习的我,按照自己的猜测,在leetcode上重点的刷了下字符串、动态规划之类的题。

可惜压题失败,真正面试时,面试官考的是二叉树中序后继。唉,当场先是有些失落,接着就是一点小紧张。

回想起来,幸亏面试是通过牛客网做的远程面试,要不这些情绪变化一定落在的面试官眼里。

心神不宁的我,又偏偏遇上了一个较真的面试官,非让我和他说清楚思路再开始写……

当场我是没有完全写出来,但心中确是很不服气,毕竟已经写出大半。于是,面试完又努力了一下,把代码完整的写了出来,发给HR。心想着如果转给面试官看一看,也许还有一线生机,但最终还是跪了……

回顾完了面试过程,下面进入正题。

题目

给一个二叉树,以及一个节点(此节点在二叉树中一定存在),求该节点的中序遍历后继,如果没有返回null

树节点的定义如下:

type TreeNode struct {
    Val int
    Parent *TreeNode
    Left *TreeNode
    Right *TreeNode
}

思路分析

首先,树的中序遍历是遵循(左)-(根)-(右)的顺序依次输出树的节点。

于是,要想知道当前节点的中序后继是什么,首先要知道当前节点在它的父节点的左侧还是右侧。

如果它是在其父节点的左侧,那就简单了,直接返回它的父节点就好。

如果它是在其父节点的右侧,情况又分为以下两种:

  1. 如果它有右侧的子节点,那么下一个节点就是以它右侧子节点为根的子树的最左侧节点。
  2. 如果它没有右侧子节点,需要向上回溯它的父节点,然后用这个父节点为新的当前节点,在排除已经访问过的节点的前提下,重复上述的判断过程。

根据这个思路,我写了下面的解法。

解法

func FindNext(root, toFind *TreeNode) *TreeNode  {
    tmpCurr := toFind
    paths := []*TreeNode {tmpCurr}

    for tmpCurr.Parent != nil{
        paths = append(paths, tmpCurr.Parent)
        tmpCurr = tmpCurr.Parent
    }

    tmpCurr = paths[0]
    leftRights := []bool{}

    for i:=1; i<len(paths); i++{
        parent := paths[i]
        if parent.Left == tmpCurr{
            leftRights = append(leftRights, true)
        }else {
            leftRights = append(leftRights, false)
        }
        tmpCurr = parent
    }

    visitedMap := make(map[*TreeNode]struct{})
    tmpCurr = toFind

    for i:=0; i<len(leftRights); {
        onLeft := leftRights[i]
        if onLeft {
            return tmpCurr.Parent
        } else {
            _, visited := visitedMap[tmpCurr.Right]
            visitedMap[tmpCurr] = struct{ }{}
            if tmpCurr.Right != nil  && !visited {
                newRoot := tmpCurr.Right
                for newRoot.Left != nil{
                    newRoot = newRoot.Left
                }
                return newRoot
            } else {
                tmpCurr = tmpCurr.Parent
                i++
            }
        }
    }

    return  nil
}

当时的测试用例:

func main() {
    tn11 := &TreeNode{Val:11}
    tn12 := &TreeNode{Val:12}

    tn8:= &TreeNode{Val:8, Left: tn11, Right: tn12}
    tn11.Parent = tn8
    tn12.Parent = tn8

    tn5 := &TreeNode{Val:5, Right:tn8}
    tn8.Parent = tn5

    tn4 := &TreeNode{Val:4}

    tn2 := &TreeNode{Val:2, Left:tn4, Right:tn5}
    tn4.Parent = tn2
    tn5.Parent = tn2

    tn9 := &TreeNode{Val:9}

    tn6 := &TreeNode{Val:6, Right:tn9}
    tn9.Parent = tn6

    tn10 := &TreeNode{Val:10}

    tn7 := &TreeNode{Val:7, Left:tn10}
    tn10.Parent = tn7

    tn3 := &TreeNode{Val:3, Left:tn6, Right:tn7}
    tn6.Parent = tn3
    tn7.Parent = tn3

    tn1 := &TreeNode{Val:1, Left:tn2, Right:tn3}
    tn2.Parent = tn1
    tn3.Parent = tn1

/**树的样子如图
                      1
                  /      \
                 2        3
                / \     /   \
               4   5    6    7
                    \    \   /
                      8   9  10
                     / \
                    11 12
                                                  **/

    res := FindNext(tn1, tn8)
    fmt.Println(res)

    res = FindNext(tn1, tn12)
    fmt.Println(res)

    res = FindNext(tn1, tn9)
    fmt.Println(res)
}

存在的问题

写文章时,我才发现,这个解法有一个bug,是判断根的节点的下一个节点时,永远都返回nil,这是不对的。

这个坑也是自己挖的,上面的代码在一开始时就计处当前节点回溯到根节点的路径,一方面这不是必须提前全部算好;另一方面,也忽略了当前节点是根节点的边界状态。

改进的解法

其实,改进也很简单,一方面,使用双指针,一个代表当前节点,另一个代表它的父节点,即可以不必提前算出当下节点到根节点的路径; 另一方面,需要对根节点是当前的节点的情况做些特殊处理,让程序“误以为”当前节点在根节点的右侧,这样后面的逻辑就能对得上了。

改进后的代码如下:

func FindNext(root, toFind *TreeNode) *TreeNode  {
    tmpCurr := toFind
    tmpParent := toFind.Parent

    if toFind == root {
        tmpParent = root // cheat the parent nil check, and it will go to the right child branch
    }

    visitedMap := make(map[*TreeNode]struct{})

    for tmpParent != nil {
        if tmpParent.Left == tmpCurr {
            return tmpParent
        } else { 
            _, visited := visitedMap[tmpCurr.Right]
            visitedMap[tmpCurr] = struct{ }{}
            if tmpCurr.Right != nil  && !visited{
                tmpCurr = tmpCurr.Right
                for tmpCurr.Left != nil {
                    tmpCurr = tmpCurr.Left
                }
                return tmpCurr
            } else {
                tmpCurr = tmpParent
                tmpParent = tmpCurr.Parent
            }
        }
    }

    return  nil
}

总结

关于面试是否要考纯算法题,一直众说纷纭。

反对者说,工作中大都是写业务代码,哪有需要用到算法的地方,能有机会写个递归就顶天了。

支持者说,算法题可以考查候选人的逻辑能力和编码的严谨性,逻辑能力强、编码严谨的工程师做业务开发也一定不会差呀!

其实,说到底考算法只是企业筛选候选人的一种方式。如果企业品牌在外,根本不差候选人,自然可以大浪淘沙,不求合适,但求最好,算法不行,一律pass。否则的话,还是需要多方面考虑,合适最重要。

从面试者的角度来看,遇到不熟悉算法题也不要慌,一般面试时考察的算法不会太冷门,只要基础扎实,冷静分析,即使做不能完全做出来,也能写出个大概来。至于能不能通过面试,那就不好说啦。

如果真不想在算法题上吃亏,请出门左拐,leetcode刷题去吧。除了刷题,好像也没啥好办法,谁让你想进大厂呢?

相关文章

  • 面试回顾与解析:在O(logN)时间复杂度下求二叉树中序后继

    回顾 话说,一多个月前,那时我刚刚开始找工作,对猎头推的一家跨境电商w**h很感兴趣,大有志在必得的兴致。 猎头和...

  • P75-求n次方

    O(logn)时间复杂度求Fibonacci数列

  • 10.算法设计思想之"分而治之"

    时间复杂度 O(logN) && 空间复杂度 O(n) LeeCode:226.翻转二叉树 时间复杂度 O(n) ...

  • 归并排序 by Python

    最好时间复杂度:O(n*logn)最坏时间复杂度:O(n*logn)平均时间复杂度:O(n*logn)空间复杂度:...

  • 快速排序 by Python

    最好时间复杂度:O(n*logn)最坏时间复杂度:O(n²)平均时间复杂度:O(n*logn)空间复杂度:O(1)...

  • 总结八

    时间复杂度 由上图,可见,O(1) 最小,O(logn) 次之,比如二分查找就是 O(logn) 时间复杂度可见 ...

  • 分治与递归--实数的整数次幂

    给定实数 x 和整数 n, 求 x的n次幂时间复杂度:O(logN)

  • 时间复杂度

    时间复杂度o(1), o(n), o(logn), o(nlogn) 1、时间复杂度o(1), o(n), o(l...

  • 查找算法

    顺序查找 时间复杂度O(n) 折半查找(二分查找) 时间复杂度O(logn) 把n个数化为一颗二叉树输的高度为l...

  • 复杂度

    1. 时间复杂度 常见时间复杂度高低 O(1) < O(logn) < O(n) < O(nlogn) < O(l...

网友评论

      本文标题:面试回顾与解析:在O(logN)时间复杂度下求二叉树中序后继

      本文链接:https://www.haomeiwen.com/subject/dnviwctx.html