올바른 루프를 작성하는 방법?


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] = value 또는 array [j] = value인지 혼동되었습니다.

그러한 실수를 피하기위한 도구 / 정신 모델은 무엇입니까?


6
어떤 상황에서 이것이 j >= 0실수 라고 생각 하십니까? 나는 당신이 액세스 array[j]하고 있다는 것을 array[j + 1]먼저 확인하지 않고 있다는 사실에 더 조심할 것 입니다 array.length > (j + 1).
벤 Cottrell

5
@LightnessRacesinOrbit의 말과 유사하게 이미 해결 된 문제를 해결하고있을 가능성이 있습니다. 일반적으로 데이터 구조를 처리해야하는 루핑은 일부 핵심 모듈이나 클래스 ( Array.prototypeJS 예에서) 에 이미 존재합니다 . 이렇게하면 map모든 어레이에서 작동하는 것과 같은 에지 조건이 발생하지 않습니다 . 슬라이스와 연결을 사용하여 위의 문제를 해결하여 모두 함께 루프를 피할 수 있습니다. codepen.io/anon/pen/ZWovdg?editors=0012 루프를 작성하는 가장 올바른 방법은 전혀 쓰지 않는 것입니다.
Jed Schneider

13
실제로 해결 된 문제를 해결하십시오. 이것을 연습이라고합니다. 그냥 게시하지 않아도됩니다. 즉, 솔루션을 개선 할 수있는 방법이 없다면 말입니다. 즉, 바퀴를 재발 명하는 것은 바퀴보다 더 많은 것을 포함합니다. 전체 휠 품질 관리 시스템과 고객 지원이 필요합니다. 여전히 커스텀 림이 좋습니다.
candied_orange

53
나는 우리가 여기 잘못된 방향으로 향하고있는 것을 두려워한다. 그의 예제가 잘 알려진 알고리즘의 일부이기 때문에 CodeYogi를 포기하는 것은 다소 근거가 없습니다. 그는 자신이 새로운 것을 발명했다고 주장하지 않았다. 그는 루프를 작성할 때 매우 일반적인 경계 오류를 피하는 방법을 묻고 있습니다. 라이브러리는 먼 길을 왔지만 루프를 작성하는 방법을 알고있는 사람들에게는 여전히 미래가 있습니다.
candied_orange

5
일반적으로 루프와 인덱스를 다룰 때는 인덱스가 요소 사이를 가리키고 반 열린 간격에 익숙해 져야 한다는 사실을 알아야 합니다 (실제로 같은 개념의 양면). 이러한 사실을 알게되면 많은 루프 / 인덱스 헤드 스크래칭이 완전히 사라집니다.
Matteo Italia

답변:


208

테스트

아니오, 진지하게 테스트하십시오.

나는 20 년 이상 코딩을 해 왔으며 여전히 처음으로 루프를 올바르게 작성하는 것을 믿지 않습니다. 작동한다고 의심되기 전에 작동하는지 입증하는 테스트를 작성하고 실행합니다. 모든 경계 조건의 양쪽을 테스트하십시오. 예를 들어 rightIndex0의 a 는 무엇을해야합니까? -1은 어떻습니까?

단순하게 유지

다른 사람들이 한 눈에 볼 수 없다면 너무 어렵게 만드는 것입니다. 이해하기 쉬운 것을 쓸 수 있다면 성능을 무시하십시오. 드물게 필요할 때만 더 빠르게 만드십시오. 그리고 한 번이라도 당신이 정확히 무엇을 늦추고 있는지 정확히 알고 있다고 확신합니다. 실제 Big O 개선을 달성 할 수 있다면 이 액티비티는 의미가 없을 수 있지만, 가능하면 코드를 읽을 수있게 만드십시오.

하나만

손가락을 세는 것과 손가락 사이의 간격을 세는 것의 차이점을 알아야합니다. 때로는 공간이 실제로 중요한 것입니다. 손가락이주의를 산만하게하지 마십시오. 엄지 손가락이 손가락인지 확인하십시오. 새끼 손가락과 엄지 손가락 사이의 간격이 공백으로 계산되는지 확인하십시오.

코멘트

코드에서 길을 잃기 전에 영어의 의미를 말하십시오. 기대치를 명확하게 설명하십시오. 코드 작동 방식을 설명하지 마십시오. 왜 그 일을하는지 설명하십시오. 구현 세부 사항을 유지하십시오. 주석을 변경할 필요없이 코드를 리팩터링 할 수 있어야합니다.

가장 좋은 의견은 좋은 이름입니다.

좋은 이름으로 말해야 할 모든 것을 말할 수 있다면 주석으로 다시 말하지 마십시오.

추상화

객체, 함수, 배열 및 변수는 모두 주어진 이름만큼 좋은 추상화입니다. 사람들이 내부를 볼 때 그들이 찾은 것에 놀라지 않을 수 있도록 이름을 부여하십시오.

짧은 이름

단명 한 물건에는 짧은 이름을 사용하십시오. i작은 범위에서 멋진 타이트 루프의 인덱스에 대한 좋은 이름으로, 의미가 분명합니다. 경우 i충분히 삶과 혼동 될 수있는 다른 아이디어와 이름을 가진 줄 끝에서 라인을 통해 퍼져 얻을 i그것은 제공하는 시간 i편안한 긴 설명 이름을.

긴 이름

단순히 줄 길이를 고려하여 이름을 줄이지 마십시오. 코드를 배치하는 다른 방법을 찾으십시오.

공백

읽을 수없는 코드에 숨어있는 결함. 언어에 따라 들여 쓰기 스타일을 최소한 일관성있게 선택할 수 있다면. 코드를 잡음처럼 보이게 만들지 마십시오. 코드는 행진하는 것처럼 보일 것입니다.

루프 구문

귀하의 언어로 된 루프 구조를 배우고 검토하십시오. 디버거가 for(;;)루프를 강조하는 것을 보는 것은 매우 유익합니다. 모든 양식을 배우십시오. while, do while, while(true), for each. 멀리 갈 수있는 가장 간단한 것을 사용하십시오. 펌프 프라이밍을 찾아 보십시오 . 무엇을 배울 break그리고 continue당신이 그 (것)이있는 경우 않습니다. 의 차이를 알아야 c++하고 ++c. 항상 닫는 것이 필요한 모든 것을 닫는 한 일찍 돌아 오는 것을 두려워하지 마십시오. 마지막으로 열 때 자동 닫힘으로 표시되는 항목을 차단 하거나 선호하는 경우 : Using statement / Try with Resources .

