包含给定字符集的最小子串

给定一个字符集合 must [0,...,m-1 ] 和一个字符串str [0,...,n-1 ]。假定 n <= m 。找出 str 中包含 must 中所有字符的最短子串。

例如:给一个字符串s1,和一个小串s2,求算法能在s1中找到包含s2里所有字符的最小子串。比如:
s1 = “ADOBECODEBANC”
s2 = “ABC” 
最小子串是 “BANC”,要求O(N)的算法。


1、最直接和简单的算法当然是暴力搜索(brute-force search):

minimal := str [0,...,n-1 ]

for i from to n-1 do

       search smallest kisuch that str [i,...,ki]contains all characters from must .

       //寻找最小的 ki,使得str [i,...,ki] 包括所有must 中的字符

       if str [i,...ki] is shorter than minimal , then update minimal to str [i,...,ki]

      //如果该子串比当前已知的最知子串还短,则设置当前最小子串为该子串

    可以看出,该算法的复杂度为 O(nk ) 。如果 k =O(n ),则为O(n2)。那么,有没有更好的算法呢?一般来说,暴力算法会包含很多不必要的或重复的计算。这些不必要的计算有时可以通过剪枝的方法来避免。接下来,我们就来研究如何避免不必要的计算。

    首先,如果 str [i ] 不在 must 中,则可以安全的跳过以 str [i ] 开始的子串(注意:这里所说的以 str [i ] 开始的子串,是指从 str 的位置 i 开始的子串,即 str [i,...,k ],而不是以 str [i ] 这个字符开头的子串;下同。)这是基于如下观察:

   观察1: 如果 str [i,...,ki] 是最短的包含 must 中所有字符的子串,则 str [i ] 和 str [ki都在 must 中。

   其次,我们还有

   观察2: 如果 str [i,...,ki] 是包含 must 的子串,并且 str [i ] 在 str [i,...,ki中不止出现一次str [i+1,...,ki也包含 must 中的所有字符。

   基于观察2,在暴力算法中,当然从 i 转到 i+1 时,如果能以某种快速的方法知道 str [i ] 在 str [i,...,ki] 中不只出现一次,则无须搜索以 str [i+1 ] 开始的符合要求的子串:因为 str [i+1,...,ki] 就是我们要寻找的。

   观察3:如果 str [i,...,ki] 是包含 must 的子串,但 str [i ] 只在 str [i,...,ki] 出现一次,显然,str [i+1,...,ki] 包含了除 str [i ]外 must 中所有的字符。如果我们在 str [ki +1,..., n-1 ] 中寻找第一个出现的 str [i ],记为 ki+1,则str [i+1,...,ki+1] 是以 str [i+1 ] 开头的包含 must 的最短子串。

   基于这以上三个观察,我们可以减少暴力算法中的多少不必要的计算呢?以下我们先给出剪枝后的算法。

   step 1: 首先,从 i := 0 ,我们找到第一个包含 must 的以str [0 ] 开始的最小子串:str [0,...,k0]。显    然,这也是当前我们所知的最短子串。设 j := k0

   step 2: 接下来,假定我们已经知道 str [i,...,j ] 包含了 must 。

   step 3: 考虑当前 i 。

      1) 如果 str [i ] 不在 must 中,根据观察1,我们继续向前移动 i := i + 1 ;

      2) 如果 str [i ] 在 must 中,且在 str [i ] 在 str [i,.j ] 中不止出现一次,根据观察2,继续向前移动 i= i + 1 ;

      3) 否则,如果 str [i,...,j ] 比 mininal 还要短,则我们找到了更短的子串,设置 mininal := str [i,...,j ],然后向前移动 j 直到 str [j ] == str [i ] 或者 j < n-1 。如果 j n-1 ,则结束程序;否则,回到 step 3。

    注意到我们在 step 3 中的 1)和 2)并没有更新 mininal 。这是因为那是多余的。如果发生了1),则在之后的某一刻一定会发生2)或者3)(这里假定 must 不为空);如果发生了2),则在之后的某一刻会发生3)。而由于基于以上三个观察的剪枝是安全的,即不会影响到解的正确性,所以,上述算法也是正确的。事实上,该算法的正确性也可以通过归纳法得以证明。上面的描述基本上就是遵从了归纳法的描述框架。

    那么,剪枝后的算法复杂度呢?注意到 step 3 的循环中,我们每次都至少向前移动 或 一步,因此,该循环最多执行 2n 次。如果我们使用一个哈希表来记录 str [i,...,j ] 中每个在 must 中的字符的出现次数,就可以在O(1)的时间内决定是分支到1)2)3)中哪一支。而显然,在1)和2)分支中,时间为 O(1);而在 3)中,除掉移动 的步骤,时间也为 O(1),而移动 的步骤已经被计算到循环所需要的次数中了。因此,整个算法的复杂度为 O(n )。


    另外一种思路:
使用一个256元素大小的Hash表存储字符出现的个数.初始时将Hash中在s2中出现了的元素初始化为0,其余初始化为-1.

    两指针p1,p2都指向s1首字符,在p2向后移动过程中,如果遇到了hash值为0时则count计数加1,如果count的值等于s2的长度时证明p1~p2之间已经有了一个子串为s2了,此时记录下他们的位置和距离。继续把p1向后移,注意更新hash值和count,直到找到最短的

int findMinString(char *s1,char *s2)
{
    int hash[255];
    for(int i=0;i<255;i++)
    {
        hash[i]=-1;
    }
    //将字符集s2中的字符对应的数组元素初始化为0,s2中的字符在hash中的值>=0
    for(char *p=s2;*p!='\0';p++)
    {
        hash[*p]=0;
    }
    char *p1=s1;
    char *p2=s1;
    char *min_p1=s1;
    char *min_p2=s1;
    int minlen=INT_MAX;
    int count=0;//标识已经是否有了一个串中包含字符集s2
    int len_s2=strlen(s2);
    while(*p2!='\0' || len_s2==count)
    {
        if(count<len_s2)
        {
            if(hash[*p2]==0)//第一次遇到与s2中某字符相等的字符
            {
                count++;
                hash[*p2]++;
            }
            else if(hash[*p2]>0)//多次遇到与s2中某字符相等的字符
            {
                hash[*p2]++;
            }
            p2++;
        }

        if(count==len_s2)//即找到了一个串中包含s2中所有字符的串
        {
            if(p2-p1<minlen)//如果发现更小的则替换
            {
                min_p1=p1;
                min_p2=p2;
                minlen=p2-p1;
            }
            hash[*p1]--;
            if(hash[*p1]==0)//如果p1指向的字符在p1到p2之间只出现了一次
            {
                count--;
            }
            p1++;
        }
    }

    while(min_p1<=min_p2)
    {
        cout<<*min_p1;
        min_p1++;
    }
    cout<<endl;
    return minlen;
}



你可能感兴趣的:(包含给定字符集的最小子串)