[Day 8] 字串 & leetcode 相關練習

今天要來講 Kotlin 在字串上的處理,字串處理算是平日處理商業邏輯很常碰到的,所以不可掉以輕心。

substring

subtring 跟以往 Java 使用的方式其實差不多

  • 這裡首先用 indexOf 取得 的 index
  • until 這個 ranges function,讓我們寫起來更有語意上的感覺,也就是要取 index 0 ~ 3 的子字串,也就是 "Tim"
  • 下面兩種寫法就跟 Java 內的方式一樣
val str = "Tim's string"
val index = str.indexOf('\'')

println(str.substring(0 until index)) // Tim
println(str.substring(0)) // Tim's string
println(str.substring(0, index)) // Tim

split

這裡使用了 split 依據逗號把字串拆成一個 array

每個變數在賦予 array 內的內容

val names = "tim, jerry, anna"

val data = names.split(',') // ["tim", "jerry", "anna"]
val tim = data[0]   // "tim"
val jerry = data[1] // "jerry"
val anna = data[2]  //"anna"

解構 (destructuring)

之前的程式也可以寫成這樣

解構函式在寫 JS 的時候我覺得真的是一個很方便的東西

val (tim, jerry, anna) = names.split(',')

如此就可以依順序直接把值塞入這三個變數!

這裡介紹的是 Collections 的解構(像是 List, Map..等等),物件的解構之後會再講到 Class 再講

在 Kotlin 裡面,如果不需要的參數會用底線來表示

像是這個例子不需要 data[1] 的資料,所以用底線表示不賦予值

val (tim2, _, anna2) = names.split(',')

Nullable 的時候

以往寫程式的時候,最常使用的就是要判斷字串是不是空或 null,還是含有空白的字串

Kotlin 很貼心,幾乎都內建好方法了,搭配之前提到的 ?. 更方便,終於可以不用依靠一些 lib 了...

這裏的變數 s 是一個可以 null 的字串

  • isEmpty() 指得是真的長度為 0 的空字串
  • 因為 isEmpty(), isNotEmpty() 並沒有判斷 null,所以需要 ? Safe Calls 符號來判斷,如果是 null 就不會呼叫下去
  • isNullOrEmpty() 就會包含 null 的判斷所以不需要 ? Safe Calls 符號來判斷 null
val s: String? = "kotlin"
// isEmpty, isNotEmpty 要有 ?, isNullOrEmpty 不需要
println(s?.isEmpty()) // length == 0
println(s?.isNotEmpty())
println(s.isNullOrEmpty())

isBlank() 指得是有空白的字串或長度為 0 的空字串

其他就跟上面解釋的差不多意思

// isBlank, isNotBlank 要有 ?, isNullOrBlank 不需要
println(s?.isBlank()) //  length == 0 或有空白
println(s?.isNotBlank())
println(s.isNullOrBlank())

字串相接

要連接字串很簡單,一樣用 +號或 plus 來串接字串

println(s + " very good") 
println(s.plus(" very good"))
println(s.plus(" very good").plus("haha"))
println("$s very good")

StringBuilder

上面是串接很少字串的狀況

但如果今天是多字串的串接,例如跑了一個 for 迴圈要把字串串起來

跟在 Java 一樣,String 是 immutable 的, 串接字串用 + 或 plus 都會造成 String 新的記憶體不斷的被創造出來。

在這種狀況下還是使用 StringBuilder 比較好

val builder = StringBuilder()
builder.append("Aloha")
        .append(" ")
        .append("Tim")

String template 會幫你優化成 StringBuilder

值得注意的是,如果是使用 String template 串接的話,去看 byte code 的話,Kotlin 也會自動幫我們優化成 StringBuilder!

val a = "Aloha"
val b = " "
val c = "Tim"
val d = "$a$b$c"
// 會幫我們轉成 (new StringBuilder()).append(a).append(b).append(c).toString();

但在這個例子,byte code 裡面還是是使用 +號,因為這裡只是兩個字串的相接,並不需要 StringBuilder

println("$s very good")
// var19 = s + " very good"; 只有一次的字串串接, 還是會只用 +

最後,來讓我們練習個 leetcode 吧!

Leetcode 344. Reverse String

https://leetcode.com/problems/reverse-string/

難度: EASY

這題很簡單其實就是要我們做字串反轉,但當然不能呼叫 reversed() api

解法跟 Java 類似可以用 two points 的方式,字串頭尾互相對調,最後就會是反轉的字串了

