高级字符串算法

72. 编辑距离

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
示例 2:
输入:word1 = “intention”, word2 = “execution”
输出:5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)

dp[i][j] // word1.substr(0, i)与word2.substr(0, j)之间的编辑距离
word1:""
word2:""
dp[i][j] 代表 word1 到 i 位置转换成 word2 到 j 位置需要最少步数

所以,
当 word1[i] == word2[j],dp[i][j] = dp[i-1][j-1];
当 word1[i] != word2[j],dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1
其中,dp[i-1][j-1] 表示替换操作,dp[i-1][j] 表示删除操作,dp[i][j-1] 表示插入操作。
注意,针对第一行,第一列要单独考虑,我们引入 ‘’ 下图所示:

在这里插入图片描述
第一行,是 word1 为空变成 word2 最少步数,就是插入操作
第一列,是 word2 为空,需要的最少步数,就是删除操作

class Solution(object):
    def minDistance(self, word1, word2):
        n1 = len(word1)
        n2 = len(word2)
        dp = [[0] * (n2 + 1) for _ in range(n1 + 1)]
        # 第一行
        for j in range(1, n2 + 1):
            dp[0][j] = dp[0][j-1] + 1
        # 第一列
        for i in range(1, n1 + 1):
            dp[i][0] = dp[i-1][0] + 1
        for i in range(1, n1 + 1):
            for j in range(1, n2 + 1):
                if word1[i-1] == word2[j-1]:
                    dp[i][j] = dp[i-1][j-1]
                else:
                    dp[i][j] = min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1] ) + 1
        #print(dp)      
        return dp[-1][-1]

1143. 最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace”,它的长度为 3。
示例 2:
输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc”,它的长度为 3。
示例 3:
输入:text1 = “abc”, text2 = “def”
输出:0
解释:两个字符串没有公共子序列,返回 0。
提示:
1 <= text1.length <= 1000
1 <= text2.length <= 1000

子序列和子串的区别:子序列之间可以有间隔,而子串是连续的
动态转移方程:如果s1和s2某个字符相同,则说明已经找到了一个共同的字符,那么test1和test2的下标都要减少一位,因为第i个字符和第j个字符已经被用,否则不同的话,要不就是i删掉一个字符,或者j删掉一个字符,注意这里不能同时删掉,因为两者是不相同的
dp[i][j] = dp[i-1][j-1] + 1 (if s1[i-1] == s2[j-1])
else dp[i][j] = max(dp[i-1][j], dp[i][j-1])

substring
public String substring(int beginIndex,int endIndex)
返回一个新字符串,它是此字符串的一个子字符串。
该子字符串从指定的 beginIndex 处开始,一直到索引 endIndex - 1 处的字符。
因此,该子字符串的长度为 endIndex-beginIndex。
示例:
“hamburger”.substring(4, 8) returns “urge”
“smiles”.substring(1, 5) returns “mile”
参数:
beginIndex - 开始处的索引(包括)。
endIndex - 结束处的索引(不包括)。
返回:
指定的子字符串。

 def longestCommonSubsequence(self, text1, text2):
        if not text1 or not text2:
            return 0
        m = len(text1)
        n = len(text2)
        dp = [[0]*(n+1) for _ in range(m+1)]
        for i in range(1, m+1):
            for j in range(1, n+1):
                if text1[i-1] == text2[j-1]:
                    dp[i][j] = 1 + dp[i-1][j-1]
                else:
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1])
        return dp[m][n]

在这里插入图片描述

  1. Longest common substring (最长子串)
    dp[i][j] = dp[i-1][j-1] + 1 (if s1[i-1] == s2[j-1])
    else dp[i][j] = 0 // 此位置没有公共子串,最后的值是在所有位置中找到最大值即可

5. 最长回文子串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。
示例 2:
输入: “cbbd”
输出: “bb”

1 嵌套循环,枚举i(起点), j(终点),判断该子串是否回文
2 中间向两边扩张法
3 DP[i][j]

