今天同事估计闲得蛋疼,突然开始回忆以前面试过程中被面到过的一些面试问题,有逻辑的,有算法的,然后来考我思路,标题对应的算法就是他碰到的面试算法题之一。
拿到题目的第一个感觉,就是Linq,被Linq带坏了,这种考算法的题目直接来Linq你都逗谁呢,整理一下思路,恩,题目只要求左侧奇数,右侧偶数,并未要求两侧的整数还要分别排序,那算法思路就这么定下来了,按索引从低向高循环,如果遇到偶数,则在循环内进行反向循环,即从高位向低位循环找奇数来实现两者位置交替,算法实现如下:
static void SetLeftSingleAndRightDoubleOne(int[] arr)
{
int num = 0;
var prevIndex = arr.Length - 1;
for (var i = 0; i <= prevIndex; i++)
{
if (arr[i] % 2 == 0)
{//偶数
for (var j = prevIndex; j > i; j--)
{
num++;
if (arr[j] % 2 != 0)
{
//奇数
var b = arr[i];
prevIndex = j - 1;//当前位置已与偶数更换过位置,所以设置值的索引可以减一
arr[i] = arr[j];
arr[j] = b;
break;
}
}
}
else
{
num++;
//奇数
continue;
}
}
//Console.WriteLine("ONE RUN NUMS:" + num);
}
咋看算法没问题,时间复杂度应当为f(n)=n-1,但实际一运行,却发现偶尔会出现 循环次数高于数组长度的现象,这是为什么呢?因为问题出在反向循环上,如果反向循环到i为止,都未能找到奇数,那也就不会有prevIndex的赋值,那外层的循环也就会继续下去,然后就会出现循环次数高于数组长度的情况,修正代码,增加判断停止标志位:
static void SetLeftSingleAndRightDoubleOne(int[] arr)
{
int num = 0;
var prevIndex = arr.Length - 1;
for (var i = 0; i <= prevIndex; i++)
{
if (arr[i] % 2 == 0)
{//偶数
bool stop = false;//停止标志位
for (var j = prevIndex; j > i; j--)
{
num++;
if (arr[j] % 2 != 0)
{
//奇数
var b = arr[i];
prevIndex = j - 1;//当前位置已与偶数更换过位置,所以设置值的索引可以减一
arr[i] = arr[j];
arr[j] = b;
break;
}
stop = j == i + 1;//多了一步赋值
}
if (stop)//虽然多了一步判断,但循环次数有减少
{//因为反向遍历已经遍历到了i的前一位,所以可以停止遍历了
break;
}
}
else
{
num++;
//奇数
continue;
}
}
//Console.WriteLine("ONE RUN NUMS:" + num);
}
OK,算法已经实现,然后得到同事的实现,大体思路一致,都是左右遍历交替,具体如下:
static void SetLeftSingleAndRightDoubleTwo(int[] arr)
{
int left = 0;
int right = arr.Length - 1;
int num = 0;
while (left < right)
{
num++;
bool leftDouble = arr[left] % 2 == 0;//判断左侧是否是偶数
bool rightSingle = arr[right] % 2 != 0;//判断右侧是不是奇数
if (!leftDouble)
{
left++;
}
if (!rightSingle)
{
right--;
}
if (leftDouble && rightSingle)
{
int t = arr[left];
arr[left] = arr[right];
arr[right] = t;
left++;
right--;
}
}
//Console.WriteLine("TWO RUN NUMS:" + num);
}
与实现一相比,每次循环都会低位高位找两个位置,并判断是否能进行替换,因为每次循环都判断高低两个位置,所以时间复杂度上要低于实现一
最后就是具体的测试代码,为测试结果更为精准,每个实现都执行10次
Stopwatch watch= new Stopwatch();
for (var i = 0; i < 10; i++)
{
Console.WriteLine("*******************************");
Console.WriteLine("Run the " + (i + 1) + "st time");
var tmp = Enumerable.Range(1, 50/*00000*/).OrderBy(_ => Guid.NewGuid()).Skip(30).ToArray();
//arr = Enumerable.Repeat(1, 50).ToArray();
//arr = new int[50];
var arr1 = tmp.ToArray();
var arr2 = tmp.ToArray();
watch.Reset();
watch.Start();
SetLeftSingleAndRightDoubleOne(arr1);
watch.Stop();
Console.WriteLine("ONE MS:" + watch.ElapsedMilliseconds);
//Console.WriteLine(string.Join(",", arr1));
watch.Reset();
watch.Start();
SetLeftSingleAndRightDoubleTwo(arr2);
watch.Stop();
Console.WriteLine("TWO MS:" + watch.ElapsedMilliseconds);
//Console.WriteLine(string.Join(",", arr2));
}
最后分别按正确性和执行时间截了两张图,正确性方面两者一致,执行时间方面实现二要略优于实现一
第二位同事思考后给实现一提出优化,取消标志位,因为本身可以在循环结束后进行i和j的相应判断,实现一算法修正如下:
static void SetLeftSingleAndRightDoubleOne(int[] arr)
{
int num = 0;
var prevIndex = arr.Length - 1;
for (var i = 0; i <= prevIndex; i++)
{
if (arr[i] % 2 == 0)
{//偶数
var j = prevIndex;
for (; j > i; j--)
{
num++;
if (arr[j] % 2 != 0)
{
//奇数
var b = arr[i];
prevIndex = j - 1;//当前位置已与偶数更换过位置,所以设置值的索引可以减一
arr[i] = arr[j];
arr[j] = b;
break;
}
}
if (j <= i + 1)
{//因为反向遍历已经遍历到了i的前一位,所以可以停止遍历了
//思考下这里为什么要判断i+1,而不是i?
break;
}
}
else
{
num++;
//奇数
continue;
}
}
Console.WriteLine("ONE RUN NUMS:" + num);
}
因为少了标志位赋值那一块,结果在5000000数组的情况下,实现一的执行速度多数情况下略高于实现二,虽然实现二的循环次数还是低于实现一
好吧,实现一为什么要那么逗,没事弄个j出来,最后还要做判断,傻了,直接用prevIndex不就可以了
static void SetLeftSingleAndRightDoubleOne(int[] arr)
{
int num = 0;
var prevIndex = arr.Length - 1;
for (var i = 0; i <= prevIndex; i++)
{
if (arr[i] % 2 == 0)
{//偶数
for (; prevIndex > i; prevIndex--)
{
num++;
if (arr[prevIndex] % 2 != 0)
{
//奇数
var b = arr[i];
arr[i] = arr[prevIndex];
arr[prevIndex] = b;
prevIndex--;
break;
}
}
}
else
{
num++;
//奇数
continue;
}
}
//Console.WriteLine("ONE RUN NUMS:" + num);
}