【数据结构与算法】数据结构与算法之字符串String(Java版)

如果觉得有帮助,麻烦动动手指点赞加关注💗💗💗 非常感谢!!!

有想看源码的小伙伴请移步这里👉https://gitee.com/fearless123/demo/tree/master/src/main/java/com/ma

一、串


1. 定义

   串(也称字符串)是由n(n>=0)个字符串组成的有限序列。抽象含义的串一般记为s=“s1,s2…sn”,其中s为串名,n 为串长度,双引号括起来的字符序列成为串值。每个字符si(0<i<n)可以是任意的Unicode码字符,一般是字母、数子、标点符号等屏幕可显示的字符。
   含有零个元素的串称为空串,串中任意连续的字符组成的字符序列作为该串的子串,包含子串的串称为主串。要注意的是,空格也是串字符集合的一个元素,由一个或多个空格组成的串称为空格串。(空格串不是空串)

2. 存储结构
  • 定长顺序存储表示,用字符数组来存储,变量Length来专门存储串的长度
  • 变长分配存储表示,用指针变量指向动态分配存储区的首地址,同样变量Length来专门存储串的长度
3. 基本操作

下面是Java中String类常用的api

1、获取长度
public int length(); // 这与数组中的length属性获取长度不同

2、比较相等
public boolean equals(Object anObject);//判断内容是否相同

public boolean equalsIgnoreCase(String anotherString);//忽略大小写的情况下判断内容是否相同
如果想对字符串中的部分内容是否相同进行比较,可以用reagionMatches() 有两种
public boolean regionMatches(int toffset, String other,int ooffset,int len);表示如果String对象的一个子字符串与参数other的一个子字符串是相同的字符序列,则为true.要比较的String对象的字符串从索引toffset开始,other的字符串从索引ooffset开始,长度为len。
public boolean reagionMatches(boolean ignoreCase,int toffset,String other,int ooffset,int len);//用布尔类型的参数指明两个字符串的比较是否对大小写敏感。

3、比较大小
public int compareTo(String anotherString); //判断字符串的大小关系
public int compareToIgnoreCase(String str); //在比较时忽略字母大小写

4、查找字符串中某个位置的字符
public char charAt(int index);//返回指定索引index位置上的字符,索引范围从0开始

5、查找指定字符串在字符串中第一次或最后一词出现的位置
在String类中提供了两种查找指定位置的字符串第一次出现的位置的方法
public int indexOf(String str);//从字符串开始检索str,并返回第一次出现的位置,未出现返回-1
public int indexOf(String str,int fromIndex);//从字符串的第fromIndex个字符开始检索str
查找最后一次出现的位置有两种方法
public int lastIndexOf(String str);
public int lastIndexOf(String str,int fromIndex);
如果不关心字符串的确切位置则可使用public boolean contains(CharSequence s);

6、检查字符串的起始字符和结束字符
开始的字符串两种方法
public boolean starWith(String prefix,int toffset);//如果参数prefix表示的字符串序列是该对象从索引toffset处开始的子字符串,则返回true
public boolean starWith(String prefix);
结束的字符串方法
public boolean endsWith(String suffix);

7、截取子串
public String subString(int beginIndex);// 返回以beginIndex开头直至该字符串结尾的串
public String subString(int beginIndex,int endIndex);//返回的字符串是从beginIndex开始到endIndex-1的串,要返回后4位可以这样写Syetem.out.println(*.subString()(*.length()-4));

8、字符串的替换
public String replace(char oldChar,char newChar);
public String replace(CharSequence target,CharSequence replacement);//把原来的etarget子序列替换为replacement序列,返回新串
public String replaceAll(String regex,String replacement);//用正则表达式实现对字符串的匹配

9、字符串的大小写替转换
public String toLowerCase(Locale locale);
public String toLowerCase();
public String toupperCase(Locale locale);
public String toUpperCase();

10、去除字符串首尾空格
public String trim();

11、字符串转换
字符串转换成字符数组
public char[] toCharArray();
将字符串转换成字符串数组
public String[] split(String regex);//regex 是给定的匹配
将其它数据类型转化为字符串
public static String valueOf(T t);
4. KMP算法及改进

1)简单模式匹配算法

