目录
《剑指offer》面试题20:判断字符串是否表示一个数值(鲁棒性)
《剑指offer》面试题50(题目一):字符串中第一个只出现一次的字符(哈希表)
《剑指offer》面试题50(题目二):字符流中第一个只出现一次的字符(哈希表)
《剑指offer》面试题58(题目二):左旋转字符串(两次翻转、字符串拼接)
《剑指offer》面试题38(扩展):求字符的所有组合(组合)
Leetcode49:字母异位词分组(排序 哈希表 medium)
Leetcode242:有效的字母异位词(排序 哈希表 easy)
Leetcode212:单词搜索II(Trie DFS hard)
Leetcode91:数字字符串解码(动态规划 medium)
Lintcode119:单词转换的最少次数(动态规划 medium)
Leetcode3:不含重复字符的最长子串(滑动窗口 哈希表 medium)
Leetcode395:至少有K个重复字符的最长子串(二分法 哈希表 medium)
Leetcode76:最小覆盖子串(滑动窗口 哈希表 hard)
Leetcode30:所有单词相连的子串(哈希表 hard)
Leetcode125:判断字符串是否是回文串(双指针 easy)
Leetcode131:分割回文串(动态规划 DFS medium)
Leetcode139:单词拆分I(动态规划 medium)
Lintcode77:两个字符串的最长公共子序列(动态规划 medium)
Lintcode79:两个字符串的最长公共子串(动态规划 medium)
Leetcode14:多个字符串的最长公共前缀(扫描 分治 二分查找 Trie easy)
字符串与数字
《剑指offer》面试题20:判断字符串是否表示一个数值(鲁棒性
)
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100"
,"5e2"
,"-123"
,"3.1416"
和"-1E-16"
都表示数值。 但是"12e"
,"1a3.14"
,"1.2.3"
,"+-5"
和"12e+4.3"
都不是
解答
设字符串被'e'
或'E'
分个为part1
和part2
两部分:(part1 , 'e' | 'E' , part2)
因此,
- 1)如果出现
'e'
或'E'
,那么part1
和part2
必须包含数字。这意味着下列情况不能表示为数值:"e12","+e10","12e"。所以需要一个bool变量num1
表示part1
中是否存在数字,num2
表示part2
中是否存在数字 - 2)
'e'
或'E'
能且仅能出现1次,并且只有num1
为真后才能出现 - 3)
'.'
只能出现在part1
中,并且只能出现1次 - 4)
'+'
和'-'
只能出现在part1
或part2
的开始 - 5)
'空格'
只能出现在part1
之前、part2
之后,所以使用指针start
指向part1
的第一个字符,指针end
指向part2
的最后一个字符,然后只处理start
到end
的字符 -
public boolean isNumeric(char[] s) { int start = 0,end = s.length - 1; //先除去空格 while(start < s.length && s[start] == ' ') ++start; while(end >= 0 && s[end] == ' ') --end; //分别标记 小数点、eE、num1、num2 几个部分 boolean point = false,e = false,num1 = false,num2 = false; for(int i = start;i <= end;i++){ switch(s[i]){ case '.' : //出现小数点时候,之前不能有小数点 && 小数点只能在e出现之前 if(point || e) return false; point = true; break; case '+' : case '-' : //+-只能出现在num1或者num2之前的位置 if(i != start && s[i - 1] != 'e' && s[i - 1] != 'E') return false; break; case 'e' : case 'E' : //eE只有在num1存在时才存在 && 只能出现一次 if(!num1 || e) return false; e = true; break; default : //如果是普通数字,e没有出现则属于num1部分,否则属于num2部分 if(s[i] < '0' || s[i] > '9') return false; if(!e) num1 = true; else num2 = true; break; } } //只有num1则不能有e ||num1、num2、e都存在 return (num1 && !e) || (num1 && e && num2); }
《剑指offer》面试题67:把字符串转换成整数(鲁棒性
)
public int StrToInt(String str) {
if(str.length() == 0)
return 0;
//判断正负
int flag = 0;
if(str.charAt(0) == '+')
flag = 1;
else if(str.charAt(0) == '-')
flag = 2;
//确定数字开始位
int start = flag > 0 ? 1 : 0;
long res = 0;
while(start < str.length()){
if(str.charAt(start) > '9' || str.charAt(start) < '0')
return 0;
res = res * 10 + (str.charAt(start) - '0');
start ++;
}
return flag == 2 ? -(int)res : (int)res;
}
字符查找
《剑指offer》面试题50(题目一):字符串中第一个只出现一次的字符(哈希表
)
题目描述:在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).
哈希表:用一个辅助HashMap,键为字符,值为字符出现次数,第一次遍历时统计每个字符的出现次数,第二次遍历找到第一个只出现一次的字符
public int FirstNotRepeatingChar(String str)
{
char[] c = str.toCharArray();
int[] a = new int['z'];
for (char i : c)
a[(int) i]++;
for (int i = 0; i < c.length; i++)
if (a[(int) c[i]] == 1)
return i;
return -1;
}
《剑指offer》面试题50(题目二):字符流中第一个只出现一次的字符(哈希表
)
题目:请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。
输出描述:如果当前字符流没有存在出现一次的字符,返回#字符。
public class Solution {
HashMap<Character, Integer> map = new HashMap<Character, Integer>();
StringBuffer s = new StringBuffer();
//Insert one char from stringstream
public void Insert(char ch)
{
s.append(ch);
if(map.containsKey(ch)){
map.put(ch, map.get(ch)+1);
}else{
map.put(ch, 1);
}
}
//return the first appearence once char in current stringstream
public char FirstAppearingOnce()
{
for(int i = 0; i < s.length(); i++){
if(map.get(s.charAt(i)) == 1)
return s.charAt(i);
}
return '#';
}
}
翻转
《剑指offer》面试题58(题目一):翻转单词顺序
题目:给定一个字符串,逐个翻转字符串中的每个单词。示例:输入: "the sky is blue",输出: "blue is sky the".
解析:两次翻转字符串(先翻转所有,再翻转每个单词或先翻转每个单词,再翻转所有)
public class Solution {
public String ReverseSentence(String str) {
if(str.length() == 0){
return "";
}
char[] chars = str.toCharArray();
reverse(chars, 0, chars.length - 1);//整个字符串翻转
int start = 0;
for(int i = 0; i <= chars.length; i++){//最后一个没有空格,用越界判断
if(i == chars.length || chars[i] == ' '){
reverse(chars, start, i - 1);
start = i + 1;
}
}
return String.valueOf(chars);
}
public void reverse(char[] chars, int start, int end){//翻转字符数组
char temp = ' ';
while(start < end){
temp = chars[start];
chars[start] = chars[end];
chars[end] = temp;
start++;
end--;
}
}
}
《剑指offer》面试题58(题目二):左旋转字符串(两次翻转、字符串拼接)
题目:字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串“abcdefg”
和数字2
,函数返回左旋转两位得到的结果“cdefgab”
解答:
法1:两次翻转;先对前半部和后半部的字符串做翻转,然后对整个字符串做翻转;比如上面的例子,对前半部和后半部翻转后得到“bagfedc”
,然后翻转整个字符串,得到"cdefgab"
法2:字符串拼接;先连接成“abcdefgabcdefg”,然后从第2位开始截取得到“cdefgab”
Leetcode344:翻转字符串(双指针
easy
)
题目:
请编写一个函数,其功能是将输入的字符串反转过来。示例:输入:s = "hello",返回:"olleh"
public String reverse(String str){
char[] chars = str.toCharArray();
int l = 0, r = str.length()-1;
char temp = ' ';
while(l<r){
temp = chars[l];
chars[l] = chars[r];
chars[r] = temp;
l++;
r--;
}
return chars.toString();
}
排列组合
《剑指offer》面试题38:字符串的排列(排列
)
题目:输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba
输入描述:输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母
题解:我们求整个字符串的排列,其实可以看成两步:
-
第一步求所有可能出现在第一个位置的字符(即把第一个字符和后面的所有字符交换[相同字符不交换]);
-
第二步固定第一个字符,求后面所有字符的排列。这时候又可以把后面的所有字符拆成两部分(第一个字符以及剩下的所有字符),依此类推。这样,我们就可以用递归的方法来解决。
固定第一个字符,递归取得首位后面的各种字符串组合;再将第一个字符与后面每一个字符交换,同样递归获得其字符串组合;每次递归都是到最后一位时结束,递归的循环过程,就是从每个子串的第二个字符开始依次与第一个字符交换,然后继续处理子串。
import java.util.ArrayList;
import java.util.Collections;
public class Solution {
ArrayList<String> res = new ArrayList<String>();
public ArrayList<String> Permutation(String str) {
if(str == null)
return res;
PermutationHelper(str.toCharArray(), 0);
Collections.sort(res);
return res;
}
public void PermutationHelper(char[] str, int i){
if(i == str.length - 1){
res.add(String.valueOf(str));
}else{
//先固定第i个元素
for(int j = i; j < str.length; j++){
//如果相等跳出这次循环(不交换)
if(j!=i && str[i] == str[j])
continue;
//i与之后的每个元素交换
swap(str, i, j);
//递归进行子串的全排序
PermutationHelper(str, i+1);
//还原父串,让 i 归位
swap(str, i, j);
}
}
}
public void swap(char[] str, int i, int j) {
char temp = str[i];
str[i] = str[j];
str[j] = temp;
}
}
《剑指offer》面试题38(扩展):求字符的所有组合(组合
)
题目:给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。说明:解集不能包含重复的子集
题解:
法1:使用DFS,每次将一个元素添加到路径中,递归DFS返回后从路径中删除元素
法2:每添加一个元素就形成一个子集
//法1:DFS遍历
public static List<List<Integer>> subsets(int[] nums){
List<List<Integer>> res = new LinkedList<>();
List<Integer> sub = new LinkedList<>();
if(nums.length == 0) return res;
dfs(nums,0,res,sub);
return res;
}
public static void dfs(int[] nums,int i,List<List<Integer>> res,List<Integer> sub){
if(i == nums.length) {
//注意:这里是new LinkedList<Integer>(sub)
res.add(new LinkedList<Integer>(sub));
return;
}
sub.add(nums[i]);
dfs(nums, i + 1, res, sub);
sub.remove(sub.size()-1);
dfs(nums, i + 1, res, sub);
}
//法2
public static LinkedList<LinkedList<Integer>> allSets(int[] nums){
LinkedList<LinkedList<Integer>> res = new LinkedList<>();
LinkedList<Integer> sub = new LinkedList<>();
if(nums.length == 0) return res;
dfs(nums,0,res,sub);
return res;
}
public static void helper(int[] nums,int i,LinkedList<LinkedList<Integer>> res,LinkedList<Integer> sub){
if(i <= nums.length) {
res.add(sub);
}
for(int j=i;j<nums.length;j++) {
sub.add(nums[j]);
//注意:new LinkedList<Integer>(sub)
helper(nums, j + 1, res, new LinkedList<Integer>(sub));
sub.remove(sub.size()-1);
}
return;
}
删除替换
《剑指offer》面试题5:替换空格
题目描述:请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
解题思路
法1,遍历替换;
法2,计算出新的字符串长度,遍历从后向前添加
//法1
public String replaceSpace(StringBuffer str) {
StringBuffer res = new StringBuffer();
int len = str.length() - 1;
for(int i = 0; i <= len; i++){
if(str.charAt(i) == ' ')
res.append("%20");
else
res.append(str.charAt(i));
}
return res.toString();
}
//法2
public static String replaceSpace(StringBuffer str) {
if (str.length()==0||str==null){
return "";
}
int space = 0;//空格数
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == ' ') {
space++;
}
}
int oldLength = str.length(); //老字符串长度
int indexOld = oldLength-1;//老字符串最后一个字符下标
int newLength = oldLength+space*2; //新字符串长度(包括空格)
int indexNew = newLength-1;//要赋值的新字符串的下标
str.setLength(newLength); //扩大到转换成%20之后的长度,防止下标越界
while(indexNew>-1){
if(space==0){ //如果没有空格,不需要复制,直接跳出
break;
}
if (str.charAt(indexOld)==' '){
indexOld--;
str.setCharAt(indexNew--,'0');
str.setCharAt(indexNew--,'2');
str.setCharAt(indexNew--,'%');
space--;
}else{
str.setCharAt(indexNew--,str.charAt(indexOld--));
}
}
return str.toString();
}
Leetcode71:简化路径(medium
)
题目:给定一个文档 (Unix-style) 的完全路径,请进行路径简化。例如,path = "/home/"
, => "/home";
path = "/a/./b/../../c/"
, => "/c"
边界情况:
- 你是否考虑了 路径 =
"/../"
的情况?在这种情况下,你需返回"/"
。 - 此外,路径中也可能包含多个斜杠
'/'
,如"/home//foo/"
。在这种情况下,你可忽略多余的斜杠,返回"/home/foo"
。
解答
根据'/'
将路径分割成多个路径组成部分part
:
- 如果
part
等于"."
或""
,那么处理下一个组成部分 - 否则,如果
part
等于".."
,将上一个组成部分删除 - 否则,将组成部分保存在一个vector或者stack中
最后,根据vector或stack中的路径组成部分,生成简化后的路径
public String simplifyPath(String path) {
Deque<String> stack = new LinkedList<>();
Set<String> skip = new HashSet<>(Arrays.asList("..",".",""));
for (String dir : path.split("/")) {
if (dir.equals("..") && !stack.isEmpty()) stack.pop();
else if (!skip.contains(dir)) stack.push(dir);
}
String res = "";
for (String dir : stack) res = "/" + dir + res;
return res.isEmpty() ? "/" : res;
}
异位词
Leetcode49:字母异位词分组(排序
哈希表
medium
)
给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。
输入: ["eat", "tea", "tan", "ate", "nat", "bat"];输出:[ ["ate","eat","tea"], ["nat","tan"], ["bat"]]。
法1:排序后映射
可以比较每个字符串排序后的字符串,字符异位的字符串排序后肯定是同一个字符串,因此,使用一个map存储“排序后字符串”和“异位字符串集合”的映射,key的类型是string,表示排序后的字符串,value是vector<string>,表示每个一组异位词。时间复杂度:O(N*K*log(K))空间复杂度:O(N*K)
Leetcode242:有效的字母异位词(排序
哈希表
easy
)
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的一个字母异位词。
法1:排序;将两个字符串按字典序排序,然后判断是否相等
法2:哈希表;使用哈希表,统计字符串s中各字符的出现次数,然后遍历字符串t,如果遇到一个s中的字符,则计数减1,如果字符不在s中,则返回false。最终如果哈希表中所有字符的计数都刚好减为0那么就是异位词:
单词查找
《剑指offer》面试题12:单词搜索(DFS
)
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则之后不能再次进入这个格子
法1:使用辅助空间;遍历矩阵,以每个字符为起点,使用DFS进行查找匹配,使用矩阵flag
记录走过的位置,防止重复
基本思想:
0.根据给定数组,初始化一个标志位数组,初始化为false,表示未走过,true表示已经走过,不能走第二次
1.根据行数和列数,遍历数组,先找到一个与str字符串的第一个元素相匹配的矩阵元素,进入judge
2.根据i和j先确定一维数组的位置,因为给定的matrix是一个一维数组
3.确定递归终止条件:越界,当前找到的矩阵值不等于数组对应位置的值,已经走过的,这三类情况,都直接false,说明这条路不通
4.若k,就是待判定的字符串str的索引已经判断到了最后一位,此时说明是匹配成功的
5.下面就是本题的精髓,递归不断地寻找周围四个格子是否符合条件,只要有一个格子符合条件,就继续再找这个符合条件的格子的四周是否存在符合条件的格子,直到k到达末尾或者不满足递归条件就停止。
6.走到这一步,说明本次是不成功的,我们要还原一下标志位数组index处的标志位,进入下一轮的判断。
public class Solution {
public boolean hasPath(char[] matrix, int rows, int cols, char[] str)
{
//标志位,初始化为false
boolean[] flag = new boolean[matrix.length];
for(int i=0;i<rows;i++){
for(int j=0;j<cols;j++){
//循环遍历二维数组,找到起点等于str第一个元素的值,再递归判断四周是否有符合条件的----回溯法
if(judge(matrix,i,j,rows,cols,flag,str,0)){
return true;
}
}
}
return false;
}
//judge(初始矩阵,索引行坐标i,索引纵坐标j,矩阵行数,矩阵列数,待判断的字符串,字符串索引初始为0即先判断字符串的第一位)
private boolean judge(char[] matrix,int i,int j,int rows,int cols,boolean[] flag,char[] str,int k){
//先根据i和j计算匹配的第一个元素转为一维数组的位置
int index = i*cols+j;
//递归终止条件
if(i<0 || j<0 || i>=rows || j>=cols || matrix[index] != str[k] || flag[index] == true)
return false;
//若k已经到达str末尾了,说明之前的都已经匹配成功了,直接返回true即可
if(k == str.length-1)
return true;
//要走的第一个位置置为true,表示已经走过了
flag[index] = true;
//回溯,递归寻找,每次找到了就给k加一,找不到,还原
if(judge(matrix,i-1,j,rows,cols,flag,str,k+1) ||
judge(matrix,i+1,j,rows,cols,flag,str,k+1) ||
judge(matrix,i,j-1,rows,cols,flag,str,k+1) ||
judge(matrix,i,j+1,rows,cols,flag,str,k+1) )
{
return true;
}
//走到这,说明这一条路不通,还原,再试其他的路径
flag[index] = false;
return false;
}
}
Leetcode212:单词搜索II(Trie
DFS
hard
)
给定一个二维网格 board 和一个字典中的单词列表 words,找出所有同时在二维网格和字典中出现的单词。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。
解答:利用字典树Trie(实现Trie树)来做, 就是将要搜索的单词先添加到字典树中, 然后从地图board的每一个元素搜索, 如果往上下左右搜索的时候其元素可以在字典树中找到, 那么就继续搜索下去, 并且如果搜索到某个结点的时候发现到这个结点构成了一个单词, 那么就将单词添加到结果集合中. 如果在字典树中无法找到这个元素, 那么就结束当前分支的搜索。另外还需要标记搜索过的点, 可以再开一个二维数组来标记, 也可直接在原矩阵上修改, 搜索完之后再改回来:
https://leetcode.com/problems/word-search-ii/discuss/59780/Java-15ms-Easiest-Solution-(100.00)
public List<String> findWords(char[][] board, String[] words) {
List<String> res = new ArrayList<>();
TrieNode root = buildTrie(words);
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
dfs (board, i, j, root, res);
}
}
return res;
}
public void dfs(char[][] board, int i, int j, TrieNode p, List<String> res) {
char c = board[i][j];
if (c == '#' || p.next[c - 'a'] == null) return;
p = p.next[c - 'a'];
if (p.word != null) { // found one
res.add(p.word);
p.word = null; // de-duplicate
}
board[i][j] = '#';
if (i > 0) dfs(board, i - 1, j ,p, res);
if (j > 0) dfs(board, i, j - 1, p, res);
if (i < board.length - 1) dfs(board, i + 1, j, p, res);
if (j < board[0].length - 1) dfs(board, i, j + 1, p, res);
board[i][j] = c;
}
public TrieNode buildTrie(String[] words) {
TrieNode root = new TrieNode();
for (String w : words) {
TrieNode p = root;
for (char c : w.toCharArray()) {
int i = c - 'a';
if (p.next[i] == null) p.next[i] = new TrieNode();
p = p.next[i];
}
p.word = w;
}
return root;
}
class TrieNode {
TrieNode[] next = new TrieNode[26];
String word;
}
字符串转换
Leetcode91:数字字符串解码(动态规划
medium
)
题目:一条包含字母 A-Z
的消息编码对应数字 1-26;给定一个只包含数字的非空字符串,请计算解码方法的总数。
法1:正向动态规划;used a dp array of size n + 1 to save subproblem solutions. dp[0]
means an empty string will have one way to decode, dp[1]
means the way to decode a string of size 1. I then check one digit and two digit combination and save the results along the way. In the end, dp[n]
will be the end result.
public class Solution {
public int numDecodings(String s) {
if(s == null || s.length() == 0) {
return 0;
}
int n = s.length();
int[] dp = new int[n+1];
dp[0] = 1;
dp[1] = s.charAt(0) != '0' ? 1 : 0;
for(int i = 2; i <= n; i++) {
int first = Integer.valueOf(s.substring(i-1, i));
int second = Integer.valueOf(s.substring(i-2, i));
if(first >= 1 && first <= 9) {
dp[i] += dp[i-1];
}
if(second >= 10 && second <= 26) {
dp[i] += dp[i-2];
}
}
return dp[n];
}
}
public class Solution {
public int numDecodings(String s) {
int n = s.length();
if (n == 0) return 0;
int[] memo = new int[n+1];
memo[n] = 1;
memo[n-1] = s.charAt(n-1) != '0' ? 1 : 0;
for (int i = n - 2; i >= 0; i--)
if (s.charAt(i) == '0') continue;
else memo[i] = (Integer.parseInt(s.substring(i,i+2))<=26) ? memo[i+1]+memo[i+2] : memo[i+1];
return memo[0];
}
}
Leetcode127:单词阶梯(BFS
medium
)
Lintcode119:单词转换的最少次数(动态规划
/ DFS medium
)
给出两个单词word1和word2,计算出将word1 转换为word2的最少操作次数。你总共三种操作方法:插入一个字符,删除一个字符,替换一个字符
1. DFS
Time complexity: O(3^n)
//可以看法2 讲解
public int minDistance(String word1, String word2) {
return minDistance(word1, word1.length(), word2, word2.length());
}
private int minDistance(String x, int i, String y, int j) {
if (i == 0) return j;
if (j == 0) return i;
int cost = (x.charAt(i - 1) == y.charAt(j - 1)) ? 0 : 1;
return min(minDistance(x, i - 1, y, j) + 1,
minDistance(x, i, y, j - 1) + 1,
minDistance(x, i - 1, y, j - 1) + cost);
}
2. DFS + Memoization
Time complexity: O(nm)
Space complexity: O(nm)
public int minDistance(String word1, String word2) {
int n = word1.length();
int m = word2.length();
//注意memo数组大小
Integer[][] memo = new Integer[n + 1][m + 1];
return minDistance(word1, n, word2, m, memo);
}
private int minDistance(String x, int i, String y, int j, Integer[][] memo) {
if (i == 0) return j;
if (j == 0) return i;
if (memo[i][j] != null) return memo[i][j];
// If last characters of substring X and substring Y matches, nothing needs to be done.
// We simply recurse for remaining substring X[0..i-1], Y[0..j-1].
// As no edit operation is involved, the cost will be 0.
int cost = (x.charAt(i - 1) == y.charAt(j - 1)) ? 0 : 1;
int dist = min(minDistance(x, i - 1, y, j, memo) + 1, // deletion
minDistance(x, i, y, j - 1, memo) + 1, // insertion
minDistance(x, i - 1, y, j - 1, memo) + cost); // substitution
return memo[i][j] = dist;
}
3. Bottom-up (Wagner–Fischer algorithm)
Time complexity: O(nm)
Space complexity: O(nm)
public int minDistance(String word1, String word2) {
int n = word1.length();
int m = word2.length();
int[][] dp = new int[n + 1][m + 1];
for (int i = 1; i <= n; i++) {
dp[i][0] = i; // the distance of any first string to an empty second string
// (transforming the string of the first i characters of word1 into
// the empty string requires i deletions)
}
for (int j = 1; j <= m; j++) {
dp[0][j] = j; // the distance of any second string to an empty first string
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1]; // no operation required
} else {
dp[i][j] = min(
dp[i - 1][j] + 1, // a deletion
dp[i][j - 1] + 1, // an insertion
dp[i - 1][j - 1] + 1 // a substitution
);
}
}
}
return dp[n][m];
}
// utility function to find minimum of three numbers
private int min(int a, int b, int c) {
return Math.min(a, Math.min(b, c));
}
子串与子序列
Leetcode28:子串查找(KMP
easy
)
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
1)蛮力法;假设haystack长度为n, needle长度为m,遍历haystack的前n-m个字符,对于每个字符,如果等于needle的首字符则继续比较;时间复杂度:O(n*m)
public int strStr(String s, String t) {
if (t.isEmpty()) return 0; // edge case: "",""=>0 "a",""=>0
for (int i = 0; i <= s.length() - t.length(); i++) {
for (int j = 0; j < t.length() && s.charAt(i + j) == t.charAt(j); j++)
if (j == t.length() - 1) return i;
}
return -1;
}
2)KMP算法 KMP算法——知乎海纳
PMT(部分匹配表):PMT中的值是字符串的前缀集合与后缀集合交集中最长串的长度(注意,这里的前缀和后缀都不包括字符串本身)
那么PMT如何在字符串查找中运用?在字符串查找中,我们为模版字符串创建PMT表,若模版字符串长度len,就有len个PMT值。假设有如下例子:主字符串:"ababababca"
,模版字符串:"abababca"
模版字符串“abababca”
相应的PMT值如下表:
- 字符串
"a"
不包含前缀和后缀(因为前缀和后缀不包含字符串本身),所以PMT中相应的值为0 - 字符串
"ab"
包含前缀{"a"}
,包含后缀{"b"}
,没有交集,所以PMT中相应的值为0 - 字符串
"aba"
包含前缀{"a","ab"}
,包含后缀{"a","ba"}
交集为"a"
,所以相应的PMT值为1 - ...
那么当字符串查找过程中在模板字符j
位置开始不匹配时(如下图(a)),这意味着主字符串从 i−j
到 i
这一段是与模板字符串的 0
到 j
这一段是完全相同的(即字符串"ababab"
)。通过查找PMT表我们知道,字符串"ababab"
前缀集合与后缀集合交集中最长串("abab"
)的长度为4,这说明了主字符串中 i
指针之前的 PMT[j − 1](此处值为4)
位就一定与模板字符串的第 0
位至第 PMT[j − 1](此处值为4)
位是相同的。这样一来,就可以将这些字符段的比较省略掉。具体的做法是,保持i
指针不动,然后将j
指针指向模式字符串的PMT[j − 1]
位即可(这也是KMP的核心:通过使用PMT省略不必要字符段的比较),从而进入图(b)的状态:
如果是在 j
位失配,那么影响 j
指针回溯的位置的其实是第 j − 1
位的 PMT 值,所以为了编程的方便,我们不直接使用PMT数组,而是将PMT数组向后偏移一位。我们把新得到的这个数组称为next数组:
public class Solution {
private int[] failureFunction(char[] str) {
int[] f = new int[str.length+1];
for (int i = 2; i < f.length; i++) {
int j = f[i-1];
while (j > 0 && str[j] != str[i-1]) j = f[j];
if (j > 0 || str[j] == str[i-1]) f[i] = j+1;
}
return f;
}
public int strStr(String haystack, String needle) {
if (needle.length() == 0) return 0;
if (needle.length() <= haystack.length()) {
int[] f = failureFunction(needle.toCharArray());
int i = 0, j = 0;
while (i < haystack.length()) {
if (haystack.charAt(i) == needle.charAt(j)) {
i++; j++;
if (j == needle.length()) return i-j;
} else if (j > 0) j = f[j];
else i++;
}
}
return -1;
}
}
Leetcode3:不含重复字符的最长子串(滑动窗口
哈希表
medium
)
给定一个字符串,找出不含有重复字符的最长子串的长度。
解答:使用一个HashMap存储字符串中每个字母和出现的位置,使用两个指针记录最大的子串首尾位置
public int lengthOfLongestSubstring(String s) {
if (s.length()==0) return 0;
HashMap<Character, Integer> map = new HashMap<Character, Integer>();
int max=0;
for (int i=0, j=0; i<s.length(); ++i){
if (map.containsKey(s.charAt(i))){
//注意此处:防止 j 指针退回
j = Math.max(j,map.get(s.charAt(i))+1);
}
map.put(s.charAt(i),i);
max = Math.max(max,i-j+1);
}
return max;
}
Leetcode395:至少有K个重复字符的最长子串(二分法
哈希表
medium
)
Leetcode76:最小覆盖子串(滑动窗口
哈希表
hard
)
Leetcode30:所有单词相连的子串(哈希表
hard
)
Leetcode5:最长回文子串(动态规划
medium
)
法1:动态规划:使用一个矩阵记录每个子串是不是回文,字符串的任意单个字符组成的字符串肯定都是回文,即矩阵的对角线都为true,判断任意子串是不是回文,即求dp[i][j]
,
1.如果i = j
,即“a”,dp[i][j] = true
2.如果i + 1 = j或i + 2 = j,即"aa","aba",
那么 dp[i][j] = (s[i] == s[j])
3.否则,dp[i][j] = (s[i] == s[j]) && dp[i + 1][j - 1]
public String longestPalindrome(String s) {
int n = s.length();
String res = null;
//dp(i, j) represents whether s(i ... j) can form a palindromic substring
boolean[][] dp = new boolean[n][n];
for (int i = n - 1; i >= 0; i--) {
for (int j = i; j < n; j++) {
//j - i < 3考虑到"a","aa","aba"情况
dp[i][j] = s.charAt(i) == s.charAt(j) && (j - i < 3 || dp[i + 1][j - 1]);
if (dp[i][j] && (res == null || j - i + 1 > res.length())) {
res = s.substring(i, j + 1);
}
}
}
return res;
}
Leetcode125:判断字符串是否是回文串(双指针
easy
)
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
题解:使用两个变量l和r,一个从字符串左边出发,一个从右边出发,每次都找到一个数字或字母。
public class Solution {
public boolean isPalindrome(String s) {
if (s.isEmpty()) {
return true;
}
int head = 0, tail = s.length() - 1;
char cHead, cTail;
while(head <= tail) {
cHead = s.charAt(head);
cTail = s.charAt(tail);
if (!Character.isLetterOrDigit(cHead)) {
head++;
} else if(!Character.isLetterOrDigit(cTail)) {
tail--;
} else {
if (Character.toLowerCase(cHead) != Character.toLowerCase(cTail)) {
return false;
}
head++;
tail--;
}
}
return true;
}
}
字符串拆分
Leetcode131:分割回文串(动态规划
DFS
medium
)
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。
解答:
public class Solution {
public List<List<String>> partition(String s) {
List<List<String>> res = new ArrayList<List<String>>();
List<String> list = new ArrayList<String>();
dfs(s,0,list,res);
return res;
}
public void dfs(String s, int pos, List<String> list, List<List<String>> res){
if(pos==s.length()) res.add(new ArrayList<String>(list));
else{
for(int i=pos;i<s.length();i++){
if(isPal(s,pos,i)){
list.add(s.substring(pos,i+1));
dfs(s,i+1,list,res);
list.remove(list.size()-1);
}
}
}
}
public boolean isPal(String s, int low, int high){
while(low<high) if(s.charAt(low++)!=s.charAt(high--)) return false;
return true;
}
}
Leetcode139:单词拆分I(动态规划
medium
)
Leetcode140:单词拆分II(动态规划
hard
)
字符串匹配
Leetcode10:正则表达式匹配(动态规划
hard
)
Leetcode44:通配符匹配(动态规划
hard
)
最长公共问题
最长公共子串问题是寻找两个或多个已知字符串最长的子串。此问题与最长公共子序列问题的区别在于子序列不必是连续的,而子串却必须是。
Lintcode77:两个字符串的最长公共子序列(动态规划
medium
)
给出2个字符串,找出它们的最长公共子序列(LCS)。返回其长度
法1:动态规划,这里结果就是dp[n][m]
1.确定状态:dp[i][j]数组: A前i个元素,B前j个元素中最长公共子序列
2.转移方程:dp[i][j] = max( dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1] +1 )
3.初始化及边界情况:长度为零的串与任何序列的公共子序列都为0;
4.计算顺序:dp[1][1], dp[1][2], ...dp[1][n], ...dp[n][n]
public static int findLCS1(String A, String B) {
int n = A.length(),m = B.length();
int[][] dp = new int[n + 1][m + 1];
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
dp[i][j] = 0;
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (A.charAt(i - 1) == B.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = dp[i - 1][j] > dp[i][j - 1] ? dp[i - 1][j] : dp[i][j - 1];
}
}
}
return dp[n][m];
}
//这里边界条件改一下,本质一样
public static int findLCS2(String str1, String str2) {
if (str1.length() == 0 || str2.length() == 0) return 0;
int m = str1.length(), n = str2.length();
int[][] dp = new int[m][n];
dp[0][0] = str1.charAt(0) == str2.charAt(0) ? 1 : 0;
for(int i=1;i<m;i++){
dp[i][0] = str1.charAt(i) == str2.charAt(0) ? 1 : dp[i-1][0];
}
for(int j=1;j<n;j++){
dp[0][j] = str1.charAt(0) == str2.charAt(j) ? 1 : dp[0][j-1];
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
if(str1.charAt(i) == str2.charAt(j)){
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-1][n-1];
}
Lintcode79:两个字符串的最长公共子串(动态规划
medium
)
给出2个字符串,找出它们的最长公共子串,返回其长度
法1:动态规划,这里结果是dp数组中最大的那个值
1.确定状态:dp[i][j]数组: A前i个元素,B前j个元素中最长公共子串
2.转移方程:如果A[i] == B[j] 则 dp[i][j] = dp[i - 1][j - 1] +1; 如果A[i] != B[j] 则 dp[i][j] = 0;
3.初始化及边界情况:相等为1,不等为0
private static int getCommonStrLength(String str1, String str2) {
if (str1.length() == 0 || str2.length() == 0) return 0;
int m = str1.length(), n = str2.length();
int[][] dp = new int[m][n];
for(int i=0;i<m;i++){
dp[i][0] = str1.charAt(i) == str2.charAt(0) ? 1 : 0;
}
for(int j=0;j<n;j++){
dp[0][j] = str1.charAt(0) == str2.charAt(j) ? 1 : 0;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j] = str1.charAt(i) == str2.charAt(j) ? dp[i-1][j-1] + 1 : 0;
}
}
int max = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (dp[i][j] > max) {
max = dp[i][j];
}
}
}
return max;
}
Leetcode14:多个字符串的最长公共前缀(扫描
分治
二分查找
Trie
easy
)
编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 ""。
法1:垂直扫描:首先判断所有字符串第一个字符是否相等,然后判断第二个,第三个...
法2:分治算法:将字符串分为2组,求出第一组的前缀和第二组的前缀,然后求两个前缀字符串的公共前缀:
法3:先求出第一个与第二个的最长公共前缀,记为pre,然后求pre与下一个的的最长公共前缀,依次向下执行
法4:遍历找到最大的字符串和最小的字符串,然后比较两者的最长公共前缀
//法3:
class Solution {
public String longestCommonPrefix(String[] strs) {
if(strs == null || strs.length == 0) return "";
String pre = strs[0];
int i = 1;
while(i<strs.length){
while(!strs[i].startsWith(pre))
pre = pre.substring(0,pre.length()-1);
i++;
}
return pre;
}
}