Java学习笔记 | 4万字长文详细介绍Java数据类型:字符串及其数据结构 | 字符串底层原理 | 正则表达式书写语法| Regex | 字符串匹配 | KPM模式匹配 | 朴素的模式匹配

Alt
在这里插入图片描述

🙋大家好!我是毛毛张!
🌈个人首页: 神马都会亿点点的毛毛张

Alt

📖本节内容是对Java字符串相关知识点的总结,由于这三部分具有相关性🌉,毛毛张把它们串在一起🍡进行总结,主要包含三个部分:第1️⃣部分是Java中String类的使用;第2️⃣部分是和字符串相关的正则表达式;第3️⃣部分是介绍字符串这个数据结构

文章目录

1. String类

1.1 String类概述

  • String 类代表字符串,Java 程序中的所有字符串文字(例如“abc”)都被实现为此类的实例。
  • 定义: Java 程序中所有的双引号字符串,都是 String 类的对象。
  • 所属包: String 类在 java.lang 包下,所以使用的时候不需要导包。

1.2 String类的特点

  • 字符串不可变,它们的值在创建后不能被更改
  • 虽然 String 的值是不可变的,但是它们可以被共享
  • 字符串效果上相当于字符数组( char[] ),但是底层原理是字节数组( byte[] )

1.3 创建字符串对象

  • 通过构造方法创建

    • 通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同
      方式方法名说明
      利用构造方法public String()创建一个空白字符串对象,不含有任何内容
      利用构造方法public String(String original)根据传入的字符串,创建字符串对象
      利用构造方法public String(char[] chs)根据字符数组的内容,来创建字符串对象
      利用构造方法public String(byte[] bys)根据字节数组的内容,来创建字符串对象
  • 直接赋值方式创建

    • 语法:String s = “abc”;
    • 该方式创建的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会建立一个 String 对象,并在字符串池中维护
  • 代码演示:

    public class StringDemo01 {
        public static void main(String[] args) {
            //public String():创建一个空白字符串对象,不含有任何内容
            String s1 = new String();
            System.out.println("s1:" + s1);
            
            //public   String(String original):根据传入的字符串,创建字符串对象
    		String s2 = new String("abc");
            System.out.println("s2:" + s2);
            
            //public String(char[] chs):根据字符数组的内容,来创建字符串对象
            char[] chs = {'a', 'b', 'c'};
            String s3 = new String(chs);
            System.out.println("s3:" + s3);
    
            //public String(byte[] bys):根据字节数组的内容,来创建字符串对象
            //应用场景:以后在网络当中传输的数据都是字节信息,一般要把字节信息转成字符串
            byte[] bys = {97, 98, 99};
            String s4 = new String(bys);
            System.out.println("s4:" + s4);
    
            String s5 = "abc";//直接赋值的方式创建字符串对象,内容就是abc
            System.out.println("s5:" + s5);
        }
    }
    

1.4 String和基本数据类型的转换

1.4.1 基本数据类型转String类型

  • 语法:将基本类型的值+”“即可

  • 代码演示:

    //基本数据类型 转 字符串
    int n1 =100;
    float n2 = 1.2F;
    double n3 = 3.4;
    boolean b1 = true;
    String str1 = n1 + "";
    String str2 = n2 + "";
    String str3 = n3 + "";
    String str4 = b1 + "";
    System.out.println(str1 + " " + str2 + " " + str3 + " " + str4);
    

1.4.2 String类型转基本数据类型

  • 语法:通过基本类型的包装类调用parseXX方法即可

  • 代码演示:

    //字符串 转 对应的基本数据类型
    String s5  = "123";
    //会在 OOP 讲对象和方法的时候会详细讲解
    int num1 = Integer.parseInt(s5);
    double num2 = Double.parseDouble(s5);
    float num3 = Float.parseFloat(s5);
    short num4 = Short.parseShort(s5);
    long num5 = Long.parseLong(s5);
    byte num6 = Byte.parseByte(s5);
    boolean b = Boolean.parseBoolean("true");
    System.out.println("========");
    System.out.println(num1);//123
    System.out.println(num2);//123.0
    System.out.println(num3);//123.0
    System.out.println(num4);//123
    System.out.println(num5);//123
    System.out.println(num6);//123
    System.out.println(b);//true
    
    
    //怎么把字符串转成字符 char -> 含义是指 把字符串的第一个字符得到
    //解读 s5.charAt(0) 得到 s5 字符串的第一个字符 '1'
    System.out.println(s5.charAt(0));
    

1.4.3 细节

  • 在将 String 类型转成 基本数据类型时,要确保String类型能够转成有效的数据 ,比如 我们可以把 “123” , 转成一个整数,但是不能把 “hello” 转成一个整数

  • 如果格式不正确,就会抛出异常NumberFormatException,程序就会终止

  • 代码演示:

    //1) 在将 String 类型转成 基本数据类型时,要确保String类型能够转成有效的数据 ,
    //比如 我们可以把 "123" , 转成一个整数,但是不能把 "hello" 转成一个整数
    String str = "123";
    //转成int
    int n1 = Integer.parseInt(str);
    System.out.println(n1);
    
    //2) 如果格式不正确,就会抛出异常,程序就会终止, 这个问题在异常处理章节中会处理
    //String str2 = "hello";
    转成int
    //int n2 = Integer.parseInt(str2);//Exception in thread "main" java.lang.NumberFormatException: For input string: "hello"
    //System.out.println(n2);
    

1.5 字符串的比较

1.5.1 ==号的作用

  • 比较基本数据类型:比较的是具体的值
  • 比较引用数据类型:比较的是对象地址值

1.5.2 equals方法的作用

  • 方法介绍

    方法说明
    public boolean equals(String s) 比较两个字符串内容是否相同、区分大小写
    public boolean equalsIgonreCase(String s) 忽略大小写的比较,应用:验证码的比较
  • 示例演示:

    public class StringDemo02 {
        public static void main(String[] args) {
            //构造方法的方式得到对象
            char[] chs = {'a', 'b', 'c'};
            String s1 = new String(chs);
            String s2 = new String(chs);
    
            //直接赋值的方式得到对象
            String s3 = "abc";
            String s4 = "abc";
    
            //比较字符串对象地址是否相同
            System.out.println(s1 == s2);//false
            System.out.println(s1 == s3);//false
            System.out.println(s3 == s4);//true
            System.out.println("--------");
    
            //比较字符串内容是否相同
            System.out.println(s1.equals(s2));//true
            System.out.println(s1.equals(s3));//true
            System.out.println(s3.equals(s4));//true
        }
    }
    
  • 注意细节:

    • 通过键盘输入的字符串在底层是属于new创建的对象实例,因此在比较的时候属于一个新的对象。
      • 代码演示:

        public class Test1 {
            public static void main(String[] args) {
                /*
                	已知用户名和密码,请用程序实现模拟用户登录。总共给三次机会,登录之后,给出相应的提示
                */
                	
                //1.定义两个变量用来记录正确的用户名和密码
                String rightUsername = "itheima";
                String rightPassword = "1234qwer";
        
                //2.键盘录入用户名和密码
                //ctrl + alt + T 选择包裹方式
        
                for (int i = 0; i < 3; i++) {//0 1 2
                    Scanner sc = new Scanner(System.in);
                    System.out.println("请输入用户名");
                    String username = sc.next();
                    System.out.println("请输入密码");
                    String password = sc.next();
        
                    //3.判断比较
                    if (username.equals(rightUsername) && password.equals(rightPassword)) {
                        System.out.println("登录成功");
                        //如果正确,循环结束
                        break;
                    } else {
                        //最后一次机会
                        if(i == 2){
                            System.out.println("账户" + username + "被锁定,请联系黑马程序员官方小姐姐:XXXXXXX");
                        }else{
                            //不是最后一次机会
                            System.out.println("用户名或密码错误,登录失败,还剩下" + (2 - i) + "次机会");//2 1 0
                        }
                    }
                }
        
            }
        }
        

1.6 String常用方法

