从数组中返回不一样的绝对值个数

这是一道我在做线上试题时候遇到的一个问题,感觉还是不错的,可以考察程序员的抽象思考能力。题目是这样的:

一个有序数组,从小到大排列,要求写一个函数返回绝对值不一样的个数。比如:

数组是:1, 3, 5, 7, 9。那么返回值是5,因为不一样的绝对值分别是:1,3,5,7,9;

数组是:1, 3, 3, 5, 7, 8, 8, 9。那么返回值是6,因为不一样的绝对值分别是:1,3,5,7,8,9;

数组是:-6, -5, -5, -1, 0, 1, 3, 3, 5, 6。那么返回值是5,因为不一样的绝对值分别是0, 1, 3, 5,6。

算法时间要求是O(n),空间要求是O(1)。

这道题目其实主要考程序员的抽象思维。一般想到的做法是:

第一种情况:若最大值和最小值符号一样,则过一遍数组,遇到和上一次重复的就不累加,否则累加,最后返回累加值。这种情况很简单,代码容易写出。

第二种情况:正负数都存在数组中,这时用两个指针在数组两头开始向中间移动,直到正负值都或者其中之一的值对比完,若有剩余,则是上一种情况。

但第二种情况的代码很难写出,即使写出,也极其难懂(其实这个算法是有bug的)。实际上,第二种情况的做法若是这样,就容易理解很多,而且代码也简单好写了:

第二种情况相当于把一个数组插到另一个数组中,若遇到上一个相同的数字则丢弃,否则累加,被插入的数组同时舍去相同的数字。若要解决上面提到的第三组数组,这种方法其实就是这样:

6 5 5        1

6 5     3 3 1 0

也就是说,第二种情况可以这么理解:取绝对值,合并两个数组,同时去掉重复的数字。因为不是真正的合并,所以不会有额外的存储空间。

0可以看出是正数。算法的执行是这样的:一边从-6开始,一边从6开始,6和-6绝对值相等,累加1,双方步进,5和-5绝对值相等,累加1, 双方步进,-5和3绝对值不相等,由于-5和上一个数重复丢弃,不累加,同时负数数组步进,正数数组等待负数数组等于或者超越他的数的绝对值出现,这时负数数组出现-1,其绝对值小于3,于是累加1(因为3才累加),正数数组步进,发现还是3,丢弃,步进,发现是1,与负数数组的-1绝对值相等,累加1,双方步进,这时负数数组结束,后面处理只有单一符号的正数数组,0,累加1。一共累加了5次,于是返回5。

代码如下:

// 正负数混合
int CountAbsoluteNumber(int* pArray, const int iSize)
{
	if (NULL == pArray || 0 == iSize)
	{
		return 0;
	}

	// 一下相当于把正数部分插入到负数部分,由大到小插
	int i = 0, j = iSize - 1, iCount = 0, iLast = -1;
	while (pArray[i] < 0 && pArray[j] >= 0)
	{
		if (pArray[j] > abs(pArray[i]))
		{
			if (iLast != pArray[j])
			{
				iLast = pArray[j];
				++iCount;
			}

			--j;
		}
		else if (pArray[j] == abs(pArray[i]))
		{
			if (iLast != pArray[j])
			{
				iLast = pArray[j];
				++iCount;
			}

			--j;
			++i;
		}
		else if (pArray[j] < abs(pArray[i]))
		{
			++iCount;
			while (pArray[i] < 0) // 跳至下一个不一样的数
			{
				if (pArray[i] != pArray[i + 1])
				{
					++i;
					break;
				}

				++i;
			}
		}
	}

	// 处理余下的
	iLast = pArray[i - 1];
	while (pArray[i] < 0)
	{
		if (pArray[i] != iLast)
		{
			++iCount;
			iLast = pArray[i];
		}
		++i;
	}

	iLast = pArray[j + 1];
	while (pArray[j] >= 0)
	{
		if (pArray[j] != iLast)
		{
			++iCount;
			iLast = pArray[j];
		}
		--j;
	}

	return iCount;
}

// 单一符号
int CountNumber(const int* pArray, const int iSize)
{
	int iCount = 0;
	for (int i = 1; i < iSize; ++i)
	{
		if (pArray[i - 1] == pArray[i])
		{
			++iCount;
		}
	}

	return iSize - iCount;
}

// 主函数
int AbsoluteNumber(int* pArray, const int iSize)
{
	if (NULL == pArray || 0 == iSize)
	{
		return 0;
	}

	if (pArray[0] < 0 && pArray[iSize - 1] > 0)
	{
		return CountAbsoluteNumber(pArray, iSize);
	}

	return CountNumber(pArray, iSize);

}

这里还需要说的就是测试用例的写法。测试用例很重要,一个好的测试用例可以消灭大部分bug。这个例子的测试用例怎么写才好呢?好的测试用例一定可以让测试者只关注:输入,预计输出和输出结果评估。并且,测试应该是自动进行的。

当然本例是一个很简单的例子,不太可能写出很复杂的测试例子,但是仍然要遵循好的测试用例规则(根据经验bug多数时候在程序员认为简单的时候出现,判断一个程序员是否老道,就看他分析需求的时候是不是拍拍脑袋说简单)。测试代码应该一目了然。

一下是测试代码,我设计了一个数组,数组元素第一个是数组大小,第二个是正确的返回值,之后才是输入数据,这样就可以让测试自动进行验证,如果以后我们发现有特殊的测试数据,可以直接添加测试数据,而不再用写逻辑代码。比如全0数组。

int main()
{
	int pTestArray1[] = {12, 8, 0, 1, 2, 2, 2, 2, 3, 3, 5, 7, 8, 9};//0, 1, 2, 3, 5, 7, 8, 9
	int pTestArray2[] = {5, 5, -5, -4, -3, -2, -1, -1};//1, 2, 3, 4, 5
	int pTestArray3[] = {14, 5, -11, -8, -8, -3, -3, 3, 3, 8, 8, 8, 9, 11, 11, 12};//3, 8, 9, 11, 12
	int pTestArray4[] = {8, 2, -3, -3, -3, 0, 3, 3, 3, 3};//0, 3
	int pTestArray5[] = {8, 1, -3, -3, -3, 3, 3, 3, 3, 3};//3
	int pTestArray6[] = {1, 1, -3};//3

	int * ppTestSet[] = {
		pTestArray1,
		pTestArray2,
		pTestArray3,
		pTestArray4,
		pTestArray5,
		pTestArray6,
	};

	int iCount = sizeof(ppTestSet) / sizeof(*ppTestSet);
	for (int i = 0; i < iCount; ++i)
	{
		int iResult = AbsoluteNumber(ppTestSet[i] + 2, ppTestSet[i][0]);
		std::cout << "test pTestArray." << i + 1 << " result is " << iResult;
		if (iResult != ppTestSet[i][1])
		{
			std::cout << "...wrong\n";
		}
		else
		{
			std::cout << "...correct" << std::endl;
		}
	}

	getchar();
}


有经验的人一定发现我没测试两种重要的情况:输入无效参数时和输入的数组是无序时。前一种情况其实是测试用例必不可少的测试例子,但我这里着重讲算法,省去了。后一种情况属于未定义行为,因为我们不太可能在函数中检查数组是否有序,这个不属于算法的一部分,因此我也省去了,但实际项目中是必须加上的。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值