C++算法之指针三剑客二

1.数据结构介绍

作为(单)链表的升级版,我们通常接触的树都是二叉树(binary tree),即每个节点最多有两个子节点;且除非题目说明,默认树中不存在循环结构。LeetCode 默认的树表示方法如下。

struct TreeNode
{
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x):val(x),left(NULL),right(NULL){}
};

可以看出,其与链表的主要差别就是多了一个子节点的指针。

2.树的递归

对于一些简单的递归题,某些 LeetCode 达人喜欢写 one-line code,即用一行代码解决问题,把 if-else 判断语句压缩成问号冒号的形式。我们也会展示一些这样的代码,但是对于新手仍然建议您使用 if-else 判断语句。
在很多时候,树递归的写法与深度优先搜索的递归写法相同。

题目一:求一个二叉树的最大深度
在这里插入图片描述在这里插入图片描述
利用递归,可以很方便的求出最大深度。

int maxDepth(TreeNode* root)
{
    return root? 1+max(maxDepth(root->left),maxDepth(root->right)):0;
}

题目二:判断一个二叉树是否平衡。树平衡的定义是,对于树上的任意节点,其两侧节点的最大深度
的差值不得大于 1。
在这里插入图片描述
解法类似于求树的最大深度,但有两个不同的地方:一是我们需要先处理子树的深度再进行比较,二是如果我们在处理子树时发现其已经不平衡了,则可以返回一个-1,使得所有其长辈节点可以避免多余的判断(本题的判断比较简单,做差后取绝对值即可;但如果此处是一个开销较大的比较过程,则避免重复判断可以节省大量的计算时间)。

//主函数
bool isBalanced(TreeNode* root)
{
    return helper(root) != -1;
}
//辅函数
int helper(TreeNode* root)
{
    if(!root)
    {
        return 0;
    }
    int left = helper(root->left),right = helper(root->right);
    if(left == -1 || right == -1 || abs(left - right) > 1)
    {
        return -1;
    }
    return 1 + max(left,right);
}

题目三:求一个二叉树的最长直径。直径的定义是二叉树上任意两节点之间的无向距离。
在这里插入图片描述
同样的,我们可以利用递归来处理树。解题时要注意,在我们处理某个子树时,我们更新的最长直径值和递归返回的值是不同的。这是因为待更新的最长直径值是经过该子树根节点的最长直径(即两侧长度);而函数返回值是以该子树根节点为端点的最长直径值(即一侧长度),使用这样的返回值才可以通过递归更新父节点的最长直径值)。

//主函数
int diameterOfBinaryTree(TreeNode* root)
{
    int diameter = 0;
    helper(root,diameter);
    return diameter;
}
//辅函数
int helper(TreeNode* node,int& diameter)
{
    if(!node)
    {
        return 0;
    }
    int l = helper(node->left,diameter), r = helper(node->right,diameter);
    diameter = max(l+r,diameter);
    return max(l,r)=1;
}

题目四:给定一个整数二叉树,求有多少条路径节点值的和等于给定值。
在这里插入图片描述
递归每个节点时,需要分情况考虑:(1)如果选取该节点加入路径,则之后必须继续加入连续节点,或停止加入节点(2)如果不选取该节点加入路径,则对其左右节点进行重新进行考虑。因此一个方便的方法是我们创建一个辅函数,专门用来计算连续加入节点的路径。

//主函数
int pathSum(TreeNode* root,int sum)
{
    return root? pathSumStartWithRoot(root,sum) + 
        pathSum(root->left,sum) + pathSum(root->right,sum): 0;
}
//辅函数
int pathSumStartWithRoot(TreeNode* root,int sum)
{
    if(!root)
    {
        return 0;
    }
    int count = root->val == sum? 1:0;
    count += pathSumStartWithRoot(root->left,sum - root->val);
    count += pathSumStartWithRoot(root->right,sum - root->val);
    return count;
}

题目五:判断一个二叉树是否对称
在这里插入图片描述
判断一个树是否对称等价于判断左右子树是否对称。一般习惯将判断两个子树是否相等或对称类型的题的解法叫做“四步法”:(1)如果两个子树都为空指针,则它们相等或对称(2)如果两个子树只有一个为空指针,则它们不相等或不对称(3)如果两个子树根节点的值不相等,则它们不相等或不对称(4)根据相等或对称要求,进行递归处理。

