【问题标题】:Generate all unique substrings for given string为给定字符串生成所有唯一子字符串
【发布时间】:2011-02-03 08:29:04
【问题描述】:

给定一个字符串s,生成一组所有唯一子字符串的最快方法是什么?

例如:对于str = "aba",我们会得到substrs={"a", "b", "ab", "ba", "aba"}

简单的算法是遍历整个字符串,在每次迭代中生成长度为1..n 的子字符串,产生一个O(n^2) 上限。

有更好的界限吗?

(这在技术上是家庭作业,因此也欢迎指点)

【问题讨论】:

  • @Yuval..你得到高效的算法了吗?如果有请分享。 TIA。
  • 我真的不记得发生了什么。但很可能我最终实现了某种后缀树。没有代码了,抱歉。
  • 所提出的算法不会在 O(n²) 时间内运行!

标签: algorithm language-agnostic


【解决方案1】:

正如其他发帖者所说,给定字符串可能存在 O(n^2) 个子字符串,因此打印出来的速度不会比这更快。但是,存在可以在线性时间内构建的集合的有效表示:the suffix tree

【讨论】:

  • 这是 O(n+L) 其中 L 是唯一子串的数量,我相信。所以它是最优的。
【解决方案2】:

没有办法比 O(n2) 更快,因为字符串中总共有 O(n2) 个子字符串,所以如果你必须生成它们all,在最坏的情况下它们的数量将是n(n + 1) / 2,因此 O(n2upper 的下限sup>) Ω(n2).

【讨论】:

  • 我们希望有一个输出敏感的算法,运行在 O(# of unique strings)...
  • 您的意思是 Ω(n^2) 的下限。 :)
  • 后缀树在 O(n + L) 时间内完成,其中 L 是唯一子串的数量。对于像 'aaaaa....aaaaa' 这样的字符串,L = O(n)。所以关于 Omega(n^2) 的说法是不正确的。
  • @Moron - 我很好奇后缀树如何在 O(n + L) 中解决这个问题。介意发布算法吗?
  • @IVlad:只需遍历整个后缀树并打印路径/子路径即可。那不是O(L)吗?当然,这假设打印字符串是 O(1)(例如,我们只打印开始和结束索引)。如果我们考虑打印字符串 x 花费 O(|x|) 时间,那么是的,它是 Omega(n^2)。您认为您的帖子并不清楚,我猜您的帖子实际上暗示了该案例的 Omega(n^3) 下限。
【解决方案3】:

第一个是蛮力,其复杂度为 O(N^3),可以降低到 O(N^2 log(N)) 第二个使用复杂度为 O(N^2) 的 HashSet 第三个使用 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)
  • 好的。您已经计算了所有唯一的子字符串。伟大的。但是,您将如何使用后缀数组/最长公共前缀数组“为给定字符串生成所有唯一子字符串”?
【解决方案4】:

对于大哦...你能做的最好是 O(n^2)

无需重新发明轮子,它不是基于字符串,而是基于集合,因此您必须将这些概念应用到您自己的情况中。

算法

Really Good White Paper from MS

In depth PowerPoint

Blog on string perms

【讨论】:

  • 错误,什么? O(a^n)?什么是 a,你从哪里得出这个结果?
  • 对不起,符号定义了随着时间的推移而增长......我切换回来了
  • 使用 LCP(longest common suffix) 可以将最佳时间拉低到 O(NLog N)
  • @SumitKumarSaha 你的意思是最长的公共前缀
  • @goelakash 是前缀,我无法编辑我之前的评论
【解决方案5】:

好吧,因为可能有 n*(n+1)/2 不同的子字符串(空子字符串 +1),我怀疑你会比 O(n*2) 更好(最坏的情况)。最简单的方法是生成它们并使用一些不错的O(1) 查找表(例如哈希图)在找到它们时立即排除重复项。

【讨论】:

  • 它是n(n+1)/2。 “abc”有 3*4/2 = 6 个子字符串(“a”、“b”、“c”、“ab”、“bc”、“abc”)而不是 3*2/2 = 3 个子字符串。
  • 请注意,哈希表使用哈希码和等号,字符串长度为 O(n)。
  • 哦,是的,对不起...会解决这个问题... :)
  • @fgb 使用滚动哈希,例如,您可以获得 O(1) 查找。
【解决方案6】:
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);
            }
        }
    }
}

