坦率地講,我一直覺得樹這個結構特別複雜,主要是我搞不太清楚遞歸的過程,所以老是忘(就在剛纔,我又忘了怎麼把一個有序數組變成BST),主要還是不理解吧……所以就總結一下,方便下次查詢。
二叉搜索樹BST是一個很常見的結構,並且有一些特別好的性質:
- 節點 N 左子樹上的所有節點的值都小於等於節點 N 的值
- 節點 N 右子樹上的所有節點的值都大於等於節點 N 的值
- 左子樹和右子樹也都是 BST
因爲有這樣的特點,所以BST有一個很重要的特點:中序遍歷的結果事實上是一個有序的數組。可以利用這個性質解決一些問題。
此外,爲方便說明,下文中的TreeNode結構體定義如下:
struct TreeNode
{
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
首先是把有序數組轉換成BST,這個可以利用二分法來實現:
TreeNode *getBST(vector<int> &nums, int b, int e)
{
if (b == e)
{
return new TreeNode(nums[b]);
}
if (b > e)
{
return nullptr;
}
int mid = (b + e + 1) / 2;
TreeNode *root = new TreeNode(nums[mid]);
root->left = getBST(nums, b, mid - 1);
root->right = getBST(nums, mid + 1, e);
return root;
}
如果數據量較大,中間求mid的過程可以改成類似於b + ( e - b ) / 2
,通過減法來避免加法的上溢。
然後是一類比較特殊的用法:求二叉搜索樹的衆數和最小絕對差。看起來沒啥關係,其實是一類的,都依賴於當前值的上下文,因爲二叉搜索樹的中序遍歷是有序的。假如在數組的語境下,比如說求一個有序數組的衆數,第一反應肯定是弄一個計數器,每次遇到不一樣的數就清空,然後找出最大的數,大概像這樣(如果衆數不止一個,都返回):
vector<int> findModeInArray(vector<int> &arr)
{
vector<int> mode;
int max_count = 0;
int cur_count = 1;
int length = arr.size();
for (int i = 1; i < length; ++i)
{
cur_count = arr[i] == arr[i - 1] ? cur_count + 1 : 1;
if (cur_count == max_count)
{
mode.push_back(arr[i]);
}
else if (cur_count > max_count)
{
mode.clear();
mode.push_back(arr[i]);
max_count = cur_count;
}
}
return mode;
}
如果放到BST的語境下,也是一樣的,就是多了一個“翻譯”的過程。而且,由於需要遞歸,需要通過函數的參數來保存當時的上下文。如果使用全局變量,可能會出現全局變量被意外修改的情況。解法來自junstat@LeetCode:
void inOrder(TreeNode* root, TreeNode*& pre, int& curTimes,
int& maxTimes, vector<int>& res){
if (!root) return;
inOrder(root->left, pre, curTimes, maxTimes, res);
if (pre)
curTimes = (root->val == pre->val) ? curTimes + 1 : 1;
if (curTimes == maxTimes)
res.push_back(root->val);
else if (curTimes > maxTimes){
res.clear();
res.push_back(root->val);
maxTimes = curTimes;
}
pre = root;
inOrder(root->right, pre, curTimes, maxTimes, res);
}
vector<int> findMode(TreeNode* root) {
vector<int> res;
if (!root) return res;
TreeNode* pre = NULL;
int curTimes = 1, maxTimes = 0;
inOrder(root, pre, curTimes, maxTimes, res);
return res;
}
可以看到,幾乎是完全一致的,唯一的區別在於,數組語境下使用的是for循環來進行遍歷,而在BST語境下使用的是遞歸的中序遍歷,然後把每次遍歷用到的數據放在了參數裏(有點閉包的意思)。
然後是BST的最小絕對差。因爲是有序的,最小的絕對差必然在相鄰的兩個元素之間產生。放在數組語境下,就是每次用後一個減前一個,然後找到差的最小值,沒什麼可說的;放在BST語境下,也差不多是這樣:
void inOrderTraverse(TreeNode *root, TreeNode *&pre, int &min_diff)
{
if (root)
{
inOrderTraverse(root->left, pre, min_diff);
if (pre)
{
min_diff = min(root->val - pre->val, min_diff);
}
pre = root;
inOrderTraverse(root->right, pre, min_diff);
}
}
int getMinimumDifference(TreeNode *root)
{
TreeNode *pre = nullptr;
int min_diff = INT_MAX; // #include <climits>
inOrderTraverse(root, pre, min_diff);
return min_diff;
}
後來又看到一個把BST變成累加樹的題目:
給定一個二叉搜索樹(Binary Search Tree),把它轉換成爲累加樹(Greater Tree),使得每個節點的值是原來的節點值加上所有大於它的節點值之和。
這個稍微有點特殊,我一開始想的是中序遍歷,然後用HashMap保存每個Node對應的累加值,然後再遍歷一遍修改原來的樹的值(我好像特別喜歡HashMap?):
unordered_map<int, int> sum_map;
void traverse(TreeNode *root)
{
if (root)
{
traverse(root->left);
for (auto &p : sum_map)
{
p.second += root->val;
}
sum_map[root->val] = root->val;
traverse(root->right);
}
}
void convert(TreeNode *root)
{
if (root)
{
convert(root->left);
root->val = sum_map[root->val];
convert(root->right);
}
}
TreeNode *convertBST(TreeNode *root)
{
traverse(root);
convert(root);
return root;
}
結果跑出來220ms,人都傻了。然後看了題解,發現中序遍歷還能倒着用的,當時的心情真是難以言喻:
int sum = 0;
TreeNode *convertBST(TreeNode *root)
{
if (root)
{
convertBST(root->right);
sum += root->val;
root->val = sum;
convertBST(root->left);
}
return root;
}
但是聯想一下,如果是在數組語境下,大概也是這麼個做法。所以,概括下來,BST的題目大概也就是這麼個做法,說起來也很簡單:
- 考慮在數組語境下的做法
- 遞歸進行中序遍歷
最後補充一點,CSDN的md編輯器居然不能識別c++,只能識別cpp?