子串子序列常见算法面试题

子串子序列

本人最近被一系列子串子序列递增最长连续 个数 和 之类的问题逼疯,特此整理

基础判断

首先要知道判断是否是子串还是子序列

判断子串

子串比较简单,因为是连续的

判断子序列

子序列其实也很简单,不用想的复杂,贪心就好

越早匹配到越好,只要匹配到一个就可以往后面走 leetcode392

class Solution {
    public boolean isSubsequence(String s, String t) {
        int n = s.length(), m = t.length();
        int i = 0, j = 0;
        while (i < n && j < m) {
            if (s.charAt(i) == t.charAt(j)) {
                i++;
            }
            j++;
        }
        return i == n;
    }
}
//时间复杂度:O(n+m) 空间复杂度:O(1)

题目

题目的话其实一般都是考察子序列的,因为子序列难度会大一些,子串实际上没什么意义。

单个字符串中的判断

求出所有递增子序列

leetcode491

关于这种找出所有的排列组合的问题,真的二进制十分十分重要,而且其实实现起来也十分的简单,就是要熟练掌握。

核心思想是:set一共有n个元素,转换为n位二进制,0表示该元素没有取,1表示该元素取了,然后判断的范围就是从0-2n,然后将每个数字 不断右移跟1比较或者是跟 i左移多少位 来比较,可以判断是否是1,从而判断该元素是否获取,然后就可以拿到所有的情况

这题的难度是如何来消除重复!

  • 基本想法是找出每一个递增子序列然后来判断是否重复,是否重复其实有很多方法,如果最傻的循环判断也不是不行,但是这里的解法是自定义了一个哈希来判断是否重复
  • 但是这题最好的方法应该是dfs+剪枝,而且十分巧妙如何去重
    • 如果向前位置的元素≥上一个元素 则需要dfs加入这个元素的情况
    • 那么对于做dfs不加入这个元素的情况呢
      • 2 2 都取
      • 2取 2不取 (重复了,所以如果当前元素跟前一个元素一样了话,那当前位置就不考虑不取了,因为会出现重复
      • 2不取 2取
      • 2不取 2不取
class Solution {
public:
    vector<int> temp; 
    vector<vector<int>> ans;

    void dfs(int cur, int last, vector<int>& nums) {
        if (cur == nums.size()) {
            if (temp.size() >= 2) {
                ans.push_back(temp);
            }
            return;
        }
        if (nums[cur] >= last) {
            temp.push_back(nums[cur]);
            dfs(cur + 1, nums[cur], nums);
            temp.pop_back();
        }
        if (nums[cur] != last) {
            dfs(cur + 1, last, nums);
        }
    }

    vector<vector<int>> findSubsequences(vector<int>& nums) {
        dfs(0, INT_MIN, nums);
        return ans;
    }
};

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/increasing-subsequences/solution/di-zeng-zi-xu-lie-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

求出 所有不重复的子序列的个数

leetcode940

这题有一种仿佛看题很简单的感觉,因为是求所有子序列,那全部dfs就好了,还不用判断递增,但是需要判断是否重复,(这个时间复杂度应该很夸张)仔细想一下,这肯定不可能是求出所有的满足条件的子序列了,因为数字会很大,只会是求个数,要求取余。

  • 求个数的话,还是会想到dp,因为该位置的满足条件的序列一定跟前一个位置有关。
  • 不考虑重复的话,很简单dp[i]=2*dp[i-1],dp[i]表示0-i元素可以组成的子序列的数目,就是num[i]加或者num[i]不加,因此是两倍
  • 但如果考虑重复的话,需要好好思考的一下的。比如现在要插入元素b,那么如果之前就出现过元素b的话,之前b没插入,和现在b插入就会重复(这一部分有点乱,需要灵光一现领悟的那种
    • 首先是 2*dp[i-1],即所有的情况都算进去了,包括每个位置上面取或者不取的
    • 然后要减掉重复的元素,什么重复呢,就是上次没放这次放了,那跟上次放了这次没放会有重复,因此要减掉重复的部分,上次放一共有多少情况呢,那是dp[last[s[k]]-1]种情况
class Solution {
    public int distinctSubseqII(String S) {
        int MOD = 1_000_000_007;
        int N = S.length();
        int[] dp = new int[N+1];
        dp[0] = 1;

        int[] last = new int[26];
        Arrays.fill(last, -1);

        for (int i = 0; i < N; ++i) {
            int x = S.charAt(i) - 'a';
            dp[i+1] = dp[i] * 2 % MOD;
            if (last[x] >= 0)
                dp[i+1] -= dp[last[x]];
            dp[i+1] %= MOD;
            last[x] = i;
        }

        dp[N]--;
        if (dp[N] < 0) dp[N] += MOD;
        return dp[N];
    }
}

作者:LeetCode
链接:https://leetcode-cn.com/problems/distinct-subsequences-ii/solution/bu-tong-de-zi-xu-lie-ii-by-leetcode/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

最长递增子序列的长度是?

这就是经典dp,也是基础dp,dp[i]表示以元素i结尾的递增子序列的最长长度,因此其实是双重循环来判断的,dp[i]实际上是 由前面所有满足元素大小关系的 最长dp[j]+1; leetcode300

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
      int n= nums.size();
        if(n==0){
            return 0;
        }
        //dp[i]表示 0-i中最长的递增子序列
        vector<int> dp(n);
        
        for(int i=0;i<n;i++){
            dp[i]=1;
            for(int j=0;j<i;j++)
            {
                if(nums[i]>nums[j]){
                    dp[i]=max(dp[i],dp[j]+1);
                }
            }
        }
        
        return *max_element(dp.begin(),dp.end());
    }
};