fun reverseString(s: CharArray): Unit {
    var left = 0
    var right = s.size - 1
    while (left < right) {
        val temp = s[left]
        s[left] = s[right]
        s[right] = temp
        left++
        right--
    }
}

或是跑 for 從 0 到 index 的一半位置,對 index 和 (lastIndex - index) 做對調,這樣也會是頭尾對調

fun reverseString(s: CharArray): Unit {
    if (s.isEmpty()) return
    for (i in 0..s.lastIndex/2) {
        val temp = s[i]
        s[i] = s[s.lastIndex - i]
        s[s.lastIndex - i] = temp
    }
}

這題很簡單,讓我們來看下一題

Leetcode 468. Validate IP Address

https://leetcode.com/problems/validate-ip-address/

難度: MEDIUM

這題算是很生活化的 leetcode 題目吧!

https://ithelp.ithome.com.tw/upload/images/20200917/20129902ojinpMI4JJ.png

https://ithelp.ithome.com.tw/upload/images/20200917/201299024yYe6ueUMZ.png

總結上面的描述和例子,可以歸納出

題目描述了蠻多 ipv4 和 ipv6 的限制狀況

  • ipv4 由 4 組數字用 . 號隔開:
    • 內容都是數字會介於 0~255,,所以要排除有文字的例外狀況
  • ipv6 由 8 組文數字用 : 隔開:
    • 內容是 16進位字串
    • 所以內容會有
      • 數字 和 字母 'a' 到 'f' 和大寫的 字母 'A' 到 'F' (因為 16進位是到 F )
      • 可以 0 開頭
      • 長度可以 1~4

以下是解法,首先在主函數用 . 符號或 : 符號來判斷要檢查的是 ipv4 還是 ipv6,其他狀況都回傳 "Neither",這裡會用 try catch 包起來是因為,檢查的內容我有使用到字串轉數字,不合法的輸入時會直接丟出 exception,所以統一處理掉。

fun validIPAddress(IP: String): String {
    return try {
        when {
            IP.count { it == '.' } == 3 -> validateIPv4(IP)
            IP.count { it == ':' } == 7 -> validateIPv6(IP)
            else -> "Neither"
        }
    } catch (e: Exception) {
        "Neither"
    }
}

檢查 ipv4,依照之前分析的,如果不是數字,it.toInt() 回直接拋出 exception

i !in 0..255 檢查數字是否落在 0~255

it != i.toString() 把原本的 ip 和 ip 轉成數字再轉成字串做比較,看是不是轉換後還是一樣,用來處理一些特殊的 test case,像是 01.01.01.01

// it != i.toString() 處理一些特殊的 test case, 像是 01.01.01.01
private fun validateIPv4(IP: String): String {
    IP.split('.').forEach {
        val i = it.toInt()
        if (i !in 0..255 || it != i.toString()) {
            return "Neither"
        }
    }
    return "IPv4"
}

ipv6 的部分就判斷

內容的文數字有沒有超過長度4 it.length > 4

然後還有用正規表示式來判斷內容是否合法 Regex("^[0-9A-Fa-f]+\$")

private fun validateIPv6(IP: String): String {
    IP.split(':').forEach {
        if (it.length > 4 || !it.contains(Regex("^[0-9A-Fa-f]+\$"))) {
            return "Neither"
        }
    }
    return "IPv6"
}

以下是完整程式碼

class Solution {
    fun validIPAddress(IP: String): String {
        return try {
            when {
                IP.count { it == '.' } == 3 -> validateIPv4(IP)
                IP.count { it == ':' } == 7 -> validateIPv6(IP)
                else -> "Neither"
            }
        } catch (e: Exception) {
            "Neither"
        }
    }

    // it != i.toString() 處理一些特殊的 test case, 像是 01.01.01.01
    private fun validateIPv4(IP: String): String {
        IP.split('.').forEach {
            val i = it.toInt()
            if (i !in 0..255 || it != i.toString()) {
                return "Neither"
            }
        }
        return "IPv4"
    }

    private fun validateIPv6(IP: String): String {
        IP.split(':').forEach {
            if (it.length > 4 || !it.contains(Regex("^[0-9A-Fa-f]+\$"))) {
                return "Neither"
            }
        }
        return "IPv6"
    }
}

有做了些練習是不是覺得比較踏實呢

今天字串的部分就講到這!謝謝大家我們明天見!

今日練習的程式在這: 請點我