루프 대안

가능하다면 다른 일을 반복하십시오. 눈에 더 쉽고 이미 디버깅되었습니다. 다음은 여러 형태로 제공 : 컬렉션 또는 허용 스트림 map(), reduce(), foreach(), 및 기타 방법 람다를 적용합니다. 와 같은 특수 기능을 찾으십시오 Arrays.fill(). 재귀도 있지만 특별한 경우에는 일을 쉽게하기를 기대합니다. 대체 방법이 무엇인지 알 때까지 일반적으로 재귀를 사용하지 마십시오.

아, 그리고 테스트.

테스트, 테스트, 테스트

테스트에 대해 언급 했습니까?

한가지 더있었습니다. 기억이 안나 T로 시작 ...


36
좋은 대답이지만 테스트를 언급해야 할 수도 있습니다. 단위 테스트에서 무한 루프를 어떻게 처리합니까? 루프가 테스트를 '중단'하지 않습니까 ???
GameAlchemist

139
@GameAlchemist 피자 테스트입니다. 코드가 제 시간에 실행을 멈추지 않으면 피자를 만드는 데 걸리는 것이 잘못되었다고 생각하기 시작합니다. Alan Turing의 멈춤 문제에 대한 치료법은 아니지만 적어도 나는 피자를 거래에서 제외시킵니다.
candied_orange

12
@CodeYogi-실제로 매우 가까워 질 수 있습니다 . 단일 값에서 작동하는 테스트로 시작하십시오. 루프없이 코드를 구현하십시오. 그런 다음 두 가지 값으로 작동하는 테스트를 작성하십시오. 루프를 구현하십시오. 이다 매우 당신이 그런 실수를 할 경우 거의 모든 상황 중 하나 또는 두 가지 테스트의 다른 실패 때문에, 당신은 당신이 이런 식으로 할 경우 루프에 잘못된 경계 조건을 얻을 것이다 가능성.
Jules

15
@CodeYogi Dude는 TDD에게만 지불해야하지만 테스트는 >> 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]));
};

이제 루프가 간단합니다 ( j에서 rightIndex로 이동 0).

마지막으로 이제 테스트해야합니다.

  • 경계 조건을 생각하십시오 ( rightIndex == 0, rightIndex == array.size - 2)
  • 생각 value보다 작은 것 array[0]보다 더 이상array[rightIndex]
  • 생각 value에 동일하고 array[0], array[rightIndex]또는 일부 중간 색인

또한 퍼징을 과소 평가하지 마십시오 . 실수를 잡으려면 어설 션이 있으므로 임의의 배열을 생성하고 방법을 사용하여 정렬하십시오. 어설 션이 발생하면 버그가 발견되어 테스트 스위트를 확장 할 수 있습니다.


8
@ CodeYogi : With ... 테스트. 문제는 어설 션으로 모든 것을 표현하는 것이 비현실적 일 수있다 . 어설 션이 단순히 코드를 반복한다면 새로운 것을 가져 오지 않는다 (반복은 품질에 도움이되지 않는다). 이것이 내가 루프에서 주장하지 않은 이유 0 <= j <= rightIndex또는 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 년 이후 올바른 루프를 작성하는 데 도움이됩니다. "아직도 이러한 기술은 10 년 이상 알려져 있지만 대부분의 사람들은 그 기술을 들어 본 적이 없습니다."
Joker_vD

1
@MatthieuM. 나는 그것이 매우 낮은 가치에 동의합니다. 그러나 나는 그것이 복사 / 붙여 넣기 때문에 발생한다고 생각하지 않습니다. insert()이전 배열에서 새 배열로 복사하여 작동 하도록 리팩토링 하고 싶다고 가정 해보 십시오. 그것은 당신의 다른 사람을 방해하지 않고 할 수 있습니다 assert. 그러나이 마지막 것은 아닙니다. 다른 사람이 얼마나 잘 assert설계 되었는지 보여줍니다 .
candied_orange

29

단위 테스트 / TDD 사용

for루프를 통해 시퀀스에 액세스해야하는 경우 단위 테스트 , 특히 테스트 중심 개발을 통해 실수를 피할 수 있습니다 .

0보다 우수한 값을 역순으로 취하는 메소드를 구현해야한다고 상상해보십시오. 어떤 테스트 사례를 생각할 수 있습니까?

  1. 시퀀스에는 0보다 우수한 하나의 값이 포함됩니다.

    실제 : [5]. 예상 : [5].

    요구 사항을 충족하는 가장 간단한 구현은 단순히 소스 시퀀스를 호출자에게 반환하는 것입니다.

  2. 시퀀스에는 두 값이 모두 0보다 우수합니다.

    실제 : [5, 7]. 예상 : [7, 5].

    이제 시퀀스를 반환 할 수는 없지만 반전해야합니다. for (;;)루프 를 사용하겠습니까 , 다른 언어 구조 또는 라이브러리 방법은 중요하지 않습니다.

  3. 시퀀스에는 세 개의 값이 있으며 하나는 0입니다.

    실제 : [5, 0, 7]. 예상 : [7, 5].

    이제 코드를 변경하여 값을 필터링해야합니다. 다시, 이것은 if진술이나 선호하는 프레임 워크 메소드 호출을 통해 표현 될 수 있습니다 .

  4. 알고리즘에 따라 (이것은 화이트 박스 테스트이므로 구현이 중요하므로) 빈 시퀀스 [] → []케이스 를 처리해야 할 수도 있고 그렇지 않을 수도 있습니다. 또는 모든 값이 음수 인 경우를 [-4, 0, -5, 0] → []올바르게 처리하거나 경계 음수 값을 처리 할 수 [6, 4, -1] → [4, 6]; [-1, 6, 4] → [4, 6]있습니다. 그러나 대부분의 경우 위에서 설명한 세 가지 테스트 만 수행 할 수 있습니다. 추가 테스트를 수행해도 코드가 변경되지 않으므로 관련이 없습니다.

