6.20-合併二叉樹

七龙猪發表於2024-06-20

617.合併二叉樹

題意描述:

給你兩棵二叉樹: root1root2

想象一下,當你將其中一棵覆蓋到另一棵之上時,兩棵樹上的一些節點將會重疊(而另一些不會)。你需要將這兩棵樹合併成一棵新二叉樹。合併的規則是:如果兩個節點重疊,那麼將這兩個節點的值相加作為合併後節點的新值;否則,不為 null 的節點將直接作為新二叉樹的節點。

返回合併後的二叉樹。

注意: 合併過程必須從兩個樹的根節點開始。

示例 1:

img

輸入:root1 = [1,3,2,5], root2 = [2,1,3,null,4,null,7]
輸出:[3,4,5,5,4,null,7]

示例 2:

輸入:root1 = [1], root2 = [1,2]
輸出:[2,2]

提示:

  • 兩棵樹中的節點數目在範圍 [0, 2000]
  • -104 <= Node.val <= 104

思路:

相信這道題目很多同學疑惑的點是如何同時遍歷兩個二叉樹呢?

其實和遍歷一個樹邏輯是一樣的,只不過傳入兩個樹的節點,同時操作。

遞迴

二叉樹使用遞迴,就要想使用前中後哪種遍歷方式?

本題使用哪種遍歷都是可以的!

我們下面以前序遍歷為例。

動畫如下:

617.合併二叉樹

那麼我們來按照遞迴三部曲來解決:

  1. 確定遞迴函式的引數和返回值:

首先要合入兩個二叉樹,那麼引數至少是要傳入兩個二叉樹的根節點,返回值就是合併之後二叉樹的根節點。

程式碼如下:

TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
  1. 確定終止條件:

因為是傳入了兩個樹,那麼就有兩個樹遍歷的節點t1 t2,如果t1 == NULL 了,兩個樹合併就應該是 t2 了(如果t2也為NULL也無所謂,合併之後就是NULL)。

反過來如果t2 == NULL,那麼兩個數合併就是t1(如果t1也為NULL也無所謂,合併之後就是NULL)。

程式碼如下:

if (t1 == NULL) return t2; // 如果t1為空,合併之後就應該是t2
if (t2 == NULL) return t1; // 如果t2為空,合併之後就應該是t1
  1. 確定單層遞迴的邏輯:

單層遞迴的邏輯就比較好寫了,這裡我們重複利用一下t1這個樹,t1就是合併之後樹的根節點(就是修改了原來樹的結構)。

那麼單層遞迴中,就要把兩棵樹的元素加到一起。

t1->val += t2->val;

接下來t1 的左子樹是:合併 t1左子樹 t2左子樹之後的左子樹。

t1 的右子樹:是 合併 t1右子樹 t2右子樹之後的右子樹。

最終t1就是合併之後的根節點。

程式碼如下:

t1->left = mergeTrees(t1->left, t2->left);
t1->right = mergeTrees(t1->right, t2->right);
return t1;

此時前序遍歷,完整程式碼就寫出來了,如下:

class Solution {
public:
    TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
        if (t1 == NULL) return t2; // 如果t1為空,合併之後就應該是t2
        if (t2 == NULL) return t1; // 如果t2為空,合併之後就應該是t1
        // 修改了t1的數值和結構
        t1->val += t2->val;                             // 中
        t1->left = mergeTrees(t1->left, t2->left);      // 左
        t1->right = mergeTrees(t1->right, t2->right);   // 右
        return t1;
    }
};

那麼中序遍歷也是可以的,程式碼如下:

class Solution {
public:
    TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
        if (t1 == NULL) return t2; // 如果t1為空,合併之後就應該是t2
        if (t2 == NULL) return t1; // 如果t2為空,合併之後就應該是t1
        // 修改了t1的數值和結構
        t1->left = mergeTrees(t1->left, t2->left);      // 左
        t1->val += t2->val;                             // 中
        t1->right = mergeTrees(t1->right, t2->right);   // 右
        return t1;
    }
};

後序遍歷依然可以,程式碼如下:

class Solution {
public:
    TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
        if (t1 == NULL) return t2; // 如果t1為空,合併之後就應該是t2
        if (t2 == NULL) return t1; // 如果t2為空,合併之後就應該是t1
        // 修改了t1的數值和結構
        t1->left = mergeTrees(t1->left, t2->left);      // 左
        t1->right = mergeTrees(t1->right, t2->right);   // 右
        t1->val += t2->val;                             // 中
        return t1;
    }
};

但是前序遍歷是最好理解的,我建議大家用前序遍歷來做就OK。

如上的方法修改了t1的結構,當然也可以不修改t1t2的結構,重新定義一個樹。

不修改輸入樹的結構,前序遍歷,程式碼如下:

class Solution {
public:
    TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
        if (t1 == NULL) return t2;
        if (t2 == NULL) return t1;
        // 重新定義新的節點,不修改原有兩個樹的結構
        TreeNode* root = new TreeNode(0);
        root->val = t1->val + t2->val;
        root->left = mergeTrees(t1->left, t2->left);
        root->right = mergeTrees(t1->right, t2->right);
        return root;
    }
};

迭代法

使用迭代法,如何同時處理兩棵樹呢?

思路我們在二叉樹:我對稱麼? (opens new window)中的迭代法已經講過一次了,求二叉樹對稱的時候就是把兩個樹的節點同時加入佇列進行比較。