方法说明
public char[] toCharArray()将字符串转换为字符数组
public char chatAt(int index)根据索引获取对应的字符
public int length()返回字符串的长度
public String substring(int beginIndex)从开始索引截取到末尾
public String substring(int beginIndex, int endIndex)根据开始和结束索引做截取, 包含头不包含尾
public String replace(char oldChar, char newChar)替换
public String[] split(String regex)根据传入的字符串作为规则进行切割,将切割后的内容存入字符串数组中,并返回
  1. 字符串直接遍历案例:

    • 键盘录入一个字符串,使用程序实现在控制台遍历该字符串

    • 代码演示:

      public class Test2 {
          public static void main(String[] args) {
              //1.键盘录入一个字符串
              Scanner sc = new Scanner(System.in);
              System.out.println("请输入字符串");
              String str = sc.next();
              System.out.println(str);
      
              //2.遍历
              for (int i = 0; i < str.length(); i++) {
                  //i 依次表示字符串的每一个索引
                  //索引的范围:0 ~  长度-1
      
                  //根据索引获取字符串里面的每一个字符
                  //ctrl + alt + V 自动生成左边的接受变量
                  char c = str.charAt(i);
                  System.out.println(c);
              }
          }
      }
      
      
  2. 统计字符次数案例

    • 键盘录入一个字符串,统计该字符串中大写字母字符,小写字母字符,数字字符出现的次数(不考虑其他字符)

    • 代码演示:

      public class Test4 {
          public static void main(String[] args) {
              //键盘录入一个字符串,统计大写,小写,数字出现的次数
      
      
              //1.键盘录入一个字符串
              Scanner sc = new Scanner(System.in);
              System.out.println("请输入一个字符串");
              String str = sc.next();
      
      
              //2.统计 --- 计数器count
              //此时我要统计的有3样东西,所以要定义3个计数器分别进行统计
              int bigCount = 0;
              int smallCount = 0;
              int numberCount = 0;
              //得到这个字符串里面每一个字符
              for (int i = 0; i < str.length(); i++) {
                  //i 表示字符串中的索引
                  //c 表示字符串中的每一个字符
                  char c = str.charAt(i);
      
                  //对c进行判断
                  if (c >= 'a' && c <= 'z') {
                      smallCount++;
                  }else if(c >= 'A' && c <= 'Z'){
                      bigCount++;
                  }else if(c >= '0' && c <= '9'){
                      numberCount++;
                  }
              }
      
              //3.当循环结束之后,三个变量记录的就是对应的个数
              System.out.println("大写字符有:" + bigCount + "个");
              System.out.println("小写字符有:" + smallCount + "个");
              System.out.println("数字字符有:" + numberCount + "个");
          }
      }
      
  3. 数组拼接成字符串

    • 定义一个方法,把 int 数组中的数据按照指定的格式拼接成一个字符串返回,调用该方法,并在控制台输出结果。例如,数组为 int[] arr = {1,2,3}; ,执行方法后的输出结果为:[1, 2, 3]

    • 代码演示:

      public class Test5{
          public static void main(String[] args) {
              //定义一个方法,把 int 数组中的数据按照指定的格式拼接成一个字符串返回,调用该方法,
              //并在控制台输出结果。例如,数组为 int[] arr = {1,2,3};
              //执行方法后的输出结果为:[1, 2, 3]
      
              int[] arr = {1, 2, 3, 4, 5};
      
              String str = arrToString(arr);
              System.out.println(str);
          }
      
      
          //作用:把一个数组变成字符串
          public static String arrToString(int[] arr) {
              String s = "";
              //拼接左括号
              s = s + "["; //此时是拿着长度为0的字符串,跟[进行拼接,产生一个新的字符串。
              //把新的字符串再赋值给s,此时变量s记录的就是新的字符串"["的地址值
      
              //下面我想得到数组里面的每一个元素并进行拼接
              //那么就需要遍历数组,得到每一个元素才行
              for (int i = 0; i < arr.length; i++) {
                  //假设第一次循环:i = 0 获取的就是0索引上的元素
                  //在拼接的时候:"[" + 1 + ", " 拼接完毕之后产生一个新的字符串 "[1, "
                  //第二次循环:i = 1 获取的就是1索引上的元素
                  //在拼接的时候: 此时s就是第一次循环结束后拼接完毕的结果:"[1, "
                  //在拼接的时候:"[1, " + 2 + ", " 拼接完毕之后产生一个新的字符串 "[1, 2, "
                  //...
                 if(i == arr.length - 1){
                     //如果是最后一个元素,那么不需要拼接逗号空格
                     s = s + arr[i];
                 }else{
                     //如果不是最后一个元素,需要拼接元素和逗号空格
                     s = s + arr[i] + ", ";
                 }
              }
              //等循环结束之后,再拼接最后一个右括号
              s = s + "]";
              return s;
          }
      
      
          //用来遍历数组
          public static void printArr(int[] arr) {
              System.out.print("[");
              for (int i = 0; i < arr.length; i++) {
                  if (i == arr.length - 1) {
                      System.out.print(arr[i]);
                  } else {
                      System.out.print(arr[i] + ", ");
                  }
              }
              System.out.println("]");
      
              //[1, 2, 3, 4, 5]
              //我们现在要知道,这个最终结果是怎么来的?
              //从到右依次打印得来的。
          }
      }
      
  4. 字符串反转案例

    • 定义一个方法,实现字符串反转。键盘录入一个字符串,调用该方法后,在控制台输出结果。例如,键盘录入 abc,输出结果 cba

    • 代码演示:

      public class Test6反转字符串 {
          public static void main(String[] args) {
              /*定义一个方法,实现字符串反转。键盘录入一个字符串,调用该方法后,在控制台输出结果
              例如,键盘录入 abc,输出结果 cba*/
      
      
      
              //1.定义一个字符串
              Scanner sc = new Scanner(System.in);
              System.out.println("请输入一个字符串");
              String str = sc.next();
              //2.定义一个方法,反转字符串
              //abc  --->  cba
              //可以把字符串倒着遍历,再拼接
              String result = reverse(str);
              System.out.println(result);
      
      
          }
      
          //注释:方法的作用就是反转字符串
          //把传递进来的字符串进行反转
          public static String reverse(String str){//abc
              //核心思想:倒着遍历并进行拼接就可以了
              //fori :正着遍历  forr:倒着遍历
              String s = "";
              for (int i = str.length() - 1; i >= 0; i--) {
                  //i 依次表示字符串里面的每一个索引(倒着的)
                  //我们就可以拿到里面的每一个字符并拼接
                  s = s + str.charAt(i);
              }
      
              //把倒着拼接之后的结果返回即可
              return s;
      
          }
      }
      
      
  5. 金额转换

    • 把2135变成:零佰零拾零万贰仟壹佰叁拾伍元 ;把789变成:零佰零拾零万零仟柒佰捌拾玖元

    • 代码演示:

      package com.itheima.stringdemo;
      
      import java.util.Scanner;
      
      public class StringDemo9 {
          public static void main(String[] args) {
              //1.键盘录入一个金额
              Scanner sc = new Scanner(System.in);
              int money;
              while (true) {
                  System.out.println("请录入一个金额");
                  money = sc.nextInt();
                  if (money >= 0 && money <= 9999999) {
                      break;
                  } else {
                      System.out.println("金额无效");
                  }
              }
      
              //定义一个变量用来表示钱的大写
              String moneyStr = "";
      
              //2.得到money里面的每一位数字,再转成中文
              while (true) {//2135
                  //从右往左获取数据,因为右侧是数据的个位
                  int ge = money % 10;
                  String capitalNumber = getCapitalNumber(ge);
                  //把转换之后的大写拼接到moneyStr当中
                  moneyStr = capitalNumber + moneyStr;
                  //第一次循环 : "伍" + "" = "伍"
                  //第二次循环 : "叁" + "伍" = "叁伍"
                  //去掉刚刚获取的数据
                  money = money / 10;
      
                  //如果数字上的每一位全部获取到了,那么money记录的就是0,此时循环结束
                  if (money == 0) {
                      break;
                  }
              }
      
              //3.在前面补0,补齐7位
              int count = 7 - moneyStr.length();
              for (int i = 0; i < count; i++) {
                  moneyStr = "零" + moneyStr;
              }
              System.out.println(moneyStr);//零零零贰壹叁伍
      
              //4.插入单位
              //定义一个数组表示单位
              String[] arr = {"佰","拾","万","仟","佰","拾","元"};
              //               零    零   零   贰   壹   叁   伍
      
              //遍历moneyStr,依次得到 零    零   零   贰   壹   叁   伍
              //然后把arr的单位插入进去
      
              String result = "";
              for (int i = 0; i < moneyStr.length(); i++) {
                  char c = moneyStr.charAt(i);
                  //把大写数字和单位拼接到result当中
                  result = result + c + arr[i];
              }
      
              //5.打印最终结果
              System.out.println(result);
      
          }
      
      
          //定义一个方法把数字变成大写的中文
          //1 -- 壹
          public static String getCapitalNumber(int number) {
              //定义数组,让数字跟大写的中文产生一个对应关系
              String[] arr = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"};
              //返回结果
              return arr[number];
          }
      
      }
      
  6. 手机号屏蔽

    • 需求:以字符串的形式从键盘接受一个手机号,将中间四位号码屏蔽,最终效果为:131****9468

    • 代码演示:

      public class Test8{
          public static void main(String[] args) {
              /*以字符串的形式从键盘接受一个手机号,将中间四位号码屏蔽
              最终效果为:131****9468*/
      
              //1.键盘录入一个手机号码
              Scanner sc = new Scanner(System.in);
              System.out.println("请输入手机号码");
              String phoneNumber = sc.next();//13112349408
      
              //2.截取手机号码中的前三位
              String star = phoneNumber.substring(0, 3);
      
              //3.截取手机号码中的最后四位
              //此时我用substring方法,是用1个参数的,还是两个参数的?1个参数的会更好
              //因为现在我要截取到最后,所以建议使用1个参数的。
              String end = phoneNumber.substring(7);
      
              //4.拼接
              String result = star + "****" + end;
      
              System.out.println(result);
      
          }
      }
      
      
  7. 敏感词替换

    • 需求1:键盘录入一个 字符串,如果字符串中包含(TMD),则使用***替换

      public class Test9 {
          public static void main(String[] args) {
              //1.定义一个变量表示骂人的话
              String talk = "后裔你玩什么啊,TMD";
      
      
              //2.把这句话中的敏感词进行替换
              String result = talk.replace("TMD", "***");
      
              //3.打印
              System.out.println(talk);
              System.out.println(result);
          }
      }
      
      
    • 需求2:如果要替换的敏感词比较多怎么办?

      public class Test10 {
          public static void main(String[] args) {
              //实际开发中,敏感词会有很多很多
      
              //1.先键盘录入要说的话
              Scanner sc = new Scanner(System.in);
              System.out.println("请输入要说的话");
              String talk = sc.next();//后裔你玩什么啊,TMD,GDX,ctmd,ZZ
      
              //2.定义一个数组用来存多个敏感词
              String[] arr = {"TMD","GDX","ctmd","ZZ","lj","FW","nt"};
      
              //3.把说的话中所有的敏感词都替换为***
      
              for (int i = 0; i < arr.length; i++) {
                  //i 索引
                  //arr[i] 元素 --- 敏感词
                  talk = talk.replace(arr[i],"***");
              }
      
              //4.打印结果
              System.out.println(talk);//后裔你玩什么啊,***,***,***,***
      
          }
      }
      
  8. 身份证信息查看

    • 身份证的每一位都是有固定的含义:

      • 1、2位:省份
      • 3、4位:城市
      • 5、6位:区县
      • 7-14位:出生年、月、日
      • 15、16位:所在地派出所
      • 17位:性别(奇数男性,偶数女性)
      • 18位:个人信息码(随机产生)
      • 要求打印内容方式如下:
        • 人物信息为:
        • 出生年月日:XXXX年X月X日
        • 性别为:男/女
    • 代码演示:

      package com.itheima.stringdemo;
      
      public class StringDemo11 {
          public static void main(String[] args) {
              //1.定义一个字符串记录身份证号码
              String id = "321281202001011234";
      
              //2.获取出生年月日
              String year = id.substring(6, 10);
              String month = id.substring(10, 12);
              String day = id.substring(12, 14);
      
      
              System.out.println("人物信息为:");
              System.out.println("出生年月日:" + year + "年" + month + "月" + day + "日");
      
              //3.获取性别
              char gender = id.charAt(16);//'3'  ---> 3
              //利用ASCII码表进行转换
              //'0' --->  48
              //'1' --->  49
              //'2' --->  50
              //'3' --->  51
              //'4' --->  52
              //'5' --->  53
              //'6' --->  54
              //'7' --->  55
              //'8' --->  56
              //'9' --->  57
      
             int num = gender - 48;
              if(num % 2 == 0){
                  System.out.println("性别为:女");
              }else{
                  System.out.println("性别为:男");
              }
          }
      }
      