더 높은 추상화 수준에서 작업

그러나 대부분의 경우 기존 라이브러리 / 프레임 워크를 사용하여 더 높은 추상화 수준에서 작업하여 이러한 오류를 대부분 피할 수 있습니다. 이러한 라이브러리 / 프레임 워크를 통해 시퀀스를 되돌리기, 정렬, 분할 및 조인하고 배열 또는 이중 연결 목록 등에 값을 삽입하거나 제거 할 수 있습니다.

일반적으로 foreach대신 for경계 조건 검사를 사용하지 않고 사용할 수 있습니다 . 언어가 대신 사용 합니다. 파이썬과 같은 일부 언어에는 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

18
: 방에있는 코끼리를 언급 한 것에 대해 감사드립니다 루프를 사용하지 않는 전혀 . 사람들이 여전히 1985 년과 같이 코드를 작성하는 이유는 무엇입니까 (그리고 관대합니다). BOCTAOE.
Jared Smith

4
@JaredSmith 컴퓨터가 실제로 해당 코드를 실행하면 거기에 점프 명령이 없다는 것을 얼마나 내기하고 싶습니까? LINQ를 사용 하면 루프를 추상화 하지만 여전히 있습니다. 나는 화가 Shlemiel의 노력에 대해 배우지 못한 동료들에게 이것을 설명했다 . 루프에서 발생하는 위치를 이해하지 못하면 코드에서 추상화되고 결과적으로 코드를 훨씬 더 읽기 쉽게 읽을 수 있습니다. 거의 수정하지 않고 설명 할 수없는 성능 문제가 거의 항상 발생합니다.
CVn

6
@ MichaelKjörling : 당신은 LINQ를 사용할 때 루프가 존재하지만, 구조가이 루프의 매우 설명하지 않을 것이다 . 중요한 측면은 LINQ (Python의 목록 이해 및 다른 언어의 유사한 요소 포함)는 최소한 원래 질문의 범위 내에서 경계 조건이 대부분 관련이 없다는 것입니다. 그러나 LINQ를 사용할 때, 특히 게으른 평가와 관련하여 후드에서 일어나는 일을 이해해야 할 필요성에 대해 더 이상 동의하지 않습니다. for(;;)
Arseni Mourzenko

