完成了我们的专题1——树 部分的刷题练习之后 我们(终于!)来到了第二部分:数组与字符串
经历了专题1大量题目洗礼过后的我们 应该变得对刷题更有自信了!(没看过专题1的内容不妨回去看一眼~)那么 我们继续!
【1】先对
LeetBook
中的内容进行一个学习
数组
是数据结构中的基本模块之一。- 因为
字符串
是由字符数组形成的,所以二者是相似的。
——我们面临的大多数面试问题都属于这个范畴。要刷的题目涉及如下专题——
(1)专题1 理解数组的基本概念
及其操作方式
;
(2)专题2 理解二维数组
的基本概念,熟悉二维数组的使用;
(3)专题3 了解字符串
的概念以及字符串所具有的不同特性;理解字符串匹配中的KMP 算法
;
(4)专题4 能够运用双指针
解决实际问题。
【2】在解决
LeetBook
中推荐的题目的过程中 我发现了非常有趣的两个专题——
(1)专题5 前缀和思想求解子数组&子串问题
(2)专题6 二分查找数组中元素问题
字符串就是 由字符构成的数组 嘛~
维基百科:字符串是由零个或多个字符组成的有限序列。一般记为 s = a1a2…an。它是编程语言中表示文本的数据类型。
本章将深入研究字符串 可以让我们掌握——
- 熟悉字符串中的
基本操作
,尤其是在数组中没有的独特操作;- 理解不同
比较
函数之间的区别;- 理解字符串
是否可变
以及导致连接过程中出现的问题;- 能够解决与字符串相关的基本问题,如排序、子串、字符串匹配等。
下面来做个字符串的简介嗷~先掌握了概念再去刷题!
参考LeetBook
中的内容 字符串简介
字符串和数组其实是有很多相似之处的~
最基础的 使用 字符串名数组名[下标索引]得到元素(数组元素/字符)
然而,存在这样一个问题:
我们可以用 “==” 来比较两个字符串吗?
这取决于下面这个问题的答案:
我们使用的语言是否支持运算符重载?
给出两个例子 对cpp
java
中结果进行比较
#include
int main() {
string s1 = "Hello World";
cout << "s1 is \"Hello World\"" << endl;
string s2 = s1;
cout << "s2 is initialized by s1" << endl;
string s3(s1);
cout << "s3 is initialized by s1" << endl;
// compare by '=='
cout << "Compared by '==':" << endl;
cout << "s1 and \"Hello World\": " << (s1 == "Hello World") << endl;
cout << "s1 and s2: " << (s1 == s2) << endl;
cout << "s1 and s3: " << (s1 == s3) << endl;
// compare by 'compare'
cout << "Compared by 'compare':" << endl;
cout << "s1 and \"Hello World\": " << !s1.compare("Hello World") << endl;
cout << "s1 and s2: " << !s1.compare(s2) << endl;
cout << "s1 and s3: " << !s1.compare(s3) << endl;
}
// "static void main" must be defined in a public class.
public class Main {
public static void main(String[] args) {
// initialize
String s1 = "Hello World";
System.out.println("s1 is \"" + s1 + "\"");
String s2 = s1;
System.out.println("s2 is another reference to s1.");
String s3 = new String(s1);
System.out.println("s3 is a copy of s1.");
// compare using '=='
System.out.println("Compared by '==':");
// true since string is immutable and s1 is binded to "Hello World"
System.out.println("s1 and \"Hello World\": " + (s1 == "Hello World"));
// true since s1 and s2 is the reference of the same object
System.out.println("s1 and s2: " + (s1 == s2));
// false since s3 is refered to another new object
System.out.println("s1 and s3: " + (s1 == s3));
// compare using 'equals'
System.out.println("Compared by 'equals':");
System.out.println("s1 and \"Hello World\": " + s1.equals("Hello World"));
System.out.println("s1 and s2: " + s1.equals(s2));
System.out.println("s1 and s3: " + s1.equals(s3));
// compare using 'compareTo'
System.out.println("Compared by 'compareTo':");
System.out.println("s1 and \"Hello World\": " + (s1.compareTo("Hello World") == 0));
System.out.println("s1 and s2: " + (s1.compareTo(s2) == 0));
System.out.println("s1 and s3: " + (s1.compareTo(s3) == 0));
}
}
对于不同的编程语言中,字符串可能是可变的,也可能是不可变的。
不可变意味着一旦字符串被初始化,你就无法改变它的内容。
#include
int main() {
string s1 = "Hello World";
s1[5] = ',';//这里可以随意修改字符串的内容
cout << s1 << endl;
}
Java的话 一旦初始化了字符串就“落子无悔”了
// "static void main" must be defined in a public class.
public class Main {
public static void main(String[] args) {
String s1 = "Hello World";
s1[5] = ',';//这里是修改不了滴~
System.out.println(s1);
}
}
显然,不可变字符串无法被修改。
哪怕我们只是想修改其中的一个字符,也必须创建一个新的字符串!
以 Java 为例,来看一个在 for
循环中重复进行字符串连接的例子:
// "static void main" must be defined in a public class.
public class Main {
public static void main(String[] args) {
String s = "";
int n = 10000;
for (int i = 0; i < n; i++) {
s += "hello";
}
}
}
对于 Java来说,由于字符串是不可变的,因此在连接时首先为新字符串分配足够的空间,复制旧字符串中的内容并附加到新字符串。
时间复杂度直接爆炸
5+5×2+5×3+…+5×n=5×(1+2+3+…+n)=5×n×(n+1)/2
即 O(N2)
针对 Java 中出现的此问题,我们提供了以下解决方案:
- 如果你确实希望你的字符串是可变的,则可以使用
toCharArray
将其转换为字符数组。—— 这个有点高级哈哈哈- 如果你经常必须连接字符串,最好使用一些其他的数据结构,如
StringBuilder
.toCharArray()
String str = "leetcode!";
char[] charArray = str.toCharArray();
//charArray这个char[]类型的数组存储了 拆分开来的字符串str
//简单来说 .toCharArray()方法将字符串str转换为字符数组charArray
for(char i : charArray){
System.out.println(i);
}
for循环 + charAt()
这个方法用的比较多
因为我们可以更加直观地在遍历中对字符串中的每一个字符进行操作~
String str = "leetcode!";
for(int i = 0; i < str.length(); i++){
System.out.println(str.charAt(i));
//charAt()方法用于返回指定索引出的字符!
}
for循环 + substring(i, i+1)
String str = "leetcode!";
for(int i = 0; i < str.length(); i++){
System.out.println(str.substring(i, i + 1));
//substring() 方法返回字符串的子字符串。
//两个参数分别为起始索引(包括)&结束索引(不包括)
}
剑指 Offer 05. 替换空格
请实现一个函数,把字符串 s
中的每个空格替换成"%20"。
示例 1:
输入:s = "We are happy."
输出:"We%20are%20happy."
相当简单的一道题~
新建一个空的数组
遍历一遍 (遵循 遇到空格就输入%20 & 遇到字符串就直接输出 的原则)将元素迁移到空数组中
最后将数组转换为字符串 res.toString()
class Solution {
public String replaceSpace(String s) {
return s.replace(" ", "%20");
}
}
面试题05. 替换空格 (字符串修改,清晰图解)
暂时没有考虑这个方法 大家可以去瞅一眼这种更高级的解法
另外 回头看剑指原书的时候 可以对上面的解法进行参考
class Solution {
public String replaceSpace(String s) {
StringBuilder res = new StringBuilder();
for(int i = 0; i < s.length(); i++){
char c = s.charAt(i);
if(c == ' ') res.append("%20");
else res.append(c);
}
return res.toString();//将数组转换为字符串 输出
}
}
剑指 Offer 50. 第一个只出现一次的字符
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
示例:
s = "abaccdeff"
返回 "b"
s = ""
返回 " "
真的是相当暴力的一个解法了。
使用for循环+charAt()
的方法外层循环遍历字符串s中的所有字符
然后再在内层对每个字符进行比对 如果发现有相同的字符 直接跳出内存循环 遍历到下一个字符
for(int j = 0; j < i; j++){
if(c == s.charAt(j)){//如果遇到了重复的字符
count = 1;
break;
}
}//跳过当前字符 这方法好笨啊!!!
for(int j = i + 1; j < s.length(); j++){
if(c == s.charAt(j)){//如果遇到了重复的字符
count = 1;
break;
}
}
class Solution {
public char firstUniqChar(String s) {
if(s.length() == 0){
return ' ';
}
else{
for(int i = 0; i < s.length(); i++){
char c = s.charAt(i);
int count = 0;
for(int j = 0; j < i; j++){
if(c == s.charAt(j)){//如果遇到了重复的字符
count = 1;
break;
}
}
for(int j = i + 1; j < s.length(); j++){
if(c == s.charAt(j)){//如果遇到了重复的字符
count = 1;
break;
}
}
//从遍历到的字符串开始 把后面的都遍历了一遍
if(count == 0){
return c;
}
}//结束字符串不为空情况的循环 遍历了一遍字符串
}
return ' ';
}
}
14. 最长公共前缀
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""
。
示例 1:
输入:strs = ["flower","flow","flight"]
输出:"fl"
示例 2:
输入:strs = ["dog","racecar","car"]
输出:""
解释:输入不存在公共前缀。
参考官方题解
用LCP(S1…Sn)表示字符串S1…Sn的最长公共前缀
可以得到以下结论
LCP(S1…Sn) = LCP( LCP (LCP(S1,S2) ,S3) ,…Sn)
基于该结论,可以得到一种查找字符串数组中的最长公共前缀的简单方法。
依次遍历字符串数组中的每个字符串,对于每个遍历到的字符串,更新最长公共前缀,当遍历完所有的字符串以后,即可得到字符串数组中的最长公共前缀。
这里可以看一下官方题解中的解题视频[方法1]
说得超级清楚的!
如果在尚未遍历完所有的字符串时,最长公共前缀已经是空串,则最长公共前缀一定是空串,因此不需要继续遍历剩下的字符串,直接返回空串即可。
class Solution {
public String longestCommonPrefix(String[] strs) {
if (strs == null || strs.length == 0) {
return "";
}
String prefix = strs[0];
int count = strs.length;
for (int i = 1; i < count; i++) {
prefix = longestCommonPrefix(prefix, strs[i]);
if (prefix.length() == 0) {
break;
}
}
return prefix;
}
public String longestCommonPrefix(String str1, String str2) {
int length = Math.min(str1.length(), str2.length());
int index = 0;
while (index < length && str1.charAt(index) == str2.charAt(index)) {
index++;
}
return str1.substring(0, index);
}
}
20. 有效的括号
tag:
栈
字符串
之前在字节校园每日一题中做过这道题~
说明是道高频的面试题咯~
用到了HashMap 哈希表
做题之前要把这个先做一个简单的了解
有效字符串需满足:
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
示例 4:
输入:s = "([)]"
输出:false
示例 5:
输入:s = "{[]}"
输出:true
使用栈这个数据结构来解决
参考官方题解
我们遍历给定的字符串 ss。当我们遇到一个左括号时,我们会期望在后续的遍历中,有一个相同类型的右括号将其闭合。由于后遇到的左括号要先闭合,因此我们可以将这个左括号放入栈顶。
当我们遇到一个右括号时,我们需要将一个相同类型的左括号闭合。此时,我们可以取出栈顶的左括号并判断它们是否是相同类型的括号。
如果是相同的类型 则对应的左括号出栈——
如果不是相同的类型,或者栈中并没有左括号,那么字符串 s 无效,返回False。为了快速判断括号的类型,我们可以使用哈希表存储每一种括号。哈希表的键为右括号,值为相同类型的左括号。
在遍历结束后,如果栈中没有左括号,说明我们将字符串 s 中的所有左括号闭合,返回True 否则返回False
下面是一个完整过程的例子 动图参考 逐步分析,图解栈 感谢大佬的分享!
注意到有效字符串的长度一定为偶数,因此如果字符串的长度为奇数,我们可以直接返回 False,省去后续的遍历判断过程。
我们使用哈希表,在字符串遍历的过程中 快速地检查遍历到的那个右括号与栈顶的左括号是否是一个类型的——
if(pairs.containsKey(ch)){//判断Map集合对象中是否包含指定的键名
if(stack.isEmpty() || stack.peek() != pairs.get(ch)) {
return false;
//1.栈为空 说明右括号在左括号前面 false
//2.栈顶元素(实际的左括号)如果不等于当前ch(右括号 键) 对应的值(理论上正确的左括号) false
}
stack.pop();//如果上面的都符合了 说明找到了匹配的左右括号 栈中的左括号可以出栈了!
另外 参考了这篇文章 Java Map.containsKey()
方法:判断Map集合对象中是否包含指定的键名
了解了下 containsKey()
这个方法 包括还有 containsValue()
分别 用来判断Map集合对象中是否包含指定的键名&值名(;还有创建Map对象使用 map.put(']', '[')
的方法
class Solution {
public boolean isValid(String s) {
int n = s.length();
if(n % 2 == 1){
return false;
}
Map<Character, Character> pairs = new HashMap<Character, Character>(){{
//为了快速判断括号的类型(即为判断右括号是否与左括号是一类)建立哈希表 用来存储每一种括号
//哈希表的键——右括号 哈希表的值——左括号
put(')', '(');
put(']', '[');
put('}', '{');
}};//至此 哈希表建立完成
Deque<Character> stack = new LinkedList<Character>();//建立栈 用于存放左括号
for(int i = 0; i < n; i++){
char ch = s.charAt(i);//取出字符串中的字符ch
if(pairs.containsKey(ch)){//判断Map集合对象中是否包含指定的键名
if(stack.isEmpty() || stack.peek() != pairs.get(ch)) {
return false;
//1.栈为空 说明右括号在左括号前面 false
//2.栈顶元素(左括号)如果不等于当前ch(右括号 也就是哈希表中的键)对应的value(左括号 也就是哈希表中的值) false
}
stack.pop();//如果上面的都符合了 说明找到了匹配的左右括号 栈中的左括号可以出栈了!
}
else{
stack.push(ch);
}
}
return stack.isEmpty();//如果栈是空的(返回true) 那么说明所有括号的是匹配(有效)的
}
}