1. 程式人生 > 實用技巧 >二叉樹常見演算法總結和C++實現

二叉樹常見演算法總結和C++實現

二叉樹

知識點

前序遍歷:先訪問根節點,再前序遍歷左子樹,然後前序遍歷右子樹

中序遍歷:先中序遍歷左子樹,再訪問根節點,然後中序遍歷右子樹

後序遍歷:先後續遍歷左子樹,再後續遍歷右子樹,然後訪問根節點
注意:

  • 以根節點訪問順序決定什麼遍歷
  • 左子樹都是優於右子樹

前序遍歷

struct TreeNode
{
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

void preorderTraversal(TreeNode* root)
{
    if (root == NULL)
        return;
    cout << root->val << " ";
    preorderTraversal(root->left);
    preorderTraversal(root->right);
}

前序非遞迴

vector<int> preorderTraversal(TreeNode* root)
{
    vector<int> result;
    if (root == NULL)
        return result;
    stack<TreeNode*> s;
    while (root != NULL || !s.empty()) 
    {
        while (root != NULL)
        {
            result.push_back(root->val);
            s.push(root);
            root = root->left;
        }

        TreeNode* node = s.top();
        s.pop();
        root = node->right;
    }
    return result;
}

中序非遞迴

vector<int> inorderTraversal(TreeNode* root)
{
    vector<int> result;
    if (root == NULL) 
        return result;
    stack<TreeNode*> s;
    while (root != NULL || !s.empty())
    {
        while (root != NULL)
        {
            s.push(root);
            root = root->left;
        }
        TreeNode* node = s.top();
        s.pop();
        result.push_back(node->val);
        root = node->right;
    }
    return result;
}

後續非遞迴

核心:根節點必須在右節點彈出之後,再彈出

vector<int> postorderTraversal(TreeNode* root)
{
    vector<int> result;
    if (root == NULL)
        return result;
    stack<TreeNode*> s;
    TreeNode* lastVisit;
    while (root != NULL || !s.empty())
    {
        while (root != NULL)
        {
            s.push(root);
            root = root->left;
        }
        // 這裡先看看,不彈出
        TreeNode* node = s.top();
        // 根節點必須在右節點彈出之後,再彈出
        if (node->right == NULL || node->right == lastVisit)
        {
            s.pop();
            result.push_back(node->val);
            lastVisit = node;
        } 
        else
            root = node->right;
    }
    return result;
}

DFS深度搜索-從上到下

// 深度優先
void dfs(TreeNode* root, vector<int>& result)
{
    if (root == NULL)
        return;
    result.push_back(root->val);
    dfs(root->left, result);
    dfs(root->right, result);
}

vector<int> preorderTraversal(TreeNode* root)
{
    vector<int> result;
    dfs(root, result);
    return result;
}

DFS深度搜索-從下到上(分治法)

vector<int> divideAndConquer(TreeNode* root)
{
    vector<int> result;
    if (root == NULL)
        return result;

    // 分治
    vector<int> left = divideAndConquer(root->left);
    vector<int> right = divideAndConquer(root->right);
    // 合併結果
    result.push_back(root->val);
    result.insert(result.end(), left.begin(), left.end()); 
    result.insert(result.end(), right.begin(), right.end()); 
    return result;
}

vector<int> preorderTraversal(TreeNode* root)
{
    return divideAndConquer(root);
}

注意點:
DFS深度搜索(從上到下)和分治法區別:前者一般將最終結果通過引用引數傳入,或者一般遞迴返回結果最終合併

BFS層次遍歷

vector<int> levelTraversal(TreeNode* root)
{
    vector<int> result;
    queue<TreeNode*> q;
    q.push(root);
    while (!q.empty())
    {
        TreeNode *node = q.front();
        q.pop();
        result.push_back(node->val);
        if (node->left != NULL)
            q.push(node->left);
        if (node->right != NULL)
            q.push(node->right);
    }
    return result;
}

分治法應用

先分別處理區域性,再合併結果
適用場景

  • 快速排序
  • 歸併排序
  • 二叉樹相關問題

常見題目示例

104. 二叉樹的最大深度

給定一個二叉樹,找出其最大深度

class Solution {
public:
    int maxDepth(TreeNode* root) {
        if (root == NULL)
            return 0;
        int left = maxDepth(root->left);
        int right = maxDepth(root->right);
        return (left > right ? left : right) + 1; 
    }
};

110. 平衡二叉樹

給定一個二叉樹,判斷它是否是高度平衡的二叉樹

思路:分治法,左邊平衡&&右邊平衡&&左右高度差<= 1,因為需要返回是否平衡及高度,要麼返回兩個資料,要麼合併兩個資料,所以用-1表示不平衡,>=0表示數高度(二義性:一個變數有兩個含義)

class Solution {
public:
    bool isBalanced(TreeNode* root) {
        return (maxDepth(root) == -1) ? false : true;
    }
    int maxDepth(TreeNode* root) {
        if (root == NULL)
            return 0;
        int left = maxDepth(root->left);
        int right = maxDepth(root->right);
        // 為什麼返回-1呢?(變數具有二義性)
        if (left == -1 || right == -1 || abs(left - right) > 1)
            return -1;
        return (left > right ? left : right) + 1;
    }
};

注意
一般工程中,結果通過兩個變數來返回,不建議用一個變量表示兩種含義

124. 二叉樹中的最大路徑和

給定一個非空二叉樹,返回其最大路徑和

思路:分治法,分三種情況:左子樹最大路徑和,右子樹最大路徑和,左右子樹最大加根節點

class Solution {
public:
    int result = INT_MIN;

    int maxPathSum(TreeNode* root) {
        helper(root);
        return result;
    }

