也谈KMP算法

看毛片算法,念书时学过,几年下来也就只记得个名字了。前几天偶然遇到,顺道捡起来摸一摸,还是那手感…… ^_^

 

言归正传,这是个模式匹配算法(好吧,正传第一句居然是废话~)。谈它之前,先说说最简单直观的模式匹配算法——朴素匹配。

 

朴素匹配,就是不动脑子去匹配的算法:

 

源串S[0…n-1]  模式串P[0…m-1] 很显然,n > = m

 

源串和模式串分别有一个指针标记,

对指针指向的源串中每一个位置[0…n-m](n-m向后的不用考虑,剩余长度都不够m了),模式串都是从第一位开始比对,匹配则两个指针同时后移一位;

如果遇到某一位不匹配,源串中开始比较位置后移一位,模式串仍然从第一位开始继续匹配。

 

直至模式串扫描完毕表示匹配 或者源串扫描完毕,表示不匹配。

 

给一个Java实现

	public int StringMatch(String sourceStr, String patternStr) {
		char[] source = sourceStr.toCharArray();
		char[] pattern = patternStr.toCharArray();
		
		int idxSource, idxPattern;
		
		// for each i, check if source[i~...i+m-1] matches pattern[0~...m]
		for(int i = 0; i <= source.length - pattern.length; i++) {
			idxSource = i;
			idxPattern = 0;
			while(idxPattern < pattern.length
					&&source[idxSource] == pattern[idxPattern]) {
				idxSource++;
				idxPattern++;
			}
			
			if(idxPattern == pattern.length)
				return i;
				
		}
		return -1;
	}


简单分析下这个算法,由于外层循环是扫描源串S[0…n-m],内层循环最坏要扫描整个模式串P[m],故算法是O(nm)的。

 

朴素算法里,比如某趟从源串的i = 4,模式串j=0位置开始比对,i和j依次后移,到了i=7 j=3时发现不匹配了,那么下一趟 i又回到5,j继续从0开始匹配下去……

我们发现,源串的指针一直在做前后的“折返跑”(哦,好吧,行话叫“回溯”),有没有办法减少甚至消除此类回溯从而提高效率呢? 额,设问句问出来了,答案当然是有! 而且是本篇猪脚——KMP算法就是基于这种想法而提出的,它消除源串的回溯,是最坏情况O(n)的一个匹配算法!

 

具体说KMP,就是借助一个辅助数组,在匹配过程中一旦出现不匹配,源串指针不用回溯,直接移动模式串继续匹配,直到结束。

 

