如何编写正确的循环?


65

大多数时候,在编写循环时,我通常会写错边界条件(例如:错误的结果),或者我对循环终止的假设是错误的(例如:无限运行的循环)。尽管经过反复尝试后我的假设是正确的,但是由于头脑中缺乏正确的计算模型,我感到非常沮丧。

/**
 * Inserts the given value in proper position in the sorted subarray i.e. 
 * array[0...rightIndex] is the sorted subarray, on inserting a new value 
 * our new sorted subarray becomes array[0...rightIndex+1].
 * @param array The whole array whose initial elements [0...rightIndex] are 
 * sorted.
 * @param rightIndex The index till which sub array is sorted.
 * @param value The value to be inserted into sorted sub array.
 */
function insert(array, rightIndex, value) {
    for(var j = rightIndex; j >= 0 && array[j] > value; j--) {
        array[j + 1] = array[j];
    }   
    array[j + 1] = value; 
};

我最初犯的错误是:

  1. 而不是j> = 0我将其保留为j> 0。
  2. 对array [j + 1] =值还是array [j] =值感到困惑。

有什么工具/心理模型可以避免此类错误?


6
您认为在什么情况下j >= 0是错误的?我会更警惕您正在访问array[j]并且array[j + 1]没有先检查的事实array.length > (j + 1)
Ben Cottrell

5
类似于@LightnessRacesinOrbit所说的,您可能正在解决已经解决的问题。一般来说,您需要对数据结构进行的任何循环都已经存在于某个核心模块或类中(在Array.prototypeJS示例中)。这可以防止您遇到边缘条件,因为类似的方法map适用于所有阵列。您可以使用slice和concat解决上述问题,从而避免全部循环:codepen.io/anon/pen/ZWovdg?editors=0012编写循环的最正确方法是根本不编写循环。
杰德·施耐德

13
实际上,请继续解决已解决的问题。这就是练习。只是不必打扰发布它们。也就是说,除非您找到改善解决方案的方法。也就是说,重塑轮子不仅仅涉及轮子。它涉及整个车轮质量控制系统和客户支持。不过,定制轮辋还是不错的。
candied_orange

53
我担心我们在这里走错了方向。因为CodeYogi的示例是众所周知的算法的一部分,所以给它代码废话是毫无根据的。他从未声称自己发明了新东西。他在问编写循环时如何避免一些非常常见的边界错误。图书馆已经走了很长一段路,但是对于那些知道如何编写循环的人来说,我仍然看到了一个未来。
candied_orange

5
通常,在处理循环和索引时,您应该了解索引指向元素之间,并使自己熟悉半开间隔(实际上,它们是同一概念的两个方面)。一旦掌握了这些事实,就可以从头开始的许多循环/索引中完全消失。
Matteo Italia

Answers:


208

测试

不,认真地测试。

我已经编码20多年了,但我仍然不相信自己第一次能正确编写循环。我编写并运行测试以证明它有效,然后再怀疑它是否有效。测试每个边界条件的每一侧。例如rightIndex0的a应该做什么?-1怎么样?

把事情简单化

如果其他人一眼看不到它的作用,那么您将变得太难了。请随意忽略性能,如果这意味着您可以编写易于理解的内容。仅在确实需要的极少数情况下加快速度。即使如此,也只有一次您完全确定自己确切地知道什么在拖慢速度。如果您可以实现实际的Big O改进,则此活动可能并非毫无意义,但即使如此,也应使代码尽可能易读。

一折

了解数手指和数手指间距的区别。有时空格是真正重要的。不要让手指分散您的注意力。知道你的拇指是否是手指。知道小指和拇指之间的间隙是否算作空格。

评论

在迷上代码之前,请先说英语的意思。明确陈述您的期望。不要解释代码的工作原理。解释为什么要让它执行其功能。保留实施细节。应该可以重构代码而无需更改注释。

最好的评论是一个好名字。

如果您可以说出所有需要用好名字说的话,请不要再发表评论。

抽象

对象,函数,数组和变量都是抽象,其名称与其所提供的名称一样好。给他们起名字,以确保当人们看到他们时,他们不会为所发现的事物感到惊讶。

简称

对短暂的事物使用简称。i是在很小范围内的紧密循环中索引的好名字,这使得它的含义显而易见。如果i寿命足够长,可以与其他可能混淆的想法和名称逐行传播,i那么该给i一个很好的解释性长名称了。

长名

切勿仅出于行长考虑而缩短名称。寻找另一种布局代码的方法。

空格

缺陷喜欢隐藏在不可读的代码中。如果您的语言允许您选择缩进样式,则至少要保持一致。不要让您的代码看起来像一堆杂波。代码看起来应该像编队一样。

循环构造

学习和检查您所用语言的循环结构。 看着调试器突出显示一个for(;;)循环 可能很有启发性。了解所有表格。 whiledo whilewhile(true)for each。使用最简单的方法即可。查找注水泵。了解如何处理breakcontinue如果有的话。知道之间的区别c++++c。只要您始终关闭需要关闭的所有内容,就不要害怕早日返回。最后,在打开时将其阻塞或最好将其标记为自动关闭:使用statement / Try Resources

循环替代

如果可以的话,让其他人进行循环。它看起来更容易并且已经调试。这些有多种形式:集合或允许流map()reduce()foreach(),和类似的方法,即应用拉姆达。寻找诸如的特殊功能Arrays.fill()。也有递归,但只有在特殊情况下才能使事情变得容易。通常不要使用递归,直到您看到替代方案的外观。

哦,测试一下。

测试,测试,测试。

我有提到测试吗?

还有一件事。不记得了。从T ...


36
好的答案,但也许您应该提到测试。一个如何处理单元测试中的无限循环?这样的循环不会“破坏”测试吗?
GameAlchemist

139
@GameAlchemist那是比萨测试。如果我的代码在制作比萨花的时间内没有停止运行,我就开始怀疑出了点问题。当然,这无法解决Alan Turing的停顿问题,但至少我得到了一笔超出交易的比萨。
candied_orange

12
@CodeYogi-实际上,它可以非常接近。从对单个值进行操作的测试开始。实现代码而无需循环。然后编写一个对两个值进行运算的测试。实现循环。像这样进行操作,不可能在边界上出错边界条件,因为在几乎所有情况下,如果您犯了这样的错误,那么这两个测试中的一个或另一个将失败。
Jules

