遞歸轉循環的通法


前段時間看書發現,但凡提到遞歸的地方,都會說一句,遞歸和循環是可以相互轉化的。剛開始,也沒有想到將所有遞歸轉為循環的辦法。像計算階乘,那自然沒什么好說的。但是有些問題,用遞歸真的很方便,可以不用管具體的實現順序,只要分析清楚終止條件和一次處理的邏輯就行了。那如果要把遞歸轉為循環,忍不住就要想幾個問題:

1、遞歸的原理是什么,若轉為循環,那么原理依舊相同嗎?還是說另辟蹊徑?

2、在循環的每一次迭代中,怎么保證下次迭代的順序和正確遞歸的順序保持一致?

3、循環的條件和遞歸的終結條件的異同。

第一個問題,也看了一些書籍和博客,並沒有發現別的思路,遞歸轉循環無非就是顯式地使用棧,思想是一樣的,對需要保存的信息壓棧,當前處理完畢之后出棧。其好處在於,與尾遞歸相似,不用多次創建棧,效率會有提升。

第二個問題,這個要根據情況處理,如果循環內可以有個完美的判斷鏈,自然可以使得順序與遞歸同步。如果情況復雜一點,需要自己設置額外變量,幫助判斷下次循環的運行方向。

第三個問題,循環的條件和遞歸的終結條件基本相同,對於循環使用的棧的判斷可以放到循環的邏輯里,不用加到判斷條件中。

鄙人不才,用python實現了兩個遞歸轉循環的實例。介紹兩個實例之前,給出自己的棧、節點、完全二叉樹實現代碼:

NONE_POINTER = -1
# 有二叉鏈表和三叉鏈表,區別在於是否包含parent節點的信息
class Node():
    # def __init__(self, data, parent=-1, left_child=-1, right_child=-1):
    def __init__(self, data):
        self.data = data
        self.left_child = NONE_POINTER
        self.right_child = NONE_POINTER
        self.l_tag = LINK
        self.r_tag = LINK
        # self.parent = NONE_POINTER

class Binary_Tree():
    logger_instance = my_logger('binary_tree')
    logger = logger_instance.initial_logger()

    def __init__(self, data_list):
        self.data_list = data_list
        self.check_data_list()
        self.root = Node(data_list.pop(0))
        self.level = 1
        self.level_nodes = [self.root]
        self.pre_node = NONE_POINTER
        self.create_bi_tree()

    def check_data_list(self):
        if len(self.data_list) <= 1:
            raise Exception('Error hmm_segger list!')

    # 使用層級遍歷的方式構造完全二叉樹
    def create_bi_tree(self):
        temp_nodes = []
        self.level += 1

        for node in self.level_nodes:
            if len(self.data_list) > 0:
                node.left_child = Node(self.data_list.pop(0))
            else:
                break
            if len(self.data_list) > 0:
                node.right_child = Node(self.data_list.pop(0))
            else:
                break
            temp_nodes.append(node.left_child)
            temp_nodes.append(node.right_child)

        if len(self.data_list) > 0:
            self.level_nodes = temp_nodes
            self.create_bi_tree()

 

准備代碼完成之后,展示python的中序遍歷的循環代碼。

插,為什么要設置額外變量,因為中序遍歷對應的循環內容有兩種可能,一個是將自己壓棧向左子樹前進遍歷,一個是遍歷自己后向右子樹前進遍歷。正好對應一次遞歸中的兩次子遞歸的過程,所以需要額外變量幫助判斷轉向。

見下:

def in_order_traverse(binary_tree):
    tree_stack = my_stack()
    cur_node = binary_tree  # 記錄當前處理節點
    state = 0               # (靠額外變量判斷循環的迭代方向)記錄節點遍歷的方向,0表示向左遍歷,1表示輸出自己后向右遍歷

    # 對左子樹進行遍歷
    while cur_node != NONE_POINTER:
        if state == 0:
            if cur_node.left_child != NONE_POINTER:     # 不為空,繼續向左遍歷
                tree_stack.push(cur_node)
                cur_node = cur_node.left_child
            else:
                print '',
                print cur_node.data,                     # 為空輸出自己,出棧並將遍歷方向改為輸出自己向右遍歷
                cur_node = tree_stack.pop() if not tree_stack.is_empty() else NONE_POINTER
                state = 1
        else:
            print '',
            print cur_node.data,
            if cur_node.right_child != NONE_POINTER:    # 右不為空,則右子樹代替當前節點,從右子樹根節點向左遍歷
                cur_node = cur_node.right_child
                state = 0
            else:
                cur_node = tree_stack.pop() if not tree_stack.is_empty() else NONE_POINTER

快速排序的循環版本代碼如下:

def quick_sort(nums):
    start, end = 0, len(nums)-1
    nums_stack = my_stack()

    while start < end:
        flag = nums[start]
        cur_index = start
        # 快排邏輯
        for i in xrange(start+1, end+1):
            if nums[i] < flag:
                nums[cur_index] = nums[i]
                nums[i] = nums[cur_index+1]
                cur_index += 1
        nums[cur_index] = flag

        # 安排下一次迭代起止位
        if cur_index - start > 1:
            if end - cur_index > 1:     # 壓棧
                nums_stack.push([cur_index+1, end])
            end = cur_index - 1

        elif end - cur_index > 1:
            start = cur_index + 1

        elif not nums_stack.is_empty():
            [start, end] = nums_stack.pop()     # 出棧
        else:
            start = end = 0

代碼有點糙,格式不太規范,請輕拍。

后續分界線


時隔兩個月,在思考DFS算法時,我又想到了用棧顯式的去實現。想了好一會,寫出了一段偽代碼,慚愧。。。。 先給出相關數據結構的定義,此處采用鄰接鏈表的形式存儲圖。

// 圖節點
class Node<T>{
    T data;
    neighborNode neighbor;
}

// 鄰接鏈表節點
class neighborNode{
    int index;
    neighborNode next;
}

在寫之前,我問了自己幾個問題,並如下作答:

  • 什么時候壓棧?

向更深層次遍歷的時候,將下個鄰居壓棧。

  • 什么時候出棧?

無更深層次節點可遍歷 或者說 所有鄰居都遍歷過 的時候。

  • 下個遍歷節點的優先級?

1>未訪問過的鄰居   2>未訪問過的棧頂節點

  • 什么時候結束?

需要出棧但棧為空的時候。(也是讓我再寫一版的原因)

然后我就寫了如下偽代碼:

 1 DFS(w)    //鄰接鏈表,節點保存在數組中,每個節點都有個指向其鄰居鏈表的指針
 2     visit w and mark it as visited;
 3     if w.next is not null then
 4         Stack.push(w.next);
 5     end if
 6     
 7     while(Stack is not empty) do    
 8         pointer ← Stack.pop();    //一次回退
 9         while(arr[pointer.index] is visited and pointer.next is not null) do
10             pointer ← pointer.next;
11         end while
12         
13         //深入遍歷
14         while(arr[pointer.index] is not visited) do
15             visit arr[pointer.index] and mark it as visited;
16             
17             for each neighbor p of arr[pointer.index] do
18                 if arr[p.index] is not visited then
19                     pointer ← p;
20                     Stack.push(pointer.next); //向下一層遍歷之前,將下個位置壓棧
21                     break;
22                 end if
23             end for
24         end while

說實話,寫了好幾次,最后才定義清楚 終止條件和一次循環的工作內容。 剛開始覺得非常晦澀,難懂也難記。在鄙人看來,真正好的算法應該是思路清晰且簡單的,看了之后可以舉一反三,所以對於上面的代碼我是拒絕的。就此代碼來說,如果定義棧空為終止條件,那么勢必要額外處理邊界情況。而且一次出棧為一個循環的話,一次循環的工作內容又過多,寫出來又要花很多時間思考。

后來,我又來翻看這篇博客,看到二叉樹中序遍歷的循環寫法,一直縈繞心頭的循環條件終於有了新的出路。棧不一定要為空,每次循環只做一件事,即便利后確定下個遍歷節點。對,無節點可遍歷就是循環終止條件。所以,我又寫了一個版本,代碼本身是可以更短的,這里為了方便理解,就不縮減了。

 1 DFS(w)    //鄰接鏈表,節點保存在數組中,每個節點都有個指向其鄰居鏈表的指針
 2     visit w and mark it as visited;
 3     if w.next is not null then
 4         pointer ← w.next;
 5     end if
 6     
 7     while(arr[pointer.index] is not visited) do    //一次只遍歷一個,在無可遍歷節點時退出
 8         visit arr[pointer.index] and mark it as visited;
 9         
10         for each neighbor p of arr[pointer.index] do //優先選取未遍歷的鄰居
11             if arr[p.index] is not visited then
12                 pointer ← p;
13                 Stack.push(pointer.next); //向下一層遍歷之前,將下個位置壓棧
14                 break;
15             end if
16         end for
17         
18         while(arr[pointer.index] is visited and Stack is not empty) //鄰居都遍歷過,退棧
19             pointer ← Stack.pop();
20             //若節點已遍歷,再優先選取其未遍歷的鄰居
21             while(arr[pointer.index] is visited and pointer.next is not null) do
22                 pointer ← pointer.next;
23             end while
24         end while

以上


 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM