Nhận chỉ số của lần xuất hiện thứ n của một chuỗi?


100

Trừ khi tôi thiếu một phương thức tích hợp rõ ràng, cách nhanh nhất để nhận được lần xuất hiện thứ n của một chuỗi trong một chuỗi là gì?

Tôi nhận ra rằng tôi có thể lặp phương thức IndexOf bằng cách cập nhật chỉ mục bắt đầu của nó trên mỗi lần lặp lại của vòng lặp. Nhưng làm theo cách này có vẻ lãng phí đối với tôi.


Tôi sẽ sử dụng một biểu thức chính quy cho điều đó, sau đó bạn phải có cách tối ưu để khớp chuỗi trong chuỗi. Đây là một trong những DSL đẹp mà tất cả chúng ta nên sử dụng khi có thể. Một ví dụ trong VB.net, mã gần như giống nhau trong C #.
bovium

2
Tôi sẽ đặt tiền vào phiên bản biểu thức chính quy khó hơn đáng kể so với "tiếp tục lặp lại và thực hiện String.IndexOf đơn giản". Biểu thức chính quy có vị trí của chúng, nhưng không nên sử dụng khi có các lựa chọn thay thế đơn giản hơn.
Jon Skeet

Câu trả lời:


52

Về cơ bản, đó là những gì bạn cần làm - hoặc ít nhất, đó là giải pháp dễ dàng nhất. Tất cả những gì bạn đang "lãng phí" là chi phí của n lần gọi phương thức - bạn sẽ không thực sự kiểm tra bất kỳ trường hợp nào hai lần, nếu bạn nghĩ về nó. (IndexOf sẽ quay lại ngay sau khi tìm thấy kết quả phù hợp và bạn sẽ tiếp tục từ nơi nó đã dừng lại.)


2
Tôi cho rằng quyền của bạn, có vẻ như cần phải có một phương thức tích hợp sẵn, tôi chắc chắn rằng đó là một sự xuất hiện phổ biến.
PeteT 9/10/08

4
Có thật không? Tôi không thể nhớ đã bao giờ phải làm điều đó trong khoảng 13 năm phát triển Java và C #. Điều đó không có nghĩa là tôi thực sự chưa bao giờ phải làm điều đó - nhưng chỉ là không đủ thường xuyên để nhớ.
Jon Skeet

Nói về Java, chúng ta có StringUtils.ordinalIndexOf(). C # với tất cả Linq và các tính năng tuyệt vời khác, chỉ là không có hỗ trợ tích hợp cho việc này. Và có, rất bắt buộc phải có sự hỗ trợ của nó nếu bạn đang xử lý trình phân tích cú pháp và mã hóa.
Annie

3
@Annie: Bạn nói "chúng tôi có" - ý bạn là trong Apache Commons? Nếu vậy, bạn có thể viết thư viện bên thứ ba của riêng mình cho .NET dễ dàng như bạn có thể cho Java ... vì vậy, đó không phải là thứ mà thư viện chuẩn Java có mà .NET không có. Và tất nhiên trong C # bạn có thể thêm nó như là một phương pháp mở rộng trên string:)
Jon Skeet

108

Bạn thực sự có thể sử dụng biểu thức chính quy /((s).*?){n}/để tìm kiếm lần xuất hiện thứ n của chuỗi con s.

Trong C #, nó có thể trông như thế này:

public static class StringExtender
{
    public static int NthIndexOf(this string target, string value, int n)
    {
        Match m = Regex.Match(target, "((" + Regex.Escape(value) + ").*?){" + n + "}");

        if (m.Success)
            return m.Groups[2].Captures[n - 1].Index;
        else
            return -1;
    }
}

Lưu ý: Tôi đã thêm vào Regex.Escapegiải pháp gốc để cho phép tìm kiếm các ký tự có ý nghĩa đặc biệt đối với công cụ regex.


2
bạn có nên thoát khỏi value? Trong trường hợp của tôi, tôi đang tìm kiếm một dấu chấm msdn.microsoft.com/en-us/library/…
russau

3
Regex này không hoạt động nếu chuỗi đích chứa dấu ngắt dòng. Bạn có thể sửa chữa nó? Cảm ơn.
Ignacio Soler Garcia

