1. 程式人生 > 其它 >程式碼手記筆錄——二叉樹

程式碼手記筆錄——二叉樹

寫在前面

(1)二叉樹遍歷順序包括先序遍歷、中序遍歷、後序遍歷。二叉樹遍歷實現包括迭代實現、遞迴實現。
(2)二叉樹遍歷性質:若設定空節點元素值為 NULL,則先序/中序遍歷一棵二叉樹,能唯一得到一組數值序列。且該數值序列唯一對應一棵二叉樹。故可以通過遍歷數值序列判斷兩棵樹是否具有相同的(全域性/區域性)結構。
(3)遞迴遍歷與遞迴-回溯的關聯:先序遍歷若在 左遞迴右遞迴 函式之間進行回溯操作,則可變成 遞迴-回溯 演算法:257 二叉樹的所有路徑。
(4)層序遍歷/迭代遍歷很難記錄結點的父結點,所以不太適合重建樹、構造樹的任務。對於重建樹、構造樹的任務,最適合的是遞迴遍歷。例如617 合併二叉樹。

二叉樹遍歷的迭代實現

二叉樹遍歷的迭代實現共用一個模板,特殊的是後序遍歷需要在當前節點存在右節點且右節點未被訪問條件下將彈出的元素再次壓棧=> if(p->right && pre != p->right);。為了標誌上一次訪問過的節點,每次訪問一個節點就用 pre 指標指向它。=> pre = p;,將其從原來的樹中取出=>p=nullptr;
二叉樹迭代遍歷初始時並不將根壓入棧,且 p 指向 root,並且跳出迴圈的條件=> while (!st.empty() || p)

先序遍歷模板

while (!st.empty() || p) {
    while (p) {
        ...  // read root data
        st.push(p);  // push into stack
        p = p->left;  // 一直左走
    }
    // 彈出棧頂元素並向其右孩子走
    p = st.top();
    st.pop();
    p = p->right;
}

中序遍歷模板

while (!st.empty() || p) {
     while (p) {
        st.push(p);  // push into stack
        p = p->left;  // 一直左走
    }
    // 彈出棧頂元素並向其右孩子走
    p = st.top();
    st.pop();
    ... // read root data
    p = p->right;
}

後序遍歷模板

while (!st.empty() || p) {
    while (p) {
        st.push(p);  // push into stack
        p = p->left;  // 一直左走
    }
    // 彈出棧頂元素並向其右孩子走
    p = st.top();
    st.pop();
    // 若右節點有,且未訪問過,則將右節點重新放入棧
    if (p->right && p->right != pre) {
        st.push(p);
        p = p->right;
    }
    else {
        ... // read root data
        pre = p;   // 每次訪問一個節點就用 pre 指標指向它。
        p = nullptr; 
    }
}

章節題目總結

101 對稱二叉樹

演算法思想:複製根節點為另一棵二叉樹,採用層序遍歷方法,一棵二叉樹先左節點後右節點,一棵二叉樹反過來——先右節點後左節點。兩棵樹的結點交叉進佇列。

TreeNode* p, *q;
deq.push_back(left);        
deq.push_back(right);
while (!deq.empty()) {
    p = deq.front();
    deq.pop_front();
    q = deq.front();
    deq.pop_front();
    if (!p && !q) 
        continue;
    if (!p || !q || (p->val != q->val))
        return false;
    // 交叉進佇列
    deq.push_back(p->left);
    deq.push_back(q->right);
    deq.push_back(p->right);
    deq.push_back(q->left);
}

222 完全二叉樹的個數【妙:深度遍歷+位操作】

註釋:這道題很妙,妙在利用結點的數值記號的數學規律判斷其在樹中的位置,有選擇性地進行深度遍歷,減少了遍歷時間複雜度。
完全二叉樹性質:

  • 深 h 的滿二叉樹總個數推導【等比數列求和a1(s-q^n)/(1-q)】【h 從 0 開始】
  • 第 h 層結點個數的推導
  • 結點數值記號的數學規律:按 1 開始標記,9號元素=1001 代表在第元素 4 層,先從根向左走,再 2 次向右走,再向左走。
class Solution {
public:
    int countNodes(TreeNode* root) {
        if (!root)
            return 0;
        if (!root->left && !root->right)
            return 1;
        TreeNode* p = root;
        int h = -1;
        while (p) {
            ++h;
            p = p->left;
        }
        int low = 1 << h, high = 1<<(h+1), mid, ans;

        while (low < high) {
            mid = low + ((high-low)>>1);
            if (exist(root, mid, h)) {
                ans = mid;
                low = mid + 1;
            }
            else
                high = mid;
        }
        return ans;
    }
    bool exist(TreeNode* root, int k, int h) {
        TreeNode *p = root;
        int bitFlag = 1 << (h-1);
        while (bitFlag) {
            int dir = k & bitFlag;
            if (!dir)
                p = p->left;
            else
                p = p->right;
            if (!p)
                return false; 
            bitFlag >>= 1;
        }
        return true;
    }
};

二叉樹遍歷的遞迴實現

我們在遞迴遍歷二叉樹的時候,要有意識地知道當前使用的遞迴是對樹進行先序遍歷/中序遍歷/後序遍歷。

先序遍歷模板

void traversal(TreeNode* cur, vector<int>& vec) {
    if (cur == NULL) return;
    vec.push_back(cur->val);    // 中
    traversal(cur->left, vec);  // 左
    traversal(cur->right, vec); // 右
}

中序遍歷模板

void traversal(TreeNode* cur, vector<int>& vec) {
    if (cur == NULL) return;
    traversal(cur->left, vec);  // 左
    vec.push_back(cur->val);    // 中
    traversal(cur->right, vec); // 右
}

