2020年8月31日 星期一

Graph DFS and BFS 深度優先 廣度優先 Tree Traversal

 今天想要探討一下關於Graph裡面的DFS、BFS、Stack Treversal以及Binary Tree的Preorder、InOrder、PostOrder Traversal。

先看一下DFS的Iterative代碼

def dfs(graph, node):

    parents = {node:None}

    visited = {}

    stack = [node]

    while stack:

        v = stack.pop()

        if not v in visited:

            visited[v] = parents[v]

            for a in reversed(graph[v]):

                stack.append(a)

                parents[a] = v

        print(v, ''.join(stack))

    return visited

DFS代碼最重要的是使用stack並且拿出最後一個node出來,然後檢查確認這個node沒有曾經處理過的話才會被“搜全家”並且放進stack等待處理,換句話說如果這個node曾經被處理過的話下次再遇到的話它就會被忽略掉。那麼什麼時候一個node在stack拿出來的時候就已經被處理過呢?唯一的可能性就是一個node被重複放到stack裡面,第一次拿出來的時候沒有處理過,第二次拿出來的時候就知道已經處理過了。Wikipedia也寫著"DFS has worst case O(|E|) with possible duplication of nodes on the stack"。

舉個例子掌心對著自己的那5隻手指全部互聯的話會產生5角形以及5角星合共10條邊,在第一次iteration處理拇指之後stack裡面就有4根手指,在第二次iteration處理食指之後stack除了原有拇指連著的的中無尾3根手指之外又要添加4根重複的母中無尾手指合共7根手指。stack裡面最長的暫存資料是5435454321或者“尾無中尾無尾無中食母”,剛好是10個資料並且是合共的10條邊。

正因為node可以被多次重複放進stack,所以才能夠過讓stack裡面的node“走得更遠”,並且達到最大的深度。DFS會從第一個分支的頭部開始一邊蔓延一邊儲存在stack裡面直到末端,然後才跑到第二個分支。


再來看一下BFS的Iterative代碼

def bfs(graph, node):

    parents = {node:None}

    visited = {node:None}

    deque = collections.deque([node])

    while deque:

        v = deque.popleft()

        for a in reversed(graph[v]):

            if a not in visited:

                parents[a] = v

                visited[a] = parents[a]

                deque.append(a)

        print(v, ''.join(deque))

    return visited

BFS的代碼特點除了使用queue代替stack去暫存資料之外,它的處理方法是先“搜全家”然後確認沒有被處理過才會被放進暫存queue裡面,而且由於全部node都會確認沒有被處理過才放進去queue裡面,因此queue裡面不可能有重複的node,而且每一個node都只會被放進queue裡面1次而不是多次。


Stack Traversal

BFS與DFS看起來有點相似的,的確它們都是用來搜索整顆樹,但是他們的特點都是有各自的優先的。假設在BFS的演算法裡面的queue換成stack就會變成DFS嗎?不會啊,姑且叫它做stack traversal,雖然兩者都可以搜索整顆樹,但是stack traversal不是深度優先。

深度優先的演算法的重點在於每個node的“全家”會被無條件添加到stack裡面,至於要不要處理就等到他們的從stack拿出來的時候才檢查,越遲檢查的話stack的深度就越長,達到深度優先的效果。

那麼stack traversal為什麼不是深度優先?stack traversal將BFS的queue換成stack,僅此而已。因此BFS的其中一個特質被保留:先檢查node沒有處理過才會放進暫存的stack或者queue,因此接近七點的node的家人一早就被認定已經處理過了,而且這些node只會被放進暫存最多1次而不是多次,因此其後如果發現有backedge的話那些node不會在被放進暫存了,因此深度被中斷了,也因此不是深度優先。


比較DFS與BFS

DFS的精髓在於允許node重複放進暫存,並且等到拿出來的時間才檢查是否曾經處理過,因此暫存的stack增長的很快很深而且有重複。

BFS則是先檢查是否曾經處理過才放進暫存queue的,而且不允許重複放進queue裡面,因此queue裡面的資料就是“沿著起點繞圈”往外擴展,也許將BFS叫做Sprial Search更為生動一些。


Preorder

Preorder使用“上左右”的方法穿越整顆樹,其實就是用DFS的無HashMap版本。不需要HashMap的原因是因為BinaryTree沒有backedge不會存在一個node被處理多次。

class Solution:

    def preorderTraversal(self, root: TreeNode) -> List[int]:

        answer = []

        stack = [root]

        while stack:

            node = stack.pop()

            if node:

                answer.append(node)

                stack.append(node.right)

                stack.append(node.left)

        return answer


Postorder

Postorder使用“左右上”的方法穿越整顆樹,其實也是使用DFS的無HashMap版本。Postorder與Preorder都是從上到下穿越整顆樹的,只是最後結果需要反向一次。

class Solution:

    def postorderTraversal(self, root: TreeNode) -> List[int]:

        answer = []

        stack = [root]

        while stack:

            node = stack.pop()

            if node:

                answer.append(node.val)

                stack.append(node.left)

                stack.append(node.right)

        return reversed(answer)


Inorder

Inorder使用的方法是先靠左DFS,去到盡頭之後將指針往右移動,然後繼續DFS。演算法運行起來像中文字的”久“。

class Solution:

    def inorderTraversal(self, root: TreeNode) -> List[int]:

    # func(left)+[val]+func(right)

        answer = []

        stack = []

        while stack or root:

            if root:

                stack.append(root)

                root = root.left

            else:

                node = stack.pop()

                if node:

                    answer.append(node)

                    root = node.right

        return answer






End

沒有留言:

張貼留言

2007 to 2023 HP and Dell Servers Comparison

  HP Gen5 to Gen11  using ChatGPT HP ProLiant Gen Active Years CPU Socket Popular HP CPUs Cores Base Clock Max RAM Capacity Comparable Dell ...