1.7 String扩展类

1.7.1 StringBuilder类

  • StringBuilder可以看成是一个容器,创建之后里面的内容是可变的。
  • 作用:提高字符串的操作效率
  • 应用场景:拼接字符串和反转字符串
1.7.1.1 方法
类型方法说明
构造方法public StringBuilder()创建一个空白可变字符串对象,不含有任何内容
构造方法public StringBuilder(String str)根据字符串来创建可变字符串对象
成员方法public StringBuilder append(任意类型)添加数据,并返回对象本身
成员方法public StringBuilder reverse()反转容器中的内容
成员方法public int length()返回长度(字符出现的个数)
成员方法public String toString()通过toString()方法就可以实现把StringBuilder转换为String
1.7.1.2 基本使用
public class StringBuilderDemo3 {
    public static void main(String[] args) {
        //1.创建对象
        StringBuilder sb = new StringBuilder("abc");

        //2.添加元素
        /*sb.append(1);
        sb.append(2.3);
        sb.append(true);*/

        //反转
        sb.reverse();

        //获取长度
        int len = sb.length();
        System.out.println(len);


        //打印
        //普及:
        //因为StringBuilder是Java已经写好的类
        //java在底层对他做了一些特殊处理。
        //打印对象不是地址值而是属性值。
        System.out.println(sb);
    }
}
1.7.1.3 链式编程
  • 当我们在调用一个方法的时候,不需要用变量接收他的结果,可以继续调用其它方法

    • 代码演示:

      public class StringBuilderDemo4 {
          public static void main(String[] args) {
              //1.创建对象
              StringBuilder sb = new StringBuilder();
      
              //2.添加字符串
              sb.append("aaa").append("bbb").append("ccc").append("ddd");
      
              System.out.println(sb);//aaabbbcccddd
      
              //3.再把StringBuilder变回字符串
              String str = sb.toString();
              System.out.println(str);//aaabbbcccddd
      
          }
      }
      
1.7.1.4 练习
  1. 对称字符串: 键盘接受一个字符串,程序判断出该字符串是否是对称字符串,并在控制台打印是或不是

    • 代码演示:

      public class StringBuilderDemo6 {
          //使用StringBuilder的场景:
          //1.字符串的拼接
          //2.字符串的反转
      
          public static void main(String[] args) {
              //1.键盘录入一个字符串
              Scanner sc = new Scanner(System.in);
              System.out.println("请输入一个字符串");
              String str = sc.next();
      
              //2.反转键盘录入的字符串
              String result = new StringBuilder().append(str).reverse().toString();
      
              //3.比较
              if(str.equals(result)){
                  System.out.println("当前字符串是对称字符串");
              }else{
                  System.out.println("当前字符串不是对称字符串");
              }
      
          }
      }
      
  2. 拼接字符串:

    需求:定义一个方法,把 int 数组中的数据按照指定的格式拼接成一个字符串返回。调用该方法,并在控制台输出结果。

    例如:数组为int[] arr = {1,2,3};

    执行方法后的输出结果为:[1, 2, 3]

    • 代码演示:

      public class StringBuilderDemo7 {
          public static void main(String[] args) {
              //1.定义数组
              int[] arr = {1,2,3};
      
              //2.调用方法把数组变成字符串
              String str = arrToString(arr);
      
              System.out.println(str);
      
          }
      
      
          public static String arrToString(int[] arr){
              StringBuilder sb = new StringBuilder();
              sb.append("[");
      
              for (int i = 0; i < arr.length; i++) {
                  if(i == arr.length - 1){
                      sb.append(arr[i]);
                  }else{
                      sb.append(arr[i]).append(", ");
                  }
              }
              sb.append("]");
      
              return sb.toString();
          }
      }
      

1.7.2 StringJoiner类

  • StringJoiner跟StringBuilder一样,也可以看成是一个容器,创建之后里面的内容是可变的。
  • 作用:提高字符串的操作效率,而且代码编写特别简洁,但是目前市场上很少有人用。
  • JDK8出现的
1.7.2.1 方法
类型方法说明
构造方法public StringJoiner(间隔符号)创建一个StringJoiner对象,指定拼接时的间隔符号
构造方法public StringJoiner(间隔符号,开始符号,结束符号)创建一个StringJoiner对象,指定拼接时的间隔符号、开始符号和结束符号
成员方法public StringJoiner add(添加的内容)添加数据,并返回对象本身,目前学习只能添加字符串
成员方法public int length()返回长度(字符出现的个数)
成员方法public String toString()通过toString()方法就可以实现把StringBuilder转换为String
1.7.2.2 基本使用
//1.创建一个对象,并指定中间的间隔符号
StringJoiner sj = new StringJoiner("---");
//2.添加元素
sj.add("aaa").add("bbb").add("ccc");
//3.打印结果
System.out.println(sj);//aaa---bbb---ccc
//1.创建对象
StringJoiner sj = new StringJoiner(", ","[","]");
//2.添加元素
sj.add("aaa").add("bbb").add("ccc");
int len = sj.length();
System.out.println(len);//15
//3.打印
System.out.println(sj);//[aaa, bbb, ccc]
String str = sj.toString();
System.out.println(str);//[aaa, bbb, ccc]

1.8 字符串底层

1.8.1 字符串存储的内存原理

  • 直接赋值会复用字符串常量池中的
    String s1 = "abc"String s2 = "abc"
    • 说明:此时s1所代表的字符串abc是存在字符串常量池中的。当定义字符串变量s2时,JVM会先检查字符串常量池中有没有字符串abc,如果有,不会创建新的,而是直接复用。如果没有abc,才会创建一个新的。所以,直接赋值的方式,代码简单,而且节约内存。
    • s1和s2是直接赋值的,所以记录的是字符串常量池中的地址值。
  • 利用关键字new创建的字符串对象不会复用,而是在堆区开辟了一个小空间
    String s3 = new String("abc");
    
    • 说明:s3记录的是new出来的,在堆里面的地址值。

1.8.2 ==号比较的原理

  • 如果比较的是基本数据类型:比的是具体的数值是否相等。
  • 如果比较的是引用数据类型:比的是地址值是否相等。
  • 结论:==号只能用于比较基本数据类型;不能比较引用数据类型。
  • 图解:
    字符串常量池

1.8.3 字符串拼接的底层原理

  • 在拼接的时候没有变量,都是字符串,此时会触发字符串的优化机制,在编译的时候就已经是最终结果
    • 示例:
      public class Test{
          public static void main(String[] args){
              String s = "a" + "b" + "c";
              System.out.println(s);
          }
      }
      
    • 图解:
      image-20240327204638294
  • 在拼接的时候有变量参与,每一行拼接的代码,都会在内存中创建新的字符串。
    • 示例:

      public class Test{
          public static void main(String[] args){
              String s1 = "a";
              String s2 = s1 + "b";
              String s3 = s2 + "c";
              System.out.println(s3);
          }
      }
      
    • JDK8以前系统底层会自动创建一个StringBuilder对象,然后再调用其append方法完成拼接。拼接后,再调用其toString方法转换为String类型,而toString方法的底层是直接new了一个字符串对象。

      • 图解:
        image-20240327204956093
    • JDK8版本系统会预估要字符串拼接之后的总大小,把要拼接的内容都放在数组中,此时也是产生一个新的字符串。

  • 总结: 当字符串凭拼接的时候有变量参与,在内存中创建了很多对象,浪费空间,浪费时间。所以很多字符串变量拼接,不要直接使用+号,可以使用StringBuilder类

1.8.4 StringBuilder提高效率原理图

  • 图解:
    image-20240327211954692

1.8.5 StringBuilder源码分析

  • 默认创建一个长度为16的字节数组

  • 当添加的内容长度小于16,直接存

  • 当添加的内容大于16会扩容(原来的容量*2 + 2)

  • 如果扩容之后还不够,以实际长度为准

    public class StringBuilderDemo7 {
        public static void main(String[] args) {
            StringBuilder sb = new StringBuilder();
            //容量:最多装多少
            //长度:已经装了多少
            System.out.println(sb.capacity());//16
            System.out.println(sb.length());//0
            
            sb.append("abcdefghijklmnopqrstuvwxyz");
            System.out.println(sb.capacity());//34
            System.out.println(sb.length());//26
    
            sb.append("abcdefghijklmnopqrstuvwxyz1234abcdefghijklmnopqrstuvwxyz1234");
            System.out.println(sb.capacity());//86
            System.out.println(sb.length());//86
        }
    }
    

2.正则表达式

学习目标:

  • 能够理解正则表达式的作用
  • 能够使用正则表达式的字符类
  • 能够使用正则表达式的逻辑运算符
  • 能够使用正则表达式的预定义字符类
  • 能够使用正则表达式的限定符
  • 能够使用正则表达式的分组
  • 能够在String的split方法中使用正则表达式

2.1 正则表达式的概念及演示

  • 在Java中,我们经常需要验证一些字符串,例如:年龄必须是2位的数字、用户名必须是8位长度而且只能包含大小写字母、数字等。正则表达式就是用来验证各种字符串的规则。它内部描述了一些规则,我们可以验证用户输入的字符串是否匹配这个规则。
  • 作用:
    • 验证字符串是否满足规则
    • 在一段文本中查找满足要求的内容
  • 正则表达式示例:验证QQ号
    • QQ号码必须是5–15位长度
    • 而且必须全部是数字
    • 而且首位不能为0
    • 代码演示:
      • 原始:

        public class RegexDemo1 {
            public static void main(String[] args) {
                /* 假如现在要求校验一个qq号码是否正确。
                    规则:6位及20位之内,日不能在开头,必须全部是数字。
                    先使用目前所学知识完成校验需求然后体验一下正则表达式检验。
                */
        
                String qq ="1234567890";
                System.out.println(checkQQ(qq));
        
                System.out.println(qq.matches("[1-9]\\d{5,19}"));
        
            }
        
            public static boolean checkQQ(String qq) {
                //规则:6位及20位之内,日不能在开头,必须全部是数字 。
                //核心思想:
                //先把异常数据进行过滤
                //下面的就是满足要求的数据了。
                int len = qq.length();
                if (len < 6 || len > 20) {
                    return false;
                }
                //0不能在开头
                if (qq.startsWith("0")) {
                    return false;
                }
                //必须全部是数字
                for (int i = 0; i < qq.length(); i++) {
                    char c = qq.charAt(i);
                    if (c < '0' | c > '9') {
                        return false;
                    }
                }
                return true;
            }
        }
        
      • 正则表达式

        public class Demo {
            public static void main(String[] args) {
                String qq ="1234567890";
                System.out.println(qq.matches("[1-9]\\d{5,19}"));
            }
        }
        

