暴力行為からの長い記事は、KMPアルゴリズムの進化を証明するために、JAVAやJSを達成します

  

  それは不慣れな補助配列を学習し始め、溶液の伝統的な添加と比較して、コードよりも小さいため、KMPアルゴリズムは広く理解することはより困難にので、新しいの場合には、文字列照合アルゴリズムを用いています。

  文字列マッチング暴力的なソリューション、KMPアルゴリズムの進化の伝統的な考え方からの暴力の解決策を見てみましょうとスタート。

  まず、我々は我々が含まれている場合、ソースのターゲットを含めるかどうかを知っている、と、位置のソースをターゲットにする、ソース文字列のソースを持っている、とターゲット文字列ターゲット。

  

  ソリューションの暴力によると、私たちのアイデアは、ソースとターゲットの最初の要素への2つのポインタをそれぞれiとj、ゼロ点を維持することです。

  0 J + 1、jおよびリターン - i及びj、異なる場合には、I I復元するながらシフト後であれば、同一の、継続的比較、iとjを比較する要素を指します。それは、ゼロからの次のIソースを開始するには、ターゲットと比較されます。

  私たちは、比較の過程を見てみましょう。

  まず、我々は二つのポインタjを、ソースとターゲットの最初の要素をポイントして、私を初期化します。

 

 

   我々は2つの要素が、ある、I〜Jの後方同時に同じであることがわかりました。

 

 

   Bは、また、第2の要素、継続的なシフトに等しいです。

 

 

   または同等の、シフト後も継続します

   今見つけ、iとjのポイントはもはや等しい要素に、それであるが、私はゼロからスタートし、ターゲット・マッチが上にある一致していない、我々は1 iとターゲットで始まるに一致するようにしてみてください。

   このように、我々は、マッチングの新しいラウンドを開始し、全配向処理サイクルの上記ステップを繰り返すこと、最悪の場合は、開始とターゲットのサブストリングと比較した(長さ0~4)Iを必要とします、ソース長はM、長さnの対象である場合、アルゴリズムの時間計算量はO(M * N)です。

  JAVA暴力的な解決策は、以下のことを達成するために:

    / **
     * @Author NXY
     * @Date 2020年2月16日夜05時47分
     * @Param source:源字符串数组,target:目标字符串数组
     * @Return -1:target 不是 source 的子串;其它返回值:target 在 source 中的位置
     * @Exception
     * @Description 判断目标字符串是否为源字符串子串
     */
    public static final int isSonStr(char[] source, char[] target) {
        if (target == null || source == null) {
            throw new RuntimeException("入参存在空串!");
        }
        int sourceLength = source.length;
        int targetLength = target.length;
        //特殊情况处理
        if (targetLength > sourceLength) {
            return -1;
        }
        int i=0;
        while(i < sourceLength-targetLength) {
            for (int j = 0; j <= targetLength; j++) {
                //target 匹配完成,返回结果
                if (j == targetLength) {
                    return i - j ;
                }
                if (source[i] == target[j]) {
                    i++;
                } else {
                    i = i - j + 1;
                    break;
                }
            }
        }
        return -1;
    }

  暴力解法 JS 实现如下:

