算法面試題 - 二叉樹 - 節點路徑問題 - swift

題目:在一個二叉樹中(假定沒有重復元素),查找指定元素,輸出從根節點到該元素路徑上的所有節點的值
例子:假設有一個二叉樹如下:


那么5的路徑為[2, 7, 6, 5],4的路徑為[2, 5, 9, 4]

問題分析

由于題目給出的二叉樹并沒有排序,要找出對應節點必須要對整個樹進行遍歷,直到找到目標節點為止,可以采取后序深度優先遍歷,這樣的好處在于深度優先遍歷當查找到對應的節點時,當前保存在棧里的路徑剛好是問題需要的路徑。

數據結構定義

首先給出一個基本的數據結構定義

class Tree<T> {
  var left: Tree<T>? //為nil時表示沒有左子樹
  var right: Tree<T>? //為nil時表示沒有右子樹
  let value: T
  init(_ val: T) {
    value = val
  }
}

在這個定義中使用了Optional類型的數據來區分一個節點是否包含左右子樹,整體比較簡單

遞歸解法

一般的二叉樹的數據結構,都可以優先考慮使用遞歸來解決問題,遞歸本身也是處理這種問題比較自然的思路,并且非常簡潔,在這個題目里面使用遞歸的思路來描述解題思路用狀態來描述就是:
path(root, val) = \left\{ \begin{array}{ll} [val] & \textrm{如果root的值就是val}\\ [val] + path(root.left, val) & \textrm{如果val在左子樹}\\ [val] + path(root.right, val) & \textrm{如果val在右子樹}\\ nil & \textrm{找不到val}\\ \end{array} \right.
使用代碼來實現就是:

func find_path1<T: Equatable>(_ root: Tree<T>?, _ val: T) -> [T]? {
    guard let root = root else {
        return nil
    }
    if val == root.value {
        return [val]
    }
    //這里把左右子樹的查找合并在一起,減少一些代碼,邏輯和上面分析的是一致的
    if let path = find_path1(root.left, val) ?? find_path1(root.right, val) {
        return [root.value] + path
    }
    return nil
}

大概解釋一下這一段代碼:

  1. 要比較val的值,必須實現了判斷相等的協議Equatable
  2. 返回值是[T]?,而不是[T],實際上空數組也可以表示為沒有找到,這里考慮在實現的時候要判斷左右子樹返回的是否為空,用nil比較容易判斷,可以比較方便使用if let??操作符
  3. 參數是Tree<T>?而不是Tree<T>,這里也考慮的是遞歸調用的時候不需要判斷左右子樹是否為空
    當然代碼也可以像下面這樣,沒有太大區別,看個人習慣就好
func find_path2<T: Equatable>(_ root: Tree<T>, _ val: T) -> [T] {
    if val == root.value {
        return [val]
    }
    if let left = root.left {
        let path = find_path2(left, val)
        if !path.isEmpty {
            return [root.value] + path
        }
    }
    if let right = root.right {
        let path = find_path2(right, val)
        if !path.isEmpty {
            return [root.value] + path
        }
    }
    return [ ]
}

非遞歸解法

為什么要有非遞歸解法,確實,有了遞歸之后已經很方便了,但是非遞歸解法也有一些可取之處,這里不對遞歸和非遞歸優缺點進行討論。
使用非遞歸對二叉樹進行深度優先遍歷本身需要stack的支持,如果只是遍歷的話,有前中后三種順序進行輸出,不過我們要保存最后訪問元素和根節點之間的節點,不能多也不能少,剛好和后序遍歷的堆棧結構比較接近,所以根據后序遍歷的解法進行修改:

func find_path3<T: Equatable>(_ root: Tree<T>, _ val: T) -> [T] {
    var prev = root
    var stk = [root]

    while !stk.isEmpty {
        let top = stk.last!
        if top.value == val {
            //找到目標,返回路徑對應的值
            return stk.map { $0.value}
        }
        //if top為葉子結點或者左右節點都訪問過了,top出棧
        //else top的左節點訪問過了,那么下一步訪問右節點
        //其它情況訪問左節點
        if (top.left == nil && top.right == nil) ||
            prev === top.right ?? top.left {
            prev = stk.popLast()!
        } else if prev === top.left || top.left == nil {
            stk += [top.right!]
        } else {
            stk += [top.left!]
        }
    }
    //沒找到目標,返回空數組
    return [ ]
}

這個遍歷還有一種優化寫法,就是每次往棧中push節點的時候,一次性把這個節點的左子樹循環壓棧,這么修改一下算法稍微有一些區別,就是在判斷的地方要記得左子樹已經入棧了不用再判斷了,修改后的代碼如下:

func push_all_left<T>(_ node: Tree<T>) -> [Tree<T>] {
    var top : Tree<T>? = node
    var stk: [Tree<T>] = []
    while top != nil {
        stk += [top!]
        top = top?.left
    }
    return stk
}
func find_path4<T: Equatable>(_ root: Tree<T>, _ val: T) -> [T] {
    var prev = root
    var stk = push_all_left(root)

    while !stk.isEmpty {
        let top = stk.last!
        if top.value == val {
            return stk.map { $0.value}
        }
        if top.right == nil || prev === top.right {
            prev = stk.popLast()!
        } else {
            stk += push_all_left(top.right!)
        }
    }
    return []
}

遞歸數據結構

在函數式編程語言中最常見的就是歸納型數據,其實就是遞歸的數據結構,前面的數據結構中Tree里面包含Tree,其實就是一種歸納性質了,swift中還有另外一種另外一種數據結構的定義方法,更接近與函數式編程語言里面的概念,配合模式匹配使用,也是一種非常強大的功能。

indirect enum 數據結構定義

indirect enum BTree<T> {
    case Empty
    case Node(T, BTree<T>, BTree<T>)
}

定義中indirect表示這個數據結構可以遞歸使用,沒有這個關鍵詞就會報錯啦。
對于定義的兩個case,一個表示空二叉樹,一個表示節點,節點由一個值和左右子樹構成,整體和前面的結構并沒有太大區別,下面看看在實現find_path算法上有什么樣的不同:

func find_path5<T: Equatable>(_ tree: BTree<T>, _ val: T) -> [T]? {
    switch tree {
    case .Empty:
        return nil
    case .Node(val, _, _):
        return [val]
    case .Node(let value, let left, let right):
        if let path = find_path5(left, val) ?? find_path5(right, val) {
            return [value] + path
        }
    }
    return nil
}

這里在算法上和前面的遞歸算法并沒有區別,十分的簡單,使用enum的算法看起來更加清晰一些。

測試代碼

//從BTree到Tree的轉換
func convert<T>(tree: BTree<T>) -> Tree<T>? {
    switch tree {
    case .Empty:
        return nil
    case .Node(let root, let left, let right):
        let t = Tree(root)
        t.left = convert(tree: left)
        t.right = convert(tree: right)
        return t
    }
}

//開頭提到的那棵樹
let test = BTree.Node(2, .Node(7, .Node(2, .Empty, .Empty), .Node(6, .Node(5, .Empty, .Empty), .Node(11, .Empty, .Empty))), .Node(5, .Empty, .Node(9, .Node(4, .Empty, .Empty), .Empty)))

let test1 = convert(tree: test)!

let val = 4
let p1 = find_path1(test1, val)
let p2 = find_path2(test1, val)
let p3 = find_path3(test1, val)
let p4 = find_path4(test1, val)
let p5 = find_path5(test, val)

代碼還在這里:https://repl.it/@lency/path-for-tree

總結

遞歸的解法會比較優雅,簡潔,一般情況下考慮遞歸方式。
這個算法題目本身比較簡單,更多的是學習一些swift語法。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容