2.2 正则表达式基础语法

2.2.1 匹配单个字符

  • 语法示例:

    格式说明
    [abc]代表a或者b,或者c字符中的一个。
    [^abc]代表除a,b,c以外的任何字符。
    [a-z]代表a-z的所有小写字符中的一个。
    [A-Z]代表A-Z的所有大写字符中的一个。
    [0-9]代表0-9之间的某一个数字字符。
    [a-zA-Z0-9]代表a-z或者A-Z或者0-9之间的任意一个字符。
    [a-dm-p] 或者 [a-d[m-p]]a 到 d 或 m 到 p之间的任意一个字符。
    [a-z&&[def]]a-z和def的交集。为:d,e,f
    [a-z&&[^bc]]a-z和非bc的交集。等同于[ad-z]
    [a-z&&[^m-p]]a-z和除了m到p的交集。等同于[a-lq-z]
  • 代码示例:

    public class RegexDemo2 {
        public static void main(String[] args) {
            //public boolean matches(String regex):判断是否与正则表达式匹配,匹配返回true
            // 只能是a b c
            System.out.println("-----------1-------------");
            System.out.println("a".matches("[abc]")); // true
            System.out.println("z".matches("[abc]")); // false
            System.out.println("ab".matches("[abc]")); // true
            System.out.println("ab".matches("[abc][abc]")); // true
    
            // 不能出现a b c
            System.out.println("-----------2-------------");
            System.out.println("a".matches("[^abc]")); // false
            System.out.println("z".matches("[^abc]")); // true
            System.out.println("zz".matches("[^abc]")); //false
            System.out.println("zz".matches("[^abc][^abc]")); //true
    
            // a到zA到Z(包括头尾的范围)
            System.out.println("-----------3-------------");
            System.out.println("a".matches("[a-zA-z]")); // true
            System.out.println("z".matches("[a-zA-z]")); // true
            System.out.println("aa".matches("[a-zA-z]"));//false
            System.out.println("zz".matches("[a-zA-Z]")); //false
            System.out.println("zz".matches("[a-zA-Z][a-zA-Z]")); //true
            System.out.println("0".matches("[a-zA-Z]"));//false
            System.out.println("0".matches("[a-zA-Z0-9]"));//true
    
    
            // [a-d[m-p]] a到d,或m到p
            System.out.println("-----------4-------------");
            System.out.println("a".matches("[a-d[m-p]]"));//true
            System.out.println("d".matches("[a-d[m-p]]")); //true
            System.out.println("m".matches("[a-d[m-p]]")); //true
            System.out.println("p".matches("[a-d[m-p]]")); //true
            System.out.println("e".matches("[a-d[m-p]]")); //false
            System.out.println("0".matches("[a-d[m-p]]")); //false
    
            // [a-z&&[def]] a-z和def的交集。为:d,e,f
            //细节:如果要求两个范围的交集,那么需要写符号 &&
            //如果写成一个&,那么此时&就不是交集了,而是一个简简单单的&符号
            System.out.println("----------5------------");
            System.out.println("a".matches("[a-z&[def]]")); //true
            System.out.println("&".matches("[a-z&[def]]")); //true
            System.out.println("d".matches("[a-z&&[def]]")); //true
            System.out.println("0".matches("[a-z&&[def]]")); //false
    
            // [a-z&&[^bc]] a-z和非bc的交集。(等同于[ad-z])
            System.out.println("-----------6------------_");
            System.out.println("a".matches("[a-z&&[^bc]]"));//true
            System.out.println("b".matches("[a-z&&[^bc]]")); //false
            System.out.println("0".matches("[a-z&&[^bc]]")); //false
    
            // [a-z&&[^m-p]] a到z和除了m到p的交集。(等同于[a-1q-z])
            System.out.println("-----------7-------------");
            System.out.println("a".matches("[a-z&&[^m-p]]")); //true
            System.out.println("m".matches("[a-z&&[^m-p]]")); //false
            System.out.println("0".matches("[a-z&&[^m-p]]")); //false
        }
    }
    

2.2.2 逻辑运算符

  • 语法示例:

    符号说明
    &&并且
    |或者
    \转义字符
  • 代码示例:

    public class Demo {
    	public static void main(String[] args) {
    		String str = "had";
    		
    		//1.要求字符串是小写辅音字符开头,后跟ad
    		String regex = "[a-z&&[^aeiou]]ad";
    		System.out.println("1." + str.matches(regex));
    		
    		//2.要求字符串是aeiou中的某个字符开头,后跟ad
    		regex = "[a|e|i|o|u]ad";//这种写法相当于:regex = "[aeiou]ad";
    		System.out.println("2." + str.matches(regex));
    	}
    }
    
    public class RegexDemo3 {
        public static void main(String[] args) {
            // \ 转义字符 改变后面那个字符原本的含义
            //练习:以字符串的形式打印一个双引号
            //"在Java中表示字符串的开头或者结尾
    
            //此时\表示转义字符,改变了后面那个双引号原本的含义
            //把他变成了一个普普通通的双引号而已。
            System.out.println("\"");
    
            // \表示转义字符
            //两个\的理解方式:前面的\是一个转义字符,改变了后面\原本的含义,把他变成一个普普通通的\而已。
            System.out.println("c:Users\\moon\\IdeaProjects\\myapi\\src\\itheima\\RegexDemo1.java");
    
        }
    }
    

2.2.3 预定义字符

  • 语法示例:

    符号说明
    .匹配任何字符
    \d匹配一个数字:[0-9]
    \D匹配任何非数字[^0-9]的简写
    \s一个空白字符:[ \t\n\x0B\f\r] 的简写
    \S非空白字符:[^\s] 的简写
    \w单词字符(英文、数字、下划线):[a-zA-Z_0-9]的简写
    \W非单词字符:[^\w]
  • 代码示例:

    public class Demo {
    	public static void main(String[] args) {
            //.表示任意一个字符
            System.out.println("你".matches("..")); //false
            System.out.println("你".matches(".")); //true
            System.out.println("你a".matches(".."));//true
    
            // \\d 表示任意的一个数字
            // \\d只能是任意的一位数字
            // 简单来记:两个\表示一个\
            System.out.println("a".matches("\\d")); // false
            System.out.println("3".matches("\\d")); // true
            System.out.println("333".matches("\\d")); // false
    
            //\\w只能是一位单词字符[a-zA-Z_0-9]
            System.out.println("z".matches("\\w")); // true
            System.out.println("2".matches("\\w")); // true
            System.out.println("21".matches("\\w")); // false
            System.out.println("你".matches("\\w"));//false
    
            // 非单词字符
            System.out.println("你".matches("\\W")); // true
            System.out.println("---------------------------------------------");
            // 以上正则匹配只能校验单个字符。
    
    
            // 必须是数字 字母 下划线 至少 6位
            System.out.println("2442fsfsf".matches("\\w{6,}"));//true
            System.out.println("244f".matches("\\w{6,}"));//false
    
            // 必须是数字和字符 必须是4位
            System.out.println("23dF".matches("[a-zA-Z0-9]{4}"));//true
            System.out.println("23 F".matches("[a-zA-Z0-9]{4}"));//false
            System.out.println("23dF".matches("[\\w&&[^_]]{4}"));//true
            System.out.println("23_F".matches("[\\w&&[^_]]{4}"));//false
    		
    	}
    }
    

2.2.4 数量词

  • 语法示例:

    符号说明
    X?0次或1次
    X*0次到多次
    X+1次或多次
    X{n}恰好n次
    X{n,}至少n次
    X{n,m}n到m次(n和m都是包含的)
  • 代码示例:

    public class Demo {
    	public static void main(String[] args) {
    		 // 必须是数字 字母 下划线 至少 6位
            System.out.println("2442fsfsf".matches("\\w{6,}"));//true
            System.out.println("244f".matches("\\w{6,}"));//false
    
            // 必须是数字和字符 必须是4位
            System.out.println("23dF".matches("[a-zA-Z0-9]{4}"));//true
            System.out.println("23 F".matches("[a-zA-Z0-9]{4}"));//false
            System.out.println("23dF".matches("[\\w&&[^_]]{4}"));//true
            System.out.println("23_F".matches("[\\w&&[^_]]{4}"));//false
    	}
    }
    

2.2.5 分组

2.2.5.1 分组括号( )
  • 示例:

    String regex1 = "\\w+@[\\w&&[^_]]{2,6}(\\.[a-zA-Z]{2,3}){1,2})"; //邮箱号
    String regex2 = "([01]\\d|2[0-3])(:[0-5]\\d){2}"; //简易身份证号
    String regex3 = "([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d"; //24小时时间
    String regex4 = "([01]\\d|2[0-3])(:[0-5]\\d){2}"; //24小时时间
    
  • 在书写正则表达式的时候我们需要将正则表达式的某个部分作为单个单元进行处理,就需要用到分组括号

    • 分组括号中每一组括号有组号,也就是序号
    • 组号的识别规则:
      • 从1开始,连续不间断
      • 以左括号为基准,最左边的是第一组,其次为第二组,以此类推。(只看左括号,不看右括号,按照左括号的顺序,从左往右,依次为第一组,第二组,第三组等等)
