找到包含给定单词的最短子串的方法:需要优化

时间:2013-05-03 21:57:10

标签: java algorithm

我有一个程序要求我找到给定String的最短子段,其中包含一个单词列表。即使我的程序是正确的,我也无法在执行的时间范围内(5秒)交付。我认为问题是由于我使用的复杂(平凡)算法。它由嵌套循环组成,需要多次扫描list_of_words数组。这是我的搜索功能代码。 a[]包含由单词存储的原始字符串,b[]包含要查找以形成子段的单词列表。 String g存储由原始字符串中的单词形成的临时子分段,包括列表中的单词。

private static void search() // Function to find the subsegment with a required list of words
{
   int tail,x;//counters 
   String c[]=new String[b.length]; //initializing a temporary array to copy the list of words array.

   for(int i =0; i<a.length;i++)// looping throw original string array
    {
       System.arraycopy(b, 0, c, 0, b.length);//copying the list of words array to the temporary array

        for (int j=0;j<b.length;j++)//looping throw the temporary array
        { 
            x=0; //counter for temporary array

            if(a[i].equalsIgnoreCase(b[j]))//checking for a match with list of words
            {
                tail=i;
//adds the words starting from the position of the first found word(tail) till all the words from the list are found
                while((x<b.length)&&(tail<a.length))

                {
                    g=g+" "+a[tail];//adds the words in the string g

                    for(int k=0;k<c.length;k++) //checks for the found word from the temporary array and replaces it with ""    
                    {
                        if(c[k].equalsIgnoreCase(a[tail]))
                        {
                            c[k]=""; x++;
                        }
                    }
                    tail++;

                }
                if((tail==a.length)&&(x!=b.length))//checks if the string g does not contains all the listed word
                {
                    g="";
                }
                else
                    {
                    count(g);g="";//check for the shortest string.
                    }
            }
        }

    }print();
}

示例:

原始字符串:这是一个测试。这是一个编程测试。这是一个编程测试。

要找到的词语:this,test,a,programming。

子分段:

这是一个测试这是一个编程

这是编程测试

编程测试这个编程测试

编程测试编程测试这个

测试编程测试

编程测试

最短子段:编程测试

有关数据结构或循环结构更改的任何建议,甚至优化相同的算法更改都会有所帮助。

8 个答案:

答案 0 :(得分:6)

动态规划解决方案:

为您要查找的每个单词设置最后一个位置变量。

拥有您正在寻找的截然不同的单词总数(永远不会减少,max =您正在寻找的单词数量)。

对于输入中的每个单词位置:

  • 如果您要查找的单词列表中存在该单词,请更新该单词的最后位置。
  • 如果未初始化更新的最后位置,则增加总计数。
  • 如果总计数等于最大值,则循环到最后位置并找到最小位置。当前位置与该值之间的距离将是子字符串的长度。记录这些值并在所有位置找到最佳值。

优化是为最后一个位置设置a heap以减少查找最小位置所需的时间(应与一些允许快速查找指针的结构(可能是哈希或树图)一起使用)给了一个单词堆。)

示例:

输入:This is a test. This is a programming test. a programming test this is

寻找:this, test, a, programming

                1    2  3  4     5    6  7  8           9     10 11          12   13   14
                This is a  test. This is a  programming test. a  programming test this is
this         -1 1    1  1  1     5    5  5  5           5     5  5           5    13   13
test         -1 -1   -1 -1 4     4    4  4  4           9     9  9           12   12   12
a            -1 -1   -1 3  3     3    3  7  7           7     10 10          10   10   10
programming  -1 -1   -1 -1 -1    -1   -1 -1 8           8     8  11          11   11   11
Count        0  1    1  2  3     3    3  3  4           4     4  4           4    4    4
Substr len   NA NA   NA NA NA    NA   NA NA 5           5     6  7           8    4    5
Shortest len NA NA   NA NA NA    NA   NA NA 5           5     5  5           5    4    4

最佳结果:a programming test this,长度= 4.

复杂性分析:

n为输入中的字数,k为我们要查找的字数。

该算法只传递一个输入,并且在每一步中,O(log k)都可以用于getMin操作(使用堆优化)。