本題我們也使用佇列,模擬的層序遍歷,程式碼如下:

class Solution {
public:
    TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
        if (t1 == NULL) return t2;
        if (t2 == NULL) return t1;
        queue<TreeNode*> que;
        que.push(t1);
        que.push(t2);
        while(!que.empty()) {
            TreeNode* node1 = que.front(); que.pop();
            TreeNode* node2 = que.front(); que.pop();
            // 此時兩個節點一定不為空,val相加
            node1->val += node2->val;

            // 如果兩棵樹左節點都不為空,加入佇列
            if (node1->left != NULL && node2->left != NULL) {
                que.push(node1->left);
                que.push(node2->left);
            }
            // 如果兩棵樹右節點都不為空,加入佇列
            if (node1->right != NULL && node2->right != NULL) {
                que.push(node1->right);
                que.push(node2->right);
            }

            // 當t1的左節點 為空 t2左節點不為空,就賦值過去
            if (node1->left == NULL && node2->left != NULL) {
                node1->left = node2->left;
            }
            // 當t1的右節點 為空 t2右節點不為空,就賦值過去
            if (node1->right == NULL && node2->right != NULL) {
                node1->right = node2->right;
            }
        }
      //因為最後返回t1,所以t1非空,t2空這種情況不用管,直接返回t1即可
        return t1;
    }
};

擴充

當然也可以秀一波指標的操作,這是我寫的野路子,大家就隨便看看就行了,以防帶跑偏了。

如下程式碼中,想要更改二叉樹的值,應該傳入指向指標的指標。

程式碼如下:(前序遍歷)

class Solution {
public:
    void process(TreeNode** t1, TreeNode** t2) {
        if ((*t1) == NULL && (*t2) == NULL) return;
        if ((*t1) != NULL && (*t2) != NULL) {
            (*t1)->val += (*t2)->val;
        }
        if ((*t1) == NULL && (*t2) != NULL) {
            *t1 = *t2;
            return;
        }
        if ((*t1) != NULL && (*t2) == NULL) {
            return;
        }
        process(&((*t1)->left), &((*t2)->left));
        process(&((*t1)->right), &((*t2)->right));
    }
    TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
        process(&t1, &t2);
        return t1;
    }
};

總結

合併二叉樹,也是二叉樹操作的經典題目,如果沒有接觸過的話,其實並不簡單,因為我們習慣了操作一個二叉樹,一起操作兩個二叉樹,還會有點懵懵的。

這不是我們第一次操作兩棵二叉樹了,在二叉樹:我對稱麼? (opens new window)中也一起操作了兩棵二叉樹。

迭代法中,一般一起操作兩個樹都是使用佇列模擬類似層序遍歷,同時處理兩個樹的節點,這種方式最好理解,如果用模擬遞迴的思路的話,要複雜一些。

最後擴充中,我給了一個操作指標的野路子,大家隨便看看就行了,如果學習C++的話,可以再去研究研究。


700.二叉搜尋樹中的搜尋

題意描述:

給定二叉搜尋樹(BST)的根節點 root 和一個整數值 val

你需要在 BST 中找到節點值等於 val 的節點。 返回以該節點為根的子樹。 如果節點不存在,則返回 null

示例 1:

img

輸入:root = [4,2,7,1,3], val = 2
輸出:[2,1,3]

示例 2:

img

輸入:root = [4,2,7,1,3], val = 5
輸出:[]

提示:

  • 樹中節點數在 [1, 5000] 範圍內
  • 1 <= Node.val <= 107
  • root 是二叉搜尋樹
  • 1 <= val <= 107

思路:

二叉搜尋樹是一個有序樹:

  • 若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;
  • 若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;
  • 它的左、右子樹也分別為二叉搜尋樹

這就決定了,二叉搜尋樹,遞迴遍歷和迭代遍歷和普通二叉樹都不一樣。

本題,其實就是在二叉搜尋樹中搜尋一個節點。那麼我們來看看應該如何遍歷。

遞迴法

  1. 確定遞迴函式的引數和返回值

遞迴函式的引數傳入的就是根節點和要搜尋的數值,返回的就是以這個搜尋數值所在的節點。

程式碼如下:

TreeNode* searchBST(TreeNode* root, int val)
  1. 確定終止條件

如果root為空,或者找到這個數值了,就返回root節點。

if (root == NULL || root->val == val) return root;
  1. 確定單層遞迴的邏輯

看看二叉搜尋樹的單層遞迴邏輯有何不同。

因為二叉搜尋樹的節點是有序的,所以可以有方向的去搜尋

如果root->val > val,搜尋左子樹,如果root->val < val,就搜尋右子樹,最後如果都沒有搜尋到,就返回NULL。

程式碼如下:

TreeNode* result = NULL;
if (root->val > val) result = searchBST(root->left, val);
if (root->val < val) result = searchBST(root->right, val);
return result;

很多錄友寫遞迴函式的時候 習慣直接寫 searchBST(root->left, val),卻忘了 遞迴函式還有返回值。

遞迴函式的返回值是什麼? 是 左子樹如果搜尋到了val,要將該節點返回如果不用一個變數將其接住,那麼返回值不就沒了。

所以要 result = searchBST(root->left, val)

整體程式碼如下:

class Solution {
public:
    TreeNode* searchBST(TreeNode* root, int val) {
        if (root == NULL || root->val == val) return root;
        TreeNode* result = NULL;
        if (root->val > val) result = searchBST(root->left, val);
        if (root->val < val) result = searchBST(root->right, val);
        return result;
    }
};

或者我們也可以這麼寫

class Solution {
public:
    TreeNode* searchBST(TreeNode* root, int val) {
        if (root == NULL || root->val == val) return root;
        if (root->val > val) return searchBST(root->left, val);
        if (root->val < val) return searchBST(root->right, val);
        return NULL;
    }
};

迭代法

一提到二叉樹遍歷的迭代法,可能立刻想起使用來模擬深度遍歷,使用佇列來模擬廣度遍歷。

對於二叉搜尋樹可就不一樣了,因為二叉搜尋樹的特殊性,也就是節點的有序性,可以不使用輔助棧或者佇列就可以寫出迭代法。

對於一般二叉樹,遞迴過程中還有回溯的過程,例如走一個左方向的分支走到頭了,那麼要調頭,在走右分支。

對於二叉搜尋樹,不需要回溯的過程,因為節點的有序性就幫我們確定了搜尋的方向。

例如要搜尋元素為3的節點,我們不需要搜尋其他節點,也不需要做回溯,查詢的路徑已經規劃好了。

中間節點如果大於3就向左走,如果小於3就向右走,如圖:

二叉搜尋樹

所以迭代法程式碼如下:

class Solution {
public:
    TreeNode* searchBST(TreeNode* root, int val) {
        while (root != NULL) {
            if (root->val > val) root = root->left;
            else if (root->val < val) root = root->right;
            else return root;
        }
        return NULL;
    }
};

第一次看到了如此簡單的迭代法,是不是感動的痛哭流涕,哭一會~

總結

本篇我們介紹了二叉搜尋樹的遍歷方式,因為二叉搜尋樹的有序性,遍歷的時候要比普通二叉樹簡單很多。

但是一些同學很容易忽略二叉搜尋樹的特性,所以寫出遍歷的程式碼就未必真的簡單了。

所以針對二叉搜尋樹的題目,一樣要利用其特性。

文中我依然給出遞迴和迭代兩種方式,可以看出寫法都非常簡單,就是利用了二叉搜尋樹有序的特點。


98.驗證二叉搜尋樹

題意描述:

給你一個二叉樹的根節點 root ,判斷其是否是一個有效的二叉搜尋樹。

有效 二叉搜尋樹定義如下:

  • 節點的左子樹只包含小於 當前節點的數。
  • 節點的右子樹只包含 大於 當前節點的數。
  • 所有左子樹和右子樹自身必須也是二叉搜尋樹。

示例 1:

img

輸入:root = [2,1,3]
輸出:true

示例 2:

img

輸入:root = [5,1,4,null,null,3,6]
輸出:false
解釋:根節點的值是 5 ,但是右子節點的值是 4 。

提示:

  • 樹中節點數目範圍在[1, 104]
  • -231 <= Node.val <= 231 - 1

思路:

要知道中序遍歷下,輸出的二叉搜尋樹節點的數值是有序序列。

有了這個特性,驗證二叉搜尋樹,就相當於變成了判斷一個序列是不是遞增的了。

遞迴法

可以遞迴中序遍歷將二叉搜尋樹轉變成一個陣列,程式碼如下:

vector<int> vec;
void traversal(TreeNode* root) {
    if (root == NULL) return;
    traversal(root->left);
    vec.push_back(root->val); // 將二叉搜尋樹轉換為有序陣列
    traversal(root->right);
}

然後只要比較一下,這個陣列是否是有序的,注意二叉搜尋樹中不能有重複元素

traversal(root);
for (int i = 1; i < vec.size(); i++) {
    // 注意要小於等於,搜尋樹裡不能有相同元素
    if (vec[i] <= vec[i - 1]) return false;
}
return true;

整體程式碼如下:

class Solution {
private:
    vector<int> vec;
    void traversal(TreeNode* root) {
        if (root == NULL) return;
        traversal(root->left);
        vec.push_back(root->val); // 將二叉搜尋樹轉換為有序陣列
        traversal(root->right);
    }
public:
    bool isValidBST(TreeNode* root) {
        vec.clear(); // 不加這句在leetcode上也可以過,但最好加上
        traversal(root);
        for (int i = 1; i < vec.size(); i++) {
            // 注意要小於等於,搜尋樹裡不能有相同元素
            if (vec[i] <= vec[i - 1]) return false;
        }
        return true;
    }
};

以上程式碼中,我們把二叉樹轉變為陣列來判斷,是最直觀的,但其實不用轉變成陣列,可以在遞迴遍歷的過程中直接判斷是否有序。

這道題目比較容易陷入兩個陷阱:

  • 陷阱1

不能單純的比較左節點小於中間節點,右節點大於中間節點就完事了

寫出了類似這樣的程式碼:

if (root->val > root->left->val && root->val < root->right->val) {
    return true;
} else {
    return false;
}

我們要比較的是 左子樹所有節點小於中間節點,右子樹所有節點大於中間節點。所以以上程式碼的判斷邏輯是錯誤的。

例如: [10,5,15,null,null,6,20] 這個case:

二叉搜尋樹

節點10大於左節點5,小於右節點15,但右子樹裡出現了一個6 這就不符合了!

  • 陷阱2