2.2.5.2 捕获分组
  • 捕获分组:表示后续还要继续使用本组的数据

  • 使用语法:

    • 正则内部使用语法:\\组号

      //需求1:判断一个字符串的开始字符和结束字符是否一致?只考虑一个字符
      //举例: a123a b456b 17891 &abc& a123b(false)
      // \\组号:表示把第X组的内容再出来用一次
      String regex1 = "(.).+\\1";
      System.out.println("a123a".matches(regex1));
      System.out.println("b456b".matches(regex1));
      System.out.println("17891".matches(regex1));
      System.out.println("&abc&".matches(regex1));
      System.out.println("a123b".matches(regex1));
      System.out.println("--------------------------");
      
      
      //需求2:判断一个字符串的开始部分和结束部分是否一致?可以有多个字符
      //举例: abc123abc b456b 123789123 &!@abc&!@ abc123abd(false)
      String regex2 = "(.+).+\\1";
      System.out.println("abc123abc".matches(regex2));
      System.out.println("b456b".matches(regex2));
      System.out.println("123789123".matches(regex2));
      System.out.println("&!@abc&!@".matches(regex2));
      System.out.println("abc123abd".matches(regex2));
      System.out.println("---------------------");
      
      //需求3:判断一个字符串的开始部分和结束部分是否一致?开始部分内部每个字符也需要一致
      //举例: aaa123aaa bbb456bbb 111789111 &&abc&&
      //(.):把首字母看做一组
      // \\2:把首字母拿出来再次使用
      // *:作用于\\2,表示后面重复的内容出现日次或多次
      String regex3 = "((.)\\2*).+\\1";
      //这个正则表达式是
      System.out.println("aaa123aaa".matches(regex3));
      System.out.println("bbb456bbb".matches(regex3));
      System.out.println("111789111".matches(regex3));
      System.out.println("&&abc&&".matches(regex3));
      System.out.println("aaa123aab".matches(regex3));
      
    • 正则外部使用语法:$组号

      /*
      需求:
      ​    将字符串:我要学学编编编编程程程程程程。
      ​    替换为:我要学编程
      */
      String str = "我要学学编编编编程程程程程程";
      
      //需求:把重复的内容 替换为 单个的
      //学学                学
      //编编编编            编
      //程程程程程程        程
      //  (.)表示把重复内容的第一个字符看做一组
      //  \\1表示第一字符再次出现
      //  + 至少一次
      //  $1 表示把正则表达式中第一组的内容,再拿出来用
      String result = str.replaceAll("(.)\\1+", "$1");
      System.out.println(result);
      
2.2.5.3 非捕获分组
  • 非捕获分组:分组之后不需要再用本组数据,仅仅是把数据括起来

  • 书写规则:

    符号含义举例
    (?:正则)获取所有Java(?:8|11|17)
    (?=正则)获取前面部分Java(?=8|11|17)
    (?!正则)获取不是指定内容的前面部分Java(?!8|11|17)
  • 案例演示:

    //身份证号码的简易正则表达式
    //非捕获分组:仅仅是把数据括起来
    //特点:不占用组号
    //这里\\1报错原因:(?:)就是非捕获分组,此时是不占用组号的。
    
    //(?:) (?=) (?!)都是非捕获分组
    //更多的使用第一个
    String regex1 ="[1-9]\\d{16}(?:\\d|x|x)";
    String regex2 ="[1-9]\\d{16}(\\dXx)";
    //上面两者等价
    //^([01]\d|2[0-3]):[0-5]\d:[@-5]\d$
    
    System.out.println("41080119930228457x".matches(regex1));
    

2.2.6 忽略大小写

  • 写法规则:(?i)要忽略的字母

    //忽略abc的大小写
    String regex = "(?i)abc";
    //a需要一模一样,忽略bc的大小写
    String regex = "a(?i)bc";
    //ac需要一模一样,忽略b的大小写
    String regex = "a((?i)b)c";
    

2.3 数据爬取

  • 正则表达式的作用之一是在一段文本中查找满足要求的内容。

  • 这个作用的应用场景之一就是数据爬取

  • 正则表达式类:Pattern

    • public static Pattern compile(String regex):将给定的正则表达式编译到Pattern中
    • public Matcher matcher(CharSequence input):创建匹配给定输入与此模式的Matcher对象
  • 文本匹配器:Matcher

    • 作用:按照正则表达式的规则去读取字符串,从头开始读取。在大串中去找符合匹配规则的子串。
    • public boolean matches():尝试将整个区域与模式匹配
  • 匹配流程:指定为字符串的正则表达式必须首先被编译为Pattern的实例。然后,可将得到的模式用于创建 Matcher 对象,依照正则表达式,该对象可以与任意字符序列匹配。执行匹配所涉及的所有状态都驻留在匹配器中,所以多个匹配器可以共享同一模式。

    • 典型的调用顺序是:

       Pattern p = Pattern.compile("a*b");
       Matcher m = p.matcher("aaaaab");
       boolean b = m.matches();
      
    • 在仅使用一次正则表达式时,可以方便地通过此类定义 matches方法。此方法编译表达式并在单个调用中将输入序列与其匹配。

       boolean b = Pattern.matches("a*b", "aaaaab");
      

2.3.1 本地数据爬取

package com.itheima.a08regexdemo;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexDemo6 {
    public static void main(String[] args) {
        /* 有如下文本,请按照要求爬取数据。
                Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和Java11,
                因为这两个是长期支持版本,下一个长期支持版本是Java17,相信在未来不久Java17也会逐渐登上历史舞台
                要求:找出里面所有的JavaXX
         */

        String str = "Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和Java11," +
                "因为这两个是长期支持版本,下一个长期支持版本是Java17,相信在未来不久Java17也会逐渐登上历史舞台";


        //1.获取正则表达式的对象
        Pattern p = Pattern.compile("Java\\d{0,2}");
        //2.获取文本匹配器的对象
        //拿着m去读取str,找符合p规则的子串
        Matcher m = p.matcher(str);

        //3.利用循环获取
        while (m.find()) {
            String s = m.group();
            System.out.println(s);
        }


    }

    private static void method1(String str) {
        //Pattern:表示正则表达式
        //Matcher: 文本匹配器,作用按照正则表达式的规则去读取字符串,从头开始读取。
        //          在大串中去找符合匹配规则的子串。

        //获取正则表达式的对象
        Pattern p = Pattern.compile("Java\\d{0,2}");
        //获取文本匹配器的对象
        //m:文本匹配器的对象
        //str:大串
        //p:规则
        //m要在str中找符合p规则的小串
        Matcher m = p.matcher(str);

        //拿着文本匹配器从头开始读取,寻找是否有满足规则的子串
        //如果没有,方法返回false
        //如果有,返回true。在底层记录子串的起始索引和结束索引+1
        // 0,4
        boolean b = m.find();

        //方法底层会根据find方法记录的索引进行字符串的截取
        // substring(起始索引,结束索引);包头不包尾
        // (0,4)但是不包含4索引
        // 会把截取的小串进行返回。
        String s1 = m.group();
        System.out.println(s1);


        //第二次在调用find的时候,会继续读取后面的内容
        //读取到第二个满足要求的子串,方法会继续返回true
        //并把第二个子串的起始索引和结束索引+1,进行记录
        b = m.find();

        //第二次调用group方法的时候,会根据find方法记录的索引再次截取子串
        String s2 = m.group();
        System.out.println(s2);
    }
}

2.3.2 网络数据爬取(了解)

public class RegexDemo7 {
    public static void main(String[] args) throws IOException {
        /* 扩展需求2:
            把连接:https://m.sengzan.com/jiaoyu/29104.html?ivk sa=1025883i
            中所有的身份证号码都爬取出来。
        */

        //创建一个URL对象
        URL url = new URL("https://m.sengzan.com/jiaoyu/29104.html?ivk sa=1025883i");
        //连接上这个网址
        //细节:保证网络是畅通
        URLConnection conn = url.openConnection();//创建一个对象去读取网络中的数据
        BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        String line;
        //获取正则表达式的对象pattern
        String regex = "[1-9]\\d{17}";
        Pattern pattern = Pattern.compile(regex);//在读取的时候每次读一整行
        while ((line = br.readLine()) != null) {
            //拿着文本匹配器的对象matcher按照pattern的规则去读取当前的这一行信息
            Matcher matcher = pattern.matcher(line);
            while (matcher.find()) {
                System.out.println(matcher.group());
            }
        }
        br.close();
    }
}

2.3.3 按要求爬取

public class RegexDemo9 {
    public static void main(String[] args) {
        /*
            有如下文本,按要求爬取数据。
                Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和Java11,
                因为这两个是长期支持版本,下一个长期支持版本是Java17,相信在未来不久Java17也会逐渐登上历史舞台


            需求1:爬取版本号为8,11.17的Java文本,但是只要Java,不显示版本号。
            需求2:爬取版本号为8,11,17的Java文本。正确爬取结果为:Java8 Java11 Java17 Java17
            需求3:爬取除了版本号为8,11.17的Java文本,
        */
        String s = "Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和Java11," +
            "因为这两个是长期支持版本,下一个长期支持版本是Java17,相信在未来不久Java17也会逐渐登上历史舞台";

        //1.定义正则表达式
        //?理解为前面的数据Java
        //=表示在Java后面要跟随的数据
        //但是在获取的时候,只获取前半部分
        //需求1:
        String regex1 = "((?i)Java)(?=8|11|17)";
        //需求2:
        String regex2 = "((?i)Java)(8|11|17)";
        String regex3 = "((?i)Java)(?:8|11|17)";
        //需求3:
        String regex4 = "((?i)Java)(?!8|11|17)";

        Pattern p = Pattern.compile(regex4);
        Matcher m = p.matcher(s);
        while (m.find()) {
            System.out.println(m.group());
        }
    }
}

2.3.4 贪婪爬取和非贪婪爬取

  • 贪婪爬取:在爬取数据的时候尽可能的多获取数据

    • ?
    • *
  • 非贪婪爬取:在爬取数据的时候尽可能的少获取数据

    • +?
    • *?
  • 举例:

    如果获取数据:ab+
    贪婪爬取获取结果:abbbbbbbbbbbb
    非贪婪爬取获取结果:ab

  • 代码示例:

    public class RegexDemo10 {
        public static void main(String[] args) {
            /*
                只写+和*表示贪婪匹配
    
                +? 非贪婪匹配
                *? 非贪婪匹配
    
                贪婪爬取:在爬取数据的时候尽可能的多获取数据
                非贪婪爬取:在爬取数据的时候尽可能的少获取数据
    
                ab+:
                贪婪爬取:abbbbbbbbbbbb
                非贪婪爬取:ab
            */
            String s = "Java自从95年问世以来,abbbbbbbbbbbbaaaaaaaaaaaaaaaaaa" +
                    "经历了很多版木,目前企业中用的最多的是]ava8和]ava11,因为这两个是长期支持版木。" +
                    "下一个长期支持版本是Java17,相信在未来不久Java17也会逐渐登上历史舞台";
    
            String regex = "ab+";
            Pattern p = Pattern.compile(regex);
            Matcher m = p.matcher(s);
    
            while (m.find()) {
                System.out.println(m.group());
            }
        }
    }
    

2.4.正则表达式在String类中的应用

