April01xxx

导航

Substring with Concatenation of All Words

1.题目

You are given a string, s, and a list of words, words, that are all of the same length. Find all starting indices of substring(s) in s that is a concatenation of each word in words exactly once and without any intervening characters.
Example 1:
Input:
 s = "barfoothefoobarman",
 words = ["foo","bar"]
Output: [0,9]
Explanation: Substrings starting at index 0 and 9 are "barfoor" and "foobar" respectively.
The output order does not matter, returning [9,0] is fine too.
Example 2:
Input:
 s = "wordgoodstudentgoodword",
 words = ["word","student"]
Output: []

  题目的大意是给定一个字符串s和一个子串的数组words,要求在s找出这样一个子串,它满足以下特点:
  1. 由words中的所有子串组成,每个子串恰好出现一次,顺序无关;
  2. 每个子串之间不能交叉重叠,必须紧挨着;

2.思路

  第一想法是找出words中每个子串出现的下标位置,然后判断下标能否组合为一个有效的匹配.以题目中给的第一个例子来说明:

char s[] = "barfoothefoobarman";
char *words[] = {
  "foo",
  "bar"
};
"foo": [3, 9];
"bar": [0, 12];

接着对每个子串的下标进行排列组合,得到[3, 0], [3, 12], [9, 0], [9, 12]四种组合,判断每个组合是否一个有效的匹配.显然这是暴力破解了.假设字符串s的长度为M,words中有N个子串,每个子串的长度为L,则整个算法的时间复杂度是O(NM),这还是忽略了最后进行排列组合的时间的结果.
  很容易注意到,只要将字符串s完整遍历一趟实际上就能找到words中所有子串出现的下标信息,我们需要将这些信息保存起来便于后续的查找,适合的数据结构是hash表.那么接下来的问题是如何存储信息?是子串的起始下标还是整个字符串?
  假设存储子串的起始下标,我们后续在判断是否找到一个完整的匹配时无法甄别每个子串是否恰好出现一次,所以我们还需要能存储标识子串是哪个子串的信息.一个自然的想法是将整个子串和下标一起保存,但我们可以做的更好:存储子串的特征信息,这样在后续进行子串比较时只需要比较其特征信息即可,避免了逐个字符比较的情况.如何找到这个特征信息使其具有唯一性呢?这个问题实际就是找到一个hash函数,使其对于不同的子串,得到不同的输出.
  根据题设,在字符串中可能出现的字符是[a-zA-Z],共52个字符,我们对其进行编号:[a-z]映射到2*i+1,[A-Z]映射到2*(i+1),其中i取值[0, 25].有了这个基础,我们就可以计算一个字符串的唯一标识.下面通过一个例子来说明:

indices: 0 1 2 3
string:  a b c d
No:      1 3 5 7
hash = 52^3 * 1 + 52^2 * 3 + 52^1 * 5 + 52^0 * 7

这样就得到了一个字符串的整数标识,为了加快这个计算的过程,可以选择每次都乘以64,这样可以使用位移运算符替代乘法运算.
3.实现

  下面给出具体的实现,我们用一个与字符串s长度一致的数组来保存每个下标处的子串匹配信息,若在该位置成功匹配到一个子串,则记录其hash值,否则记录为0.

/**
 * 计算一个字符串的唯一hash值.
 */
unsigned int
hash(char *s) {
  unsigned int h = 0;
  size_t i;

  for (i = 0; s[i] != 0; ++i) {
    if ('a' <= s[i] && s[i] <= 'z')
      h = (h << 6) + 2 * (s[i] - 'a' + 1) - 1;
    else if ('A' <= s[i] && s[i] <= 'Z')
      h = (h << 6) + 2 * (s[i] - 'A' + 1);
  }
  return h;
}

int *
findSubstring(char *s, char **words, int wordsSize, int *returnSize) {
  int i, j;
  int slen, wordlen;
  unsigned int sum = 0, check;
  unsigned int *h, *match;
  int *ret;

  *returnSize = 0;

  slen = strlen(s);
  wordlen = strlen(*words);
  if (wordsSize == 0 || slen < wordlen)
    return NULL;

  /* 计算每个子串的hash值,并将所有子串的hash之和保存. */
  h = (unsigned int *) malloc(wordsSize * sizeof(unsigned int));
  for (i = 0; i < wordsSize; ++i) {
    h[i] = hash(words[i]);
    sum += h[i];
  }

  match = (unsigned int *) calloc(slen, sizeof(unsigned int));
  /* 遍历s,并记录每个下标处的匹配信息. */
  for (i = 0; i <= slen - wordlen; ++i) {
    for (j = 0; j < wordsSize; ++j) {
      if (strncmp(s + i, words[j], wordlen) == 0) {
        /* 成功匹配一个子串,记录子串的hash值,i就是子串的起始下标. */
        match[i] = h[j];
        break;
      }
    }
  }

  /* 遍历匹配信息match数组.
   * 若下标i处match[i] > 0,说明从此处开始匹配了一个子串,接着取
   * match[i + wordlen]处的值,若也大于0,则说明连续匹配继续取下
   * 一个wordlen处的值,否则说明不是连续匹配,下标i递增.何时停止
   * 循环呢?注意到所有子串的总长度为wordsSize * wordlen,故i的
   * 最大取值为slen - wordsSize * wordlen.
   */
  ret = (int *) malloc(slen * sizeof(int));
  for (i = 0; i <= slen - wordsSize * wordlen; ++i) {
    if (match[i] == 0)
      continue;
    check = 0;
    for (j = 0; j < wordsSize && match[i + j * wordlen] != 0; ++j)
      check += match[i + j * wordlen];
    if (j == wordsSize && check == sum)
      ret[(*returnSize)++] = i;
  }

  free(h);
  free(match);

  return ret;
}

posted on 2018-07-22 17:06  April01xxx  阅读(92)  评论(0编辑  收藏  举报