15
@CodeYogi Dude全部归功于TDD,但应归功于Testing >> TDD。输出值可以用来测试,第二眼可以看到您的代码正在测试(您可以将其形式化为代码审查,但我经常会与某人进行5分钟的交谈)。测试是您表达失败意图的任何机会。地狱你可以和妈妈谈一个想法来测试你的代码。盯着淋浴间的瓷砖时,我在代码中发现了错误。TDD是一种有效的正规学科,您不会在每家商店中都找到。我从未在人们未进行测试的地方编写过代码。
candied_orange

12
在听说TDD之前的几年里,我一直在进行编码和测试。直到现在,我才意识到那几年与不穿裤子的编码时间之间的联系。
candied_orange

72

进行编程时,考虑以下几点很有用:

探索未知领域(如与指数杂耍)时,它可以是非常,非常,非常有用不只是去想那些,但实际上使他们在与代码明确断言

让我们以原始代码为例:

/**
 * Inserts the given value in proper position in the sorted subarray i.e. 
 * array[0...rightIndex] is the sorted subarray, on inserting a new value 
 * our new sorted subarray becomes array[0...rightIndex+1].
 * @param array The whole array whose initial elements [0...rightIndex] are 
 * sorted.
 * @param rightIndex The index till which sub array is sorted.
 * @param value The value to be inserted into sorted sub array.
 */
function insert(array, rightIndex, value) {
    for(var j = rightIndex; j >= 0 && array[j] > value; j--) {
        array[j + 1] = array[j];
    }   
    array[j + 1] = value; 
};

并检查我们有什么:

  • 前提条件:array[0..rightIndex]已排序
  • 后置条件:array[0..rightIndex+1]已排序
  • 不变的:0 <= j <= rightIndex但是似乎有点多余;或@Jules在评论中指出的“回合”结束时,for n in [j, rightIndex+1] => array[j] > value
  • 不变式:在“回合”结束时进行array[0..rightIndex+1]排序

因此,您可以首先编写一个is_sorted函数以及min在数组切片上工作的函数,然后断言:

function insert(array, rightIndex, value) {
    assert(is_sorted(array[0..rightIndex]));

    for(var j = rightIndex; j >= 0 && array[j] > value; j--) {
        array[j + 1] = array[j];

        assert(min(array[j..rightIndex+1]) > value);
        assert(is_sorted(array[0..rightIndex+1]));
    }   
    array[j + 1] = value; 

    assert(is_sorted(array[0..rightIndex+1]));
};

还有一个事实,就是您的循环条件有点复杂。您可能希望通过拆分来使自己更轻松:

function insert(array, rightIndex, value) {
    assert(is_sorted(array[0..rightIndex]));

    for (var j = rightIndex; j >= 0; j--) {
        if (array[j] <= value) { break; }

        array[j + 1] = array[j];

        assert(min(array[j..rightIndex+1]) > value);
        assert(is_sorted(array[0..rightIndex+1]));
    }   
    array[j + 1] = value; 

    assert(is_sorted(array[0..rightIndex+1]));
};

现在,循环很简单(jrightIndex转到0)。