//主函数
bool isSymmetric(TreeNode *root)
{
    return root? isSymmetric(root->left,root->right):true;
}
//辅函数
bool isSymmetric(treeNode* left,TreeNode* right)
{
    if(!left && !right)
    {
        return true;
    }
    if(!left || !right)
    {
        return false;
    }
    if(left->val != right->val)
    {
        return false;
    }
    return isSymmetric(left->left,right->right) && isSymmetric(left->right,right->left);
}

题目六:给定一个整数二叉树和一些整数,求删掉这些整数对应的节点后,剩余的子树。

例如:输入是一个整数二叉树和一个一维整数数组,输出一个数组,每个位置存储一个子树(的根节点)
在这里插入图片描述
这道题最主要需要注意的细节是如果通过递归处理原树,以及需要在什么时候断开指针。同时,为了便于寻找待删除节点,可以建立一个哈希表方便查找。

//主函数
vector<TreeNode*>delNodes(TreeNode* root,vector<int>&to_delete)
{
    vector<TreeNode*>forest;
    unordered_set<int>dict(to_delete.begin(),to_delete.end());
    root = helper(root,dict,forest);
    if(root)
    {
        forest.push_back(root);
    }
    return forest;
}
//辅函数
TreeNode* helper(TreeNode* root,unordered_set<int>&dict,vector<TreeNode*>&forest)
{
    if(!root)
    {
        retrun root;
    }
    root->left = helper(root->left,dict,forest);
    root->right = helper(root->right,dict,forest);
    if(dict.count(root->val))
    {
        if(root->left)
        {
            forest.push_back(root->left);
        }
        if(root->right)
        {
            forest.push_back(root->right);
        }
        root = NULL;
    }
    return root;
}

3.层次遍历

我们可以使用广度优先搜索进行层次遍历。注意,不需要使用两个队列来分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。

题目一:给定一个二叉树,求每一层的节点值的平均数
在这里插入图片描述
利用广度搜索可以求取每层的平均值

vector<double>averageOfLevels(TreeNode* root)
{
    vector<double>ans;
    if(!root)
    {
        return ans;
    }
    queue<TreeNode*>q;
    q.push(root);
    while(!q.empty())
    {
        int count=q.size();
        double sum=0;
        for(int i=0;i<count;i++)
        {
            TreeNode* node = q.front();
            q.pop();
            sum += node->val;
            if(node->left)
            {
                q.push(node->left);
            }
            if(node->right)
            {
                q.push(node->right);
            }
        }
        ans.push_back(sum/count);
    }
    return ans;
}
              

4.前中后序遍历

在这里插入图片描述
前序遍历先遍历父结点,再遍历左结点,最后遍历右节点,我们得到的遍历顺序是 [1 2 4 5 3 6]。

void inorder(TreeNode* root)
{
    visit(root);
    preorder(root->left);
    preorder(root->right);
}

中序遍历先遍历左节点,再遍历父结点,最后遍历右节点,我们得到的遍历顺序是 [4 2 5 1 6]。

void inorder(TreeNode* root)
{
    inorder(root->left);
    visit(root);
    inorder(root->right);
}

后序遍历先遍历左节点,再遍历右结点,最后遍历父节点,我们得到的遍历顺序是 [4 5 2 6 3 1]。

void postorder(TreeNode* root)
{
    postorder(root->left);
    postorder(root->right);
    visit(root);
}

题目一:给定一个二叉树的前序遍历和中序遍历结果,尝试复原这个树。已知树里不存在重复值的节点。
在这里插入图片描述
前序遍历的第一个节点是 4,意味着 4 是根节点。我们在中序遍历结果里找到 4 这个节点,根据中序遍历的性质可以得出,4 在中序遍历数组位置的左子数组为左子树,节点数为 1,对应的是前序排列数组里 4 之后的 1 个数字(9);4 在中序遍历数组位置的右子数组为右子树,节点数为 3,对应的是前序排列数组里最后的 3 个数字。有了这些信息,我们就可以对左子树和右子树进行递归复原了。为了方便查找数字的位置,我们可以用哈希表预处理中序遍历的结果。