樣例中最小節點 可能是int的最小值(- 2 ^ 31),如果這樣使用最小的int來比較也是不行的。

此時可以初始化比較元素為longlong的最小值。

問題可以進一步演進:如果樣例中根節點的val 可能是longlong的最小值 又要怎麼辦呢?

瞭解這些陷阱之後我們來看一下程式碼應該怎麼寫:

遞迴三部曲:

  1. 確定遞迴函式,返回值以及引數

要定義一個longlong的全域性變數,用來比較遍歷的節點是否有序,因為後臺測試資料中有int最小值,所以定義為longlong的型別,初始化為longlong最小值。

注意遞迴函式要有bool型別的返回值, 我們在二叉樹:遞迴函式究竟什麼時候需要返回值,什麼時候不要返回值? (opens new window)中講了,只有尋找某一條邊(或者一個節點)的時候,遞迴函式會有bool型別的返回值。

其實本題是同樣的道理,我們在尋找一個不符合條件的節點,如果沒有找到這個節點就遍歷了整個樹,如果找到不符合的節點了,立刻返回。

程式碼如下:

long long maxVal = LONG_MIN; // 因為後臺測試資料中有int最小值
bool isValidBST(TreeNode* root)
  1. 確定終止條件

如果是空節點 是不是二叉搜尋樹呢?

是的,二叉搜尋樹也可以為空!

程式碼如下:

if (root == NULL) return true;
  1. 確定單層遞迴的邏輯

中序遍歷,一直更新maxVal,一旦發現maxVal >= root->val,就返回false,注意元素相同時候也要返回false

程式碼如下:

bool left = isValidBST(root->left);         // 左

// 中序遍歷,驗證遍歷的元素是不是從小到大
if (maxVal < root->val) maxVal = root->val; // 中
else return false;

bool right = isValidBST(root->right);       // 右
return left && right;

整體程式碼如下:

class Solution {
public:
    long long maxVal = LONG_MIN; // 因為後臺測試資料中有int最小值
    bool isValidBST(TreeNode* root) {
        if (root == NULL) return true;

        bool left = isValidBST(root->left);
        // 中序遍歷,驗證遍歷的元素是不是從小到大
        if (maxVal < root->val) maxVal = root->val;
        else return false;
        bool right = isValidBST(root->right);

        return left && right;
    }
};

以上程式碼是因為後臺資料有int最小值測試用例,所以都把maxVal改成了longlong最小值。

如果測試資料中有 longlong的最小值,怎麼辦?

不可能在初始化一個更小的值了吧。 建議避免 初始化最小值,如下方法取到最左面節點的數值來比較。

程式碼如下:

class Solution {
public:
    TreeNode* pre = NULL; // 用來記錄前一個節點
    bool isValidBST(TreeNode* root) {
        if (root == NULL) return true;
        bool left = isValidBST(root->left);

        if (pre != NULL && pre->val >= root->val) return false;
        pre = root; // 記錄前一個節點

        bool right = isValidBST(root->right);
        return left && right;
    }
};

最後這份程式碼看上去整潔一些,思路也清晰。

迭代法

迭代法中序遍歷稍加改動就可以了,程式碼如下:

class Solution {
public:
    bool isValidBST(TreeNode* root) {
        stack<TreeNode*> st;
        TreeNode* cur = root;
        TreeNode* pre = NULL; // 記錄前一個節點
        while (cur != NULL || !st.empty()) {
            if (cur != NULL) {
                st.push(cur);
                cur = cur->left;                // 左
            } else {
                cur = st.top();                 // 中
                st.pop();
                if (pre != NULL && cur->val <= pre->val)
                return false;
                pre = cur; //儲存前一個訪問的結點

                cur = cur->right;               // 右
            }
        }
        return true;
    }
};

這題不能用queue作為容器,因為遞迴搜尋的pushpop是對上一層處理,que不行

總結

這道題目是一個簡單題,但對於沒接觸過的同學還是有難度的。

所以初學者剛開始學習演算法的時候,看到簡單題目沒有思路很正常,千萬別懷疑自己智商,學習過程都是這樣的,大家智商都差不多。

只要把基本型別的題目都做過,總結過之後,思路自然就開闊了,加油💪


530.二叉搜尋樹的最小絕對差

題意描述:

給你一個二叉搜尋樹的根節點 root ,返回 樹中任意兩不同節點值之間的最小差值

差值是一個正數,其數值等於兩值之差的絕對值。

示例 1:

img

輸入:root = [4,2,6,1,3]
輸出:1

示例 2:

img

輸入:root = [1,0,48,null,null,12,49]
輸出:1

提示:

  • 樹中節點的數目範圍是 [2, 104]
  • 0 <= Node.val <= 105

注意:本題與 783 https://leetcode-cn.com/problems/minimum-distance-between-bst-nodes/ 相同

思路:

題目中要求在二叉搜尋樹上任意兩節點的差的絕對值的最小值。

注意是二叉搜尋樹,二叉搜尋樹可是有序的。

遇到在二叉搜尋樹上求什麼最值啊,差值之類的,就把它想成在一個有序陣列上求最值,求差值,這樣就簡單多了。

遞迴

那麼二叉搜尋樹採用中序遍歷,其實就是一個有序陣列。

在一個有序陣列上求兩個數最小差值,這是不是就是一道送分題了。