其实该算法有时间复杂度更小一点的方法:贪心+二分查找,不过总是记不住怎么做罢了。面试和笔试的时候写出dp的基本就好了。

考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。

在这一题目的基础上,有两道变形,其实道理都是一样的,只是需要暂时存的内容会有所变化

最长递增子序列的个数是?

leetcode673

因为此时需要记录最长递增子序列的个数,之前dp[i]只保存了最长递增子序列的长度,因此对应还需要一个cnt的数组来保存个数,同时注意更新的时候,即长度发生变化的时候,cnt也要对应发生变化。

  • 如果dp[j]+1>dp[i] 则i位置的最长长度应为dp[j]+1,且个数应该跟cnt[i]相同
  • 如果dp[j]+1==dp[i],则i位置的最长长度还是dp[i],但是个数的话是两者相加cnt[i]+ cnt[j]
  • 每个位置dp[i]判断完后,再跟maxlen进行判断,决定是否更新ans
int findNumberOfLIS(vector<int>& nums) {
        int n=nums.size();
        vector<int> dp(n,1);
        vector<int> cnt(n,1);
    
        int maxlen=1;
        int ans =1;
        for(int i=1;i<n;i++)
        {
            int tmp=0;
            for(int j=i-1;j>=0;j--)
            {
                if(nums[i]>nums[j])
                {
                    if(dp[j]+1>dp[i])
                    {
                        dp[i]=dp[j]+1;
                        cnt[i]=cnt[j];
                    }else if(dp[j]+1==dp[i])
                    {
                        cnt[i]+=cnt[j];
                    }

                }
            }

            if(dp[i]>maxlen)
            {
                maxlen = dp[i];
                ans=cnt[i];
            }else if(dp[i]==maxlen)
            {
                ans+=cnt[i];
            }

        }
        return ans;
    }

最长递增子序列是?

这题我印象中是有原题的,但是一直没能搜到,也是在面试中遇到过的题目,当时太傻了没有做出来,但其实还是很简单的,核心思想是不变的,转移方程也是一样的,只是需要再多一个变量来存放当前的数组是什么样子的,vector来暂存就好了,如果dp[i]=dp[j]+1,那么vector[i]=vector[j]+元素i,所以其实还是在leetcode300的基础上面来做的

#include <iostream>
#include <vector>
using namespace std;

class Solution {
public:
    vector<int> lengthOfLIS(vector<int>& nums) {
        int n= nums.size();
        if(n==0){
            return vector<int>();
        }
        //dp[i]表示 0-i中最长的递增子序列
        vector<int> dp(n);
        vector<vector<int>> cur_vector(n);
        for(int i=0;i<n;i++)
        {
            cur_vector[i].push_back(nums[i]);
        }
        cout<<"1"<<endl;
        for(int i=0;i<n;i++){
            dp[i]=1;
            for(int j=0;j<i;j++)
            {
                if(nums[i]>nums[j]){
                    if(dp[j]+1>dp[i])
                    {
                        dp[i]=dp[j]+1;
                        cur_vector[i]=cur_vector[j];
                        cur_vector[i].push_back(nums[i]);
                    }
                }
            }
        }
        cout<<"2"<<endl;
        int index=0;
        int maxx=0;
        for(int i=0;i<n;i++)
        {
            if(dp[i]>maxx)
            {
                maxx=dp[i];
                index=i;
            }
        }
        cout<<"3"<<endl;
        cout<<index<<endl;
        return cur_vector[index];
    }
};