最后,现在需要测试:

  • 考虑边界条件(rightIndex == 0rightIndex == array.size - 2
  • 想到value小于array[0]或大于array[rightIndex]
  • 想到value等于array[0]array[rightIndex]或一些中间指标

另外,不要小看模糊。您有适当的断言来捕获错误,因此请生成一个随机数组并使用您的方法对其进行排序。如果断言触发,则说明您发现了一个错误,可以扩展您的测试套件。


8
@CodeYogi:通过...测试。问题是,用断言来表示所有内容可能是不切实际的:如果断言仅重复代码,则不会带来新的内容(重复对质量没有帮助)。这就是为什么在这里我没有在循环中断言0 <= j <= rightIndexor array[j] <= value,它只会重复代码。另一方面,is_sorted带来了新的保证,因此很有价值。然后,这就是测试的目的。如果调用insert([0, 1, 2], 2, 3)函数而不是输出,[0, 1, 2, 3]那么您发现了一个错误。
Matthieu M.

3
@MatthieuM。不要仅仅因为断言重复代码而忽略断言的值。实际上,如果您决定重写代码,则这些声明可能是非常有价值的声明。测试具有复制的一切权利。而是考虑该断言是否与单个代码实现如此耦合,以至于任何重写都会使该断言无效。那是你在浪费时间的时候。好的答案。
candied_orange

1
@CandiedOrange:通过复制代码,我的意思是从字面上看array[j+1] = array[j]; assert(array[j+1] == array[j]);。在这种情况下,该值似乎非常低(它是复制/粘贴)。如果您复制含义但以另一种方式表示,那么它将变得更有价值。
Matthieu M.

10
Hoare的规则:自1969年以来就一直帮助编写正确的循环。
Joker_vD

1
@MatthieuM。我同意它的价值很低。但是我不认为它是由复制/粘贴引起的。说我想重构,insert()以便通过从旧阵列复制到新阵列来工作。可以做到而不会破坏您的其他assert人。但不是最后一个。仅显示您的其他产品assert的设计水平。
candied_orange

29

使用单元测试/ TDD

如果您确实需要通过for循环访问序列,则可以通过单元测试(尤其是测试驱动的开发)来避免错误。

假设您需要实现一种方法,该方法以相反的顺序接受大于零的值。您会想到什么测试用例?

  1. 序列包含一个大于零的值。

    实际:[5]。预期:[5]

    满足要求的最直接的实现包括简单地将源序列返回给调用方。

  2. 一个序列包含两个值,两个值均大于零。

    实际:[5, 7]。预期:[7, 5]

    现在,您不能只返回序列,而应该将其反转。您是否要使用for (;;)循环,其他语言构造或库方法都没有关系。

  3. 一个序列包含三个值,一个为零。

    实际:[5, 0, 7]。预期:[7, 5]

    现在,您应该更改代码以过滤值。同样,这可以通过if声明或调用您喜欢的框架方法来表达。

  4. 根据您的算法(由于这是白盒测试,因此实现很重要),您可能需要专门处理空序列[] → []情况,也可能不需要。或者,您可以确保[-4, 0, -5, 0] → []正确处理所有值为负的边缘情况,甚至可以确保边界负值为:[6, 4, -1] → [4, 6]; [-1, 6, 4] → [4, 6]。但是,在许多情况下,您只有上述三个测试:任何其他测试都不会使您更改代码,因此这是无关紧要的。

以更高的抽象级别工作

但是,在许多情况下,可以使用现有的库/框架在更高的抽象级别上进行操作,从而避免大多数此类错误。这些库/框架使恢复,排序,拆分和加入序列,在数组或双向链表中插入或删除值等成为可能。

通常,foreach可以使用代替for,使边界条件检查无关紧要:该语言为您完成。某些语言(例如Python)甚至没有for (;;)构造,而只有for ... in ...

在C#中,LINQ在处理序列时特别方便。

var result = source.Skip(5).TakeWhile(c => c > 0);

与它的for变体相比,它具有更高的可读性和更少的错误发生率:

for (int i = 5; i < source.Length; i++)
{
    var value = source[i];
    if (value <= 0)
    {
        break;
    }

    yield return value;
}

3
好吧,从您最初的问题开始,我给您的印象是,一方面选择使用TDD并获得正确的解决方案,另一方面跳过测试部分,使边界条件错误。
Arseni Mourzenko '16

18
感谢您成为一个提在房间里的大象:不使用循环可言为什么人们仍然像1985年一样编码(我很慷慨),这超出了我的理解范围。BOCTAOE。
贾里德·史密斯

4
@JaredSmith一旦计算机实际执行了该代码,您想打赌那里没有跳转指令是多少?通过使用LINQ,您可以抽象出循环,但是它仍然存在。我向同事们解释了这件事,他们未能了解画家Shlemiel的努力工作。即使不了解循环发生在哪儿,即使将其从代码中抽象出来,从而使代码的可读性大大提高,也几乎总是会导致性能问题,使您无所适从地进行解释,更不用说修复了。
CVn

6
@MichaelKjörling:当您使用LINQ的循环是存在的,但一个for(;;)结构不会很描述这个循环。一个重要方面是,LINQ(以及Python中的列表理解和其他语言中的类似元素)使边界条件几乎无关紧要,至少在原始问题的范围之内。但是,对于使用LINQ(特别是在进行惰性评估时)需要了解的幕后事情,我完全同意。
Arseni Mourzenko '16

4
@MichaelKjörling我不一定在谈论LINQ,我有点看不到你的意思。forEachmapLazyIterator,等被语言的编译器或运行环境提供,这些可以说是不太可能会走回到在每次迭代油漆桶。那就是,可读性和一次性错误是将这些功能添加到现代语言中的两个原因。
贾里德·史密斯

15

我同意说测试您的代码的其他人。但是,将它放在第一位也很不错。在许多情况下,我倾向于错误地定义边界条件,因此我已经开发出了预防这些问题的技巧。

对于索引为0的数组,正常情况将是:

for (int i = 0; i < length; i++)

要么

for (int i = length - 1; i >= 0; i--)

这些模式应该成为第二天性,您完全不必考虑这些模式。

但是,并非所有事物都遵循确切的模式。因此,如果您不确定自己是否编写正确,请执行以下步骤:

插入值并评估您自己的代码。尽可能简单地考虑。如果相关值为0,会发生什么?如果它们为1,会发生什么?

for(var j = rightIndex; j >= 0 && array[j] > value; j--) {
    array[j + 1] = array[j];
}   
array[j + 1] = value;

在您的示例中,您不确定是[j] = value还是[j + 1] = value。是时候开始手动评估它了:

如果数组长度为0会怎样?答案变得显而易见:rightIndex必须为(length-1)== -1,因此j从-1开始,因此要在索引0处插入,需要加1。

因此,我们证明了最终条件是正确的,但不是循环的内部。

如果您有一个包含1个元素10的数组,而我们尝试插入5个元素会怎样?对于单个元素,rightIndex应该从0开始。因此,第一次通过循环,j = 0,因此“ 0> = 0 && 10> 5”。由于我们要在索引0处插入5,因此10应该移至索引1,因此array [1] = array [0]。由于这是在j为0时发生的,因此array [j + 1] = array [j + 0]。

如果您尝试想象一些大型阵列,然后将其插入任意位置,您的大脑可能会不知所措。但是,如果您坚持使用简单的0/1/2大小的示例,则应该很容易进行快速的脑筋急转弯,看看边界条件在哪里破裂。

想象一下,您以前从未听说过栅栏杆问题,我告诉您我有100条直线的栅栏杆,它们之间有多少段。如果您想像一下脑海中有100个栅栏,那您将不知所措。那么制作有效栅栏的最少栅栏柱是多少?您需要2个来制作栅栏,因此,请想象2个帖子,并且帖子之间的单个分段的心理图像非常清晰。您不必坐在那里计算帖子和句段,因为您将问题变成了直观上显而易见的东西。

一旦您认为它是正确的,那么最好通过测试运行它,并确保计算机能够执行您认为应做的事情,但是在那一点上,这应该只是一种形式。


4
我真的很喜欢for (int i = 0; i < length; i++)。一旦养成了习惯,我<=几乎就停止使用了,感觉循环变得更简单了。但是for (int i = length - 1; i >= 0; i--),与以下情况相比,for (int i=length; i--; )它似乎过于复杂:(while除非我试图使其i作用域/寿命更短,否则将其编写为循环可能更明智)。结果仍然通过i == length-1(最初)到i == 0一直通过循环运行,唯一的区别是while()版本在循环后(如果存在)以i == -1结尾,而不是i = = 0。
TOOGAM '16

2
@TOOGAM(int i = length; i--;)在C / C ++中工作,因为0的计算结果为false,但并非所有语言都具有这种等效性。我猜你可能会说我
Bryce Wagner

自然,如果使用要求“ > 0”才能获得所需功能的语言,则应使用此类字符,因为该语言需要它们。不过,即使在这种情况下,仅使用了“ > 0”是不是做的过程分为两个部分简单减去一个,然后使用“ >= 0”。一旦我通过一些经验中学到了这一点,便养成了>= 0在循环测试条件下使用等号(例如“ ”)的习惯,而且这种习惯变得越来越简单了。
TOOGAM

1
@BryceWagner如果需要i-- > 0,为什么不尝试经典的笑话i --> 0
porglezomp

3
@porglezomp啊,是的,进入运营商。大多数类似C的语言,包括C,C ++,Java和C#,都具有这种语言。
CVn

11

由于头脑中缺少正确的计算模型,我感到非常沮丧。

这个问题很有趣,它产生了以下评论:

只有一种方法:更好地了解您的问题。但这和您的问题一样普遍。– 托马斯·垃圾

...托马斯是对的。没有明确的功能意图的应该是一个红旗-明确表明您应该立即停止,拿铅笔和纸,离开IDE并正确解决问题;或至少非常理智地检查您的工作。

我已经看到太多的函数和类变得一团糟,因为作者在完全定义问题之前就尝试定义实现。而且非常容易处理。

如果您不完全理解问题,那么您也不太可能编写最佳解决方案(从效率或清晰度方面),也无法在TDD方法中创建真正有用的单元测试。

以您的代码为例,其中包含许多您尚未考虑的潜在缺陷,例如:

  • 如果rightIndex太低怎么办?(提示:这涉及数据丢失)
  • 如果rightIndex在数组范围之外怎么办?(您会得到一个例外,还是只是创建了一个缓冲区溢出?)

还有其他一些与代码的性能和设计有关的问题。

  • 此代码是否需要扩展?保持数组排序是最佳选择,还是应该查看其他选择(例如链表?)
  • 您能确定自己的假设吗?(您可以保证对数组进行排序吗?如果不排序该怎么办?)
  • 你在重新发明轮子吗?排序数组是一个众所周知的问题,您是否研究了现有的解决方案?是否已使用您的语言(例如SortedList<t>C#)提供解决方案?
  • 您应该一次手动复制一个阵列条目吗?还是您的语言提供了诸如JScript的通用功能Array.Insert(...)?这段代码会更清晰吗?

有很多方法可以改进此代码,但是在您正确定义需要执行此代码的功能之前,您无需开发代码,只是将它们一起破解,以希望它可以工作。投资时间,您的生活会变得更加轻松。


2
即使将索引传递给现有函数(例如Array.Copy),也仍然需要考虑正确的绑定条件。想象一下在0长度,1长度和2长度的情况下会发生什么,这是确保您不会复制太少或太多的最佳方法。
布莱斯·瓦格纳

@BryceWagner-绝对正确,但是对于到底是什么问题却没有一个清晰的想法,您实际上正在解决,您将花费大量时间在黑暗中进行“打击并希望”策略的思考,这到目前为止是OP的此时最大的问题。
詹姆斯·斯内尔

2
@CodeYogi-正如其他人所指出的那样,您将问题分解为子问题的能力很差,这就是为什么许多答案都将解决问题的方法称为避免问题的方法。这不是您应该亲自处理的事情,而只是来自我们在那里的那些人的经验。
詹姆斯·斯内尔

2
@CodeYogi,我想您可能已经将该站点与Stack Overflow混淆了。该站点相当于在白板上而不是在计算机终端上进行问答环节。“向我显示代码”非常明确地表明您在错误的站点上。
通配符

2
@Wildcard +1:对我来说,“向我展示代码”是一个很好的指示,表明了为什么这个答案是正确的,也许我需要努力更好地证明这是人为因素/设计问题,通过人为过程的变化来解决-没有大量的代码可以教它。
詹姆斯·斯内尔

10

一对一错误是最常见的编程错误之一。即使是经验丰富的开发人员,有时也会出错。高级语言通常具有类似的迭代构造,foreach或者map完全避免显式索引。但是有时您确实需要显式索引,例如您的示例。

挑战在于如何考虑阵列单元的范围。如果没有清晰的思维模型,何时包含或排除端点会变得混乱。

描述数组范围时,约定应包括下界,排除上界。例如,范围0..3是单元格0,1,2。这个约定在整个0索引语言中使用的,例如,slice(start, end)在JavaScript方法返回子阵列开始指数start高达但不包括索引end

当您将范围索引描述为描述数组单元格之间的边缘时,会更清楚。下图是一个长度为9的数组,单元格下面的数字与边缘对齐,用于描述数组段。例如,从图中可以明显看出,范围2..5是单元格2、3、4。

┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │   -- cell indexes, e.g array[3]
└───┴───┴───┴───┴───┴───┴───┴───┴───┘
0   1   2   3   4   5   6   7   8   9   -- segment bounds, e.g. slice(2,5) 
        └───────────┘ 
          range 2..5

此模型与使数组长度为数组的上限一致。长度为5的数组具有单元0..5,这意味着有五个单元0、1、2、3、4。这也意味着段的长度是上限减去下限,即段2..5具有5-2 = 3个像元。

在向上或向下迭代时要牢记此模型,这使得何时包含或排除端点变得更加清晰。向上迭代时,您需要包括起点但不包括终点。向下迭代时,您需要排除起点(上限),但要包括终点(下限)。

由于您要在代码中向下迭代,因此需要包括下限0,因此您要对进行迭代j >= 0

鉴于此,您选择让rightIndex参数代表子数组中的最后一个索引将违反约定。这意味着您必须在迭代中包括两个端点(0和rightIndex)。这也使得很难表示空段(开始排序时需要)。插入第一个值时,实际上必须使用-1作为rightIndex。这似乎很不自然。rightIndex在段之后指示索引似乎更自然,因此0表示空段。

当然,您的代码会更加混乱,因为它会将排序后的子数组扩展为1,在最初排序后的子数组之后立即覆盖该项目。因此,您从索引j读取,但将值写入j + 1。在这里您应该清楚地知道j是插入之前在初始子数组中的位置。当索引操作变得棘手时,它可以帮助我在一张网格纸上绘制它。


4
@CodeYogi:我会在一张纸上绘制一个小阵列作为网格,然后用铅笔手动完成循环的迭代。这有助于我弄清实际发生的情况,例如,一系列单元格向右移动以及新值插入的位置。
JacquesB '16

3
“计算机科学中有两件难事:缓存失效,命名和一次错误。”
Digital Trauma 2016年

1
@CodeYogi:添加了一个小图以显示我在说什么。
JacquesB '16

1
深刻的见解尤其是您最后两个参数值得一读,这也是由于for循环的本质所致,即使我在终止之前一次找到正确的索引,使循环递减j也可以使我退后一步。
CodeYogi '16

1
非常。优秀的。回答。而且我还要补充一点,此包含/排除索引约定也受myArray.Lengthor 的值的启发,该值myList.Count始终比从零开始的“最右边”索引多一。...解释的长度掩盖了这些显式编码启发式方法的实际和简单应用。TL; DR人群不见了。
—radarbob

5

对问题的介绍使我认为您尚未学会正确编码。使用命令式语言编程超过几周的任何人,实际上应该在90%以上的情况下第一次正确地实现循环界限。也许您急于在未充分考虑问题之前就开始编码。

我建议您通过(重新)学习如何编写循环来纠正此缺陷-我建议您花几个小时用纸和铅笔遍历一系列循环。休息一个下午去做。然后花45分钟左右的时间解决这个问题,直到您真正理解它为止。

一切都非常好,但是您应该进行测试,以期通常可以正确地实现循环界限(以及代码的其余部分)。


4
对于OP的技能,我不会那么自信。容易犯边界错误,尤其是在压力很大的情况下,例如招聘面试。有经验的开发人员也可以犯这些错误,但是显然,有经验的开发人员首先要通过测试避免此类错误。
Arseni Mourzenko '16

3
@MainMa-我认为虽然Mark可能会更敏感,但我认为他是对的-面试压力很大,只是一起修改代码而没有适当考虑定义问题。问题的措辞方式非常明确地指出了后者,从长远来看,最好的方法是确保您有坚实的基础,而不是通过破坏IDE来解决
James Snell

@JamesSnell我认为您对自己变得越来越自信。查看代码,然后告诉我是什么让您认为其文档不足?如果您清楚地看到没有地方提到我无法解决问题?我只是想知道如何避免重复同样的错误。我认为您可以一次性完成所有程序的设置。
CodeYogi '16

4
@CodeYogi如果您必须进行“反复试验”,并且对代码感到“沮丧”和“犯同样的错误”,则表明您在开始编写之前对问题的理解不够充分。没有人说您不理解它,但是您的代码本可以被更好地考虑,并且它们表明您正在苦苦挣扎,您可以选择继续学习或不学习。
詹姆斯·斯内尔

2
@CodeYogi ...而且,正如您所问,我很少会出现循环和分支错误的情况,因为我着重于在编写代码之前清楚地了解需要实现的目标,因此对诸如排序这样的简单操作并不难数组类。作为程序员,最困难的事情之一就是承认您是问题所在,但是除非您这样做,否则您将不会开始编写真正的好代码。
詹姆斯·斯内尔

3

也许我应该在评论中充实一点:

只有一种方法:更好地了解您的问题。但这与您的问题一样普遍

你的意思是

尽管经过反复尝试后我的假设是正确的,但是由于头脑中缺乏正确的计算模型,我感到非常沮丧。

当我阅读时trial and error,我的闹钟响起。当然,我们中的许多人都知道心态,当一个人想解决一个小问题并把头缠在其他事情上时,就开始以一种或另一种方式进行猜测,以使代码seem可以执行,应该执行的操作。一些骇人听闻的解决方案就是由此产生的-其中一些是纯粹的天才 ; 但说实话:大多数不是。我包括在内,知道这种状态。

与您的具体问题无关,您提出了有关如何改进的问题:

1)测试

那是别人说的,我没有什么可补充的

2)问题分析