4
@ MichaelKjörling 나는 반드시 LINQ에 대해 이야기하지 않았고 나는 당신의 요점을 보지 못했습니다. forEach, map, LazyIterator, 등 언어의 컴파일러 나 런타임 환경을 제공하고 틀림없이 있습니다 있습니다 다시 각각의 반복에 페인트 통에 걸어 될 가능성이. 즉, 가독성 및 일대일 오류는 이러한 기능이 현대 언어에 추가 된 두 가지 이유입니다.
Jared Smith

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는 (길이-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 = 대신 루프 이후 (있는 경우) i == -1로 끝나는 것입니다. = 0.
TOOGAM

2
@TOOGAM (int i = length; i--;)은 0이 거짓으로 평가되기 때문에 C / C ++에서 작동하지만 모든 언어가 동일한 것은 아닙니다. 당신이 나
Bryce Wagner

당연히 > 0원하는 기능을 얻기 위해 " " 이 필요한 언어를 사용하는 경우 해당 언어에 필요한 문자가 필요하므로 이러한 문자를 사용해야합니다. 그러나 심지어 이러한 경우, 단지이 "사용 > 0"의 두 부분으로 처리하는 것보다 간단 다음 하나를 감산하고, 또한 사용하는 " >= 0". 약간의 경험을 통해 >= 0루프 테스트 조건에서 등호 (예 : " ")를 훨씬 덜 자주 사용해야하는 습관을들 이었고 결과 코드는 일반적으로 단순 해졌습니다.
TOOGAM

1
@BryceWagner 필요한 경우 i-- > 0클래식 농담을 시도해보십시오 i --> 0!
porglezomp

3
@porglezomp 아, 네, 운영자 에게 갑니다 . C, C ++, Java 및 C #을 포함한 대부분의 C와 같은 언어에는 그 언어가 있습니다.
CVn

11

머리에 올바른 컴퓨팅 모델이 없기 때문에 너무 좌절했습니다.

이 질문에 대한 매우 흥미로운 지적이며 다음과 같은 주석을 생성했습니다.

한 가지 방법 만 있습니다 : 문제를 더 잘 이해하십시오. 그러나 그것은 당신의 질문만큼 일반적입니다. – 토마스 정크

... 그리고 토마스가 옳아. 기능에 대한 명확한 의도가없는 것은 빨간색 플래그 여야합니다. 즉각 중지하고 연필과 종이를 잡고 IDE에서 발을 떼고 문제를 올바르게 분류해야한다는 명확한 표시입니다. 또는 최소한 당신이 한 일을 제대로 확인하십시오.

저자가 문제를 완전히 정의하기 전에 구현을 정의하려고 시도했기 때문에 완전한 혼란이 된 많은 함수와 클래스를 보았습니다. 그리고 다루기가 너무 쉽습니다.

문제를 완전히 이해하지 못하면 효율성이나 명확성 측면에서 최적의 솔루션을 코딩하지 않을 수도 있고 TDD 방법론에서 진정으로 유용한 단위 테스트를 만들 수도 없습니다.

여기에 코드를 예로 들어, 아직 고려하지 않은 많은 잠재적 결함이 있습니다.

  • rightIndex가 너무 낮 으면 어떻게해야합니까? (단서 : 데이터 손실 포함됨)
  • rightIndex가 배열 범위를 벗어나면 어떻게됩니까? (예외를 받거나 버퍼 오버플로를 만들었습니까?)

코드의 성능 및 디자인과 관련된 몇 가지 다른 문제가 있습니다.

  • 이 코드를 확장해야합니까? 배열을 최상의 옵션으로 정렬합니까, 아니면 다른 옵션 (링크 된 목록과 같은)을보아야합니까?
  • 당신의 가정을 확신 할 수 있습니까? (배열이 정렬되도록 보장 할 수 있습니까? 그렇지 않으면 어떻게해야합니까?)
  • 바퀴를 재창조하고 있습니까? 정렬 된 배열은 잘 알려진 문제입니다. 기존 솔루션을 연구 했습니까? 귀하의 언어로 이미 사용 가능한 솔루션이 있습니까 (예 : SortedList<t>C #)?
  • 한 번에 하나의 배열 항목을 수동으로 복사해야합니까? 또는 귀하의 언어가 JScript와 같은 공통 기능을 제공 Array.Insert(...)합니까? 이 코드가 더 명확합니까?

이 코드를 개선 할 수있는 방법은 많이 있지만,이 코드에 필요한 것을 올바르게 정의 할 때까지는 코드를 개발하지 않고 작동하기를 희망하여 함께 해킹하는 것입니다. 그것에 시간을 투자하면 인생이 쉬워 질 것입니다.


2
인덱스를 기존 함수 (예 : Array.Copy)로 전달하더라도 바인딩 된 조건을 올바르게 작성해야합니다. 길이가 0, 길이 1, 길이 2 인 상황을 상상해 보는 것이 너무 적거나 너무 많이 복사되지 않도록하는 가장 좋은 방법입니다.
브라이스 와그너

@BryceWagner-물론 사실이지만 어떤 문제에 대한 명확한 아이디어가 없다면 실제로 당신이 실제로 해결하는 데 많은 시간을 할애 할 수 있습니다. 이 시점에서 가장 큰 문제.
James Snell

2
@CodeYogi-다른 사람들이 지적한 것처럼 문제를 하위 문제로 다소 나쁘게 만들었으므로 많은 답변에서 문제를 피하는 방법으로 문제를 해결하는 방법을 언급합니다. 그것은 당신이 개인적으로 취해야 할 것이 아니며, 그곳에 있었던 우리의 경험입니다.
James Snell

2
@ CodeYogi,이 사이트를 스택 오버플로와 혼동했을 수 있습니다. 이 사이트는 컴퓨터 단말기가 아닌 화이트 보드에서 Q & A 세션 에 해당합니다. "코드 표시"는 잘못된 사이트에 있다는 것을 나타내는 명백한 표시입니다.
와일드 카드

2
@Wildcard +1 : "코드를 보여줘"는이 대답이 왜 옳은지에 대한 훌륭한 지표이며, 그것이 단지 그것이 인간적 요소 / 디자인 문제라는 것을 더 잘 설명하기 위해 노력해야 할 수도 있습니다. 인간 프로세스의 변화에 ​​의해 해결 될 것입니다.
제임스 스넬

10

일대일 오류 는 가장 일반적인 프로그래밍 실수 중 하나입니다. 숙련 된 개발자조차도 때때로이 오류가 발생합니다. 높은 수준의 언어는 일반적으로 명시 적 인덱싱을 피 foreach하거나 map피하는 반복 구조를 갖습니다 . 그러나 때로는 예와 같이 명시적인 색인 생성이 필요합니다.

도전은 배열 셀 의 범위 를 생각하는 방법 입니다. 명확한 정신 모델이 없으면 종점을 포함하거나 제외 할 때 혼란스러워집니다.

배열 범위를 설명 할 때 규칙은 하한포함하고 상한을 제외하는 것 입니다. 예를 들어 범위 0..3은 셀 0,1,2입니다. 이 규칙은 예를 들어, 0 인덱스 언어 전반에 걸쳐 사용되는 slice(start, end)자바 스크립트의 방법은 인덱스로 시작하는 부분 배열을 반환 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가 있으며 이는 5 개의 셀 0,1,2,3,4가 있음을 의미합니다. 이것은 또한 세그먼트의 길이가 더 높은 경계에서 하한을 뺀 것을 의미합니다. 즉, 세그먼트 2..5에는 5-2 = 3 개의 셀이 있습니다.

위 또는 아래로 반복 할 때이 모델을 염두에두면 끝점을 포함하거나 제외 할 때 훨씬 더 명확 해집니다. 위로 반복 할 때는 시작점을 포함시켜야하지만 종료점은 제외해야합니다. 아래쪽으로 반복 할 때는 시작점 (상한)을 제외하고 끝점 (하한)을 포함해야합니다.

코드에서 아래쪽으로 반복하므로 low bound 0을 포함해야하므로 while 반복 j >= 0합니다.

이 경우 rightIndex인수가 하위 배열의 마지막 인덱스를 나타내도록 선택 하면 규칙이 깨집니다. 즉, 반복에 두 끝점 (0 및 rightIndex)을 모두 포함해야합니다. 또한 빈 세그먼트 (정렬을 시작할 때 필요한)를 나타내 기가 어렵습니다. 첫 번째 값을 삽입 할 때는 실제로 -1을 rightIndex로 사용해야합니다. 이것은 부자연 스럽습니다. rightIndex세그먼트 다음에 인덱스를 나타내는 것이 더 자연스러워 보이 므로 0은 빈 세그먼트를 나타냅니다.

물론 코드는 정렬 된 하위 배열을 하나로 확장하여 처음 정렬 된 하위 배열 바로 다음에 항목을 덮어 쓰기 때문에 혼란 스럽습니다. 따라서 인덱스 j에서 읽지 만 값을 j + 1에 씁니다. 여기에서 삽입하기 전에 j가 초기 하위 배열의 위치임을 분명히 알 수 있습니다. 인덱스 작업이 너무 까다로워지면 그리드 종이에 다이어그램을 그리는 데 도움이됩니다.


4
@ CodeYogi : 종이에 작은 배열을 그리드로 그린 다음 연필로 루프를 수동으로 반복합니다. 이를 통해 실제 범위, 예를 들어 셀 범위가 오른쪽으로 이동하고 새 값이 삽입되는 위치를 명확하게 알 수 있습니다.
JacquesB

3
"컴퓨터 과학에는 캐시 무효화, 이름 지정 작업 및 일대일 오류라는 두 가지 어려운 작업이 있습니다."
Digital Trauma

1
@ CodeYogi : 내가 말하는 것을 보여주는 작은 다이어그램이 추가되었습니다.
JacquesB

1
위대한 통찰력 특히 마지막 두 파를 읽을 가치가 있습니다. 혼란은 for 루프의 특성으로 인해 발생합니다. 종료 전에 루프가 j 감소 한 번 올바른 인덱스를 찾음으로써 나에게 한 발짝 물려 있습니다.
CodeYogi

1
대단히. 우수한. 대답. 그리고이 포괄 / 배타적 인덱스 규칙은 또한 0의 "가장 오른쪽"인덱스보다 하나 이상인 myArray.Lengthor 의 값에 의해 동기가 부여된다고 myList.Count덧붙입니다. ... 설명의 길이는 이러한 명시 적 코딩 휴리스틱의 실용적이고 간단한 적용에 달려 있습니다. TL; DR 군중이 누락되었습니다.
radarbob

5

귀하의 질문에 대한 소개는 귀하가 올바르게 코딩하는 법을 배우지 못했다고 생각합니다. 몇 주 이상 동안 명령형 언어로 프로그래밍하는 사람은 90 %가 넘는 경우에 루프 루프를 처음부터 제대로 적용해야합니다. 아마도 당신은 문제를 충분히 생각하기 전에 코딩을 시작하기 위해 서두르고 있습니다.

나는 루프를 작성하는 방법을 (재) 학습함으로써이 결함을 바로 잡을 것을 제안한다. 그리고 나는 종이와 연필로 다양한 루프를 다루는 것을 권장한다. 이렇게하려면 오후를 쉬십시오. 그런 다음 실제로 얻을 때까지 45 분 정도 하루를 주제로 작업하십시오.

그것은 모두 잘 테스트되지만 일반적으로 루프 범위 (및 나머지 코드)를 올바르게 얻을 것으로 기대하고 테스트해야합니다.


4
나는 OP의 기술에 대해 너무 독단적이지 않을 것이다. 특히 채용 인터뷰와 같은 스트레스가 많은 상황에서 경계 실수를하는 것은 쉽습니다. 숙련 된 개발자도 이러한 실수를 할 수 있지만, 숙련 된 개발자는 테스트를 통해 이러한 실수를 피할 수 있습니다.
Arseni Mourzenko

3
@MainMa-Mark가 더 민감 할 수 있다고 생각하지만, 그가 옳다고 생각합니다-인터뷰 스트레스가 있으며 문제를 정의하지 않고 코드를 해킹하는 것만 있습니다. 질문이 후자를 매우 강력하게 지적하는 방식은 IDE에서 해킹하는 것이 아니라 탄탄한 토대를 갖도록함으로써 장기적으로 가장 잘 해결할 수있는 방법입니다.
James Snell

@JamesSnell 나는 당신이 자신에 대해 확신하고 있다고 생각합니다. 코드를보고 그 내용이 문서화되었다고 생각하는 이유를 알려주십시오. 당신이 명확하게 본다면, 그 문제를 해결할 수 없다는 언급이 없습니다. 나는 단지 같은 실수를 반복하지 않도록하는 방법을 알고 싶었습니다. 한 번에 모든 프로그램을 올바르게 얻을 수 있다고 생각합니다.
CodeYogi

4
@CodeYogi '시행과 오류'를 수행해야하고 코딩에 '좌절'하고 '같은 실수를 저지르는'경우, 글을 쓰기 전에 문제를 충분히 이해하지 못했다는 신호입니다 . 아무도 당신이 그것을 이해하지 못했다고 말하지는 않지만 코드가 더 잘 이해되었을 수 있으며, 당신이 선택하고 배우거나 선택하지 않기로 고군분투하고 있다는 신호입니다.
James Snell

2
@CodeYogi ... 그리고 당신이 묻는 이후로, 코드를 작성하기 전에 달성해야 할 것을 분명히 이해하기 때문에 루핑과 분기가 잘못되는 경우가 거의 없습니다. 정렬 된 것과 같은 간단한 일을하기가 어렵지 않습니다. 배열 클래스. 프로그래머로서 가장 어려운 일 중 하나는 문제라는 것을 인정하지만 그렇게 할 때까지는 정말 좋은 코드를 작성하지 않습니다.
제임스 스넬

3

아마도 내 의견에 육체를 넣어야 할 것입니다.

한 가지 방법 만 있습니다 : 문제를 더 잘 이해하십시오. 그러나 그것은 당신의 질문만큼 일반적입니다

요점은

몇 가지 시행 착오 후에 내 가정은 정확하지만 머리에 올바른 컴퓨팅 모델이 없기 때문에 너무 좌절했습니다.

을 읽으면 trial and error알람 벨이 울리기 시작합니다. 물론 우리 중 많은 사람들이 작은 문제를 해결하고 싶을 때 다른 일에 머리를 감고 다른 방법으로 추측하기 시작하여 코드 seem를 수행하기 위해해야 할 일을 염두에두고 있습니다. 일부 해킹 솔루션은 이것에서 나옵니다. 그리고 그들 중 일부는 순수한 천재입니다 . 그러나 정직하게 말하면 : 그들 대부분은 그렇지 않습니다 . 이 상태를 알면서도 포함시켰다.

구체적인 문제와 독립적으로 개선 방법에 대한 질문을했습니다.

1) 시험

그것은 다른 사람들이 말한 것이며 추가 할 가치가 없습니다.

2) 문제 분석

