为给定的字符串生成所有唯一的子字符串

72
给定一个字符串 s,生成它所有唯一子串的最快方法是什么?
例如:对于 str = "aba",我们将获得 substrs={"a", "b", "ab", "ba", "aba"}
朴素算法是在每个迭代中遍历整个字符串,并以长度为 1..n 的方式生成子串,产生 O(n^2) 的上限。
是否有更好的上限?
(这是技术作业,因此仅接受指针。)

@Yuval.. 你得到高效算法了吗?如果有的话,请分享一下。谢谢。 - sgowd
2
我不太记得发生了什么。但最有可能的是我最终实现了某种后缀树。抱歉,我没有代码了。 - Yuval Adam
1
该算法不以O(n²)的时间运行! - Raphael
13个回答

50

正如其他帖子中提到的,对于给定的字符串,可能存在O(n^2)的子字符串,因此打印它们的速度不能更快。但是存在一种有效的集合表示方法,可以在线性时间内构建: 后缀树


4
这是O(n+L),其中L是唯一子字符串的数量,我相信。因此它是最优的。 - Aryabhatta

12

由于一个字符串中共有O(n2)个子字符串,所以如果你必须生成它们全部,最坏情况下它们的数量将为n(n + 1) / 2,因此无法比O(n2)更快,即Ω(n2)是下限。


1
我们可以期望一个输出敏感算法,它在O(唯一字符串数量)的时间内运行... - user287792
3
你的意思是Ω(n^2)的下界为n的平方。 :) - kennytm
后缀树可以在O(n + L)的时间内完成,其中L是唯一子字符串的数量。对于像'aaaaa....aaaaa'这样的字符串,L = O(n)。因此,关于Omega(n^2)的说法是不正确的。 - Aryabhatta
@Moron - 我很好奇后缀树如何以O(n + L)的时间复杂度解决这个问题。能否发一下算法? - IVlad
1
@IVlad:只需遍历整个后缀树并在沿途打印路径/子路径。这不是O(L)吗?当然,这假定打印字符串是O(1)(例如,我们仅打印起始和结束索引)。如果我们认为将字符串x打印需要O(|x|)的时间,则是Omega(n ^ 2)。从您的帖子中并不清楚您已经考虑到了这一点,我猜测您的帖子实际上暗示了该情况的Omega(n ^ 3)下限。 - Aryabhatta
@IVlad:当然,我还没有尝试证明关于后缀树的主张,但它对我来说似乎是正确的。另外,我们可以考虑在上面的评论中将print视为generate,我想。 - Aryabhatta

9

第一种方法是暴力破解,时间复杂度为O(N^3),但可以降低到O(N^2 log(N))。

第二种方法是使用HashSet,时间复杂度为O(N^2)。

第三种方法是使用LCP,首先找到给定字符串的所有后缀,时间复杂度最坏情况下为O(N^2),最佳情况下为O(N Log(N))。

第一种解决方案:

import java.util.Scanner;

public class DistinctSubString {
  public static void main(String[] args) {
    Scanner in = new Scanner(System.in);
    System.out.print("Enter The string");
    String s = in.nextLine();
    long startTime = System.currentTimeMillis();
    int L = s.length();
    int N = L * (L + 1) / 2;
    String[] Comb = new String[N];
    for (int i = 0, p = 0; i < L; ++i) {
      for (int j = 0; j < (L - i); ++j) {
        Comb[p++] = s.substring(j, i + j + 1);
      }
    }
    /*
     * for(int j=0;j<N;++j) { System.out.println(Comb[j]); }
     */
    boolean[] val = new boolean[N];
    for (int i = 0; i < N; ++i)
      val[i] = true;
    int counter = N;
    int p = 0, start = 0;
    for (int i = 0, j; i < L; ++i) {
      p = L - i;
      for (j = start; j < (start + p); ++j) {
        if (val[j]) {
          //System.out.println(Comb[j]);
          for (int k = j + 1; k < start + p; ++k) {
            if (Comb[j].equals(Comb[k])) {
              counter--;
              val[k] = false;
            }
          }
        }

      }

      start = j;
    }
    System.out.println("Substrings are " + N
        + " of which unique substrings are " + counter);
    long endTime = System.currentTimeMillis();
    System.out.println("It took " + (endTime - startTime) + " milliseconds");
  }
}

第二种解决方案:
import java.util.*;

public class DistictSubstrings_usingHashTable {

