数据结构--串+Java中String类常用方法

版权声明:本文为博主原创文章,如有问题,请不吝指出,转载注明出处: https://blog.csdn.net/c851204293/article/details/82782297

目录

一.引言

二.串的定义及概念

三.模式匹配算法

四.Java中String类

1,获取

2,判断

3,转换

4,替换

5,切割

6,子串。获取字符串中的一部分

7,转换,去除空格,比较


一.引言

本文先介绍下串的一些基本定义及概念,以及串中最重要的操作--模式匹配(子串的定位),最后对Java中String类的常用方法进行了总结介绍。

二.串的定义及概念

1.串的定义

串(string)是由零个或多个宇符组成的有限序列,又名叫字符串。

一般记为s = "a,a2……an" (n>0),其中,s是串的名称,用双引号(有些书中也用单引号)括起来的字符序列是串的值,注意单引号不属于串的内容。ai(1<= i<= n)可以是字母、数字或其他字符,i就是该字符在串中的位置。串中的字符数目n称为串的长度,定义中谈到“有限”是指长度n是一个有限的数值。

2.串的基本概念

子串:串中任意个连续字符组成的子序列称为该串的子串

主串:包含子串的串相应地称为主串

位置:字符在串中的序号为该字符在串中的位置。子串的第一个字符在主串中的位置表示子串在主串中的位置

例如:A=‘This is a string’   B=‘is’

BA的子串,A为主串。BA中出现了两次,首次出现所对应的主串位置是3。称BA中的序号(或位置)3

空串是任意串的子串,任意串是其自身的子串。

两个串相等:长度相等,对应位置字符都相等。

程序中使用的串可分为两种:串变量串常量

三.模式匹配算法

什么是KMP算法:

KMP是三位大牛:D.E.Knuth、J.H.Morris和V.R.Pratt同时发现的。

KMP算法要解决的问题就是在字符串(也叫主串)中的模式(pattern)定位问题。说简单点就是我们平时常说的关键字搜索。模式串就是关键字(接下来称它为P),如果它在一个主串(接下来称为T)中出现,就返回它的具体位置,否则返回-1(常用手段)。

 

首先,对于这个问题有一个很单纯的想法:从左到右一个个匹配,如果这个过程中有某个字符不匹配,就跳回去,将模式串向右移动一位。这有什么难的?

我们可以这样初始化:

 

之后我们只需要比较i指针指向的字符和j指针指向的字符是否一致。如果一致就都向后移动,如果不一致,如下图:

A和E不相等,那就把i指针移回第1位(假设下标从0开始),j移动到模式串的第0位,然后又重新开始这个步骤:

 