很难对此提出建议。我只能给您两个提示,这可能有助于您提高该主题的技能:

  • 从长远来看,最明显,最琐碎的方法是最有效的:解决许多问题。在练习和重复时,您会养成思维定式,可以帮助您完成将来的任务。编程就像通过努力练习可以改善的任何其他活动一样

代码Katas是一种方法,可能会有所帮助。

您如何成为一名出色的音乐家?它有助于了解理论并了解仪器的力学原理。拥有人才会有所帮助。但是最终,伟大来自实践。一遍又一遍地运用理论,每次都使用反馈使自己变得更好。

卡塔代码

我非常喜欢的一个站点:Code Wars

通过挑战获得精通能力通过与其他人一起进行有关实际代码挑战的培训来提高技能

它们是相对较小的问题,可帮助您提高编程技能。我最喜欢Code Wars的是,您可以将您的解决方案与其他解决方案进行比较。

或者,也许您应该在Exercism.io上获得社区的反馈。

  • 另一个建议几乎是微不足道的:学习解决问题 您必须训练自己,将问题分解为很小的问题。如果您说的话,您在编写循环时会遇到问题,则会犯错,即您将循环视为一个整体结构,而不会将其分解为多个部分。如果您学会逐步解决问题,就可以避免这种错误。

我知道-正如我上面所说的,有时您处于这样的状态-很难将“简单”的事情分解为更多“简单的死”任务;但这很有帮助。