  public static void main(String args[]) {
    // create a hash set

    Scanner in = new Scanner(System.in);
    System.out.print("Enter The string");
    String s = in.nextLine();
    int L = s.length();
    long startTime = System.currentTimeMillis();
    Set<String> hs = new HashSet<String>();
    // add elements to the hash set
    for (int i = 0; i < L; ++i) {
      for (int j = 0; j < (L - i); ++j) {
        hs.add(s.substring(j, i + j + 1));
      }
    }
    System.out.println(hs.size());
    long endTime = System.currentTimeMillis();
    System.out.println("It took " + (endTime - startTime) + " milliseconds");
  }
}

第三种解决方案:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;

public class LCPsolnFroDistinctSubString {

  public static void main(String[] args) throws IOException {

    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    System.out.println("Enter Desired String ");
    String string = br.readLine();
    int length = string.length();
    String[] arrayString = new String[length];
    for (int i = 0; i < length; ++i) {
      arrayString[i] = string.substring(length - 1 - i, length);
    }

    Arrays.sort(arrayString);
    for (int i = 0; i < length; ++i)
      System.out.println(arrayString[i]);

    long num_substring = arrayString[0].length();

    for (int i = 0; i < length - 1; ++i) {
      int j = 0;
      for (; j < arrayString[i].length(); ++j) {
        if (!((arrayString[i].substring(0, j + 1)).equals((arrayString)[i + 1]
            .substring(0, j + 1)))) {
          break;
        }
      }
      num_substring += arrayString[i + 1].length() - j;
    }
    System.out.println("unique substrings = " + num_substring);
  }

}

第四种解决方案:

  public static void printAllCombinations(String soFar, String rest) {
    if(rest.isEmpty()) {
        System.out.println(soFar);
    } else {
        printAllCombinations(soFar + rest.substring(0,1), rest.substring(1));
        printAllCombinations(soFar , rest.substring(1));
    }
}

Test case:-  printAllCombinations("", "abcd");

第三种解决方案的最佳情况是O(nlogn)。 - Walt
好的。你已经计算了所有独特的子字符串。不错。但是,如何使用后缀数组/最长公共前缀数组“生成给定字符串的所有独特子字符串”? - keyboardSmasher

3

错误,什么?O(a^n)?a是什么,你从哪里得出这个结果的? - IVlad
抱歉,这个符号表示随着时间的增长而增加...我切换回去了。 - Nix
通过使用最长公共后缀(LCP),时间复杂度可以降到O(NLogN)。 - Sumit Kumar Saha
@SumitKumarSaha 你是指最长公共前缀。 - goelakash
@goelakash 是的前缀,我无法编辑我的先前评论。 - Sumit Kumar Saha

2

嗯,由于可能有 n*(n+1)/2 种不同的子字符串(空字符串算一种),所以我怀疑你不可能比 O(n*2) 更快(最坏情况下)。最简单的方法是生成它们,并在找到它们时使用一些不错的 O(1) 查找表(如哈希表)来排除重复项。


这是 n(n+1)/2。"abc" 有 34/2 = 6 个子字符串("a", "b", "c", "ab", "bc", "abc"),而不是 32/2 = 3 个子字符串。 - IVlad
1
请注意,哈希表使用哈希码和相等函数,其时间复杂度为 O(n),其中 n 为字符串的长度。 - fgb
哦,好的,抱歉...会修复的... :) - back2dos
通过使用滚动哈希,例如可以获得O(1)的查找。 - IVlad

2
class SubstringsOfAString {
    public static void main(String args[]) {

        String string = "Hello", sub = null;

        System.out.println("Substrings of \"" + string + "\" are :-");

        for (int i = 0; i < string.length(); i++) {
            for (int j = 1; j <= string.length() - i; j++) {
                sub = string.substring(i, j + i);
                System.out.println(sub);
            }
        }
    }
}

1
class program
{

        List<String> lst = new List<String>();
        String str = "abc";
        public void func()
        {

            subset(0, "");
            lst.Sort();
            lst = lst.Distinct().ToList();

            foreach (String item in lst)
            {
                Console.WriteLine(item);
            }
        }
        void subset(int n, String s)
        {
            for (int i = n; i < str.Length; i++)
            {
                lst.Add(s + str[i].ToString());
                subset(i + 1, s + str[i].ToString());
            }
        }
}

1
许多包括两个for循环和一个.substring()调用的答案声称具有O(N^2)时间复杂度。然而,需要注意的是,在Java中.substring()调用的最坏情况(Java 7更新6后)为O(N)。因此,通过在代码中添加.substring()调用,N的顺序增加了一倍。
因此,两个for循环和这些循环中的一个.substring()调用等于O(N^3)时间复杂度。

1

这个程序打印出唯一的子字符串。 https://ideone.com/QVWOh0

def uniq_substring(test):
    lista=[]
    [lista.append(test[i:i+k+1]) for i in range(len(test)) for k in
    range(len(test)-i) if test[i:i+k+1] not in lista and 
    test[i:i+k+1][::-1] not in lista]
    print lista

