原文地址:三豐的網絡日誌
內容摘要
字符串是程序中使用最廣的數據結構之一,本文以大量的示例和代碼講解了字符串的常見算法,包括大小寫轉換、Atoi、寶石與石頭(leetcode第771題),希望對大家有所幫助。
如果您覺得有用,也歡迎將本文推薦給您的朋友。
什麼是字符串
字符串(string),是由零個或多個字符組成的有限序列,是編程中最重要的數據結構之一。
詳細定義請參見維基百科。
你的字符串是immutable嗎?
immutable意味着不可改變,是一個常量。比如光在真空中的速度就是一個宇宙級的常量,又比如數字"3",也是一個常量。
但是變量是可以改變指向的,比如name = “andy”,"andy"這個字符串常量是不可改變的,但是name變量可以改變指向,比如name = “jack”。
字符串在各語言中是否是immutable
- 在java, c#, javascript, python, go中,string是immutable的。
- 在ruby, php中,string是mutable。
- c語言本身並沒有string類型,而只有char *類型或char數組,我們可用使用char *來實現string,當然它是mutable的; c++中的string是mutable的,雖然可以添加const來限制其爲immutable,但是可以很容易的通過const_cast移除掉,因此c++ string中的immutability是比較弱的。
更詳細的分析請參見Are your strings immutable?
字符串常見算法
字符串算法最豐富的數據結構之一,凝聚着很多計算機科學家及工程師智慧的結晶,也是許多有意思的問題及解決方案,這裏先列舉幾個基礎的字符串算法,後續本專欄也會持續推出更多的字符串算法,比如字符串反轉、字符串匹配,字符串查詢,異位詞,迴文串等,敬請期待。
大小寫轉換
大小寫轉換是字符串中常見的操作之一,經常用於數據分析或協議轉換中,用來標準化各種輸入。這裏以Java爲例進行講解,其他語言原理類似。
解法1:庫函數
比如Java的String提供了toLowerCase函數,可以直接調用。
class ToLowerCase709 {
public String toLowerCase(String str) {
return str.toLowerCase();
}
}
解法2:AscII碼轉換
有時候,語言本身並沒有提供相應String大小寫轉換的功能,比如C語言,或在面試中,要求面試者自己實現相應功能的時候,這時我們就不得不自己實現了。
/**
* 大小寫字母的ASCII碼
* [a,z] = [97,122]
* [A,Z] = [65,90]
*/
public class ToLowerCase709 {
// 轉換爲小寫
public String toLowerCase(String str) {
// 參數合法性校驗
if (str == null) {
return null;
}
int index = 0;
StringBuilder result = new StringBuilder();
while (index < str.length()) {
if (str.charAt(index) >= 'A' && str.charAt(index) <= 'Z') {
result.append((char)(str.charAt(index) + ('a' - 'A')));
} else {
result.append(str.charAt(index));
}
index++;
}
return result.toString();
}
}
解法3:二進制
基本解法和解法2類似,在轉換的時候可以通過位運算來加速
大寫變小寫: ASCII碼 |= 32
/**
* 大小寫字母的ASCII碼
* [a,z] = [97,122]
* [A,Z] = [65,90]
*/
public class ToLowerCase709 {
// 轉換爲小寫
public String toLowerCase(String str) {
// 參數合法性校驗
if (str == null) {
return null;
}
int index = 0;
StringBuilder result = new StringBuilder();
while (index < str.length()) {
if (str.charAt(index) >= 'A' && str.charAt(index) <= 'Z') {
result.append((char)(str.charAt(index) | (char)32));
} else {
result.append(str.charAt(index));
}
index++;
}
return result.toString();
}
}
Atoi,字符串轉整形(ascii to integer)
Atoi也比較簡單,主要就是掃描字符串,每掃描一位,就相當於整形中乘了一個10,在掃描過程中注意空格及符號位的處理。由於string底層實現都是字符數組,因此每一步的越界檢查也需要特別小心。
public class MyAtoi8 {
// atoi, 轉換失敗返回0
public int myAtoi(String str) {
int total = 0;
int sign = 1;
int index = 0;
if (str == null || str.isEmpty()) {
return 0;
}
// left trim
while (index < str.length() && str.charAt(index) == ' ') {
index++;
}
// 符號位處理
if (index < str.length() && (str.charAt(index) == '-' || str.charAt(index) == '+')) {
sign = str.charAt(index) == '-' ? -1 : 1;
index++;
}
// 掃描字符串
while (index < str.length()) {
int d = str.charAt(index) - '0';
if (d < 0 || d > 9) {
break;
}
// 最大值處理
if (Integer.MAX_VALUE / 10 < total || Integer.MAX_VALUE / 10 == total
&& Integer.MAX_VALUE % 10 < d) {
return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
}
total = total * 10 + d;
index++;
}
return total * sign;
}
public static void main(String[] args) {
String s0 = "+1345";
MyAtoi8 so = new MyAtoi8();
System.out.println(so.myAtoi(s0));
String s1 = null;
System.out.println(so.myAtoi(s1));
String s2 = "-234289884f89fh4";
System.out.println(so.myAtoi(s2));
String s3= "3237598394989349893893948593953495435";
System.out.println(so.myAtoi(s3));
String s4 = "!!r3r3";
System.out.println(so.myAtoi(s4));
String s5 = " 123678";
System.out.println(so.myAtoi(s5));
String s6 = "";
System.out.println(so.myAtoi(s6));
String s7 = " ";
System.out.println(so.myAtoi(s7));
}
}
輸出:
1345
0
-234289884
2147483647
0
123678
0
0
寶石與石頭
接下來我們來看看一個比較有意思的題目:
給定字符串J 代表石頭中寶石的類型,和字符串 S代表你擁有的石頭。 S 中每個字符代表了一種你擁有的石頭的類型,你想知道你擁有的石頭中有多少是寶石。
J 中的字母不重複,J 和 S中的所有字符都是字母。字母區分大小寫,因此"a"和"A"是不同類型的石頭。
示例 1:
輸入: J = "aA", S = "aAAbbbb"
輸出: 3
示例 2:
輸入: J = "z", S = "ZZ"
輸出: 0
注意:
S 和 J 最多含有50個字母。
J 中的字符不重複。
題目來自leetcode第771題:
https://leetcode-cn.com/problems/jewels-and-stones/
解法分析
- 暴力法:可以遍歷S,然後針對S中的每個字母,去遍歷J,看看這個字母是不是在J中,如果是,則當前這個字母就代表一個寶石;這種解法的時間複雜度爲O(M * N)(假設J的長度是M,S的長度是N)。
- 使用hash表加速:暴力法每次都要遍歷J,可以把J中的字母存放在hash表中,這樣掃描S中的每個字母的時候,可以在O(1)的時間內返回其是否在J中;時間複雜度爲O(N),由於使用了額外的數據結構,空間複雜度爲O(M)。
- 使用字母表加速:由於J和S的所有字符都是字母,可以用一個字符數組把J中的字符存起來,並把J中有的字母相應的位置置1,這樣在掃描S的時候,也可以在O(1)的時間內進行判斷。
這種利用字母表的思想,在字符串的計算中非常常見,使用得好的話,經常可以省去不必要的遍歷操作,這也是空間換時間思維的一種典型應用。
// 字母表加速解法
public class NumJewelsInStones771 {
public int numJewelsInStones(String J, String S) {
int[] alphabet = new int[52];
for (char c : J.toCharArray()) {
if (c >= 'A' && c <= 'Z') {
alphabet[c - 'A'] = 1;
} else {
alphabet[c - 'a' + 26] = 1;
}
}
// 寶石個數
int count = 0;
for (char c : S.toCharArray()) {
if (c >= 'A' && c <= 'Z' && alphabet[c - 'A'] == 1) {
count++;
} else if (c >= 'a' && c <= 'z' && alphabet[c - 'a' + 26] == 1) {
count++;
}
}
return count;
}
}
總結
今天我們探討了字符串的定義,字符串的可變性以及常見的一些字符串的操作;字符串的基礎使用看起來簡單,其實是非常考驗大家編程功底的地方,怎樣把代碼寫的簡單高效,需要在平時工作中不斷的去發掘和練習。