基础和技术就像无敌的深渊,小伙子,你要不断的学哟~~…
特此鸣谢在leetcode上分享答案的各位大神,让我能够对自己的笔记有如下补充:
Part1:字符串
- 字符串都以字符’\0’结尾
- 这样咱们相当于找到这个字符就相当于找到字符串的结尾了,这也算是寻找字符串结尾的一个方法
- 但是这个会很容易造成字符串越界,别忽略这一个字符,给人家分配初始容量时少分配一个
- 为了节省空间,常量字符串(与字符串数组一定要区分开来)常常被放到一个单独的内存空间。当几个指针
赋值给相同的常量字符串时
这几个指针会指向相同的内存地址
。String s1 = "hhb min";// 在堆中创建字符串对象"hhb min",并同时将字符串对象"hhb min"的引用保存在字符串常量池中 String s2 = "hhb min";//直接返回字符串常量池中字符串对象"hhb min"的引用 //s1 == s2,此时无须为s1、s2分配不同地址的内存来存储字符串的内容,而只需要把他们指向“hhb min”在内存中的地址就行了(此时只有一个hhb min常量字符串哈,所以内存中仅此一份,独一份,相当于在内存中只有一个拷贝,所以这两个指针指向的是同一个地址) Char[] str3 = "hhb min"; char[] str4 = "hhb min"; //str3 != str4;因为这两个相当于实例化了一个(字符串)数组(数组有三种实例化方式,比如char[] s = new char[2];char[] s = new char[]{...};...),数组是引用类型,所以他的初始化过程是这样的,找两个不同的地址空间,把”hhb min“的内容分别复制到两个不同地址的数组中,这两个数组地址都不同了,怎么可能==呢
- 字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,
主要目的是为了避免字符串的重复创建
。对于编译期可以确定值的字符串,也就是常量字符串
,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池
,这个得益于编译器的优化。- 在编译过程中,Javac 编译器会进行一个叫做 常量折叠(Constant Folding) 的代码优化,常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
比如对于 String str3 = "str" + "ing"; 编译器会给你优化成 String str3 = "string
";
- 并不是所有的常量都会进行折叠,引用的值在程序编译期是无法确定的【如果 编译器在运行时才能知道其确切值的话,就无法对其优化】,编译器无法对其进行优化。只有编译器在程序编译期就可以确定值的常量才可以:比如下面几种:
- 基本数据类型( byte、boolean、short、char、int、float、long、double)以及字符串常量。
- final 修饰的基本数据类型和字符串变量
- 字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。
被 final 关键字修改之后的 String 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量
。
- 字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。
- 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
- 如果 编译器在运行时才能知道其确切值的话,就无法对其优化,比如下面的str2
- 在编译过程中,Javac 编译器会进行一个叫做 常量折叠(Constant Folding) 的代码优化,常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
- String s1 = new String(“abc”);
这句话会创建 1 或 2 个字符串对象
。- 如果字符串常量池中不存在字符串对象“abc”的引用,那么会在堆中创建 2 个字符串对象“abc”。
- 如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”
- 如果字符串常量池中不存在字符串对象“abc”的引用,那么会在堆中创建 2 个字符串对象“abc”。
- 字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,
- String、StringBuilder、StringBuffer的区别:
- StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,
最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法
- String 类型和 StringBuffer 的主要性能区别
String 是不可变的对象,因为内部使用final关键字修饰, 因此在每次对 String 类型进行改变的时候【比如对字符串进行拼接或者裁剪时】,都会生成一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String
,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,性能就会降低;使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。所以多数情况下推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下
。【StringBuffer为了解决字符串拼接过程中产生太多中间对象而生,StringBuffer提供了append方法把字符串添加到已经有的序列的末尾或者指定位置】- 在某些特别情况下, String 对象的字符串拼接其实是被 Java Compiler 编译成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢
如果要操作少量的数据,用String
;单线程操作大量数据,用StringBuilder
;多线程操作大量数据,用StringBuffer
。- String 中的对象是
不可变的,也就可以理解为常量,线程安全
。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的
。
- String 中的对象是
- 不要使用String类的"+"来进行频繁的拼接,因为那样的性能极差的,应该使用StringBuffer或StringBuilder类,这在Java的优化上是一条比较重要的原则
- 每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符
。
- 可以看出字符串对象通过“+”的字符串拼接方式,
实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象
。
- 不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,
会导致创建过多的 StringBuilder 对象【StringBuilder 对象是在循环内部被创建的,这意味着每循环一次就会创建一个 StringBuilder 对象。】
。
- 可以看出字符串对象通过“+”的字符串拼接方式,
- 为了获得更好的性能,在构造 StringBuffer 或 StringBuilder 时应尽可能指定它们的容量。当然,如果你操作的字符串长度(length)不超过 16 个字符就不用了,当不指定容量(capacity)时默认构造一个容量为16的对象。不指定容量会显著降低性能。
- StringBuilder 一般使用在方法内部来完成类似 + 功能,因为是线程不安全的,所以用完以后可以丢弃。StringBuffer 主要用在全局变量中。
- 相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。而在现实的模块化编程中,负责某一模块的程序员不一定能清晰地判断该模块是否会放入多线程的环境中运行,因此:
除非确定系统的瓶颈是在 StringBuffer 上,并且确定你的模块不会运行在多线程模式下,才可以采用 StringBuilder;否则还是用 StringBuffer
。- StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的
- StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,
- String中的内容是不可变的,这句话怎么理解呢
- String类中使用final关键字修饰字符数组来保存字符串【
final 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象
】,但用final并不是String不可变的根本原因,因为这个数组保存的字符串是可变的(final 修饰引用类型变量的情况)。public final class String implements java.io.Serializable, Comparable<String>, CharSequence { private final char value[]; //... }
- 保存字符串的数组被 final 修饰且为私有的,
并且String 类没有提供/暴露修改这个字符串的方法
。 - String 类被 final 修饰导致其不能被继承,进而
避免了子类破坏 String 不可变
。
- 保存字符串的数组被 final 修饰且为私有的,
- 试图改变String内容或者试图把一个常量字符串赋值给一个String实例时其实都是产生了一个新的String实例(而不是把原来String的内容改成赋值的字符串),
不管咱调用的是String的哪个方法,其实都是相当于copy出来了一个副本
(每一次修改都会产生一个临时对象),然后生成一个新的String实例在返回值中返回- 有的时候产生的这个副本或者说新的实例或者说新的临时对象很影响效率,所以出现了一个新的与字符串相关的类型叫StringBuilder
String str1 = "hhb min";//此时生成了一个内容是hhb min的String实例 ... public static void method(String str2){ String str1 = "minhhb";//此时又生成了一个新的内容是”minhhb“的String实例,先生成新实例然后把str1这个指针从原来的hhb min转过来指向新的minhhbString新实例。 ...... } //但是当方法执行完了后出了这个方法后str1还是会继续重新指向原来的字符串hhb min
- 有的时候产生的这个副本或者说新的实例或者说新的临时对象很影响效率,所以出现了一个新的与字符串相关的类型叫StringBuilder
- 在 Java 9 之后,
String 、StringBuilder 与 StringBuffer 的实现改用 byte 数组存储字符串
String 的底层实现由 char[] 改成了 byte[]
- 新版的 String 其实支持两个编码方案: Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案【JDK 官方就说了绝大部分字符串对象只包含 Latin-1 可表示的字符。如果字符串中包含的汉字超过 Latin-1 可表示范围内的字符,byte 和 char 所占用的空间是一样的】。Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。
- String.intern()
- String.intern() 是一个 native(本地)方法,作用是
将指定的字符串对象的引用保存在字符串常量池中
,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
- String.intern() 是一个 native(本地)方法,作用是
- String类中使用final关键字修饰字符数组来保存字符串【
- 做题时碰见了一个小技巧,如果我想给一个String[]字符串数组里面的每个位置上或者说每个索引上的字符串们排序,我该怎么排,思路就是我得先循环遍历把String数组每个索引位置上的字符串取出来,然后toCharArray,再排序,排完序后再转回String字符串,ABA一下
//strs代表字符串数组
for(String string : strs){
//将String[]数组中取出每个位置上的或者说每个索引上的字符串转为字符数组,为啥要转呢,因为咱们想对数组中的每个索引上的字符串中的字符们也就是小写字母们做一下统计
char[] chars = string.toCharArray();//比如咱们第一次肯定是取到了String[]数组的第一个索引上的字符串"eat",然后把字符串"eat"toCharArray()后就变成了[e, a, t]
//相当于对String[]数组每个位置上或者每个索引上的字符串按照小写字母的顺序排个序,为啥要排序,因为一排序同源不同源不一眼就看出来了嘛
Arrays.sort(chars);
String key = String.valueOf(chars);//又把按照小写字母排好序的char[]数组转换为Sring,也就是一个字符串
...
}
Notes:最长上升或者叫递增子串(子序列)及其长度+最长回文子串(子序列)及其长度+最长不重复子串(子序列)及其长度+最长公共子串(子序列)及其长度+给两个子串让判断这一个是不是那一个的子串
- 走一题:最长不重复子串(子序列)及其长度:两种方法,单列集合做窗口、双列集合做窗口
class Solution {
public int lengthOfLongestSubstring(String s) {
//官话判空
if (s.length() == 0 || s == null) {
return 0;
}
/**
* 一般关于子串、子序列的题会用滑动窗口来解决,
* 而滑动窗口一般先搞出来两个神器,(用单双列集合或者多态啥的,随机应变)如下:(不一定都能用上,随机应变)
*
* //神器1,need相当于一个计数器,来记录某个字符串中某些字符出现次数
* HashMap<Character, Integer> needs = new HashMap<>();
* //神器2,window看名字就知道,来记录窗口中某个字符出现的次数了呗
* HashMap<Character, Integer> window = new HashMap<>();
*/
HashMap<Character, Integer> window = new HashMap<Character, Integer>();
int max = 0;
int left = 0;//窗口的左边界
for(int right = 0; right < s.length(); right++){
//刚开始窗口的左右指针或者说左右边界都是从0开始的,而咱们这里用的是右指针进行遍历游走,所以右指针开始走
if(window.containsKey(s.charAt(right))){//刚开始右指针从0开始,所以肯定if条件不成立,直接跳过if判断
left = Math.max(left, window.get(s.charAt(right)) + 1);//如果这个s.charAt(right)这个字符已经在窗口中出现了一次了那按照题目要求就不能再进入到窗口中了,所以就得记录下这个长度,然后代表窗口左边界的左指针向前步进一步。比比看如果已经有了一次,那么左边界就相当于这个已经出现的字符的下一个,也就是window.get(s.charAt(i)) + 1,
}
//跳过if判断不就到这了嘛,下面这两句意思就是,窗口里面不包含我就把右指针从0开始扫到的字符一个一个添加到窗口中,直到遇到重复的(因为遇到重复字符时就被上面的if循环拦住了,下不来了呀)
window.put(s.charAt(right), right);//如果没执行上面那个if说明窗口里面没有这个字符,那就把这个字符塞进窗口中
max = Math.max(max, right - left + 1);//这个就是右指针扫呀扫扫呀扫,不重复的子串,咱不得把长度记下来嘛,不然不就白干了嘛
}
return max;
}
}
class Solution{
public int lengthOfLongestSubstring(String s){
/**
* 一般关于子串、子序列的题会用滑动窗口来解决,
* 而滑动窗口一般先搞出来两个神器,(用单双列集合或者多态啥的,随机应变)如下:(不一定都能用上,随机应变)
*
* //神器1,need相当于一个计数器,来记录某个字符串中某些字符出现次数
* HashMap<Character, Integer> needs = new HashMap<>();
* //神器2,window看名字就知道,来记录窗口中某个字符出现的次数了呗
* HashMap<Character, Integer> window = new HashMap<>();
*/
Set<Character> window = new HashSet<>();
int result = 0;
//窗口,怎么能免得了双指针呢
//左指针left代表这符合条件的无重复的最长子串的起始位置
int right = -1;//右指针right代表这符合条件的无重复的最长子串的结束位置。刚开始先把右指针放在字符串的左边界的左侧
for(int left = 0; left < s.length(); left++){//开始遍历字符串,left++相当于咱们枚举下一个字符作为符合条件的无重复的最大子串的窗口的起始位置
//刚开始left是等于0的呀,所以,刚开始第一次这个if(left != 0){}里面的代码不会被执行,所以直接到下面while循环中
if(left != 0){
//左指针向右移动一格相当于移除一个字符
window.remove(s.charAt(left - 1));
}
//这个while循环看这条件,是会从第一个字符开始找,找一个字符不重复找一个不重复...,直到碰到了一个重复的字符,跳出while循环
//在不断枚举下一个下下一个下下下一个...字符为符合条件的无重复的子串的窗口的起始位置时,咱们肯定要用右指针作为窗口边界来替咱们把关,判断是不是符合最大呀,是不是符合无重复呀
while(right < s.length() - 1 && !window.contains(s.charAt(right + 1))){
//不断移动右指针相当于给窗口中从右边新添进来字符
window.add(s.charAt(right + 1));
right++;
}
//跳出while不就到这了嘛,相当于记录一下此时的符合条件的最长的无重复的字符子串(此时i到right是一个最长的无重复的字符子串)
result = Math.max(result, right - left + 1);
}
return result;
}
}
- 与这个无重复最长字符串相似的题还有这个:滑动窗口的最大值(给定一个长度为 n 的数组 nums 和滑动窗口的大小 size ,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。)
//若是一个数字A进入窗口后,若是比窗口内其他数字都大,那么这个数字之前的数字都没用了,因为它们必定会比A早离开窗口,在A离开之前都争不过A,所以A在进入时依次从尾部排除掉之前的小值再进入,而每次窗口移动要弹出窗口最前面值,因此队首也需要弹出,所以我们选择双向队列。
//O(n),数组长度为n,只遍历一遍数组
//空间复杂度:O(m),窗口长度m,双向队列最长时,将窗口填满
import java.util.*;
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size) {
ArrayList<Integer> res = new ArrayList<Integer>();
//首先检查窗口大小与数组大小。窗口大于数组长度的时候,返回空
if(size <= num.length && size != 0){
//维护一个双向队列,用来存储数列的下标。
ArrayDeque <Integer> dq = new ArrayDeque<Integer>();
//先遍历一个窗口.先遍历第一个窗口,如果即将进入队列的下标的值大于队列后方的值,依次将小于的值拿出来去掉,再加入,保证队列是递增序。
for(int i = 0; i < size; i++){
//去掉比自己先进队列的小于自己的值
while(!dq.isEmpty() && num[dq.peekLast()] < num[i])
dq.pollLast();
dq.add(i);
}
//遍历后续数组元素.遍历后续窗口,每次取出队首就是最大值,如果某个下标已经过了窗口,则从队列前方将其弹出。
for(int i = size; i < num.length; i++){
//取窗口内的最大值
res.add(num[dq.peekFirst()]);
while(!dq.isEmpty() && dq.peekFirst() < (i - size + 1))
//弹出窗口移走后的值
dq.pollFirst();
//加入新的值前,去掉比自己先进队列的小于自己的值
while(!dq.isEmpty() && num[dq.peekLast()] < num[i])
dq.pollLast();
dq.add(i);
}
res.add(num[dq.pollFirst()]);
}
return res;
}
}
//直接遍历两层:第一层为窗口起点[第一次遍历数组每个位置作为窗口的起点。],第二层为窗口长度,即遍历了所有的窗口的每个位置。
//时间复杂度:O(nm),其中nnn为数组长度,m为窗口长度,双层for循环
//空间复杂度:O(1),没有使用额外的辅助空间,暂存的结果res不算入空间开销
import java.util.*;
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size) {
ArrayList<Integer> res = new ArrayList<Integer>();
//窗口大于数组长度的时候,返回空
if(size <= num.length && size != 0)
//数组后面要空出窗口大小,避免越界
for(int i = 0; i <= num.length - size; i++){
//寻找每个窗口最大值.从每个起点开始遍历窗口长度,查找其中的最大值。
int max = 0;
for(int j = i; j < i + size; j++){
if(num[j] > max)
max = num[j];
}
res.add(max);
}
return res;
}
}
- 走两题:
- 判断一个字符串是不是回文串
//判断一个字符串是不是回文串就不需要考虑奇偶情况,只需要双指针技巧,从两端向中间逼近
boolean isPalindrome(String s){
int left = 0;
int right = s.length - 1;
while(left < right){
if(s[left] != s[right]){
return false;
}
left++;
right--;
}
return true;
}
肯定也不止这些,比如判断一个字符串是不是回文串、最大回文子串、回文字串的个数、回文字串的长度等,除了子串,还有回文子序列、回文子数组等(回文相关题目在leetcode可不止这几道,随便搜索就很多:),去其他文章里面翻翻哦,惊喜连连。回文串(正着读和反着读都一样的字符串,比如aba和abba就是回文串,abac就不是回文串)问题。注意回文串的长度可能是奇数也可能是偶数,解决思路就是双指针、动态规划、中心扩散等。
- 在字符串中找一个最长的回文子串:
- 一种思路就是:既然回文串正着读和反着读都一样,那么我们如果把S反转,成为s’,然后在s和s’中寻找最长公共子串,这样就能找到最长的回文子串。(但是比如,字符串aacxycaa反转后aacyxcaa,最长字符串aac,但是正确的最长回文子串应该是aa而不是aac,所以这种思路不对,但是以后咱们要多想想这种类似的思路转化),所以说这种有例外,不能用。
- 另外两种思路就是,
- 寻找回文串的核心思想是:从中间开始向两边扩散来判断回文串。判断回文串是从两端向中间收缩。
- 动态规划
class Solution {
//时间复杂度为O(N^2),最坏情况下两个指针要把整个字符串走两遍哦,空间复杂度为O(1)
public String longestPalindrome(String s) {
//官话判空
if(s == null || s.length() == 0){
return "";
}
// ababa 求最长公共子串
int len = s.length();
String result = "";
for (int i = 0; i < len * 2 - 1; i++) {
int left = i / 2;
int right = left + i % 2;
while (left >= 0 && right < len && s.charAt(left) == s.charAt(right)) {
String tmp = s.substring(left, right + 1);
if (tmp.length() > result.length()) {
result = tmp;
}
left--;
right++;
}
}
return result;
}
然后呢,还有一位大神以及官方答案,参考记录如下:
/**
* Copyright (c) 2013-Now http://AIminminAI.com All rights reserved.
*/
package DynamicProgramming;
/**
* 为什么咱们会选用动态规划DP呢,原因如下:
* 咱们拿到这个题得先分析一下,这个题有没有层层脱皮性质与层层套皮性质
* 对于一个子串而言,如果这个子串是回文串并且长度大于2,那么层层脱皮性质就是将这个字符串首尾两个字母去除之后剩下的中间这部分还是个回文字符串,那么原来的这个字符串肯定是个回文字符串
* 为什么要长度大于2,你长度为1就一个字符,你玩呢???
* 你长度为0,官话把你给判空了,你还玩啥
* 对于一个子串,如果长度大于2而且是个回文字符串,首尾各加上一个一个相同的字符依旧是回文字符串,这是层层套皮性质
* 满足这俩性质后我们可以确定这个题就可以用动态规划DP方法解决本题
*
* @author HHB
* @version 2022年4月17日
*/
public class LongestPalindromicSubstring {
public String longestPalindrome(String str){
if(s.length() == 0 || s == null || s.equals("")){
return str;
}
/**
*
* 这一步也相当于官话判空
*/
if(str.length() < 2){
return str;
}
//str[leftBound.....rightBound]表示字符串str的第leftBound个字符到第rightBound个字符组成的串是否为回文字符串,如果是则dp[leftBound][rightBound] = true;
//dp[leftBound][rightBound]代表str[leftBound.....rightBound]是否为回文字符串,是个布尔变量哦
//刚初始化dp是这个样子:[[false, false, false, false, false], [false, false, false, false, false], [false, false, false, false, false], [false, false, false, false, false], [false, false, false, false, false]]
boolean[][] dp = new boolean[str.length()][str.length()];
//初始化:所有长度为1的子串都是回文字符串呀
//业界不也是经常会填一个dp表格嘛,这个其实也就是在填dp表格的对角线,因为对角线上的元素肯定是回文的,dp是这个样子:[[true, false, false, false, false], [false, true, false, false, false], [false, false, true, false, false], [false, false, false, true, false], [false, false, false, false, true]]
for (int i = 0; i < str.length(); i++) {
dp[i][i] = true;
}
int maxLengthResult = 1;
int begin = 0;
//原来str是这样子:babad,经过这一步就变成这样了:[b, a, b, a, d],这就是chars的样子哦,惊喜不
char[] chars = str.toCharArray();
//递推开始,两个for循环整起
//先枚举子串长度,strLen就代表咱们每次枚举的子串的长度,也就是相当于一个间距,相当于先把你要枚举的串长度掐定然后你两个指针在按照这个间距去移动遍历吧
for (int strLen = 2; strLen <= str.length(); strLen++) {
//枚举左边界,左边界的上限设置可以宽松一点
/**
* 第一轮循环leftBound=0,rightBound=1,相当于开始比较str[0...1]内的字符串是不是回文字符串
* 第二轮循环leftBound+1,leftBound=1,rightBound经计算变为2,进入else{}
* 第三轮循环leftBound=2,rightBound=3
* 第四轮循环leftBound=3,rightBound=4
* 第五轮循环leftBound=4,rightBound=5
*
*
* 先是01,12,23,34,45,接着是02,13,24,35,接着是03,14,25,接着是04,
*/
for (int leftBound = 0; leftBound < str.length(); leftBound++) {
//由strLen和leftBound可以确定判定范围内的字符的右边界(右边界就是strLen + leftBound - 1),也相当于目标字符串,或者说回文字符串
int rightBound = strLen + leftBound - 1;
// 如果右边界越界,那还判断个啥,都越界了,就可以退出当前循环
if(rightBound >= str.length()){
break;
}
if(chars[leftBound] != chars[rightBound]){
dp[leftBound][rightBound] = false;
}else {
/**
* 这个rightBound - leftBound < 3是怎么来的呢。是这样,如果判断走到if这里证明字符串头尾两个字符是相等的,那么按照动态规划的思想,我们去掉头尾两个字符,
* 中间剩下的字符串依旧是回文字符串,那中间剩下的字符串的长度不就是rightBound - 1 - (leftBound + 1) + 1 < 2,因为一个字符的咱们在上面已经判断过了呀,
* 你整理一下不就是rightBound - leftBound < 3嘛
*/
if(rightBound - leftBound < 3){
dp[leftBound][rightBound] = true;
}else {
dp[leftBound][rightBound] = dp[leftBound + 1][rightBound - 1];
}
}
//只要dp[leftBound][rightBound] == true就表示子串str[leftBound.....rightBound]是回文字符串,此时咱们就得记录下来回文串的长度和起始位置
if (dp[leftBound][rightBound] && rightBound - leftBound + 1 > maxLengthResult) {
maxLengthResult = rightBound - leftBound + 1;
begin = leftBound;
}
}
}
return str.substring(begin, begin + maxLengthResult);
}
//public static void main(String[] args) {
//String str = "babad";
//String longestPalindrome = new LongestPalindromicSubstring().longestPalindrome(str);
//System.out.println(longestPalindrome);
//}
}
过程中发现了一个好玩的事:超出时间限制的现象,与君共乐
- 在字符串中找找有多少个回文子串:
/**
* 中心扩展法 :这个方法的时间复杂度是 O(N^2),空间复杂度是 O(1)。确定了一个中心点后的寻找的路径,然后我们只要寻找到所有的中心点就成了
* 比如对一个字符串 ababa,选择最中间的a作为中心点,往两边扩散
* 第一次扩散发现 left 指向的是 b,right 指向的也是 b,所以是回文串,继续扩散
* ......
* 同理 ababa 也是回文串。
* Copyright (c) 2013-Now http://AIminminAI.com All rights reserved.
*/
package String;
/**
*
* @author HHB
* @version 2022年4月20日
*/
public class CountSubString {
public int countSubstrings(String s){
if(s.length() == 0 || s == null){
return 0;
}
int res = 0;
//之所以这样就是因为回文串正着读和反着读是一样的
/**
*中心点(中心点即 left 指针和 right 指针初始化指向的地方,可能是一个也可能是两个,不可能是三个或者四个,因为 3 个可以由 1 个扩展一次得到,4 个可以由两个扩展一次得到)不能只有单个字符构成,还要包括两个字符,比如上面这个子串 abab,就可以有中心点 ba 扩展一次得到,所以最终的中心点由 2 * len - 1 个,分别是 len 个单字符和 len - 1 个双字符。
*比如,aba 有5个中心点,分别是 a、b、c、ab、ba
* abba 有7个中心点,分别是 a、b、b、a、ab、bb、ba
*/
for(int i = 0; i < 2 * s.length(); i++){
/**
*left和中心的点有一个很明显的2倍关系的存在,其次是right,可能和 left指向同一个(偶数时),也可能往后移动一个(奇数)
*/
int left = i / 2;
int right = i / 2 + i % 2;
/**
* 第一轮,i==0,left==0,right==0,那么进入while循环体中
* 第二轮,i==1,left==0,right==1,,第一个字符a和第二个字符b不相等跳过while循环进入第三轮
* 第三轮,i==1,left==1,right==1,那么进入while循环体中
*
* 所以可以看出,由左右双指针扫描了一遍所有的可能性,然后通过while条件判断是否为回文子串,然后在while循环中调整左右指针的步进,以及代表回文字串个数的变量值res
*/
while((left >= 0) && (right < s.length()) && (s.charAt(left) == s.charAt(right))){
left--;
right++;
res++;
}
}
return res;
}
public static void main(String[] args) {
String string = "abc";
int countSubstrings = new CountSubString().countSubstrings(string);
System.out.println(countSubstrings);
char[] charArray = string.toCharArray();
new UtilsByHu.ArrayUtils().printArr(charArray);
}
}
//当然啦,回文字串怎么可能没有动态规划的身影呢,依旧是熟悉的双层for循环。时间复杂度为O(N^2),空间复杂度为O(N^2)。
class Solution {
public int countSubstrings(String s) {
// 动态规划法
//状态:dp[i][j] 表示字符串s在[i,j]区间的子串是否是一个回文串。和最大回文字串一毛一样
boolean[][] dp = new boolean[s.length()][s.length()];
int ans = 0;
for(int rightBound = 0; rightBound < s.length(); rightBound++){
for(int leftBound = 0; leftBound <= rightBound; leftBound++){
//状态转移方程:当 s[i] == s[j] && (j - i < 2 || dp[i + 1][j - 1]) 时,dp[i][j]=true,否则为false。
//当只有一个字符时,比如 a 自然是一个回文串。
//当有两个字符时,如果是相等的,比如 aa,也是一个回文串。这就是j - i < 2的意思
//当有三个及以上字符时,比如 ababa 这个字符记作串 1,把两边的 a 去掉,也就是 bab 记作串 2,可以看出只要串2是一个回文串,那么左右各多了一个 a 的串 1 必定也是回文串。所以当 s[i]==s[j] 时,自然要看 dp[i+1][j-1] 是不是一个回文串。
if(s.charAt(leftBound) == s.charAt(rightBound) && ((rightBound - leftBound < 2) || dp[leftBound + 1][rightBound - 1])){
dp[leftBound][rightBound] = true;
res++;
}
}
}
return ans;
}
}
巨人的肩膀:
作者:jawhiow ;链接:https://leetcode-cn.com/problems/palindromic-substrings/solution/liang-dao-hui-wen-zi-chuan-de-jie-fa-xiang-jie-zho/