基于 LINQ 的答案很好,并且很好地表达了基本问题。简单易读,易于理解,直接解决问题。然而,它确实存在一个问题,即它需要为原始序列中的每个元素生成一个新序列。随着序列变得越来越长,这会变得更加昂贵并且最终变得棘手。
它没有帮助,它需要使用Skip()
and Take()
,它们本身增加了处理原始序列的开销。
另一种方法是扫描序列一次,但跟踪是否已尝试删除,并且当发现失序元素时,a)立即返回false
如果已经发现删除,并且 b) 在确定序列时不包括删除的元素。
您尝试过的代码almost实现这一点。这是一个有效的版本:
static bool almostIncreasingSequence(int[] sequence)
{
bool foundOne = false;
for (int i = -1, j = 0, k = 1; k < sequence.Length; k++)
{
bool deleteCurrent = false;
if (sequence[j] >= sequence[k])
{
if (foundOne)
{
return false;
}
foundOne = true;
if (k > 1 && sequence[i] >= sequence[k])
{
deleteCurrent = true;
}
}
if (!foundOne)
{
i = j;
}
if (!deleteCurrent)
{
j = k;
}
}
return true;
}
注意:我最初认为您的尝试可以通过微小的改变来修复。但最终,事实证明它必须与我编写的通用实现本质上相同(特别是当我也修复了该实现之后......见下文)。唯一的实质性区别实际上只是使用数组还是泛型IEnumerable<T>
.
为了一笑,我编写了另一种方法,该方法符合基于 LINQ 的解决方案的脉络,因为它适用于任何序列,而不仅仅是数组。我还使它成为通用的(尽管有类型实现的约束)IComparable<T>
)。看起来像这样:
static bool almostIncreasingSequence<T>(IEnumerable<T> sequence) where T : IComparable<T>
{
bool foundOne = false;
int i = 0;
T previous = default(T), previousPrevious = default(T);
foreach (T t in sequence)
{
bool deleteCurrent = false;
if (i > 0)
{
if (previous.CompareTo(t) >= 0)
{
if (foundOne)
{
return false;
}
// So, which one do we delete? If the element before the previous
// one is in sequence with the current element, delete the previous
// element. If it's out of sequence with the current element, delete
// the current element. If we don't have a previous previous element,
// delete the previous one.
if (i > 1 && previousPrevious.CompareTo(t) >= 0)
{
deleteCurrent = true;
}
foundOne = true;
}
}
if (!foundOne)
{
previousPrevious = previous;
}
if (!deleteCurrent)
{
previous = t;
}
i++;
}
return true;
}
当然,如果您愿意将原始序列复制到临时数组中(如果它还不是临时数组),那么您可以轻松地使基于数组的版本通用,这将使代码更简单,但仍然通用。这仅取决于您的优先事项是什么。
附录:
LINQ 方法和线性方法(例如上面我的方法)之间的基本性能差异是显而易见的,但我很好奇并想量化这种差异。因此,我使用随机生成的序列进行了一些测试,以粗略地了解差异。
我执行了两个版本的测试:第一个,我运行了 1000 次试验的循环,其中序列的长度可以在 10 到 100 个元素之间;第二个是 10,000 次试验,序列长度在 100 到 1000 个元素之间。我执行了第二个版本,因为在我的笔记本电脑上,1000 次试验的整个测试用较短的序列在不到 1/20 秒内完成,时间太短,让我对结果的有效性没有信心。
在第一个版本中,代码调用线性检查方法花费了大约 1 毫秒,调用 LINQ 方法花费了大约 30 毫秒,速度相差了 30 倍。将试验次数增加到 10,000 次证实了结果;每种方法的时间几乎精确地缩放了 10 倍,保持了 30 倍的差异。
在第二个版本中,差异接近400x。线性版本大约需要 0.07 秒,而 LINQ 版本需要 30 秒。
正如预期的那样,序列越长,差异越严重。对于非常短的序列,不仅代码不太可能在序列检查逻辑上花费太多时间,而且线性方法和 LINQ 方法之间的差异也相对较小。但随着序列变长,这种差异将导致 LINQ 版本的性能非常差,而线性版本仍然表现出色。
LINQ 版本是very可读且简洁。因此,在输入总是相对较短(最多十两个元素的量级)的情况下,我会选择 LINQ 版本。但是,如果我希望使用比这更长的数据定期执行此测试,我将避免使用 LINQ 并坚持使用更高效的线性方法。
关于随机生成序列的注释:我编写了代码来生成所需长度的非负数单调递增序列,然后插入 0 到 2 个(含)新元素,其值为int.MinValue
or int.MaxValue
(对于每次插入也是随机选择的)。通过这种方式,三分之一的测试涉及平凡有效的序列,第三个涉及需要找到要删除的正确单个元素的序列,第三个测试是无效的(即不满足可以使其单调递增的要求)最多删除一个元素)。