2
$\begingroup$

Let's say that we have a string "1.2.3" and want to find a match for it in the following records:

1.2.9
1.4.5
1.*.3

For each record:

  • The . is a separator that divides the string into segments.
  • Each segment can have a static value or * that matches any segment value.
  • There are no duplicated records. If there are multiple matches for a given string, only one is returned, following an exact match and left to right rule. So, for the following hypothetical matches for 1.2.3, the previous has priority over the latter: 1.2.3 > 1.2.* > 1.*.3 > *.2.3.

In the initial example, the matching record will be "1.*.3".

Note: here, the numbers used to represent a record are a simplification of a more complex, unpredictable, variable-sized value, such as abc.wxyz.fghijklm. So it is not possible to build tables for possible wildcard values.

I'm looking for the (theoretically) fastest lookup algorithm to find a matching record in memory given these non-requirements:

  • Longest prefix matches are not a requirement;
  • Only lookup speed matters: record insertion or deletion are not required to be fast;
  • Memory space efficiency is not a requirement.

I experimented a range of trie variants and I am guessing (sorry if this sounds naive) that tries, or any datastructure, can't find a match for the proposed problem without backtracking. I'd appreciate some comments on this.

So, which datastructure would best fit my (non-)criteria?

$\endgroup$
  • $\begingroup$ Does every string contain exactly 3 segments? Do you have an upper bound on the number of segments per string, or the number of wildcards? $\endgroup$ – D.W. Nov 6 '15 at 17:46
  • $\begingroup$ @D.W., the number of segments is not fixed and vary from 1 to 5, maybe more but not much more. There's no upper bound on the number of wildcards or segments, but we could limit that. $\endgroup$ – moraes Nov 6 '15 at 19:14
  • 1
    $\begingroup$ Do you just need to test for membership, or is there a payload associated with each record? If there are multiple matches (e.g. 1.2.3, 1.*.3 and *.2.3 for 1.2.3), do you want to return all of them, any of them, one of them with a particular disambiguation rule? $\endgroup$ – Gilles Nov 7 '15 at 12:22
  • $\begingroup$ @Gilles If there are multiple matches, only one is returned, following a exact match and left to right rule: so 1.2.3 > 1.2.* > 1.*.3 > *.2.3. There are no duplicated records. $\endgroup$ – moraes Nov 7 '15 at 16:01
2
$\begingroup$

One candidate approach would be build a BDD or a deterministic finite-state automaton that accepts all strings that match at least one of the records in your set of records. These data structures can handle wildcards. To use a BDD, treat each segment as a bit string, so the alphabet is $\{0,1,.\}$ (perhaps it's now a trinary decision diagram rather than a binary decision diagram, but the ideas remain the same).


An alternative approach (if the number of segments per string will always be relatively small): You can solve your problem by building a set of indices, one index for each subset of possible segments.

Suppose we're guaranteed that every string will have exactly 3 segments, call them segments ABC. We're now going to build seven indices:

  • The ABC index contains the set of all records with no wildcard. e.g., 1.2.3
  • The AB index contains the set of all records with a single wildcard, in the 3rd (C) segment. e.g., 1.2.*
  • The AC index contains the set of all records with a single wildcard, in the 2nd (B) segment. e.g., 1.*.3
  • The BC index contains the set of all records with a single wildcard, in the 1st (A) segment. e.g., *.2.3
  • The A index contains the set of all records with two wildcards, in the 2nd and 3rd (B and C) segments. e.g., 1.*.*
  • The B index contains the set of all records with two wildcards, in the 1st and 3rd (A and C) segments. e.g., *.2.*
  • The C index contains the set of all records with two wildcards, in the 1st and 2nd (A and B) segments. e.g., *.*.3
  • I assume you won't have any records with three wildcards (*.*.*), but if that was possible, you'd have an 8th index that records whether you have such a record or not.

Each index is stored as a hash table, so given a string you can quickly (in $O(1)$ time) check whether that string is present in that index.

Now, when you receive a string 4.5.6, you look it up in each index. For instance, you look up 4.5.6 in the ABC index, 4.5.* in the AB index, 4.*.6 in the AC index, and so on. If any of these lookups succeeds, then you know that 4.5.6 matches one of the records. Otherwise, 4.5.6 does not match any of the records. The running time for a lookup is $O(1)$.

If you know that every string will have exactly 5 segments, you'll need 31 indices and the time for a lookup is $O(1)$ (31 lookups into 31 hashtables). If every string has exactly 5 segments and you know that no record will have more than 2 wildcards, then it's enough to have $1+5+10=16$ indices.

This will probably be OK for your particular parameters, where every string has at most 5 segments. Obviously, though, if the number of segments is too large, this will be extremely slow and inefficient.

$\endgroup$
  • $\begingroup$ Thanks for the insights and sorry for my lacking description of the problem. I updated the question to say that the numbers used to represent a record are a simplification of a more complex, unpredictable, variable-sized value, such as abc.wxyz.fghijklm. That is, there's no way to build tables with possible values for wildcards. $\endgroup$ – moraes Nov 8 '15 at 1:54
  • $\begingroup$ @moraes, I think it's possible you might have misunderstood my solution. You can still build the tables. When inserting a record into a hash table, you just treat the * as a single character, not as a special wildcard. (Or, if you prefer: remove the *, leaving just that segment blank.) $\endgroup$ – D.W. Nov 8 '15 at 3:16
1
$\begingroup$

Edit: I was short on time earlier, but I'll expand my reasoning a bit:

Assumptions:

  1. No bound on insertion time or memory size.

  2. Lookup speed should be minimal, ie O(query length) which is the time to read the input.

  3. Backtracking during search or any extra search time beyond O(length) would violate #2.

Given that there are no constraints on memory or insertion time, one solution is to enumerate each wildcard during insertion, with backtracking, and insert each key generated in a trie or hash table.

So for 1.*.3 above you generate 1.0.3, 1.1.3, ... and insert them into the index. For multiple wildcards this obviously becomes large, but it guarantees that any match will be found as-is in the index.

This idea simply moves any backtracking to the insertion phase, and guarantees optimal search times.

Rivest also gives some ways to bound this process if #1 is not actually true; his approach in this case would be to reduce the number of keys in the index by allowing some wildcards, but only up to a fixed number k, so that the amount of backtracking during search is O(1).

A quick example:

Say k=1 but we're inserting a key $2.*.3.*$; we generate all keys

2.[0-9].3.* = 2.0.3.*, 2.1.3.*, etc.
2.*.3.[0-9]

And insert them in the index. Then at search time, if we have a key

2.1.3.1

We generate and search for all 1-wildcard variants:

*.1.3.1
2.*.3.1  (match)
2.1.*.1
2.1.3.*  (match)

and probe for those exact strings; the number of variants is bounded by our choice of k (and the key length).

=== original:

Your situation is a bit reversed from the usual partial match problem, where the wildcards appear in the queries, but the data is fixed.

If the number of wildcards is bounded, you can expand the wildcards on insertion to get o(length) lookup time.

$\endgroup$
  • $\begingroup$ I realized that my question was poorly described when I saw your answer and D.W.'s. Thanks for the insights, though. I updated the question to make it more clear that there's no way to build tables with possible values for wildcards, because actual records have segments with unpredictable, variable-sized value, such as abc.wxyz.fghijklm. $\endgroup$ – moraes Nov 8 '15 at 1:57
  • $\begingroup$ Is the number of wildcard substitutions unbounded? I can see that would be a problem. I assumed you had a reasonably small alphabet to work through. $\endgroup$ – KWillets Nov 8 '15 at 17:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.