2.4.1 String类的split方法中使用正则表达式

  • String类的split()方法原型:

    public String[] split(String regex) 
    //参数regex表示正则表达式。可以将当前字符串中匹配regex正则表达式的符号作为"分隔符"来切割字符串。
    
  • 代码演示:

    /*
        有一段字符串:小诗诗dqwefqwfqwfwq12312小丹丹dqwefqwfqwfwq12312小惠惠
        要求1:把字符串中三个姓名之间的字母替换为vs
        要求2:把字符串中的三个姓名切割出来
    */
    
    String s = "小诗诗dqwefqwfqwfwq12312小丹丹dqwefqwfqwfwq12312小惠惠";
    //细节:
    //方法在底层跟之前一样也会创建文本解析器的对象
    //然后从头开始去读取字符串中的内容,只要有满足的,那么就切割。
    String[] arr = s.split("[\\w&&[^_]]+");
    for (int i = 0; i < arr.length; i++) {
        System.out.println(arr[i]);
    }
    

2.4.2 String类的replaceAll方法中使用正则表达式

  • String类的replaceAll()方法原型:

    public String replaceAll(String regex,String newStr)
    //参数regex表示一个正则表达式。可以将当前字符串中匹配regex正则表达式的字符串替换为newStr。
    
  • 代码演示:

    /*
        有一段字符串:小诗诗dqwefqwfqwfwq12312小丹丹dqwefqwfqwfwq12312小惠惠
        要求1:把字符串中三个姓名之间的字母替换为vs
        要求2:把字符串中的三个姓名切割出来
    */
    
    String s = "小诗诗dqwefqwfqwfwq12312小丹丹dqwefqwfqwfwq12312小惠惠";
    //细节:
    //方法在底层跟之前一样也会创建文本解析器的对象
    //然后从头开始去读取字符串中的内容,只要有满足的,那么就用第一个参数去替换。
    String result1 = s.replaceAll("[\\w&&[^_]]+", "vs");
    System.out.println(result1);
    

2.5 总结

2.5.1 常用符号总结

符号含义举例
[]里面的内容出现一次[0-9] [a-zA-Z0-9]
()分组a(bc)+
^取反[^abc]
&&交集,不能写单个的&[a-z&&m-p]
|写在方括号外面表示并集[a-zA-Z0-9] x|X
.任意字符\n回车符号不匹配
\转义字符\d
\d0-9\d+
\D非0-9\D+
\s空白字符[\t\n\x0B\f\r]
\w单词字符[a-zA-Z_0-9]
\W非单词字符[^\w]
?0次或1次\d?
*0次或多次\d* (abc)*
+1次或多次\d+ (abc)+
{}具体次数a{7} \d{7,19}
(?!)忽略后面字符的大小写(?!)abc
a((?!)b)c只忽略b的大小写a((?!)b)c
  • 注意细节:以上规则不用硬记,在API文档里面搜索Pattern类或者String类中的matches方法即可跳转到Pattern类,该文档里面里面详细介绍了正则表达式的书写规则
  • 在书写正则表达式的时候很多时候并不是一步就写出来的,而是匹配着一个正确的符合要求的式子,一步一步不断修改得来的

