前言:
字符串其实只是一种数据结构,非常常见的数据结构,不过因为我自己使用java写的算法,感觉java中的字符串包还是优点不同的:
JAVA中的String类,StringBuilder类和StringBuffer类
java中的String内容是不可以改变的,
java中的StringBuilder内容是可以变的,且线程不安全,
java中的StringBuffer内容是可以变的,且线程安全
String、StringBuffer、StringBuilder的区别:
String、StringBuffer和StringBuilder是Java中用来处理字符串的类,它们之间的主要区别包括:
1. 不可变性:
- String是不可变的,一旦被创建,其值不能被修改。任何对String对象的操作都会返回一个新的String对象。
- StringBuffer和StringBuilder是可变的,可以通过方法修改其值。StringBuffer是线程安全的,而StringBuilder不是线程安全的。
2. 线程安全性:
- String是不可变的,因此在多线程环境下是安全的。
- StringBuffer是线程安全的,可以在多线程环境下安全使用。
- StringBuilder不是线程安全的,因此在多线程环境下可能会有并发访问问题。
3. 性能:
- 由于String是不可变的,每次对String对象进行操作都会创建一个新的对象,可能会导致性能下降。
- StringBuffer是线程安全的,但由于它的方法都是同步的,可能会带来额外的性能开销。
- StringBuilder是非线程安全的,但在单线程环境下比StringBuffer更高效,因为不需要进行同步操作。
综上所述,选择使用String、StringBuffer或StringBuilder取决于具体的应用场景和需求:如果需要频繁修改字符串,并且在多线程环境中操作,建议使用StringBuffer;在单线程环境中,推荐使用StringBuilder以获得更好的性能;如果字符串不需要被修改,可以使用String类来确保不可变性。
关于这个多线程环境的理解:
1. 多线程环境:
在一个服务器程序中,有多个客户端同时向服务器发送请求并进行处理。这时服务器程序中可能会存在多个线程同时访问某个共享的数据结构,比如字符串。如果使用非线程安全的类来处理这个共享的字符串,可能会导致并发访问问题,造成数据混乱或者程序崩溃。在这种情况下,应该使用线程安全的类比如StringBuffer来保证数据操作的安全性。
2. 单线程环境:
在一个简单的命令行程序中,只有一个程序在顺序执行各个步骤,没有多个线程同时访问同一个数据结构的情况。在这种情况下,使用非线程安全的类比如StringBuilder来处理字符串是没有问题的,因为不涉及多线程并发访问的情况,不会出现数据安全性问题。
总的来说,多线程环境是指多个线程同时并发执行程序,可能访问共享数据;而单线程环境是指只有一个线程在执行程序,不涉及多线程并发访问。根据实际的应用场景和需求,选择合适的字符串处理类来确保程序的正常运行和数据安全。
字符串中常见的库函数:
String类的常用方法:
1:public char charAt(int index):返回一个字符
2:equals方法,去比较字符串是否相同(在做题的时候,有一道关于对空格的比较就要用到equals)
3:toCharArray方法:将一个字符串变成字符数组。
4:转化为:StringBuilder:
直接调用StringBuilder的构造方法:StringBuilder str = new StringBuilder(s)
5:删掉字符串两端的所有空格(中间的空格不会删除):trim(); s.trim()
6:将字符串按照规定字符分割,返回的类型是一个字符串数组:s.spilt("");
String[] ans = s.trim().split(" ");
按照空格分割这个字符串,返回一个字符数组。
7:substring(int start, int end):返回一个新的String,其中包含当前包含在此序列中的字符的子序列
(substring遵循左闭右开的原则)就是下标为end的那个字符不会被返回。
StringBuilder类的常用方法:
1:反转字符串:
StringBuilder str = new StringBuilder(s);
str.reverse();
2:变成字符串:str.toString();
3:str.append(string / char)
4:substring(int start, int end):返回一个新的String,其中包含当前包含在此序列中的字符的子序列
(substring遵循左闭右开的原则)就是下标为end的那个字符不会被返回。
5:delete(int start, int end):删除此序列的子字符串中的字符
注意:很多字符串函数都是左闭右开,我现在是记得蛮清楚的,不知道过一段时间会不会忘记,所以我先在这标记一下。
字符串的常见题目:
leetcode 344. 反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s
的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
public void reverseString(char[] s) {
int len = s.length;
char temp;
for(int i=0;i<len/2;++i){
temp = s[i];
s[i] = s[len-1-i];
s[len-1-i] = temp;
}
}
这道题目其实没什么好说的,就是实现一个reverse函数。
leetcode:541. 反转字符串 II
给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。
如果剩余字符少于 k 个,则将剩余字符全部反转。
如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。
public String reverseStr(String s, int k) {
char[] str = s.toCharArray();
int i,j;
for(i=0;i<s.length();i+=(2*k)){
if((i+k)<=s.length()){
reverser(str,i,(i+k)-1);
continue;
}
reverser(str,i,s.length()-1);
}
return new String(str);
}
public void reverser(char[] s,int left,int right){
char temp;
for(;left<right;++left,right--){
temp = s[left];
s[left] = s[right];
s[right] = temp;
}
}
分析:分析:这道题目没有考到什么算法,单纯是一道模拟题,就是考察一个代码的掌握能力。
注意点:
1:对于这个字符串的遍历,我们可以根据题目要求:i+=(2*k);2k2k的进行遍历。这是一个小技巧,就可以省去很多对于下标的处理
2:说到底,这道题目还是要反转字符串:java的reverse函数只能反转整个字符串,所有,我们要反转指定长度的字符串,我们需要重写一个reverse函数
,同时也要注意这个函数的下标,是[ ]还是[ )还是( ]。
3:就是对于最后的字符串的处理,比如,说有abcdefg七个字符,k=2,那这样处理到最后三个字符的时候,就得特殊处理了
卡码网KamaCoder55. 右旋字符串(第八期模拟笔试)
字符串的右旋转操作是把字符串尾部的若干个字符转移到字符串的前面。给定一个字符串 s 和一个正整数 k,请编写一个函数,将字符串中的后面 k 个字符移到字符串的前面,实现字符串的右旋转操作。
例如,对于输入字符串 "abcdefg" 和整数 2,函数应该将其转换为 "fgabcde"。、
第一种方法:
第一种方法是我自己一开始想到的方法:就是申请一个新的字符串的空间,如何按要求把题目给的字符串逆序添加进来就行
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int k = sc.nextInt();
String str = sc.next();
solutionofrightspin so = new solutionofrightspin();
System.out.println(so.rightspin(str,k));
}
}
class solutionofrightspin{
public String rightspin(String s,int k){
StringBuilder res = new StringBuilder();
int count = k;
for(int i=s.length()-k;i<s.length();i++){
res.append(s.charAt(i));
}
for(int i=0;i<s.length()-k;i++){
res.append(s.charAt(i));
}
return res.toString();
}
}
第二种方法
分为三步:
先将整个字符串旋转一遍,再将前k个旋转一边,再将后k~len-1旋转一边
比如abcdefg k=2
第一步:gfedcba
第二步:fgedcba
第三步:fgabcde
这样就能得到最后的想要的字符串了:
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = Integer.parseInt(in.nextLine());
String s = in.nextLine();
int len = s.length(); //获取字符串长度
char[] chars = s.toCharArray();
reverseString(chars, 0, len - 1); //反转整个字符串
reverseString(chars, 0, n - 1); //反转前一段字符串,此时的字符串首尾尾是0,n - 1
reverseString(chars, n, len - 1); //反转后一段字符串,此时的字符串首尾尾是n,len - 1
System.out.println(chars);
}
public static void reverseString(char[] ch, int start, int end) {
//异或法反转字符串,参照题目 344.反转字符串的解释
while (start < end) {
ch[start] ^= ch[end];
ch[end] ^= ch[start];
ch[start] ^= ch[end];
start++;
end--;
}
}
leetcode151. 反转字符串中的单词
给你一个字符串 s
,请你反转字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s
中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
注意:输入字符串 s
中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
第一种解法:
申请一个新的stringbuilder,用java的库函数trim删掉前后的空格和spilt再按空格进行分组,分成字符串数组。
然后倒序加入新的申请的stringbuilder
public String reverseWords(String s) {
StringBuilder res = new StringBuilder();
String[] ans = s.trim().split(" ");
for(int i=ans.length-1;i>=0;i--){
if(!ans[i].equals("")){
res.append(ans[i]+" ");
}
}
return res.toString().trim();
}
这种思路就是利用了库函数。
第二种解法:
用双指针
1:先删除字符串两端的空格
2:搞两个指针:i,j;
一开始都指向字符串的最后面。
然后i开始往前跑,i去找什么呢,i去找空格,i找到空格之后,此时i和j之间的位置,就是一个单词,把这个单词添加到ans中
然后i再跳过空格,等i再次遍历到一个字母的时候,j就跟上i,再继续之前的过程。
举个例子:i am a boy,i和j一开始都指向y然后,i往前跑,指向b前面一个空格,然后就把boy加入ans中,i再跑,跑到a,这个时候j跟上i都指向
a,i再往前走,发现a前面也是一个空格,然后就把a也放到ans中。
public String reverseWords(String s) {
s = s.trim(); // 删除首尾空格
int j = s.length() - 1, i = j;
StringBuilder res = new StringBuilder();
while (i >= 0) {
while (i >= 0 && s.charAt(i) != ' ') i--; // 搜索首个空格
res.append(s.substring(i + 1, j + 1) + " "); // 添加单词
while (i >= 0 && s.charAt(i) == ' ') i--; // 跳过单词间空格
j = i; // j 指向下个单词的尾字符
}
return res.toString().trim(); // 转化为字符串并返回
}
下面再分享几道比较难的字符串模拟题:
leetcode2232.向表达式添加括号后的最小结果
给你一个下标从 0 开始的字符串 expression
,格式为 "<num1>+<num2>"
,其中 <num1>
和 <num2>
表示正整数。
请你向 expression
中添加一对括号,使得在添加之后, expression
仍然是一个有效的数学表达式,并且计算后可以得到 最小 可能值。左括号 必须 添加在 '+'
的左侧,而右括号必须添加在 '+'
的右侧。
返回添加一对括号后形成的表达式 expression
,且满足 expression
计算得到 最小 可能值。如果存在多个答案都能产生相同结果,返回任意一个答案。
生成的输入满足:expression
的原始值和添加满足要求的任一对括号之后 expression
的值,都符合 32-bit 带符号整数范围
输入:expression = "247+38"
输出:"2(47+38)"
解释:表达式计算得到 2 * (47 + 38) = 2 * 85 = 170 。
注意 "2(4)7+38" 不是有效的结果,因为右括号必须添加在 '+' 的右侧。
可以证明 170 是最小可能值。
思路:
整体思路其实不难,就是把这个字符串按照 +号分成两个部分,左边和右边
我们根据括号的位置又可以把这两段字符串分成四个部分
- left1:左括号左边
- left2:左括号右边
- right1:右括号左边
- right2:右括号右边
最后我们要求的值就是 left1 * (left2+right1)* right2
然后我们开始枚举,因为这个括号左边和右边都得有一个。
所以我们可以固定左边的括号一开始在最左边,这种清空,我们可以把left1赋值为1,对right2同理
找到最小值后把答案拼起来就行。
public String minimizeResult(String expression) {
String left = "";
String right = "";
int i,j,flag=0;
for(i=0;i<expression.length();++i){
if(expression.charAt(i)=='+'){
flag = i;
left = expression.substring(0,i);
right = expression.substring(i+1,expression.length());
break;
}
}
String ans = new String();
int t = Integer.MAX_VALUE;
int temp = 0;
for(i=0;i<left.length();++i){
for(j=right.length()-1;j>=0;j--){
int left1 = (i==0?1:Integer.parseInt(left.substring(0,i)));
int left2 = Integer.parseInt(left.substring(i));
int right1 = Integer.parseInt(right.substring(0,j+1));
int right2 = (j==right.length()-1?1:Integer.parseInt(right.substring(j+1)));
temp = left1*(left2+right1)*right2;
if(temp<=t){
t = temp;
ans = expression.substring(0,i)+"(" + expression.substring(i,flag+j+2)+")"+expression.substring(flag+j+2);
}
}
}
return ans;
}
leetcode1324. 竖直打印单词
给你一个字符串 s
。请你按照单词在 s
中的出现顺序将它们全部竖直返回。
单词应该以字符串列表的形式返回,必要时用空格补位,但输出尾部的空格需要删除(不允许尾随空格)。
每个单词只能放在一列上,每一列中也只能有一个单词。
输入:s = "TO BE OR NOT TO BE" 输出:["TBONTB","OEROOE"," T"] 解释:题目允许使用空格补位,但不允许输出末尾出现空格。 "TBONTB" "OEROOE" " T"
这也是一个模拟题
思路:
我们就是先对这个字符串进行分组,按照空格来分组即可,
接着我们再找到这个字符串数组中最长的单词长度,这也做为我们二维数组的维度
然后我们按照题目要求,竖着放。
最后,我们遍历数组,取出每个字符拼到最后的字符串集合中即可。
class Solution {
public List<String> printVertically(String s) {
String[] strs = s.split(" ");
int i,j;
int max = -1;
for(i=0;i< strs.length;++i){
if(strs[i].length()>max){
max = strs[i].length();
}
}
char[][] matrix = new char[max][strs.length];
for(i=0;i<strs.length;++i){
for(j=0;j<strs[i].length();++j){
matrix[j][i] = strs[i].charAt(j);
}
}
List<String> ans = new ArrayList<>();
for(i=0;i<matrix.length;++i){
StringBuilder temp = new StringBuilder();
int index = 0;
for(j=0;j<matrix[i].length;++j){
if(matrix[i][j]=='\u0000'){
temp.append(" ");
}else{
index = j;
temp.append(matrix[i][j]);
}
}
ans.add(temp.substring(0,index+1).toString());
}
return ans;
}
}
leetcode2075:解码斜向换位密码:
这题的题干说实话看不怎么懂,直接上一个例子:
输入:encodedText = "iveo eed l te olc", rows = 4 输出:"i love leetcode" 解释:上图标识用于编码 originalText 的矩阵。 蓝色箭头展示如何从 encodedText 找到 originalText 。
思路:
我们先开一个二维数组用于记录,维度分别使:rows 和 encodedText.length()/rows
接着我们将这个encodedText填入到这个二维数组中
最后,我们按照规则取出来就行
这里有一个注意点,就是我取出来的时候,是把所有符合条件的字符都取出来了,包括这张图右上角的三个空格。所以,我们最后处理的时候,我们需要删掉空格。
class Solution {
public String decodeCiphertext(String encodedText, int rows) {
//1:先开一个二维数组
int cols = encodedText.length()/rows;
char[][] dp = new char[rows][cols];
//2:将encodedText依次填入这个字符数组
int index = 0;
int i,j;
for(i=0;i<rows;++i) {
for (j = 0; j < cols; ++j) {
dp[i][j] = encodedText.charAt(index++);
}
}
//3:根据规则将dp数组中的字符加入到ans中
StringBuilder ans = new StringBuilder();
int count = 0;
while(count<cols){
for(i=0,j=count;i<rows&&j<cols;++i,++j){
ans.append(dp[i][j]);
}
count++;
}
i = ans.length()-1;
while(i>=0&&ans.charAt(i)==' '){
ans.deleteCharAt(i--);
}
return ans.toString();
}
}
KMP:
给你两个字符串 haystack
和 needle
,请你在 haystack
字符串中找出 needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle
不是 haystack
的一部分,则返回 -1
输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
这种就是字符串的经典问题了:给你一个主串和一个字串,返回字串在主串中的下标,如果没有,返回-1;
这也是经典的KMP算法问题
介绍第一种方法:BF算法:暴力搜索:
public int strStr(String haystack, String needle) {
int i=0,j=0;
int flag = -1;
while(i<haystack.length()&&j<needle.length()){
if(haystack.charAt(i)==needle.charAt(j)){
i++;j++;
}
else {
i=i-j+1;j=0;
}
}
return flag = j==needle.length()?(i-needle.length()):-1;
}
举个例子很简单明白:
比如主串是aabaabaaf
字串是aabaaf
i,j
先开始匹配 前几个都相同,当匹配到f的时候,发现f和b不同,就要开始回溯,这个时候i就要回溯到之前开始匹配字母的下一个字母,一开始匹配的时候是a(第一个a),回溯就会回溯到第二个a,然后继续往下匹配
这里一个注意的点就是这个
i=i-j+1;j=0;这一行代码,这一行代码主要就是用来找到开始匹配字母的下一个字母。
第二种算法:KMP算法,也是字符串算法里面最重要的算法:
为什么会有KMP算法呢:仔细看上面这个代码的回溯位置:会发现每次回溯都要回溯到开始匹配字母的下一个字母,这样的时间复杂度是O(m*n)
如果我们能知道回溯到哪里是最好的,那就可以节约很多时间。
介绍KMP算法之前,得先介绍两个概念:前缀和后缀
//前缀:包含首字母不包含尾字母的所有字串
//后缀,包含尾字母不包含首字母的所有字串
//最长相等前后缀
根据上面,我们需要一个next数组来保存这个回退的位置:next数组的核心就是保存回退的这个位置(最长相等前后缀的长度)
当你发生了字符不相等的情况(也就是发生冲突)的时候,我们需要会退的位置:前一位next数组的值
请记住这个关键的东西,前一个next数组的值,不关我们求next数组也要用,当我们在遍历主串的时候,我们也要用
我们先假设我们已经知道了next数组的值,我们来遍历一下主串:
还是刚才的例子:
比如主串是a a b a a b a a f
字串是a a b a a f
当f和b发生冲突的时候,我们回退的位置,根据next数组里面的值,我们是回退到j = 2的位置(next[j-1])
注意,这里的i是不动的,只有j在动(这跟我们后面分析时间复杂度有关系)
for (int i = 0, j = 0; i < n; i++) {
while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
j = pi[j - 1];
}
if (haystack.charAt(i) == needle.charAt(j)) {
j++;
}
if (j == m) {
return i - m + 1;
}
}
这里还有一个注意点:就是当这个两个字符不等的时候,我们用的循环时while,就是要在这个模式串中找到,与这个字符相等的位置。
当然这个j>0,如果主串中有一个字符在模式串中没有的话,我们就跳过。