Có vẻ sẽ khóa nếu không có trận đấu thứ N. Tôi cần giới hạn giá trị được phân tách bằng dấu phẩy ở 1000 giá trị và giá trị này bị treo khi csv có ít hơn. Vì vậy, @Yogesh - có lẽ không phải là một câu trả lời tuyệt vời được chấp nhận như hiện tại. ;) Sử dụng một biến thể của câu trả lời này (có một phiên bản chuỗi thành chuỗi ở đây ) và thay đổi vòng lặp để dừng ở số thứ n .
ruffin

Đang cố gắng tìm kiếm trên \, giá trị được truyền vào là "\\" và chuỗi khớp trông giống như thế này trước hàm regex.match: ((). *?) {2}. Tôi gặp lỗi này: phân tích cú pháp "((). *?) {2}" - Không đủ). Định dạng chính xác để tìm kiếm dấu gạch chéo ngược mà không có lỗi là gì?
RichieMN

3
Xin lỗi nhưng một lời chỉ trích nhỏ: các giải pháp regex là không tối ưu, bởi vì sau đó tôi phải học lại regexs lần thứ n. Về cơ bản, mã khó đọc hơn khi sử dụng regex.
Mark Rogers

19

Về cơ bản, đó là những gì bạn cần làm - hoặc ít nhất, đó là giải pháp dễ dàng nhất. Tất cả những gì bạn đang "lãng phí" là chi phí của n lần gọi phương thức - bạn sẽ không thực sự kiểm tra bất kỳ trường hợp nào hai lần, nếu bạn nghĩ về nó. (IndexOf sẽ quay lại ngay sau khi tìm thấy kết quả phù hợp và bạn sẽ tiếp tục từ nơi nó đã dừng lại.)

Đây là triển khai đệ quy ( ý tưởng ở trên ) như một phương thức mở rộng, bắt chước định dạng của (các) phương thức khung:

public static int IndexOfNth(this string input,
                             string value, int startIndex, int nth)
{
    if (nth < 1)
        throw new NotSupportedException("Param 'nth' must be greater than 0!");
    if (nth == 1)
        return input.IndexOf(value, startIndex);
    var idx = input.IndexOf(value, startIndex);
    if (idx == -1)
        return -1;
    return input.IndexOfNth(value, idx + 1, --nth);
}

Ngoài ra, đây là một số bài kiểm tra đơn vị (MBUnit) có thể giúp bạn (để chứng minh nó là đúng):

using System;
using MbUnit.Framework;

namespace IndexOfNthTest
{
    [TestFixture]
    public class Tests
    {
        //has 4 instances of the 
        private const string Input = "TestTest";
        private const string Token = "Test";

        /* Test for 0th index */

        [Test]
        public void TestZero()
        {
            Assert.Throws<NotSupportedException>(
                () => Input.IndexOfNth(Token, 0, 0));
        }

        /* Test the two standard cases (1st and 2nd) */

        [Test]
        public void TestFirst()
        {
            Assert.AreEqual(0, Input.IndexOfNth("Test", 0, 1));
        }

        [Test]
        public void TestSecond()
        {
            Assert.AreEqual(4, Input.IndexOfNth("Test", 0, 2));
        }

        /* Test the 'out of bounds' case */

        [Test]
        public void TestThird()
        {
            Assert.AreEqual(-1, Input.IndexOfNth("Test", 0, 3));
        }

        /* Test the offset case (in and out of bounds) */

        [Test]
        public void TestFirstWithOneOffset()
        {
            Assert.AreEqual(4, Input.IndexOfNth("Test", 4, 1));
        }

        [Test]
        public void TestFirstWithTwoOffsets()
        {
            Assert.AreEqual(-1, Input.IndexOfNth("Test", 8, 1));
        }
    }
}

Tôi đã cập nhật các trường hợp định dạng và thử nghiệm của mình dựa trên phản hồi tuyệt vời của Weston (cảm ơn Weston).
Tod Thomson

14
private int IndexOfOccurence(string s, string match, int occurence)
{
    int i = 1;
    int index = 0;

    while (i <= occurence && (index = s.IndexOf(match, index + 1)) != -1)
    {
        if (i == occurence)
            return index;

        i++;
    }

    return -1;
}

