1. 程式人生 > 程式設計 >面試回顧與解析:在O(logN)時間複雜度下求二叉樹中序後繼

面試回顧與解析:在O(logN)時間複雜度下求二叉樹中序後繼

回顧

話說,一多個月前,那時我剛剛開始找工作,對獵頭推的一家跨境電商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刷題去吧。除了刷題,好像也沒啥好辦法,誰讓你想進大廠呢?