그것에 대한 조언을하기는 어렵습니다. 내가 줄 수있는 힌트는 두 가지뿐입니다.이 주제에 대한 기술을 향상시키는 데 도움이 될 것입니다.

  • 명백하고 가장 사소한 것은 장기적으로 가장 효과적입니다. 많은 문제를 해결하십시오. 연습하고 반복하면서 미래의 과제에 도움이되는 사고 방식을 개발할 수 있습니다. 열심히 연습함으로써 프로그래밍은 다른 활동과 마찬가지로 향상됩니다

코드 Katas는 약간 도움이 될 수있는 방법입니다.

훌륭한 음악가가 되려면 어떻게해야합니까? 이론을 이해하고 계측기의 역학을 이해하는 데 도움이됩니다. 재능을 갖는 데 도움이됩니다. 그러나 궁극적으로 위대함은 실천에서 비롯됩니다. 피드백을 사용하여 매번 더 나아질 수 있도록 이론을 반복해서 적용합니다.

코드 카타

내가 가장 좋아하는 사이트 : Code Wars

챌린지를 통해 숙달하기 실제 코드 챌린지에 대해 다른 사람들과 훈련하여 기술을 향상 시키십시오

비교적 작은 문제이므로 프로그래밍 기술을 연마하는 데 도움이됩니다. 그리고 내가 무엇에 가장 좋아하는 코드 전쟁은 비교할 수있다 당신 의 일에 솔루션을 다른 사람 .