    int helper(TreeNode* root)
    {
        if (root == NULL)
            return 0;

        int left = max(helper(root->left), 0);
        int right = max(helper(root->right), 0);

        // 求的過程中考慮包含當前根節點的最大路徑
        result = max(result, root->val + left + right);
        // 只返回包含當前根節點和左子樹或者右子樹的路徑,返回上一層和result比較
        return root->val + max(left, right);
    }
};

236. 二叉樹的最近公共祖先

給定一個二叉樹, 找到該樹中兩個指定節點的最近公共祖先

思路:
兩個節點 p,q 分為兩種情況:

  • p 和 q 在相同子樹中

  • p 和 q 在不同子樹中
    從根節點遍歷,遞歸向左右子樹查詢節點資訊
    遞迴終止條件:如果當前節點為空或等於 p 或 q,則返回當前節點

  • 遞迴遍歷左右子樹,如果左右子樹查到節點都不為空,則表明 p 和 q 分別在左右子樹中,因此,當前節點即為最近公共祖先;

  • 如果左右子樹其中一個不為空,則返回非空節點。

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == NULL || p == root || q == root)
            return root;
        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);
        if (left != NULL && right != NULL)
            return root;
        return left != NULL ? left : right;
    }
};

102. 二叉樹的層序遍歷

給你一個二叉樹,請你返回其按 層序遍歷 得到的節點值。 (即逐層地,從左到右訪問所有節點)

思路:用一個佇列記錄一層的元素,然後掃描這一層元素並新增下一層元素到佇列

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> result;
        if (root == NULL)
            return result;

        queue<TreeNode*> q;
        q.push(root);
        while (!q.empty())
        {
            vector<int> level;
            // 記錄當前層有多少元素(遍歷當前層,再新增下一層)
            int size = q.size();
            for (int i = 0; i < size; i++)
            {
                TreeNode* node = q.front();
                q.pop();
                level.push_back(node->val);
                if (node->left) 
                    q.push(node->left);
                if (node->right)
                    q.push(node->right);
            }
            result.push_back(level);
        }
        return result;
    }
};

107. 二叉樹的層次遍歷 II

給定一個二叉樹,返回其節點值自底向上的層次遍歷。 (即按從葉子節點所在層到根節點所在的層,逐層從左向右遍歷)

思路:在層級遍歷的基礎上,翻轉一下結果即可

class Solution {
public:
    vector<vector<int>> levelOrderBottom(TreeNode* root) {
        vector<vector<int>> result;
        if (root == NULL)
            return result;

        queue<TreeNode*> q;
        q.push(root);
        while (!q.empty())
        {
            vector<int> level;
            // 記錄當前層有多少元素(遍歷當前層,再新增下一層)
            int size = q.size();
            for (int i = 0; i < size; i++)
            {
                TreeNode* node = q.front();
                q.pop();
                level.push_back(node->val);
                if (node->left) 
                    q.push(node->left);
                if (node->right)
                    q.push(node->right);
            }
            result.push_back(level);
        }
        // 翻轉
        reverse(result.begin(), result.end());
        return result;
    }
};

103. 二叉樹的鋸齒形層次遍歷

給定一個二叉樹,返回其節點值的鋸齒形層次遍歷。(即先從左往右,再從右往左進行下一層遍歷,以此類推,層與層之間交替進行)

class Solution {
public:
    vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
        vector<vector<int>> result;
        if (root == NULL)
            return result;
        queue<TreeNode*> q;
        q.push(root);
        bool isReverse = false;
        while (!q.empty())
        {
            vector<int> level;
            int size = q.size();
            for (int i = 0; i < size; i++)
            {
                TreeNode* node = q.front();
                q.pop();
                level.push_back(node->val);
                if (node->left)
                    q.push(node->left);
                if (node->right)
                    q.push(node->right);
            }
            if (isReverse)
                reverse(level.begin(), level.end());

            result.push_back(level);
            isReverse = !isReverse;  // 是否轉置交替進行
        }
        return result;
    }
};

98. 驗證二叉搜尋樹

給定一個二叉樹,判斷其是否是一個有效的二叉搜尋樹。
假設一個二叉搜尋樹具有如下特徵:

  • 節點的左子樹只包含小於當前節點的數。
  • 節點的右子樹只包含大於當前節點的數。
  • 所有左子樹和右子樹自身必須也是二叉搜尋樹。
class Solution {
public:
    // 中序遍歷思想
    bool isValidBST(TreeNode* root) {
        if (root == NULL)
            return true;

        if (!isValidBST(root->left)) 
            return false;
            
        if (prev != NULL && root->val <= prev->val)
            return false;

        prev = root;

        return isValidBST(root->right);
    }
    TreeNode* prev{NULL};
};

701. 二叉搜尋樹中的插入操作

給定二叉搜尋樹(BST)的根節點和要插入樹中的值,將值插入二叉搜尋樹。 返回插入後二叉搜尋樹的根節點。 保證原始二叉搜尋樹中不存在新值。
注意,可能存在多種有效的插入方式,只要樹在插入後仍保持為二叉搜尋樹即可。 你可以返回任意有效的結果。
思路:找到最後一個葉子節點滿足插入條件即可

class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        if (root == NULL)
        {
            root = new TreeNode(val);
            return root;
        }

        if (val < root->val)
            root->left = insertIntoBST(root->left, val);
        else
            root->right = insertIntoBST(root->right, val);
            
        return root;
    }
};

總結

  • 掌握二叉樹遞迴與非遞迴遍歷
  • 理解DFS前序遍歷與分治法
  • 理解BFS層次遍歷

參考:https://github.com/greyireland/algorithm-pattern