最直觀的想法,就是把二叉搜尋樹轉換成有序陣列,然後遍歷一遍陣列,就統計出來最小差值了。

程式碼如下:

class Solution {
private:
vector<int> vec;
void traversal(TreeNode* root) {
    if (root == NULL) return;
    traversal(root->left);
    vec.push_back(root->val); // 將二叉搜尋樹轉換為有序陣列
    traversal(root->right);
}
public:
    int getMinimumDifference(TreeNode* root) {
        vec.clear();
        traversal(root);
        if (vec.size() < 2) return 0;
        int result = INT_MAX;
        for (int i = 1; i < vec.size(); i++) { // 統計有序陣列的最小差值
            result = min(result, vec[i] - vec[i-1]);
        }
        return result;
    }
};

以上程式碼是把二叉搜尋樹轉化為有序陣列了,其實在二叉搜素樹中序遍歷的過程中,我們就可以直接計算了。

雙指標法:

需要用一個pre節點記錄一下cur節點的前一個節點。如圖:

530.二叉搜尋樹的最小絕對差

一些同學不知道在遞迴中如何記錄前一個節點的指標,其實實現起來是很簡單的,大家只要看過一次,寫過一次,就掌握了。

程式碼如下:

class Solution {
private:
int result = INT_MAX;
TreeNode* pre = NULL;
void traversal(TreeNode* cur) {
    if (cur == NULL) return;
    traversal(cur->left);   // 左
    if (pre != NULL){       // 中
        result = min(result, cur->val - pre->val);
    }
    pre = cur; // 記錄前一個
    traversal(cur->right);  // 右
}
public:
    int getMinimumDifference(TreeNode* root) {
        traversal(root);
        return result;
    }
};

是不是看上去也並不複雜!

迭代

下面我給出其中的一種中序遍歷的迭代法,程式碼如下:

class Solution {
public:
    int getMinimumDifference(TreeNode* root) {
        stack<TreeNode*> st;
        TreeNode* cur = root;
        TreeNode* pre = NULL;
        int result = INT_MAX;
        while (cur != NULL || !st.empty()) {
            if (cur != NULL) { // 指標來訪問節點,訪問到最底層
                st.push(cur); // 將訪問的節點放進棧
                cur = cur->left;                // 左
            } else {
                cur = st.top();
                st.pop();
                if (pre != NULL) {              // 中
                    result = min(result, cur->val - pre->val);
                }
                pre = cur;
                cur = cur->right;               // 右
            }
        }
        return result;
    }
};

總結

遇到在二叉搜尋樹上求什麼最值,求差值之類的,都要思考一下二叉搜尋樹可是有序的,要利用好這一特點。

同時要學會在遞迴遍歷的過程中如何記錄前後兩個指標,這也是一個小技巧,學會了還是很受用的。

後面我將繼續介紹一系列利用二叉搜尋樹特性的題目。


501.二叉搜尋樹中的眾數

題意描述:

給你一個含重複值的二叉搜尋樹(BST)的根節點 root ,找出並返回 BST 中的所有 眾數(即出現頻率最高的元素)。

如果樹中有不止一個眾數,可以按 任意順序 返回。

假定 BST 滿足如下定義:

  • 結點左子樹中所含節點的值 小於等於 當前節點的值
  • 結點右子樹中所含節點的值 大於等於 當前節點的值
  • 左子樹和右子樹都是二叉搜尋樹

示例 1:

img

輸入:root = [1,null,2,2]
輸出:[2]

示例 2:

輸入:root = [0]
輸出:[0]

提示:

  • 樹中節點的數目在範圍 [1, 104]
  • -105 <= Node.val <= 105

進階:你可以不使用額外的空間嗎?(假設由遞迴產生的隱式呼叫棧的開銷不被計算在內)

思路:

這道題目呢,遞迴法我從兩個維度來講。

首先如果不是二叉搜尋樹的話,應該怎麼解題;

是二叉搜尋樹,又應該如何解題,兩種方式做一個比較,可以加深大家對二叉樹的理解。

遞迴法

如果不是二叉搜尋樹

如果不是二叉搜尋樹,最直觀的方法一定是把這個樹都遍歷了,用map統計頻率,把頻率排個序,最後取前面高頻的元素的集合。

具體步驟如下:

  1. 這個樹都遍歷了,用map統計頻率

至於用前中後序哪種遍歷也不重要,因為就是要全遍歷一遍,怎麼個遍歷法都行,層序遍歷都沒毛病!

這裡採用前序遍歷,程式碼如下:

// map<int, int> key:元素,value:出現頻率
void searchBST(TreeNode* cur, unordered_map<int, int>& map) { // 前序遍歷
    if (cur == NULL) return ;
    map[cur->val]++; // 統計元素頻率
    searchBST(cur->left, map);
    searchBST(cur->right, map);
    return ;
}
  1. 把統計的出來的出現頻率(即map中的value)排個序

有的同學可能可以想直接對map中的value排序,還真做不到,C++中如果使用std::map或者std::multimap可以對key排序,但不能對value排序。

所以要把map轉化陣列即vector,再進行排序,當然vector裡面放的也是pair<int, int>型別的資料,第一個int為元素,第二個int為出現頻率。

程式碼如下:

bool static cmp (const pair<int, int>& a, const pair<int, int>& b) {
    return a.second > b.second; // 按照頻率從大到小排序
}

