目录
11.regular-expression-matching
12.wildcard-matching
13.count-and-say
14.reverse-integer
15.palindrome-number
16.implement-strstr
17.substring-with-concatenation-of-all-words
18.anagrams
19.zigzag-conversion
20.text-justification
21.simplify-path
题目:请实现支持'.'and'*'.的通配符模式匹配。字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)
分析:方法一,见剑指offer面试题19 https://blog.csdn.net/Nibaby9/article/details/103822794
方法二,采用动态规划,f(i,j)表示字符串的前i个字符与模式串的前j个字符是否匹配,如果当前字符匹配,f(i,j) = f(i-1,j-1);如果模式串当前字符为‘*’,根据匹配串的上个字符与字符串当前字符是否匹配分成两种情况,若不匹配说明‘*’匹配0个字符,f(i,j) = f(i,j-1),若匹配则f(i,j) = f(i,j-1) || f(i-1,j-1) || f(i-1,j),也就是*匹配0个字符,匹配1个字符或者是匹配多个字符。
public boolean isMatch(String s, String p) {
int m = s.length(), n = p.length();
boolean[][] res = new boolean[m + 1][n + 1];
res[0][0] = true;
for (int i = 0; i < n; i++) {
if (p.charAt(i) == '*' && res[0][i - 1])
res[0][i + 1] = true;
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (p.charAt(j) == '.')
res[i + 1][j + 1] = res[i][j];
if (p.charAt(j) == s.charAt(i))
res[i + 1][j + 1] = res[i][j];
if (p.charAt(j) == '*') {
if (s.charAt(i) != p.charAt(j - 1) && p.charAt(j - 1) != '.')
res[i + 1][j + 1] = res[i + 1][j - 1];
else {
//res[i + 1][j - 1] 表示*一个都不匹配;
//res[i + 1][j]表示匹配一个
//res[i][j + 1]表示匹配多个
res[i + 1][j + 1] = res[i + 1][j - 1] || res[i + 1][j] || res[i][j + 1];
}
}
}
}
return res[m][n];
}
题目:请实现支持'?'and'*'.的通配符模式匹配.'?' 可以匹配任何单个字符,'*' 可以匹配任何字符序列(包括空序列)
分析:这题与上题非常相似,但是这题如果采用类似递归的方式会超时。这里采用双指针的方法,每次碰到字符 ‘*’ 就标记好匹配串的当前位置和匹配串*的位置,这样在每次无法匹配时,就能回溯,从匹配串标记的下一个位置再重新进行匹配。
public boolean isMatch(String s, String p) {
int i = 0,j = 0,backI = -1,backJ = -1;
while(i < s.length()){
if(j < p.length() && (p.charAt(j) == '?' || s.charAt(i) == p.charAt(j))){//当前字符匹配
i++;j++;
}
else if(j < p.length() && p.charAt(j) == '*'){//若不匹配,如果模式串当前字符为*,记录匹配串和模式串回溯位置
backI = i;backJ = j;j++;
}
else if(backJ != -1) {//根据上一个*进行回溯
i = backI + 1;j = backJ + 1;backI = i;
}
else
return false;
}
while (j < p.length() && p.charAt(j) == '*')
j++;
return j == p.length();
}
题目:count-and-say数列的前几项如下:1, 11, 21, 1211, 111221, ...。1读作“1个1”或11,11读作“2个1“或者21,21读作”1个2,1个1“或者1211。给出一个整数n,请给出序列的第n项。注意:序列中的数字用字符串表示
分析:先依次求出序列的n-1项,再根据第n-1项求出第n项即可,当前项其实就是在前一项的基础上统计各个数字连续出现的次数。
public String countAndSay(int n) {
if(n <= 0)
return "";
String result = "1";
for(int i = 1;i < n;i++){
String temp = "";
int count = 1;
int j = 1;
for(;j < result.length();j++){
if(result.charAt(j) == result.charAt(j-1))
count++;
else {
temp += ("" + count);
temp += result.charAt(j - 1);
count = 1;
}
}
temp += ("" + count);
temp += result.charAt(j - 1);
result = temp;
}
return result;
}
题目:给出一个 32 位的有符号整数,将这个整数中每位上的数字进行反转。例1:x=123,返回321;例2:x=-123,返回-321。假设我们的环境只能存储得下 32 位的有符号整数,则其数值范围为 [−231, 231 − 1]。请根据这个假设,如果反转后整数溢出那么就返回 0。
分析:通过循环将数字x
的每一位拆开,然后在计算新值时每一步都判断是否溢出。
public int reverse(int x) {
int result = 0;
while(x != 0){
int pop = x % 10;
x /= 10;
if (result > Integer.MAX_VALUE/10 || (result == Integer.MAX_VALUE / 10 && pop > 7))
return 0;
if (result < Integer.MIN_VALUE/10 || (result == Integer.MIN_VALUE / 10 && pop < -8))
return 0;
result = result * 10 + pop;
}
return result;
}
题目:判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
分析:一种方法是将数字转化为字符串,并检查字符串是否为回文,但这需要使用额外的空间。另一种方法是将整数反转,但从上题可以知道,整数翻转可能会出现溢出。为了避免这个问题,我们可以只反转数字的一半,如果该数字是回文,其后半部分反转后应该与原始数字的前半部分相同。首先我们需要处理一些特殊情况,负数不是回文,数字末尾为0也不是回文(0除外),然后按照反转整数的思路依次构建新值,当x的值小于等于新值时意味着我们已经翻转了一半,以12321为例,当新值为123、x为12就已经反转了一半。
public boolean isPalindrome(int x) {
if(x < 0 || (x != 0 && x % 10 == 0))
return false;
int reverse = 0;
while(x > reverse){
reverse = reverse * 10 + x % 10;
x = x / 10;
}
return reverse == x || reverse / 10 == x;
}
题目:实现函数 strStr。函数声明如下:char *strStr(char *haystack, char *needle),返回一个指针,指向needle第一次在haystack中出现的位置,如果needle不是haystack的子串,则返回null。例如"mississippi","issi",返回"ississippi".
分析:考查KMP算法,它是一种改进的字符串模式匹配算法,可以在O(n+m)的时间复杂度以内完成字符串的匹配操作,其核心思想在于当一趟匹配过程中出现字符不匹配时,不需要回溯主串的指针,而是利用已经得到的“部分匹配”,将模式串尽可能多地向右“滑动”一段距离,然后继续比较。KMP算法先计算模式串的next数组,next[i]表示模式串前i个字符的部分匹配值(”前缀”和”后缀”的最长的公有元素的长度),如”A”的前缀和后缀都为空集,部分匹配值为0,“ABCDAB”的部分匹配值为2。求解next数组的步骤:
求解完next数组后,开始遍历主串来匹配模式串,如果不匹配则利用next数组进行对模式串进行偏移。
public String strStr(String haystack, String needle) {
if(needle.length() == 0)
return haystack;
if(needle.length() > haystack.length())
return null;
int[] next = cal_next(needle);
int i = 0,j = 0;
while(i < haystack.length() && j < needle.length()){
if(haystack.charAt(i) == needle.charAt(j) ){
i++;j++;
}
else if(next[j] == -1){
j = 0;i++;
}
else
j = next[j];
}
if(j == needle.length())
return haystack.substring(i-j);//易错
return null;
}
private int[] cal_next(String needle) {
int[] next = new int[needle.length()];
next[0] = -1;//初始化
for(int i = 2;i < needle.length();i++){
int k = next[i-1];
while(k != -1 && needle.charAt(i-1) != needle.charAt(k))
k = next[k];
next[i] = k + 1;
}
return next;
}
题目:给出一个字符串S和一组单词L,L中单词的长度都相等,找出S中的符合以下要求的子串在S中的起始位置索引:子串为L中所有单词串联在一起(单词的顺序随意),L中的每个单词只出现一次,中间不能有其他的字符。例如:给定S="barfoothefoobarman",L为["foo", "bar"],返回的索引应该是[0,9].(不用关心给出索引的顺序)
分析:符合条件的子串长度必然为len = L[0].length * L.length,所以可以依次遍历S的第i到i+len-1个字符,判断是否符合子串条件,为了方便判断,可以将L中各个单词的个数存在HashMap中。
public ArrayList findSubstring(String S, String[] L) {
ArrayList res = new ArrayList<>();
if(L.length == 0 || S.length() == 0)
return res;
int len = L[0].length(),num = L.length;
HashMap map = new HashMap<>();
for(String s : L){
if(map.containsKey(s))
map.put(s,map.get(s) + 1);
else
map.put(s,1);
}
//从i开始,后面至少要有len * num个字符
HashMap cur = new HashMap<>();
for(int i = 0;i <= S.length() - len * num;i++){
cur.putAll(map);//map的深拷贝
int j = i;
for(;j < i + len * num;j+=len){//判断[j,j+len*num)是否符合子串条件
String str = S.substring(j,j+len);
if(cur.containsKey(str) && cur.get(str) > 0){
cur.put(str,cur.get(str)-1);
}
else
break;
}
if(j == i+len*num)
res.add(i);
}
return res;
}
题目:给出一个字符串数组,返回所有互为“换位词(anagrams)”的字符串的组合。(换位词就是包含相同字母,但字母顺序可能不同的字符串)备注:所有的输入都是小写字母。例如:输入["tea","and","ate","eat","den"],返回["tea","ate","eat"]
分析:方法一:根据换位词按字母排序是同一个字符串的特点,将这个排好序的字符串作为HashMap的key,而value就是对应的所有换位词,最后只要遍历map将这些list加入到结果集合中即可。时间复杂度为(NKlogK),空间复杂度为(NK)。
方法二:让每个字符对应于一个质数,于是这些数值一定不会有公约数,不在一组的数,它们的乘积一定不相等。因此可以将每个字符串映射成Integer值,作为HashMap的key。时间复杂度和空间复杂度均为(NK),相比于方法一显然节省了排序的时间。
//方法一:利用内部排好序的String作为HashMap的key
public List> groupAnagrams(String[] strs) {
List> res = new ArrayList<>();
if(strs.length == 0)
return res;
HashMap> map = new HashMap<>();
for(String s : strs){
char[] c = s.toCharArray();
Arrays.sort(c);
String key = String.valueOf(c);
if(!map.containsKey(key))
map.put(key,new ArrayList());
map.get(key).add(s);
}
for(String s : map.keySet())
res.add(map.get(s));
return res;
}
//方法二:使用质数作为乘法因子得到HashMap的key
public List> groupAnagrams(String[] strs) {
List> res = new ArrayList<>();
if(strs.length == 0)
return res;
int[] primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37,
41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101};
HashMap> map = new HashMap<>();
for(String s : strs){
int key = 1;
for(int i = 0;i < s.length();i++)
key *= primes[s.charAt(i) - 'a'];
if(!map.containsKey(key))
map.put(key,new ArrayList<>());
map.get(key).add(s);
}
for(Integer i : map.keySet())
res.add(map.get(i));
return res;
}
题目:将一个给定字符串根据给定的行数,以从上往下、从左到右进行 Z 字形排列。比如输入字符串为 "LEETCODEISHIRING" 行数为 3 时,排列如下:
L C I R
E T O E S I I G
E D H N
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"LCIRETOESIIGEDHN"。编写代码完成将字符串转化为指定行数的Z字形字符串。
分析:假设转换后Z字形的行数分别为0~nRows-1,可以发现,在按顺序遍历字符串时,我们是依次将字符放入到第0~nRows-1行,再反过来放入nRows-2~0行,因此我们可以模拟这个行索引的变化,在遍历 s
时把每个字符填到正确的行 ss[i],在
达到 Z字形转折点时,执行反向。
public String convert(String s, int nRows) {
if(nRows <= 1)
return s;
StringBuilder[] ss = new StringBuilder[nRows];
for(int i = 0;i < nRows;i++)
ss[i] = new StringBuilder();
int flag = -1,row = 0;
for(int i = 0;i < s.length();i++){
ss[row].append(s.charAt(i));
if(row == 0 || row == nRows - 1)
flag = -flag;
row += flag;
}
StringBuilder result = new StringBuilder();
for(int i = 0;i < nRows;i++)
result.append(ss[i]);
return result.toString();
}
题目:给定一个单词数组和长度L,将该单词数组中文本两端对齐(左边和右边),使每一行都有L个字符。你要在每一行中尽可能多地填充单词。在必要时填充额外的空格' ',使每行正好有L个字符。单词之间的额外空格要尽可能均匀地分布。如果一行的空格数不能在单词之间平均分配,请在左边分配更多的空格对于最后一行文本,它应该左对齐,并且单词之间不插入额外的空格。例如,单词数组为:["This", "is", "an", "example", "of", "text", "justification."],L:16.
输出:[
"This is an",
"example of text",
"justification. "
]
分析:先计算出一行最多能够容纳的个数,再将空格数先平均分配给各个单词中间,再将多余的空格从前往后依次分配。注意有两种特殊情况,一种是 一行只有一个单词,只要在单词右侧填充空格,另一种是如果当前行为最后一行时,前面单词与单词中间只有一个空格,最后一个单词右侧填满空格。
public ArrayList fullJustify(String[] words, int L) {
ArrayList res = new ArrayList<>();
StringBuffer cur;
int i = 0;
while(i < words.length){
cur = new StringBuffer(words[i]);
int len = words[i].length(),word_num = 1,j = i + 1;
while(j < words.length && len + words[j].length() < L){
len += words[j].length() + 1;
word_num++;j++;
}
if(j == i+1)//只有一个单词
cur.append(nspace(L - words[i].length()));
else if(j == words.length){//最后一行
for(int k = i+1;k < j;k++)
cur.append(" " + words[k]);
cur.append(nspace(L - cur.length()));
}
else{
int space = (L - len) / (word_num - 1),remain = (L - len) % (word_num - 1);
String add = nspace(space + 1);
for(int k = i+1;k < j;k++){
if(k-i <= remain)
cur.append(" " + add + words[k]);
else
cur.append(add + words[k]);
}
}
res.add(cur.toString());
i = j;//勿漏,更新i的值
}
return res;
}
private String nspace(int n){
if(n == 0)
return "";
String a = nspace(n / 2);
if((n & 1) == 1)
return a + a + " ";
else
return a + a;
}
题目:请简化给出的Unix样式的文件绝对路径,也就是转换成规范路径。在Unix样式的文件系统中, .代表当前目录,.. 表示将目录向上移动一级,更多的介绍可以查看 Absolute path vs relative path in Linux/Unix。请注意,返回的规范路径必须以斜杠“/”开头,并且两个目录名之间只能有一个斜杠“/”开头。如果存在的最后一级目录的话不能以“/”结尾。另外,转化出的规范路径必须是能表示给出的绝对路径的最短字符串。例如:文件路径 = "/home/", =>"/home"文件路径 = "/a/./b/../../c/", =>"/c"特殊样例:你有考虑过样例 文件路径 ="/../"吗? 这个样例应该返回"/".另一种特殊样例是路径中可能相邻的有多个“/”,例如“/home//foo/”。这种情况下应该忽略多余的“/”,这个样例应该回"/home/foo".
分析:因为需要对".."进行返回上一级的操作,如果从前往后遍历会比较麻烦,所以可以从后往前遍历,如果遇到“..”,意味着要跳过“..”前面的一个有效目录,对这个需要跳过的目录数量进行累加。如果当前目录不是空目录也不是“.”,并且跳过的目录数量为0,就将目录名添加到简化的目录之前。
public String simplifyPath(String path) {
String[] name = path.split("/");
StringBuilder res = new StringBuilder();
int skip = 0;
for(int i = name.length - 1;i >= 0;i--){
//跳过空目录和当前目录
if(name[i].length() == 0 || name[i].equals("."))
continue;
//记录好跳过的目录数量
else if(name[i].equals(".."))
skip++;
else if(skip > 0){
skip--;
continue;
}
else
res.insert(0,"/" + name[i]);
}
if(res.length() == 0)
return "/";
else
return res.toString();
}