代码手记笔录——二叉树

写在前面

(1)二叉树遍历顺序包括先序遍历、中序遍历、后序遍历。二叉树遍历实现包括迭代实现、递归实现。
(2)二叉树遍历性质:若设置空节点元素值为 NULL,则先序/中序遍历一棵二叉树,能唯一得到一组数值序列。且该数值序列唯一对应一棵二叉树。故可以通过遍历数值序列判断两棵树是否具有相同的(全局/局部)结构。
(3)递归遍历与递归-回溯的关联:先序遍历若在 左递归右递归 函数之间进行回溯操作,则可变成 递归-回溯 算法:257 二叉树的所有路径。
(4)层序遍历/迭代遍历很难记录结点的父结点,所以不太适合重建树、构造树的任务。对于重建树、构造树的任务,最适合的是递归遍历。例如617 合并二叉树。

二叉树遍历的迭代实现

二叉树遍历的迭代实现共用一个模板,特殊的是后序遍历需要在当前节点存在右节点且右节点未被访问条件下将弹出的元素再次压栈=> if(p->right && pre != p->right);。为了标志上一次访问过的节点,每次访问一个节点就用 pre 指针指向它。=> pre = p;,将其从原来的树中取出=>p=nullptr;
二叉树迭代遍历初始时并不将根压入栈,且 p 指向 root,并且跳出循环的条件=> while (!st.empty() || p)

先序遍历模板

while (!st.empty() || p) {
    while (p) {
        ...  // read root data
        st.push(p);  // push into stack
        p = p->left;  // 一直左走
    }
    // 弹出栈顶元素并向其右孩子走
    p = st.top();
    st.pop();
    p = p->right;
}

中序遍历模板

while (!st.empty() || p) {
     while (p) {
        st.push(p);  // push into stack
        p = p->left;  // 一直左走
    }
    // 弹出栈顶元素并向其右孩子走
    p = st.top();
    st.pop();
    ... // read root data
    p = p->right;
}

后序遍历模板

while (!st.empty() || p) {
    while (p) {
        st.push(p);  // push into stack
        p = p->left;  // 一直左走
    }
    // 弹出栈顶元素并向其右孩子走
    p = st.top();
    st.pop();
    // 若右节点有,且未访问过,则将右节点重新放入栈
    if (p->right && p->right != pre) {
        st.push(p);
        p = p->right;
    }
    else {
        ... // read root data
        pre = p;   // 每次访问一个节点就用 pre 指针指向它。
        p = nullptr; 
    }
}

章节题目总结

101 对称二叉树

算法思想:复制根节点为另一棵二叉树,采用层序遍历方法,一棵二叉树先左节点后右节点,一棵二叉树反过来——先右节点后左节点。两棵树的结点交叉进队列。

TreeNode* p, *q;
deq.push_back(left);        
deq.push_back(right);
while (!deq.empty()) {
    p = deq.front();
    deq.pop_front();
    q = deq.front();
    deq.pop_front();
    if (!p && !q) 
        continue;
    if (!p || !q || (p->val != q->val))
        return false;
    // 交叉进队列
    deq.push_back(p->left);
    deq.push_back(q->right);
    deq.push_back(p->right);
    deq.push_back(q->left);
}

222 完全二叉树的个数【妙:深度遍历+位操作】

注释:这道题很妙,妙在利用结点的数值记号的数学规律判断其在树中的位置,有选择性地进行深度遍历,减少了遍历时间复杂度。
完全二叉树性质:

  • 深 h 的满二叉树总个数推导【等比数列求和a1(s-q^n)/(1-q)】【h 从 0 开始】
  • 第 h 层结点个数的推导
  • 结点数值记号的数学规律:按 1 开始标记,9号元素=1001 代表在第元素 4 层,先从根向左走,再 2 次向右走,再向左走。
class Solution {
public:
    int countNodes(TreeNode* root) {
        if (!root)
            return 0;
        if (!root->left && !root->right)
            return 1;
        TreeNode* p = root;
        int h = -1;
        while (p) {
            ++h;
            p = p->left;
        }
        int low = 1 << h, high = 1<<(h+1), mid, ans;

        while (low < high) {
            mid = low + ((high-low)>>1);
            if (exist(root, mid, h)) {
                ans = mid;
                low = mid + 1;
            }
            else
                high = mid;
        }
        return ans;
    }
    bool exist(TreeNode* root, int k, int h) {
        TreeNode *p = root;
        int bitFlag = 1 << (h-1);
        while (bitFlag) {
            int dir = k & bitFlag;
            if (!dir)
                p = p->left;
            else
                p = p->right;
            if (!p)
                return false; 
            bitFlag >>= 1;
        }
        return true;
    }
};