//主函数
TreeNode* buildTree(vector<int>&preorder,vector<int>&inorder)
{
    if(preorder.empty())
    {
        return nullptr;
    }
    unordered_map<int,int>hash;
    for(int i=0;i<preorder.size();++i)
    {
        hash[inorder[i]]=i;
    }
    return buildTreeHelper(hash,preorder,0,preorder.size()-1,0);
}
//辅函数
TreeNode* buildTreeHelper(unordered_map<int,int>&hash,vector<int>&preorder,int s0,int e0,int s1)
{
    if(s0>e0)
    {
        return nullptr;
    }
    int mid=preorder[s1],index=hash[mid],leftLen=index-s0-1;
    TreeNode* node = new TreeNode(mid);
    node->left=buildTreeHelper(hash,preorder,s0,index-1,s1+1);
    node->right=buildTreeHelper(hash,preorder,index+1,e0,s1+2+leftLen);
    return node;
}

题目二:不使用递归,实现二叉树的前序遍历
在这里插入图片描述
因为递归的本质是栈调用,因此我们可以通过栈来实现前序遍历。注意入栈的顺序。

vector<int>preorderTraversal(TreeNode* root)
{
    vector<int>ret;
    if(!root)
    {
        return ret;
    }
    stack<TreeNode*>s;
    s.push(root);
    while(!s.empty())
    {
        TreeNode* node=s.top();
        s.pop();
        ret.push_back(node->val);
        if(node->right)
        {
            s.push(node->right);  //先右后左,保证左子树先遍历
        }
        if(node->left)
        {
            s.push(node->left);
        }
    }
    return ret;
}
        

5.二叉查找树

二叉查找树(Binary Search Tree, BST)是一种特殊的二叉树:对于每个父节点,其左子节点的值小于等于父结点的值,其右子节点的值大于等于父结点的值。因此对于一个二叉查找树,我们可以在 O(n log n) 的时间内查找一个值是否存在:从根节点开始,若当前节点的值大于查找值则向左下走,若当前节点的值小于查找值则向右下走。同时因为二叉查找树是有序的,对其中序遍历的结果即为排好序的数组。
在这里插入图片描述
一个二叉查找树的实现如下。

template<class T>
class BST
{
    struct Node{
        T data;
        Node* left;
        Node* right;
    }
    Node* root;
    Node* makeEmpty(Node* t)
    {
        if(t==NULL) return NULL:
        makeEmpty(t->left);
        makeEmpty(t->right);
        delete t;
        return NULL;
    }
    Node* insert(Node* t,T x)
    {
        if(t==NULL)
        {
            t=new Node;
            t->data=x;
            t->left=t->right=NULL;
        }
        else if(x<t->data){
            t->left=insert(t-left,x);
        }
        else if(x>t->data){
            t->right=insert(t->right,x);
        }
        return t;
    }
    Node* find(Node* t,T x)
    {
        if(t==NULL) return NULL;;
        if(x<t->data) return find(t->left,x);
        if(x>t-data) return find(t->right,x);
        return t;
    }
    Node* findMin(Node* t)
    {
        if(t==NULL || t->left==NULL) return t;
        return findMin(t->left);
    }
    Node* findMax(Node* t)
    {
        if(t==NULL || t->right==NULL) return t;
        return findMax(t->right);
    }
    Node* remove(Node* t,T x)
    {
        Node* temp;
        if(t==NULL) return NULL;
        else if(x<t->data) t->left=remove(t->left,x);
        else if(x>t->data) t-right=remove(t->right,x);
        else if(t->left && t->right){
            temp=findMin(t->right);
            t->data=temp->data;
            t->right=remove(t->right,t->data);
       }
       else{
           temp=t;
           if(t->left==NULL) t=t->right;
           else if(t->right==NULL) t=t->left;
           delete temp;
       }
       return t;
   }
public:
   BST():root(NULL){}
   ~BST(){
       root=makeEmpty(root);
   }
   void insert(T x)
   {
       insert(root,x);
   }
   void  remove(T x)
   {
       remove(root,x);
   }
};
                    

题目一:给定一个二叉查找树,已知有两个节点被不小心交换了,试复原此树。