int main()
{
    vector<int> nums{10,9,2,5,3,7,101,18};
    vector<int> ans;
     cout<<"4"<<endl;
    ans = Solution().lengthOfLIS(nums);
     cout<<"5"<<endl;
    for(int i=0;i<ans.size();i++)
    {
        cout<<ans[i]<<" ";
    }
    return 0;
}

最长和谐子序列

leetcode594

做这种子序列的题目最怕的就是思维定式,总感觉需要dp来做,但是如果题目有所变化的话,要学会如何进行转型,和谐子序列的意思是这个序列当中 最大与最小的元素相差不多于1,如果这题还想着dfs来找子序列的话,时间复杂度太高了。。当然也可以做,虽然有相当枚举的方法,但是看评论之后发现,这道题目其实就是求相邻元素个数罢了

有双指针的解法,也有哈希的解法

public class Solution {
    public int findLHS(int[] nums) {
        HashMap < Integer, Integer > map = new HashMap < > ();
        int res = 0;
        for (int num: nums) {
            map.put(num, map.getOrDefault(num, 0) + 1);
        }
        for (int key: map.keySet()) {
            if (map.containsKey(key + 1))
                res = Math.max(res, map.get(key) + map.get(key + 1));
        }
        return res;
    }
}

作者:LeetCode
链接:https://leetcode-cn.com/problems/longest-harmonious-subsequence/solution/zui-chang-he-xie-zi-xu-lie-by-leetcode/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

递增的三元子序列

leetcode334

求是否存在递增的长度为3的子序列,dfs笨方法做当然是可以的。但是这题很巧妙的方法是题解中有的,主要是贪心,small的值尽可能的小,然后再存一个mid值,如果存在一个值比mid和small都大的话,则说明存在这样子的三元子序列

class Solution {
public:
    bool increasingTriplet(vector<int>& nums) {
        int small=INT_MAX,mid=INT_MAX;
        for(int n:nums){
            if(n<=small){
                small=n;
            }else if(n<=mid){
                mid=n;
            }else if(n>=mid){
                return true;
                break;
            }
        }
        return false;
    }
};

多个字符串的判断

这主要是两个字符串之间进行转换,比如删除增加替换。然后最小的多少,那就是二维dp

或者是求公共子序列的问题,这也是常常会遇到的题目

最长公共子序列

https://leetcode-cn.com/problems/qJnOS7/

经典题目了真的是,经典二维dp

  • dp[i][j]表示 s 0→i-1, t 0→j-1字符匹配的最长公共子序列的长度
  • 如果s[i-1]==t[j-1], 那么就可以把该位置算进去 dp[i][j]=dp[i-1][j-1]+1
  • 如果不同的话,取最大的 dp[i][j]=max(dp[i-1][j],dp[i][j-1])
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length(), n = text2.length();
        int[][] dp = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++) {
            char c1 = text1.charAt(i - 1);
            for (int j = 1; j <= n; j++) {
                char c2 = text2.charAt(j - 1);
                if (c1 == c2) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[m][n];
    }
}

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/qJnOS7/solution/zui-chang-gong-gong-zi-xu-lie-by-leetcod-ugg7/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

判断子序列的数目,t在s的子序列中出现的次数

https://leetcode-cn.com/problems/21dk04/

或者说是判断一个指定字符串在 源字符串中可能出现的次数

依旧是动态规划最主要是状态如何来建立 dp[i][j]表示 target[j:t-1]字符串在source[i:s-1]的子序列中出现的次数

  • j为t时,则target为空,在dp[i][t]=1; 当i为s是,则source为空所以 dp[s][j]=0;
  • 当s[i]=t[j],
    • 如果他两匹配则,target这个位置可以匹或者不匹
    • 如果不匹配,则target这个位置只能是不匹
class Solution {
    public int numDistinct(String s, String t) {
        int m = s.length(), n = t.length();
        if (m < n) {
            return 0;
        }
        int[][] dp = new int[m + 1][n + 1];
        for (int i = 0; i <= m; i++) {
            dp[i][n] = 1;
        }
        for (int i = m - 1; i >= 0; i--) {
            char sChar = s.charAt(i);
            for (int j = n - 1; j >= 0; j--) {
                char tChar = t.charAt(j);
                if (sChar == tChar) {
                    dp[i][j] = dp[i + 1][j + 1] + dp[i + 1][j];
                } else {
                    dp[i][j] = dp[i + 1][j];
                }
            }
        }
        return dp[0][0];
    }
}

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/21dk04/solution/zi-xu-lie-de-shu-mu-by-leetcode-solution-l8v1/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

——————————————————————————
以后遇到类似的问题,再来及时归纳整理 2021.11.1

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值