後序遍歷模板

void traversal(TreeNode* cur, vector<int>& vec) {
    if (cur == NULL) return;
    traversal(cur->left, vec);  // 左
    traversal(cur->right, vec); // 右
    vec.push_back(cur->val);    // 中
}

章節題目總結

110 平衡二叉樹

該解法相當於對二叉樹進行後序遍歷。

class Solution {
public:
    bool isBalanced(TreeNode* root) {
        int depth = 1;
        return isBalanced(root, depth);
    }
    bool isBalanced(TreeNode* root, int &depth) {
        if (!root)
            return true;
        bool lftBalanced, rgtBalanced;
        int lftDepth = depth+1, rgtDepth = depth+1;
        lftBalanced = isBalanced(root->left, lftDepth);
        if (!lftBalanced)
            return false;
        rgtBalanced = isBalanced(root->right, rgtDepth);
        if (!rgtBalanced)
            return false;
        depth = max(lftDepth, rgtDepth);  // 如果使用傳引數,別忘了要動態更新這個值
        return abs(lftDepth-rgtDepth) <= 1 ? true : false;
    }
};

572. 另一棵樹的子樹

演算法思想:任何一棵樹,先序遍歷的序列是唯一的。因此我們可分別對主樹與子樹進行先序遍歷,然後進行 KMP 匹配 。由於會出現空節點,因此我們要 用 lNull,rNull分別表示左右兩個空節點。可以令 lNULL=INT_MIN,rNULL=INT_MAX,也可以先遍歷樹找出這棵樹的最大值,然後將不位於該數的值作為lNull,rNull。

class Solution {
public:
    bool isSubtree(TreeNode* root, TreeNode* subRoot) {
        vector<int> mTreeVec, sTreeVec;
        lNULL = INT_MIN, rNULL = INT_MAX;
        dfsTree(root, mTreeVec);
        dfsTree(subRoot, sTreeVec);
        vector<int> next(sTreeVec.size(), 0);
        getNext(sTreeVec, next);
        return KMP(mTreeVec, sTreeVec, next);
    }
private:
    int lNULL, rNULL;
    void dfsTree(TreeNode* root, vector<int> &vec) {
        if (!root)
            return;
        vec.push_back(root->val);
        if (root->left)
            dfsTree(root->left, vec);
        else
            vec.push_back(lNULL);
        if (root->right)
            dfsTree(root->right, vec);
        else
            vec.push_back(rNULL);
    }
    bool KMP(const vector<int> &mTreeVec, const vector<int> &sTreeVec, const vector<int> &next) {
        int j = 0;
        for (int i=0; i<mTreeVec.size(); ++i) {
            while (j>0 && mTreeVec[i] != sTreeVec[j])
                j = next[j-1];
            if (mTreeVec[i] == sTreeVec[j])
                ++j;
            if (j >= sTreeVec.size())
                return true;
        }
        return false;
    }
    void getNext(const vector<int> &sTreeVec, vector<int> &next){
        int pre = 0;
        next[pre] = 0;
        for (int cur=1; cur < sTreeVec.size(); ++cur) {
            while (pre > 0 &&  sTreeVec[cur] != sTreeVec[pre])
                pre = next[pre-1];
            if (sTreeVec[cur] == sTreeVec[pre])
                ++pre;
            next[cur] = pre;
        }
    }
};

257 二叉樹的所有路徑

class Solution {
public:
    vector<string> binaryTreePaths(TreeNode* root) {
        vector<string> ans;
        string path;
        binaryTreePaths(ans, root, path);
        return ans;
    }
private:
    void binaryTreePaths(vector<string> &ans, TreeNode* root, string &path) {
        string pValStr = to_string(root->val);
        if (path.size() > 0)
            path += "->";
        path += pValStr;
        if (!root->left && !root->right) {
            ans.push_back(path);
            return;
        }
        if (root->left) {
            binaryTreePaths(ans, root->left, path);  // 遞迴
            string lftValStr = to_string(root->left->val); // 回溯
            path.resize(path.size() - lftValStr.size() - 2);
        }
        if (root->right) {
            binaryTreePaths(ans, root->right, path);
            string rgtValStr = to_string(root->right->val);
            path.resize(path.size() - rgtValStr.size() - 2);
        }
    }
};

106. 從中序與後序遍歷序列構造二叉樹

核心程式碼:

...   // 找左-右子樹結點值區間
TreeNode* root = new TreeNode(rootVal);  // 構造根節點  
root->left = buildTree(...);   // 根節點指向左子樹的根節點
root->right = buildTree(...);  // 根節點指向右子樹的根節點

二叉樹層序遍歷

演算法思想:二叉樹層序遍歷初始時將根壓入佇列,p 不指向 root,lvlEnd指向 root,並且跳出迴圈條件=>while (!deq.empty()) ,並且是在 lvlEnd 出佇列時,再次重新 lvlEnd,指向下一層的最後節點
層序遍歷只適合輸出資料的任務,由於其很難記錄結點的父結點,所以不太適合重建樹、構造樹的任務。例如不適合617 合併二叉樹。

層序遍歷模板(帶每層最後結點的標誌):

// 開始時lvlEnd指向 root
TreeNode* p, *lvlEnd = root;
deque<TreeNode*> deq;
deq.push_back(root);
while (!deq.empty()) {
    p = deq.front();
    deq.pop_front();
    if (p->left)
        deq.push_back(p->left);
    if (p->right)
        deq.push_back(p->right);
    if (p == lvlEnd) {
        lvlEnd = deq.back();
    }
}