vector<pair<int, int>> vec(map.begin(), map.end());
sort(vec.begin(), vec.end(), cmp); // 給頻率排個序
  1. 取前面高頻的元素

此時陣列vector中已經是存放著按照頻率排好序的pair,那麼把前面高頻的元素取出來就可以了。

程式碼如下:

result.push_back(vec[0].first);
for (int i = 1; i < vec.size(); i++) {
    // 取最高的放到result陣列中
    if (vec[i].second == vec[0].second) result.push_back(vec[i].first);
    else break;
}
return result;

整體C++程式碼如下:

class Solution {
private:

void searchBST(TreeNode* cur, unordered_map<int, int>& map) { // 前序遍歷
    if (cur == NULL) return ;
    map[cur->val]++; // 統計元素頻率
    searchBST(cur->left, map);
    searchBST(cur->right, map);
    return ;
}
bool static cmp (const pair<int, int>& a, const pair<int, int>& b) {
    return a.second > b.second;
}
public:
    vector<int> findMode(TreeNode* root) {
        unordered_map<int, int> map; // key:元素,value:出現頻率
        vector<int> result;
        if (root == NULL) return result;
        searchBST(root, map);
        vector<pair<int, int>> vec(map.begin(), map.end());
        sort(vec.begin(), vec.end(), cmp); // 給頻率排個序
        result.push_back(vec[0].first);
        for (int i = 1; i < vec.size(); i++) {
            // 取最高的放到result陣列中
            if (vec[i].second == vec[0].second) result.push_back(vec[i].first);
            else break;
        }
        return result;
    }
};

所以如果本題沒有說是二叉搜尋樹的話,那麼就按照上面的思路寫!

二叉搜尋樹中的眾數.html#是二叉搜尋樹)是二叉搜尋樹

既然是搜尋樹,它中序遍歷就是有序的

如圖:

501.二叉搜尋樹中的眾數1

中序遍歷程式碼如下:

void searchBST(TreeNode* cur) {
    if (cur == NULL) return ;
    searchBST(cur->left);       // 左
    (處理節點)                // 中
    searchBST(cur->right);      // 右
    return ;
}

遍歷有序陣列的元素出現頻率,從頭遍歷,那麼一定是相鄰兩個元素作比較,然後就把出現頻率最高的元素輸出就可以了。

關鍵是在有序陣列上的話,好搞,在樹上怎麼搞呢?

這就考察對樹的操作了。

二叉樹:搜尋樹的最小絕對差 (opens new window)中我們就使用了pre指標和cur指標的技巧,這次又用上了。

弄一個指標指向前一個節點,這樣每次cur(當前節點)才能和pre(前一個節點)作比較。

而且初始化的時候pre = NULL,這樣當preNULL時候,我們就知道這是比較的第一個元素。

程式碼如下:

if (pre == NULL) { // 第一個節點
    count = 1; // 頻率為1
} else if (pre->val == cur->val) { // 與前一個節點數值相同
    count++;
} else { // 與前一個節點數值不同
    count = 1;
}
pre = cur; // 更新上一個節點

此時又有問題了,因為要求最大頻率的元素集合(注意是集合,不是一個元素,可以有多個眾數),如果是陣列上大家一般怎麼辦?

應該是先遍歷一遍陣列,找出最大頻率(maxCount),然後再重新遍歷一遍陣列把出現頻率為maxCount的元素放進集合。(因為眾數有多個)

這種方式遍歷了兩遍陣列。

那麼我們遍歷兩遍二叉搜尋樹,把眾數集合算出來也是可以的。

但這裡其實只需要遍歷一次就可以找到所有的眾數。

那麼如何只遍歷一遍呢?

如果 頻率count 等於 maxCount(最大頻率),當然要把這個元素加入到結果集中(以下程式碼為result陣列),程式碼如下:

if (count == maxCount) { // 如果和最大值相同,放進result中
    result.push_back(cur->val);
}

是不是感覺這裡有問題,result怎麼能輕易就把元素放進去了呢,萬一,這個maxCount此時還不是真正最大頻率呢。

所以下面要做如下操作:

頻率count 大於 maxCount的時候,不僅要更新maxCount,而且要清空結果集(以下程式碼為result陣列),因為結果集之前的元素都失效了。

if (count > maxCount) { // 如果計數大於最大值
    maxCount = count;   // 更新最大頻率
    result.clear();     // 很關鍵的一步,不要忘記清空result,之前result裡的元素都失效了
    result.push_back(cur->val);
}