또는 커뮤니티에서 피드백을받는 Exercism.io를 살펴 봐야 합니다.

  • 다른 조언은 거의 사소한 것입니다. 문제를 해결하는 법 배우기 자신을 훈련시키고 문제를 아주 작은 문제로 나눠야합니다. 루프작성하는 데 문제가 있다면 실수를 저지르고 루프를 전체 구조로보고 루프를 조각으로 분해하지 않는다고 말하십시오. 단계별로 물건을 나누는 법을 배우면 그러한 실수를 피하는 법을 배웁니다.

나는 위에서 말했듯이 때때로 당신은 그런 상태에 처해 있습니다. "단순한"것을 더 "죽은 단순한"작업으로 나누기가 어렵습니다. 하지만 많은 도움이됩니다.

전문 프로그래밍을 처음 배웠을 때 코드 디버깅에 문제가 있었다는 것을 기억 합니다. 문제는 무엇 이었습니까? Hybris는 - 내가 때문에 오류가, 코드 등과 같은 지역에서 수 없습니다 알고 는있을 수 없다. 그리고 결과적으로? 코드를 분석하는 대신 코드를 훑어 보았습니다 . 교육을 위해 코드를 다운시키는 것이 지루한 경우에도 배워야했습니다 .

3) 툴 벨트 개발

언어와 도구를 알고 게다가 - - 나는이 개발자가 처음 생각있는 반짝이는 것을 알고 알고리즘 학습 (읽기 일명).

다음은 두 권의 책으로 시작됩니다.

이것은 요리를 시작하는 몇 가지 요리법을 배우는 것과 같습니다. 처음에 당신은 무엇을 해야할지 모르므로 , 먼저 요리사 가 당신을 위해 요리 한 것을보아야 합니다. algortihms도 마찬가지입니다. 알고리즘은 일반적인 식사 (데이터 구조, 정렬, 해싱 등)에 대한 요리 레시피와 같습니다. 마음에 의해 알고 있으면 (적어도 시도), 좋은 출발점이됩니다.

3a) 프로그래밍 구조 이해

이 점은 파생 상품입니다. 당신의 언어를 아십시오-그리고 더 나은 : 당신의 언어 에서 어떤 구성이 가능한지 아 십시오.

나쁜 또는 inefficent 코드에 대한 일반적인 점은 프로그래머가 루프의 다른 유형의 차이점을 알고하지 않습니다, 때때로 (을 for-, while-그리고 do-loops). 그것들은 어떻게 든 상호 교환 가능합니다. 그러나 어떤 환경에서는 다른 루핑 구조를 선택하면 더 우아한 코드가 생성됩니다.

그리고 더프의 장치가 있습니다 ...

추신:

그렇지 않으면 귀하의 의견은 Donal Trump보다 좋지 않습니다.

예, 코딩을 다시 훌륭하게해야합니다!

Stackoverflow의 새로운 모토.


아, 한 가지 진지하게 말씀 드리겠습니다. 나는 당신이 언급 한 모든 것을하고 있으며 심지어 그 사이트에서 내 링크를 줄 수도 있습니다. 그러나 나를 실망시키는 것은 내 질문에 대한 대답을 얻는 대신 프로그래밍에 대한 가능한 모든 조언을 얻는 것입니다. pre-post지금까지 한 사람이 지금까지 언급 한 조건에 대해 감사합니다.
CodeYogi

당신이 말하는 것에서, 당신의 문제가 어디에 있는지 상상하기가 어렵습니다. 아마도 은유가 도움이 될 것입니다. 저에게는 "어떻게 볼 수 있습니까?"라고 말하는 것과 같습니다. 저에게 명백한 대답은 "당신의 눈을 사용하는 것"입니다. 귀하의 질문에 대해서도 마찬가지입니다.
Thomas Junk

"시도 및 오류"에 대한 경고음과 완전히 동의하십시오. 문제 해결 사고 방식을 완전히 배우는 가장 좋은 방법은 종이와 연필로 알고리즘과 코드를 실행하는 것입니다.
와일드 카드

음 ... 프로그래밍에 대한 답의 한가운데에 맥락없이 인용 된 정치 후보에 비평 등 한 문법적으로 가난한 b은 왜 있습니까?
와일드 카드

2

문제를 올바르게 이해했다면 귀하의 질문은 루프가 올바른지 확인하는 방법이 아니라 첫 번째 시도에서 루프를 얻는 방법입니다 (다른 답변에서 설명한대로 답변이 테스트되는지).

내가 좋은 접근법으로 생각하는 것은 루프없이 첫 번째 반복을 작성하는 것입니다. 이 작업을 수행 한 후에는 반복간에 무엇이 변경되어야하는지 알 수 있습니다.

0 또는 1과 같은 숫자입니까? 그렇다면 아마도 빙고가 필요할 것입니다. 그런 다음 같은 일을 몇 번 실행하고 싶은지 생각하면 최종 조건도 갖게됩니다.

정확히 몇 번 실행되는지 모르는 경우 for를 필요로하지 않지만 잠시 동안 또는 잠시 동안해야합니다.

기술적으로 모든 루프를 다른 루프로 변환 할 수 있지만 올바른 루프를 사용하면 코드를 쉽게 읽을 수 있으므로 다음과 같은 팁이 있습니다.

  1. for 내부에 if () {...; break;}를 작성하고 있다면 잠시 시간이 필요하며 이미 조건이 있습니다.

  2. "While"은 모든 언어에서 가장 많이 사용되는 루프 일 수 있지만 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는이 두 루프가 모두 정확 하고 똑같은 일을 한다는 것을 우리에게 증명할 것입니다.