(注意,由于有形形色色的辅助函数,原理虽然一样,但是边界条件各有不同。各位童鞋如果考试遇到了,请务必按照自己教材的辅助函数计算方式,不然答错了概不负责哈&#**%¥…… 当然话说回来,作者一直认为,数据结构&算法分析啥的考试,让学生手动模拟是很傻很天真的…… 额 貌似又跑偏了)

 

算法思路:

 

如果 S[i-j...i] P[0…j] 在P[j] 失配了,即之前的S[i-j…i-1] = P[0…j-1]  *

但 S[i]<>P[j]

为了达到S串不回溯,需要把模式串P向右移动到某位置(比如K),继续比较S[i] 和 P[k]

 

但是这有一个前提,必须满足P[0…k-1] = S[i-k…i-1] = P[j-k…j-1]  (红色部分是由*式得到的), 否则在P[j]之前的部分就已经不匹配了

 

整理下,就是说如果 S[i-j...i] P[0…j] P[j] 失配了,我们希望得到一个值K,可以让我们的匹配从S[i]P[k]继续尝试下去

 

而K的求法,就是满足P[0…k-1] = P[j-k…j-1]的最大k ( k < j)

 

预定义边界条件 aux[0] = -1 ,如果P[0]失配,S串指针直接后移一位。

 

我们的辅助数组aux[n]定义就是上面的蓝色粗体字,再强调一遍

 

aux[j] = k 表示如果 S[i-j...i] P[0…j] P[j] 失配了,我们可以从S[i]P[k]继续匹配下去。

那么如果S[i] 和 P[k] 还不匹配呢? 再看看上面的定义,P[k]失配, 我们是不是要尝试S[i] 和 P[ P[k] ] ?

没错,重复这个过程,直到遇到S[i]可以和某个P[#] 匹配,或者 #=0还不匹配了,S串指针后移到i+1, P串再重头来过。

(注意,初涉KMP会导致习惯性头晕,个人建议头晕时反复咀嚼蓝色粗体字,效果不错哦~ 实际上,我写这篇的时候也嚼了好几遍的,嚼着嚼着就不晕了 :P)

 

有了aux数组,我们的匹配就可以在源串中一路向西了,哦不,是一路到底哈……

下面给出一个KMP的匹配实现,计算aux数组后面会谈到

(这里注意,预定义 aux[0] = -1)

	public int StringMatchKMP(String sourceStr, String patternStr) {
		// initialize auxiliary array
		int [] auxKMP = auxInitialNonRecursive(patternStr);
//		int [] auxKMP = auxInitialRecursive(patternStr);
		
		char[] source = sourceStr.toCharArray();
		char[] pattern = patternStr.toCharArray();
		
		for(int i = 0; i <= sourceStr.length() - patternStr.length(); i++) {
			int idxPattern = 0;
			while(idxPattern < pattern.length && idxPattern >= 0) {
				if(source[i] == pattern[idxPattern]) {
					i++;
					idxPattern++;
				}
				else { // mismatch in source[i] and pattern[idxPattern]
					idxPattern = auxKMP[idxPattern];
				}
			}
			if(idxPattern == pattern.length)
				return i - pattern.length;
		}
		
		return -1;
	}


下面说说计算aux数组(其取值仅与模式串本身有关)

再嚼一次:aux[j] = k 表示如果 S[i-j...i] P[0…j] P[j] 失配了,我们可以从S[i]P[k]继续尝试匹配。

K的求法,就是满足P[0…k-1] = P[j-k…j-1]的最大K

 

第一种方法,根据 “满足P[0…k-1] = P[j-k…j-1]的最大K”求K

即对每个1 <= j < m  求满足条件的k,如果没有,返回0

(神码?为啥返回0?  头晕嚼一嚼, aux[j] = 0 是不是表明, P[j]失配,我们用P[0]尝试)

 

同样,Java实现如下

	private int[] auxInitialNonRecursive(String patternStr) {
		char[] pattern = patternStr.toCharArray();
		
		int [] aux = new int[patternStr.length()];
		aux[0] = -1;
		
		for(int j = 1; j < patternStr.length(); j++) {
	        // check k from j-1 to 1, does p[0...k-1] matches p[j-k...j-1] ?
            int k = j - 1;
            while(k > 0) {
            	if(checkMax(k, j, pattern)) {
					aux[j] = k;
					break;
				}
            	k--;
            }
            if(k == 0)
            	aux[j] = 0;
		}
		return aux;
	}

	/** check if pattern[0...k-1] matches pattern[j-k...j-1]*/
	private boolean checkMax(int k, int j, char[] pattern) {
		int m = 0;
		int n = j - k;
		
		while(pattern[m] == pattern[n] && m < k) {
			m++;
			n++;
		}
		
		return (m == k);
	}

 

第二种方法,这也是大多数算法书上使用的,递归计算辅助数组

初值还是 aux[0] = -1

假设我们已知 aux[j-1] = k,现在要计算 aux[j]

还是从定义出发, aux[j-1] = k 表示 当P[j-1]失配时,应该移动P串到P[k]开始尝试匹配,而且P[0…k-1] = P[j-k-1…j-2] (这个等式换个角度看其实就是P串自身和自身在做匹配了,当然看不出来也木有关系的)

 

那么,如果 P[k] = P[j-1] 那么P[0…k] = P[j-k-1…j-1] 对比下前面定义,等价于 aux[j] = k+1(这说明当P[j]失配时,移动P串到P[k+1]开始尝试匹配,而且P[0…k] = P[j-k-1…j-1] )

如果 P[k] <> P[j-1] ,相当于 P[k]失配了, 我们令 k' = P[ P[k] ] ,当然也有P[0…k'-1] = P[j-k-1…j-2],

同样,如果P[k'] = P[j-1] 那么P[0…k'] = P[j-k-1…j-1], 等价于 aux[j] = k'+1

重复上面的过程,直到找出aux[j] 或者 某个k = -1,此时 aux[j] = 0

 

递归实现如下

	private int[] auxInitialRecursive(String patternStr) {
		char[] pattern = patternStr.toCharArray();
		
		int [] aux = new int[patternStr.length()];
		aux[0] = -1;
		
		for(int j = 1; j < patternStr.length(); j++) {
			int k = aux[j - 1];
			
			while(k >= 0 && pattern[k] != pattern[j - 1])
				k = aux[k];
			
			aux[j] = (k < 0) ? 0 : k + 1;
		}
		
		return aux;
	} 

可以借助平摊分析,得出KMP算法是O(n)的时间复杂度,至此,KMP算法就算谈完了

总结一下,

  1. KMP分两个部分

求辅助数组 & 应用辅助数组进行串匹配

  1. 通过使用辅助数组,KMP消除源串的回溯,(严格说,还是有原地踏步匹配的,但没有回头重复匹配的);

模式串是有回溯的——但未必每次都要回到第一位开始匹配,这个取决于辅助函数的返回

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
1 目标检测的定义 目标检测(Object Detection)的任务是找出图像中所有感兴趣的目标(物体),确定它们的类别和位置,是计算机视觉领域的核心问题之一。由于各类物体有不同的外观、形状和姿态,加上成像时光照、遮挡等因素的干扰,目标检测一直是计算机视觉领域最具有挑战性的问题。 目标检测任务可分为两个关键的子任务,目标定位和目标分类。首先检测图像中目标的位置(目标定位),然后给出每个目标的具体类别(目标分类)。输出结果是一个边界框(称为Bounding-box,一般形式为(x1,y1,x2,y2),表示框的左上角坐标和右下角坐标),一个置信度分数(Confidence Score),表示边界框中是否包含检测对象的概率和各个类别的概率(首先得到类别概率,经过Softmax可得到类别标签)。 1.1 Two stage方法 目前主流的基于深度学习的目标检测算法主要分为两类:Two stage和One stage。Two stage方法将目标检测过程分为两个阶段。第一个阶段是 Region Proposal 生成阶段,主要用于生成潜在的目标候选框(Bounding-box proposals)。这个阶段通常使用卷积神经网络(CNN)从输入图像中提取特征,然后通过一些技巧(如选择性搜索)来生成候选框。第二个阶段是分类和位置精修阶段,将第一个阶段生成的候选框输入到另一个 CNN 中进行分类,并根据分类结果对候选框的位置进行微调。Two stage 方法的优点是准确度较高,缺点是速度相对较慢。 常见Tow stage目标检测算法有:R-CNN系列、SPPNet等。 1.2 One stage方法 One stage方法直接利用模型提取特征值,并利用这些特征值进行目标的分类和定位,不需要生成Region Proposal。这种方法的优点是速度快,因为省略了Region Proposal生成的过程。One stage方法的缺点是准确度相对较低,因为它没有对潜在的目标进行预先筛选。 常见的One stage目标检测算法有:YOLO系列、SSD系列和RetinaNet等。 2 常见名词解释 2.1 NMS(Non-Maximum Suppression) 目标检测模型一般会给出目标的多个预测边界框,对成百上千的预测边界框都进行调整肯定是不可行的,需要对这些结果先进行一个大体的挑选。NMS称为非极大值抑制,作用是从众多预测边界框中挑选出最具代表性的结果,这样可以加快算法效率,其主要流程如下: 设定一个置信度分数阈值,将置信度分数小于阈值的直接过滤掉 将剩下框的置信度分数从大到小排序,选中值最大的框 遍历其余的框,如果和当前框的重叠面积(IOU)大于设定的阈值(一般为0.7),就将框删除(超过设定阈值,认为两个框的里面的物体属于同一个类别) 从未处理的框中继续选一个置信度分数最大的,重复上述过程,直至所有框处理完毕 2.2 IoU(Intersection over Union) 定义了两个边界框的重叠度,当预测边界框和真实边界框差异很小时,或重叠度很大时,表示模型产生的预测边界框很准确。边界框A、B的IOU计算公式为: 2.3 mAP(mean Average Precision) mAP即均值平均精度,是评估目标检测模型效果的最重要指标,这个值介于0到1之间,且越大越好。mAP是AP(Average Precision)的平均值,那么首先需要了解AP的概念。想要了解AP的概念,还要首先了解目标检测中Precision和Recall的概念。 首先我们设置置信度阈值(Confidence Threshold)和IoU阈值(一般设置为0.5,也会衡量0.75以及0.9的mAP值): 当一个预测边界框被认为是True Positive(TP)时,需要同时满足下面三个条件: Confidence Score > Confidence Threshold 预测类别匹配真实值(Ground truth)的类别 预测边界框的IoU大于设定的IoU阈值 不满足条件2或条件3,则认为是False Positive(FP)。当对应同一个真值有多个预测结果时,只有最高置信度分数的预测结果被认为是True Positive,其余被认为是False Positive。 Precision和Recall的概念如下图所示: Precision表示TP与预测边界框数量的比值 Recall表示TP与真实边界框数量的比值 改变不同的置信度阈值,可以获得多组Precision和Recall,Recall放X轴,Precision放Y轴,可以画出一个Precision-Recall曲线,简称P-R
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值