【讨论】:

    【解决方案7】:
    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());
                }
            }
    }
    

    【讨论】:

      【解决方案8】:

      这会打印唯一的子字符串。 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']
      

      【讨论】:

        【解决方案9】:

        包括 2 个 for 循环和一个 .substring() 调用的许多答案声称 O(N^2) 时间复杂度。但是,重要的是要注意,Java 中的 .substring() 调用(Java 7 中的更新后 6)的最坏情况是 O(N)。因此,通过在代码中添加 .substring() 调用,N 的阶数增加了 1。

        因此,2 个 for 循环和这些循环中的 .substring() 调用等于 O(N^3) 时间复杂度。

        【讨论】:

          【解决方案10】:

          它只能在 o(n^2) 时间内完成,因为字符串的唯一子字符串的总数将是 n(n+1)/2。

          例子:

          字符串 s = "abcd"

          pass 0:(所有字符串的长度都是1)

          a, b, c, d = 4 个字符串

          pass 1:(所有字符串的长度都是2)

          ab, bc, cd = 3 个字符串

          pass 2:(所有字符串的长度都是3)

          abc, bcd = 2 个字符串

          pass 3:(所有字符串的长度都是4)

          abcd = 1 个字符串

          使用这个类比,我们可以写出时间复杂度为 o(n^2) 且空间复杂度为常数的解。

          源码如下:

          #include<stdio.h>
          
          void print(char arr[], int start, int end)
          {
              int i;
              for(i=start;i<=end;i++)
              {
                  printf("%c",arr[i]);
              }
              printf("\n");
          }
          
          
          void substrings(char arr[], int n)
          {
              int pass,j,start,end;
              int no_of_strings = n-1;
          
              for(pass=0;pass<n;pass++)
              {
                  start = 0;
                  end = start+pass;
                  for(j=no_of_strings;j>=0;j--)
                  {
                      print(arr,start, end);
                      start++;
                      end = start+pass;
                  }
                  no_of_strings--;
              }
          
          }
          
          int main()
          {   
              char str[] = "abcd";
              substrings(str,4);
              return 0;
          }
          

          【讨论】:

            【解决方案11】:

            这是我的 Python 代码。它生成任何给定字符串的所有可能的子字符串。

            def find_substring(str_in):
                substrs = []
                if len(str_in) <= 1:
                    return [str_in]
            
                s1 = find_substring(str_in[:1])
                s2 = find_substring(str_in[1:])
            
                substrs.append(s1)
                substrs.append(s2)
                for s11 in s1:
                    substrs.append(s11)            
                    for s21 in s2:            
                        substrs.append("%s%s" %(s11, s21))
            
                for s21 in s2:
                    substrs.append(s21)
            
                return set(substrs)
            

            如果将 str_ = "abcdef" 传递给函数,它会生成以下结果:

            a,ab,abc,abcd,abcde,abcdef,abcdf,abce,abcef,abcf,abd,abde,abdef,abdf,abe,abef,abf,ac,acd,acde,acdef,acdf,ace,acef , acf, ad, ade, adef, adf, ae, aef, af, b, bc, bcd, bcde, bcdef, bcdf, bce, bcef, bcf, bd, bde, bdef, bdf, be, bef, bf, c , cd, cde, cdef, cdf, ce, cef, cf, d, de, def, df, e, ef, f

            【讨论】:

              【解决方案12】:

              朴素算法需要 O(n^3) 时间而不是 O(n^2) 时间。 有 O(n^2) 个子串。 如果你放 O(n^2) 个子串,例如,设置, 然后 set 比较每个字符串的 O(lgn) 比较,以检查它是否已经存在于集合中。 此外,字符串比较需要 O(n) 时间。 因此,如果你使用 set,它需要 O(n^3 lgn) 时间。如果你使用 hashtable 而不是 set,你可以减少 O(n^3) 时间。

              重点是字符串比较而不是数字比较。

              因此,如果您使用后缀数组和最长公共前缀 (LCP) 算法,那么最好的算法之一可以减少此问题的 O(n^2) 时间。 使用 O(n) 时间算法构建后缀数组。 LCP 的时间 = O(n) 时间。 由于对于后缀数组中的每一对字符串,都进行 LCP,所以总时间是 O(n^2) 时间来找到不同子字符串的长度

              此外,如果您想打印所有不同的子字符串,则需要 O(n^2) 时间。

              【讨论】:

              • 存在由哈希表支持的集合,因此您对 O(log n) 查找集合的评论不正确。此外,如果存在哈希冲突,您只需进行字符串相等性检查。
              【解决方案13】:

              使用后缀数组和最长公共前缀尝试此代码。它还可以为您提供唯一子字符串的总数。该代码可能会在 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 和 10,000 的字符串长度测试了这两种算法。第一个算法的时间:0.33 s 和 12 s;对于第二种算法,它是 0.535 秒和 20 秒。所以看起来一般第一个算法更快。

              【讨论】:

                猜你喜欢
                • 2021-02-02
                • 1970-01-01
                • 2012-10-23
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多