二叉树相关的数据结构和算法

1. 二叉树(binary tree):最基本树型结构,每个结点最多有两个子节点,分别成为左子节点(left child node)和右子节点(right child node)。

相关题目:

a. 给定根结点,计算二叉树的最大深度。

View Code
//递归解法
class Solution {
public:
int maxDepth(TreeNode* root)
{
    if (root == NULL) return 0;
    if (root->left == NULL && root->right == NULL) return 1;
    if (root->left == NULL) return 1 + maxDepth(root->right);
    if (root->right == NULL) return 1 + maxDepth(root->left);
    int l_max = maxDepth(root->left);
    int r_max = maxDepth(root->right);
    return 1 + (l_max > r_max ? l_max : r_max);
}
View Code

回顾: 其实求二叉树的深度可以有很多种方法,用递归的方式最简单,应用动态规划的思想,把一个大问题分解成为三个小问题,给出递归终结条件后递归。但是递归的问题在于堆栈上需要保存很多函数调用相关的地址变量等,需要消耗大量的堆栈空间,所以通过使用stack和循环,我们可以用相对复杂的代码实现同样的功能。引入新的结构体的目的在于mark结点于其深度的关系,否则在遍历树的过程中深度信息会丢失。本质上,这道题建立再遍历二叉树的基础上,因此也可以使用层序遍历来解决,需要使用队列并且在每层遍历结束时更新深度。

b. 给定根结点,算出最大的路径的之和。

View Code

回顾:求和最大的路径,很自然的想法就是分解成包含左结点的最长路径和包含又结点的最长路径,然后综合考虑各种情况,比较典型的分治套路,不多说。

c. 给定根节点,求二叉树的最小深度。

View Code

回顾:求最小层,很自然的想到层序遍历二叉树,但是这个题需要考虑到每一层遍历结束时候的状态要处理正确,尤其是改层最后一个结点有没有子节点的情况要想清楚。非递归的层序遍历,可以使用一个queue+marker的方式来标记一层遍历结束,也可以使用两个queue轮流push和pop,负责pop的queue变为空就表示一层的结点已经遍历完毕,然后互换pop queue和push queue,上面的代码还可以改进的更通俗易懂。

d. 给定根节点,从底向上打印结点值。

View Code

回顾:本质上还是层序遍历,从上到下还是从下到上其实并不重要,比如这道题虽然要求从下到上存储结点值,但我们还是从上到下遍历每层,只不过在插入层结点值的时候玩点花样而已。另外,标记一层结点遍历结束可以通过一个标记指针的方式完成,之前没有想到这一点,所以用了两个queue之类的方式来辅助,却显得复杂了。同样的思路可以解决的类似于给二叉树中的左右结点加link之类。

e. 给定二叉树根节点,根据中序遍历的顺序将其转化为链表(全部放在右子树结点)。

View Code
class Solution{
public:
    void flatTree(TreeNode* root) {
        if (!root) return; 
        TreeNode* tmp_root = root;
        while(tmp_root) {
            TreeNode* tmp_left = tmp_root->left;
            tmp_root->left = NULL;
            TreeNode* tmp_left_rightMost = tmp_left;
            while(tmp_left_rightMost->right) tmp_left_rightMost = tmp_left_rightMost->right; 
            tmp_left_rightMost->right = tmp_root->right;
            tmp_root->right = tmp_left;
            tmp_root = tmp_root->right;
        }
    }
}

回顾:变化二叉树为其他结构,主要是考虑如何合并子树,这个题就是简单的寻找了左子树的最右结点,然后合并子树进行遍历,思路比较简单。

f. 给定二叉树根节点,判断其是否是平衡树(所有结点的左右子节点的深度相差不超过1)。

View Code
class Solution{
    bool isBalance(TreeNode* root) {
        using std::queue;
        using std::stack;
        using std::map;
        if (!root) return true;
        queue<TreeNode*> q_node;
        stack<TreeNode*> s_node;
        map<TreeNode*, int> m_depth;
        q_node.push(root);
        while(!q_node.empty()) {
            TreeNode* tmp = q_node.front();
            q_node.pop();
            if (tmp->left == NULL && tmp->right == NULL) map[tmp] = 0;
            if (tmp->left) q_node.push(tmp->left); 
            if (tmp->right) q_node.push(tmp->right); 
            s_node.push(tmp); 
        }
        while(!s_node.empty()) {
            TreeNode* tmp = s_node.top();
            int leftDepth = tmp->left ? m_depth[tmp->left] : -1; 
            int rightDepth = tmp->right ? m_depth[tmp->right] : -1;
            int diff = leftDepth - rightDepth; 
            if (diff > 1 || diff < -1) return false;
            int depth = leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
            m_depth[tmp] = depth; 
        }
        return true;
}

回顾:判断平衡树需要从底层的结点开始,很自然的联想到二叉树的后续遍历,思路就是先遍历左右结点算出其高度,然后在根据其信息算出父节点的高度,依次向上类推。用递归实现后续遍历比较简单,但是为了节省堆栈,我还是选择用queue + stack的方式来后续遍历,然后将结点的高度信息存入一个map中。暂时没有想到办法可以用其他结构或者算法来更好的处理每个结点的高度信息,只好引入map,作为back up。

2. 二叉搜索树(binary search tree):由二叉树引申而来的树型结构,在保留二叉树所有属性的基础上,还要求:

  • 左子节点以及所有左子节点的子节点的键值小于该节点的键值。
  • 右子节点以及所有右子节点的子节点的键值大于该节点的键值。
  • 没有任何两个节点拥有同样的键值。
  • 左子节点和右子节点的分支树(sub-tree)也是二叉搜索树。

二叉树搜索树本质上是将键值有序的分配在一颗二叉树上,好处是在做搜索的过程中可以快速定位一个节点,比如根据父节点的键值就可以知道应该去左子节点还是右子节点去进一步查找,因为左子节点的键值<当前节点的键值<右子节点的键值。

相关题目:

a. 讲有序数组/有序单链表转化成平衡的二叉搜索树

View Code
//array
class Solution{
    //with sorted array
    TreeNode* sortedArrayToBST(vector<int>& num) {
         if (!num.size()) return NULL;
         return buildSubTree(num, 0, num.size() - 1); 
    }
    TreeNode* buildSubTree(vector<int>& num, int start, int end) {
        if (start > end) return NULL;
        if (start == end) return new TreeNode(num[start]); 
        int mid = start + ((end-start)>>1); 
        TreeNode* root = new TreeNode(num[mid]);
        TreeNode* left = buildSubTree(num, start, mid - 1); 
        TreeNode* right = buildSubTree(num, mid + 1, end); 
        root->left = left;
        root->right = right;
        return root;
    }

    //with sorted valued linked list
    TreeNode* sortedListToBST(ListNode* head) {
        if (!head) return NULL;
        return buildSubTree(head, countOfList(head)); 
    }
    TreeNode* buildSubTree(ListNode*& head, int cout) {
        if (!head) return NULL;
        if (n <= 0) return NULL;
        TreeNode* left = buildSubTree(head, (count>>1)); 
        TreeNode* root = new TreeNode(head->val); 
        head = head->next;
        TreeNode* right = buildSubTree(head, ((count - 1) >> 1)); 
        root->left = left;
        root->right = right;
        return root;
    }
}

回顾:对于有序数组重建,下标索引是最快速的方法,从根结点向下扩展。而链表重建的过程恰恰相反,有序遍历的有向性,我们只能从叶结点向根节点推进。而且值得注意的是链表中头结点的移动,每次使用递归的时候,链表的头结点几乎都是移动的,但是貌似这种方法的堆栈空间可能会很多。

b. 二叉搜索树中有两个结点被交换了,给定根节点,把交换的结点换回来。

View Code
class Solution {
    void recoverTree(TreeNode* root) {
        if (!root) return; 
        TreeNode* p1(NULL);
        TreeNode* p2(NULL);
        TreeNode* p1_next(NULL);
        TreeNode* prevNode(NULL); 
        TreeNode* tmp = root;
        stack<TreeNode*> stack;
        while(tmp || !stack.empty()) {
            if (tmp) {
                stack.push(tmp);
                tmp = tmp->left;
            }
            else {
                tmp = stack.top();
                stack.pop();
                if (prevNode) {
                    if (tmp->val < prevNode->val) {
                        if (!p1) {
                            p1 = prevNode;
                            p1_next = tmp;
                        }
                        else {
                            p2 = tmp;
                            break;
                         }
                    }
                }
                else prevNode = tmp; 
                tmp = tmp->right; 
            }
        }
        if (!p1) throw std::exception();
        if (!p2) p2 = p1_next;
        int tmpVal = p1->val;
        p1->val = p2->val;
        p2->val = tmpVal;
    }
}

回顾:由于二叉搜索树的特型,其中序遍历的结果应该是单调递增的,本题就是根据这个特型来找出递增数列中异常的两个数,然后将其值交换即可。需要特别注意的是如果两个相邻的数被交换,那么很有可能只找出一个异常,此时就需要在保存第一个异常的后结点。

c. 判断一颗二叉树是否是二叉搜索树(BST)

View Code
class Solution{
    bool isBST(TreeNode* root) {
        if (!root) return false;
        TreeNode* tmp = root;
        TreeNode* prev = NULL;
        stack<TreeNode*> stack;
        while(tmp || !stack.empty()) {
            if (tmp) {
                stack.push(tmp);
                tmp = tmp->left;
            }
            else {
                tmp = stack.top();
                stack.pop();
                if (prev) if (tmp->val <= prev->val) return false;
                prev = tmp;
                tmp = tmp->right;
            }
        }
        return true;
    }
}

回顾:由于二叉搜索树的结构特点,其中序遍历得出的数列应该是单调递增的数列,然后适当的保存节点信息就好。类似的题目可以是判断一颗二叉树是否是对称树,也是通过中序遍历然后查看结果数组是否是(奇数)回文数组即可。

posted on 2013-01-25 01:25  梁霄  阅读(302)  评论(0)    收藏  举报

导航