对于一个串中的子串的定位操作称为串的模式匹配,其中待定位的子串称为模式串。简答模式匹配算法的基本思想是:从主串的第一个位置起和模式串的第一个字符开始比较,如果相等,则继续逐一比较后续字符;否则从主串的第二字符开始,再重新用上一步的方法与模式串中的字符做比较,依次类推,直到比较完模式串中的所有字符。

    /**
     * 简单模式匹配算法,相当于java.lang.String的indexOf
     * @param str
     * @param substr
     * @return
     */
    public static int match(String str, String substr){
        // 1.从主串的第一个位置起和模式串的第一个字符开始比较,如果相等,则继续逐一比较后续字符;
        // 否则从主串的第二字符开始,再重新用上一步的方法与模式串中的字符做比较,依次类推,直到比较完模式串中的所有字符。
        int index = -1;
        boolean match = true;
        for (int i = 0; i < str.length() - substr.length(); i++) {
            match = true;
            for (int j = 0; j < substr.length(); j++) {
                if (str.charAt(i + j) != substr.charAt(j)){
                    match = false;
                }
            }
            if (match){
                index = i;
                break;
            }
        }
        return index;
    }

2)KMP算法

  Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。下面先直接给出KMP的算法流程(如果感到一点点不适,没关系,坚持下,稍后会有具体步骤及解释)

假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置

  • 如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;
  • 如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了 j - next [j] 位。
    换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值,即移动的实际位数为:j - next[j],且此值大于等于1。

  很快,你也会意识到next 数组各值的含义:代表当前字符前半部分子串的最长相同前后缀的长度。例如,如果next [j] = k,代表j 之前的字符串中有最大长度为k 的相同前缀后缀。
  此也意味着在某个字符失配时,该字符对应的next 值会告诉你下一步匹配中,模式串应该跳到哪个位置(跳到next [j] 的位置)。如果next [j] 等于0或-1,则跳到模式串的开头字符,若next [j] = k 且 k > 0,代表下次匹配跳到j 之前的某个字符,而不是跳到开头,且具体跳过了k 个字符。

寻找最大前后缀

如果给定的模式串是:“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示:
在这里插入图片描述
也就是说,原模式串子串对应的各个前缀后缀的公共元素的最大长度表为(下简称《最大长度表》):
在这里插入图片描述
注:求解最大相同前后缀详细流程可以点这里 👉阮一峰 老师的KMP算法的讲解


根据《最大长度表》求next 数组

  由上文,我们已经知道,字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为:
在这里插入图片描述
而且,根据这个表可以得出下述结论:失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值。
  上文利用这个表和结论进行匹配时,我们发现,当匹配到一个字符失配时,其实没必要考虑当前失配的字符,更何况我们每次失配时,都是看的失配字符的上一位字符对应的最大长度值。如此,便引出了next 数组。可求得它的next 数组如下:
在这里插入图片描述
把next 数组跟之前求得的最大长度表对比后,不难发现,next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1。意识到了这一点,你会惊呼原来next 数组的求解竟然如此简单:就是找最大对称长度的前缀后缀,然后整体右移一位,初值赋为-1(当然,你也可以直接计算某个字符对应的next值,就是看这个字符之前的字符串中有多大长度的相同前缀后缀)。
  换言之,对于给定的模式串:ABCDABD,它的最大长度表及next 数组分别如下:
在这里插入图片描述
  根据最大长度表求出了next 数组后,从而有另外一个结论:失配时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值

  而后,你会发现,无论是基于《最大长度表》的匹配,还是基于next 数组的匹配,两者得出来的向右移动的位数是一样的。为什么呢?因为:

  • 根据《最大长度表》,失配时,模式串向右移动的位数 = 已经匹配的字符数 - 失配字符的上一位字符的最大长度值
  • 而根据《next 数组》,失配时,模式串向右移动的位数 = 失配字符的位置 - 失配字符对应的next 值

  其中,从0开始计数时,失配字符的位置 = 已经匹配的字符数(失配字符不计数),而失配字符对应的next 值 = 失配字符的上一位字符的最大长度值,两相比较,结果必然完全一致。
  所以,你可以把《最大长度表》看做是next 数组的雏形,甚至就把它当做next 数组也是可以的,区别不过是怎么用的问题。(利用最大长度表也可以实现kmp算法)