그런 다음 내 경험을 바탕으로 올바른 역방향 루프를 작성하는 방법에 대해 일반적인 주장을 할 것입니다. 이는 미래에 이러한 상황에 직면했을 때 도움이 될 것입니다.

1 부-메소드의 스펙 작성

우리가 직면 한 첫 번째 과제는 방법이 실제로해야 할 일을 결정하는 것입니다. 이를 위해 방법의 동작을 지정하는 사전 및 사후 조건을 설계했습니다. 사양을보다 정확하게 만들기 위해 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이다. 그러나이 방법이 어디에서 호출되는지 알 수 없으므로 변경이 프로그램의 나머지 부분에 어떤 영향을 미치는지 알 수 없습니다.

2 부-루프 불변 값 결정

이제 우리는 메소드의 동작에 대한 사양을 가지고 있습니다. 루프를 실행하면 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에서 확인합니다. 이 링크따라 가면 직접 볼 수 있습니다 . 따라서 루프는 내가 1 부에서 작성한 메소드 사양을 올바르게 구현합니다. 이 방법 사양이 실제로 원하는 동작인지 결정해야합니다.

Dafny는 여기서 정확성을 증명하고 있습니다. 이것은 테스트를 통해 얻을 수있는 것보다 훨씬 정확한 정확성을 보장합니다.

3 부-더 간단한 루프

이제 루프의 동작을 캡처하는 메소드 스펙이 있습니다. 루프 동작을 변경하지 않았다는 확신을 유지하면서 루프 구현을 안전하게 수정할 수 있습니다.

루프 조건과의 최종 값에 대한 원래 직감과 일치하도록 루프를 수정했습니다 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;
}

4 부-역 루핑에 대한 조언

몇 년 동안 많은 루프를 작성하고 수정 한 후, 뒤로 루프하는 것에 대한 다음과 같은 일반적인 조언이 있습니다.

감소가 끝이 아닌 루프의 시작 부분에서 수행되면 거의 항상 생각하고 뒤로 (감소) 루프를 작성하는 것이 더 쉽습니다.

불행히도 for많은 언어로 된 루프 구조는 이것을 어렵게 만듭니다.

나는 이러한 복잡성이 루프가 무엇이고 실제로 필요한 것이 무엇인지에 대한 직관의 차이를 초래 한 것으로 의심합니다 (그러나 증명할 수는 없습니다). 순방향 (증가) 루프에 대해 생각하는 데 익숙합니다. 역방향 (감소) 루프를 작성하려면 순방향 (증가) 루프에서 발생하는 순서를 반대로하여 루프를 만듭니다. 그러나 for구조가 작동 하는 방식 때문에 할당 및 루프 변수 업데이트의 순서를 반대로하는 것을 무시했습니다. 이는 역방향 및 순방향 루프 사이의 작업 순서를 실제로 반전시키는 데 필요합니다.

5 부-보너스

완성도를 높이기 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
downvote
flamingpenguin

2

바로이 받기 쉽게 하고 유창하게하는 것은 경험의 문제이다. 언어를 통해 직접 표현할 수 없거나 간단한 내장 기능이 처리 할 수있는 것보다 더 복잡한 경우를 사용하더라도 "각 요소를 한 번 방문하여 순서대로 다시보기"와 같이 더 높은 수준 이라고 생각 하는 경우 경험이 풍부한 코더는 여러 번 해냈 기 때문에 즉시 올바른 세부 사항으로 변환합니다.

당신이 작성하는 것은 일반적이기 때문에 그렇다하더라도, 좀 더 복잡한 경우에 따라서는 오해 쉽게 하지 통조림 일반적인 것. 더 현대적인 언어와 라이브러리를 사용하면 통조림으로 된 구성이나 호출이 있기 때문에 쉬운 것을 쓰지 않습니다 . C ++에서 현대의 만트라는 "코드 작성보다는 알고리즘 사용"입니다.

따라서 이런 종류의 상황에서 올바른지 확인하는 방법은 경계 조건 을 보는 것 입니다. 상황이 바뀌는 가장자리의 몇 가지 경우에 대해 머리의 코드를 추적하십시오. 인 경우 index == array-max어떻게됩니까? 무엇에 대해 max-1? 코드가 잘못 회전하면 이러한 경계 중 하나에있게됩니다. 일부 루프는 첫 번째 또는 마지막 요소뿐만 아니라 루프 구조가 경계를 올바르게 얻는 것에 대해 걱정해야합니다. 예를 들어, 당신이 참조하는 경우 a[I]a[I-1]때 어떤 일이 발생 I최소 값은?

또한 (올바른) 반복 횟수가 극단적 인 경우를 살펴보십시오. 경계가 충족되고 반복 횟수가 0 인 경우 특별한 경우없이 작동합니까? 그리고 가장 낮은 경계가 동시에 가장 높은 경계인 1 회 반복은 어떻습니까?

루프 케이스를 작성할 때 수행해야 할 작업과 코드 검토에서 수행해야 할 작업은 에지 사례 (각 에지의 양쪽)를 검사하는 것입니다.


1

나는 이미 언급 한 많은 주제를 피하려고 노력할 것이다.

그러한 실수를 피하기위한 도구 / 정신 모델은 무엇입니까?

도구

나를 위해, 가장 큰 도구가 더 나은 쓰기 forwhile어떤을 작성하지 않는 루프 for또는 while전혀 반복합니다.

대부분의 현대 언어는이 문제를 어떤 방식 으로든 다른 방식으로 목표로 삼습니다. 예를 들어, Java Iterator는 처음부터 사용하기가 쉽지 않았지만, 더 짧은 릴리스에서 더 쉽게 사용할 수 있도록 바로 가기 구문을 도입했습니다. C #에도 마찬가지입니다.

나의 현재 선호하는 언어, 루비, 기능적 접근 방식 (촬영했다 .each, .map등) 전체 앞. 이것은 매우 강력합니다. 방금 작업중 인 일부 Ruby 코드베이스에서 빠른 계산을 수행했습니다. 약 10.000 줄의 코드에는 0 for과 5가 while있습니다.