我记得,当我第一次学习专业编程时,在调试代码时遇到了很多问题。怎么了 Hybris-错误不能出现在这样的代码区域中,因为我知道这不可能。结果呢?我浏览了代码而不是分析 我必须学习的代码,即使将我的代码分解为指令是很乏味的。

3)开发工具带

除了了解您的语言和工具(我知道这些是开发人员首先想到的闪亮事物)之外,还学习算法(又名阅读)。

这是两本书的开头:

这就像学习一些食谱以开始烹饪。起初,您不知道该怎么做,因此您必须先看一下以前的厨师为您做的。同样适用于算法。算法就像是普通饭菜的烹饪食谱(数据结构,排序,哈希等)。如果您内心(至少尝试)知道它们,那么您将有一个很好的起点。

3a)了解编程构造

这一点是衍生的-可以这么说。了解您的语言-并且更好:知道,使用您的语言可以构造什么

坏或inefficent代码的公共点有时,程序员不知道不同类型的循环之间的差异(for-while-do-loops)。它们都可以互换使用。但是在某些情况下,选择其他循环结构会导致代码更优美。

还有Duff的设备 ...

PS:

否则,您的评论不如Donal Trump好。

是的,我们应该使编码再次出色!

Stackoverflow的新格言。


啊,让我非常认真地告诉你一件事。我一直在做您提到的所有事情,甚至可以给我我在这些站点上的链接。但是令我感到沮丧的是,我没有得到问题的答案,而是获得了有关编程的所有建议。pre-post到目前为止,只有一个人提到过这种情况,对此我表示感谢。
CodeYogi '16

从您所说的话,很难想象问题出在哪里。也许隐喻会有所帮助:对我而言,这就像说“我怎么能看到”一样-对我来说显而易见的答案是“用你的眼睛”,因为眼见为我,我自然无法想象,怎么也看不到。您的问题也是如此。
Thomas Junk

完全同意“尝试和错误”的警钟。我认为,充分学习解决问题的心态的最好方法是使用纸和铅笔运行算法和代码。
通配符

嗯...为什么您对编程的回答中间有一个不合时宜的语法差劲的嘲讽政治候选人?
通配符

2

如果我正确理解了问题,那么您的问题是如何从一开始就尝试正确地获得循环,而不是如何确保您的循环正确(对此答案将按照其他答案中的说明进行测试)。

我认为一种好的方法是编写没有任何循环的第一次迭代。完成此操作后,您应该注意到在迭代之间应更改的内容。

是数字,例如0或1?然后,您很可能需要for和bingo,您也有起步i。然后想想您想运行同一件事多少次,并且您也将拥有最终条件。

如果您不确切知道它将运行多少次,则不需要for,而是一会儿或一会儿。