uniq_substring('rohit')
uniq_substring('abab')

['r', 'ro', 'roh', 'rohi', 'rohit', 'o', 'oh', 'ohi', 'ohit', 'h',   
'hi', 'hit', 'i', 'it', 't']
['a', 'ab', 'aba', 'abab', 'b', 'bab']

0
尝试使用后缀数组和最长公共前缀来编写此代码。它还可以为您提供唯一子字符串的总数。在Visual Studio中,该代码可能会导致堆栈溢出,但在Eclipse C++中运行良好。这是因为函数返回向量。尚未对极长字符串进行测试。将进行测试并报告结果。
  // C++ program for building LCP array for given text
#include <bits/stdc++.h>
#include <vector>
#include <string>
using namespace std;

#define MAX 100000
int cum[MAX];


// Structure to store information of a suffix
struct suffix
{
    int index; // To store original index
    int rank[2]; // To store ranks and next rank pair
};

// A comparison function used by sort() to compare two suffixes
// Compares two pairs, returns 1 if first pair is smaller
int cmp(struct suffix a, struct suffix b)
{
    return (a.rank[0] == b.rank[0])? (a.rank[1] < b.rank[1] ?1: 0):
        (a.rank[0] < b.rank[0] ?1: 0);
}

// This is the main function that takes a string 'txt' of size n as an
// argument, builds and return the suffix array for the given string
vector<int> buildSuffixArray(string txt, int n)
{
    // A structure to store suffixes and their indexes
    struct suffix suffixes[n];

    // Store suffixes and their indexes in an array of structures.
    // The structure is needed to sort the suffixes alphabatically
    // and maintain their old indexes while sorting
    for (int i = 0; i < n; i++)
    {
        suffixes[i].index = i;
        suffixes[i].rank[0] = txt[i] - 'a';
        suffixes[i].rank[1] = ((i+1) < n)? (txt[i + 1] - 'a'): -1;
    }

    // Sort the suffixes using the comparison function
    // defined above.
    sort(suffixes, suffixes+n, cmp);

    // At his point, all suffixes are sorted according to first
    // 2 characters. Let us sort suffixes according to first 4
    // characters, then first 8 and so on
    int ind[n]; // This array is needed to get the index in suffixes[]
    // from original index. This mapping is needed to get
    // next suffix.
    for (int k = 4; k < 2*n; k = k*2)
    {
        // Assigning rank and index values to first suffix
        int rank = 0;
        int prev_rank = suffixes[0].rank[0];
        suffixes[0].rank[0] = rank;
        ind[suffixes[0].index] = 0;

        // Assigning rank to suffixes
        for (int i = 1; i < n; i++)
        {
            // If first rank and next ranks are same as that of previous
            // suffix in array, assign the same new rank to this suffix
            if (suffixes[i].rank[0] == prev_rank &&
                    suffixes[i].rank[1] == suffixes[i-1].rank[1])
            {
                prev_rank = suffixes[i].rank[0];
                suffixes[i].rank[0] = rank;
            }
            else // Otherwise increment rank and assign
            {
                prev_rank = suffixes[i].rank[0];
                suffixes[i].rank[0] = ++rank;
            }
            ind[suffixes[i].index] = i;
        }

        // Assign next rank to every suffix
        for (int i = 0; i < n; i++)
        {
            int nextindex = suffixes[i].index + k/2;
            suffixes[i].rank[1] = (nextindex < n)?
                                suffixes[ind[nextindex]].rank[0]: -1;
        }

        // Sort the suffixes according to first k characters
        sort(suffixes, suffixes+n, cmp);
    }

    // Store indexes of all sorted suffixes in the suffix array
    vector<int>suffixArr;
    for (int i = 0; i < n; i++)
        suffixArr.push_back(suffixes[i].index);

    // Return the suffix array
    return suffixArr;
}

/* To construct and return LCP */
vector<int> kasai(string txt, vector<int> suffixArr)
{
    int n = suffixArr.size();

    // To store LCP array
    vector<int> lcp(n, 0);

    // An auxiliary array to store inverse of suffix array
    // elements. For example if suffixArr[0] is 5, the
    // invSuff[5] would store 0. This is used to get next
    // suffix string from suffix array.
    vector<int> invSuff(n, 0);

    // Fill values in invSuff[]
    for (int i=0; i < n; i++)
        invSuff[suffixArr[i]] = i;

    // Initialize length of previous LCP
    int k = 0;

    // Process all suffixes one by one starting from
    // first suffix in txt[]
    for (int i=0; i<n; i++)
    {
        /* If the current suffix is at n-1, then we don’t
        have next substring to consider. So lcp is not
        defined for this substring, we put zero. */
        if (invSuff[i] == n-1)
        {
            k = 0;
            continue;
        }

        /* j contains index of the next substring to
        be considered to compare with the present
        substring, i.e., next string in suffix array */
        int j = suffixArr[invSuff[i]+1];

        // Directly start matching from k'th index as
        // at-least k-1 characters will match
        while (i+k<n && j+k<n && txt[i+k]==txt[j+k])
            k++;

        lcp[invSuff[i]] = k; // lcp for the present suffix.

        // Deleting the starting character from the string.
        if (k>0)
            k--;
    }

    // return the constructed lcp array
    return lcp;
}

