Go语言实现字符串搜索算法Boyer-Moore

 更新时间:2023年11月15日 17:01:41   作者:YanChen11  
Boyer-Moore 算法是一种非常高效的字符串搜索算法,被广泛的应用于多种字符串搜索场景,下面我们就来学习一下如何利用Go语言实现这一字符串搜索算法吧

Boyer-Moore 算法是一种非常高效的字符串搜索算法,被广泛的应用于多种字符串搜索场景:

  • 文本搜索(尤其是大篇幅的文本搜索)
  • 文档编辑器以及 IDE 工具中的字符串搜索/替换
  • 编译器中搜索源代码中的关键字/符号
  • 文件系统中搜索给定的文件名

通常,在字符串搜索过程中,我们期望尽快得到结果(提高算法运行速度,降低时间复杂度),为此我们需要对字符串(文本以及搜索的子串)进行一些预处理。对于大文本,该预处理过程会消耗可观的内存空间,而如果在搜索过程中该预处理过程需要反复进行,则又会消耗相当的 CPU 资源。

Boyer-Moore 算法只需要对被搜索的子串进行一次预处理,通过在本次预处理过程中收集到的信息来提升算法的运行效率,使得算法的时间复杂度尽量趋近于 O(n)。Boyer-Moore 算法的一个显著特征是匹配过程从子串的末尾开始向前,如果遇到不匹配的字符,则根据在预处理过程中收集到的信息进行跳跃/移动,避免逐个字符进行比较。而具体的跳跃/移动规则,则同时使用两种策略实现:

  • 坏字符启发
  • 好后缀启发

⒈ 坏字符启发

在将子串中的字符与文本中的字符进行比较时,如果文本中的字符与子串中的字符不匹配,我们称文本中的当前字符为坏字符。对于坏字符,通常有两种处理方式:

坏字符在子串中的其他位置存在

当坏字符在子串中存在时,如果坏字符在子串中的位置位于当前索引位置之前,则将子串中的坏字符与文本中的坏字符对齐,然后重新从子串的最后开始向前与文本中的字符进行匹配;如果坏字符在子串中的位置位于当前索引位置之后,则将子串向后移动一个字符的位置,然后重新开始从子串的最后开始向前与文本中的字符进行匹配。

如上图所示,从后往前将子串中的字符与文本中的字符进行比较,子串中的字符 C 与文本中的 R 不匹配。但文本中的字符 R 在子串中的其他位置存在,此时将子串中的 R 与文本中的 R 对齐,然后重新从后往前进行匹配。

在对子串进行预处理时,由于子串中 R 出现了两次,所以实际预处理完成之后收集到的信息中记录的是位于 C 之后的 R 的位置信息。所以,实际在代码中,遇到这种情况,子串只能向后移动一个字符的位置。

坏字符在子串中不存在

如果坏字符在子串中不存在,那么移动子串,将子串的第一个字符与文本中坏字符之后的字符对齐,然后重新从后往前将子串中的字符与文本中的字符进行匹配。

如上图所示,坏字符 G 与子串中的字符 Q 不匹配,并且坏字符 G 在子串中并不存在,此时将子串移动到与坏字符 G 后的字符对齐,然后重新从后往前开始匹配。

package main

import (
	"fmt"
)

func main() {
	text := "AYRRQMGRPCRQ"
	subStr := "RPCRQ"
	fmt.Printf("text = %+v\n", text)
	fmt.Printf("subStr = %+v\n", subStr)

	// 构建子字符串中各个字符及相应的索引的映射
	m := make(map[byte]int, len(subStr))
	for i := 0; i < len(subStr); i ++ {
		m[subStr[i]] = i
	}
	fmt.Printf("m = %+v\n", m)

	shiftLength := 0
	subIndex := len(subStr) - 1
	for shiftLength <= len(text) - len(subStr) {
		// 每次比较都从子字符串的末尾开始向前,逐个字符进行比较
		for subIndex >= 0 && text[shiftLength + subIndex] == subStr[subIndex] {
			subIndex --
		}

		if subIndex == -1 {
			// 子字符串在文本中出现,跳过文本中的子字符串继续向后查找
			fmt.Printf("subStr found in text at index %+v\n", shiftLength)
			shiftLength += len(subStr)
		} else if v, ok := m[text[shiftLength + subIndex]]; ok {
			// 文本中的字符与子字符串中相应位置的字符不匹配,但该字符在子字符串中存在
			if subIndex > v {
				// 如果该字符在子字符串中的位置在当前索引位置之前,则将二者位置对齐,然后重新查找
				shiftLength += subIndex - v
			} else {
				// 文本中的索引位置向前移动一个字符(考虑子字符串中同一个字符重复出现的情况)
				shiftLength += 1
			}
		} else {
			// 文本中的字符在子字符串中不存在
			shiftLength += subIndex
		}

		subIndex = len(subStr) - 1
	}
}

⒉ 好后缀启发

在将子串按照从后往前的顺序与文本中的字符进行比较时,遇到不匹配的字符时,子串末尾已经匹配得字符即为好后缀。此时根据好后缀在子串中其他位置是否存在,重新确定子串在文本中开始匹配的位置。

好后缀或好后缀的后缀在子串中的其他位置存在

如上图所示,从后往前将子串中的字符与文本进行匹配,文本中字符 Y 与子串中相应位置的字符 Q 不匹配,此时出现好后缀 CRQ。虽然好后缀 CRQ 在子串中只出现了一次,但好后缀的后缀 RQ 却在子串的头部再次出现,此时将子串头部的 RQ 与文本中的 RQ 对齐,然后重新从后往前匹配。

