在 C# 中查找子数组的第一个出现/起始索引

Posted

技术标签:

【中文标题】在 C# 中查找子数组的第一个出现/起始索引【英文标题】:Find the first occurrence/starting index of the sub-array in C# 【发布时间】:2010-12-19 07:42:45 【问题描述】:

给定两个数组作为参数(x 和 y),并找到 x 中第一次出现 y 的起始索引。我想知道最简单或最快的实现是什么。

例子:

when x = 1,2,4,2,3,4,5,6
     y =       2,3
result
     starting index should be 3

更新:由于我的代码错误,我将其从问题中删除。

【问题讨论】:

您的代码是否试图找到子数组的第一个出现/起始索引?如果那样的话,您的结果框中的第二个示例不是,3 首先出现在 0 处吗?不是 2? 【参考方案1】:

最简单的写法?

    return (from i in Enumerable.Range(0, 1 + x.Length - y.Length)
            where x.Skip(i).Take(y.Length).SequenceEqual(y)
            select (int?)i).FirstOrDefault().GetValueOrDefault(-1);

当然没有那么高效……更像是这样:

private static bool IsSubArrayEqual(int[] x, int[] y, int start) 
    for (int i = 0; i < y.Length; i++) 
        if (x[start++] != y[i]) return false;
    
    return true;

public static int StartingIndex(this int[] x, int[] y) 
    int max = 1 + x.Length - y.Length;
    for(int i = 0 ; i < max ; i++) 
        if(IsSubArrayEqual(x,y,i)) return i;
    
    return -1;

【讨论】:

Marc,你能解释一下max 变量吗?为什么我们不能使用源数组的长度(x)? @Yair 如果源是 20 长,并且正在寻找长度为 5 的子数组,那么查看从索引(基于 0)16、17、18 开始的数组是没有意义的或 19:我们知道它不可能匹配,因为剩下的元素不够多。 因为看第 15 个索引会满足(x[15++]).. 如果我理解正确 @Yair 15++ 是什么意思?无论哪种方式:不,如果没有足够的元素,它就不能是子数组匹配 我喜欢你的 Linq 解决方案!【参考方案2】:

这是一个简单(但相当有效)的实现,它可以找到数组的所有出现,而不仅仅是第一个:

static class ArrayExtensions 

  public static IEnumerable<int> StartingIndex(this int[] x, int[] y) 
    IEnumerable<int> index = Enumerable.Range(0, x.Length - y.Length + 1);
    for (int i = 0; i < y.Length; i++) 
      index = index.Where(n => x[n + i] == y[i]).ToArray();
    
    return index;
  


例子:

int[] x =  1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4 ;
int[] y =  2, 3 ;
foreach (int i in x.StartingIndex(y)) 
  Console.WriteLine(i);

输出:

1
5
9

该方法首先循环遍历x数组以查找y数组中第一项的所有出现,并将它们的索引放置在index数组中。然后它继续通过检查其中哪些也匹配y 数组中的第二项来减少匹配。当检查y 数组中的所有项目时,index 数组只包含完整匹配项。

编辑: 另一种实现是从循环中的语句中删除 ToArray 调用,使其成为:

index = index.Where(n => x[n + i] == y[i]);

这将完全改变该方法的工作方式。它不是逐级循环遍历项目,而是返回一个带有嵌套表达式的枚举器,将搜索推迟到迭代枚举器的时间。这意味着如果您愿意,您只能获得第一场比赛:

int index = x.StartingIndex(y).First();

这不会找到所有匹配项然后返回第一个,它只会搜索直到找到第一个然后返回它。

【讨论】:

@Guffa 您似乎对 Enumerable 非常熟悉,您在回答我的另一个问题时使用了类似的方法***.com/questions/1253454 @Jeffrey:我添加了对上述算法的解释。 @Mark:我在上面添加了另一种方法,可以解决仅获得第一个匹配项的问题。 这是一个非常令人印象深刻的算法,但是没有 ToArray 的第二个变体会抛出一个索引超出范围异常,而第一个可以完美运行。 是的,因为在 Where() 子句的 lambda 中捕获了对 i 的引用。由于 linq 查询是惰性求值的,因此当 lambda 运行时 i 已经等于 y.Length ,从而创建了超出范围的异常。您可以通过在每次循环运行中将值复制到保持不变的局部变量中来解决此问题,如下所示:``` var i1 = i; index = index.Where(n => x[n + i1] == y[i1]); ```【参考方案3】:

最简单的方法大概是这样的:

public static class ArrayExtensions

    private static bool isMatch(int[] x, int[] y, int index)
    
        for (int j = 0; j < y.Length; ++j)
            if (x[j + index] != y[j]) return false;
        return true;
    

    public static int IndexOf(this int[] x, int[] y)
    
        for (int i = 0; i < x.Length - y.Length + 1; ++i)
            if (isMatch(x, y, i)) return i;
        return -1;
    

但这绝对不是最快的方式。

【讨论】:

【参考方案4】:

这是基于Mark Gravell's answer,但我将其设为通用并添加了一些简单的边界检查以防止抛出异常

private static bool IsSubArrayEqual<T>(T[] source, T[] compare, int start) where T:IEquatable<T>

    if (compare.Length > source.Length - start)
    
        //If the compare string is shorter than the test area it is not a match.
        return false;
    

    for (int i = 0; i < compare.Length; i++)
    
        if (source[start++].Equals(compare[i]) == false) return false;
    
    return true;

可以通过实现Boyer-Moore 进一步改进,但对于短模式它可以正常工作。

【讨论】:

【参考方案5】:

在这种情况下,“最简单”和“最快”是对立的,此外,为了描述快速算法,我们需要了解很多关于源数组和搜索数组如何相互关联的信息。

这与在字符串中查找子字符串本质上是相同的问题。假设您正在“快速棕色狐狸跳过懒狗”中寻找“狐狸”。在这种情况下,朴素的字符串匹配算法非常好。如果您要在格式为“banananananabananabananabananabananabananananananbananana...”的百万字符字符串中搜索“bananananananananananananananananana”,那么朴素的子字符串匹配算法是可怕的——通过使用可以获得更快的结果更复杂和复杂的字符串匹配算法。基本上,朴素算法是 O(nm),其中 n 和 m 是源字符串和搜索字符串的长度。有 O(n+m) 个算法,但它们要复杂得多。

您能告诉我们更多有关您正在搜索的数据的信息吗?它有多大,有多冗余,搜索数组有多长,匹配错误的可能性有多大?

【讨论】:

您是发布模糊问题的人;我不知道你的数据集有多大,你的应用程序是什么,或者你的性能要求是什么。你期望我会这样做是不合理的。此外,一个 600 字符的评论很难概括关于高效字符串搜索算法的大量文献。拿起一本关于算法设计的优秀大学本科教科书,您将获得大量用于子串匹配的不同算法的示例。【参考方案6】:

我发现以下内容更直观,但这可能是个人喜好问题。

public static class ArrayExtensions

    public static int StartingIndex(this int[] x, int[] y)
    
        var xIndex = 0;
        while(xIndex < x.length)
        
            var found = xIndex;
            var yIndex = 0;
            while(yIndex < y.length && xIndex < x.length && x[xIndex] == y[yIndex])
            
                xIndex++;
                yIndex++;
            

            if(yIndex == y.length-1)
            
                return found;
            

            xIndex = found + 1;
        

        return -1;
    

此代码还解决了我认为您的实现在 x = 3, 3, 7, y = 3, 7 等情况下可能遇到的问题。我认为您的代码会发生什么情况是它匹配第一个数字,然后在第二个数字上重新设置,但在第三个数字上再次开始匹配,而不是在开始匹配后立即返回索引。可能会遗漏一些东西,但这绝对是需要考虑的事情,并且应该可以在您的代码中轻松修复。

【讨论】:

您的代码遇到了与 Jeffreys 相同的问题:它在 new[] 9, 8, 3 .StartingIndex(new[] 3, 4 ) 上失败。 已通过在内部 while 中添加一个额外的子句来检查 xIndex 是否仍在范围内来解决此问题。【参考方案7】:
    //this is the best in C#

    //bool contains(array,subarray)
    //  when find (subarray[0])
    //      while subarray[next] IS OK
    //          subarray.end then Return True
    public static bool ContainSubArray<T>(T[] findIn, out int found_index,
 params T[]toFind)
    
        found_index = -1;
        if (toFind.Length < findIn.Length)
        

            int index = 0;
            Func<int, bool> NextOk = (i) =>
                
                    if(index < findIn.Length-1)
                        return findIn[++index].Equals(toFind[i]);
                    return false;
                ;
            //----------
            int n=0;
            for (; index < findIn.Length; index++)
            
                if (findIn[index].Equals(toFind[0]))
                
                    found_index=index;n=1;
                    while (n < toFind.Length && NextOk(n))
                        n++;
                
                if (n == toFind.Length)
                
                    return true;
                
            

        
        return false;
    

【讨论】:

【参考方案8】:
using System;
using System.Linq;

public class Test

    public static void Main()
    
        int[] x = 1,2,4,2,3,4,5,6;
        int[] y =       2,3;
        int? index = null;

        for(int i=0; i<x.Length; ++i)
        
            if (y.SequenceEqual(x.Skip(i).Take(y.Length)))
            
                index = i;
                break;
            
        
        Console.WriteLine($"index");
    

输出

3

【讨论】:

以上是关于在 C# 中查找子数组的第一个出现/起始索引的主要内容,如果未能解决你的问题,请参考以下文章

数字在排序数组中出现的起始索引號

查找数组中第一个出现序列的中心索引

面试之基础算法题:求一个数字在给定的已排序数组中出现的起始终止索引号(Java版)

查找第二次出现索引最低的第一个重复元素

C#如何从字节数组中提取字节?已知起始字节

在 C# 中的任意起始索引上初始化数组