2.5.2 练习

  1. 练习1

    • 需求:
      • 请编写正则表达式验证用户输入的手机号码是否满足要求。
      • 请编写正则表达式验证用户输入的邮箱号是否满足要求。
      • 请编写正则表达式验证用户输入的电话号码是否满足要求。
      • 验证手机号码 13112345678 13712345667 13945679027 139456790271
      • 验证座机电话号码 020-2324242 02122442 027-42424 0712-3242434

    ​ 验证邮箱号码 3232323@qq.com zhangsan@itcast.cnn dlei0009@163.com dlei0009@pci.com.cn

    • 代码示例:

      public class RegexDemo4 {
          public static void main(String[] args) {
              /*
                  需求
                  请编写正则表达式验证用户输入的手机号码是否满足要求。
                  请编写正则表达式验证用户输入的邮箱号是否满足要求。
                  请编写正则表达式验证用户输入的电话号码是否满足要求。
                  验证手机号码 13112345678 13712345667 13945679027 139456790271
                  验证座机电话号码 020-2324242 02122442 027-42424 0712-3242434
                  验证邮箱号码 3232323@qq.com zhangsan@itcast.cnn dlei0009@163.com dlei0009@pci.com.cn
              */
      
              //心得:
              //拿着一个正确的数据,从左到右依次去写。
              //13112345678
              //分成三部分:
              //第一部分:1 表示手机号码只能以1开头
              //第二部分:[3-9] 表示手机号码第二位只能是3-9之间的
              //第三部分:\\d{9} 表示任意数字可以出现9次,也只能出现9次
              String regex1 = "1[3-9]\\d{9}";
              System.out.println("13112345678".matches(regex1));//true
              System.out.println("13712345667".matches(regex1));//true
              System.out.println("13945679027".matches(regex1));//true
              System.out.println("139456790271".matches(regex1));//false
              System.out.println("-----------------------------------");
      
              //座机电话号码
              //020-2324242 02122442 027-42424 0712-3242434
              //思路:
              //在书写座机号正则的时候需要把正确的数据分为三部分
              //一:区号@\\d{2,3}
              //      0:表示区号一定是以0开头的
              //      \\d{2,3}:表示区号从第二位开始可以是任意的数字,可以出现2到3次。
              //二:- ?表示次数,日次或一次
              //三:号码 号码的第一位也不能以日开头,从第二位开始可以是任意的数字,号码的总长度:5-10位
              String regex2 = "0\\d{2,3}-?[1-9]\\d{4,9}";
              System.out.println("020-2324242".matches(regex2));
              System.out.println("02122442".matches(regex2));
              System.out.println("027-42424".matches(regex2));
              System.out.println("0712-3242434".matches(regex2));
      
              //邮箱号码
              //3232323@qq.com zhangsan@itcast.cnn dlei0009@163.com dlei0009@pci.com.cn
              //思路:
              //在书写邮箱号码正则的时候需要把正确的数据分为三部分
              //第一部分:@的左边 \\w+
              //      任意的字母数字下划线,至少出现一次就可以了
              //第二部分:@ 只能出现一次
              //第三部分:
              //      3.1         .的左边[\\w&&[^_]]{2,6}
              //                  任意的字母加数字,总共出现2-6次(此时不能出现下划线)
              //      3.2         . \\.
              //      3.3         大写字母,小写字母都可以,只能出现2-3次[a-zA-Z]{2,3}
              //      我们可以把3.2和3.3看成一组,这一组可以出现1次或者两次
              String regex3 = "\\w+@[\\w&&[^_]]{2,6}(\\.[a-zA-Z]{2,3}){1,2}";
              System.out.println("3232323@qq.com".matches(regex3));
              System.out.println("zhangsan@itcast.cnn".matches(regex3));
              System.out.println("dlei0009@163.com".matches(regex3));
              System.out.println("dlei0009@pci.com.cn".matches(regex3));
      
      
              //24小时的正则表达式
              String regex4 = "([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d";
              System.out.println("23:11:11".matches(regex4));
      
              String regex5 = "([01]\\d 2[0-3])(:[0-5]\\d){2}";
              System.out.println("23:11:11".matches(regex5));
          }
      }
      
    • 总结:

      手机号码:1[3-9]\\d{9}
      座机号码:0\\d{2,3}-?[1-9]\\d{4,9}
      邮箱号码:\\w+@[\\w&&[^_]]{2,6}(\\.[a-zA-Z]{2,3}){1,2}
      24小时:([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d
      	   ([01]\\d|2[0-3])(:[0-5]\\d){2}
      用户名:	\\w{4,16}
      身份证号码,简单校验:
      		[1-9]\\d{16}(\\d|X|x)
      		[1-9]\\d{16}[\\dXx]
      		[1-9]\\d{16}(\\d(?i)X)
      身份证号码,严格校验:
      		[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9|[12])\\d|3[01])\\d{3}[\\dXx]
      
    1. 练习2

      • 需求
        请编写正则表达式验证用户名是否满足要求。要求:大小写字母,数字,下划线一共4-16位
        请编写正则表达式验证身份证号码是否满足要求。
        简单要求:
        18位,前17位任意数字,最后一位可以是数字可以是大写或小写的x
        复杂要求:
        按照身份证号码的格式严格要求。

      • 身份证号码:

        • 41080119930228457x
        • 510801197609022309
        • 15040119810705387X
        • 130133197204039024
        • 430102197606046442
      • 代码演示:

        public class RegexDemo5 {
            public static void main(String[] args) {
                /*
                    正则表达式练习:
                    需求
                    请编写正则表达式验证用户名是否满足要求。要求:大小写字母,数字,下划线一共4-16位
                    请编写正则表达式验证身份证号码是否满足要求。
                    简单要求:
                        18位,前17位任意数字,最后一位可以是数字可以是大写或小写的x
                    复杂要求:
                        按照身份证号码的格式严格要求。
        
                    身份证号码:
                    41080119930228457x
                    510801197609022309
                    15040119810705387X
                    130133197204039024
                    430102197606046442
                */
        
                //用户名要求:大小写字母,数字,下划线一共4-16位
                String regex1 = "\\w{4,16}";
                System.out.println("zhangsan".matches(regex1));
                System.out.println("lisi".matches(regex1));
                System.out.println("wangwu".matches(regex1));
                System.out.println("$123".matches(regex1));
        
        
                //身份证号码的简单校验:
                //18位,前17位任意数字,最后一位可以是数字可以是大写或小写的x
                String regex2 = "[1-9]\\d{16}(\\d|x|x)";
                String regex3 = "[1-9]\\d{16}[\\dXx]";
                String regex5 = "[1-9]\\d{16}(\\d(?i)x)";
        
                System.out.println("41080119930228457x".matches(regex3));
                System.out.println("510801197609022309".matches(regex3));
                System.out.println("15040119810705387X".matches(regex3));
                System.out.println("130133197204039024".matches(regex3));
                System.out.println("430102197606046442".matches(regex3));
        
        
                //忽略大小写的书写方式
                //在匹配的时候忽略abc的大小写
                String regex4 = "a((?i)b)c";
                System.out.println("------------------------------");
                System.out.println("abc".matches(regex4));//true
                System.out.println("ABC".matches(regex4));//false
                System.out.println("aBc".matches(regex4));//true
        
        
                //身份证号码的严格校验
                //编写正则的小心得:
                //第一步:按照正确的数据进行拆分
                //第二步:找每一部分的规律,并编写正则表达式
                //第三步:把每一部分的正则拼接在一起,就是最终的结果
                //书写的时候:从左到右去书写。
        
                //410801 1993 02 28 457x
                //前面6位:省份,市区,派出所等信息,第一位不能是0,后面5位是任意数字       [1-9]\\d{5}
                //年的前半段: 18 19 20                                                (18|19|20)
                //年的后半段: 任意数字出现两次                                           \\d{2}
                //月份: 01~ 09 10 11 12                                               (@[1-9]|1[0-2])
                //日期: 01~09 10~19 20~29 30 31                                       (0[1-9]|[12]\\d|3[01])
                //后面四位: 任意数字出现3次 最后一位可以是数字也可以是大写x或者小写x        \\d{3}[\\dXx]
                String regex6 = "[1-9]\\d{5}(18|19|20)\\d{2}(@[1-9]|1[0-2])(@[1-9]|[12]\\d|3[01])\\d{3}[\\dxXx]";
        
                System.out.println("41080119930228457x".matches(regex6));
                System.out.println("510801197609022309".matches(regex6));
                System.out.println("15040119810705387X".matches(regex6));
                System.out.println("130133197204039024".matches(regex6));
                System.out.println("430102197606046442".matches(regex6));
            }
        }
        
    2. 爬取数据练习

      • 需求:把下面文本中的座机电话,邮箱,手机号,热线都爬取出来。

      • 来黑马程序员学习Java,手机号:18512516758,18512508907或者联系邮箱:boniu@itcast.cn,座机电话:01036517895,010-98951256邮箱:bozai@itcast.cn,热线电话:400-618-9090 ,400-618-4000,4006184000,4006189090手机号的正则表达式:1[3-9]\d{9}

      • 代码示例:

        import java.util.regex.Matcher;
        import java.util.regex.Pattern;
        
        public class Test {
            public static void main(String[] args) {
                /*
                    需求:把下面文本中的座机电话,邮箱,手机号,热线都爬取出来。
                    来黑马程序员学习Java,
                    手机号:18512516758,18512508907或者联系邮箱:boniu@itcast.cn,
                    座机电话:01036517895,010-98951256邮箱:bozai@itcast.cn,
                    热线电话:400-618-9090 ,400-618-4000,4006184000,4006189090
        
                    手机号的正则表达式:1[3-9]\d{9}
                    邮箱的正则表达式:\w+@[\w&&[^_]]{2,6}(\.[a-zA-Z]{2,3}){1,2}
                    座机电话的正则表达式:θ\d{2,3}-?[1-9]\d{4,9}
                    热线电话的正则表达式:400-?[1-9]\\d{2}-?[1-9]\\d{3}
        
        
                */
        
                String s = "来黑马程序员学习Java," +
                        "电话:18512516758,18512508907" + "或者联系邮箱:boniu@itcast.cn," +
                        "座机电话:01036517895,010-98951256" + "邮箱:bozai@itcast.cn," +
                        "热线电话:400-618-9090 ,400-618-4000,4006184000,4006189090";
        
                String regex = "(1[3-9]\\d{9})|(\\w+@[\\w&&[^_]]{2,6}(\\.[a-zA-Z]{2,3}){1,2})" +
                        "|(0\\d{2,3}-?[1-9]\\d{4,9})|(400-?[1-9]\\d{2}-?[1-9]\\d{3})";
        
                //1.获取正则表达的对象
                Pattern p = Pattern.compile(regex);
        
                //2.获取文本匹配器对象
                Matcher m =p.matcher(s);
        
                //3.利用循环获取每一个数据
                while(m.find()){
                    String str = m.group();
                    System.out.println(str);
                }
            }
        }
        

3.串的数据结构

  • 这部分内容是对字符串这个数据结构的研究

3.1 串的定义

  • 串(String)是由零个或多个字符组成的有限序列,又叫字符串
  • 一般记为s = “a1a2…an ”(n≥0),其中s是串的名称,用双引号括起来的字符序列是串的值
  • 相关概念:
    • 长度:串中的字符数目n
    • 空串:零个字符的串
    • 空格串:只包含空格的串
    • 子串与主串:串中任意个数的连续字符组成的子序列称为该串的子串,相应地,包含子串的串称为主串

3.2 串的抽象数据类型

ADT String {
    数据对象:D={ ai | ai∈ CharacterSet,i=1 ,2 ,, n,n≥ 0 }
    结构关系:R1={< ai-1,ai  >| ai-1,ai ∈ D ,i=2 ,, n  }
    基本操作:
        StrAssign( &T , chars )
            初始条件:chars是字符串常量。
            操作结果:生成一个其值等于chars的串TStrCopy( &T , S )
            初始条件:串S存在。
            操作结果:由串S复制得到串TStrEmpty( S )
            初始条件:串S存在。
            操作结果:若S是空串,返回true,否则返回falseStrCompare(S,T)
            初始条件:串ST存在。
            操作结果:若S>T,则返回值>0;S=T,则返回值=0;S<T,则返回值<0;
        StrLength(S)
            初始条件:串S存在。
            操作结果:返回S的元素个数,称为串的长度。
        ClearString (&S)
            初始条件:串S存在。
            操作结果:将S清为空串。
        Concat ( &T, S1, S2)
            初始条件:串S1S2存在。
            操作结果:用T返回S1S2连接在一起的串。
        SubString( &Sub,S,pos,len)
            初始条件:串S存在,1<=pos<=StrLength(S)0<=len<=StrLength(S)-pos+1//(注意:pos位置是指第pos位,所以是从第一位开始算)
            操作结果:用Sub返回串S的第pos个字符起长度为len的子串。
        Index(S,T,pos)
            初始条件:串ST存在,且T为非空串,1<=pos<=StrLength(S)。
            操作结果:若主串S存在和串T值相同的子串,则返回它在主串中第pos个字符后第一次出现的位置,否则函数值为0Replace(&S, T,V)
            初始条件:串STV存在,T为非空串。
            操作结果:用V替换主串S中出现的所有与T相等的不重复的子串。
        StrInsert(&S, pos, T)
            初始条件:串ST存在,1<=pos<=StrLength(S)+1。
            操作结果:在串S的第pos个字符之前插入串TStrDelete(&S, pos, len)
            初始条件:串S存在,1<=pos<=StrLength(S)-len+1。
            操作结果:从串S中删除第pos个字符起长度为len的子串。
        DestroyString(&S)
            初始条件:串S存在。
            操作结果:串S被销毁。
}ADT String 

3.3 串的存储结构

  • 顺序存储结构:串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列
  • 链式存储结构:

3.4 串的匹配

  • 当有两个字符串Str = "abdabcde;和 modStr = “abcd”;时,如果要在Str中查找与modelStr相等的子串,则称Str为主串,modelStr为模式串。在查找过程中,从Str中的第一个字符进行比较,如果找到与modelStr相同的子串,函数返回modelStr字符串第一次出现的位置,否则返回-1。以上过程就称为模式匹配
  • 模式匹配算法,最广为人知的有两种算法,一种为简单且暴力的BF算法,一种为效率极高的KMP算法。

3.4.1 串的比较

3.4.2 朴素的模式匹配算法

  • 算法思想:BF算法为朴素模式匹配算法的经典,该算法的主要思想,从主串的第一个字符与模式串中的第一个字符进行比较,若相等则继续比较,若不相等则从主串中不相等的位置与模式串中的第一个字符进行比较。

  • 图解:

    image-20240523164037980
  • 算法实现:

    package com.string.kmp;
    
    public class BF {
        public static void main(String[] args) {
            System.out.println(Index_BF("aaaaaaab", "aaab"));
        }
    
        public static int Index_BF(String S, String T) {
            char[] s = S.toCharArray();
            char[] t = T.toCharArray();
    
            int i = 0; // 主串的位置
            int j = 0; // 模式串的位置
    
            while (i < s.length && j < t.length) {
                if (s[i] == t[j]) {
                    i++;
                    j++;
                } else {
                    i = i - j + 1;
                    j = 0;
                }
            }
            if (j >= s.length) {
                return i - t.length;
            } else {
                return -1;
            }
        }
    }
    
  • 算法分析:指针i指向主串,j指向模式串,当匹配失败时,i移动的位置为i = i - j + 1。当所有字符串匹配成功后,指针j指向了模式串末尾的下一个,在进行匹配的过程中,不难发现,当每次匹配失败后,i指针都进行了不必要的回溯,这个回溯过程造成了时间的大量浪费。

3.4.3 KMP模式匹配算法

3.4.3.1 KMP算法的优势
  • KMP算法就是针对BF算法的改良,当进行模式匹配时,出现字符比较不等的情况,不用回溯i指针,而是利用已经匹配过后的结果,向右尽可能滑动的稍远一些,再继续进行比较。
3.4.3.2 KMP算法的原理
  • 下面通过三个示例来说明KMP算法的原理

  • 示例1:字符串Str = "acabaabaabcacaabc";modStr = "abaabcac";

    • i=8, j=6时,发现匹配失败: 如果按照BF算法, i指针会回溯至4的位置重新开始比较。但是这种i的回溯是有必要的吗?显然duck不必。
    • 观察一下,j1=j4, j2=j5且i3=j1,i4=j2,所以回溯i指针到4的位置完全没有必要,根据上述等式关系,我们只需要将j指针的位置移动到j4的位置重新开始匹配。
    image-20240523164117659
  • 示例2: str=“aabababcaad” modStr=“babc”

    • i=6, j=4时发生失配,但j1=j3,所以直接将j指针移动到j4的位置,开始匹配。
    image-20240523164152000
  • 示例3:str=“aacabcd”, modStr=“abcd,当发生失配时,怎么操作。

    • 在发生失配时,将i指针向后移动一位,继续进行比较。
    image-20240523164236913
  • 综合上述例子,不难看出模式串中存在着字符相同的元素,在模式串的子串中构成了前缀与后缀。并且在失配之后对于i指针的移动存在一定的规律,从而引出了next数组的概念,next数组用于存放模式匹配失配时,模式串的滑动距离。

3.4.3.3 next数组的构造
  • next[]数组存放的内容为:模式串与目标串匹配失败时用于寻找模式串回溯位置的数组。

  • 如何计算next数组呢?其中最重要的概念就是最大前缀与最大后缀。

    • 公式:
    image-20240523164320309
  • 示例:str="abaabcac"以这个字符串为例

    • 表示出它的前缀与后缀

      image-20240523164352801
    • 对于这个字符串它的前缀与后缀是这样的,所以它的最大前缀=最大后缀的字符串是a;接下来就要通过这个最大前缀=最大后缀的概念,来进行失配后模式串回溯的位置计算。

    • next[]数组计算步骤:设模式串为abaabcac( j指向的位置为当前匹配位置,假设在当前位置失配,求出在当前位置失配的回溯位置,j指针前方字符串为要计算回溯位置的子串)

      1. j=1失配 (因为j=0 所以next[j]=0)
        image-20240523164425876

      2. j=2失配 (没有前后缀,属于其他情况)
        image-20240523164442281

      3. j=3失配(前缀不等与后缀)
        image-20240523164454797

      4. j=4失配(最大前缀=最大后缀,所以k=2)
        image-20240523164512491

      5. j=5失配
        image-20240523164528659

      6. j=6失配 (最大前缀 ab = 最大后缀 ab, 所以k=3)
        image-20240523164609196

      7. j=7失配
        image-20240523164626965

      8. j=8失配

        image-20240523164644150
      9. 以上为next数组求法,但字符串下表一般从0开始,所以要对next数组中元素-1,结果如下图

        image-20240523164718213

    至此,next数组已经求解完毕,如何利用next数组进行模式匹配,继续阅读下文。

3.4.3.4 利用next数组匹配的过程
  • 在这一部分,将会说明模式匹配是如何利用next数组进行模式匹配。

  • str = "acabaabaabcacaabc";modStr = "abaabcac";为例,字符串下标从0开始,进行说明:

    image-20240523164747614
  1. 第一次匹配(i=1,j=1位置发生失配):根据next数组可知,将j指针回溯到j0位置进行重新匹配。

    image-20240523164837059
  2. 第二次匹配(i=1,j=0位置发生失配):next数组中没有相关跳转位置,所以i指针后移一位,开始匹配。

    image-20240523165033277
  3. 第三次匹配(i=7,j=5位置发生失配):根据next数组,可知j回溯到 2的位置重新开始匹配

    image-20240523165004450
  4. 匹配成功

    image-20240523164945214

以上就为KMP算法的匹配过程

3.4.3.5 算法实现
public class KMP {
    public static void main(String[] args) {
        System.out.println(Index_KMP("goodgoogle", "aaabd"));
        System.out.println(Index_KMP("aaaaaaaaaaaaaaaaaaaab", "aaabaaabaaaaaaaa"));
        System.out.println(Index_KMP("aaaaaaaaaaaaaaaaaaaab", "aaab"));
    }

    public static int Index_KMP(String ts, String ps) {

        char[] t = ts.toCharArray();

        char[] p = ps.toCharArray();

        int i = 0; // 主串的位置

        int j = 0; // 模式串的位置

        int[] next = getNext(ps);
        int[] next = getNextval(ps);

        while (i < t.length && j < p.length) {
            if (j == -1 || t[i] == p[j]) { // 当j为-1时,要移动的是i,当然j也要归0
                i++;
                j++;
            } else {
                // i不需要回溯了
                // i = i - j + 1;
                // j回溯
                j = next[j]; // j回到指定位置
            }
        }

        if (j == p.length) {
            return i - j;
        } else {
            return -1;
        }
    }

    public static int[] getNext(String ps) {
        //1.将子串变成字符数组
        char[] p = ps.toCharArray();
        //2.定义一个next数组,长度和子串长度相等
        int[] next = new int[p.length];
        //3.初始化next的0索引
        next[0] = -1;

        //4.开始计算next数组
        int i = 0;//后缀
        int j = -1;//前缀
        while (i < p.length - 1) {
            if (j == -1 || p[i] == p[j]) {
                ++i;
                ++j;
                next[i] = j;
                //next[++i] = ++j;
            } else {

                j = next[j];
            }
        }

        return next;
    }

    public static int[] getNextval(String ps) {
        //1.将子串变成字符数组
        char[] p = ps.toCharArray();
        //2.定义一个next数组,长度和子串长度相等
        int[] nextval = new int[p.length];

        //3.初始化next的0索引
        nextval[0] = -1;

        //4.开始计算next数组
        int i = 0;//后缀
        int j = -1;//前缀
        while (i < p.length - 1) {
            if (j == -1 || p[i] == p[j]) {
                ++i;
                ++j;
                if (p[i] != p[j]) {
                    nextval[i] = j;
                } else {
                    nextval[i] = nextval[j];
                }
            } else {
                j = nextval[j];
            }
        }

        return nextval;
    }
}
3.4.3.6 算法分析
  • 最好情况的时间复杂度O(n+m)

  • 最坏情况的时间复杂度O(n×m)

3.4.4 KMP模式匹配算法改进

  • 克服不必要重复比较的方法是对next数组的修正,在算法中求得next[j]=k后,要继续判断tₖ和tⱼ是否相等,若相等还要使next[j]=next[k]。修正后的next数组称为nextval数组

  • 示例:已知主串s="aaabaaaab"和模式串t=“aaaab”,利用KMP算法进行模式匹配

    image-20240523165113432

    根据KMP算法,当i=3,j=3时,s₃≠ t₃,由next[j]的指示还需要进行i=3、j=2,i=3、j=1,i=3、j=0等三次比较。实际上,模式串中下标为0、1、2的字符和下标为3的字符都相等,就不需要和主串中 下标为3的字符进行比较,可以将模式一起向右滑动4个字符的位置直接进行i=4、j=0的字符比较

    计算nextval的具体过程:

    • 第一位的nextval值必定为0,第二位如果与第一位不同则为0,相同则为-1
    • 第三位的next值为1,那么将第三位和第一位进行比较,均为a,相同,则第三位的nextval值为第一位的nextval值,为-1
    • 第四位的next值为2,那么将第四位和第二位进行比较,均为a,相同,则第三位的nextval值为第二位的nextval值,为-1
    • 第五位的next值为3,那么将第五位和第三位进行比较,不同,则第五位的nextval值为其next值,为3
  • 算法实现:

    public class KMP {
        public static void main(String[] args) {
            System.out.println(Index_KMP("goodgoogle", "aaabd"));
            System.out.println(Index_KMP("aaaaaaaaaaaaaaaaaaaab", "aaabaaabaaaaaaaa"));
            System.out.println(Index_KMP("aaaaaaaaaaaaaaaaaaaab", "aaab"));
        }
    
        public static int Index_KMP(String ts, String ps) {
    
            char[] t = ts.toCharArray();
            char[] p = ps.toCharArray();
    
            int i = 0; // 主串的位置
            int j = 0; // 模式串的位置
    
            int[] next = getNextval(ps);
    
            while (i < t.length && j < p.length) {
                if (j == -1 || t[i] == p[j]) { // 当j为-1时,要移动的是i,当然j也要归0
                    i++;
                    j++;
                } else {
                    // i不需要回溯了
                    // i = i - j + 1;
                    // j回溯
                    j = next[j]; // j回到指定位置
                }
            }
    
            if (j == p.length) {
                return i - j;
            } else {
                return -1;
            }
        }
    
        public static int[] getNextval(String ps) {
            //1.将子串变成字符数组
            char[] p = ps.toCharArray();
            //2.定义一个next数组,长度和子串长度相等
            int[] nextval = new int[p.length];
    
            //3.初始化next的0索引
            nextval[0] = -1;
    
            //4.开始计算next数组
            int i = 0;//后缀
            int j = -1;//前缀
            while (i < p.length - 1) {
                if (j == -1 || p[i] == p[j]) {
                    ++i;
                    ++j;
                    if (p[i] != p[j]) {
                        nextval[i] = j;
                    } else {
                        nextval[i] = nextval[j];
                    }
                } else {
                    j = nextval[j];
                }
            }
    
            return nextval;
        }
    }
    

参考文献

  • https://blog.csdn.net/weixin_43717520/article/details/115592947

  • https://blog.csdn.net/m0_68320520/article/details/127613211

  • https://blog.csdn.net/qq_51066068/article/details/124118929

  • https://blog.csdn.net/qq_62173470/article/details/127296344
    tval的具体过程:

    • 第一位的nextval值必定为0,第二位如果与第一位不同则为0,相同则为-1
    • 第三位的next值为1,那么将第三位和第一位进行比较,均为a,相同,则第三位的nextval值为第一位的nextval值,为-1
    • 第四位的next值为2,那么将第四位和第二位进行比较,均为a,相同,则第三位的nextval值为第二位的nextval值,为-1
    • 第五位的next值为3,那么将第五位和第三位进行比较,不同,则第五位的nextval值为其next值,为3
  • 算法实现:

    public class KMP {
        public static void main(String[] args) {
            System.out.println(Index_KMP("goodgoogle", "aaabd"));
            System.out.println(Index_KMP("aaaaaaaaaaaaaaaaaaaab", "aaabaaabaaaaaaaa"));
            System.out.println(Index_KMP("aaaaaaaaaaaaaaaaaaaab", "aaab"));
        }
    
        public static int Index_KMP(String ts, String ps) {
    
            char[] t = ts.toCharArray();
            char[] p = ps.toCharArray();
    
            int i = 0; // 主串的位置
            int j = 0; // 模式串的位置
    
            int[] next = getNextval(ps);
    
            while (i < t.length && j < p.length) {
                if (j == -1 || t[i] == p[j]) { // 当j为-1时,要移动的是i,当然j也要归0
                    i++;
                    j++;
                } else {
                    // i不需要回溯了
                    // i = i - j + 1;
                    // j回溯
                    j = next[j]; // j回到指定位置
                }
            }
    
            if (j == p.length) {
                return i - j;
            } else {
                return -1;
            }
        }
    
        public static int[] getNextval(String ps) {
            //1.将子串变成字符数组
            char[] p = ps.toCharArray();
            //2.定义一个next数组,长度和子串长度相等
            int[] nextval = new int[p.length];
    
            //3.初始化next的0索引
            nextval[0] = -1;
    
            //4.开始计算next数组
            int i = 0;//后缀
            int j = -1;//前缀
            while (i < p.length - 1) {
                if (j == -1 || p[i] == p[j]) {
                    ++i;
                    ++j;
                    if (p[i] != p[j]) {
                        nextval[i] = j;
                    } else {
                        nextval[i] = nextval[j];
                    }
                } else {
                    j = nextval[j];
                }
            }
    
            return nextval;
        }
    }
    

参考文献

  • 16
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神马都会亿点点的毛毛张

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值