从技术上讲,任何循环都可以转换为任何其他循环,但是如果使用正确的循环,则代码更易于阅读,因此这里有一些技巧:

  1. 如果发现自己在for中编写了if(){...; break;},则需要一段时间,并且您已经具备了条件

  2. 在任何语言中,“虽然”可能是最常用的循环,但是不应该使用imo。如果您发现自己正在写bool ok = True; while(check){做一些事情,希望可以在某个时候改变ok};那么您就不需要一会儿了,而是一会儿,因为这意味着您拥有运行第一次迭代所需的一切。

现在有点上下文...当我第一次学习编程(帕斯卡)时,我不会说英语。对于我来说,“ for”和“ while”并没有多大意义,但是“ repeat”(在C语言中执行)关键字在我的母语中几乎是相同的,因此我会在所有内容中使用它。在我看来,重复(do while)是最自然的循环,因为几乎总是您想做某事,然后又想一次又一次地做,直到达到目标。“ For”只是一个捷径,它为您提供了迭代器,并将条件怪异地放置在代码的开头,即使您几乎总是希望完成某件事,直到发生某件事为止。另外,while只是if(){do while()}的快捷方式。快捷键很适合以后使用,


2

我将给出一个更详细的示例,说明如何使用前置/后置条件和不变式来开发正确的循环。此类断言一起称为规范或合同。

我不建议您尝试为每个循环执行此操作。但是我希望您会发现查看所涉及的思维过程很有用。

为此,我将把您的方法转换为一个名为Microsoft Dafny工具,该工具旨在证明此类规范的正确性。它还检查每个循环的终止。请注意,Dafny没有for循环,因此我不得不使用while循环。

最后,我将展示如何使用这些规范来设计一个可以说是稍微简单些的循环版本。这个更简单的循环版本实际上确实具有循环条件j > 0和赋值array[j] = value-就像您的最初直觉一样。

Dafny将为我们证明这两个循环都是正确的,并且做同样的事情。

然后,根据我的经验,我将对如何编写正确的向后循环做出一般性声明,如果将来遇到这种情况,这可能会对您有所帮助。

第一部分-编写方法规范

我们面临的第一个挑战是确定该方法的实际作用。为此,我设计了指定方法行为的前提条件和条件条件。为了使规范更加精确,我增强了方法以使其返回value插入位置的索引。

method insert(arr:array<int>, rightIndex:int, value:int) returns (index:int)
  // the method will modify the array
  modifies arr
  // the array will not be null
  requires arr != null
  // the right index is within the bounds of the array
  // but not the last item
  requires 0 <= rightIndex < arr.Length - 1
  // value will be inserted into the array at index
  ensures arr[index] == value 
  // index is within the bounds of the array
  ensures 0 <= index <= rightIndex + 1
  // the array to the left of index is not modified
  ensures arr[..index] == old(arr[..index])
  // the array to the right of index, up to right index is
  // shifted to the right by one place
  ensures arr[index+1..rightIndex+2] == old(arr[index..rightIndex+1])
  // the array to the right of rightIndex+1 is not modified
  ensures arr[rightIndex+2..] == old(arr[rightIndex+2..])

该规范完全捕获了该方法的行为。我对此规范的主要观察结果是,如果将值传递给rightIndex+1而不是,则将简化该过程rightIndex。但是由于我看不到从何处调用此方法,所以我不知道更改将对程序的其余部分产生什么影响。

第二部分-确定循环不变性

现在我们有了方法行为的规范,我们必须添加循环行为的规范,这将使Dafny确信执行循环将终止并导致所需的最终状态array

以下是您的原始循环,将其翻译为Dafny语法并添加了循环不变式。我也将其更改为返回插入值的索引。

{
    // take a copy of the initial array, so we can refer to it later
    // ghost variables do not affect program execution, they are just
    // for specification
    ghost var initialArr := arr[..];


    var j := rightIndex;
    while(j >= 0 && arr[j] > value)
       // the loop always decreases j, so it will terminate
       decreases j
       // j remains within the loop index off-by-one
       invariant -1 <= j < arr.Length
       // the right side of the array is not modified
       invariant arr[rightIndex+2..] == initialArr[rightIndex+2..]
       // the part of the array looked at by the loop so far is
       // shifted by one place to the right
       invariant arr[j+2..rightIndex+2] == initialArr[j+1..rightIndex+1]
       // the part of the array not looked at yet is not modified
       invariant arr[..j+1] == initialArr[..j+1] 
    {
        arr[j + 1] := arr[j];
        j := j-1;
    }   
    arr[j + 1] := value;
    return j+1; // return the position of the insert
}

这在Dafny中进行了验证。您可以通过以下链接自己查看。因此,您的循环可以正确实现我在第一部分中编写的方法规范。您将需要确定此方法规范是否确实是您想要的行为。

请注意,Dafny在此处提供了正确性证明。这比通过测试可能获得的正确性要强得多。

第三部分-更简单的循环

现在,我们有了捕获循环行为的方法规范。我们可以安全地修改循环的实现,同时仍然可以确信我们没有改变循环的行为。

我已经修改了循环,使其与您有关循环条件和的最终值的原始直觉相匹配j。我认为这个循环比您在问题中描述的循环更简单。它通常j比能够使用j+1

  1. 从开始j rightIndex+1

  2. 将循环条件更改为 j > 0 && arr[j-1] > value

  3. 将分配更改为 arr[j] := value

  4. 在循环结束而不是开始时减少循环计数器

这是代码。注意,循环不变式现在也更容易编写:

method insert2(arr:array<int>, rightIndex:int, value:int) returns (index:int)
  modifies arr
  requires arr != null
  requires 0 <= rightIndex < arr.Length - 1
  ensures 0 <= index <= rightIndex + 1
  ensures arr[..index] == old(arr[..index])
  ensures arr[index] == value 
  ensures arr[index+1..rightIndex+2] == old(arr[index..rightIndex+1])
  ensures arr[rightIndex+2..] == old(arr[rightIndex+2..])
{
    ghost var initialArr := arr[..];
    var j := rightIndex+1;
    while(j > 0 && arr[j-1] > value)
       decreases j
       invariant 0 <= j <= arr.Length
       invariant arr[rightIndex+2..] == initialArr[rightIndex+2..]
       invariant arr[j+1..rightIndex+2] == initialArr[j..rightIndex+1]
       invariant arr[..j] == initialArr[..j] 
    {
        j := j-1;
        arr[j + 1] := arr[j];
    }   
    arr[j] := value;
    return j;
}

第四部分-关于反向循环的建议

在写了很多年并证明正确的许多循环之后,我对反向循环有以下一般建议。

如果在循环的开始而不是在循环的末尾执行递减操作,几乎总是更容易考虑和编写反向(递减)循环。