因此,所花费的总时间为O(n log k)

处理重复项:

如果我们要查找的单词中允许重复(并且目标序列必须匹配所有出现的位置),上面的算法将无法正常工作,但一个简单的解决方法是让每个不同的单词都有自己的堆指向原始堆的指针(此堆中的值与原始堆中的值相同),此堆的最大大小是我们要查找的单词中该单词的出现次数。

答案 1 :(得分:4)

这是我发生的实现。

//Implementing here with two List<String>
//Should be easy enough to use arrays, or streams, or whatever.
public static int getShortestSubseqWith(List<String> text, List<String> words) {
    int minDistance = Integer.MAX_VALUE;
    //Create a map of the last known position of each word
    Map<String, Integer> map = new HashMap();
    for (String word : words) {
        map.put(word, -1);
    }
    String word;
    //One loop through the main search string
    for (int position = 0; position < text.size(); position++){
        word = text.get(position);
        //If the current word found is in the list we're looking for
        if (map.containsKey(word)) {
            //Update the map
            map.put(word, position);
            //And if the current positions are the closest seen so far, update the min value.
            int curDistance = getCurDistance(map);
            if (curDistance < minDistance)
                minDistance = curDistance;
        }
    }
    return minDistance;
}

//Get the current distance between the last known position of each value in the map
private static int getCurDistance(Map<String, Integer> map) {
    int min = Integer.MAX_VALUE;
    int max = 0;
    for (Integer value : map.values()) {
        if (value == -1)
            return Integer.MAX_VALUE;
        else {
            max = Math.max(max,value);
            min = Math.min(min,value);
        }
    }
    return max - min;
}

这里的主要性能影响,如果命中相对稀疏,并且搜索相对较小的术语列表应该只是要搜索的text上的循环。如果点击非常频繁,由于getCurDistance更频繁地运行,性能可能会受到影响。

答案 2 :(得分:2)

另一种方法可能是将b []中的每个单词映射到[]中的出现索引。

Map<Integer, List<Integer>> occurrence = new HashMap<Integer, List<Integer>>();
for(int idx = 0; idx < a.length; idx++)
{
  int bIdx = ... retrieve the index of the word a[idx] in b or -1 if it doesn't exist;

  if(bIdx >= 0)
  {
    List<Integer> bIdxOccurs = occurrence.get(bIdx);
    //some code to initially create the lists
    bIdxOccurs.add(idx);
  }
}

然后找到地图中每个单词的出现组合,其索引彼此最接近。天真的方法是生成每个组合并比较最小和最大索引之间的距离,但可能有更快的方法。我必须考虑一下......

最后,从[]中取出所有最短序列的最小和最大索引之间的单词。

答案 3 :(得分:1)

String[] a; // larger string
String[] b; // list of words to search

int index = -1;

for (int i = 0; i < a.length - b.length; i++)
{
    HashSet<String> set = new HashSet<String>(b.length);
    for (String s : b)
        set.add(s);

    boolean found = true;

    for (int j = 0; j < b.length; j++)
    {
        if (set.contains(a[i+j]))
            set.remove(a[i+j]);
        else
        {
            found = false;
            break;
        }
    }
    if (found)
    {
        index = i;
        break;
    }
}

如果您能够拥有给定单词的多个实例,则会变得更容易。这假定b中的每个单词都是唯一的。

答案 4 :(得分:1)

我可以将此问题视为minimum window width problem的替代方案。而不是字符,这里是文字。

与Dukeling给出的解决方案几乎相同。唯一的附加组件是使用LinkedHashMap跟踪订单中找到的单词。可以找到一个java解决方案here

这是我的python实现