function isSonArr(source,target){
    if(typeof(source)=="undefined"||typeof(target)=="undefined"){
        throw new Error("入参存在空串!");
    }
    var sourceLength=source.length;
    var targetLength=target.length;
    if(targetLength>sourceLength){
        return -1;
    }
    var i=0;
    while(i<sourceLength-targetLength){
        for(var j=0;j<=targetLength;j++){
            if(j==targetLength){
                return i-j;
            }
            if(source[i]==target[j]){
                i++;
            }else{
                i=i-j+1;
                break;
            }
        }
    }
    return -1;
}

  如果只是要得到正确结果的话,暴力解法没有任何问题,但是如果对时间复杂度有一定的要求,暴力解法就有些力不从心了。

  我们回看一下,比对的过程,当我们发现,i 与 j 所指向的元素不同时:

  对于 i 指针的回退,我们会直接回退到 i-j+1 ,我们能不能直接略过已经比对过的元素(标红)呢:

   从肉眼看便知道答案是否定的。我们不能略过 source[3] 也就是 a 。造成这种问题的原因是,source[3] 虽然不等于 target[3] ,但是 source[3] = target[0] , source[3] 虽然不能作为子串的第四个元素,但是 source[3] 是可以做为子串的起点

  所以我们在略过已经比对过的元素时,原则便是不能略过 source 中可以作为 target 前缀的元素。 

   我们要做的是找到 source 中除 source[0] 外下一个可以作为 target[0] 的元素。这时我们在 source 中 i 应该回退到哪里呢?

   source[0],source[1],source[2] 我们都已经做过比对,其分别等于 target 中的 0到2 ,且在 target 中我们便知道 0到2 下标的元素不可以作为头元素,那么0,1,2我们全部略过,至于 source[3] ,因为不等于target[3] ,所以我们无法从target 知道它是否可以作为 target 的头元素,保险起见我们从 source[3] 开始与 target[0] 进行下一轮的比对,将 source[1],source[2] 都略过,这样便避免了许多无效计算。

  以上思路和核心是,我们从 target 中得知,已比对过的元素中哪些可以作为 target 的起始元素,这些元素不能略过,应做为起始位置与 target 进行对比。而其余不能作为起始元素的,我们直接略过避免无效的运算。

  我们对原来的算法做一下优化,我们新建一个数组,用于标识 target 中哪些元素可以作为 target 的起始元素。很显然数组为:[ 1 ,0,0,0 ] 。也就是说这些元素除头元素外都不能作为 target 的起始元素。那么我们在进行比对时,每次 source[i] != target[j] ,i 不必回退到 i-j+1 ,i 只需要呆在原地与 target[0] 比较就好了,因为我们知道 source 的 0到 2 与 target 的 0到 2 均相等,而且 target 的 0 到 2 均不能作为 target 的起始元素,回退到这些元素与 target[0] 比较没有意义。

  那么下面让我们来看一种更复杂的情况,也就是 target 中除头元素外,有元素可以作为起始元素的情况,来讨论如何求得我们应略过的位数:

 

   我们略过相同的部分,直接到需要 i 发生回溯的比较位置:

 

  我们可以看到,当 i = j =4 时,source [ 4 ]  !=  target [ 4 ] ,此时我们需要将指针 i 进行回溯。

  按照暴力解法, i 应该回溯到 1 位置。但是正如上面所说,因为两个数组中下标为 0 到 3  的元素相同,所以我们可以直接从 target 中判断, source 中下标 0 到 3 的元素可不可以作为 target 的头元素。

  我们可以看到:

  下标为 1 的元素不能作为头元素,所以我们在将 i 进行回退时可以直接略过:

  原来我们需要将 i 回退为 i-j+1=1 的位置来与 target 进行新一轮的比较,但现在因为我们知道了,从 target 数组看,b是不能作为头元素的,我们可以直接略过。

  我们新建一个数组,用于标识出 target 数组中,可以作为头元素的元素,0 表示该元素不能作为头元素,而 1 表示该元素可以作为头元素:

  得到的辅助数组我们称为 prefix 数组(前缀表),当我们的 i 与 j 指向的元素不匹配时,如果 next[j] =1, 也就是当前 j 可以作为 target 的头元素,我们直接将 i 移动到 source 中指向这个 j 的元素,也就是 source 中当前 i 不变,j 置为 0 。

  这样,我们可以在 j 指向可以作为头元素的元素时,若需要回退 i ,减少 i 的回退次数。

  但还有一种情况是,如果在 j 为 3 时发生了不匹配:

   通过上面的数组我们发现 target[3] 不能作为 target 的头元素,所以 i 依然要回退到最初的位置进行比较。

  但实际上,因为其前驱元素可以作为 target 的头元素,所以 i 只需要向前移动一位即可。

  为了表示这种关系,我们将 next[4] 中的置为 2,表示在其前驱元素可以作为 target 数组的头元素的前提下,其可以作为 target 数组的第二个元素(可以作为第 n 个元素的情况同样)。这样,i 在回溯时只需要回溯一个位置,回溯到离其最近的头元素即可。 

  这样,我们在 i 与 j 不匹配时,通过查 next 表,便可以知道 i 需要向前回溯几个位置,也就是 i 需要向前回溯 next[j]-1 个位置(next[j] 非零的情况下)。

  我们再来考虑一种情况,当 j 指向 target[5] 也就是 c 时,如果发生不匹配是否需要将 i 回退到与 target[3] 对应的 source 的元素呢。答案是没有必要的,因为虽然 target[2]=a 可以作为 target[0],target[3]=b 可以作为 target[1] ,但是 target[4]=b 不能作为 target[2] ,所以即使从 target[2] 作为头开始匹配,也会在 target[4] 处匹配失败

  以上过程说明,我们在 i 跳跃时,不只是跳跃到离当前 i 最近的头元素,还要跳到离当前 i 最近的有效的头元素。

  而最近的头元素是否有效,需要看当前 i 的前驱元素是否是有效的。

  所以我们的比对过程便成了:

  如果 source[i] == target[j] ,则 i++,j++

  如果 source[i] != target[j] ,我们需要查询 next[j-1] 是否为0,如果为 0 ,则 i 不变,j变为0 继续比对。否则 i 回退 next[j-1] 位回退到最近的有效头元素,j 变为 0 继续比对。

  当元素发生失配时,source中发生失配的元素与target中对应的元素不同,我们不能通过 target 的 next 数组判断 source 中的该元素是否可以作为 target 中的第 n 号元素,所以我们通过其前驱元素(因为前驱元素 source 中与 target 中相同)判断距离最近的可作为头元素的节点是否有效,从而决定是否跳转。整个逻辑非常复杂,思路清奇,总之我是裂开了。

  

  

  

 

おすすめ

転載: www.cnblogs.com/niuyourou/p/12319702.html