基于这个想法我们可以得到以下的程序:

 1 /**
 2 
 3  * 暴力破解法
 4 
 5  * @param ts 主串
 6 
 7  * @param ps 模式串
 8 
 9  * @return 如果找到,返回在主串中第一个字符出现的下标,否则为-1
10 
11  */
12 
13 public static int bf(String ts, String ps) {
14 
15     char[] t = ts.toCharArray();
16 
17     char[] p = ps.toCharArray();
18 
19     int i = 0; // 主串的位置
20 
21     int j = 0; // 模式串的位置
22 
23     while (i < t.length && j < p.length) {
24 
25        if (t[i] == p[j]) { // 当两个字符相同,就比较下一个
26 
27            i++;
28 
29            j++;
30 
31        } else {
32 
33            i = i - j + 1; // 一旦不匹配,i后退
34 
35            j = 0; // j归0
36 
37        }
38 
39     }
40 
41     if (j == p.length) {
42 
43        return i - j;
44 
45     } else {
46 
47        return -1;
48 
49     }
50 
51 

上面的程序是没有问题的,但不够好!(想起我高中时候数字老师的一句话:我不能说你错,只能说你不对~~~)

如果是人为来寻找的话,肯定不会再把i移动回第1位,因为主串匹配失败的位置前面除了第一个A之外再也没有A,我们为什么能知道主串前面只有一个A?因为我们已经知道前面三个字符都是匹配的!(这很重要)。移动过去肯定也是不匹配的!有一个想法,i可以不动,我们只需要移动j即可,如下图:

 

上面的这种情况还是比较理想的情况,我们最多也就多比较了再次。但假如是在主串“SSSSSSSSSSSSSA”中查找“SSSSB”,比较到最后一个才知道不匹配,然后i回溯,这个的效率是显然是最低的。

大牛们是无法忍受“暴力破解”这种低效的手段的,于是他们三个研究出了KMP算法。其思想就如同我们上边所看到的一样:“利用已经部分匹配这个有效信息,保持i指针不回溯,通过修改j指针,让模式串尽量地移动到有效的位置。”

所以,整个KMP的重点就在于当某一个字符与主串不匹配时,我们应该知道j指针要移动到哪

接下来我们自己来发现j的移动规律:

 

如图:C和D不匹配了,我们要把j移动到哪?显然是第1位。为什么?因为前面有一个A相同啊:

 

如下图也是一样的情况:

 

可以把j指针移动到第2位,因为前面有两个字母是一样的:

 

至此我们可以大概看出一点端倪,当匹配失败时,j要移动的下一个位置k。存在着这样的性质:最前面的k个字符和j之前的最后k个字符是一样的

如果用数学公式来表示是这样的

P[0 ~ k-1] == P[j-k ~ j-1]

(补充:为什么要这样确定k呢?我们先来整理下思路,BF算法需要一个回溯的过程,这种比较方法会造成重复比较,浪费资源,那么我们在此就想到了,我们能不能利用我们已经比较过的结果不进行回溯来进行我们接下来的比较呢?答案是肯定的。我们可以滑动一段距离来进行比较,这个滑动距离如何确定呢?我这边首先想到的是,直接滑动到不匹配处,进行接下来的比较,但是,这当中有一个问题,就是当子串与主串相同部分串(以下称为相同串)不存在子子串(相同串中的子串,非最前端子子串)与最前端的子子串(相同串中最前端的子串)相等时可以这样做,当存在相等的时候又会出现漏判断。因此,我们需要进行是否存在子子串(相同串中的子串,非最前端子子串)与最前端的子子串(相同串中最前端的子串)相等的判断,若存在,则记录下子子串(相同串中的子串,非最前端子子串)的位置作为下一次比较的起始位置,但是这样真的好吗?不好,存在两点问题,第一点:实际操作困难,需要在相同串中再进行子子串的比较,这实际提高了查找的复杂性;第二点:即便找到了,若子子串(相同串中的子串,非最前端子子串)不在相同串的结尾,那比较也没有意义,这是因为,若子子串(相同串中的子串,非最前端子子串)不在相同串的结尾,那么说明它后面还有字符,并且这个字符和最前端的子子串(相同串中最前端的子串)后面的字符肯定不相等,这便没有必要进行比较了。因此,我们确定通过上面的公式来判断滑动的距离,和比较开始的位置k)

这个相当重要,如果觉得不好记的话,可以通过下图来理解:

 

弄明白了这个就应该可能明白为什么可以直接将j移动到k位置了。

因为:

当T[i] != P[j]时

有T[i-j ~ i-1] == P[0 ~ j-1]

由P[0 ~ k-1] == P[j-k ~ j-1]

必然:T[i-k ~ i-1] == P[0 ~ k-1]

公式很无聊,能看明白就行了,不需要记住。

这一段只是为了证明我们为什么可以直接将j移动到k而无须再比较前面的k个字符。

好,接下来就是重点了,怎么求这个(这些)k呢?因为在P的每一个位置都可能发生不匹配,也就是说我们要计算每一个位置j对应的k,所以用一个数组next来保存,next[j] = k,表示当T[i] != P[j]时,j指针的下一个位置。

很多教材或博文在这个地方都是讲得比较含糊或是根本就一笔带过,甚至就是贴一段代码上来,为什么是这样求?怎么可以这样求?根本就没有说清楚。而这里恰恰是整个算法最关键的地方。

 1 public static int[] getNext(String ps) {
 2 
 3     char[] p = ps.toCharArray();
 4 
 5     int[] next = new int[p.length];
 6 
 7     next[0] = -1;
 8 
 9     int j = 0;
10 
11     int k = -1;
12 
13     while (j < p.length - 1) {
14 
15        if (k == -1 || p[j] == p[k]) {
16 
17            next[++j] = ++k;
18 
19        } else {
20 
21            k = next[k];
22 
23        }
24 
25     }
26 
27     return next;
28 
29 }

这个版本的求next数组的算法应该是流传最广泛的,代码是很简洁。可是真的很让人摸不到头脑,它这样计算的依据到底是什么?

好,先把这个放一边,我们自己来推导思路,现在要始终记住一点,next[j]的值(也就是k)表示,当P[j] != T[i]时,j指针的下一步移动位置

先来看第一个:当j为0时,如果这时候不匹配,怎么办?

 

像上图这种情况,j已经在最左边了,不可能再移动了,这时候要应该是i指针后移。所以在代码中才会有next[0] = -1;这个初始化。

如果是当j为1的时候呢?

 

显然,j指针一定是后移到0位置的。因为它前面也就只有这一个位置了~~~

下面这个是最重要的,请看如下图:

  

请仔细对比这两个图。

我们发现一个规律:

当P[k] == P[j]时,

有next[j+1] == next[j] + 1

其实这个是可以证明的:

因为在P[j]之前已经有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)

