views:

333

answers:

9

Hi! I have a string s and I want to search the most prevalent substring of length X in s, overlap are admited. For example: s="aoaoa" and X=3, return "aoa" (which appear 2 times in s).

Is it an algorithm that founds this substring in O(n)?

+1  A: 

It should be O(n*m) where m is the average length of a string in the list. For very small values of m then the algorithm will approach O(n)

  • Build a hashtable of counts for each string length
  • Iterate over your collection of strings, updating the hashtable accordingly, storing the current most prevelant number as an integer variable separate from the hashtable
  • done.
Chris Ballance
Given that there are /(n*(n-1)/ substrings of a string of length /n/, I don't see how your roughly-described algorithm can be O(n) -- your iteration step should be at least O(N^2)
Ian Clelland
@Ian Clelland: `X` is fixed, isn't it?
J.F. Sebastian
Absolutely right -- I missed that in the problem description.
Ian Clelland
'X' is fixed, one iteration = O(n)
Chris Ballance
+1 for providing guidance rather than answers.
Erik Forbes
It's still not really O(N) -- At each of the N positions, you need to look at the next M characters (e.g. produce a hash of M characters), so the complexity is O(NM). Treating it as O(N) simply means you're assuming M is so small that its contribution is negligible.
Jerry Coffin
@Jerry, good point, answer updated accordingly
Chris Ballance
I think this answer would be a lot better if it included some pseudocode.
Joren
@Chris: I should add, however, that I think O(NM) is probably as good as anybody can do. Given N characters, you have to look at all N-M possible substrings, and to decide whether two substrings are equal, you have to compare all M characters of those strings.
Jerry Coffin
@Jerry Coffin: there are sub-linear algorithms for searching substrings e.g., [pdf] http://alexandria.tue.nl/extra1/wskrap/publichtml/200407.pdf
J.F. Sebastian
Yes, searching for a substring can be sublinear -- but in this case you can't use that, because you're interested in *every* possibly substring, you just have to determine which one you're dealing with at each step in the larger string.
Jerry Coffin
A: 

I don't immediately see a way to do this in O(n).

A: 

Naive solution in Python

from collections import defaultdict
from operator    import itemgetter

def naive(s, X):
    freq = defaultdict(int)
    for i in range(len(s) - X + 1):
        freq[s[i:i+X]] += 1
    return max(freq.iteritems(), key=itemgetter(1))

print naive("aoaoa", 3)
# -> ('aoa', 2)

In plain English

  1. Create mapping: substring of length X -> how many times it occurs in the s string

    for i in range(len(s) - X + 1):
        freq[s[i:i+X]] += 1
    
  2. Find a pair in the mapping with the largest second item (frequency)

    max(freq.iteritems(), key=itemgetter(1))
    
J.F. Sebastian
complexity of freq[s[i:i+X]] is O(n)...This is a O(n^2) algorithm
qjkx
Looks like a homework question, which is why I did not provide a code example.
Chris Ballance
@qjkx: complexity of `freq[s[i:i+X]] is O(X)... This is a O(n*X) algorithm.
J.F. Sebastian
+2  A: 

I don't see an easy way to do this in strictly O(n) time, unless X is fixed and can be considered a constant. If X is a parameter to the algorithm, then most simple ways of doing this will actually be O(n*X), as you will need to do comparison operations, string copies, hashes, etc., on a substring of length X at every iteration.

(I'm imagining, for a minute, that s is a multi-gigabyte string, and that X is some number over a million, and not seeing any simple ways of doing string comparison, or hashing substrings of length X, that are O(1), and not dependent on the size of X)

It might be possible to avoid string copies during scanning, by leaving everything in place, and to avoid re-hashing the entire substring -- perhaps by using an incremental hash algorithm where you can add a byte at a time, and remove the oldest byte -- but I don't know of any such algorithms that wouldn't result in huge numbers of collisions that would need to be filtered out with an expensive post-processing step.

Update

Keith Randall points out that this kind of hash is known as a rolling hash. It still remains, though, that you would have to store the starting string position for each match in your hash table, and then verify after scanning the string that all of your matches were true. You would need to sort the hashtable, which could contain n-X entries, based on the number of matches found for each hash key, and verify each result -- probably not doable in O(n).

Ian Clelland
A: 

There's no way to do this in O(n).

Feel free to downvote me if you can prove me wrong on this one, but I've got nothing.

Dean J
+5  A: 

You can do this using a rolling hash in O(n) time (assuming good hash distribution). A simple rolling hash would be the xor of the characters in the string, you can compute it incrementally from the previous substring hash using just 2 xors. (See the Wikipedia entry for better rolling hashes than xor.) Compute the hash of your n-x+1 substrings using the rolling hash in O(n) time. If there were no collisions, the answer is clear - if collisions happen, you'll need to do more work. My brain hurts trying to figure out if that can all be resolved in O(n) time.

Update:

Here's a randomized O(n) algorithm. You can find the top hash in O(n) time by scanning the hashtable (keeping it simple, assume no ties). Find one X-length string with that hash (keep a record in the hashtable, or just redo the rolling hash). Then use an O(n) string searching algorithm to find all occurrences of that string in s. If you find the same number of occurrences as you recorded in the hashtable, you're done.

If not, that means you have a hash collision. Pick a new random hash function and try again. If your hash function has log(n)+1 bits and is pairwise independent [Prob(h(s) == h(t)) < 1/2^{n+1} if s != t], then the probability that the most frequent x-length substring in s hash a collision with the <=n other length x substrings of s is at most 1/2. So if there is a collision, pick a new random hash function and retry, you will need only a constant number of tries before you succeed.

Now we only need a randomized pairwise independent rolling hash algorithm.

Update2:

Actually, you need 2log(n) bits of hash to avoid all (n choose 2) collisions because any collision may hide the right answer. Still doable, and it looks like hashing by general polynomial division should do the trick.

Keith Randall
That's the kind of hash I was thinking of -- thanks for the link. For more brain hurting: You have to assume that collisions happened, and check each of them manually, so now you're at O(n + X*(number of possible collisions)) which may or may not be more than O(n)
Ian Clelland
A: 

LZW algorithm does this

This is exactly what Lempel-Ziv-Welch (LZW used in GIF image format) compression algorithm does. It finds prevalent repeated bytes and changes them for something short.

LZW on Wikipedia

Robert Koritnik
I believe LZW is O(n) to decode, but slower than that on the encode, which is what the OP was looking for.
Dean J
LZW itself is yes because it looks for all length repeated substrings. I was just saying that he could use a similar principle but looking for fixed length strings.
Robert Koritnik
A: 

Here is a version I did in C. Hope that it helps.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    char *string = NULL, *maxstring = NULL, *tmpstr = NULL, *tmpstr2 = NULL;
    unsigned int n = 0, i = 0, j = 0, matchcount = 0, maxcount = 0;

    string = "aoaoa";
    n = 3;

    for (i = 0; i <= (strlen(string) - n); i++) {
        tmpstr = (char *)malloc(n + 1);
        strncpy(tmpstr, string + i, n);
        *(tmpstr + (n + 1)) = '\0';
        for (j = 0; j <= (strlen(string) - n); j++) {
            tmpstr2 = (char *)malloc(n + 1);
            strncpy(tmpstr2, string + j, n);
            *(tmpstr2 + (n + 1)) = '\0';
            if (!strcmp(tmpstr, tmpstr2))
                matchcount++;
        }
        if (matchcount > maxcount) {
            maxstring = tmpstr;
            maxcount = matchcount;
        }
        matchcount = 0;
    }

    printf("max string: \"%s\", count: %d\n", maxstring, maxcount);

    free(tmpstr);
    free(tmpstr2);

    return 0;
}
John Scipione
A: 

You can build a tree of sub-strings. The idea is to organise your sub-strings like a telephone book. You then look up the sub-string and increase its count by one.

In your example above, the tree will have sections (nodes) starting with the letters: 'a' and 'o'. 'a' appears three times and 'o' appears twice. So those nodes will have a count of 3 and 2 respectively.

Next, under the 'a' node a sub-node of 'o' will appear corresponding to the sub-string 'ao'. This appears twice. Under the 'o' node 'a' also appears twice.

We carry on in this fashion until we reach the end of the string.

A representation of the tree for 'abac' might be (nodes on the same level are separated by a comma, sub-nodes are in brackets, counts appear after the colon).

a:2(b:1(a:1(c:1())),c:1()),b:1(a:1(c:1())),c:1()

If the tree is drawn out it will be a lot more obvious! What this all says for example is that the string 'aba' appears once, or the string 'a' appears twice etc. But, storage is greatly reduced and more importantly retrieval is greatly speeded up (compare this to keeping a list of sub-strings).

To find out which sub-string is most repeated, do a depth first search of the tree, every time a leaf node is reached, note the count, and keep a track of the highest one.

The running time is probably something like O(log(n)) not sure, but certainly better than O(n^2).

Jonathan Swift