本文正在参加「金石计划 . 瓜分6万现金大奖」
前言:最近自己也开始系统的刷面试题了,算法是过不去的坎,希望自己能够坚持下去✊,同行的朋友们共勉。
题一:验证回文串
如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串
字母和数字都属于字母数字字符
给你一个字符串 s,如果它是 回文串 ,返回 true ;否则,返回 false。
示例 1:
输入: s = "A man, a plan, a canal: Panama"
输出:true
解释:"amanaplanacanalpanama" 是回文串。
示例 2:
输入:s = "race a car"
输出:false
解释:"raceacar" 不是回文串。
示例 3:
输入:s = " "
输出:true
解释:在移除非字母数字字符之后,s 是一个空字符串 "" 。
由于空字符串正着反着读都一样,所以是回文串。
解题思路:双指针、Reversed
回文串:字符对称出现; 基于这个特性,我们可以有两种做法:
- 使用双指针判断前后相等。
- 颠倒字符串判断是否相等;
解法一:双指针
双指针 i(➡️), j = n-i-1(⬅️)
判断两个指针指向的字符是否相同;
如果相同,则同时移动位置,i++、j--
;如果不相同,则立即跳出循环;
在判断相同之前,还需要处理字符的合法性,如果哪一个指针的字符不合法,则需要继续移位,直到找到合法的字符为止。
时间复杂度:O(n)
空间复杂度:O(1)
代码
func isPalindrome(_ s: String) -> Bool {
let sList = Array(s)
var res = true
guard sList.count > 0 else { return res }
var leftIndex = 0
var rightIndex = sList.count - 1
while leftIndex < rightIndex {
while leftIndex < rightIndex {
let lChar = sList[leftIndex]
if String(lChar).isAlphanumeric { break }
leftIndex += 1
}
while leftIndex < rightIndex {
let rChar = sList[rightIndex]
if String(rChar).isAlphanumeric { break }
rightIndex -= 1
}
if sList[leftIndex].lowercased() != sList[rightIndex].lowercased() {
res = false
return res
}
leftIndex += 1
rightIndex -= 1
}
return res
}
解法二:筛选 + 双指针
基于上一种解法,我们知道在判断字符串之前还需要验证字符串。如果字符串中的其他的字符(除字母和数字以外)比较多的话,那么解法一会在验证字符串上耗时较长。
所以解法二采用先筛选合法的字符串,再使用双指针遍历。
时间复杂度:O(n) n取决于s的长度
空间复杂度:O(n)
代码
func isPalindrome(_ s: String) -> Bool {
var res = true
guard s.count > 0 else { return res }
var list = [String]()
for char in s {
if String(char).isAlphanumeric {
list.append(String(char))
}
}
var leftIndex = 0
var rightIndex = list.count - 1
while leftIndex < rightIndex {
if list[leftIndex].lowercased() != list[rightIndex].lowercased() {
res = false
return res
}
leftIndex += 1
rightIndex -= 1
}
return res
}
解法三:Reversed
有种用魔法打败魔法的感觉。
代码
func isPalindrome3(_ s: String) -> Bool {
var res = true
guard s.count > 0 else { return res }
let str = s.matchAlphanumeric()
let tempStr = String(str.reversed())
if str != tempStr {
res = false
}
return res
}
题二:字符串转换整数 (atoi)
请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。
函数 myAtoi(string s) 的算法如下:
读入字符串并丢弃无用的前导空格
检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。
读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。
将前面步骤读入的这些数字转换为整数(即,"123" -> 123, "0032" -> 32)。如果没有读入数字,则整数为 0 。必要时更改符号(从步骤 2 开始)。
如果整数数超过 32 位有符号整数范围 [−2^31^, 2^31^ − 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −2^31^ 的整数应该被固定为 −2^31^ ,大于 2^31^ − 1 的整数应该被固定为 2^31^ − 1 。
返回整数作为最终结果。
示例 1:
输入:s = "42"
输出:42
示例 2:
输入:s = " -42"
输出:-42
示例 3:
输入:s = "4193 with words"
输出:4193
解题思路:数学计算(考虑溢出)
字符串转换整数需要考虑到以下三点:
- 先过滤空格
- 识别 +、—
- 过滤>9,<0的符号,以及数字溢出的处理;
代码
func myAtoi(_ s: String) -> Int {
guard s.count > 0 else { return 0 }
var res: Int = 0
var index = 0
var sign = 1
let array = Array(s)
/// 第一步
while index < s.count, array[index] == " " {
index+=1
}
if index == array.count { return Int(res) }
/// 第二步
if array[index] == "+" {
index+=1
} else if array[index] == "-"{
sign = 0
index+=1
}
/// 第三步
while index < s.count {
let char = array[index]
if char > "9" || char < "0" {
break
}
let charNum = (char.asciiValue ?? 0) - (Character("0").asciiValue ?? 0)
if res > Int32.max / 10 || (res == Int32.max / 10 && charNum > Int32.max % 10) {
res = Int(Int32.max)
break
}
if res < Int32.min / 10 || (res == Int32.min / 10 && charNum > -(Int32.min % 10)) {
res = Int(Int32.min)
break
}
res = res * 10 + Int(charNum) * (sign == 1 ? 1 : -1)
index+=1
}
return Int(res)
}
题三:实现 strStr()
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
示例 1:
输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:
输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
解题思路: 匹配字符串
匹配字符串通俗来讲就是找到字串,而找字串的话,最简单的做法就是从两个字符串开头开始匹配,如果两个字符相同,则继续往后匹配,如果两个字符不匹配,则回溯继续找。
这种解法中,因为有回溯的过程,所以有一定的时间上的耗时。虽然可以简单的实现,但不是最优解。
如果 h[i] == n[j], i++, j++;
如果 h[i] != n[j], i=i-j+1, j=0;
代码
func strStr(_ haystack: String, _ needle: String) -> Int {
let h = Array(haystack)
let n = Array(needle)
var res = -1
var i = 0, j = 0
while i < h.count && j < n.count {
if h[i] == n[j] {
i+=1; j+=1
} else {
i = i-j+1; j=0
}
if j == n.count {
res = i-j
break
}
}
return res
}
题四:外观数列
给定一个正整数 n ,输出外观数列的第 n 项。
「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。
你可以将其视作是由递归公式定义的数字字符串序列:
countAndSay(1) = "1"
countAndSay(n) 是对 countAndSay(n-1) 的描述,然后转换成另一个数字字符串。
前五项如下:
1. 1
2. 11
3. 21
4. 1211
5. 111221
第一项是数字 1
描述前一项,这个数是 1 即 “ 一 个 1 ”,记作 "11"
描述前一项,这个数是 11 即 “ 二 个 1 ” ,记作 "21"
描述前一项,这个数是 21 即 “ 一 个 2 + 一 个 1 ” ,记作 "1211"
描述前一项,这个数是 1211 即 “ 一 个 1 + 一 个 2 + 二 个 1 ” ,记作 "111221"
解题思路:递归法
当后一项需要依赖前一项的结果时,可以使用递归法。思路其实就是先压栈,然后再依次出栈。
掌握了这个思路,将所有项都依次进栈,f(n)、f(n-1)、f(n-2)、...、f(1)
,描述完前一项之后,依次出栈f(1)、f(2)、f(3)、...、f(n)
。
入栈:
f(n) -> f(n-1) -> f(n-2) -> ... -> f(1)
描述:
前项描述 + \(count)\(item)
出栈:
f(n) <- f(n-1) <- f(n-2) <- ... <- f(1)
代码
func countAndSay(_ n: Int) -> String {
if n == 1 { return "1" }
let s = countAndSay(n-1)
var count = 1
var string = ""
var item = s[s.index(s.startIndex, offsetBy: 0)]
for i in 1..<s.count {
let startIndex = s.index(s.startIndex, offsetBy: i)
let char: Character = s[startIndex]
if char == item {
count += 1
} else {
string += ("\(count)\(item)")
count = 1
item = char
}
}
return string + "\(count)\(item)"
}
题五:最长公共前缀
编写一个函数来查找字符串数组中的最长公共前缀。 如果不存在公共前缀,返回空字符串 ""。
示例 1:
输入: strs = ["flower","flow","flight"]
输出: "fl"
示例 2:
输入: strs = ["dog","racecar","car"]
输出: ""
解释: 输入不存在公共前缀。
解题思路:横向查找、纵向查找
解法一:横向查找
横向查找是指依次遍历每个字符串,更新最长公共前缀。
代码
func longestCommonPrefix(_ strs: [String]) -> String {
guard strs.count > 0 else { return "" }
var str = strs[0]
for i in 1..<strs.count {
str = commonPrefix(str,strs[i])
if str.count == 0 { break }
}
return str
}
func commonPrefix(_ str1: String, _ str2: String) -> String {
let length = min(str1.count, str2.count)
var index = 0
while index < length {
let char1 = str1[str1.index(str1.startIndex, offsetBy: index)]
let char2 = str2[str2.index(str2.startIndex, offsetBy: index)]
if char1 != char2 {
break
}
index+=1
}
return String(str1[str1.startIndex..<str1.index(str1.startIndex, offsetBy: index)])
}
解法二:纵向查找
纵向查找是指从前往后遍历所有字符串的每一列,比较相同列上的字符是否相同,如果相同则继续对下一列进行比较,如果不相同则当前列不再属于公共前缀,当前列之前的部分为最长公共前缀。
代码
func longestCommonPrefix(_ strs: [String]) -> String {
guard strs.count > 0 else { return "" }
var str = strs[0]
for i in 0..<str.count {
var char = str[str.index(str.startIndex, offsetBy: i)]
for j in 1..<strs.count {
let subStrs = Array(strs[j])
if i == subStrs.count || subStrs[i] != char {
return String(str[str.startIndex..<str.index(str.startIndex, offsetBy: i)])
}
}
}
return strs[0]
}
小结
字符串篇的初级算法看看你们都学会了吗?总结下来,匹配字符串、反转字符串、转换数字、找子串、寻找前后缀等。针对不同的题型,我们可以使用不同的方法,频率出现较多的是双指针、哈希表。
大多时候,可以使用哈希表判断重复字符,使用双指针减少时间复杂带来的耗时。对于依赖前项的这种数列,可以使用递归法。
字符串:双指针、哈希表、集合、递归、横向查找、纵向查找