public String longestPalindrome(String s) {
	int len = s.length();
	if (len < 2) {
		return s;
	}
	int maxLen = 1;// 第一个字符是自己的回文子串
	int begin = 0;
	// s.charAt(i)每次都会检查数组下标越界,因此先转换成字符数组,这一步非必须
	char[] charArray = s.toCharArray();
	// 枚举所有长度严格大于1的子串 charArray[i..j]
	for (int i = 0; i < len - 1; i++) {
		
		for (int j = i+1; j < len; j++) {
			if (j - i + 1 > maxLen && validPalindromic(charArray, i, j)) {
				maxLen = j - i + 1;	
				begin = i; // 必须写到这里(记录当前回文子串开始的位置),因为如果写到该循环外面的话,i是一直走的,所以begin = i肯定到最后i 为len - 1了,
			}
		}
	}
	return s.substring(begin, begin + maxLen);
}
// 验证子串s[left...right]是否为回文串
private boolean validPalindromic(char[] charArray, int left, int right) {
	while(left < right) {
		if (charArray[left] != charArray[right]) {
			return false;
		}
			left++;
			right--;	
	}
	return true;
}

// 中心扩散
在这里插入图片描述
在这里插入图片描述

public String longestPalindrome(String s) {
	int len = s.length();
	if (len < 2) {
		return s;
	}
	int maxLen = 1;// 第一个字符是自己的回文子串
	int begin = 0;
	// s.charAt(i)每次都会检查数组下标越界,因此先转换成字符数组,这一步非必须
	char[] charArray = s.toCharArray();
	// 枚举所有长度严格大于1的子串 charArray[i..j]
	for (int i = 0; i < len - 1; i++) {
		int oddLen = expandAroundCenter(charArray, i, i);
		int evenLen = expandAroundCenter(charArray, i, i + 1);
		int curMaxLen = Math.max(oddLen, evenLen);
		if (curMaxLen > maxLen) {
			maxLen = curMaxLen;
			// 这一步要在纸上画图发现规律:i为当前回文子串中心的下标,(maxLen - 1) / 2是回文子串长度所对应的中心下标,当前的下标减去实际的下标就是回文子串的起始点
			begin = i - (maxLen - 1) / 2;
		}
	}
	return s.substring(begin, begin + maxLen);
}


//或者
  public String longestPalindrome2(String s) {
        if (s == null || s.length() < 1) return "";
        int start = 0, end = 0;
        for (int i = 0; i < s.length(); i++) {
            int len1 = expandAroundCenter(s, i, i);
            int len2 = expandAroundCenter(s, i, i + 1);
            int len = Math.max(len1, len2);
            if (len > end - start) {
                start = i - (len - 1) / 2;
                end = i + len / 2;
            }
        }
        return s.substring(start, end + 1);
    }


// 验证子串s[left...right]是否为回文串
/**
@param charArray 原始字符串的字符数组
@param left 起始左边界(可以取到)
@param right 起始有边界(可以取到)
@return 回文串的长度
*/
private int expandAroundCenter(char[] charArray, int left, int right) {
	// 当left == right 回文中心是一个字符,回文串的长度是奇数
	// 当right = left + 1 ,回文中心是两个字符,回文串的长度是偶数
	int len = charArray.length;
	int i = left;
	int j = right;
	while(i >= 0 && j < len) {
		if(charArray[i] == charArray[j]) {
			i--;
			j++;
		}else{
		   break;
		}
	}
	return j - i + 1 - 2;  // j - i + 1是回文串的长度包含了i和j所对应的元素.,
	// 跳出while循环,是满足s.charAt(i) != s.charAt(j),
	// 退出循环后i和j中间的部分是回文子串不包含i和j指向的字符, 回文串的长度是j - i + 1 - 2 = j - i - 1
}

方法三:DP