hoặc trong C # với các phương thức mở rộng

public static int IndexOfOccurence(this string s, string match, int occurence)
{
    int i = 1;
    int index = 0;

    while (i <= occurence && (index = s.IndexOf(match, index + 1)) != -1)
    {
        if (i == occurence)
            return index;

        i++;
    }

    return -1;
}

5
Nếu tôi không nhầm, phương pháp này không thành công nếu chuỗi cần khớp bắt đầu ở vị trí 0, có thể được sửa bằng cách đặt indexban đầu thành -1.
Peter Majeed

1
Bạn cũng có thể muốn kiểm tra các chuỗi rỗng hoặc rỗng và khớp với nhau hoặc nó sẽ ném nhưng đó là một quyết định thiết kế.

Cảm ơn @PeterMajeed - nếu "BOB".IndexOf("B")trả về 0, thì hàm này cũng nên choIndexOfOccurence("BOB", "B", 1)
PeterX 17/02/15

2
Yours có lẽ là giải pháp cuối cùng vì nó có cả chức năng mở rộng và nó tránh được regex và đệ quy, cả hai đều làm cho mã khó đọc hơn.
Mark Rogers

@tdyen Thật vậy, Mã Phân tích sẽ phát hành "CA1062: đối số Validate các phương pháp công cộng" nếu IndexOfOccurencekhông kiểm tra nếu snull. Và String.IndexOf (String, Int32) sẽ ném ArgumentNullExceptionnếu matchnull.
DavidRR

1

Có lẽ cũng sẽ rất tuyệt khi làm việc với String.Split()Phương thức và kiểm tra xem sự xuất hiện được yêu cầu có trong mảng hay không, nếu bạn không cần chỉ mục, nhưng giá trị tại chỉ mục


1

Sau một số điểm chuẩn, đây có vẻ là giải pháp đơn giản và hiệu quả nhất

public static int IndexOfNthSB(string input,
             char value, int startIndex, int nth)
        {
            if (nth < 1)
                throw new NotSupportedException("Param 'nth' must be greater than 0!");
            var nResult = 0;
            for (int i = startIndex; i < input.Length; i++)
            {
                if (input[i] == value)
                    nResult++;
                if (nResult == nth)
                    return i;
            }
            return -1;
        }

1

System.ValueTuple ftw:

var index = line.Select((x, i) => (x, i)).Where(x => x.Item1 == '"').ElementAt(5).Item2;

viết một hàm từ đó là bài tập về nhà


0

Câu trả lời của Tod có thể được đơn giản hóa phần nào.

using System;

static class MainClass {
    private static int IndexOfNth(this string target, string substring,
                                       int seqNr, int startIdx = 0)
    {
        if (seqNr < 1)
        {
            throw new IndexOutOfRangeException("Parameter 'nth' must be greater than 0.");
        }

        var idx = target.IndexOf(substring, startIdx);

        if (idx < 0 || seqNr == 1) { return idx; }

        return target.IndexOfNth(substring, --seqNr, ++idx); // skip
    }

    static void Main () {
        Console.WriteLine ("abcbcbcd".IndexOfNth("bc", 1));
        Console.WriteLine ("abcbcbcd".IndexOfNth("bc", 2));
        Console.WriteLine ("abcbcbcd".IndexOfNth("bc", 3));
        Console.WriteLine ("abcbcbcd".IndexOfNth("bc", 4));
    }
}

Đầu ra

1
3
5
-1

0

Hoặc tương tự như thế này với vòng lặp do while

 private static int OrdinalIndexOf(string str, string substr, int n)
    {
        int pos = -1;
        do
        {
            pos = str.IndexOf(substr, pos + 1);
        } while (n-- > 0 && pos != -1);
        return pos;
    }

-4

Điều này có thể làm được:

Console.WriteLine(str.IndexOf((@"\")+2)+1);

2
Tôi không biết điều này sẽ hoạt động như thế nào. Bạn có thể bao gồm một lời giải thích ngắn gọn về những gì điều này làm?
Bob Kaufman
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.