昨天去一个外企去面试,面试官是公司的一个副总,技术出身,所以聊了我的一些经历之后问了一些C++方面的问题,不过还是static, 线程和进程等问题,回答得还可以,然后就说出一道编程题让我在小白板上做一下。
题目是使用C#编程,实现一个函数,该函数将一个字符串List(任何一种List)中与给定字符串相同的字符串全部删去。
题目倒不是很复杂,一会儿就写出来了,如下所示:
public virtual void Trim(IList 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 _list; private string dst; private int times; ////// /// /// The times to be run; /// The length of the list to be tested 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(); 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 l = new List(); l.AddRange(_list); Trim(l, dst); } DateTime endTime = DateTime.Now; TimeSpan cost = endTime - startTime; Console.WriteLine("Time cost=" + cost); return cost; } ////// Remove all the strings that equals to s in the list /// /// /// public virtual void Trim(IList 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 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 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) { } ////// 将--放到了后面,因为上一个版本在while()中自减需要后减操作,那样会保存一个临时变量,然后再把while的条件 /// 判断执行后再将新值给count。把--操作改成先减,这样汇编语言中有直接指令INC支持,会使操作加快 /// /// /// public override void Trim(IList 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%