二叉樹與遞歸的前世今生

我的CSDN: ListerCi
我的簡書: 東方未曦

一炫彩、二叉樹與遞歸

二叉樹與遞歸有著千絲萬縷的聯(lián)系匾七,二叉樹在定義時就使用了遞歸的概念:一棵二叉樹可能是空樹,如果不是空樹江兢,那么它的左右子樹都是二叉樹昨忆。我們一般這樣定義二叉樹的節(jié)點TreeNode,它包含三個成員:該節(jié)點的值杉允,該節(jié)點的左子樹和該節(jié)點的右子樹邑贴。TreeNode的構(gòu)造函數(shù)在新建節(jié)點時會將它的左右子樹都賦值為空。

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

二叉樹與線性數(shù)據(jù)結(jié)構(gòu)不同的是叔磷,它無法像數(shù)組或者鏈表一樣使用一個循環(huán)就可以從頭到尾遍歷所有的數(shù)據(jù)拢驾。二叉樹是發(fā)散的,你不知道他的左子樹或是右子樹延伸了多少個節(jié)點世澜,你只有在遍歷完一棵二叉樹時你才知道它的結(jié)構(gòu)独旷。那么該怎么遍歷二叉樹呢?
首先寥裂,二叉樹的遍歷分為深度優(yōu)先遍歷廣度優(yōu)先遍歷,深度優(yōu)先遍歷分為先序案疲、中序和后序封恰。以先序遍歷為例,程序會先訪問root節(jié)點褐啡,然后依次遍歷root的左子樹和右子樹诺舔。在遍歷左、右子樹時备畦,仍然先訪問根結(jié)點低飒,然后遍歷左子樹,最后遍歷右子樹懂盐,如果二叉樹為空則返回褥赊。
前序遍歷很容易讓人想到遞歸,它會訪問根節(jié)點之后再遞歸地訪問左子樹莉恼,之后再遞歸地訪問右子樹拌喉。根據(jù)這樣的思路可以寫出前序遍歷的遞歸代碼,遞歸最重要的是返回條件俐银,這里的返回條件是二叉樹為空尿背。

void preOrder(TreeNode* root) {
    if (root == NULL) return; // 當(dāng)前二叉樹為空,則表示該分支已經(jīng)訪問結(jié)束
    cout << root->value;
    preOrder(root->left); // 遞歸訪問左子樹
    preOrder(root->right); // 遞歸訪問右子樹
}

了解前序遍歷之后捶惜,也就能很輕易地寫出中序和后序的代碼田藐,這里不再贅述。

二、二叉樹遞歸實戰(zhàn)

1. 二叉樹的深度

Leetcode地址:104. 二叉樹的最大深度
這道題要求的是二叉樹的深度汽久,或者說高度鹤竭,也就是求二叉樹根節(jié)點到最遠(yuǎn)的葉節(jié)點所經(jīng)過的節(jié)點數(shù)。下方的二叉樹深度為3回窘,因為從根節(jié)點3到葉節(jié)點15和到葉節(jié)點7都要經(jīng)過3個節(jié)點诺擅。

    3
   / \
  9  20
    /  \
   15   7

很顯然,一棵二叉樹的高度 = max(左子樹高度, 右子樹高度) + 1啡直,上方二叉樹的左子樹高度為1烁涌,右子樹高度為2,那么很容易得出整個二叉樹高度為3酒觅。而左右子樹的高度又可以遞歸求出撮执,只需在二叉樹為空時返回高度0即可。

int maxDepth(TreeNode* root) {
    if (root == NULL) return 0;
    return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}

2. 翻轉(zhuǎn)二叉樹

Leetcode地址:226. 翻轉(zhuǎn)二叉樹
例:

輸入:
     4
   /   \
  2     7
 / \   / \
1   3 6   9

輸出:
     4
   /   \
  7     2
 / \   / \
9   6 3   1

可以看到舷丹,翻轉(zhuǎn)二叉樹時會將所有節(jié)點的左右子樹都進(jìn)行一次翻轉(zhuǎn)抒钱。相對于根節(jié)點,節(jié)點2和節(jié)點7的位置進(jìn)行了對調(diào)颜凯,而相對于節(jié)點2谋币,節(jié)點3和節(jié)點1的位置進(jìn)行了對調(diào)……因此可以通過遞歸來對每個節(jié)點的左右子樹進(jìn)行對調(diào),在遇到空樹時退出遞歸症概。

TreeNode* invertTree(TreeNode* root) {
    invert(root);
    return root;
}

void invert(TreeNode* root) {
    if (root == NULL) return;
    // 對調(diào)左右子樹
    TreeNode* tmp = root->left;
    root->left = root->right;
    root->right = tmp;
    // 遞歸左右子樹
    invert(root->left);
    invert(root->right);
}

