【算法學堂】字符串基礎算法

原文地址:三豐的網絡日誌

內容摘要

字符串是程序中使用最廣的數據結構之一,本文以大量的示例和代碼講解了字符串的常見算法,包括大小寫轉換、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;
  }
}

總結

今天我們探討了字符串的定義,字符串的可變性以及常見的一些字符串的操作;字符串的基礎使用看起來簡單,其實是非常考驗大家編程功底的地方,怎樣把代碼寫的簡單高效,需要在平時工作中不斷的去發掘和練習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章