定义dp(i, j) = true; s[i, j]是回文串;dp(i, j) = false; s[i, j]不是回文串
如果i和j相等,或者i和j相差一位的话,说明子串长度为0,或者为1,肯定是回文串
if i ==j or j - i + 1
接下来只要是s[i] == sj[]是i和j不断的向外扩散, i每次+1,j每次-1,就是将ture传递出去
初始状态是空或者长度为1的子串,看能不能向上扩展到更长更大的子串

/*
枚举i(起点), j(终点)
*/
public String longestPalindrome(String s) {
	int n = s.length();
	String res = "";
	boolean[][] dp = new boolean[n][n];
	for (int i = n - 1; i >= 0; i--) {
		for (int j = i; j < n; j++) {
		// 只要 s.charAt(i) == s.charAt(j)就可以吧dp[i][j]扩张出去 且(dp[i+1][j-1]也是true的情况下即子串也是回文串或者是空或者长度为1的子串)
			dp[i][j] = s.charAt(i) == s.charAt(j) && (j-i < 2 || dp[i+1][j-1]);
			if (dp[i][j] && j - i + 1 > res.length()) {
				res = s.substring(i, j+1);
			}
		}
	}
	return res;
}

10. 正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘’ 的正则表达式匹配。
‘.’ 匹配任意单个字符
'
’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和
示例 1:
输入:
s = “aa”
p = “a”
输出: false
解释: “a” 无法匹配 “aa” 整个字符串。
示例 2:
输入:
s = “aa”
p = "a
"
输出: true
解释: 因为 ‘’ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。
示例 3:
输入:
s = “ab”
p = ".
"
输出: true
解释: "." 表示可匹配零个或多个(’’)任意字符(’.’)。
示例 4:
输入:
s = “aab”
p = “cab”
输出: true
解释: 因为 '’ 表示零个或多个,这里 ‘c’ 为 0 个, ‘a’ 被重复一次。因此可以匹配字符串 “aab”。
示例 5:
输入:
s = “mississippi”
p = "mis
isp."
输出: false

思路:
第一步:暂时不管正则符号,如果两个普通字符串进行比较

bool isMatch(string text, string pattern) {
	if (text.size() != pattern.size()) {
		return false;
	}
	for (int j = 0; j < pattern.size(); j++) {
		if (pattern[j] != text[j])
			return false;
	}
	return true;
}

稍微改一下为

bool isMatch(string text, string pattern) {
	int i = 0; // text的索引位置
	int j = 0; // pattern 的索引位置
	while (j < pattern.size()) {
		if (i >= text.size())
			return false;
		if (pattern[j++] != text[i++]) {
			return false;
		}
	}
	// 判断pattern和text是否一样长
	return j == text.size()
}

递归

def isMatch(text, pattern) -> bool:
	#如果pattern为空就返回text是否为空
	if pattern is empty: return text is empty  
	#如果第一个字符是一样的就要看剩余的字符是否一样
	first_match = (text not empty) and pattern[0] == text[0] 
	# text去掉第一个字符的子串和pattern去掉第一个字符的子串,两者是否match
	return first_match and isMatch(text[1:] , pattern[1:])

第二步:处理点号.通配符

def isMatch(text, pattern) -> bool:
	# pattern为空 返回text为空
	if not pattern: return not text
	# 如果text不为空,且pattern[0]等于text[0]或者是pattern[0]是一个点的话, first_match为true
	first_match = bool(text) and pattern[0] in {text[0], '.'}
	return first_match and isMatch(text[1:] , pattern[1:])
	

第三步:处理*通配符

def isMatch(text, pattern) -> bool:
	# pattern为空 返回text为空
	if not pattern: return not text
	# 如果text不为空,且pattern[0]等于text[0]或者是pattern[0]是一个点的话, first_match为true
	first_match = bool(text) and pattern[0] in {text[0], '.'}
	# *永远要跟它前面的字符一起出现,因为*本身是要把前面的字符复制0份或多份
		if len(pattern) >= 2 and pattern[1] == '*':
		#发现通配符*有两种情况:
		 # 匹配该字符0次,跳过该字符和'*';意思就是把前面字符和*直接当成是0,这时候pattern要略过2位,但text没有任何略过
		 # 或者 当pattern[0]和text[0]匹配后,移动text;如果第一位符合,text匹配到1位,向后走1位,pattern不需要动因为*可以再重复出来
			return isMatch(text, pattern[2:]) or first_match and isMatch(text[1:], pattern)
	return first_match and isMatch(text[1:] , pattern[1:])
	