3. 相同的樹

Leetcode地址:100. 相同的樹
該題要判斷兩個二叉樹是否相同蕾额。只有兩個二叉樹的結(jié)構(gòu)相同并且每個節(jié)點上的值相同,它們才是相同的樹彼城。
既然要判斷所有的節(jié)點诅蝶,那么很顯然可以用遞歸求解:假設(shè)當(dāng)前有兩個二叉樹root1root2,如果其中一個為空而另一個不為空募壕,返回false调炬;如果兩個都為空,返回true舱馅;如果都不為空缰泡,則判斷這兩個節(jié)點的值是否相同,不同則返回false习柠,相同的話則再判斷它們的左右子樹是否相同匀谣。

bool isSameTree(TreeNode* p, TreeNode* q) {
    if (p == NULL && q == NULL) return true; // 兩個節(jié)點都為空時代表相同
    if (p == NULL || q == NULL) return false;
    if (p->val == q->val) {
        return (isSameTree(p->left, q->left) && isSameTree(p->right, q->right));
    } else {
        return false;
    }
}

4. 合并二叉樹

Leetcode地址:617. 合并二叉樹
給定兩個二叉樹,想象當(dāng)你將它們中的一個覆蓋到另一個上時资溃,兩個二叉樹的一些節(jié)點便會重疊武翎。
你需要將他們合并為一個新的二叉樹。合并的規(guī)則是如果兩個節(jié)點重疊溶锭,那么將他們的值相加作為節(jié)點合并后的新值宝恶,否則不為 NULL 的節(jié)點將直接作為新二叉樹的節(jié)點。
例:

輸入: 
    Tree 1                     Tree 2                  
          1                         2                             
         / \                       / \                            
        3   2                     1   3                        
       /                           \   \                      
      5                             4   7                  
輸出: 
合并后的樹:
         3
        / \
       4   5
      / \   \ 
     5   4   7

這道題很明顯也是一道遞歸題,我們需要同時遍歷兩棵樹垫毙,如果當(dāng)前節(jié)點都不為空霹疫,則將兩個值相加形成新的節(jié)點;如果其中一個為空综芥,則以該節(jié)點的值新建節(jié)點丽蝎;如果都為空,則返回空膀藐。

TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
    if (t1 != NULL && t2 != NULL) { // 都不為空屠阻,則將兩個節(jié)點的值相加
        TreeNode* node = new TreeNode(t1->val + t2->val);
        node->left = mergeTrees(t1->left, t2->left);
        node->right = mergeTrees(t1->right, t2->right);
        return node;
    } else if (t1 != NULL) { // t1不為空, t2為空
        TreeNode* node = new TreeNode(t1->val);
        node->left = mergeTrees(t1->left, NULL);
        node->right = mergeTrees(t1->right, NULL);
        return node;
    } else if (t2 != NULL) { // t1為空, t2不為空
        TreeNode* node = new TreeNode(t2->val);
        node->left = mergeTrees(t2->left, NULL);
        node->right = mergeTrees(t2->right, NULL);
        return node;
    } else {
        return NULL;
    }
}

5. 從前序與中序遍歷序列構(gòu)造二叉樹

Leetcode地址:105. 從前序與中序遍歷序列構(gòu)造二叉樹
例:
前序遍歷preorder = [3, 9, 20, 15, 7]
中序遍歷 inorder = [9, 3, 15, 20, 7]
返回如下的二叉樹:

    3
   / \
  9  20
    /  \
   15   7

根據(jù)上面的例子,前序遍歷的第一個節(jié)點3就是二叉樹的根節(jié)點额各,在中序遍歷中找到3国觉,中序遍歷中,3之前的就是左子樹的節(jié)點虾啦,3之后的就是右子樹的節(jié)點麻诀。那么左子樹的前序遍歷就是[9],中序遍歷也是[9]傲醉,所以左子樹只有一個值為9的節(jié)點蝇闭。右子樹的前序遍歷為[20, 15, 7],中序遍歷為[15, 20, 7]硬毕,那么右子樹的根節(jié)點就是15丁眼,以此類推……

很容易發(fā)現(xiàn)這也是一個遞歸的程序:前序遍歷的第一個值就是根節(jié)點root,然后在中序遍歷中找到root昭殉。那么在中序遍歷中root前的n個值就是左子樹的中序遍歷,root后的m個值就是右子樹的中序遍歷藐守。前序遍歷中root之后的n個值就是左子樹的前序遍歷挪丢,剩下的m個值就是右子樹的前序遍歷。
這樣就得到了左右子樹的前序遍歷和中序遍歷卢厂,即可遞歸得到整個二叉樹乾蓬。

TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
    if (preorder.size() == 0) return NULL;
    int index = preorder.size() - 1;
    return build(preorder, inorder, 0, index, 0, index);
}

// s1 和 e1 為前序遍歷的起點和終點, s2 和 e2 為中序遍歷的起點和終點
TreeNode* build(vector<int>& preorder, vector<int>& inorder, int s1, int e1, int s2, int e2) {
    if (s1 == e1) {
        TreeNode* node = new TreeNode(preorder[s1]);
        return node;
    }
    if (s1 > e1 && s2 > e2) {
        return NULL;
    }
    // 新建根節(jié)點
    TreeNode* root = new TreeNode(preorder[s1]);
    // 找到中間節(jié)點
    int middle2;
    for (int i = s2; i <= e2; ++i) {
        if (preorder[s1] == inorder[i]) {
            middle2 = i;
            break;
        }
    }
    if (middle2 == s2) {
        root->right = build(preorder, inorder, s1 + (middle2 - s2 + 1), e1, middle2 + 1, e2);
    } else if (middle2 == e2) {
        root->left = build(preorder, inorder, s1 + 1, s1 + (middle2 - s2), s2, middle2 - 1);
    } else {
        // 遞歸調(diào)用
        root->left = build(preorder, inorder, s1 + 1, s1 + (middle2 - s2), s2, middle2 - 1);
        root->right = build(preorder, inorder, s1 + (middle2 - s2 + 1), e1, middle2 + 1, e2);
    }
    return root;
}

理解該題之后還可以嘗試一下106. 從中序與后序遍歷序列構(gòu)造二叉樹

三、二叉搜索樹

二叉搜索樹(Binary Search Tree)也叫二叉排序樹慎恒,簡稱為BST任内。它具有這樣的性質(zhì):若它的左子樹不空,則左子樹上所有結(jié)點的值均小于它的根結(jié)點的值融柬; 若它的右子樹不空死嗦,則右子樹上所有結(jié)點的值均大于它的根結(jié)點的值; 它的左粒氧、右子樹也分別為二叉搜索樹越除。(仔細(xì)觀察可以發(fā)現(xiàn),二叉搜索樹的中序遍歷可以得到一個有序數(shù)組)。

在二叉搜索樹中查找某個值的操作類似有序數(shù)組中的二分查找摘盆,時間復(fù)雜度都是O(logn)翼雀,但是在BST中插入新的值比在有序數(shù)組中要快得多,因為在BST中插入新的值孩擂,只需使用O(logn)的時間找到插入點然后插入即可狼渊;而在有序數(shù)組中,找到插入點之后需要將該點之后的元素都向后移動一位类垦,移動多個元素的操作非常消耗時間狈邑。

以下圖的二叉搜索樹為例,在其中進(jìn)行查找和插入的操作护锤。
① 查找元素7:從根節(jié)點開始查找官地,根節(jié)點為15,而要查找的值7小于15烙懦,則去節(jié)點15的左子樹進(jìn)行查找驱入;查找到節(jié)點6時,發(fā)現(xiàn)要查找的值7大于6氯析,則去節(jié)點6的右子樹進(jìn)行查找亏较;查找到節(jié)點7,找到結(jié)果掩缓,退出雪情。
② 插入元素19:開始時與查找過程類似,直到找到節(jié)點20你辣,發(fā)現(xiàn)19比20小且20的左子樹為空巡通,則將19插入到節(jié)點20的左子樹。

BST.jpg

有時二叉搜索樹的插入順序不理想舍哄,那么會得到一棵偏向一邊的BST宴凉,這時的BST就退化成了鏈表。如下所示表悬,對于這幾個數(shù)據(jù)弥锄,我們期望的BST肯定是左邊這樣的,但是如果我們按1->2->3->4->5->6這樣的順序插入蟆沫,就會得到右邊這樣的BST籽暇。因此如果給定一個有序數(shù)組來構(gòu)建二叉搜索樹,需要挑選中間的值來作為根節(jié)點饭庞。

Abnormal BST.jpg

四戒悠、二叉搜索樹實戰(zhàn)

1. 二叉搜索樹中的搜索

Leetcode地址:700. 二叉搜索樹中的搜索
這道題思路很清楚:假設(shè)從節(jié)點root開始查找value,如果value == root->val但绕,則找到結(jié)果救崔;如果value < root->val惶看,則移動到root->left進(jìn)行查找;否則移動到root->right進(jìn)行查找六孵。如果查找完整棵樹還沒有發(fā)現(xiàn)纬黎,就返回空。

TreeNode* searchBST(TreeNode* root, int val) {
    if (root == NULL) return NULL;
    if (root->val == val) return root;
    else if (val < root->val) return searchBST(root->left, val);
    else return searchBST(root->right, val);
}