import collections
def minsubstring(sentence, words):
    sentence = sentence.split(' ')
    mintillnow = sentence
    words = set(words.split(' '))
    found = collections.defaultdict(lambda : [-1,-1])#position of word in the sentence and order of the word
    linked = [] # this together with 'found' provides the functionality of LinkedHashMap
    for i, word in enumerate(sentence):
        if word in words:
            found[word][0] = i
            if found[word][1] != -1:#if this word is already seen, remove it from linked list
                del(linked[found[word][1]])
            linked.append(word)#append the newly found word to the tail of the linked list
            # probably the worst part in this code, updating the indexes back to the map
            for i, wword in enumerate(linked):
                found[wword][1] = i
            # if found all the words, then check if the substring is smaller than the one till now and update
            if len(linked) == len(words):
                startPos = found[linked[0]][0]
                endPos = found[linked[-1]][0]
                if (endPos - startPos + 1) < len(mintillnow):
                    mintillnow = sentence[startPos:endPos + 1]
    return ' '.join(mintillnow)


测试结果


>>> minsubstring('This is a test. This is a programming test. a programming test this is. ','this test a programming')
'a programming test this'

答案 5 :(得分:0)

我认为你可以通过让头部和尾部指针继续向内移动直到你不再匹配然后为另一个做同样的事情并重复整个过程直到它不会向内移动来做到这一点。了。我可能会稍后尝试编写代码。

答案 6 :(得分:0)

我会勾勒出更有效的算法。

不要连接字符串。而是在添加时计算字符数,即每个单词的长度()+ 1。

对于子列表保存起始单词,结束单词,字符计数。

找到较短的列表时,请替换以上值。

编写一个方法来查找以特定元素开头的第一个子列表,并返回子列表的上述定义(start,end,char count)。

使用第一个单词调用上述方法。保存价值。 使用起始单词+ 1调用方法。在找到时冲洗并重复保存较短的值。

您甚至可以通过使用子列表中的第一个单词必须是您的搜索词之一来改进。从start + 1开始,您可以简单地查找该元素而不是所有元素,因为它是唯一缺少的元素(仍然需要使用all来查找第一个匹配的单词)。如果您在子列表中的结束词之前找到它,则您有一个较小的子列表。如果你在结尾的单词之后找到它,那就是新的结尾。

这更复杂但可能更快。一个常见的权衡。

答案 7 :(得分:0)

public final class MaxStringWindow {

    private MaxStringWindow() {}

    private static void addStringCount(Map<String, Integer> map, String str) {
        if (!map.containsKey(str)) {
            map.put(str, 1);
        } else {
            int val = map.get(str);
            map.put(str, val + 1);
        }
    }

    private static Map<String, Integer> toFindMap(List<String> strList) {
        final Map<String, Integer> toFind  = new HashMap<String, Integer>();
        for (String stri : strList) {
            addStringCount(toFind, stri);
        }
        return toFind;
    }


    public static int minWindowSize(String sentence, List<String> strList) {
        final Map<String, Integer> toFind = toFindMap(strList);
        final Map<String, Integer> hasFound  = new HashMap<String, Integer>();

        int matchCtr = 0;
        boolean matchFound = false;
        String currLeftMostString = null;

        int j = 0; // the trailing position of the sliding window
        int i = 0; // the leading position of the sliding window.

        int min = Integer.MAX_VALUE;

        String[] words = sentence.split(" "); 

        for (i = 0; i < words.length; i++) {

            if (!toFind.containsKey(words[i])) {
                continue;
            }

            if (!matchFound) {
                currLeftMostString = words[i];
                matchFound = true;
                j = i;  
            }

            addStringCount(hasFound, words[i]);

            matchCtr++;

            // check if match has been completed.
            if (matchCtr >= strList.size()) {
                if ((i - j + 1) < min) {
                    min = i - j + 1;
                }
            }

            // does the first element exceed value ?
            if (hasFound.get(currLeftMostString) > toFind.get(currLeftMostString)) {
                // advance the left pointer, such the window (i-j) is as small as possible.    
                while (!toFind.containsKey(words[j]) || hasFound.get(words[j]) > toFind.get(words[j])) {
                    if (hasFound.containsKey(words[j])) {
                        int val = hasFound.get(words[j]);
                        hasFound.put(words[j], val - 1);
                    } 
                    j++;
                }
                currLeftMostString = words[j];
            }   
        }


        if (matchCtr < strList.size()) {
            throw new IllegalArgumentException("The subset is not found in the input string.");
        }

        // note: here we dont do (i-j+1) since i has been incremented additionally in a for loop.
        return min > (i - j) ? i - j : min;
    }

}