117 填充每个节点的下一个右侧节点

层序遍历的空间复杂度是 O(n),为了降低空间空间复杂度,在已将上一层链接成链后,就可以通过条件判断下一层链表表头在何处。 如何设置新链表表头是关键,考验代码能力。

链接下一层的操作代码:若新链表为空,则将该元素设置为表头,否则将链尾链接到该元素。

void concatNodes(Node* &last, Node* &cur) {
    if (last)
        last->next = cur;
    last = cur;
}
nextStart = nextStart ? nextStart : p->left;  // 每层只更新一次

二叉树遍历的递归实现

我们在递归遍历二叉树的时候,要有意识地知道当前使用的递归是对树进行先序遍历/中序遍历/后序遍历。

先序遍历模板

void traversal(TreeNode* cur, vector<int>& vec) {
    if (cur == NULL) return;
    vec.push_back(cur->val);    // 中
    traversal(cur->left, vec);  // 左
    traversal(cur->right, vec); // 右
}

中序遍历模板

void traversal(TreeNode* cur, vector<int>& vec) {
    if (cur == NULL) return;
    traversal(cur->left, vec);  // 左
    vec.push_back(cur->val);    // 中
    traversal(cur->right, vec); // 右
}

后序遍历模板

void traversal(TreeNode* cur, vector<int>& vec) {
    if (cur == NULL) return;
    traversal(cur->left, vec);  // 左
    traversal(cur->right, vec); // 右
    vec.push_back(cur->val);    // 中
}

章节题目总结

110 平衡二叉树

该解法相当于对二叉树进行后序遍历。

class Solution {
public:
    bool isBalanced(TreeNode* root) {
        int depth = 1;
        return isBalanced(root, depth);
    }
    bool isBalanced(TreeNode* root, int &depth) {
        if (!root)
            return true;
        bool lftBalanced, rgtBalanced;
        int lftDepth = depth+1, rgtDepth = depth+1;
        lftBalanced = isBalanced(root->left, lftDepth);
        if (!lftBalanced)
            return false;
        rgtBalanced = isBalanced(root->right, rgtDepth);
        if (!rgtBalanced)
            return false;
        depth = max(lftDepth, rgtDepth);  // 如果使用传参数,别忘了要动态更新这个值
        return abs(lftDepth-rgtDepth) <= 1 ? true : false;
    }
};

572. 另一棵树的子树

算法思想:任何一棵树,先序遍历的序列是唯一的。因此我们可分别对主树与子树进行先序遍历,然后进行 KMP 匹配 。由于会出现空节点,因此我们要 用 lNull,rNull分别表示左右两个空节点。可以令 lNULL=INT_MIN,rNULL=INT_MAX,也可以先遍历树找出这棵树的最大值,然后将不位于该数的值作为lNull,rNull。

class Solution {
public:
    bool isSubtree(TreeNode* root, TreeNode* subRoot) {
        vector<int> mTreeVec, sTreeVec;
        lNULL = INT_MIN, rNULL = INT_MAX;
        dfsTree(root, mTreeVec);
        dfsTree(subRoot, sTreeVec);
        vector<int> next(sTreeVec.size(), 0);
        getNext(sTreeVec, next);
        return KMP(mTreeVec, sTreeVec, next);
    }
private:
    int lNULL, rNULL;
    void dfsTree(TreeNode* root, vector<int> &vec) {
        if (!root)
            return;
        vec.push_back(root->val);
        if (root->left)
            dfsTree(root->left, vec);
        else
            vec.push_back(lNULL);
        if (root->right)
            dfsTree(root->right, vec);
        else
            vec.push_back(rNULL);
    }
    bool KMP(const vector<int> &mTreeVec, const vector<int> &sTreeVec, const vector<int> &next) {
        int j = 0;
        for (int i=0; i<mTreeVec.size(); ++i) {
            while (j>0 && mTreeVec[i] != sTreeVec[j])
                j = next[j-1];
            if (mTreeVec[i] == sTreeVec[j])
                ++j;
            if (j >= sTreeVec.size())
                return true;
        }
        return false;
    }
    void getNext(const vector<int> &sTreeVec, vector<int> &next){
        int pre = 0;
        next[pre] = 0;
        for (int cur=1; cur < sTreeVec.size(); ++cur) {
            while (pre > 0 &&  sTreeVec[cur] != sTreeVec[pre])
                pre = next[pre-1];
            if (sTreeVec[cur] == sTreeVec[pre])
                ++pre;
            next[cur] = pre;
        }
    }
};