2. 二叉搜索樹中的插入

Leetcode地址:701. 二叉搜索樹中的插入操作
在對二叉樹進(jìn)行插入操作之前需要先找到插入點劫窒,而尋找插入點與上一題的搜索類似本今,當(dāng)找到為空的地方時就可以將當(dāng)前節(jié)點插入。

TreeNode* insertIntoBST(TreeNode* root, int val) {
    TreeNode* cur = new TreeNode(val);
    insert(root, cur);
    return root;
}

void insert(TreeNode* &node, TreeNode* cur) {
    if (node == NULL) { // 找到為空的分支, 即可插入當(dāng)前節(jié)點
        node = cur;
        return;
    }
    if (cur->val < node->val) {
        insert(node->left, cur);
    } else if (cur->val > node->val) {
        insert(node->right, cur);
    } else {
        TreeNode* temp = node->left;
        node->left = cur;
        cur->left = temp;
    }
}

3. 將有序數(shù)組轉(zhuǎn)換為二叉搜索樹

Leetcode地址:108. 將有序數(shù)組轉(zhuǎn)換為二叉搜索樹
題目要求將一個按照升序排列的有序數(shù)組主巍,轉(zhuǎn)換為一棵高度平衡二叉搜索樹冠息。既然要求BST高度平衡,那么肯定要將數(shù)組中間的值作為根節(jié)點孕索,而左子樹的根節(jié)點則是數(shù)組左半部分的中間值逛艰,右子樹的根節(jié)點是數(shù)組右半部分的中間值……依次遞歸

TreeNode* sortedArrayToBST(vector<int>& nums) {
    if (nums.size() == 0) return NULL; // 如果數(shù)組為空, 返回空樹
    TreeNode* root;
    generateBST(nums, root, 0, nums.size() - 1);
    return root;
}

void generateBST(vector<int>& nums, TreeNode* &ptr, int start, int end) {
    int middle = (start + end) / 2;
    ptr = new TreeNode(nums[middle]); // 用數(shù)組中間值新建一個節(jié)點
    if (start <= middle - 1) { // 如果左邊還有數(shù)據(jù)
        generateBST(nums, ptr->left, start, middle - 1);
    }
    if (middle + 1 <= end) { // 如果右邊還有數(shù)據(jù)
        generateBST(nums, ptr->right, middle + 1, end);
    }
}

上述代碼用到了C++中的引用,如果不習(xí)慣這種調(diào)用方式搞旭,可以這么寫散怖。

TreeNode* sortedArrayToBST(vector<int>& nums) {
    if (nums.size() == 0) return NULL;
    return build(nums, 0, nums.size() - 1);
}

// 構(gòu)建完一個節(jié)點之后直接返回
TreeNode* build(vector<int>& nums, int start, int end) {
    if (start > end) return NULL;
    int mid = (start + end) / 2;
    TreeNode* node = new TreeNode(nums[mid]);
    node->left = build(nums, start, mid - 1);
    node->right = build(nums, mid + 1, end);
    return node;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市肄渗,隨后出現(xiàn)的幾起案子镇眷,更是在濱河造成了極大的恐慌,老刑警劉巖翎嫡,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件欠动,死亡現(xiàn)場離奇詭異,居然都是意外死亡惑申,警方通過查閱死者的電腦和手機(jī)具伍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來圈驼,“玉大人沿猜,你說我怎么就攤上這事⊥爰梗” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵橄妆,是天一觀的道長衙伶。 經(jīng)常有香客問我,道長害碾,這世上最難降的妖魔是什么矢劲? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮慌随,結(jié)果婚禮上芬沉,老公的妹妹穿的比我還像新娘躺同。我一直安慰自己,他們只是感情好丸逸,可當(dāng)我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布蹋艺。 她就那樣靜靜地躺著,像睡著了一般黄刚。 火紅的嫁衣襯著肌膚如雪捎谨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天憔维,我揣著相機(jī)與錄音涛救,去河邊找鬼。 笑死业扒,一個胖子當(dāng)著我的面吹牛检吆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播程储,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蹭沛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤剔桨,失蹤者是張志新(化名)和其女友劉穎撕氧,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體溉潭,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了萝挤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡根欧,死狀恐怖怜珍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情凤粗,我是刑警寧澤酥泛,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站嫌拣,受9級特大地震影響柔袁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜异逐,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一捶索、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧灰瞻,春花似錦腥例、人聲如沸辅甥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽璃弄。三九已至,卻和暖如春底瓣,著一層夾襖步出監(jiān)牢的瞬間谢揪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工捐凭, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留拨扶,地道東北人。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓茁肠,卻偏偏與公主長得像患民,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子垦梆,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容