字符串基础

常用API

String m = "abcde";

char ch = m.charAt(String n);//获取字符串m的第(n+1)个字符
int length = m.length();//获取字符串m的长度
boolean flag = m.equals(String n);//判断字符串m和n是否相等,严格区分大小写
boolean flag = m.equalsIgnoreCase(String n);//判断字符串m和n是否相等,不区分大小写
int len = m.index(String s);//返回字符串s在m中第一次出现的位置
int compare = m.compareTo(String anotherString);//按字典序比较两个字符串,若compare>0,m大,若compare<0,m小
String s = m.concat(n);//将字符串n拼接到字符串m的结尾
boolean flag = m.contains(String n);//判断字符串m是否包含字符串n
boolean flag = m.endsWith(String s);//判断字符串m是否以字符串s结尾
String[] s = m.split(" ");//根据正则表达式拆分字符串m
String s = m.trim();//删除字符串m的前导空格和尾部空格
String s = m.subString(int i,int j);//截取字符串m中下标为i至下标为j-1的部分,即[i,j);
...

周期串

​ 思路:从 1 1 1 开始枚举周期 T T T 的大小,然后判断每个周期内的对应字符是否相同,如果不同,则直接判断下一个 T T T

public static int cycle(String s){
    char[] ch = s.toCharArray();
    int T;
    for(T=1;T<=ch.length;T++){
        if(ch.length%T==0){//周期串的长度一定是周期T的倍数
            boolean flag = true;
            for(int start = T;start<ch.length;start++){
                if(ch[start]!=ch[start%T]){
                    flag = false;
                    break;
                }
            }
            if(flag){
                break;
            }
        }
    }
    return T;
}

​ 思路: p o s pos pos 表示第二行的字符串向右移动的格数,如果移动后,第二行的字符串与第一行字符串对应位置的字符全部相同,则 p o s pos pos 就是这个字符串的周期。

图片描述

public static int cycle(String s){
	String m = s+s;
    int pos;
    for(pos=1;pos<=s.length();pos++){
        if(s.length()%pos!=0)
            continue;
        String x = m.substring(pos,pos+s.length());
        if(x.equals(s))
            break;
    }
    return pos;
}

​ 思路:如果一个字符串 s u b sub sub 是字符串 s s s 的周期,那么将字符串 s s s 中所有的 s u b sub sub 全部替换为空字符串之后,字符串的长度如果为 0 0 0 ,就表示字符串 s u b sub sub 是字符串 s s s 的周期。

public static int cycle(String s){
    for(int i=1;i<=s.length();i++){
        if(s.length()%i==0){
            String sub = s.substring(0,i);
            if(s.replace(sub,"").length()==0)
                return i;
        }
        
    }
    return 0;
}
例题:重复字符串

题目链接:重复字符串 - 蓝桥云课 (lanqiao.cn)

图片描述

​ 思路:已知重复次数为 K K K ,那么周期就是 S . l e n g t h ( ) / K S.length() / K S.length()/K ,然后只需要求出每一个周期的第 i i i 个字符,出现次数最多的字符是哪个,然后将其余字符全部改为它,那么就将 S S S 改为了重复 K K K 次的字符串,此时修改次数也是最少的。以 a b d c b b c a a b c a abdcbbcaabca abdcbbcaabca , 重复 3 3 3 次为例:

​ 将此字符串拆分为三个部分后,每个周期写在一行,结果为:

a b d c abdc abdc
b b c a bbca bbca
a b c a abca abca

​ 只需要求出每一个竖列出现次数最多的字符出现的次数,然后将其余字符全部改为它,那么这一列修改次数为( K − m a x K - max Kmax),然后将每一列的结果加起来,即为答案。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;

public class 重复字符串 {

	static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
	static PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
	public static void main(String[] args) throws IOException {
		int n = Integer.parseInt(in.readLine());
		String a = in.readLine();
		if(a.length()%n!=0 || n>a.length()) {
			System.out.println(-1);
			return;
		}
		int t = a.length() / n;
		int index = 0;
		char[][] ch = new char[n][t];
		for(int i=0;i<n;i++) 
			for(int j=0;j<t;j++)
				ch[i][j] = a.charAt(index++);
		int ans = 0;
		for(int i=0;i<t;i++) {
			int[] num = new int[26];
			int max = 0;
			for(int j=0;j<n;j++) {
				num[ch[j][i]-'a']++;
				if(max<num[ch[j][i]-'a']) {
					max = num[ch[j][i]-'a'];
				}
			}
			ans = ans + (n-max);
		}
		System.out.println(ans);
	}
}

最长回文子串

字符串的最长回文子串,是指一个字符串中包含的最长的回文子串。

如果在 " a b c d e f f g h abcdeffgh abcdeffgh " 包含的最长回文子串就是 " f f ff ff ";

朴素做法

​ 思路:

​ 1.找到字符串 S S S 的所有子串 S ′ [ ] S'[] S[]

​ 2.然后遍历所有的子串 S ′ [ ] S'[] S[] ,然后分别判断每个子串 S ′ [ i ] S'[i] S[i] 是否回文。

​ 3.字符串 S S S 共计 n 2 n^2 n2 个子串,对于每个子串的验证是 O ( n ) O(n) O(n) 所以整体复杂度是 O ( n 3 ) O(n^3) O(n3)

import java.util.Scanner;

public  class  Main{
    static String s;
    static int isPal(int start,int end){ //s的[start,end)的左闭右开区间
        int len=end-start;
        //偶数abba型,对称轴位置没有字母
        if(len%2==0){
            int mid=start+len/2;
            //找到mid的长度,只需要枚举一半即可。
            int i=start;
            //枚举起点
            int pal=end-1;
            //对应的回文的位置
            for(;i<=mid;i++,pal--){
            // i----->    向中间枚举    <------pal
                if(s.charAt(i)!=s.charAt(pal))
                    return -1;
                //有一处不回文,那么整体不回文,返回-1 代表不回文。
            }
            return len;
            //都回文那么整体回文,返回长度
        }
        //奇数aba型,对称轴位置有字母
        else{
            int mid=start+((len-1)/2);
            //找到mid的长度,只需要枚举一半即可,因为对称轴字母必然和自己对称所以可以不考虑
            int i=start;
            //枚举起点
            int pal=end-1;
            //对应的回文的位置
            for(;i<=mid;i++,pal--){
            // i----->    向中间枚举    <------pal
                if(s.charAt(i)!=s.charAt(pal))
                    return -1;
                //有一处不回文,那么整体不回文,返回-1 代表不回文
            }
            return len;
        }
    }
    //分解子串,并记录最长回文子串的长度
    static int subString(){
        int maxn=1;
        //最短是1,比如a,b,c,d
        int len=s.length();//字符串长度
        for(int i=0;i<=len;i++) {//枚举左闭端点
            for(int j=i+1;j<=len;j++){//枚举右开端点
                maxn=Math.max(maxn, isPal(i,j));
            }
        }
        return  maxn;
    }
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        s=in.next();
        System.out.println(subString());
    }
}
中心拓展法

​ 思路:

​ 回文串都是回文的,即所见即所得,既然回文它就有必然有一个对称中心。通过枚举对称中心的位置,进行扩展那么,就可以完成对回文字符串的判定。

​ 那我们就要执行以下操作:

​ 1.枚举中心位置 n 个字符和 n-1 个字符中间位置(偶数回文)

​ 2.以中心位置向两侧延伸,直至不是回文位置

​ 3.在枚举过程中统计最大的回文子串

import java.util.Scanner;
public  class  Main{
    static String s;
    public static int expand(int l, int r) {
        while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
            //如果回文处相同且不超过字符串的范围:
            l--;
            r++;
            // 扩展
        }
        return r - l - 1;
        //不能扩展返回最大长度
    }
    public static int Palindromic() {
        if (s == null || s.length() == 0) {
            return 0;
        }
        int maxn=1;
        for (int i = 0; i < s.length(); i++) {
            int l1 = expand(i, i);       // 以字符作为中心点扩展
            int l2 = expand(i, i + 1);   // 以字符间隙作为中心点扩展
            int len = Math.max(l1, l2);
            maxn=Math.max(maxn,len);
        }
        return maxn;
    }
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        s=in.next();
        System.out.println(Palindromic());
    }
}

​ 弊端:

​ 1.由于长度的奇偶性问题,不同的对称轴要分两类情况讨论分析。

​ 2.有多次重复计算。如 c b c a c b c cbcacbc cbcacbc 第一个 c b c cbc cbc 被计算过,第二个 c b c cbc cbc 也被计算过,一整个个 c b c a b c b c cbcabcbc cbcabcbc 又被计算了一次。

M a n a c h a r Manachar Manachar 算法

​ 思路:

​ 1.预处理:在所有字符间隙中和开头结尾插入同样的符号,一般使用 # \# # ,当然只要不影响原字符串本身的符号(即不在原来的字符串中出现)都是可以的。这样可以使的所有的字符串都是奇数串,即消除了奇偶变化所带来的的差异化处理。为了防止越界处理等,一般在开头和结尾在缀上两个不一样的字符。

​ 2. M a x Max Max:最远标记距离,即目前最靠右的回文串的右端点;

P o s Pos Pos: 最远中心点,即目前最靠右的回文串的中心点;

l e n g t h [ i ] length[i] length[i]:回文半径,以 i i i 为中心扩展的回文串的半径,恰好比不扩展的原字符串的直径大 1 1 1

​ 3.假设前 i − 1 i-1 i1 个字符已经被处理过,那么将处理第 i i i 个字符,则会有下面的情况。设 j j j i i i 关于 P o s Pos Pos 的对称点, j = 2 × p o s − i j = 2 \times pos - i j=2×posi M a x L MaxL MaxL M a x Max Max 关于 P o s Pos Pos 的对称点。 M a x L = 2 × P o s − M a x MaxL = 2 \times Pos - Max MaxL=2×PosMax

​ 4.当 i < M a x i<Max i<Max 时;

​ 当以 j j j 为中心的回文串被 M a x Max Max 串包含:

图片描述

​ 此时,由于 M a x Max Max 串关于 P o s Pos Pos 回文,所以以 j j j 为对称轴回文的串,也会在 P o s Pos Pos 右侧以 i i i 为对称轴出现。

所以可以得知 l e n g t h [ i ] = l e n g t h [ j ] = l e n g t h [ 2 × P o s − i ] length[i] = length [j] = length[ 2\times Pos -i ] length[i]=length[j]=length[2×Posi]

​ 当以 j j j 为中心的回文串范围超出 M a x Max Max 串:

图片描述

​ 即 j − l e n g t h [ j ] < M a x L j-length[j] < MaxL jlength[j]<MaxL 时;

​ 不能保证超出 M a x L MaxL MaxL 左侧的字符也会在 M a x Max Max 的右侧出现,所以只能最大限度的考虑 i i i 的回文半径,那就是 M a x − i Max - i Maxi

​ 可以综合考虑二者,因为他们之间肯定存在一个大小关系,如果是第一种情况,那么有 l e n g t h [ 2 × P o s − i ] < = M a x − i length[2\times Pos -i ] <= Max -i length[2×Posi]<=Maxi , 如果是第二种情况,那么就有个 l e n g t h [ 2 × P o s − i ] > = M a x − i length[2\times Pos -i ] >= Max -i length[2×Posi]>=Maxi 的情况。

​ 因此直接使用最短那个即可。

​ 即当 i < M a x i < Max i<Max 时: l e n g t h [ i ] = m i n ( l e n g t h [ 2 × p o s − i ] , M a x − i ) length[i]=min(length[2\times pos-i],Max-i) length[i]=min(length[2×posi],Maxi)

​ 5. i ≥ M a x i\ge Max iMax 的情况

图片描述

​ 此时的 i i i 并不能参考 j j j 的处理情况,也无其他规律可循,只能按照朴素的算法处理。

import java.util.Scanner;
public  class  Main{
    static String s;
    public static int manacher(String p){
        int le=p.length();
        s="$";//前缀
        //插入分隔符,奇数化
        for(int i=0;i<le;i++){
            s=s + '#';
            s=s + p.charAt(i);
        }
        s=s+'#';;
        // 前后缀,不用考虑边界处理就能不越界
        s=s+'%';//选取不可能出现的字符即可。
        int[] length =new int[s.length()];
        //预处理字符串成为一个奇数串;
        int Max=0,pos=0,ans=0;
        le=2*le+1;
        for(int i=1;i<=le;i++){
            if(Max>i){
                length[i]=Math.min(length[2*pos-i],Max-i);
            }
            else  length[i]=1;
            //尝试扩展
            while(s.charAt(i-length[i])==s.charAt(i+length[i]))
                length[i]++;
            ans =Math.max(length[i],ans);
            if(i+length[i]>Max){
                Max=length[i]+i;
                pos=i;
            }
        }
        return ans-1;
    }
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        String p=in.next();
        System.out.println(manacher(p));
    }
}

字符串匹配问题

B F BF BF 算法

​ 思路: B F BF BF 算法是一种纯暴力的字符串匹配算法,比较符合人类自然思维方式的方法,即对源字符串和目标字符串逐个字符地进行比较。算法过程,不断的比对,直到在主串中找到目标串或者,遍历完目标串。发现 B F BF BF 算法每次 t t t 匹配失败都往后移动一个位置。

static String T="蓝桥的课程我很喜欢,蓝桥杯的题目很简单";
static String P="蓝桥杯";
static int BF(String tString, String pString){
    int i=0; //主串中的位置
    int j=0; // 模式串中的位置
    for(;i<=tString.length()-pString.length();i++){
        boolean isPatt= true;
        for(;j<pString.length();j++){
            if(tString.charAt(i+j)!=pString.charAt(j)){
                isPatt= false;
                break;
                //一旦不匹配,主串位置后退到i,模式串位置j回退到0;
            }
            // 当两个字符相同,就比较下一个
        }
        if(isPatt) return i;
    }
    return -1;
}
public static void main(String[] args) {
    System.out.println(BF(T,P));
}

​ 换一种形式

static String T="ABCDEFG";
static String P="FG";
static int BF(String s,String p) {
    int i = 0; //主串中的位置
    int j = 0; // 模式串中的位置
    int sLen = s.length();
    int pLen = p.length();
    while (i < sLen && j < pLen) {
        if (s.charAt(i)==p.charAt(j)) {    // 当两个字符相同,就比较下一个
            i++;
            j++;
        } else {
            i = i - j + 1;
            j = 0; //一旦不匹配,模式串位置j回退到0;
        }
        // System.out.println(i+":"+j);
    }
    if (j == pLen)
        return (i - j);     //主串中存在该模式返回下标号
    else
        return -1;     //主串中不存在该模式
}
public static void main(String[] args) {
    System.out.println(BF(T,P));
}
K M P KMP KMP 算法

​ **核心思路:**利用已经匹配过的这一部分有效的信息,保持主串位置 i i i 的值不变,去修改模式串的位置 j j j 的值。

​ **字符串前缀:**符号串左部的任意子串(或者说是字符串的任意首部),在 K M P KMP KMP 算法中使用的是“真”前缀,即不包含自己前缀。简单记忆方式: 前缀要找除了自己,且从头开始的所有子串。

​ **字符串后缀:**符号串右部的任意子串(或者说是字符串的任意尾部),在 K M P KMP KMP 算法中使用的是“真”后缀,即不包含自己后缀。简单记忆方式: 前缀要找除了自己,且以最后一个字符结尾的所有子串。

N e x t Next Next数组的含义:

对于模式串" a b a a b c a abaabca abaabca"而言

图片描述

出现这种从第 1 1 1 位就匹配出错的情况,即使通过人工不能找到任何优化方式,于是只能将模式串右移。

图片描述

恰好模式串的 − 1 -1 1 的位置正好置于主串的 i i i 位置,这就是 n e x t next next 的第一位补 − 1 -1 1 的原因。

再看第二种情况,部分匹配成功的情况。

仍对于模式串" a b a a b c a abaabca abaabca"而言:

设主串为" a b a a e f a g e d abaaefaged abaaefaged",那么会有:

图片描述

在主串的 i = 4 i=4 i=4 位置,模式串 j = 4 j=4 j=4 的位置发生了不匹配。

通过最优人工移动可以得到:

图片描述

恰好与主串 i i i 位置对应的值是模式串的 1 1 1 号位置。

那么与计算出的 N e x t Next Next 的数组值相同。

原因:

假设某个字符串 s s s 的最长共前后缀为 X = " a b c d . . . " X="abcd..." X="abcd...",那么这个字符串一定是一下结构,开头是 X X X 结尾是 X X X 中间可能会有重叠,匹配到 s s s 的最后一个字符失败后,那知道 X X X 肯定是匹配成功了,因为 X X X 不包含最后一个字符。

既然知道 X X X 匹配成功,那么一定知道,在主串中一定是从 i i i 位置开始且一定有一个 X X X 与模式串中的 X X X 匹配成功。

如下,点为省略号:

          i
T=........X.......
S=        X.......
          j

又已知,字符串 s s s 一定有一个后缀 X X X,那么直接用 s s s 的后缀 X X X 去匹配主串的 X X X,且 X X X 是最长公共前后缀,那么就完成了最优的转移。

s s s 是模式串的从头开始的子串时,就可以得到从某一个字符不匹配时的转移情况。

K M P KMP KMP 算法框架和 B F BF BF 大致相似,根据上面的分析,对于字符串的比对我们分为三种。

  1. T [ i ] = = P [ j ] T[i]==P[j] T[i]==P[j] 的情况

    此时,两字符相同应该继续比对所以:

    i = i + 1 i=i+1 i=i+1

    j = j + 1 j=j+1 j=j+1

  2. T [ i ] ! = P [ j ] T[i]!=P[j] T[i]!=P[j] 的情况

    此时,两字符不相同, j j j 应该根据 n e x t next next 数组进行转移,所以:

    j = n e x t [ j ] j=next[j] j=next[j]

  3. j = − 1 j=-1 j=1 的情况

    因为, j = n e x t [ j ] j=next[j] j=next[j],且 n e x t next next 第一位为 − 1 -1 1 即出现了第一位就匹配失败的情况,那应该做的是,是的模式串的开头向后移动,即:

    j = j + 1 j=j+1 j=j+1

    那么 j j j 对应 i i i 的位置也变成了 i + 1 i+1 i+1 所以:

    i = i + 1 i=i+1 i=i+1

    那么与情况 1 1 1 相同,一同考虑。

public class Main {
    static String T = "ABCDEFG";
    static String P = "AB";
    static int[] next = new int[100005];
    static void Getnext(String p) {
        int pLen = p.length();
        next[0] = -1;
        int k = -1;
        int j = 0;
        while (j < pLen - 1) {
            //p[k]表示前缀,p[j]表示后缀
            if (k == -1 || p.charAt(j) == p.charAt(k)) {
                ++k;
                ++j;
                next[j] = k;
            } else {
                k = next[k];
            }
        }
    }
    static int KMP(int sStart, String s, String p) {
        /*
        tStart 从主串的哪个位置开始,从头开始为0
        s 主串
        p 模式串
        */
        int i = sStart; //从主串的sStart位置开始
        int j = 0;
        int sLen = s.length();
        int pLen = p.length();
        while (i < sLen && j < pLen) {
            //①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
            if (j == -1 || s.charAt(i) == p.charAt(j)) {
                i++;         //继续对下一个字符比较
                j++;         //模式串向右滑动
            } else {
                j = next[j];
                //②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = nextt[j]
                //nextt[j]即为j所对应的nextt值
            }
        }
        if (j == pLen)
            return (i - j);     //主串中存在该模式返回下标号
        else
            return -1;     //主串中不存在该模式
    }
    public static void main(String[] args) {
        Getnext(P);
        System.out.println(KMP(0, T, P));
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值