输入是一个被误交换两个节点的二叉查找树,输出是改正后的二叉查找树。
在这里插入图片描述
我们可以使用中序遍历这个二叉查找树,同时设置一个 prev 指针,记录当前节点中序遍历时的前节点。如果当前节点大于 prev 节点的值,说明需要调整次序。有一个技巧是如果遍历整个序列过程中只出现了一次次序错误,说明就是这两个相邻节点需要被交换;如果出现了两次次序错误,那就需要交换这两个节点。

//主函数
void recoverTree(TreeNode* root)
{
    TreeNode *mistake1 = nullptr, *mistake2 = nullptr, *prev = nullptr;
    inorder(root,mistake1,mistake2,prev);
    if(mistake1 && mistake2)
    {
        int temp = mistake1->val;
        mistake1->val = mistake2->val;
        mistake2->val = temp;
    }
}
//辅函数
void inorder(TreeNode* root,TreeNode*& mistake1,TreeNode*&mistake2,TreeNode*&prev)
{
    if(!root)
    {
        return;
    }
    if(root->left)
    {
        inorder(root->left,mistake1,mistake2,prev);
    }
    if(prev&&root->val < prev->val)
    {
        if(!mistake1)
        {
            mistake1=prev;
            mistake2=root;
        }
        else{
            mistake2=root;
        }
        cout<<mistake1->val;
        cout<<mistake2->val;
    }
    prev=root;
    if(root->right)
    {
        inorder(root->right,mistake1,mistake2,prev);
    }
}

题目二:给定一个二叉查找树和两个整数 L 和 R,且 L < R,试修剪此二叉查找树,使得修剪后所有
节点的值都在 [L, R] 的范围内。
在这里插入图片描述
利用二叉查找树的大小关系,我们可以很容易地利用递归进行树的处理。

TreeNode* trimBST(TreeNode* root,int L,int R)
{
    if(!root){
        return root;
    }
    if(root->val > R)
    {
        return trimBST(root->left,L,R);
    }
    if(root->val < L)
    {
        return trimBST(root->right,L,R);
    }
    root->left = trimBST(root->left,L,R);
    root->right=trimBST(root->right,L,R);
    return root;
}

6.字典树

字典树(Trie)用于判断字符串是否存在或者是否具有某种字符串前缀。
在这里插入图片描述
假如我们有一个储存了近万个单词的字典,即使我们使用哈希,在其中搜索一个单词的实际开销也是非常大的,且无法轻易支持搜索单词前缀。然而由于一个英文单词的长度 n 通常在 10 以内,如果我们使用字典树,则可以在 O(n)——近似 O(1)的时间内完成搜索,且额外开销非常小。

题目一:尝试建立一个字典树,支持快速插入单词、查找单词、查找单词前缀的功能。
在这里插入图片描述
以下是字典树的典型实现方法。

class TrieNode
{
public:
    TrieNode* childNode[26];
    bool isVal;
    TrieNode(): isVal(false){
        for(int i=0;i,26++i)
        {
            childNode[i]=nullptr;
        }
    }
};

class Trie{
    TrieNode* root;
public:
    Trie(): root(new TrieNode()){}
    //向字典插入一个词
    void insert(string word)
    {
        TrieNode* temp=root;
        for(int i=0;i<word.size();++i)
        {
            if(!temp->childNode[word[i]-'a']
            {
                temp->childNode[word[i]-'a']=new TrieNode();
            }
            temp=temp->childNode[word[i]-'a'];
        }
        temp->isVal=true;
    }
    //判断字典是否有一个词
    bool search(string word)
    {
        TrieNode* temp = root;
        for(int i=0;i<word.size();++i)
        {
            if(!temp)
            {
                break;
            }
            temp=temp->childNode[word[i]-'a'];
        }
        return temp? temp->isVal:false;
    }
    //判断字典树是否有一个以词开始的前缀
    bool startWith(string prefix)
    {
        TrieNode* temp=root;
        for(int i=0;i<prefix.size();++i)
        {
            if(!temp)
            {
                break;
            }
            temp=temp->childNode[prefix]-'a'];
        }
        return temp;
    }
}     
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

明月醉窗台

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值