不幸的是for,许多语言中的循环构造使这一点变得困难。

我怀疑(但不能证明)这种复杂性是导致您对回路应该是什么以及回路真正需要是什么的直觉有所不同的原因。您习惯于思考正向(递增)循环。当您想编写一个向后(递减)循环时,您可以尝试通过尝试反转向前(递增)循环中发生的顺序来创建循环。但是由于for构造的工作方式,您忽略了反转分配和循环变量更新的顺序-真正反转反向和正向循环之间的操作顺序是必需的。

第五部分-奖金

仅出于完整性考虑,这是您传递rightIndex+1给方法而不是时获得的代码rightIndex。此更改消除了+2考虑循环正确性所需的所有偏移。

method insert3(arr:array<int>, rightIndex:int, value:int) returns (index:int)
  modifies arr
  requires arr != null
  requires 1 <= rightIndex < arr.Length 
  ensures 0 <= index <= rightIndex
  ensures arr[..index] == old(arr[..index])
  ensures arr[index] == value 
  ensures arr[index+1..rightIndex+1] == old(arr[index..rightIndex])
  ensures arr[rightIndex+1..] == old(arr[rightIndex+1..])
{
    ghost var initialArr := arr[..];
    var j := rightIndex;
    while(j > 0 && arr[j-1] > value)
       decreases j
       invariant 0 <= j <= arr.Length
       invariant arr[rightIndex+1..] == initialArr[rightIndex+1..]
       invariant arr[j+1..rightIndex+1] == initialArr[j..rightIndex]
       invariant arr[..j] == initialArr[..j] 
    {
        j := j-1;
        arr[j + 1] := arr[j];
    }   
    arr[j] := value;
    return j;
}

2
如果您
不赞成,我们

2

获得这个权利很容易流畅是一个经验问题。即使该语言不允许您直接表达它,或者您使用的情况比简单的内置事物所能处理的情况还复杂,但您认为这是一个更高的层次,例如“按顺序访问每个元素”,经验丰富的编码人员可以将其立即转换为正确的细节,因为他已经完成了很多次。

即使这样,在更复杂的情况下也很容易出错,因为您所写的东西通常不是罐头普通的东西。使用更现代的语言和库,您不会编写简单的东西,因为存在罐头构造或为此而调用。在C ++中,现代的口头禅是“使用算法而不是编写代码”。

因此,特别是对于这种事情,确保正确的方法是查看边界条件。在事情改变边缘的少数情况下,通过脑海中的代码进行跟踪。如果是index == array-max,会发生什么?那max-1呢 如果代码转错了,它将处于这些边界之一。一些循环需要担心第一个或最后一个元素以及循环构造正确地确定边界。例如,如果您指的是最小值a[I]a[I-1]什么时候会发生什么I

另外,看看(正确的)迭代次数极端的情况:如果边界满足并且您将有0次迭代,那么在没有特殊情况的情况下是否可以正常工作?那么,仅1次迭代又是最低界限又是最高界限呢?

检查边缘情况(每个边缘的两面)是编写循环时应执行的操作,以及在代码审查中应执行的操作。


1

我将尽力避免已经提到的主题。

有什么工具/心理模型可以避免此类错误?

工具类

对我来说,最大的工具来写出更好的forwhile循环不写任何forwhile所有循环。

大多数现代语言都尝试以某种方式来解决此问题。例如,Java Iterator从一开始就使用起来有点笨拙,但从一开始就是正确的,但它引入了捷径语法,以便在以后的发行版中更容易使用它们。C#也有它们,等等。

我目前看好的语言,Ruby中,已对功能的方法(.each.map等)全前面。这是非常强大的。我只是在我正在研究的一些Ruby代码库中做了一个快速计数:在大约10.000行代码中,零for和5左右while

如果我被迫选择一种新语言,那么在优先级列表上寻找像这样的基于功能/数据的循环将非常重要。

心理模型

请记住,这while是您可以获得的最低的抽象限制,仅比上面高一个步骤goto。在我看来,这for会使情况变得更糟而不是更好,因为它将循环的所有三个部分紧密地分块在一起。

因此,如果我在使用的环境中for,那么我要确保所有这三个部分都是简单且始终相同的。这意味着我会写

