將有序數(shù)組轉(zhuǎn)換為二叉搜索樹
構(gòu)造二叉搜索樹,一不小心就平衡了
將有序數(shù)組轉(zhuǎn)換為二叉搜索樹
力扣題目鏈接:https://leetcode-cn.com/problems/convert-sorted-array-to-binary-search-tree
將一個(gè)按照升序排列的有序數(shù)組,轉(zhuǎn)換為一棵高度平衡二叉搜索樹。
本題中,一個(gè)高度平衡二叉樹是指一個(gè)二叉樹每個(gè)節(jié)點(diǎn) 的左右兩個(gè)子樹的高度差的絕對(duì)值不超過 1。
示例:
思路
做這道題目之前大家可以了解一下這幾道:
- 從中序與后序遍歷序列構(gòu)造二叉樹
- 最大二叉樹
- 二叉搜索樹中的插入操作
- 刪除二叉搜索樹中的節(jié)點(diǎn)
進(jìn)入正題:
題目中說要轉(zhuǎn)換為一棵高度平衡二叉搜索樹。這和轉(zhuǎn)換為一棵普通二叉搜索樹有什么差別呢?
其實(shí)這里不用強(qiáng)調(diào)平衡二叉搜索樹,數(shù)組構(gòu)造二叉樹,構(gòu)成平衡樹是自然而然的事情,因?yàn)榇蠹夷J(rèn)都是從數(shù)組中間位置取值作為節(jié)點(diǎn)元素,一般不會(huì)隨機(jī)取,所以想構(gòu)成不平衡的二叉樹是自找麻煩。
在二叉樹:構(gòu)造二叉樹登場!和二叉樹:構(gòu)造一棵最大的二叉樹中其實(shí)已經(jīng)講過了,如果根據(jù)數(shù)組構(gòu)造一顆二叉樹。
本質(zhì)就是尋找分割點(diǎn),分割點(diǎn)作為當(dāng)前節(jié)點(diǎn),然后遞歸左區(qū)間和右區(qū)間。
本題其實(shí)要比二叉樹:構(gòu)造二叉樹登場! 和 二叉樹:構(gòu)造一棵最大的二叉樹簡單一些,因?yàn)橛行驍?shù)組構(gòu)造二叉搜索樹,尋找分割點(diǎn)就比較容易了。
分割點(diǎn)就是數(shù)組中間位置的節(jié)點(diǎn)。
那么為問題來了,如果數(shù)組長度為偶數(shù),中間節(jié)點(diǎn)有兩個(gè),取哪一個(gè)?
取哪一個(gè)都可以,只不過構(gòu)成了不同的平衡二叉搜索樹。
例如:輸入:[-10,-3,0,5,9]
如下兩棵樹,都是這個(gè)數(shù)組的平衡二叉搜索樹:
將有序數(shù)組轉(zhuǎn)換為二叉搜索樹
如果要分割的數(shù)組長度為偶數(shù)的時(shí)候,中間元素為兩個(gè),是取左邊元素 就是樹1,取右邊元素就是樹2。
這也是題目中強(qiáng)調(diào)答案不是唯一的原因。理解這一點(diǎn),這道題目算是理解到位了。
遞歸
遞歸三部曲:
- 確定遞歸函數(shù)返回值及其參數(shù)
刪除二叉樹節(jié)點(diǎn),增加二叉樹節(jié)點(diǎn),都是用遞歸函數(shù)的返回值來完成,這樣是比較方便的。
相信大家如果仔細(xì)看了二叉樹:搜索樹中的插入操作和二叉樹:搜索樹中的刪除操作,一定會(huì)對(duì)遞歸函數(shù)返回值的作用深有感觸。
那么本題要構(gòu)造二叉樹,依然用遞歸函數(shù)的返回值來構(gòu)造中節(jié)點(diǎn)的左右孩子。
再來看參數(shù),首先是傳入數(shù)組,然后就是左下表left和右下表right,我們?cè)诙鏄洌簶?gòu)造二叉樹登場!中提過,在構(gòu)造二叉樹的時(shí)候盡量不要重新定義左右區(qū)間數(shù)組,而是用下表來操作原數(shù)組。
所以代碼如下:
- // 左閉右閉區(qū)間[left, right]
- TreeNode* traversal(vector<int>& nums, int left, int right)
這里注意,我這里定義的是左閉右閉區(qū)間,在不斷分割的過程中,也會(huì)堅(jiān)持左閉右閉的區(qū)間,這又涉及到我們講過的循環(huán)不變量。
數(shù)組:每次遇到二分法,都是一看就會(huì),一寫就廢;nj 在二叉樹:構(gòu)造二叉樹登場!,704. 二分查找 和59.螺旋矩陣II都詳細(xì)講過循環(huán)不變量。
- 確定遞歸終止條件
這里定義的是左閉右閉的區(qū)間,所以當(dāng)區(qū)間 left > right的時(shí)候,就是空節(jié)點(diǎn)了。
代碼如下:
- if (left > right) return nullptr;
- 確定單層遞歸的邏輯
首先取數(shù)組中間元素的位置,不難寫出int mid = (left + right) / 2;,這么寫其實(shí)有一個(gè)問題,就是數(shù)值越界,例如left和right都是最大int,這么操作就越界了,在二分法中尤其需要注意!
所以可以這么寫:int mid = left + ((right - left) / 2);
但本題leetcode的測(cè)試數(shù)據(jù)并不會(huì)越界,所以怎么寫都可以。但需要有這個(gè)意識(shí)!
取了中間位置,就開始以中間位置的元素構(gòu)造節(jié)點(diǎn),代碼:TreeNode* root = new TreeNode(nums[mid]);。
接著劃分區(qū)間,root的左孩子接住下一層左區(qū)間的構(gòu)造節(jié)點(diǎn),右孩子接住下一層右區(qū)間構(gòu)造的節(jié)點(diǎn)。
最后返回root節(jié)點(diǎn),單層遞歸整體代碼如下:
- int mid = left + ((right - left) / 2);
- TreeNode* root = new TreeNode(nums[mid]);
- root->left = traversal(nums, left, mid - 1);
- root->right = traversal(nums, mid + 1, right);
- return root;
這里int mid = left + ((right - left) / 2);的寫法相當(dāng)于是如果數(shù)組長度為偶數(shù),中間位置有兩個(gè)元素,取靠左邊的。
- 遞歸整體代碼如下:
- class Solution {
- private:
- TreeNode* traversal(vector<int>& nums, int left, int right) {
- if (left > right) return nullptr;
- int mid = left + ((right - left) / 2);
- TreeNode* root = new TreeNode(nums[mid]);
- root->left = traversal(nums, left, mid - 1);
- root->right = traversal(nums, mid + 1, right);
- return root;
- }
- public:
- TreeNode* sortedArrayToBST(vector<int>& nums) {
- TreeNode* root = traversal(nums, 0, nums.size() - 1);
- return root;
- }
- };
注意:在調(diào)用traversal的時(shí)候?yàn)槭裁磦魅氲膌eft和right為什么是0和nums.size() - 1,因?yàn)槎x的區(qū)間為左閉右閉。
迭代法
迭代法可以通過三個(gè)隊(duì)列來模擬,一個(gè)隊(duì)列放遍歷的節(jié)點(diǎn),一個(gè)隊(duì)列放左區(qū)間下表,一個(gè)隊(duì)列放右區(qū)間下表。
模擬的就是不斷分割的過程,C++代碼如下:(我已經(jīng)詳細(xì)注釋)
- class Solution {
- public:
- TreeNode* sortedArrayToBST(vector<int>& nums) {
- if (nums.size() == 0) return nullptr;
- TreeNode* root = new TreeNode(0); // 初始根節(jié)點(diǎn)
- queue<TreeNode*> nodeQue; // 放遍歷的節(jié)點(diǎn)
- queue<int> leftQue; // 保存左區(qū)間下表
- queue<int> rightQue; // 保存右區(qū)間下表
- nodeQue.push(root); // 根節(jié)點(diǎn)入隊(duì)列
- leftQue.push(0); // 0為左區(qū)間下表初始位置
- rightQue.push(nums.size() - 1); // nums.size() - 1為右區(qū)間下表初始位置
- while (!nodeQue.empty()) {
- TreeNode* curNode = nodeQue.front();
- nodeQue.pop();
- int left = leftQue.front(); leftQue.pop();
- int right = rightQue.front(); rightQue.pop();
- int mid = left + ((right - left) / 2);
- curNode->val = nums[mid]; // 將mid對(duì)應(yīng)的元素給中間節(jié)點(diǎn)
- if (left <= mid - 1) { // 處理左區(qū)間
- curNode->left = new TreeNode(0);
- nodeQue.push(curNode->left);
- leftQue.push(left);
- rightQue.push(mid - 1);
- }
- if (right >= mid + 1) { // 處理右區(qū)間
- curNode->right = new TreeNode(0);
- nodeQue.push(curNode->right);
- leftQue.push(mid + 1);
- rightQue.push(right);
- }
- }
- return root;
- }
- };
總結(jié)
在二叉樹:構(gòu)造二叉樹登場! 和 二叉樹:構(gòu)造一棵最大的二叉樹之后,我們順理成章的應(yīng)該構(gòu)造一下二叉搜索樹了,一不小心還是一棵平衡二叉搜索樹。
其實(shí)思路也是一樣的,不斷中間分割,然后遞歸處理左區(qū)間,右區(qū)間,也可以說是分治。
此時(shí)相信大家應(yīng)該對(duì)通過遞歸函數(shù)的返回值來增刪二叉樹很熟悉了,這也是常規(guī)操作。
在定義區(qū)間的過程中我們又一次強(qiáng)調(diào)了循環(huán)不變量的重要性。
最后依然給出迭代的方法,其實(shí)就是模擬取中間元素,然后不斷分割去構(gòu)造二叉樹的過程。
其他語言版本
Java
遞歸: 左閉右閉 [left,right]
- class Solution {
- public TreeNode sortedArrayToBST(int[] nums) {
- TreeNode root = traversal(nums, 0, nums.length - 1);
- return root;
- }
- // 左閉右閉區(qū)間[left, right)
- private TreeNode traversal(int[] nums, int left, int right) {
- if (left > right) return null;
- int mid = left + ((right - left) >> 1);
- TreeNode root = new TreeNode(nums[mid]);
- root.left = traversal(nums, left, mid - 1);
- root.right = traversal(nums, mid + 1, right);
- return root;
- }
- }
迭代: 左閉右閉 [left,right]
- class Solution {
- public TreeNode sortedArrayToBST(int[] nums) {
- if (nums.length == 0) return null;
- //根節(jié)點(diǎn)初始化
- TreeNode root = new TreeNode(-1);
- Queue<TreeNode> nodeQueue = new LinkedList<>();
- Queue<Integer> leftQueue = new LinkedList<>();
- Queue<Integer> rightQueue = new LinkedList<>();
- // 根節(jié)點(diǎn)入隊(duì)列
- nodeQueue.offer(root);
- // 0為左區(qū)間下表初始位置
- leftQueue.offer(0);
- // nums.size() - 1為右區(qū)間下表初始位置
- rightQueue.offer(nums.length - 1);
- while (!nodeQueue.isEmpty()) {
- TreeNode currNode = nodeQueue.poll();
- int left = leftQueue.poll();
- int right = rightQueue.poll();
- int mid = left + ((right - left) >> 1);
- // 將mid對(duì)應(yīng)的元素給中間節(jié)點(diǎn)
- currNode.val = nums[mid];
- // 處理左區(qū)間
- if (left <= mid - 1) {
- currNode.left = new TreeNode(-1);
- nodeQueue.offer(currNode.left);
- leftQueue.offer(left);
- rightQueue.offer(mid - 1);
- }
- // 處理右區(qū)間
- if (right >= mid + 1) {
- currNode.right = new TreeNode(-1);
- nodeQueue.offer(currNode.right);
- leftQueue.offer(mid + 1);
- rightQueue.offer(right);
- }
- }
- return root;
- }
- }
Python
遞歸法:
- class Solution:
- def sortedArrayToBST(self, nums: List[int]) -> TreeNode:
- def buildaTree(left,right):
- if left > right: return None #左閉右閉的區(qū)間,當(dāng)區(qū)間 left > right的時(shí)候,就是空節(jié)點(diǎn),當(dāng)left = right的時(shí)候,不為空
- mid = left + (right - left) // 2 #保證數(shù)據(jù)不會(huì)越界
- val = nums[mid]
- root = TreeNode(val)
- root.left = buildaTree(left,mid - 1)
- root.right = buildaTree(mid + 1,right)
- return root
- root = buildaTree(0,len(nums) - 1) #左閉右閉區(qū)間
- return root
本文轉(zhuǎn)載自微信公眾號(hào)「代碼隨想錄」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系代碼隨想錄公眾號(hào)。