算法基础06 KMP算法
KMP算法
[TOC]
题目
LeetCode的第28题,实现strStr()
,实现字符串的匹配,实际上就是实现一个JS的indexOf
函数
1 | const haystack = 'hello', needle = 'll'; |
这道题目可以用暴力算法,逐个移动起始位置来进行匹配实现,如下:
1 | var strStr = function (haystack, needle) { |
暴力算法的时间复杂度是${O((n - m) * m)}$
,想要降低算法复杂度,就需要使用KMP算法来进行字符串匹配
KMP算法思路
KMP是字符串匹配的最常用的算法之一,它的作用就是:如何快速在原字符串中找到匹配字符串,KMP的算法复杂度是${O(m + n)}$
。它之所以能够降低复杂度,是因为能够在『非完全匹配』的过程中提取到有效信息进行复用,减少重复匹配的消耗,提高效率
阮一峰的博客讲解的很好,但是思路与下面宫水三叶的思考角度有一点不一样,下面主要记录的是宫水三叶的思考角度
匹配过程
假设原字符串是abeababeabf
,匹配串为abeabf
:
(1)先逐个进行比较,找到第一个匹配的位置,继续向下进行,直到出现第一个不匹配的位置(红标):
(2)如果是暴力匹配的话,会将原字符串的指针移动到本次『发起点』的下一个位置b
,匹配串的指针移动到其实位置,继续尝试匹配,如果发现对不上,原串的指针会继续向后移动
这样虽然可行,但是效率很差,因为需要把搜索位置移动到已经比较过的位置,再重新比较一遍。而实际上,当a
与f
不匹配时,已经知道前面的刘个字符是abeab
。
KMP的思想就是设法利用这个已知信息,不要把搜索位置移回已经比较过的位置,而是继续向后移动,这样就提高了效率
如果利用已知信息呢?需要针对搜索出,计算出一张『部分匹配表』(Partial Match Table),我们在代码称作next
数组,数组中的每个位置的就是匹配串对应下标应该跳转的目标位置
// next数组
[0, 0, 0, 1, 2, 0]
这张表的计算后面会再介绍,这里先用起来
(3)利用这张表,发现匹配串的f
不匹配了,它对应的next[j - 1]
值是2
,所以将匹配串的指针移动到2
,继续进行匹配:
(4)跳转到下一个匹配位置后,尝试匹配,返现a
和e
对不上,继续从next
数组中寻找跳转位置,next[j - 1]
值是0
,这样只能回到字符串的起始位置进行匹配
(5)继续匹配,匹配成功
KMP的原理
KMP之所以更快,主要是因为原串指针不会进行回溯,这意味着,随着匹配过程的进行,原串指针不断右移,本质上是在不断地否决一些不可能的方案
当我们的原串指针从i
位置移动到j
位置,不仅仅代表着原串下标范围为[i, j)
的字符与匹配串匹配或者不匹配,更是在否决那些以原串下标范围为[i, j)
的『匹配发起点』的子集
构建next
数组
上面的过程中,忽略了一个步骤,就是next
数组的构建。这个构建过程的时间复杂度是${O(m)}$
(1)假设有匹配串aaabbab
,定义两个指针,i
(红色)和j
(黑色),j
从0
位置开始,i
从1
位置开始
(2)如果p[i] === p[j]
,那么next[i] = j + 1
,i
和j
同时向后移动,这个过程一直循环进行
(3)移动到此时,p[i] !== p[j]
,那么将j
指向前一位置的next
数组对应的值,即j = next[j - 1]
,循环这个过程,直到p[i] === p[j]
或者j === 0
(4)如果j === 0
,p[i]
和p[j]
仍不相等,那么next[i] = 0
,i
向后移动,j
不动
(5)继续上述过程,此时j === 0
,p[i]
和p[j]
仍不相等,所以next[i]
一直为0
,i
指针后移,直到b
再次出现
(6)继续,p[i]
和p[j]
相等,那么next[i] = 1
,i
和j
同时前进
(7)i
移动到了最后一位,此时p[i]
和p[j]
不等,给j
赋值next[j - 1]
,即j
为0
,时p[i]
和p[j]
仍不等,所以next[i]
为0
,循环结束
至此循环完成
这个部分的代码实现:
1 | function genNext(pattern) { |
代码实现
1 | function strStr(haystack, needle) { |
总结
个人感觉KMP算法还是挺复杂的,但是它的应用还是比较广泛的,理解之后,记住它,相当于大脑里有了一个时间复杂度为${O(n)}$
的API可以使用,用来返回匹配串在原串的位置
所以,理解一下,背住
练习
这道题目也可以用KMP算法解决