limit = ...;
for (idx = 0; idx < limit; idx++) { 

但是没有什么更复杂的了。也许,也许我有时会倒数,但我会尽力避免倒数。

如果使用while,则不会出现涉及循环条件的内在复杂的内部恶臭。内部的测试while(...)将尽可能简单,而我将尽可能地避免break。同样,循环将很短(计算行数),并且将排除更多的代码量。

尤其是当实际的while条件很复杂时,我将使用很容易发现的“ condition-variable”,而不是将条件放入while语句本身:

repeat = true;
while (repeat) {
   repeat = false; 
   ...
   if (complex stuff...) {
      repeat = true;
      ... other complex stuff ...
   }
}

(或者,当然,应该采取适当措施。)

这为您提供了一个非常简单的思维模型,即“此变量从0到10单调运行”或“此循环运行直到该变量为false / true”。大多数人似乎都能够处理这种抽象水平。

希望能有所帮助。


1

特别是反向循环可能很难推理,因为在通用的for循环语法中以及通过使用从零开始的半开放间隔,我们的许多编程语言都偏向于正向迭代。我并不是说做出这些选择的语言是错误的。我只是说这些选择使反向循环的考虑变得复杂。

通常,请记住,for循环只是围绕while循环构建的语法糖:

// pseudo-code!
for (init; cond; step) { body; }

等效于:

// pseudo-code!
init;
while (cond) {
  body;
  step;
}

(可能具有额外的作用域层,以使在init步骤中声明的变量保持在循环本地)。

这对于许多种循环来说都很好,但是当您向后走时,使步骤最后一步很尴尬。当向后工作时,我发现以所需的值之后的值开始循环索引,并将该step部分移至循环的顶部较为容易,如下所示:

auto i = v.size();  // init
while (i > 0) {  // simpler condition because i is one after
    --i;  // step before the body
    body;  // in body, i means what you'd expect
}

或者,作为for循环:

for (i = v.size(); i > 0; ) {
    --i;  // step
    body;
}

这似乎令人不安,因为step表达式位于主体而不是标题中。这是for循环语法中固有的前向偏置的不幸副作用。因此,有人会说您改为这样做:

for (i = v.size() - 1; i >= 0; --i) {
    body;
}

但这是灾难,如果您的索引变量是无符号类型(可能在C或C ++中)。

考虑到这一点,让我们编写您的插入函数。

  1. 由于我们将向后工作,因此我们将循环索引作为“当前”数组插槽之后的条目。我将函数设计为采用整数的大小,而不是最后一个元素的索引,因为半开范围是大多数编程语言中表示范围的自然方式,并且因为它为我们提供了一种无需求助即可表示空数组的方式达到-1之类的魔术值。

    function insert(array, size, value) {
      var j = size;
    
  2. 当新值小于上一个元素时,请继续移动。当然,前一个元素只能如果有检查以前的元素,所以我们首先要检查我们不是在开始的时候:

      while (j != 0 && value < array[j - 1]) {
        --j;  // now j become current
        array[j + 1] = array[j];
      }
    
  3. 这样j就可以在我们想要新值的地方。

      array[j] = value; 
    };
    

乔恩·本特利(Jon Bentley)编写的《编程Pearls》对插入排序(和其他算法)给出了非常清晰的解释,这可以帮助您为这类问题建立思维模型。


0

您是否只是对for循环的实际作用及其工作原理感到困惑?

for(initialization; condition; increment*)
{
    body
}
  1. 首先,执行初始化
  2. 然后检查条件
  3. 如果条件为真,则主体运行一次。如果不是#6
  4. 增量代码被执行
  5. 转到#2
  6. 循环结束

使用不同的结构重写此代码可能对您很有用。这与使用while循环相同:

initialization
while(condition)
{
    body
    increment
}

以下是一些其他建议:

  • 您可以使用诸如foreach循环之类的另一种语言构造吗?这将为您处理条件和增量步骤。
  • 可以使用地图或过滤器功能吗?某些语言具有带有这些名称的功能,这些功能会在内部为您遍历一个集合。您只需要提供收藏和身体。
  • 您应该花更多的时间熟悉for循环。您将一直使用它们。我建议您在调试器中逐步执行for循环,以查看其执行方式。

*请注意,尽管我使用了“增量”一词,但这只是一些代码,它位于正文之后,状态检查之前。可以递减,也可以完全不递。


1
为什么要下票?
user2023861 '16

0

尝试更多见解

对于带有循环的非平凡算法,您可以尝试以下方法:

  1. 创建一个具有4个位置的固定数组,并放入一些值以模拟问题;
  2. 编写您的算法来解决给定的问题没有任何循环,带有硬编码的索引
  3. 之后,用一些变量或代替代码中的硬编码索引,并根据需要增加/减少这些变量(但仍然没有任何循环);ij
  4. 重写您的代码,并将重复块放入循环中,满足前置条件和后置条件;
  5. [ 可选 ]将循环重写为您想要的形式(for / while / do while);
  6. 最重要的是正确编写算法;之后,您可以根据需要重构并优化代码/循环(但这可能会使代码对于读者而言不再是平凡的)

你的问题

//TODO: Insert the given value in proper position in the sorted subarray
function insert(array, rightIndex, value) { ... };

多次手动编写循环主体

让我们使用一个具有4个位置的固定数组,并尝试手动编写算法,而不使用循环:

           //0 1 2 3
var array = [2,5,9,1]; //array sorted from index 0 to 2
var leftIndex = 0;
var rightIndex = 2;
var value = array[3]; //placing the last value within the array in the proper position

//starting here as 2 == rightIndex

if (array[2] > value) {
    array[3] = array[2];
} else {
    array[3] = value;
    return; //found proper position, no need to proceed;
}

if (array[1] > value) {
    array[2] = array[1];
} else {
    array[2] = value;
    return; //found proper position, no need to proceed;
}

if (array[0] > value) {
    array[1] = array[0];
} else {
    array[1] = value;
    return; //found proper position, no need to proceed;
}

array[0] = value; //achieved beginning of the array

//stopping here because there 0 == leftIndex

重写,删除硬编码的值

//consider going from 2 to 0, going from "rightIndex" to "leftIndex"

var i = rightIndex //starting here as 2 == rightIndex

if (array[i] > value) {
    array[i+1] = array[i];
} else {
    array[i+1] = value;
    return; //found proper position, no need to proceed;
}

i--;
if (i < leftIndex) {
    array[i+1] = value; //achieved beginning of the array
    return;
}

if (array[i] > value) {
    array[i+1] = array[i];
} else {
    array[i+1] = value;
    return; //found proper position, no need to proceed;
}

i--;
if (i < leftIndex) {
    array[i+1] = value; //achieved beginning of the array
    return;
}

if (array[i] > value) {
    array[i+1] = array[i];
} else {
    array[i+1] = value;
    return; //found proper position, no need to proceed;
}

i--;
if (i < leftIndex) {
    array[i+1] = value; //achieved beginning of the array
    return;
}

//stopping here because there 0 == leftIndex

翻译成循环

while

var i = rightIndex; //starting in rightIndex

while (true) {
    if (array[i] > value) { //refactor: this can go in the while clause
        array[i+1] = array[i];
    } else {
        array[i+1] = value;
        break; //found proper position, no need to proceed;
    }

    i--;
    if (i < leftIndex) { //refactor: this can go (inverted) in the while clause
        array[i+1] = value; //achieved beginning of the array
        break;
    }
}

按照您想要的方式重构/重写/优化循环:

while

var i = rightIndex; //starting in rightIndex

while ((array[i] > value) && (i >= leftIndex)) {
    array[i+1] = array[i];
    i--;
}

array[i+1] = value; //found proper position, or achieved beginning of the array

for

for (var i = rightIndex; (array[i] > value) && (i >= leftIndex); i--) {
    array[i+1] = array[i];
}

array[i+1] = value; //found proper position, or achieved beginning of the array

PS:代码假定输入有效,并且该数组不包含重复项;


-1

这就是为什么我尽可能避免编写对原始索引进行操作的循环,而支持更抽象的操作的原因。

在这种情况下,我将执行以下操作(使用伪代码):

array = array[:(rightIndex - 1)] + value + array[rightIndex:]

-3

在您的示例中,循环的主体非常明显。很明显,必须在最后更改某些元素。因此,您编写了代码,但没有循环的开始,结束条件和最终赋值。

然后远离代码,找出需要执行的第一步。您更改循环的开始,以便第一步正确。您再次远离代码,找出哪一步是需要执行的最后一步。您更改结束条件,以便最后一步是正确的。最后,您离开了代码,弄清楚最终的分配必须是什么,并相应地修改了代码。


1
你能举个例子吗?
CodeYogi

OP有一个例子。
gnasher729 '16

2
你什么意思?我是OP。
CodeYogi '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.