目录
字符串简介
字符串(String)
通常把它作为字符数组
java:String内置类型,不可更改,需要更改的话把它转化为StringBuffer,StringBuilder,char[]之类
C++:std::string可更改,也可以考虑用char[](char *)
C: 只有char[]
注意:C++中的‘+’运算符,复杂度未定义,但通常认为是线性的; C++std::string substr 和 java的String的subString参数不同
字符范围:c/c++[-128,127],我们通常转化为 [0,255] java:[0.65535]
面试总体分析
和数组相关,内容广泛
概念理解:字典序
简单操作:插入,删除字符,旋转
规则判断:(罗马数字转换 是否是合法的整数 浮点数)
数字运算 (大数加法 二进制加法)
排序,交换 (partition过程)
字符计数(hash):变位词
匹配 (正则表达式,全串匹配,kmp,周期判断)
动态规划 (LCS,编辑距离,最长回文子串)
搜索 (单词变换,排列组合)
一些例题:
例1.0-1串交换顺序
int answer=0;
for(int i=0,j=len-1;i<j;i++,j--){
for(;(i<j)&&(a[i]=='0');i++);
for(;(i<j)&&(a[j]=='1');j--);
if(i<j) ++answer;
}
例2.字符的替换和复制
删除一个字符串所有的a,并且复制所有的b。 注:字符数组足够大
分析:
先删除a,可以利用原来字符串的空间
int n=0,numb=0;
for(int i=0;s[i];i++){
if(s[i]!='a'){s[n++]=s[i];}
if(s[i]=='b'){++numb;}
}
s[n]=0;
再复制b,注意字符串要加长【1.先计算字符串里有几个b,得到复制后的长度 2.然后,倒着复制--惯用技巧】
int newlen=n+numb;
s[newlen]=0;
for(int i=newlen-1,j=n-1; j>=0; j--){
s[i--]=s[j];
if(s[j]=='b')s[i--]='b';
}
例3.交换星号
一个字符串只包含*和数字,请把它的*都放到开头
方法一 快排partition——数字相对顺序会变化
循环不变式:[0...i-1]都是*,[i...j-1]是数字,[j,n-1]未探测
for(int i=0,j=0; j<n; j++)
if(s[j]=='*') swap(s[i++],s[j]);
方法二 数字相对顺序不变
“”倒着”
int j=n-1;
for(int i=n-1;i>=0;--i) //拉过来
if(isdigits(s[i])) s[j--]=s[i];
for(; j>=0; --j) s[j]='*';
例4.子串变位词
给定两个串a和b,问b是否是a的子串的变位词,例如输入a=hello,b=lel,lle,ello,都是true;但是b=elo是false。
滑动窗口的思想
动态维护一个‘窗口’
比如b的长度是3,我们考查a[0...2],[1...3],[2...4]是否和b是变位词
如何与b比较
我们用一个hash,基于字符串的特殊性,我们可以用[0...255]或者[0...65535]的数组,我们暂且认为他们都是小写英文字母,用
[0...25]来表示b中每个单词出现的次数
我们可以存一下有多少个非0次出现的,以后用
int nonZero=0;
for(int i=0;i<lenb; i++){
if(++num[b[i]-'a']==1) ++nonZero;
}
现在,我们用子串b中的次数去减去主串a中的一个窗口内的字符种类,如果结果全是0,则找到这样的子串了。注意num[]的含义变为了字符种类差
第一个窗口[0...lenb-1] (注意lena<lenb无解)
for(int i=0;i<lenb;i++){
int c=a[i]-'a';
--num[c];
if(num[c]==0) --nonZero;
else if(num[c]==-1) ++nonZero;
}
if(nonZero==0) return true;
窗口如何滑动? 向右移动一位
新窗口 a[i-lenb+1...i]
旧窗口 a[i-lenb...i-1]
扔掉a[i-lenb] 加上a[i]
for(int i=lenb; i<lena; i++){
//扔掉a[i-lenb]
int c=a[i-lenb]-'a';
++num[c];
if(num[c]==1) ++nonZero;
else if (num[c]==0) --nonZero;
//加上a[i]
c=a[i]-'a';
--num[c];
if(num[c]==0) --nonZero;
else if (num[c]==-1) ++nonZero;
if(nonZero==0) return true;
}
例:leetcode 3 无重复字符的最长子串
public class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
Set<Character> set = new HashSet<>();
int ans = 0, i = 0, j = 0;
while (i < n && j < n) {
// try to extend the range [i, j]
if (!set.contains(s.charAt(j))){
set.add(s.charAt(j++));
ans = Math.max(ans, j - i);
}
else {
set.remove(s.charAt(i++));
}
}
return ans;
}
}
滑动窗口优化
public class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length(), ans = 0;
Map<Character, Integer> map = new HashMap<>(); // current index of character
// try to extend the range [i, j]
for (int j = 0, i = 0; j < n; j++) {
if (map.containsKey(s.charAt(j))) {//当前的j指向的字母和滑动窗口中某个位置的字母重了
i = Math.max(map.get(s.charAt(j)), i); //(若重了的字母在窗口内,index>i)i直接指向重了字母的下一个位置
}
ans = Math.max(ans, j - i + 1);
map.put(s.charAt(j), j + 1);
}
return ans;
}
}
Java(假设字符集为 ASCII 128)
以前的我们都没有对字符串 s 所使用的字符集进行假设。
当我们知道该字符集比较小的时侯,我们可以用一个整数数组作为直接访问表来替换 Map。
常用的表如下所示:
int [26] 用于字母 ‘a’ - ‘z’ 或 ‘A’ - ‘Z’
int [128] 用于ASCII码
int [256] 用于扩展ASCII码
public class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length(), ans = 0;
int[] index = new int[128]; // current index of character
// try to extend the range [i, j]
for (int j = 0, i = 0; j < n; j++) {
if(index[s.charAt(j)]!=0)
i = Math.max(index[s.charAt(j)], i);
ans = Math.max(ans, j - i + 1);
index[s.charAt(j)] = j + 1;
}
return ans;
}
}
例5.单词(字符串)翻转
翻转句子中全部的单词单词内容保持不变 例:I am a student 翻转 student a am I
in-place翻转
while(i<j) swap(s[i++],s[j--]);
用处:翻两次 : 翻转整个句子 tenduts a ma I 每个单词单独翻转 student a am I
难点:如何区分单词,找空格 split
思考:字符串循环移位abcd
移动一次:bcda
移动两次:cdab
移动三次:dabc
结论:长度为n,移动m次,相当于移动m%n次 前m%n位翻转,后n-m%n位翻转 总体再翻一次
总结
in-place(原地)
-O(1)空间 递归,堆栈空间可以不考虑
原地相关问题:
字符串循环左移,右移 快排 partition
滑动窗口
-时间复杂度:O(n) 空间复杂度:O(1)
规则相关——细致
匹配:kmp
Manacher——要求比较高的面试