第四步 动态规划

# (带备忘录的递归)
def isMatch(text, pattern) -> bool:
	memo = dict() # 备忘录
	def dp(i, j):
		if (i, j) in memo: return memo[(i, j)]
		if j == len(pattern): return i == len(text)
		first = i < len(text) and pattern[j] in {text[i], '.'}

		#如果pattern的长度小于等于2的话且后面一个为*时,说明子串字符大于2且第二个字符是*
		if j <= len(pattern) - 2 and pattern[j+1] == '*':
			ans = dp(i, j+2) or first and dp(i+1, j)
		else:
			ans = first and dp(i+1, j+1)
		memo[(i, j)] = ans
		return ans
	return dp(0, 0)
class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        S = len(s)
        P = len(p)
        memo = {}
        def dp(i, j):
            if ((i, j) in memo):
                return memo[(i, j)]
            if (j==P):
                return i ==S
            pre=i<S and p[j] in {s[i], "."}
            if (j<=P-2 and p[j+1]=="*"):
                tmp = dp(i,j+2) or pre and dp(i+1,j)
            else:
                tmp = pre and dp(i+1,j+1)
            memo[(i,j)] = tmp
            return tmp
        return dp(0,0)
#没有备忘录的递归
def isMatch(text, pattern) -> bool:
	if not pattern: return not text
	first_match = bool(text) and pattern[0] in {text[0], '.'}
	if len(pattern) >= 2 and pattern[1] == '*':
			return isMatch(text, pattern[2:]) or first_match and isMatch(text[1:], pattern)
	return first_match and isMatch(text[1:] , pattern[1:])

44. 通配符匹配

给定一个字符串 (s) 和一个字符模式 § ,实现一个支持 ‘?’ 和 ‘’ 的通配符匹配。
‘?’ 可以匹配任何单个字符。
'
’ 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和
示例 1:
输入:
s = “aa”
p = “a”
输出: false
解释: “a” 无法匹配 “aa” 整个字符串。
示例 2:
输入:
s = “aa”
p = "
"
输出: true
解释: ‘’ 可以匹配任意字符串。
示例 3:
输入:
s = “cb”
p = “?a”
输出: false
解释: ‘?’ 可以匹配 ‘c’, 但第二个 ‘a’ 无法匹配 ‘b’。
示例 4:
输入:
s = “adceb”
p = “ab”
输出: true
解释: 第一个 '
’ 可以匹配空字符串, 第二个 '’ 可以匹配字符串 “dce”.
示例 5:
输入:
s = “acdcb”
p = "a
c?b"
输出: false

动态规划思路

给定的模式p中,只会有三种类型的字符出现:
(1)小写字母a-z,匹配对应的一个小写字母
(2)问号?,可以匹配任意一个小写字母
(3)星号*,可以匹配任意字符串,可以为空,也可以匹配0或任意多个小写字母
其中小写字母和问号的匹配是确定的,星号匹配不确定因此我们需要枚举所有的匹配情况。为了减少重复枚举,我们可以使用动态规划来解决本题。
在这里插入图片描述
在这里插入图片描述

class Solution(object):
    def isMatch(self, s, p):
       m, n = len(s), len(p)
       dp = [[False] * (n + 1) for _ in range(m + 1)]
       dp[0][0] = True
       for i in range(1, n+1):
           if p[i-1] == '*':
               dp[0][i] = True
           else:
                break
        
       for i in range(1, m+1):
            for j in range(1, n+1):
                if p[j-1] == '*':
                    dp[i][j] = dp[i-1][j] | dp[i][j-1]
                elif p[j-1] == '?' or s[i-1] == p[j-1]:
                    dp[i][j] = dp[i-1][j-1]
       return dp[m][n]