好后缀在子串中不存在

如上图所示,子串中不存在好后缀,此时只要将文本中的起始匹配位置向后移动一个字符,然后重新从后往前匹配。

起始匹配位置移动之后,子串中出现了好后缀 RQ,并且 RQ 在子串的头部也存在。此时,将子串头部的 RQ 与文本中的 RQ 对齐,确定新的匹配开始位置,重新匹配。

好后缀启发的关键在于对子串的预处理。

在对子串进行预处理的过程中,需要确定子串中单个字符以及多个连续字符出现的频次以及相应的位置。当匹配过程中出现好后缀时,会根据预处理过程中收集到的信息确定文本中新的开始匹配的位置。

package main

import (
	"fmt"
)

func main() {
	text := "AYCRQMGRQCRQ"
	subStr := "RQCRQ"
	fmt.Printf("text = %+v\n", text)
	fmt.Printf("subStr = %+v\n", subStr)

	shifts := make([]int, len(subStr) + 1)
	borderPosition := make([]int, len(subStr) + 1)

	// 确定搜索字符串中各个子串的边界
	i, j := len(subStr), len(subStr) + 1
	borderPosition[i] = j

	for i > 0 {
		for j >= 0 && j <= len(subStr) && subStr[i - 1] != subStr[j - 1] {
			if shifts[j] == 0 {
				shifts[j] = j - i
			}

			j = borderPosition[j]
		}

		i --
		j --
		borderPosition[i] = j
	}

	fmt.Printf("shifts = %+v\n", shifts)
	fmt.Printf("borderPosition = %+v\n", borderPosition)

	// 确定搜索字符串中各个字符与文本中的字符不匹配时应该移动的距离
	j = borderPosition[0]
	for i := 0; i <= len(subStr); i ++ {
		if shifts[i] == 0 {
			shifts[i] = j
		}

		if i == j {
			j = borderPosition[j]
		}
	}

	fmt.Printf("shifts = %+v\n", shifts)
	fmt.Printf("borderPosition = %+v\n", borderPosition)

	// 在文本中搜索字符串
	shiftLength := 0
	for shiftLength <= len(text) - len(subStr) {
		j = len(subStr) - 1
		for j >= 0 && subStr[j] == text[shiftLength + j] {
			j --
		}

		if j == -1 {
			fmt.Printf("subStr find in text at position %+v\n", shiftLength)
			shiftLength += shifts[0]
		} else {
			shiftLength += shifts[j + 1]
		}
	}
}

以上就是Go语言实现字符串搜索算法Boyer-Moore的详细内容,更多关于Go字符串搜索算法的资料请关注脚本之家其它相关文章!

相关文章

  • Golang项目在github创建release后自动生成二进制文件的方法

    Golang项目在github创建release后自动生成二进制文件的方法

    这篇文章主要介绍了Golang项目在github创建release后如何自动生成二进制文件,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-03-03
  • Golang 中 omitempty的作用

    Golang 中 omitempty的作用

    这篇文章主要介绍了Golang 中 omitempty的作用,文章围绕主题展开详细的内容介绍,具有一定的参考一下,需要的小伙伴可以参考一下
    2022-07-07
  • golang使用go mod导入本地包和第三方包的方式

    golang使用go mod导入本地包和第三方包的方式

    这篇文章主要介绍了golang使用go mod导入本地包和第三方包的方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • 使用Go语言简单模拟Python的生成器

    使用Go语言简单模拟Python的生成器

    这篇文章主要介绍了使用Go语言简单模拟Python的生成器,Python的generator是非常酷的功能,用Go实现的代码也较为简洁,需要的朋友可以参考下
    2015-08-08
  • Go语言中的流程控制结构和函数详解

    Go语言中的流程控制结构和函数详解

    这篇文章主要介绍了Go语言中的流程控制结构和函数详解,本文详细讲解了if、goto、for、switch等控制语句,同时对函数相关知识做了讲解,需要的朋友可以参考下
    2014-10-10
  • Go异步任务解决方案之Asynq库详解

    Go异步任务解决方案之Asynq库详解

    需要在Go应用程序中异步处理任务? Asynq,简单高效的任务队列实现,下面这篇文章主要给大家介绍了关于Go异步任务解决方案之Asynq库的相关资料,需要的朋友可以参考下
    2023-02-02
  • 使用Viper处理Go应用程序的配置方法

    使用Viper处理Go应用程序的配置方法

    Viper是一个应用程序配置解决方案,用于Go应用程序,它支持JSON、TOML、YAML、HCL、envfile和Java properties配置文件格式,这篇文章主要介绍了使用Viper处理Go应用程序的配置,需要的朋友可以参考下
    2023-09-09
  • Go语言基础语法之结构体及方法详解

    Go语言基础语法之结构体及方法详解

    结构体类型可以用来保存不同类型的数据,也可以通过方法的形式来声明它的行为。本文将介绍go语言中的结构体和方法,以及“继承”的实现方法
    2021-09-09
  • Go结构体指针引发的值传递思考分析

    Go结构体指针引发的值传递思考分析

    这篇文章主要为大家介绍了Go结构体指针引发的值传递思考分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12
  • Golang 实现分片读取http超大文件流和并发控制

    Golang 实现分片读取http超大文件流和并发控制

    这篇文章主要介绍了Golang 实现分片读取http超大文件流和并发控制,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12

最新评论