257 二叉树的所有路径

class Solution {
public:
    vector<string> binaryTreePaths(TreeNode* root) {
        vector<string> ans;
        string path;
        binaryTreePaths(ans, root, path);
        return ans;
    }
private:
    void binaryTreePaths(vector<string> &ans, TreeNode* root, string &path) {
        string pValStr = to_string(root->val);
        if (path.size() > 0)
            path += "->";
        path += pValStr;
        if (!root->left && !root->right) {
            ans.push_back(path);
            return;
        }
        if (root->left) {
            binaryTreePaths(ans, root->left, path);  // 递归
            string lftValStr = to_string(root->left->val); // 回溯
            path.resize(path.size() - lftValStr.size() - 2);
        }
        if (root->right) {
            binaryTreePaths(ans, root->right, path);
            string rgtValStr = to_string(root->right->val);
            path.resize(path.size() - rgtValStr.size() - 2);
        }
    }
};

106. 从中序与后序遍历序列构造二叉树

核心代码:

...   // 找左-右子树结点值区间
TreeNode* root = new TreeNode(rootVal);  // 构造根节点  
root->left = buildTree(...);   // 根节点指向左子树的根节点
root->right = buildTree(...);  // 根节点指向右子树的根节点

236 二叉树的最近公共祖先

算法思想:若当前根节点为 null,必定不包含 p or q,返回 null;若当前节点为 p or q,最深公共节点必定是它【若 q 不在该 root 的树上,也称“最深公共节点必定是它”】;否则,若 p or q 落在左子树,则为 root->left,若 p or q 落在右子树,则为 root->right。若分别落在左右子树,则为 root。那怎么判断究竟是何种情况呢?我们分别用 left、right记录 p or q在左子树/右子树的最深公共节点。

备注:这种方法的缺点是:若 p or q 不存在树上,则会出现错误结果。

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (!root)
            return nullptr;
        // 若 p 或 q就是 root 里
        if (root==p || root==q)
            return root;
        // 若 p 存在,q 不存在,返回 p 
        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);
        // 若 p、q 分别存在左右指针,则返回 root
        if (left && right)
            return root;
        if (!left && right)
            return right;
        return left;
    }
};

二叉树层序遍历

算法思想:二叉树层序遍历初始时将根压入队列,p 不指向 root,lvlEnd指向 root,并且跳出循环条件=>while (!deq.empty()) ,并且是在 lvlEnd 出队列时,再次重新 lvlEnd,指向下一层的最后节点
层序遍历只适合输出数据的任务,由于其很难记录结点的父结点,所以不太适合重建树、构造树的任务。例如不适合617 合并二叉树。

层序遍历模板(带每层最后结点的标志):

// 开始时lvlEnd指向 root
TreeNode* p, *lvlEnd = root;
deque<TreeNode*> deq;
deq.push_back(root);
while (!deq.empty()) {
    p = deq.front();
    deq.pop_front();
    if (p->left)
        deq.push_back(p->left);
    if (p->right)
        deq.push_back(p->right);
    if (p == lvlEnd) {
        lvlEnd = deq.back();
    }
}

章节题目总结

117 填充每个节点的下一个右侧节点

层序遍历的空间复杂度是 O(n),优点是代码简单

二叉搜索树(BST)

二叉搜索树理由的是二分查找的思想。
(1)中序遍历二叉搜索树可得到递增的序列。因此每个节点元素的取值范围受限于中序遍历里的前一个节点与后一个节点的值——(preVal, nextVal)
(2)根节点的值是左子树所有节点的上界,是右子树所有节点的下界。正是这种不断缩短搜索范围,使得二叉搜索树可行。换而言之,若想判断给定的二叉树是否为二叉搜索树,就要判断每个节点的 Val 是否处于限定的范围内。
(3)例如,左子树的右子树的Val虽然可以大过左子树Val,但是不可以大过根节点的Val;右子树的左子树虽然可以小于右子树的Val,但不可以小过根节点的Val。
通过 98 验证二叉搜索树 可以很好地理解二叉树的定义。

