介绍
字符串匹配算法是一种用于在一个文本字符串(称为主串)中查找一个目标字符串(称为模式串)出现位置的算法。其目标是确定模式串是否存在于主串中,并找到所有匹配的位置。
常见的字符串匹配算法有以下几种:
-
暴力匹配算法:也称为朴素匹配算法,从文本的每个位置开始,逐个字符与模式进行比较,直到找到匹配或遍历完整个文本。时间复杂度为O(n*m),其中n为文本长度,m为模式长度。
-
KMP算法(Knuth-Morris-Pratt算法):通过预处理模式串,构建一个部分匹配表(Partial Match Table),利用该表在匹配过程中跳过尽可能多的字符,从而提高效率。时间复杂度为O(n+m),其中n为文本长度,m为模式长度。
-
Boyer-Moore算法:利用坏字符规则和好后缀规则,从模式串末尾开始与文本进行比较,根据不匹配的字符在模式中的位置,跳过尽可能多的字符,从而提高效率。时间复杂度为O(n/m),其中n为文本长度,m为模式长度。
-
Rabin-Karp算法:利用哈希函数对文本中的子串和模式串进行哈希计算,比较哈希值判断是否匹配,可以快速定位潜在的匹配位置。时间复杂度为O(n+m),其中n为文本长度,m为模式长度。
这些算法在不同的场景和需求下有不同的适用性和效率。选择合适的算法可以提高字符串匹配的效率和性能。
暴力匹配算法
以下是使用暴力匹配算法实现字符串匹配的简单 Swift 代码示例:
swift
func bruteForcePatternSearch(text: String, pattern: String) -> [Int] {
let n = text.count
let m = pattern.count
var indices: [Int] = [] // 保存匹配位置的数组
for i in 0...(n - m) {
var j = 0
while j < m {
let textIndex = text.index(text.startIndex, offsetBy: i + j)
let patternIndex = pattern.index(pattern.startIndex, offsetBy: j)
if text[textIndex] != pattern[patternIndex] {
break
}
j += 1
}
if j == m {
indices.append(i) // 完全匹配,将匹配位置添加到数组中
}
}
return indices
}
// 示例用法
let text = "Hello, Hello, Hello, World!"
let pattern = "Hello"
let matchedIndices = bruteForcePatternSearch(text: text, pattern: pattern)
print("匹配位置:\(matchedIndices)") // 输出:匹配位置:[0, 7, 14]
在上述代码中,bruteForcePatternSearch
函数接受两个参数:text
(文本字符串)和 pattern
(模式字符串)。它使用两个嵌套的循环进行暴力匹配,首先从 text
的每个位置开始,然后逐个字符与 pattern
进行比较。如果在某个位置开始的子串与 pattern
完全匹配,则将该位置添加到 indices
数组中。最后,返回包含所有匹配位置的 indices
数组。
注意,暴力匹配算法的时间复杂度为 O(n*m),其中 n 是文本字符串的长度,m 是模式字符串的长度。这种算法简单直观,但对于大型文本和模式,效率较低。在实际应用中,可以考虑使用更高效的字符串匹配算法。
Rabin-Karp算法
以下是使用Rabin-Karp算法实现字符串匹配的简单 Swift 代码示例:
swift
func rabinKarpPatternSearch(text: String, pattern: String) -> [Int] {
let n = text.count
let m = pattern.count
let prime = 101 // 选取一个较大的质数作为哈希函数的模数
let base = 26 // 字符集的基数,假设为小写字母
var indices: [Int] = [] // 保存匹配位置的数组
// 计算模式串的哈希值和第一个子串的哈希值
var patternHash = 0
var textHash = 0
var power = 1
for i in 0..<m {
let patternCharIndex = pattern.index(pattern.startIndex, offsetBy: i)
let textCharIndex = text.index(text.startIndex, offsetBy: i)
patternHash = (patternHash * base + Int(pattern[patternCharIndex].asciiValue!)) % prime
textHash = (textHash * base + Int(text[textCharIndex].asciiValue!)) % prime
if i != 0 {
power = (power * base) % prime
}
}
// 在文本中滑动窗口查找匹配
for i in 0...(n - m) {
if patternHash == textHash {
var j = 0
while j < m {
let textIndex = text.index(text.startIndex, offsetBy: i + j)
let patternIndex = pattern.index(pattern.startIndex, offsetBy: j)
if text[textIndex] != pattern[patternIndex] {
break
}
j += 1
}
if j == m {
indices.append(i) // 完全匹配,将匹配位置添加到数组中
}
}
// 计算下一个窗口的哈希值
if i < n - m {
let prevTextCharIndex = text.index(text.startIndex, offsetBy: i)
let nextTextCharIndex = text.index(text.startIndex, offsetBy: i + m)
// 滑动窗口,通过减去最高位的字符的哈希值,然后加上下一个字符的哈希值
textHash = (base * (textHash - Int(text[prevTextCharIndex].asciiValue!) * power) + Int(text[nextTextCharIndex].asciiValue!)) % prime
// 处理负数情况
if textHash < 0 {
textHash += prime
}
}
}
return indices
}
// 示例用法
let text = "Hello, Hello, Hello, World!"
let pattern = "Hello"
let matchedIndices = rabinKarpPatternSearch(text: text, pattern: pattern)
print("匹配位置:\(matchedIndices)") // 输出:匹配位置:[0, 7, 14]
在上述代码中,rabinKarpPatternSearch
函数接受两个参数:text
(文本字符串)和 pattern
(模式字符串)。它使用Rabin-Karp算法来进行字符串匹配。首先,计算模式串和第一个子串的哈希值,并与文本中的子串的哈希值进行比较。如果哈希值匹配,进一步检查字符内容以确保完全匹配。如果匹配成功,则将该位置添加到 indices
数组中。
然后,通过滑动窗口的方式在文本中移动,并计算下一个窗口的哈希值。这样可以避免对每个子串进行字符比较,而是通过哈希值进行快速比较。如果哈希值匹配,则进一步检查字符内容以确保完全匹配。
需要注意的是,为了避免哈希冲突,需要选择一个适当的质数作为哈希函数的模数,并且需要处理负数的情况。
Rabin-Karp算法在平均情况下的时间复杂度为O(n+m),其中n是文本字符串的长度,m是模式字符串的长度。当哈希函数选择得当且哈希冲突较少时,该算法的性能较好。然而,在最坏情况下,时间复杂度可能达到O(n*m),因为需要在每个可能的位置进行字符比较。因此,在实际应用中,仍然需要综合考虑算法的特点和应用场景来选择适当的字符串匹配算法。
KMP算法(Knuth-Morris-Pratt算法)
KMP算法(Knuth-Morris-Pratt算法)是一种用于字符串匹配的高效算法,它可以在线性时间复杂度内完成匹配操作。KMP算法通过利用已经匹配过的部分信息,避免不必要的字符比较,从而提高匹配的效率。
KMP算法的实现步骤如下:
-
构建部分匹配表(Partial Match Table,PMT):PMT是一个用于存储模式串的前缀和后缀的最长公共部分长度的表。它可以帮助我们在匹配过程中跳过已经匹配过的部分,从而避免重复比较。通过遍历模式串,计算每个位置的最长公共部分长度,将结果存储在PMT表中。
-
匹配过程:在匹配过程中,我们维护两个指针,一个指向文本串(
text
)的当前位置,另一个指向模式串(pattern
)的当前位置。从文本串的开头开始,逐个字符进行比较。- 如果当前字符匹配成功,即
text[i] == pattern[j]
,则继续比较下一个字符,即i += 1
,j += 1
。 - 如果当前字符匹配失败,即
text[i] != pattern[j]
,则根据PMT表中的信息,将模式串的指针j
移动到PMT[j-1]的位置,并继续比较当前字符和移动后的模式串字符,即i
不变,j = PMT[j-1]
。
重复上述步骤,直到匹配成功(找到了完全匹配的子串)或者文本串遍历完毕(未找到匹配的子串)。
- 如果当前字符匹配成功,即
-
返回匹配结果:如果匹配成功,可以记录匹配的起始位置;如果匹配失败,说明文本串中不存在模式串。
KMP算法的关键是构建部分匹配表(PMT),它的计算复杂度为O(m),其中m是模式串的长度。在匹配过程中,每次匹配失败时的指针移动操作通过PMT表中的信息来确定,而不是直接回溯,因此匹配过程的时间复杂度为O(n),其中n是文本串的长度。
以下是KMP算法的Swift代码实现:
swift
func buildPMT(pattern: String) -> [Int] {
let m = pattern.count
var PMT: [Int] = [0] // PMT表,初始值为0
var i = 0 // PMT表的索引
for j in 1..<m {
let patternIndex = pattern.index(pattern.startIndex, offsetBy: j)
while i > 0 && pattern[patternIndex] != pattern[pattern.index(pattern.startIndex, offsetBy: i)] {
i = PMT[i - 1]
}
if pattern[patternIndex] == pattern[pattern.index(pattern.startIndex, offsetBy: i)] {
i += 1
}
PMT.append(i)
}
return PMT
}
func kmpPatternSearch(text: String, pattern: String) -> [Int] {
let n = text.count
let m = pattern.count
var indices: [Int] = [] // 匹配位置的数组
let PMT = buildPMT(pattern: pattern)
var i = 0 // 文本串的索引
var j = 0 // 模式串的索引
while i < n {
let textIndex = text.index(text.startIndex, offsetBy: i)
let patternIndex = pattern.index(pattern.startIndex, offsetBy: j)
if text[textIndex] == pattern[patternIndex] {
i += 1
j += 1
if j == m {
indices.append(i - m) // 记录匹配的起始位置
j = PMT[j - 1]
}
} else {
if j != 0 {
j = PMT[j - 1]
} else {
i += 1
}
}
}
return indices
}
使用以上代码可以实现KMP算法的字符串匹配功能。可以通过调用kmpPatternSearch
函数来进行匹配,它接受一个文本串和一个模式串作为参数,并返回匹配的起始位置的数组。
Boyer-Moore算法
Boyer-Moore算法是一种用于字符串匹配的高效算法,它利用了两个启发式规则:坏字符规则(Bad Character Rule)和好后缀规则(Good Suffix Rule)。通过这两个规则的应用,Boyer-Moore算法可以在平均情况下实现线性时间复杂度。
Boyer-Moore算法的实现步骤如下:
-
构建坏字符规则(Bad Character Rule):
- 对于模式串中的每个字符,记录它在模式串中最右出现的位置。如果某个字符在模式串中多次出现,则记录最右出现的位置。
- 如果某个字符不在模式串中出现,则记录模式串的长度。
-
构建好后缀规则(Good Suffix Rule):
- 对于模式串的每个后缀,记录它在模式串中的另一个匹配的子串的起始位置。
- 如果某个后缀没有其他匹配的子串,则记录模式串的长度。
-
匹配过程:
- 从文本串的末尾开始,逐个字符地与模式串进行比较。
- 如果当前字符匹配成功,则向前移动一个字符继续比较,直到完成匹配。
- 如果当前字符匹配失败:
- 根据坏字符规则,将模式串向右滑动,使得模式串中的坏字符对齐到当前字符的位置。
- 根据好后缀规则,将模式串向右滑动,使得模式串中的好后缀对齐到当前字符的位置。
- 在滑动过程中,选择坏字符规则和好后缀规则中的滑动距离较大者。
-
返回匹配结果:
- 如果匹配成功,返回匹配的起始位置。
- 如果匹配失败,表示文本串中不存在模式串。
Boyer-Moore算法的关键在于利用坏字符规则和好后缀规则来选择合适的滑动距离,从而跳过尽可能多的无效比较,提高匹配效率。