这时候现有P[k] == P[j],我们是不是可以得到P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。

即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1。

这里的公式不是很好懂,还是看图会容易理解些。

那如果P[k] != P[j]呢?比如下图所示:

像这种情况,如果你从代码上看应该是这一句:k = next[k];为什么是这样子?你看下面应该就明白了。

 

现在你应该知道为什么要k = next[k]了吧!像上边的例子,我们已经不可能找到[ A,B,A,B ]这个最长的后缀串了,但我们还是可能找到[ A,B ]、[ B ]这样的前缀串的。所以这个过程像不像在定位[ A,B,A,C ]这个串,当C和主串不一样了(也就是k位置不一样了),那当然是把指针移动到next[k]啦。

有了next数组之后就一切好办了,我们可以动手写KMP算法了:

 1 public static int KMP(String ts, String ps) {
 2 
 3     char[] t = ts.toCharArray();
 4 
 5     char[] p = ps.toCharArray();
 6 
 7     int i = 0; // 主串的位置
 8 
 9     int j = 0; // 模式串的位置
10 
11     int[] next = getNext(ps);
12 
13     while (i < t.length && j < p.length) {
14 
15        if (j == -1 || t[i] == p[j]) { // 当j为-1时,要移动的是i,当然j也要归0
16 
17            i++;
18 
19            j++;
20 
21        } else {
22 
23            // i不需要回溯了
24 
25            // i = i - j + 1;
26 
27            j = next[j]; // j回到指定位置
28 
29        }
30 
31     }
32 
33     if (j == p.length) {
34 
35        return i - j;
36 
37     } else {
38 
39        return -1;
40 
41     }
42 
43 }

和暴力破解相比,就改动了4个地方。其中最主要的一点就是,i不需要回溯了。

最后,来看一下上边的算法存在的缺陷。来看第一个例子:

显然,当我们上边的算法得到的next数组应该是[ -1,0,0,1 ]

所以下一步我们应该是把j移动到第1个元素咯:

 

不难发现,这一步是完全没有意义的。因为后面的B已经不匹配了,那前面的B也一定是不匹配的,同样的情况其实还发生在第2个元素A上。

显然,发生问题的原因在于P[j] == P[next[j]]

所以我们也只需要添加一个判断条件即可:

public static int[] getNext(String ps) {

    char[] p = ps.toCharArray();

    int[] next = new int[p.length];

    next[0] = -1;

    int j = 0;

    int k = -1;

    while (j < p.length - 1) {

       if (k == -1 || p[j] == p[k]) {

           if (p[++j] == p[++k]) { // 当两个字符相等时要跳过

              next[j] = next[k];

           } else {

              next[j] = k;

           }

       } else {

           k = next[k];

       }

    }

    return next;

} 

至此。KMP算法结束。

四.Java中String类

开始之前先开一段代码:

        String s1 = "abc";//s1是一个类类型变量, "abc"是一个对象。
						//字符串最大特点:一旦被初始化就不可以被改变。

		String s2 = new String("abc");



		System.out.println(s1==s2);
		System.out.println(s1.equals(s2));

输出结果:false,ture

s1和s2有什么区别?

  1. s1在内存中有一个对象。
  2. s2在内存中有两个对象。

String类复写了Object类中equals方法,该方法用于判断字符串是否相同(Object比较的是地址值,此处被覆写判断字符串是否相同)。

String类适用于描述字符串事物。
那么它就提供了多个方法对字符串进行操作。

常见的操作有哪些?
"abcd"

1,获取。


    1.1 字符串中的包含的字符数,也就是字符串的长度。
        int length():获取长度。

    1.2 根据位置获取位置上某个字符。
        char charAt(int index):
    1.3 根据字符获取该字符在字符串中位置。
        int indexOf(int ch):返回的是ch在字符串中第一次出现的位置。
        int indexOf(int ch, int fromIndex) :从fromIndex指定位置开始,获取ch在字符串中出现的位置。

        int indexOf(String str):返回的是str在字符串中第一次出现的位置。
        int indexOf(String str, int fromIndex) :从fromIndex指定位置开始,获取str在字符串中出现的位置。

        int lastIndexOf(int ch) :

        