關鍵程式碼都講完了,完整程式碼如下:(只需要遍歷一遍二叉搜尋樹,就求出了眾數的集合

class Solution {
private:
    int maxCount = 0; // 最大頻率
    int count = 0; // 統計頻率
    TreeNode* pre = NULL;
    vector<int> result;
    void searchBST(TreeNode* cur) {
        if (cur == NULL) return ;

        searchBST(cur->left);       // 左
                                    // 中
        if (pre == NULL) { // 第一個節點
            count = 1;
        } else if (pre->val == cur->val) { // 與前一個節點數值相同
            count++;
        } else { // 與前一個節點數值不同
            count = 1;
        }
        pre = cur; // 更新上一個節點

        if (count == maxCount) { // 如果和最大值相同,放進result中
            result.push_back(cur->val);
        }

        if (count > maxCount) { // 如果計數大於最大值頻率
            maxCount = count;   // 更新最大頻率
            result.clear();     // 很關鍵的一步,不要忘記清空result,之前result裡的元素都失效了
            result.push_back(cur->val);
        }

        searchBST(cur->right);      // 右
        return ;
    }

public:
    vector<int> findMode(TreeNode* root) {
        count = 0;
        maxCount = 0;
        pre = NULL; // 記錄前一個節點
        result.clear();

        searchBST(root);
        return result;
    }
};

迭代法

只要把中序遍歷轉成迭代,中間節點的處理邏輯完全一樣。

下面我給出其中的一種中序遍歷的迭代法,其中間處理邏輯一點都沒有變(我從遞迴法直接粘過來的程式碼,連註釋都沒改)

程式碼如下:

class Solution {
public:
    vector<int> findMode(TreeNode* root) {
        stack<TreeNode*> st;
        TreeNode* cur = root;
        TreeNode* pre = NULL;
        int maxCount = 0; // 最大頻率
        int count = 0; // 統計頻率
        vector<int> result;
        while (cur != NULL || !st.empty()) {
            if (cur != NULL) { // 指標來訪問節點,訪問到最底層
                st.push(cur); // 將訪問的節點放進棧
                cur = cur->left;                // 左
            } else {
                cur = st.top();
                st.pop();                       // 中
                if (pre == NULL) { // 第一個節點
                    count = 1;
                } else if (pre->val == cur->val) { // 與前一個節點數值相同
                    count++;
                } else { // 與前一個節點數值不同
                    count = 1;
                }
                if (count == maxCount) { // 如果和最大值相同,放進result中
                    result.push_back(cur->val);
                }

                if (count > maxCount) { // 如果計數大於最大值頻率
                    maxCount = count;   // 更新最大頻率
                    result.clear();     // 很關鍵的一步,不要忘記清空result,之前result裡的元素都失效了
                    result.push_back(cur->val);
                }
                pre = cur;
                cur = cur->right;               // 右
            }
        }
        return result;
    }
};

總結

本題在遞迴法中,我給出瞭如果是普通二叉樹,應該怎麼求眾數。

知道了普通二叉樹的做法時候,我再進一步給出二叉搜尋樹又應該怎麼求眾數,這樣鮮明的對比,相信會對二叉樹又有更深層次的理解了。

在遞迴遍歷二叉搜尋樹的過程中,我還介紹了一個統計最高出現頻率元素集合的技巧, 要不然就要遍歷兩次二叉搜尋樹才能把這個最高出現頻率元素的集合求出來。

為什麼沒有這個技巧一定要遍歷兩次呢? 因為要求的是集合,會有多個眾數,如果規定只有一個眾數,那麼就遍歷一次穩穩的了。

最後我依然給出對應的迭代法,其實就是迭代法中序遍歷的模板加上遞迴法中中間節點的處理邏輯,分分鐘就可以寫出來,中間邏輯的程式碼我都是從遞迴法中直接粘過來的。

求二叉搜尋樹中的眾數其實是一道簡單題,但大家可以發現我寫了這麼一大篇幅的文章來講解,主要是為了儘量從各個角度對本題進剖析,幫助大家更快更深入理解二叉樹


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

題意描述:

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

最近公共祖先的定義為:“對於有根樹 T 的兩個節點 p、q,最近公共祖先表示為一個節點 x,滿足 x 是 p、q 的祖先且 x 的深度儘可能大(一個節點也可以是它自己的祖先)。”

示例 1:

img

輸入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
輸出:3
解釋:節點 5 和節點 1 的最近公共祖先是節點 3 。

示例 2:

img

輸入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
輸出:5
解釋:節點 5 和節點 4 的最近公共祖先是節點 5 。因為根據定義最近公共祖先節點可以為節點本身。

示例 3:

輸入:root = [1,2], p = 1, q = 2
輸出:1

提示:

  • 樹中節點數目在範圍 [2, 105] 內。
  • -109 <= Node.val <= 109
  • 所有 Node.val 互不相同
  • p != q
  • pq 均存在於給定的二叉樹中。

思路:

遇到這個題目首先想的是要是能自底向上查詢就好了,這樣就可以找到公共祖先了。

那麼二叉樹如何可以自底向上查詢呢?

回溯啊,二叉樹回溯的過程就是從底到上。

後序遍歷(左右中)就是天然的回溯過程,可以根據左右子樹的返回值,來處理中節點的邏輯。

接下來就看如何判斷一個節點是節點q和節點p的公共祖先呢。

首先最容易想到的一個情況:如果找到一個節點,發現左子樹出現結點p,右子樹出現節點q,或者 左子樹出現結點q,右子樹出現節點p,那麼該節點就是節點p和q的最近公共祖先。

即情況一:

img

判斷邏輯是 如果遞迴遍歷遇到q,就將q返回,遇到p 就將p返回,那麼如果 左右子樹的返回值都不為空,說明此時的中節點,一定是q 和p 的最近祖先。

那麼有錄友可能疑惑,會不會左子樹 遇到q 返回,右子樹也遇到q返回,這樣並沒有找到 q 和p的最近祖先。

這麼想的錄友,要審題了,題目強調:二叉樹節點數值是不重複的,而且一定存在 q 和 p

但是很多人容易忽略一個情況,就是節點本身p(q),它擁有一個子孫節點q(p)。

情況二:

img

其實情況一 和 情況二 程式碼實現過程都是一樣的,也可以說,實現情況一的邏輯,順便包含了情況二。

因為遇到 q 或者 p 就返回,這樣也包含了 q 或者 p 本身就是 公共祖先的情況。

這一點是很多錄友容易忽略的,在下面的程式碼講解中,可以再去體會。

遞迴三部曲:

  1. 確定遞迴函式返回值以及引數

需要遞迴函式返回值,來告訴我們是否找到節點q或者p,那麼返回值為bool型別就可以了。

但我們還要返回最近公共節點,可以利用上題目中返回值是TreeNode * ,那麼如果遇到p或者q,就把q或者p返回,返回值不為空,就說明找到了q或者p。

程式碼如下:

TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
  1. 確定終止條件

遇到空的話,因為樹都是空了,所以返回空。

那麼我們來說一說,如果 root == q,或者 root == p,說明找到 q p ,則將其返回,這個返回值,後面在中節點的處理過程中會用到,那麼中節點的處理邏輯,下面講解。

程式碼如下:

if (root == q || root == p || root == NULL) return root;
  1. 確定單層遞迴邏輯

值得注意的是本題函式有返回值,是因為回溯的過程需要遞迴函式的返回值做判斷,但本題我們依然要遍歷樹的所有節點。

我們在二叉樹:遞迴函式究竟什麼時候需要返回值,什麼時候不要返回值? (opens new window)中說了 遞迴函式有返回值就是要遍歷某一條邊,但有返回值也要看如何處理返回值!

如果遞迴函式有返回值,如何區分要搜尋一條邊,還是搜尋整個樹呢?

搜尋一條邊的寫法:

if (遞迴函式(root->left)) return ;

if (遞迴函式(root->right)) return ;

搜尋整個樹寫法:

left = 遞迴函式(root->left);  // 左
right = 遞迴函式(root->right); // 右
left與right的邏輯處理;         // 中 

看出區別了沒?

在遞迴函式有返回值的情況下:如果要搜尋一條邊,遞迴函式返回值不為空的時候,立刻返回,如果搜尋整個樹,直接用一個變數left、right接住返回值,這個left、right後序還有邏輯處理的需要,也就是後序遍歷中處理中間節點的邏輯(也是回溯)

那麼為什麼要遍歷整棵樹呢?直觀上來看,找到最近公共祖先,直接一路返回就可以了。

如圖:

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

就像圖中一樣直接返回7。

但事實上還要遍歷根節點右子樹(即使此時已經找到了目標節點了),也就是圖中的節點4、15、20。

因為在如下程式碼的後序遍歷中,如果想利用left和right做邏輯處理, 不能立刻返回,而是要等left與right邏輯處理完之後才能返回。

left = 遞迴函式(root->left);  // 左
right = 遞迴函式(root->right); // 右
left與right的邏輯處理;         // 中 

所以此時大家要知道我們要遍歷整棵樹。知道這一點,對本題就有一定深度的理解了。

那麼先用leftright接住左子樹和右子樹的返回值,程式碼如下:

TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);

