Rearrange Letters from Array and check if arrangement is in array

家住魔仙堡 提交于 2019-11-30 16:12:08

Let me propose a different algorithm that depends on a lookup, not a search through an array.

Setup:

Iterate over the words in the dictionary. For each word, create a string with the same characters, sorted alphabetically. Using this string as a key, create a dictionary of arrays of the original words.

Usage:

Now You can do the check on any character combination very quickly: Just sort the characters like above and look the resulting key up in the map.

Example:

original array: ( bond, Mary, army )

anagram lookup map:

{
   bdno : ( bond ),
   amry : ( Mary, army ),
}

Using this map it's very fast to check anagrams of any word. No iteration over the dictionary array is needed.

Edit:

My proposed algorithm splits in three parts:

  1. A setup method to build the lookup map from a dictionary of objects: anagramMap
  2. A method to calculate the character-by-character sorted key: anagramKey
  3. An algorithm that finds all permutations of the characters contained in the nine letter word and looks up the words in the map: findAnagrams.

Here's an implementation of all three methods as a category on NSString:

@interface NSString (NSStringAnagramAdditions)
- (NSSet *)findAnagrams;
@end

@implementation NSString (NSStringAnagramAdditions)

+ (NSDictionary *)anagramMap
{
    static NSDictionary *anagramMap;
    if (anagramMap != nil)
        return anagramMap;

    // this file is present on Mac OS and other unix variants
    NSString *allWords = [NSString stringWithContentsOfFile:@"/usr/share/dict/words"
                                                   encoding:NSUTF8StringEncoding
                                                      error:NULL];

    NSMutableDictionary *map = [NSMutableDictionary dictionary];
    @autoreleasepool {
        [allWords enumerateLinesUsingBlock:^(NSString *word, BOOL *stop) {
            NSString *key = [word anagramKey];
            if (key == nil)
                return;
            NSMutableArray *keyWords = [map objectForKey:key];
            if (keyWords == nil) {
                keyWords = [NSMutableArray array];
                [map setObject:keyWords forKey:key];
            }
            [keyWords addObject:word];
        }];
    }

    anagramMap = map;
    return anagramMap;
}

- (NSString *)anagramKey
{
    NSString *lowercaseWord = [self lowercaseString];

    // make sure to take the length *after* lowercase. it might change!
    NSUInteger length = [lowercaseWord length];

    // in this case we're only interested in anagrams 4 - 9 characters long
    if (length < 4 || length > 9)
        return nil;

    unichar sortedWord[length];
    [lowercaseWord getCharacters:sortedWord range:(NSRange){0, length}];

    qsort_b(sortedWord, length, sizeof(unichar), ^int(const void *aPtr, const void *bPtr) {
        int a = *(const unichar *)aPtr;
        int b = *(const unichar *)bPtr;
        return b - a;
    });

    return [NSString stringWithCharacters:sortedWord length:length];
}

- (NSSet *)findAnagrams
{
    unichar nineCharacters[9];
    NSString *anagramKey = [self anagramKey];

    // make sure this word is not too long/short.
    if (anagramKey == nil)
        return nil;
    [anagramKey getCharacters:nineCharacters range:(NSRange){0, 9}];
    NSUInteger middleCharPos = [anagramKey rangeOfString:[self substringWithRange:(NSRange){4, 1}]].location;

    NSMutableSet *anagrams = [NSMutableSet set];

    // 0x1ff means first 9 bits set: one for each character
    for (NSUInteger i = 0; i <= 0x1ff; i += 1) {

        // skip permutations that do not contain the middle letter
        if ((i & (1 << middleCharPos)) == 0)
            continue;

        NSUInteger length = 0;
        unichar permutation[9];
        for (int bit = 0; bit <= 9; bit += 1) {
            if (i & (1 << bit)) {
                permutation[length] = nineCharacters[bit];
                length += 1;
            }
        }

        if (length < 4)
            continue;

        NSString *permutationString = [NSString stringWithCharacters:permutation length:length];
        NSArray *matchingAnagrams = [[self class] anagramMap][permutationString];

        for (NSString *word in matchingAnagrams)
            [anagrams addObject:word];
    }

    return anagrams;
}

@end

Assuming a test string in a variable called nineletters you would log the possible values using:

for (NSString *anagram in [nineletters findAnagrams])
    NSLog(@"%@", anagram);
Martin R

First you need a method to check if one word is an anagram of a second word. There are many possible solutions (search for "Objective-C anagram"). This is essentially the method from https://stackoverflow.com/a/13465672/1187415, written slightly differently:

- (BOOL)does:(NSString*)longWord contain:(NSString *)shortWord
{
    NSMutableString *longer = [longWord mutableCopy];
    __block BOOL retVal = YES;
    // Loop over all characters (letters) in shortWord:
    [shortWord enumerateSubstringsInRange:NSMakeRange(0, [shortWord length])
                                  options:NSStringEnumerationByComposedCharacterSequences
                               usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
        // Check if letter occurs in longer word:
        NSRange letterRange = [longer rangeOfString:substring];
        if (letterRange.location != NSNotFound) {
            // Yes. Remove from longer word and continue.
            [longer deleteCharactersInRange:letterRange];
        } else {
            // No. Set return value to NO and quit the loop.
            retVal = NO;
            *stop = YES;
        }
    }];
    return retVal;
}

Examples:

  • [self does:@"abandoned" contain:@"bond"] = YES
  • [self does:@"abandoned" contain:@"sea"] = NO, because there is no "s" in the first word.
  • [self does:@"abandoned" contain:@"noon"] = NO, because "noon" has 2 letters "o", but the first word has only one "o".

Then you can proceed as follows:

NSArray *englishWords = ...; // Your array of english words

NSString *inputWord = @"abandoned"; // The input string
NSString *middleLetter = [inputWord substringWithRange:NSMakeRange([inputWord length]/2, 1)];

NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(NSString *word, NSDictionary *bindings) {
    // Word must have at least 4 letters:
    if ([word length] < 4)
        return NO;
    // Word must contain the middle letter:
    if ([word rangeOfString:middleLetter].location == NSNotFound)
        return NO;
    // Word must contain only letters of the input word:
    if (![self does:inputWord contain:word])
        return NO;
    return YES;
}];
NSArray *matchingWords = [englishWords filteredArrayUsingPredicate:predicate];
NSLog(@"%@", matchingWords);
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!