PTAL1-011 A-B(20 分)超级详解:索引的威力

本题要求你计算A-B。不过麻烦的是,A和B都是字符串 —— 即从字符串A中把字符串B所包含的字符全删掉,剩下的字符组成的就是字符串A-B。
输入格式:
输入在2行中先后给出字符串A和B。两字符串的长度都不超过10^4^,并且保证每个字符串都是由可见的ASCII码和空白字符组成,最后以换行符结束。
输出格式:
在一行中打印出A-B的结果字符串。
输入样例:
I love GPLT!  It's a fun game!
aeiou
输出样例:

I lv GPLT!  It's  fn gm!

本题可以用三种方法解决,其中一种是暴力算法,还有一种是暴力算法的改进,最后一种是最简明的版本。

先来讲暴力算法:

暴力算法,思想简单直接,用两个for循环嵌套,外循环变量的上限是B串长,内循环变量的上限是A串长,如果A中字符等于B中字符,使用replace函数将其替代为空串,即完成一次删除,而此删除操作将产生O(N)的复杂度,总复杂度为O(N^3),实在无法忍受,提交后最后一个测试点会不过,因为超过100ms了。代码1如下:

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string A,B;
    getline(cin,A);
    getline(cin,B);   
    for(int i=0;i<B.length();i++)
    for(int j=0;j<A.length();j++)
    if(A[j]==B[i])
     {
         A.replace(j,1,"");
         j--;//处理连续两个字符相同的情况,j会++所以需要--,从原来的位置出发
     }
     cout<<A;
    return 0;
}

暴力算法的改进:引入索引

为了处理连续两个字符相同的情况,引入索引变量来标记已经删除一次的位置,下一次位置再从此位置开始判定,当输入规模很大时,效率会很高,具体我们之后再讲。这里的寻找相同字符操作不是像暴力算法那样,而是使用了find函数,代码2如下:

#include<iostream>
#include<string>
using namespace std;
int main()
{
	string A,B;
	int t,index=0;
	getline(cin,A);
	getline(cin,B);	
	for(int i=0;i<B.length();i++)
	{
		index=0;
		while((t=A.find(B[i],index))!=string::npos)
 		{
		 	A.replace(t,1,"");
		 	index=t;
		 }
	}
 	cout<<A;
	return 0;
}

标识思想的妙用,一维数组简明解决:

此算法是已知的解决此题的最简明的方法,连字符串函数都没用到,定义一个一维数组,数组下标就是字符的ASC码值,下标对应的元素只有0或1,0代表没有B的字符,1代表有B的字符,标识思想在这里发挥了极大的作用,使得算法复杂度为O(N),相对暴力算法,连降了两个数量级,代码3如下:

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string A,B;
    int book[256]={0};
    getline(cin,A);
    getline(cin,B);   
    for(int i=0;i<B.length();i++)
    book[B[i]]=1;
    for(int i=0;i<A.length();i++)
    if(!book[A[i]])
    cout<<A[i];
    return 0;
}

你可能会奇怪我为什么没有讲代码2的复杂度,当然不是因为容易看出,而是你看出了代码2的复杂度为O(N^3)也没有用。因为代码2能过,而且不是卡着耗时上限100ms过!最后一个测试点的耗时是:7ms!你可能觉得为什么同是O(N^3)复杂度,实际耗时差距怎么这么大。先不说这个,你可能还想知道代码3的复杂度,它的复杂度天生就是O(N),代码也非常简明,堪称完美算法,但是最后一个测试点的耗时也不过是5ms,在合理范围内,然而它和代码2只差2ms,明显是同一个数量级的。

为什么?效率会这么高?你肯定想到了,没错,这就是索引的威力!当输入规模相当大,也即字符串相当长时,如果待找字符只在末尾的一小段区间内,如果像代码1那样每次都从头找起,那么效率是非常低下的。而索引恰恰能避免这一情况,因为它就在原来被删除的位置上,如果待删除字符全部集中在字符串末尾的很小一段区间内,那么加上删除操作,时间复杂度将接近O(1)!可是即便如此,一开始的那次查找也会花费O(N)的时间,总体复杂度怎么会降至O(N)呢?

本人认为,应该是有两方面的因素。一方面是代码3的复杂度不是纯粹的O(N),而是O(2N),具体而言,如果B的长度为LB,A的长度为LA,则复杂度为O(LB+LA)。另一方面是如果输入规模大,根据实际测试,应该是A的长度作为较大的那个输入规模,也就是A的长度要远远大于B的长度,所以代码2的效率没有想象的那么低。如果你到现在还没明白的话,请看代码2外循环变量的上限是什么?LB!也就是说如果LA远大于LB,代码2的效率是绝不亚于代码3的。容易得出,代码2的复杂度实际将接近于O(LB*LA+LB*1*1),当LA足够大,LB足够接近1的时候,代码3将接近于O(LA+1)=O(LA),而代码2的复杂度将非常接近于O(1*LA+1*1*1)=O(LA)!这才是代码2高效的真正关键!而暴力算法呢?就算LA足够大,LB足够接近1,其复杂度也接近于O(1*LA*LA)=O(LA^2)!我们是无法忍受其低效的。

综上,即便是O(N^3)的复杂度,还是有可以大大提高效率的地方,归功于索引,用好索引,将给解题带来极大的帮助!

猜你喜欢

转载自blog.csdn.net/qq_37729102/article/details/80721364