// Utility function to print an array
void printArr(vector<int>arr, int n)
{
    for (int i = 0; i < n; i++)
        cout << arr[i] << " ";
    cout << endl;
}

// Driver program
int main()
{
    int t;
    cin >> t;
    //t = 1;

    while (t > 0)  {

        //string str = "banana";
        string str;

        cin >> str; // >> k;

        vector<int>suffixArr = buildSuffixArray(str, str.length());
        int n = suffixArr.size();

        cout << "Suffix Array : \n";
        printArr(suffixArr, n);

        vector<int>lcp = kasai(str, suffixArr);

        cout << "\nLCP Array : \n";
        printArr(lcp, n);

        // cum will hold number of substrings if that'a what you want (total = cum[n-1]
        cum[0] = n - suffixArr[0];

    //  vector <pair<int,int>> substrs[n];

        int count = 1;


        for (int i = 1; i <= n-suffixArr[0]; i++)  {
            //substrs[0].push_back({suffixArr[0],i});
            string sub_str = str.substr(suffixArr[0],i);
            cout << count << "  "  <<  sub_str << endl;
            count++;
        }

        for(int i = 1;i < n;i++)  {

            cum[i] = cum[i-1] + (n - suffixArr[i] - lcp[i - 1]);

            int end = n - suffixArr[i];
            int begin = lcp[i-1] + 1;
            int begin_suffix = suffixArr[i];

            for (int j = begin, k = 1; j <= end; j++, k++)  {

                //substrs[i].push_back({begin_suffix, lcp[i-1] + k});
        //      cout << "i push "  << i  << "   " << begin_suffix << " " << k << endl;
                string sub_str = str.substr(begin_suffix, lcp[i-1] +k);
                cout << count << "  "  <<  sub_str << endl;
                count++;

            }

        }

        /*int count = 1;

        cout << endl;

        for(int i = 0; i < n; i++){

            for (auto it = substrs[i].begin(); it != substrs[i].end(); ++it )    {

                string sub_str = str.substr(it->first, it->second);
                cout << count << "  "  <<  sub_str << endl;
                count++;
            }

        }*/


        t--;

    }

    return 0;
}

这里有一个更简单的算法:

#include <iostream>
#include <string.h>
#include <vector>
#include <string>
#include <algorithm>
#include <time.h>

using namespace std;


char txt[100000], *p[100000];
int m, n;

int cmp(const void *p, const void *q) {
    int rc = memcmp(*(char **)p, *(char **)q, m);
    return rc;
}
int main() {
    std::cin >> txt;
    int start_s = clock();


    n = strlen(txt);
    int k; int i;
    int count = 1;
    for (m = 1; m <= n; m++) {
        for (k = 0; k+m <= n; k++)
            p[k] = txt+k;
        qsort(p, k, sizeof(p[0]), &cmp);
        for (i = 0; i < k; i++) {
            if (i != 0 && cmp(&p[i-1], &p[i]) == 0){
                continue;
            }
            char cur_txt[100000];
            memcpy(cur_txt, p[i],m);
            cur_txt[m] = '\0';
            std::cout << count << "  "  << cur_txt << std::endl;
            count++;
        }
    }

    cout << --count  << endl;

    int stop_s = clock();
    float run_time = (stop_s - start_s) / double(CLOCKS_PER_SEC);
    cout << endl << "distinct substrings  \t\tExecution time = " << run_time << " seconds" << endl;
    return 0;
}

然而,对于非常长的字符串来说,这两种算法都太慢了。我测试了长度超过47,000的字符串,这两种算法需要超过20分钟才能完成计算,第一种算法需要1200秒,第二种算法需要1360秒,这还只是计算独特子串的数量,没有输出到终端。因此,对于长度不超过1000的字符串,你可能会得到一个可行的解决方案。尽管如此,这两种解决方案计算出的独特子串总数是相同的。我也测试了这两种算法对长度为2000和10000的字符串的计算时间。第一种算法的时间分别为0.33秒和12秒;第二种算法的时间分别为0.535秒和20秒。所以总体来看,第一种算法更快。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接