2,判断。


    2.1 字符串中是否包含某一个子串。
        boolean contains(str):

        特殊之处:indexOf(str):可以索引str第一次出现位置,如果返回-1.表示该str不在字符串中存在。
                所以,也可以用于对指定判断是否包含。
                if(str.indexOf("aa")!=-1)

                而且该方法即可以判断,有可以获取出现的位置。

    2.2 字符中是否有内容。
        boolean isEmpty(): 原理就是判断长度是否为0. 
    2.3 字符串是否是以指定内容开头。
        boolean startsWith(str);
    2.4 字符串是否是以指定内容结尾。
        boolean endsWith(str);
    2.5 判断字符串内容是否相同。复写了Object类中的equals方法。
        boolean equals(str);

    2.6 判断内容是否相同,并忽略大小写。
        boolean equalsIgnoreCase();


    
3,转换。


    3.1 将字符数组转成字符串。
        构造函数:String(char[])

                  String(char[],offset,count):将字符数组中的一部分转成字符串。

        静态方法:
                static String copyValueOf(char[]);
                static String copyValueOf(char[] data, int offset, int count) 

                static String valueOf(char[]):

        
    3.2 将字符串转成字符数组。
        char[] toCharArray():

    3.3 将字节数组转成字符串。
            String(byte[])

            String(byte[],offset,count):将字节数组中的一部分转成字符串。

    3.4 将字符串转成字节数组。
            byte[]  getBytes():
    3.5 将基本数据类型转成字符串。
        static String valueOf(int)
        static String valueOf(double)

        //3+"";//String.valueOf(3);

        特殊:字符串和字节数组在转换过程中,是可以指定编码表的。

4,替换


    String replace(oldchar,newchar);

5,切割


    String[] split(regex);

6,子串。获取字符串中的一部分。


    String substring(begin);
    String substring(begin,end);

7,转换,去除空格,比较。


    7.1 将字符串转成大写或则小写。
         String toUpperCase();
         String toLowerCase();

    7.2 将字符串两端的多个空格去除。
        String trim();

    7.3 对两个字符串进行自然顺序的比较。
        int compareTo(string);

具体实现代码如下:

class  StringMethodDemo
{

	public static void method_7()
	{
		String s = "    Hello Java     ";
		sop(s.toLowerCase());
		sop(s.toUpperCase());
		sop(s.trim());

		String s1 = "a1c";
		String s2 = "aaa";

		sop(s1.compareTo(s2));
	}
	public static void method_sub()
	{
		String s = "abcdef";

		sop(s.substring(2));//从指定位置开始到结尾。如果角标不存在,会出现字符串角标越界异常。
		sop(s.substring(2,4));//包含头,不包含尾。s.substring(0,s.length());
	}

	public static void  method_split()
	{
		String s = "zhagnsa,lisi,wangwu";

		String[] arr  = s.split(",");

		for(int x = 0; x<arr.length; x++)
		{
			sop(arr[x]);
		}
	}

	public static void method_replace()
	{
		String s = "hello java";

		//String s1 = s.replace('q','n');//如果要替换的字符不存在,返回的还是原串。


		String s1 = s.replace("java","world");
		sop("s="+s);
		sop("s1="+s1);
	}

	public static void method_trans()
	{
		char[] arr = {'a','b','c','d','e','f'};

		String s= new String(arr,1,3);

		sop("s="+s);

		String s1 = "zxcvbnm";

		char[] chs = s1.toCharArray();

		for(int x=0; x<chs.length; x++)
		{
			sop("ch="+chs[x]);
		}
	}
	public static void method_is()
	{
		String str = "ArrayDemo.java";
		
		//判断文件名称是否是Array单词开头。
		sop(str.startsWith("Array"));
		//判断文件名称是否是.java的文件。
		sop(str.endsWith(".java"));
		//判断文件中是否包含Demo
		sop(str.contains(".java"));


	}


	public static void method_get()
	{
		String str = "abcdeakpf";

		//长度
		sop(str.length());

		//根据索引获取字符。
		sop(str.charAt(4));//当访问到字符串中不存在的角标时会发生StringIndexOutOfBoundsException。


		//根据字符获取索引
		sop(str.indexOf('m',3));//如果没有找到,返回-1.
		
		//反向索引一个字符出现位置。
		sop(str.lastIndexOf("a"));

		
	}
	public static void main(String[] args) 
	{
		method_7();
//		method_trans();
//		method_is();
//		method_get();
		/*
		String s1 = "abc";
		String s2 = new String("abc");

		String s3 = "abc";
		System.out.println(s1==s2);
		System.out.println(s1==s3);

		*/
	}

	public static void sop(Object obj)
	{
		System.out.println(obj);
	}


}

猜你喜欢

转载自blog.csdn.net/c851204293/article/details/82782297