如果left 和 right都不為空,說明此時root就是最近公共節點。這個比較好理解

如果left為空,right不為空,就返回right,說明目標節點是透過right返回的,反之依然

這裡有的同學就理解不了了,為什麼left為空,right不為空,目標節點透過right返回呢?

如圖:

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

圖中節點10的左子樹返回null,右子樹返回目標值7,那麼此時節點10的處理邏輯就是把右子樹的返回值(最近公共祖先7)返回上去!

這裡也很重要,可能刷過這道題目的同學,都不清楚結果究竟是如何從底層一層一層傳到頭結點的。

那麼如果leftright都為空,則返回left或者right都是可以的,也就是返回空。

程式碼如下:

if (left == NULL && right != NULL) return right;
else if (left != NULL && right == NULL) return left;
else  { //  (left == NULL && right == NULL)
    return NULL;
}

那麼尋找最低公共祖先,完整流程圖如下:

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

從圖中,大家可以看到,我們是如何回溯遍歷整棵二叉樹,將結果返回給頭結點的!

整體程式碼如下:

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

        if (left == NULL && right != NULL) return right;
        else if (left != NULL && right == NULL) return left;
        else  { //  (left == NULL && right == NULL)
            return NULL;
        }

    }
};

稍加精簡,程式碼如下:

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

總結

這道題目刷過的同學未必真正瞭解這裡面回溯的過程,以及結果是如何一層一層傳上去的。

那麼我給大家歸納如下三點

  1. 求最小公共祖先,需要從底向上遍歷,那麼二叉樹,只能透過後序遍歷(即:回溯)實現從底向上的遍歷方式。
  2. 在回溯的過程中,必然要遍歷整棵二叉樹,即使已經找到結果了,依然要把其他節點遍歷完,因為要使用遞迴函式的返回值(也就是程式碼中的left和right)做邏輯判斷。
  3. 要理解如果返回值left為空,right不為空為什麼要返回right,為什麼可以用返回right傳給上一層結果。

可以說這裡每一步,都是有難度的,都需要對二叉樹,遞迴和回溯有一定的理解。

本題沒有給出迭代法,因為迭代法不適合模擬回溯的過程。理解遞迴的解法就夠了。

相關文章