關於二叉搜索樹的一些總結

坦率地講,我一直覺得樹這個結構特別複雜,主要是我搞不太清楚遞歸的過程,所以老是忘(就在剛纔,我又忘了怎麼把一個有序數組變成BST),主要還是不理解吧……所以就總結一下,方便下次查詢。

二叉搜索樹BST是一個很常見的結構,並且有一些特別好的性質:

  1. 節點 N 左子樹上的所有節點的值都小於等於節點 N 的值
  2. 節點 N 右子樹上的所有節點的值都大於等於節點 N 的值
  3. 左子樹和右子樹也都是 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的題目大概也就是這麼個做法,說起來也很簡單:

  1. 考慮在數組語境下的做法
  2. 遞歸進行中序遍歷

最後補充一點,CSDN的md編輯器居然不能識別c++,只能識別cpp?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章