昨天去一个外企去面试,面试官是公司的一个副总,技术出身,所以聊了我的一些经历之后问了一些C++方面的问题,不过还是static, 线程和进程等问题,回答得还可以,然后就说出一道编程题让我在小白板上做一下。
题目是使用C#编程,实现一个函数,该函数将一个字符串List(任何一种List)中与给定字符串相同的字符串全部删去。
题目倒不是很复杂,一会儿就写出来了,如下所示:
public virtual void Trim(IList<string> list, string s)
{
if (list == null || s == null)
{
throw new ArgumentNullException("list/s");
}
for (int i = 0; i < list.Count; )
{
if (list[i].Equals(s))
{
list.RemoveAt(i);
}
else i++;
}
}
开始问我i++为什么不放进for的括号内。当然不能了,因为删去当前的字符串项之后,i是不是再增1的,因为删除之后后面的项会向前提,实际上此时的i已经是下一项了。
然后,又问如果想改进该函数的性能,可能改的地方有哪些,应该如何修改。
为了测试该函数的性能,及改进后函数的性能,编写了一个类封装该函数,然后使用Template Method模式,将Trim定义为虚函数,Run()函数作为测试函数,在 Run()函数中调用Trim()函数,在继承的类中重写Trim()为改进后的函数,而Run()不需要更改,便于测试,将测试类封装为如下类 TrimStringList:
class TrimStringList
{
private List<string> _list;
private string dst;
private int times;
/// <summary>
///
/// </summary>
/// <param name="runCount">The times to be run;</param>
/// <param name="length">The length of the list to be tested</param>
public TrimStringList(int length, int runCount)
{
if (length == 0 || runCount == 0)
throw new ArgumentException("the argument length and runCount can't equals to 0");
_list = new List<string>();
Random ran = new Random();
dst = ran.Next(length).ToString();
times = runCount;
for (int i = 0; i < length; i++)
{
_list.Add(ran.Next(length).ToString());
}
}
public TimeSpan Run()
{
int i = times;
Console.WriteLine("/nRunning " + this.GetType().Name);
DateTime startTime = DateTime.Now;
while (i-- > 0)
{
List<string> l = new List<string>();
l.AddRange(_list);
Trim(l, dst);
}
DateTime endTime = DateTime.Now;
TimeSpan cost = endTime - startTime;
Console.WriteLine("Time cost=" + cost);
return cost;
}
/// <summary>
/// Remove all the strings that equals to s in the list
/// </summary>
/// <param name="list"></param>
/// <param name="s"></param>
public virtual void Trim(IList<string> list, string s)
{
if (list == null || s == null)
{
throw new ArgumentNullException("list/s");
}
for (int i = 0; i < list.Count; )
{
if (list[i].Equals(s))
{
list.RemoveAt(i);
}
else i++;
}
}
}
既然有循环,要提高性能肯定是减少每次循环的运算量。这个函数明显消耗时间的一个地方就是list.Count,因为这是一个属性,比使用变量或者 字段都是要消耗资源的,因为属性的读取实际上跟调用函数的开销是一样的,所以可以把这里的Count属性换到for外面,使用一个临时变量保存,但是这个 变量是变的可不是读一次就完了。再分析,什么情况下Count会变,那就是list中的项被删除时,即list.RemoveAt()被执行的时候,只要 这时候将临时变量减1就行了,修改后的代码如下所示。
class TrimStringListNoCount : TrimStringList
{
public TrimStringListNoCount(int len, int time)
: base(len, time)
{
}
public override void Trim(IList<string> list, string s)
{
if (list == null || s == null)
{
throw new ArgumentNullException("list/s");
}
int count = list.Count;
for (int i = 0; i < count; )
{
if (list[i].Equals(s))
{
list.RemoveAt(i);
count--;
}
else i++;
}
}
}
面试官对我的这个方案给予了肯定,说方案很不错,但是还有地方可以改,说到用指针,他就笑,说没必要把关公请来吧,那样改动太大了,就是用现在的 for循环,可能当时也有点儿紧张吧,真找不到可以改进的地方了。回来以后又想了想,其实i++改成++i可能会快点儿,但整个循环使用两个变量好像不是 太必要,于是改用了while循环实现如下:
class TrimStringListWhile : TrimStringList
{
public TrimStringListWhile(int len, int count)
: base(len, count)
{
}
public override void Trim(IList<string> list, string s)
{
if (list == null || s == null)
{
throw new ArgumentNullException("list/s");
}
int count = list.Count;
while (count-- > 0)
{
if (list[count].Equals(s))
{
list.RemoveAt(count);
}
}
}
}
但仔细看,count--实际上其表达式的值为自增前的count,这样不免在程序执行过程中会使用一个中间变量保存这个值,然后执行完比较操作再将该表达式存回count,实际上这一过程是没有必要的,于是做了如下改进
class TrimStringListWhilePlus : TrimStringListWhile
{
public TrimStringListWhilePlus(int len, int count)
: base(len, count)
{
}
/// <summary>
/// 将--放到了后面,因为上一个版本在while()中自减需要后减操作,那样会保存一个临时变量,然后再把while的条件
/// 判断执行后再将新值给count。把--操作改成先减,这样汇编语言中有直接指令INC支持,会使操作加快
/// </summary>
/// <param name="list"></param>
/// <param name="s"></param>
public override void Trim(IList<string> list, string s)
{
if (list == null || s == null)
{
throw new ArgumentNullException("list/s");
}
int count = list.Count;
while (count > 0)
{
if (list[--count].Equals(s))
{
list.RemoveAt(count);
}
}
}
}
这里对list的遍历不能使用foreach,因为这里会修改list,修改后会抛出异常。
下面编程测试几个实现的性能,测试程序如下:
class Program
{
static void Main(string[] args)
{
int len = 800;
int count = 1000;
int cycle = 1000;
TimeSpan t1 = new TimeSpan() ,
t2 = new TimeSpan() ,
t3 = new TimeSpan() ,
t4 = new TimeSpan() ;
do
{
TrimStringList test1 = new TrimStringList(len, count);
t1 += test1.Run();
TrimStringListNoCount test2 = new TrimStringListNoCount(len, count);
t2 += test2.Run();
TrimStringListWhile test3 = new TrimStringListWhile(len, count);
t3 += test3.Run();
TrimStringListWhilePlus test4 = new TrimStringListWhilePlus(len, count);
t4 += test4.Run();
} while (--cycle > 0);
Console.WriteLine("/n =============== Statistics ====================/n");
Console.WriteLine("improvement:{0:P2}, {1:P2}, {2:P2}", Radio(t1, t2) , Radio(t1, t3) , Radio(t1, t4));
Console.ReadKey();
}
static double Radio(TimeSpan t1, TimeSpan t2)
{
return (t1.TotalMilliseconds - t2.TotalMilliseconds) / t1.TotalMilliseconds;
}
}
因为单次运行的话各个函数使用的时间不尽相同,于是运行1000遍求平均值,下面是运行结果,可能两次运行结果不完全相同,但相差不大。
......
Running TrimStringList
Time cost=00:00:00.0312500
Running TrimStringListNoCount
Time cost=00:00:00.0312500
Running TrimStringListWhile
Time cost=00:00:00.0312500
Running TrimStringListWhilePlus
Time cost=00:00:00.0312500
=============== Statistics ==============
improvement:11.90%, 14.03%, 14.29%