115. 不同的子序列

给定一个字符串 S 和一个字符串 T,计算在 S 的子序列中 T 出现的个数。
一个字符串的一个子序列是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)
题目数据保证答案符合 32 位带符号整数范围。
示例 1:
输入:S = “rabbbit”, T = “rabbit”
输出:3
解释:
如下图所示, 有 3 种可以从 S 中得到 “rabbit” 的方案。
(上箭头符号 ^ 表示选取的字母)
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^
示例 2:
输入:S = “babgbag”, T = “bag”
输出:5
解释:
如下图所示, 有 5 种可以从 S 中得到 “bag” 的方案。
(上箭头符号 ^ 表示选取的字母)
babgbag
^^ ^
babgbag
^^ ^
babgbag
^ ^^
babgbag
^ ^^
babgbag
^^^

在这里插入图片描述
在这里插入图片描述

暴力法:
通过记录当前走到的下标 i 和 j 来判断是否相等,若 s[ j ] == t[ i ], 则可以将 目标字符串的下标加一然后继续匹配(即选中当前字符)。其中,无论 s[ j ] 是否等于 t[ i ] 都需要不选择当前字符,然后继续进行递归。递归的终止条件为字符串 s 或 t 的下标走到最后的位置,当字符串 t 走到最终位置时,发生了一次匹配, res++

class Solution {
public:
    int numDistinct(string s, string t) {
        int res = 0;
        dfs(res, s, t, 0, 0);
        return res;
    }
    void dfs(int& res, string s, string t, int i, int j){
        if(i==t.size()){
            res++;
            return ;
        }
        if(j==s.size()) return ;
        // 是否相等都要走这一步
        dfs(res, s, t, i, j+1);
        // 发生匹配时,字符串 t 的下标 +1 然后进行匹配
        if(s[j]==t[i]) dfs(res, s, t, i+1, j+1);
    }
};

在这里插入图片描述

class Solution:
    def numDistinct(self, s: str, t: str) -> int:
        n1 = len(s)
        n2 = len(t)
        dp = [[0] * (n1 + 1) for _ in range(n2 + 1)]
        for j in range(n1 + 1):
            dp[0][j] = 1
        for i in range(1, n2 + 1):
            for j in range(1, n1 + 1):
                if t[i - 1] == s[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1]  + dp[i][j - 1]
                else:
                    dp[i][j] = dp[i][j - 1]
        #print(dp)
        return dp[-1][-1]


   public int numDistinct(String s, String t) {
        int[][] dp = new int[t.length()+1][s.length()+1];
        for (int i = 0; i < s.length()+1; i++) {
            // 只要前者为空,后者随便s有多少个字符,那么出现的个数只有一个,只能全部删除变为空串
            dp[0][i] = 1;
        }
        for(int i = 1; i<t.length()+1;i++){
            for(int j =i; j<s.length()+1;j++){
                if(t.charAt(i-1) == s.charAt(j-1)) {
                    dp[i][j] = dp[i][j-1] + dp[i-1][j-1];
                }else{
                    dp[i][j] = dp[i][j-1];

                }
            }
        }
        return dp[t.length()][s.length()];
    }

或者
在这里插入图片描述

class Solution:
    def numDistinct(self, s: str, t: str) -> int:
        n1 = len(s)
        n2 = len(t)
        dp = [[0] * (n2 + 1) for _ in range(n1 + 1)]
        for i in range(n1 + 1):
            dp[i][0] = 1
        for i in range(1, n1 + 1):
            for j in range(1, n2 + 1):
                if t[j - 1] == s[i - 1]:
                    dp[i][j] = dp[i - 1][j - 1]  + dp[i-1][j]
                else:
                    dp[i][j] = dp[i-1][j]
        #print(dp)
        return dp[-1][-1]

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值