새로운 언어를 선택해야한다면, 기능 / 데이터 기반 루프를 찾는 것이 우선 순위 목록에서 매우 높을 것입니다.

정신 모델

점을 명심 while당신이 얻을 수있는 추상화의 barest 최소, 위의 한 단계입니다 goto. 내 의견 for으로는 루프의 세 부분을 모두 단단히 묶기 때문에 더 나은 대신 더 나쁘게 만듭니다.

따라서 내가 for사용되는 환경에 있다면 3 부분이 모두 간단하고 항상 동일하다는 것을 망설입니다. 이것은 내가 쓸 것이다 의미

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

그러나 더 복잡한 것은 없습니다. 어쩌면 때로는 카운트 다운을 할 수도 있지만 피하기 위해 최선을 다할 것입니다.

을 사용 while하는 경우 루프 조건과 관련된 복잡한 내부 shenannigan을 피하십시오. 내부의 테스트 while(...)는 가능한 한 간단하며 가능한 break한 최선 을 피할 것 입니다. 또한 루프가 짧아지고 (코드 라인 수 계산) 더 많은 양의 코드가 제외됩니다.

특히 실제 조건이 복잡한 경우, "조건 변수"를 사용하여 쉽게 파악할 수 있으며 조건을 while명령문 자체에 배치하지 않습니다 .

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

(또는 물론 올바른 방법으로 그런 것입니다.)

이것은 "이 변수가 0에서 10까지 단조롭게 실행 중"이거나 "이 루프가 해당 변수가 거짓 / 참이 될 때까지 실행"인 매우 쉬운 정신 모델을 제공합니다. 대부분의 뇌는이 수준의 추상화를 잘 처리 할 수있는 것 같습니다.

희망이 도움이됩니다.


1

많은 루프 프로그래밍 언어가 일반적인 for-loop 구문과 0부터 시작하는 반 개방 간격을 사용하여 순방향 반복으로 편향되어 있기 때문에 특히 역 루프는 추론하기가 어려울 수 있습니다. 언어가 그러한 선택을하는 것이 잘못되었다는 말은 아닙니다. 나는 그 선택이 리버스 루프에 대한 생각을 복잡하게 만든다고 말하고 있습니다.

일반적으로 for-loop는 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;
}

단계 표현식이 헤더가 아닌 본문에 있기 때문에 이는 문제가 될 수 있습니다. 이는 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의 Programming Pearls 은 삽입 정렬 (및 기타 알고리즘)에 대한 매우 명확한 설명을 제공하여 이러한 종류의 문제에 대한 정신 모델을 구축하는 데 도움이됩니다.


0

for루프가 실제로 무엇을하고 어떻게 작동하는지 에 대해 혼란 스러우 십니까?

for(initialization; condition; increment*)
{
    body
}
  1. 먼저 초기화가 실행됩니다
  2. 그런 다음 상태가 점검됩니다.
  3. 조건이 true이면 본문이 한 번 실행됩니다. 6 번으로 가지 않으면
  4. 증분 코드가 실행됩니다
  5. 고토 # 2
  6. 루프 끝

다른 구문을 사용하여이 코드를 다시 작성하는 것이 유용 할 수 있습니다. while 루프를 사용하는 것도 마찬가지입니다.

initialization
while(condition)
{
    body
    increment
}

다른 제안 사항은 다음과 같습니다.

  • foreach 루프와 같은 다른 언어 구성을 사용할 수 있습니까? 이것은 당신을 위해 조건과 증분 단계를 처리합니다.
  • 맵 또는 필터 기능을 사용할 수 있습니까? 일부 언어에는 이러한 이름을 가진 함수가있어 내부적으로 컬렉션을 순환합니다. 당신은 단지 수집과 몸을 제공합니다.
  • for루프에 익숙해지는 데 더 많은 시간을 할애해야합니다 . 당신은 항상 그들을 사용할 것입니다. 디버거에서 for 루프를 단계별로 실행하여 실행 방법을 확인하는 것이 좋습니다.

* "증가"라는 용어를 사용하는 동안 본문 뒤와 상태 확인 전의 일부 코드 일뿐입니다. 그것은 감소하거나 전혀 될 수 없습니다.


1
왜 downvote?
user2023861

0

추가 통찰력 시도

들어 적지 않은 알고리즘 루프를 다음과 같은 방법을 시도해 볼 수도 있습니다 :

  1. 문제 를 시뮬레이트하기 위해 4 개의 위치 로 고정 배열작성하고 값을 입력 하십시오.
  2. 당신의 알고리즘을 작성 주어진 문제를 해결 , 어떤 루프없이하드 코딩 indexings과를 ;
  3. 그런 다음 코드에서 하드 코딩 된 인덱싱 을 일부 변수 i또는로 대체j 하고 필요에 따라 이러한 변수를 늘리 거나 줄입니다 (그러나 여전히 루프는 없음).
  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

추신 : 코드는 입력이 유효하고 해당 배열에 반복이 포함되지 않는다고 가정합니다.


-1

그렇기 때문에 가능하면 더 추상화 된 작업을 위해 원시 인덱스에서 작동하는 루프를 작성하지 않는 것이 좋습니다.

이 경우 다음과 같이 (의사 코드로) 다음과 같은 작업을 수행합니다.

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

-3

귀하의 예에서 루프 본문은 매우 분명합니다. 그리고 일부 요소는 마지막에 변경되어야한다는 것이 분명합니다. 따라서 루프의 시작, 종료 조건 및 최종 할당없이 코드를 작성합니다.

그런 다음 코드에서 벗어나 수행해야 할 첫 번째 이동을 찾으십시오. 첫 번째 이동이 정확하도록 루프 시작을 변경합니다. 코드에서 다시 벗어나서 마지막으로 수행해야 할 동작을 찾으십시오. 마지막 이동이 올바르도록 종료 조건을 변경합니다. 마지막으로 코드에서 벗어나 최종 과제가 무엇인지 파악하고 그에 따라 코드를 수정하십시오.


1
예를 들어 주시겠습니까?
CodeYogi

OP에는 예가있었습니다.
gnasher729

2
무슨 소리 야? 나는 OP입니다.
CodeYogi
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.