二叉樹迭代器演算法

來源:互聯網
上載者:User

二叉樹(Binary Tree)的前序、中序和後續遍曆是演算法和資料結構中的基本問題,基於遞迴的二叉樹遍曆演算法更是遞迴的經典應用。

假設二叉樹結點定義如下:

struct Node {    int value;    Node *left;    Node *right;}

中序遞迴遍曆演算法:

// C++void inorder_traverse(Node *node) {    if (NULl != node->left) {        inorder_traverse(node->left);    }    do_something(node);    if (NULL != node->right) {        inorder_traverse(node->right);    }}

前序和後序遍曆演算法類似。

但是,僅有遍曆演算法是不夠的,在許多應用中,我們還需要對遍曆本身進行抽象。假如有一個求和的函數sum,我們希望它能應用於鏈表,數組,二叉樹等等不同的資料結構。這時,我們可以抽象出迭代器(Iterator)的概念,通過迭代器把演算法和資料結構解耦了,使得通用演算法能應用於不同類型的資料結構。我們可以把sum函數定義為:

int sum(Iterator it)

鏈表作為一種線性結構,它的迭代器實現非常簡單和直觀,而二叉樹的迭代器實現則不那麼容易,我們不能直接將遞迴遍曆轉換為迭代器。究其原因,這是因為二叉樹遞迴遍曆過程是編譯器在調用棧上自動進行的,程式員對這個過程缺乏足夠的控制。既然如此,那麼我們如果可以自己來控制整個調用棧的進棧和出棧不是就達到控制的目的了嗎?我們先來看看二叉樹遍曆的非遞迴演算法:

// C++void inorder_traverse_nonrecursive(Node *node) {    Stack stack;    do {        // node代表當前準備處理的子樹,層層向下把左孩子壓棧,對應遞迴演算法的左子樹遞迴        while (NULL != node) {            stack.push(node);            node = node->left;        }        do {            Node *top = stack.top();            stack.pop(); //彈出棧頂,對應遞迴演算法的函數返回            do_something(top);             if (NULL != top->right) {                node = top->right; //將當前子樹置為剛剛遍曆過的結點的右孩子,對應遞迴演算法的右子樹遞迴                break;            }        }        while (!stack.empty());    }    while (!stack.empty()); }

通過基於棧的非遞迴演算法我們獲得了對於遍曆過程的控制,下面我們考慮如何將其封裝為迭代器呢? 這裡關鍵在於理解遍曆的過程是由棧的狀態來表示的,所以顯然迭代器內部應該包含一個棧結構,每次迭代的過程就是對棧的操作。假設迭代器的介面為:

class Iterator {    public:        virtual Node* next() = 0;};

下面是一個二叉樹中序遍曆迭代器的實現:

//C++class InorderIterator : public Iterator {    public:        InorderIterator(Node *node) {            Node *current = node;            while (NULL != current) {                mStack.push(current);                current = current->left;            }        }        virtual Node* next() {            if (mStack.empty()) {                return NULL;            }            Node *top = mStack.top();            mStack.pop();            if (NULL != top->right) {                Node *current = top->right;                while (NULL != current) {                    mStack.push(current);                    current = current->left;                }            }            return top;         }    private:        std::stack<Node*> mStack;};

下面我們再來考察一下這個迭代器實現的時間和空間複雜度。很顯然,由於棧中最多需要儲存所有的結點,所以其空間複雜度是O(n)的。那麼時間複雜度呢?一次next()調用也最多會進行n次棧操作,而整個遍曆過程需要調用n次next(),那麼是不是整個迭代器的時間複雜度就是O(n^2)呢?答案是否定的!因為每個結點只會進棧和出棧一次,所以整個迭代過程的時間複雜度依然為O(n)。其實,這和遞迴遍曆的時空複雜度完全一樣。

除了上面顯式利用棧控制碼執行順序外,在支援yield語義的語言(C#, Python等)中,還有更為直接的做法。下面基於yield的二叉樹中序遍曆的Python實現:

// Pythondef inorder(t):    if t:        for x in inorder(t.left):            yield x        yield t.label        for x in inorder(t.right):            yield x

yield與return區別的一種通俗解釋是yield返回時系統會保留函數調用的狀態,下次該函數被調用時會接著從上次的執行點繼續執行,這是一種與棧語義所完全不同的流程式控制制語義。我們知道Python的解譯器是C寫的,但是C並不支援yield語義,那麼解譯器是如何做到對yield的支援的呢? 有了上面把遞迴遍曆變換為迭代遍曆的經驗,相信你已經猜到Python解譯器一定是對yield代碼進行了某種變換。如果你已經能夠實現遞迴變非遞迴,不妨嘗試一下能否寫一段編譯器將yield代碼變換為非yield代碼。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.