递推计算next 数组(KMP算法的核心部分)

接下来,咱们来写代码求下next 数组。
基于之前的理解,可知计算next 数组的方法可以采用递推:

  1. 如果对于值k,已有p0 p1, …, pk-1 = pj-k pj-k+1, …, pj-1,相当于next[j] = k。
    此意味着什么呢?究其本质,next[j] = k 代表p[j] 之前的模式串子串中,有长度为k 的相同前缀和后缀。有了这个next 数组,在KMP匹配中,当模式串中j 处的字符失配时,下一步用next[j]处的字符继续跟文本串匹配,相当于模式串向右移动j - next[j] 位。

KMP代码如下:

下面是代码实现:


    /**
     * 根据 subStr 字符串 来计算出对应的部分匹配值
     * @param subStr
     * @return
     */
    private static int calcMathValue(String subStr){

        int length = subStr.length();
        String prefixStr = subStr.substring(0, length - 1);
        String suffixStr= subStr.substring(1);

        while (prefixStr.length() > 0 && suffixStr.length() > 0){
            if (prefixStr.equals(suffixStr)){
                return prefixStr.length();
            }

            if (prefixStr.length() == 1 && suffixStr.length() == 1){
                break;
            }
            prefixStr = prefixStr.substring(0, prefixStr.length() - 1);
            suffixStr = suffixStr.substring(1, suffixStr.length());
        }
        return 0;
    }

    /**
     * 根据模式串 pattern 创造出对应的部分匹配表
     * @param pattern
     * @return
     */
    private static int[] createPartialMatchTable(String pattern){

        int patternLen = pattern.length();
        int[] matchTalbe = new int[patternLen];

        int i = 0;
        int matchValue = 0;
        while(i < patternLen){
            if (i == 0){
                matchValue = 0;
            } else {
                matchValue = calcMathValue(pattern.substring(0, i + 1));
            }

            matchTalbe[i] = matchValue;
            i++;
        }
        return matchTalbe;
    }

    /**
     * 使用KMP算法算出pattern字符串在target字符串当中的位置
     *  -1 代表不存在,即主串中的所有子串没有与模式串相匹配的
     *  resultIndex 返回模式串在主串中的初始位置
     * @param target
     * @param pattern
     * @return
     */
    private static int KMP(String target, String pattern){
        int[] partialMatchTable = createPartialMatchTable(pattern);

        char[] targetCharArr = target.toCharArray();
        char[] patternCharArr = pattern.toCharArray();
        int matchCharCounts = 0;    // 记录下已经匹配的字符的个数

        int i = 0, j = 0, moveCounts = 0;
        int resultIndex = -1;
        boolean firstMatchFlg = false;
        while(i < targetCharArr.length){

            // 如果当前主串和子串的字符匹配上了,那么比较下一字符是否匹配
            if (targetCharArr[i] == patternCharArr[j]){
                // 记录模式串在主串的初始位置
                if (!firstMatchFlg){
                    firstMatchFlg = true;
                    resultIndex = i;
                }
                matchCharCounts++;
                i++;
                j++;
            }
            // 如果子串的第一个元素都不和子串的元素相等,那么就拿主串的下一个元素进行比较
            else if (j == 0){
                i++;
            }
            // 如果子串不是在第一个元素的位置而是在其他位置进行了匹配,那么进行移位操作
            else {
                // 移动位数 = 已匹配的字符数 - 对应的部分匹配值
                // 对应匹配值 指的是最后一个字符的对应匹配值 j是失配的位置 所以这里是partialMatchTable[j - 1]
                moveCounts = matchCharCounts - partialMatchTable[j - 1];
                j = j - moveCounts; // 移动模式串 往前移动moveCounts位
                matchCharCounts = matchCharCounts - moveCounts;
                firstMatchFlg = false;  // 使首位下标记录失效
                resultIndex = -1;
            }

            // 如果匹配成功了 直接返回首位下标位置
            if (j == patternCharArr.length){
                return resultIndex;
            }
        }
        return resultIndex;
    }

该算法比较难理解,感兴趣的同学可以点这里👇

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值