章节题目总结

98 验证二叉搜索树


此结构可以完全应用在 530 二叉搜索树的最小绝对差

class Solution {
public:
    bool isValidBST(TreeNode* root) {
        long minData = LONG_MIN, maxData = LONG_MAX;
        return isValidBST(root, minData, maxData);
    }
private:
    bool isValidBST(TreeNode* root, long minData, long maxData) {
        if (!root)
            return true;
        if (root->val <= minData || root->val >= maxData)
            return false;
        bool lflag, rflag;
        lflag = isValidBST(root->left, minData, root->val);
        rflag = isValidBST(root->right, root->val, maxData);
        return lflag && rflag;
    }
};

530 二叉搜索树的最小绝对差

class Solution {
public:
   int minDiffInBST(TreeNode* root) {
       int low = -1000000, high = INT_MAX, minDist = 1000000;
       minDiffInBST(root, low, high, minDist);
       return minDist;
   }
private:
   void minDiffInBST(TreeNode* root, int low, int high, int &minDist) {
       int lchildMin, rchildMax;
       int minData =  min(abs(root->val-low), abs(root->val-high));
       minDist = minData < minDist ? minData : minDist;
       if (root->left)
           minDiffInBST(root->left, low, root->val, minDist);
       if (root->right)
           minDiffInBST(root->right, root->val, high , minDist);
   }
};

此外由于对二叉搜索树进行中序遍历得到的就是升序数组,要求最小绝对差,必然是最小的相邻两元素。

class Solution {
public:
    int minDiffInBST(TreeNode* root) {
        stack<TreeNode*> st;
        TreeNode* p = root;
        int preVal = -1, curVal, ans = INT_MAX;
        while (!st.empty() || p) {
            while (p) {
                st.push(p);
                p = p->left;
            }
            p = st.top();
            st.pop();
            if (preVal != -1)
                ans = ans < abs(p->val-preVal) ? ans : abs(p->val-preVal) ;
            preVal = p->val;
            p = p->right;
        }
        return ans;
    }
};

501 二叉搜索树中的众数

由于是二叉搜索树,中序遍历可得到升序数组,因此单调队列的思想适合解决这道题。由于不能使用额外的空间,我们将 vector 的功能也按照队列的方式实现,就不需要额外增添 deque 的空间。
算法思想:用pair<preVal, preFreq>记录上一个可能联系序列的 value 及frequency,用传参的方式维护 maxFreq。若当前元素与preVal相同,就让preFreq加 1;否则令preVal=curRoot->val,preFreq=1。若 preFreq 超过了maxFreq,则放入答案数组,并更新 maxFreq。这里要注意的是,由于有多个可能的众数,所以若频率等于 maxFreq,则直接放入答案数组;若超过maxFreq,则需要令答案数组置空,再放入答案数组。

  • 进阶:你可以不使用额外的空间吗?(假设由递归产生的隐式调用栈的开销不被计算在内)
class Solution {
public:
    vector<int> findMode(TreeNode* root) {
        vector<int> ans;
        int maxFreq = 0;
        pair<int, int> preRecord = make_pair(INT_MIN, 0);
        findMode(ans, root, preRecord, maxFreq);      
        return ans;  
    }
private:
    void findMode(vector<int> &ans, TreeNode* root, pair<int, int> &preRecord , int &maxFreq) {
        if (root->left)
            findMode(ans, root->left, preRecord, maxFreq);
        if (root->val == preRecord.first) 
             preRecord.second += 1;
        else {
            preRecord.first = root->val;
            preRecord.second = 1;
        }
        // 若是当前的众数
        if (preRecord.second >= maxFreq) {
            if (preRecord.second > maxFreq) {
                maxFreq = preRecord.second;
                ans.resize(0);
            }
            ans.push_back(root->val);
        }
        if (root->right)
            findMode(ans, root->right, preRecord, maxFreq);
    }
};
posted @ 2022-05-11 14:38  MasterBean  阅读(69)  评论(0)    收藏  举报