Làm thế nào để viết đúng vòng lặp?


65

Hầu hết thời gian trong khi viết các vòng lặp, tôi thường viết các điều kiện biên sai (ví dụ: kết quả sai) hoặc các giả định của tôi về việc chấm dứt vòng lặp là sai (ví dụ: vòng lặp chạy vô hạn). Mặc dù tôi đã nhận được các giả định của mình sau một số thử nghiệm và lỗi nhưng tôi đã quá nản lòng vì thiếu mô hình điện toán chính xác trong đầu.

/**
 * 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; 
};

Những sai lầm mà tôi đã làm ban đầu là:

  1. Thay vì j> = 0 tôi giữ nó j> 0.
  2. Đã nhầm lẫn liệu mảng [j + 1] = value hay mảng [j] = value.

Các công cụ / mô hình tinh thần để tránh những sai lầm như vậy là gì?


6
Trong hoàn cảnh nào bạn tin rằng đó j >= 0là một sai lầm? Tôi sẽ cảnh giác hơn với thực tế là bạn đang truy cập array[j]array[j + 1]không kiểm tra trước array.length > (j + 1).
Ben Cottrell

5
giống như những gì @LightnessRacesinOrbit đã nói, bạn có khả năng giải quyết các vấn đề đã được giải quyết. Nói chung, bất kỳ vòng lặp nào bạn cần thực hiện đối với cấu trúc dữ liệu đã tồn tại trong một số mô-đun hoặc lớp lõi (trên Array.prototypeví dụ về JS). Điều này ngăn bạn gặp các điều kiện cạnh vì một cái gì đó giống như maphoạt động trên tất cả các mảng. Bạn có thể giải quyết vấn đề trên bằng cách sử dụng lát và concat để tránh lặp tất cả cùng nhau: codepen.io/anon/pen/ZWovdg?editors=0012 Cách đúng nhất để viết một vòng lặp là không viết một vòng lặp nào cả.
Jed Schneider

13
Trên thực tế, đi trước và giải quyết vấn đề. Nó được gọi là thực hành. Đừng bận tâm xuất bản chúng. Đó là, trừ khi bạn tìm ra cách để cải thiện các giải pháp. Điều đó nói rằng, phát minh lại bánh xe liên quan nhiều hơn bánh xe. Nó bao gồm một hệ thống kiểm soát chất lượng toàn bộ bánh xe và hỗ trợ khách hàng. Tuy nhiên, vành tùy chỉnh là tốt đẹp.
candied_orange

53
Tôi sợ chúng ta đi sai hướng ở đây. Đưa ra CodeYogi tào lao vì ví dụ của anh ta là một phần của thuật toán nổi tiếng là khá vô căn cứ. Anh ấy không bao giờ tuyên bố rằng anh ấy đã phát minh ra một cái gì đó mới. Anh ấy hỏi làm thế nào để tránh một số lỗi ranh giới rất phổ biến khi viết một vòng lặp. Thư viện đã đi một chặng đường dài nhưng tôi vẫn thấy một tương lai cho những người biết cách viết các vòng lặp.
candied_orange

5
Nói chung, khi xử lý các vòng lặp và chỉ mục, bạn nên tìm hiểu các chỉ số giữa các phần tử và làm quen với các khoảng thời gian nửa mở (thực ra, chúng là hai mặt của cùng một khái niệm). Một khi bạn có được những sự thật này, phần lớn các vòng lặp / chỉ số gãi đầu hoàn toàn biến mất.
Matteo Italia

Câu trả lời:


208

Kiểm tra

Không, nghiêm túc, kiểm tra.

Tôi đã viết mã được hơn 20 năm và tôi vẫn không tin tưởng bản thân mình viết một vòng lặp chính xác ngay lần đầu tiên. Tôi viết và chạy thử nghiệm chứng minh rằng nó hoạt động trước khi tôi nghi ngờ nó hoạt động. Kiểm tra mỗi bên của mọi điều kiện biên. Ví dụ a rightIndexbằng 0 nên làm gì? Làm thế nào về -1?

Giữ cho nó đơn giản

Nếu người khác không thể nhìn thấy những gì nó làm trong nháy mắt, bạn đang làm cho nó quá khó. Xin vui lòng bỏ qua hiệu suất nếu nó có nghĩa là bạn có thể viết một cái gì đó dễ hiểu. Chỉ làm cho nó nhanh hơn trong trường hợp không thể mà bạn thực sự cần. Và thậm chí sau đó chỉ một lần bạn hoàn toàn chắc chắn rằng bạn biết chính xác điều gì đang làm bạn chậm lại. Nếu bạn có thể đạt được một cải tiến Big O thực tế thì hoạt động này có thể không phải là vô nghĩa, nhưng ngay cả khi đó, hãy làm cho mã của bạn dễ đọc nhất có thể.

Tắt bởi một

Biết sự khác biệt giữa đếm ngón tay của bạn và đếm khoảng cách giữa các ngón tay của bạn. Đôi khi các không gian là những gì thực sự quan trọng. Đừng để ngón tay làm bạn mất tập trung. Biết liệu ngón tay cái của bạn là một ngón tay. Biết liệu khoảng cách giữa ngón út và ngón tay cái của bạn có được tính là khoảng trắng không.

Bình luận

Trước khi bạn bị lạc trong mã, hãy cố gắng nói những gì bạn có nghĩa bằng tiếng Anh. Nêu rõ mong đợi của bạn. Đừng giải thích cách mã hoạt động. Giải thích tại sao bạn có nó làm những gì nó làm. Giữ chi tiết thực hiện ra khỏi nó. Có thể cấu trúc lại mã mà không cần thay đổi nhận xét.

Nhận xét tốt nhất là một cái tên tốt.

Nếu bạn có thể nói tất cả những gì bạn cần nói với một cái tên hay, ĐỪNG nói lại bằng một bình luận.

Trừu tượng

Các đối tượng, hàm, mảng và biến là tất cả các khái niệm trừu tượng chỉ tốt như tên mà chúng được đặt. Đặt cho họ những cái tên đảm bảo khi mọi người nhìn vào bên trong họ sẽ không ngạc nhiên với những gì họ tìm thấy.

Tên ngắn gọn

Sử dụng tên ngắn cho những thứ sống ngắn. ilà một tên hay cho một chỉ mục trong một vòng lặp chặt chẽ trong một phạm vi nhỏ làm cho nó có nghĩa rõ ràng. Nếu icuộc sống đủ dài để trải rộng ra sau hàng với những ý tưởng và tên khác có thể bị nhầm lẫn ithì đã đến lúc đưa ra imột cái tên giải thích dài đẹp.

Tên dài

Không bao giờ rút ngắn tên đơn giản do cân nhắc độ dài dòng. Tìm một cách khác để đặt mã của bạn.

Khoảng trắng

Khiếm khuyết tình yêu để ẩn trong mã không thể đọc được. Nếu ngôn ngữ của bạn cho phép bạn chọn phong cách thụt đầu dòng ít nhất là nhất quán. Đừng làm cho mã của bạn trông giống như một luồng nhiễu. Mã sẽ trông giống như nó đang diễu hành trong đội hình.

Cấu trúc vòng lặp

Tìm hiểu và xem xét các cấu trúc vòng lặp trong ngôn ngữ của bạn. Xem một trình sửa lỗi làm nổi bật một for(;;)vòng lặp có thể rất hướng dẫn. Tìm hiểu tất cả các hình thức. while, do while, while(true), for each. Sử dụng một trong những đơn giản nhất bạn có thể đi với. Tra cứu mồi bơm . Tìm hiểu những gì breakcontinuelàm nếu bạn có chúng. Biết sự khác biệt giữa c++++c. Đừng ngại trở về sớm miễn là bạn luôn đóng mọi thứ cần đóng. Cuối cùng chặn hoặc tốt nhất là một cái gì đó đánh dấu nó để tự động đóng khi bạn mở nó: Sử dụng câu lệnh / Thử với Tài nguyên .

Lựa chọn thay thế vòng

Hãy để một cái gì đó khác làm vòng lặp nếu bạn có thể. Nó dễ nhìn hơn và đã được gỡ lỗi. Những đến dưới nhiều hình thức: bộ sưu tập hoặc suối cho phép map(), reduce(), foreach(), và các phương pháp khác như áp dụng một lambda. Hãy tìm các chức năng đặc biệt như Arrays.fill(). Cũng có đệ quy nhưng chỉ mong rằng làm cho mọi thứ dễ dàng trong trường hợp đặc biệt. Nói chung không sử dụng đệ quy cho đến khi bạn thấy sự thay thế sẽ như thế nào.

Oh, và kiểm tra.

Kiểm tra, kiểm tra, kiểm tra.

Tôi đã đề cập đến thử nghiệm?

Còn một điều nữa. Không thể nhớ. Bắt đầu với một ...


36
Câu trả lời tốt, nhưng có lẽ bạn nên đề cập đến thử nghiệm. Làm thế nào để một người đối phó với vòng lặp vô hạn trong bài kiểm tra đơn vị? Không vòng lặp như vậy 'phá vỡ' các bài kiểm tra ???
GameAlchemist

139
@GameAlchemist Đó là bài kiểm tra pizza. Nếu mã của tôi không ngừng chạy trong thời gian tôi phải làm pizza, tôi bắt đầu nghi ngờ có gì đó không ổn. Chắc chắn không có cách chữa trị cho vấn đề tạm dừng của Alan Turing nhưng ít nhất tôi cũng nhận được một chiếc bánh pizza từ thỏa thuận này.
candied_orange

12
@CodeYogi - thực sự, nó có thể rất gần. Bắt đầu với một bài kiểm tra hoạt động trên một giá trị duy nhất. Thực hiện mã không có vòng lặp. Sau đó viết một bài kiểm tra hoạt động trên hai giá trị. Thực hiện các vòng lặp. Nó là rất khó xảy ra bạn sẽ nhận được một điều kiện biên sai về vòng lặp nếu bạn làm điều đó như thế này, bởi vì trong hầu hết mọi trường hợp một hoặc bên kia của hai bài kiểm tra này sẽ thất bại nếu bạn thực hiện một sai lầm như vậy.
Jules

15
@CodeYogi Dude tất cả tín dụng do TDD nhưng Kiểm tra >> TDD. Xuất ra một giá trị có thể đang kiểm tra, nhận được một cặp mắt thứ hai về mã của bạn đang kiểm tra (bạn có thể chính thức hóa việc này như một đánh giá mã nhưng tôi thường chỉ bắt ai đó cho một cuộc trò chuyện 5 phút). Một bài kiểm tra là bất kỳ cơ hội nào bạn đưa ra một biểu hiện về ý định thất bại của bạn. Địa ngục bạn có thể kiểm tra mã của bạn bằng cách nói chuyện thông qua một ý tưởng với mẹ của bạn. Tôi đã tìm thấy lỗi trong mã của mình khi nhìn chằm chằm vào gạch trong phòng tắm. TDD là một môn học chính thức hiệu quả mà bạn không tìm thấy ở mọi cửa hàng. Tôi chưa bao giờ mã hóa bất cứ nơi nào mà mọi người không kiểm tra.
candied_orange

12
Tôi đã viết mã và thử nghiệm nhiều năm và nhiều năm trước khi tôi nghe nói về TDD. Chỉ đến bây giờ tôi mới nhận ra mối tương quan của những năm đó với những năm đã dành cho tiền mã hóa trong khi không mặc quần.
candied_orange

72

Khi lập trình, thật hữu ích khi nghĩ về:

và khi khám phá lãnh thổ chưa được khám phá (chẳng hạn như tung hứng với các chỉ số), có thể rất, rất , rất hữu ích để không chỉ nghĩ về những điều đó mà thực sự làm cho chúng rõ ràng trong mã với các xác nhận .

Hãy lấy mã gốc của bạn:

/**
 * 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; 
};

Và kiểm tra những gì chúng ta có:

  • tiền điều kiện: array[0..rightIndex]được sắp xếp
  • hậu điều kiện: array[0..rightIndex+1]được sắp xếp
  • bất biến: 0 <= j <= rightIndexnhưng có vẻ dư thừa một chút; hoặc như @Jules ghi chú trong các bình luận, ở cuối "vòng" , for n in [j, rightIndex+1] => array[j] > value.
  • bất biến: ở cuối "vòng", array[0..rightIndex+1]được sắp xếp

Vì vậy, trước tiên bạn có thể viết một is_sortedhàm cũng như một minhàm làm việc trên một mảng mảng và sau đó xác nhận đi:

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]));
};

Cũng có một thực tế là điều kiện vòng lặp của bạn hơi phức tạp; bạn có thể muốn làm cho bản thân dễ dàng hơn bằng cách chia nhỏ mọi thứ:

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]));
};

Bây giờ, vòng lặp là đơn giản ( jđi từ rightIndexđến 0).

Cuối cùng, bây giờ điều này cần phải được kiểm tra:

  • nghĩ về điều kiện biên ( rightIndex == 0, rightIndex == array.size - 2)
  • nghĩ về valueviệc nhỏ hơn array[0]hoặc lớn hơnarray[rightIndex]
  • nghĩ về valueviệc bằng array[0], array[rightIndex]hoặc một số chỉ số trung bình

Ngoài ra, đừng đánh giá thấp sự mờ nhạt . Bạn có các xác nhận tại chỗ để bắt lỗi, vì vậy hãy tạo một mảng ngẫu nhiên và sắp xếp nó bằng phương thức của bạn. Nếu một xác nhận kích hoạt, bạn đã tìm thấy một lỗi và có thể mở rộng bộ thử nghiệm của bạn.


8
@CodeYogi: Với ... bài kiểm tra. Vấn đề là, có thể không thực tế để diễn đạt mọi thứ trong các xác nhận: nếu khẳng định đó chỉ lặp lại mã, thì nó không mang lại điều gì mới (sự lặp lại không giúp ích gì cho chất lượng). Đây là lý do tại sao ở đây tôi không khẳng định trong vòng lặp mà 0 <= j <= rightIndexhay array[j] <= value, nó sẽ chỉ lặp lại các mã. Mặt khác, is_sortedmang lại một sự đảm bảo mới, do đó, nó có giá trị. Sau đó, đó là những gì các bài kiểm tra dành cho. Nếu bạn gọi insert([0, 1, 2], 2, 3)đến chức năng của mình và đầu ra không [0, 1, 2, 3]thì bạn đã tìm thấy một lỗi.
Matthieu M.

3
@MatthieuM. không giảm giá trị của một xác nhận đơn giản vì nó sao chép mã. Nguyên vẹn, đó có thể là những xác nhận rất có giá trị nếu bạn quyết định viết lại mã. Kiểm tra có mọi quyền được nhân đôi. Thay vì xem xét nếu khẳng định nó được kết hợp với một triển khai mã đơn lẻ đến mức bất kỳ việc viết lại nào cũng sẽ làm mất hiệu lực xác nhận. Đó là khi bạn đang lãng phí thời gian của mình. Nhân tiện trả lời tốt.
candied_orange

1
@CandiedOrange: Bằng cách sao chép mã tôi có nghĩa đen array[j+1] = array[j]; assert(array[j+1] == array[j]);. Trong trường hợp này, giá trị có vẻ rất rất thấp (đó là bản sao / dán). Nếu bạn nhân đôi ý nghĩa nhưng được thể hiện theo cách khác, thì nó sẽ trở nên có giá trị hơn.
Matthieu M.

10
Quy tắc của Hoare: giúp viết các vòng lặp chính xác từ năm 1969. "Tuy nhiên, mặc dù các kỹ thuật này đã được biết đến trong hơn một thập kỷ, hầu hết các nhà lập trình chưa bao giờ nghe nói về chúng".
Joker_vD

1
@MatthieuM. Tôi đồng ý rằng nó có giá trị rất thấp. Nhưng tôi không nghĩ nó bị gây ra bởi nó là một bản sao / dán. Nói rằng tôi muốn cấu trúc lại insert()để nó hoạt động bằng cách sao chép từ một mảng cũ sang mảng mới. Điều đó có thể được thực hiện mà không phá vỡ của người khác assert. Nhưng không phải cái cuối cùng này. Chỉ cho thấy người khác của bạn assertđược thiết kế tốt như thế nào .
candied_orange

29

Sử dụng thử nghiệm đơn vị / TDD

Nếu bạn thực sự cần truy cập các chuỗi thông qua một forvòng lặp, bạn có thể tránh các lỗi thông qua kiểm tra đơn vị và đặc biệt là phát triển theo hướng kiểm tra .

Hãy tưởng tượng bạn cần thực hiện một phương thức lấy các giá trị cao hơn 0, theo thứ tự ngược lại. Những trường hợp thử nghiệm bạn có thể nghĩ đến?

  1. Một chuỗi chứa một giá trị cao hơn 0.

    Thực tế : [5]. Dự kiến ​​: [5].

    Việc thực hiện đơn giản nhất thỏa mãn các yêu cầu bao gồm đơn giản là trả về chuỗi nguồn cho người gọi.

  2. Một chuỗi chứa hai giá trị, cả hai đều vượt trội hơn không.

    Thực tế : [5, 7]. Dự kiến ​​: [7, 5].

    Bây giờ, bạn không thể trả lại chuỗi, nhưng bạn nên đảo ngược nó. Bạn sẽ sử dụng một for (;;)vòng lặp, cấu trúc ngôn ngữ khác hay phương thức thư viện không thành vấn đề.

  3. Một chuỗi chứa ba giá trị, một giá trị bằng không.

    Thực tế : [5, 0, 7]. Dự kiến ​​: [7, 5].

    Bây giờ bạn nên thay đổi mã để lọc các giá trị. Một lần nữa, điều này có thể được thể hiện thông qua một iftuyên bố hoặc một cuộc gọi đến phương thức khung yêu thích của bạn.

  4. Tùy thuộc vào thuật toán của bạn (vì đây là thử nghiệm hộp trắng, vấn đề triển khai), bạn có thể cần xử lý cụ thể [] → []trường hợp chuỗi trống hoặc có thể không. Hoặc bạn có thể đảm bảo rằng trường hợp cạnh trong đó tất cả các giá trị âm [-4, 0, -5, 0] → []được xử lý chính xác hoặc thậm chí các giá trị âm biên đó là : [6, 4, -1] → [4, 6]; [-1, 6, 4] → [4, 6]. Tuy nhiên, trong nhiều trường hợp, bạn sẽ chỉ có ba thử nghiệm được mô tả ở trên: mọi thử nghiệm bổ sung sẽ không khiến bạn thay đổi mã của mình và do đó sẽ không liên quan.

Làm việc ở mức độ trừu tượng cao hơn

Tuy nhiên, trong nhiều trường hợp, bạn có thể tránh được hầu hết các lỗi đó bằng cách làm việc ở mức độ trừu tượng cao hơn, sử dụng các thư viện / khung công tác hiện có. Những thư viện / khung công tác này có thể hoàn nguyên, sắp xếp, phân tách và nối các chuỗi, để chèn hoặc xóa các giá trị trong mảng hoặc danh sách liên kết đôi, v.v.

Thông thường, foreachcó thể được sử dụng thay vì for, làm cho các điều kiện biên kiểm tra không liên quan: ngôn ngữ làm điều đó cho bạn. Một số ngôn ngữ, chẳng hạn như Python, thậm chí không có for (;;)cấu trúc, nhưng chỉ for ... in ....

Trong C #, LINQ đặc biệt thuận tiện khi làm việc với các chuỗi.

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

dễ đọc hơn và ít bị lỗi hơn so với forbiến thể của nó :

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

    yield return value;
}

3
Chà, từ câu hỏi ban đầu của bạn, tôi có ấn tượng rằng sự lựa chọn là một mặt sử dụng TDD và nhận được giải pháp chính xác, mặt khác bỏ qua phần kiểm tra và nhận sai điều kiện biên.
Arseni Mourzenko

18
Cảm ơn bạn đã là người nhắc đến con voi trong phòng: không sử dụng vòng lặp nào cả . Tại sao mọi người vẫn viết mã như năm 1985 (và tôi hào phóng) nằm ngoài tôi. BOCTAOE.
Jared Smith

4
@JaredSmith Một khi máy tính thực sự thực thi mã đó, bạn muốn đặt cược bao nhiêu thì không có lệnh nhảy trong đó? Bằng cách sử dụng LINQ, bạn đang trừu tượng hóa vòng lặp, nhưng nó vẫn ở đó. Tôi đã giải thích điều này với đồng nghiệp, những người không tìm hiểu về công việc khó khăn của họa sĩ Shlemiel . Không hiểu được các vòng lặp xảy ra ở đâu, ngay cả khi chúng được trừu tượng hóa trong mã và kết quả là mã dễ đọc hơn, hầu như luôn luôn dẫn đến các vấn đề về hiệu năng mà người ta sẽ không thể giải thích được.
một CVn

6
@ MichaelKjorling: khi bạn sử dụng LINQ, vòng lặp ở đó, nhưng một for(;;)cấu trúc sẽ không được mô tả nhiều về vòng lặp này . Một khía cạnh quan trọng là LINQ (cũng như liệt kê sự hiểu biết trong Python và các yếu tố tương tự trong các ngôn ngữ khác) làm cho các điều kiện biên hầu như không liên quan, ít nhất là trong phạm vi của câu hỏi ban đầu. Tuy nhiên, tôi không thể đồng ý nhiều hơn về nhu cầu hiểu những gì xảy ra dưới mui xe khi sử dụng LINQ, đặc biệt là khi đánh giá lười biếng.
Arseni Mourzenko

4
@ MichaelKjorling Tôi không nhất thiết phải nói về LINQ và tôi không thấy được quan điểm của bạn. forEach, map, LazyIterator, Vv được cung cấp bởi các ngôn ngữ của trình biên dịch hoặc thời gian chạy môi trường và được cho là ít có khả năng là đi bộ trở lại thùng sơn trên mỗi lần lặp. Rằng, khả năng đọc và lỗi do lỗi là hai lý do khiến các tính năng này được thêm vào các ngôn ngữ hiện đại.
Jared Smith

15

Tôi đồng ý với những người khác nói kiểm tra mã của bạn. Tuy nhiên, thật tốt khi có được nó ngay từ đầu. Tôi có xu hướng nhận được các điều kiện biên sai trong nhiều trường hợp, vì vậy tôi đã phát triển các thủ thuật tinh thần để ngăn chặn các vấn đề như vậy.

Với mảng 0 chỉ mục, các điều kiện bình thường của bạn sẽ là:

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

hoặc là

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

Những mô hình đó sẽ trở thành bản chất thứ hai, bạn không cần phải suy nghĩ về những điều đó.

Nhưng không phải tất cả mọi thứ theo mô hình chính xác đó. Vì vậy, nếu bạn không chắc mình đã viết đúng hay không, đây là bước tiếp theo của bạn:

Cắm các giá trị và đánh giá mã trong não của bạn. Làm cho nó đơn giản để suy nghĩ về như bạn có thể. Điều gì xảy ra nếu các giá trị liên quan là 0? Điều gì xảy ra nếu họ là 1s?

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

Trong ví dụ của bạn, bạn không chắc chắn nên là [j] = value hay [j + 1] = value. Thời gian để bắt đầu đánh giá nó bằng tay:

Điều gì xảy ra nếu bạn có độ dài mảng 0? Câu trả lời trở nên rõ ràng: right Index phải là (length - 1) == -1, vì vậy j bắt đầu từ -1, vì vậy để chèn vào chỉ số 0, bạn cần thêm 1.

Vì vậy, chúng tôi đã chứng minh điều kiện cuối cùng là đúng, nhưng không phải bên trong vòng lặp.

Điều gì xảy ra nếu bạn có một mảng có 1 phần tử, 10 và chúng tôi cố gắng chèn 5? Với một phần tử duy nhất, right Index sẽ bắt đầu từ 0. Vì vậy, lần đầu tiên qua vòng lặp, j = 0, vì vậy "0> = 0 && 10> 5". Vì chúng tôi muốn chèn 5 ở chỉ số 0, nên 10 sẽ được chuyển sang chỉ mục 1, vì vậy mảng [1] = mảng [0]. Vì điều này xảy ra khi j bằng 0, mảng [j + 1] = mảng [j + 0].

Nếu bạn cố gắng tưởng tượng một mảng lớn và những gì xảy ra khi chèn vào một vị trí tùy ý, bộ não của bạn có thể sẽ bị choáng ngợp. Nhưng nếu bạn dính vào các ví dụ kích thước 0/1/2 đơn giản, bạn có thể dễ dàng thực hiện nhanh chóng tinh thần và xem nơi điều kiện ranh giới của bạn bị phá vỡ.

Hãy tưởng tượng bạn chưa bao giờ nghe về vấn đề bài hàng rào trước đây và tôi nói với bạn rằng tôi có 100 bài hàng rào theo một đường thẳng, có bao nhiêu phân đoạn giữa chúng. Nếu bạn cố gắng tưởng tượng 100 bài viết hàng rào trong đầu, bạn sẽ bị choáng ngợp. Vì vậy, những gì hàng rào ít nhất để làm một hàng rào hợp lệ? Bạn cần 2 để tạo một hàng rào, vì vậy hãy tưởng tượng 2 bài đăng và hình ảnh tinh thần của một phân đoạn duy nhất giữa các bài đăng làm cho nó rất rõ ràng. Bạn không cần phải ngồi đó đếm số bài đăng và phân đoạn vì vấn đề của bạn đã biến thành vấn đề trực giác rõ ràng đối với bộ não của bạn.

Một khi bạn nghĩ nó đúng, thì thật tốt khi chạy thử nghiệm và đảm bảo máy tính làm đúng những gì bạn nghĩ, nhưng tại thời điểm đó, nó chỉ là một hình thức.


4
Tôi thực sự thích for (int i = 0; i < length; i++). Khi tôi đã có thói quen đó, tôi đã ngừng sử dụng <=gần như thường xuyên và cảm thấy các vòng lặp trở nên đơn giản hơn. Nhưng for (int i = length - 1; i >= 0; i--)dường như quá phức tạp, so với: for (int i=length; i--; )(có lẽ sẽ hợp lý hơn khi viết dưới dạng một whilevòng lặp trừ khi tôi đang cố gắng để icó phạm vi / cuộc sống nhỏ hơn). Kết quả vẫn chạy qua vòng lặp với i == length-1 (ban đầu) đến i == 0, chỉ có sự khác biệt về chức năng là while()phiên bản kết thúc với i == -1 sau vòng lặp (nếu nó tồn tại), thay vì i = = 0.
TUYỆT VỜI

2
@TOOGAM (int i = length; i--;) hoạt động trong C / C ++ vì 0 đánh giá là sai, nhưng không phải tất cả các ngôn ngữ đều có sự tương đương đó. Tôi đoán bạn có thể nói tôi -> 0.
Bryce Wagner

Đương nhiên, nếu sử dụng ngôn ngữ yêu cầu " > 0" để có chức năng mong muốn, thì các ký tự đó sẽ được sử dụng vì ngôn ngữ đó được yêu cầu. Tuy nhiên, ngay cả trong những trường hợp đó, chỉ sử dụng " > 0" đơn giản hơn so với thực hiện quy trình hai phần đầu tiên trừ đi một, sau đó cũng sử dụng " >= 0". Khi tôi đã học được rằng thông qua một chút kinh nghiệm, tôi có thói quen cần sử dụng dấu bằng (ví dụ: " >= 0") trong các điều kiện kiểm tra vòng lặp của tôi ít thường xuyên hơn và kết quả là mã thường đơn giản hơn.
TẤT CẢ

1
@BryceWagner nếu bạn cần làm i-- > 0, tại sao không thử trò đùa kinh điển , i --> 0!
porglezomp

3
@porglezomp Ah, vâng, sự đi vào khai thác . Hầu hết các ngôn ngữ giống C, bao gồm C, C ++, Java và C #, đều có ngôn ngữ đó.
một CVn

11

Tôi đã quá nản lòng vì thiếu mô hình điện toán chính xác trong đầu.

Là một điểm rất thú vị cho câu hỏi này và nó đã tạo ra nhận xét này: -

Chỉ có một cách: hiểu vấn đề của bạn tốt hơn. Nhưng đó là chung chung như câu hỏi của bạn. - Thomas Junk

... Và Thomas đã đúng. Không có ý định rõ ràng cho một chức năng nên là cờ đỏ - một dấu hiệu rõ ràng rằng bạn nên DỪNG ngay lập tức, lấy bút chì và giấy, bước ra khỏi IDE và phá vỡ vấn đề đúng cách; hoặc ít nhất là sự tỉnh táo - kiểm tra những gì bạn đã làm.

Tôi đã thấy rất nhiều chức năng và các lớp đã trở thành một mớ hỗn độn vì các tác giả đã cố gắng xác định việc thực hiện trước khi họ đã xác định đầy đủ vấn đề. Và nó rất dễ đối phó.

Nếu bạn không hiểu đầy đủ về vấn đề thì bạn cũng không thể mã hóa giải pháp tối ưu (về hiệu quả hoặc sự rõ ràng), bạn cũng sẽ không thể tạo các bài kiểm tra đơn vị thực sự hữu ích trong phương pháp TDD.

Lấy mã của bạn ở đây làm ví dụ, nó chứa một số lỗ hổng tiềm năng mà bạn chưa xem xét ví dụ: -

  • Điều gì xảy ra nếu giá trị quá thấp? (đầu mối: nó sẽ liên quan đến mất dữ liệu)
  • Điều gì xảy ra nếu right Index nằm ngoài giới hạn mảng? (bạn sẽ nhận được một ngoại lệ, hoặc bạn vừa tạo cho mình một bộ đệm tràn?)

Có một vài vấn đề khác liên quan đến hiệu suất và thiết kế của mã ...

  • mã này sẽ cần phải mở rộng quy mô? Việc giữ mảng được sắp xếp là tùy chọn tốt nhất hay bạn nên xem các tùy chọn khác (như danh sách được liên kết?)
  • bạn có thể chắc chắn về các giả định của bạn? (bạn có thể đảm bảo mảng được sắp xếp không, và nếu không thì sao?)
  • bạn đang phát minh lại bánh xe? Mảng sắp xếp là một vấn đề nổi tiếng, bạn đã nghiên cứu các giải pháp hiện có chưa? Có một giải pháp đã có sẵn trong ngôn ngữ của bạn (chẳng hạn như SortedList<t>trong C #)?
  • Bạn có nên sao chép thủ công một mục nhập tại một thời điểm không? hoặc ngôn ngữ của bạn cung cấp các chức năng phổ biến như của JScript Array.Insert(...)? mã này sẽ rõ ràng hơn?

Có rất nhiều cách để mã này có thể được cải thiện nhưng cho đến khi bạn xác định chính xác những gì bạn cần mã này để làm, bạn không phát triển mã, bạn chỉ cần hack nó với nhau với hy vọng rằng nó sẽ hoạt động. Đầu tư thời gian vào đó và cuộc sống của bạn sẽ trở nên dễ dàng hơn.


2
Ngay cả khi bạn chuyển các chỉ mục của mình đến một hàm hiện có (chẳng hạn như Array.Copy), nó vẫn có thể yêu cầu suy nghĩ để có được các điều kiện ràng buộc chính xác. Tưởng tượng những gì xảy ra trong một tình huống 0 độ dài và 1 độ dài và 2 độ dài có thể là cách tốt nhất để đảm bảo bạn không sao chép quá ít hoặc quá nhiều.
Bryce Wagner

@BryceWagner - Hoàn toàn đúng, nhưng không có ý tưởng rõ ràng về vấn đề là bạn thực sự đang giải quyết vấn đề gì, bạn sẽ dành nhiều thời gian để chiến đấu trong bóng tối trong chiến lược 'hit and Hope' mà đến nay là OP vấn đề lớn nhất vào thời điểm này.
James Snell

2
@CodeYogi - bạn có và như đã chỉ ra bởi những người khác, bạn đã giải quyết vấn đề thành vấn đề phụ khá kém, đó là lý do tại sao một số câu trả lời đề cập đến cách tiếp cận của bạn để giải quyết vấn đề là cách để tránh vấn đề. Đó không phải là thứ bạn nên tự nhận, chỉ cần trải nghiệm từ những người trong chúng tôi đã từng ở đó.
James Snell

2
@CodeYogi, tôi nghĩ rằng bạn có thể đã nhầm lẫn trang web này với Stack Overflow. Trang web này tương đương với phiên hỏi đáp tại bảng trắng , không phải tại thiết bị đầu cuối máy tính. "Cho tôi xem mã" là một dấu hiệu khá rõ ràng rằng bạn đang truy cập sai trang web.
tự đại diện

2
@Wildcard +1: "Cho tôi xem mã" là, với tôi, một chỉ số tuyệt vời về lý do tại sao câu trả lời này là đúng và có lẽ tôi cần phải làm việc để chứng minh rõ hơn rằng đó là vấn đề về yếu tố con người / thiết kế chỉ có thể được giải quyết bằng những thay đổi trong quy trình của con người - không có số lượng mã nào có thể dạy nó.
James Snell

10

Lỗi do lỗi là một trong những lỗi lập trình phổ biến nhất. Ngay cả các nhà phát triển có kinh nghiệm đôi khi nhận được điều này sai. Các ngôn ngữ cấp cao hơn thường có các cấu trúc lặp như thế foreachhoặc maptránh hoàn toàn việc lập chỉ mục rõ ràng. Nhưng đôi khi bạn cần lập chỉ mục rõ ràng, như trong ví dụ của bạn.

Thách thức là làm thế nào để nghĩ về phạm vi của các ô mảng. Không có một mô hình tinh thần rõ ràng, nó trở nên khó hiểu khi bao gồm hoặc loại trừ các điểm kết thúc.

Khi mô tả phạm vi mảng, quy ước là bao gồm giới hạn dưới, loại trừ giới hạn trên . Ví dụ, phạm vi 0..3 là các ô 0,1,2. Các quy ước này được sử dụng trên khắp các ngôn ngữ được lập chỉ mục 0, ví dụ: slice(start, end)phương thức trong JavaScript trả về phân đoạn bắt đầu với chỉ mục startlên đến nhưng không bao gồm chỉ mục end.

Rõ ràng hơn khi bạn nghĩ về các chỉ mục phạm vi như mô tả các cạnh giữa các ô mảng. Hình minh họa dưới đây là một mảng có độ dài 9 và các số bên dưới các ô được căn chỉnh theo các cạnh và là những gì được sử dụng để mô tả các phân đoạn mảng. Ví dụ, rõ ràng từ hình minh họa hơn phạm vi 2..5 là các ô 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

Mô hình này phù hợp với việc có chiều dài mảng là giới hạn trên của một mảng. Một mảng có chiều dài 5 có các ô 0..5, có nghĩa là có năm ô 0,1,2,3,4. Điều này cũng có nghĩa là độ dài của một đoạn là giới hạn cao hơn trừ đi giới hạn dưới, tức là đoạn 2..5 có 5-2 = 3 ô.

Có mô hình này trong tâm trí khi lặp đi lặp lại lên hoặc xuống làm cho nó rõ ràng hơn nhiều khi bao gồm hoặc loại trừ các điểm kết thúc. Khi lặp lên trên, bạn cần bao gồm điểm bắt đầu nhưng loại trừ điểm kết thúc. Khi lặp xuống dưới, bạn cần loại trừ điểm bắt đầu (giới hạn cao hơn) nhưng bao gồm điểm kết thúc (giới hạn dưới).

Vì bạn đang lặp đi lặp lại trong mã của mình, bạn cần bao gồm giới hạn thấp, 0, để bạn lặp lại trong khi j >= 0.

Vì điều này, sự lựa chọn của bạn để có rightIndexđối số đại diện cho chỉ mục cuối cùng trong phân đoạn phá vỡ quy ước. Điều đó có nghĩa là bạn phải bao gồm cả hai điểm cuối (0 và right Index) trong lần lặp. Nó cũng gây khó khăn cho việc đại diện cho phân khúc trống (mà bạn cần khi bạn bắt đầu sắp xếp). Bạn thực sự phải sử dụng -1 làm đúng Index khi chèn giá trị đầu tiên. Điều này có vẻ khá không tự nhiên. Có vẻ tự nhiên hơn khi rightIndexchỉ ra chỉ số sau phân khúc, vì vậy 0 đại diện cho phân khúc trống.

Tất nhiên mã của bạn rất khó hiểu vì nó mở rộng phân đoạn được sắp xếp bằng một, ghi đè lên mục ngay sau phân đoạn được sắp xếp ban đầu. Vì vậy, bạn đọc từ chỉ số j nhưng ghi giá trị vào j + 1. Ở đây bạn chỉ cần rõ ràng rằng j là vị trí trong phân đoạn ban đầu, trước khi chèn. Khi các thao tác chỉ mục trở nên quá phức tạp, nó giúp tôi vẽ sơ đồ trên một tờ giấy lưới.


4
@CodeYogi: Tôi sẽ vẽ một mảng nhỏ dưới dạng lưới trên một tờ giấy, và sau đó bước qua một vòng lặp của vòng lặp bằng tay bằng bút chì. Điều này giúp tôi làm rõ những gì thực sự xảy ra, ví dụ: một phạm vi ô được dịch chuyển sang phải và nơi giá trị mới được chèn vào.
JacquesB

3
"có hai điều khó khăn trong khoa học máy tính: vô hiệu hóa bộ đệm, đặt tên và lỗi do lỗi một."
Chấn thương kỹ thuật số

1
@CodeYogi: Đã thêm một sơ đồ nhỏ để hiển thị những gì tôi đang nói.
JacquesB

1
Cái nhìn sâu sắc đặc biệt là hai lần phân tích cuối cùng của bạn rất đáng đọc, sự nhầm lẫn cũng là do bản chất của vòng lặp for, thậm chí tôi tìm thấy chỉ số đúng mà vòng lặp giảm xuống một lần trước khi chấm dứt và do đó khiến tôi lùi lại một bước.
CodeYogi

1
Rất. Xuất sắc. Câu trả lời. Và tôi muốn nói thêm rằng quy ước chỉ số bao gồm / độc quyền này cũng được thúc đẩy bởi giá trị của myArray.Lengthhoặc myList.Count- luôn luôn nhiều hơn chỉ số "ngoài cùng" dựa trên số không. ... Độ dài của lời giải thích cho thấy ứng dụng thực tế và đơn giản của các heuristic mã hóa rõ ràng này. Đám đông TL; DR đang bỏ lỡ.
radarbob

5

Phần giới thiệu cho câu hỏi của bạn khiến tôi nghĩ rằng bạn chưa học cách viết mã đúng. Bất cứ ai đang lập trình bằng một ngôn ngữ bắt buộc trong hơn một vài tuần nên thực sự có được giới hạn vòng lặp của họ ngay lần đầu tiên trong hơn 90% trường hợp. Có lẽ bạn đang vội vã bắt đầu viết mã trước khi bạn nghĩ đủ vấn đề.

Tôi khuyên bạn nên khắc phục sự thiếu hụt này bằng cách (học lại) cách viết các vòng lặp - và tôi khuyên bạn nên làm việc vài giờ với một loạt các vòng lặp bằng giấy và bút chì. Hãy nghỉ một buổi chiều để làm điều này. Sau đó dành 45 phút hoặc lâu hơn một ngày để làm việc về chủ đề này cho đến khi bạn thực sự có được nó.

Tất cả đều là thử nghiệm rất tốt, nhưng bạn nên thử nghiệm với hy vọng rằng bạn thường nhận được giới hạn vòng lặp của mình (và phần còn lại của mã).


4
Tôi sẽ không quyết đoán về các kỹ năng của OP. Làm cho sai lầm ranh giới là dễ dàng, đặc biệt là trong một bối cảnh căng thẳng như một cuộc phỏng vấn tuyển dụng. Một nhà phát triển có kinh nghiệm cũng có thể làm những sai lầm đó, nhưng, rõ ràng, một nhà phát triển có kinh nghiệm sẽ tránh những sai lầm như vậy ngay từ đầu thông qua thử nghiệm.
Arseni Mourzenko

3
@MainMa - Tôi nghĩ rằng trong khi Mark có thể nhạy cảm hơn, tôi nghĩ anh ấy đúng - Có căng thẳng khi phỏng vấn và chỉ hack mã mà không xem xét kỹ để xác định vấn đề. Cách câu hỏi được đặt ra rất mạnh mẽ cho câu hỏi sau và đó là điều có thể giải quyết tốt nhất trong dài hạn bằng cách đảm bảo bạn có một nền tảng vững chắc, không phải bằng cách hack tại IDE
James Snell

@JamesSnell Tôi nghĩ bạn đang quá tự tin về bản thân. Nhìn vào mã và cho tôi biết những gì làm cho bạn nghĩ rằng nó theo tài liệu? Nếu bạn thấy rõ có nơi nào đề cập rằng tôi không thể giải quyết vấn đề? Tôi chỉ muốn biết làm thế nào để tránh lặp lại sai lầm tương tự. Tôi nghĩ rằng bạn nhận được tất cả các chương trình của bạn chính xác trong một lần.
CodeYogi

4
@CodeYogi Nếu bạn phải thực hiện 'thử và sai' và bạn 'đang nản lòng' và 'mắc lỗi tương tự' với mã hóa của mình thì đó là những dấu hiệu cho thấy bạn không hiểu rõ vấn đề của mình trước khi bắt đầu viết . Không ai nói rằng bạn không hiểu nó, nhưng mã của bạn có thể được suy nghĩ tốt hơn và chúng là dấu hiệu cho thấy bạn đang vật lộn mà bạn có quyền lựa chọn và học hỏi, hoặc không.
James Snell

2
@CodeYogi ... và vì bạn hỏi, tôi hiếm khi hiểu sai và phân nhánh vì tôi hiểu rõ về những gì tôi cần đạt được trước khi viết mã, không khó để làm một việc đơn giản như sắp xếp lớp mảng. Là một lập trình viên, một trong những điều khó nhất phải làm là thừa nhận rằng bạn là vấn đề, nhưng cho đến khi bạn làm điều đó, bạn sẽ không bắt đầu viết mã thực sự tốt.
James Snell

3

Có lẽ tôi nên đưa một số xác thịt vào nhận xét của mình:

Chỉ có một cách: hiểu vấn đề của bạn tốt hơn. Nhưng đó là chung chung như câu hỏi của bạn là

Quan điểm của bạn là

Mặc dù tôi đã nhận được các giả định của mình sau một số thử nghiệm và lỗi nhưng tôi đã quá nản lòng vì thiếu mô hình điện toán chính xác trong đầu.

Khi tôi đọc trial and error, tiếng chuông báo thức của tôi bắt đầu vang lên. Tất nhiên nhiều người trong chúng ta biết trạng thái của tâm trí, khi một người muốn khắc phục một vấn đề nhỏ và quấn đầu xung quanh những thứ khác và bắt đầu đoán theo cách này hay cách khác, để tạo ra mã seemphải làm, phải làm gì. Một số giải pháp hackish ra khỏi điều này - và một số trong số họ là thiên tài thuần túy ; nhưng thành thật mà nói: hầu hết trong số họ là không . Tôi bao gồm, biết trạng thái này.

Độc lập với vấn đề cụ thể của bạn, bạn đã đặt câu hỏi, về cách cải thiện:

1) Kiểm tra

Điều đó đã được nói bởi những người khác và tôi sẽ không có gì có giá trị để thêm

2) Phân tích vấn đề

Thật khó để đưa ra một số lời khuyên cho điều đó. Chỉ có hai gợi ý tôi có thể cung cấp cho bạn, có thể giúp bạn cải thiện kỹ năng của mình về chủ đề đó:

  • một điều hiển nhiên và tầm thường nhất là về lâu dài hiệu quả nhất: giải quyết nhiều vấn đề. Trong khi thực hành và lặp lại, bạn phát triển tư duy giúp bạn cho các nhiệm vụ trong tương lai. Lập trình cũng giống như bất kỳ hoạt động nào khác được cải thiện bằng cách luyện tập chăm chỉ

Code Katas là một cách, có thể giúp một chút.

Làm thế nào để bạn trở thành một nhạc sĩ tuyệt vời? Nó giúp để biết lý thuyết, và để hiểu các cơ chế của công cụ của bạn. Nó giúp có tài năng. Nhưng cuối cùng, sự vĩ đại đến từ thực tiễn; áp dụng lý thuyết nhiều lần, sử dụng thông tin phản hồi để cải thiện mọi lúc.

Mã Kata

Một trang web mà tôi rất thích: Code Wars

Đạt được thành thạo thông qua thử thách Cải thiện kỹ năng của bạn bằng cách đào tạo với những người khác về các thử thách mã thực

Chúng là những vấn đề tương đối nhỏ, giúp bạn mài giũa kỹ năng lập trình. Và điều tôi thích nhất trên Code Wars là, bạn có thể so sánh giải pháp của mình với một trong những giải pháp khác .

Hoặc có lẽ, bạn nên có một cái nhìn về Tập thể dục.io nơi bạn nhận được phản hồi từ cộng đồng.

  • Một lời khuyên khác gần như tầm thường: Học cách phá vỡ các vấn đề Bạn phải tự rèn luyện bản thân, chia nhỏ các vấn đề thành những vấn đề thực sự nhỏ. Nếu bạn nói, bạn có vấn đề trong việc viết các vòng lặp , bạn đã phạm sai lầm, rằng bạn thấy vòng lặp là toàn bộ cấu trúc và không giải mã nó thành từng mảnh. Nếu bạn học cách tách rời từng bước một , bạn sẽ học cách tránh những sai lầm như vậy.

Tôi biết - như tôi đã nói ở trên đôi khi bạn ở trong tình trạng như vậy - rằng thật khó để phá vỡ những điều "đơn giản" thành những nhiệm vụ "đơn giản chết người" hơn; nhưng nó giúp rất nhiều

Tôi nhớ, khi tôi mới học lập trình chuyên nghiệp , tôi đã gặp vấn đề lớn với việc gỡ lỗi mã của mình. Có vấn đề gì thế? Hybris - Lỗi không thể nằm trong vùng mã và vùng như vậy, bởi vì tôi biết rằng nó không thể xảy ra. Và trong hậu quả? Tôi đọc lướt qua mã thay vì phân tích nó tôi phải học - ngay cả khi việc tẻ nhạt mã của tôi để hướng dẫn .

3) Phát triển một Toolbelt

Bên cạnh việc biết ngôn ngữ và các công cụ của bạn - tôi biết đây là những điều sáng chói mà các nhà phát triển nghĩ đầu tiên - tìm hiểu Thuật toán (còn gọi là đọc).

Đây là hai cuốn sách để bắt đầu:

Điều này giống như học một số công thức để bắt đầu nấu ăn. Lúc đầu, bạn không biết phải làm gì, vì vậy bạn phải xem, những gì các đầu bếp trước đã nấu cho bạn. Điều tương tự cũng xảy ra với đại số. Các thuật toán giống như người nhận nấu ăn cho các bữa ăn thông thường (cấu trúc dữ liệu, sắp xếp, băm, v.v.) Nếu bạn biết chúng (ít nhất là cố gắng) bằng trái tim, bạn có một điểm khởi đầu tốt.

3a) Biết các cấu trúc lập trình

Điểm này là một dẫn xuất - có thể nói như vậy. Biết ngôn ngữ của bạn - và tốt hơn: biết, những cấu trúc nào có thể có trong ngôn ngữ của bạn.

Một điểm chung cho mã xấu hoặc inefficent là đôi khi, mà các lập trình viên không biết sự khác biệt giữa các loại khác nhau của các vòng ( for-, while-do-loops). Chúng bằng cách nào đó tất cả đều có thể thay thế cho nhau; nhưng trong một số trường hợp, việc chọn một cấu trúc lặp khác dẫn đến mã thanh lịch hơn.

Và có thiết bị của Duff ...

Tái bút

nếu không bình luận của bạn không tốt hơn Donal Trump.

Vâng, chúng ta nên làm cho mã hóa trở lại tuyệt vời!

Một phương châm mới cho Stackoverflow.


Ah, để tôi nói với bạn một điều rất nghiêm túc. Tôi đã làm mọi thứ bạn đề cập và tôi thậm chí có thể cung cấp cho bạn liên kết của tôi trên các trang web đó. Nhưng điều khiến tôi bực bội là thay vì nhận được câu trả lời cho câu hỏi của mình, tôi nhận được mọi lời khuyên có thể về lập trình. Chỉ cần một anh chàng đã đề cập đến các pre-postđiều kiện cho đến bây giờ và tôi đánh giá cao nó.
CodeYogi

Từ những gì bạn nói, thật khó để tưởng tượng vấn đề của bạn ở đâu. Có lẽ một phép ẩn dụ giúp: đối với tôi nó giống như nói "làm thế nào tôi có thể nhìn thấy" - câu trả lời rõ ràng cho tôi là "sử dụng đôi mắt của bạn" bởi vì nhìn thấy rất tự nhiên đối với tôi, tôi không thể tưởng tượng, làm thế nào người ta không thể nhìn thấy. Câu hỏi của bạn cũng vậy.
Thomas Junk

Đồng ý hoàn toàn với tiếng chuông báo động về "bản dùng thử và lỗi." Tôi nghĩ cách tốt nhất để học đầy đủ tư duy giải quyết vấn đề là chạy các thuật toán và mã bằng giấy và bút chì.
tự đại diện

Ừm ... tại sao bạn lại có một câu nói không tốt về mặt ngữ pháp tại một ứng cử viên chính trị được trích dẫn mà không có ngữ cảnh ở giữa câu trả lời của bạn về lập trình?
tự đại diện

2

Nếu tôi hiểu vấn đề một cách chính xác, câu hỏi của bạn là làm thế nào để suy nghĩ để có được các vòng lặp ngay từ lần thử đầu tiên, chứ không phải làm thế nào để đảm bảo vòng lặp của bạn đúng (câu trả lời sẽ được kiểm tra như được giải thích trong các câu trả lời khác).

Những gì tôi coi là một cách tiếp cận tốt là viết lần lặp đầu tiên mà không có vòng lặp. Sau khi bạn thực hiện điều này, bạn sẽ nhận thấy những gì cần thay đổi giữa các lần lặp.

Nó là một số, như số 0 hay số 1? Sau đó, bạn rất có thể cần một cho, và bingo, bạn cũng có i bắt đầu của bạn. Sau đó suy nghĩ bao nhiêu lần bạn muốn chạy cùng một thứ, và bạn cũng sẽ có điều kiện cuối cùng của bạn.

Nếu bạn không biết CHÍNH XÁC bao nhiêu lần nó sẽ chạy, thì bạn không cần một lần, nhưng một lúc hoặc làm một lúc.

Về mặt kỹ thuật, bất kỳ vòng lặp nào cũng có thể được dịch sang bất kỳ vòng lặp nào khác, nhưng mã dễ đọc hơn nếu bạn sử dụng đúng vòng lặp, vì vậy đây là một số mẹo:

  1. Nếu bạn thấy mình viết if () {...; break;} bên trong một for, bạn cần một lúc và bạn đã có điều kiện

  2. "While" có thể là vòng lặp được sử dụng nhiều nhất trong bất kỳ ngôn ngữ nào, nhưng nó không nên imo. Nếu bạn thấy mình viết bool ok = True; while (check) {làm điều gì đó và hy vọng thay đổi ok vào một lúc nào đó}; sau đó bạn không cần một lúc, nhưng làm một lúc, bởi vì điều đó có nghĩa là bạn có mọi thứ bạn cần để chạy lần lặp đầu tiên.

Bây giờ một chút bối cảnh ... Khi tôi mới học lập trình (Pascal), tôi đã không nói tiếng Anh. Đối với tôi, "for" và "while", không có ý nghĩa gì nhiều, nhưng từ khóa "repeat" (do while in C) gần như giống nhau trong tiếng mẹ đẻ của tôi, vì vậy tôi sẽ sử dụng nó cho mọi thứ. Theo tôi, lặp lại (do while) là vòng lặp tự nhiên nhất, bởi vì hầu như bạn luôn muốn một cái gì đó được thực hiện và sau đó bạn muốn nó được thực hiện lại, và một lần nữa, cho đến khi đạt được mục tiêu. "For" chỉ là một phím tắt cung cấp cho bạn một trình vòng lặp và đặt điều kiện kỳ ​​lạ vào đầu mã, mặc dù, hầu như luôn luôn, bạn muốn một cái gì đó được thực hiện cho đến khi có điều gì đó xảy ra. Ngoài ra, while chỉ là một phím tắt cho if () {do while ()}. Các phím tắt là tốt cho sau này,


2

Tôi sẽ đưa ra một ví dụ chi tiết hơn về cách sử dụng các điều kiện trước / sau và bất biến để phát triển một vòng lặp chính xác. Cùng với các xác nhận như vậy được gọi là một đặc điểm kỹ thuật hoặc hợp đồng.

Tôi không gợi ý rằng bạn cố gắng làm điều này cho mỗi vòng lặp. Nhưng tôi hy vọng rằng bạn sẽ thấy hữu ích khi thấy quá trình suy nghĩ liên quan.

Để làm như vậy, tôi sẽ dịch phương thức của bạn thành một công cụ có tên Microsoft Dafny , được thiết kế để chứng minh tính chính xác của các thông số kỹ thuật đó. Nó cũng kiểm tra chấm dứt của mỗi vòng lặp. Xin lưu ý rằng Dafny không có forvòng lặp nên tôi phải sử dụng whilevòng lặp thay thế.

Cuối cùng tôi sẽ chỉ ra làm thế nào bạn có thể sử dụng các thông số kỹ thuật như vậy để thiết kế một phiên bản vòng lặp đơn giản hơn một chút. Phiên bản vòng lặp đơn giản hơn này không có điều kiện vòng lặp j > 0và bài tập array[j] = value- như trực giác ban đầu của bạn.

Dafny sẽ chứng minh cho chúng tôi rằng cả hai vòng lặp này đều đúng và làm cùng một việc.

Sau đó tôi sẽ đưa ra một tuyên bố chung, dựa trên kinh nghiệm của tôi, về cách viết vòng lặp ngược chính xác, điều đó có thể sẽ giúp bạn nếu gặp phải tình huống này trong tương lai.

Phần một - Viết một đặc tả cho phương thức

Thách thức đầu tiên chúng ta phải đối mặt là xác định phương pháp thực sự phải làm là gì. Cuối cùng, tôi thiết kế các điều kiện trước và sau trong đó xác định hành vi của phương thức. Để làm cho đặc tả chính xác hơn, tôi đã tăng cường phương thức để làm cho nó trả về chỉ mục nơi valueđược chèn.

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..])

Đặc điểm kỹ thuật này nắm bắt đầy đủ hành vi của phương pháp. Quan sát chính của tôi về đặc tả này là nó sẽ được đơn giản hóa nếu thủ tục được thông qua giá trị rightIndex+1chứ không phải rightIndex. Nhưng vì tôi không thể thấy phương thức này được gọi từ đâu nên tôi không biết sự thay đổi đó sẽ có ảnh hưởng gì đến phần còn lại của chương trình.

Phần thứ hai - xác định một bất biến vòng lặp

Bây giờ chúng ta có một đặc tả cho hành vi của phương thức, chúng ta phải thêm một đặc tả về hành vi vòng lặp sẽ thuyết phục Dafny rằng việc thực thi vòng lặp sẽ chấm dứt và sẽ dẫn đến trạng thái cuối cùng mong muốn array .

Sau đây là vòng lặp ban đầu của bạn, được dịch sang cú pháp Dafny với các bất biến vòng lặp được thêm vào. Tôi cũng đã thay đổi nó để trả về chỉ mục nơi giá trị được chèn vào.

{
    // 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
}

Điều này xác minh trong Dafny. Bạn có thể nhìn thấy nó bằng cách theo liên kết này . Vì vậy, vòng lặp của bạn thực hiện chính xác đặc tả phương thức mà tôi đã viết trong phần một. Bạn sẽ cần phải quyết định xem đặc tả phương thức này có thực sự là hành vi mà bạn muốn hay không.

Lưu ý rằng Dafny đang tạo ra một bằng chứng chính xác ở đây. Đây là một đảm bảo chính xác mạnh mẽ hơn nhiều so với có thể có được bằng cách thử nghiệm.

Phần thứ ba - một vòng lặp đơn giản hơn

Bây giờ chúng ta có một đặc tả phương thức nắm bắt hành vi của vòng lặp. Chúng ta có thể sửa đổi một cách an toàn việc thực hiện vòng lặp trong khi vẫn giữ niềm tin rằng chúng ta không thay đổi hành vi của vòng lặp.

Tôi đã sửa đổi vòng lặp để nó phù hợp với trực giác ban đầu của bạn về điều kiện vòng lặp và giá trị cuối cùng của j. Tôi sẽ lập luận rằng vòng lặp này đơn giản hơn vòng lặp mà bạn mô tả trong câu hỏi của bạn. Nó thường có thể sử dụng jhơn là j+1.

  1. Bắt đầu j tại rightIndex+1

  2. Thay đổi điều kiện vòng lặp thành j > 0 && arr[j-1] > value

  3. Thay đổi nhiệm vụ thành arr[j] := value

  4. Giảm bộ đếm vòng lặp ở cuối vòng lặp thay vì bắt đầu

Đây là mã. Lưu ý rằng các bất biến vòng lặp cũng có phần dễ viết hơn bây giờ:

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;
}

Phần thứ tư - lời khuyên về vòng lặp ngược

Sau khi đã viết và chứng minh đúng nhiều vòng lặp trong một vài năm, tôi có lời khuyên chung sau đây về việc lặp lại.

Hầu như luôn luôn dễ dàng hơn để suy nghĩ và viết một vòng lặp (giảm dần) nếu việc giảm được thực hiện ở đầu vòng lặp hơn là kết thúc.

Thật không may for cấu trúc vòng lặp trong nhiều ngôn ngữ làm cho điều này trở nên khó khăn.

Tôi nghi ngờ (nhưng không thể chứng minh) rằng sự phức tạp này là nguyên nhân gây ra sự khác biệt trong trực giác của bạn về vòng lặp nên là gì và nó thực sự cần phải là gì. Bạn đã quen nghĩ về các vòng lặp (tăng dần). Khi bạn muốn viết một vòng lặp (giảm dần), bạn cố gắng tạo vòng lặp bằng cách cố gắng đảo ngược thứ tự mà mọi thứ xảy ra trong một vòng lặp (tăng dần). Nhưng do cách thức thi forcông mà bạn đã bỏ qua để đảo ngược thứ tự cập nhật biến số gán và vòng lặp - điều cần thiết cho một sự đảo ngược thực sự của thứ tự các hoạt động giữa một vòng lặp ngược và tiến.

Phần thứ năm - tiền thưởng

Để cho đầy đủ, đây là mã bạn nhận được nếu bạn chuyển rightIndex+1sang phương thức chứ không phải rightIndex. Những thay đổi này giúp loại bỏ tất cả các +2độ lệch được yêu cầu để suy nghĩ về tính đúng đắn của vòng lặp.

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
Sẽ thực sự đánh giá cao một bình luận nếu bạn downvote
flamingpenguin

2

Có được quyền này một cách dễ dàngtrôi chảy là một vấn đề kinh nghiệm. Mặc dù ngôn ngữ không cho phép bạn diễn đạt trực tiếp hoặc bạn đang sử dụng một trường hợp phức tạp hơn những thứ tích hợp đơn giản có thể xử lý, bạn nghĩ gì là mức độ cao hơn như "truy cập từng yếu tố một lần theo thứ tự hoàn nguyên" và lập trình viên giàu kinh nghiệm hơn sẽ chuyển nó thành chi tiết phù hợp ngay lập tức bởi vì anh ta đã thực hiện nó rất nhiều lần.

Ngay cả sau đó, trong những trường hợp phức tạp hơn, nó rất dễ bị sai, bởi vì thứ bạn đang viết thường không phải là thứ phổ biến đóng hộp. Với các ngôn ngữ và thư viện hiện đại hơn, bạn không viết điều dễ dàng bởi vì có một cấu trúc đóng hộp hoặc gọi cho điều đó. Trong C ++, câu thần chú hiện đại là "sử dụng thuật toán thay vì viết mã".

Vì vậy, cách để đảm bảo nó đúng, đối với loại điều này nói riêng, là xem xét các điều kiện biên . Theo dõi mã trong đầu của bạn cho một vài trường hợp trên các cạnh của nơi mọi thứ thay đổi. Nếu index == array-max, những gì xảy ra? Thế còn max-1? Nếu mã rẽ sai, nó sẽ ở một trong những ranh giới này. Một số vòng lặp cần phải lo lắng về yếu tố đầu tiên hoặc cuối cùng cũng như cấu trúc vòng lặp để có được giới hạn đúng; ví dụ nếu bạn tham khảo a[I]a[I-1]điều gì xảy ra khi Igiá trị tối thiểu?

Ngoài ra, hãy xem xét các trường hợp số lần lặp (chính xác) là cực kỳ: nếu giới hạn gặp nhau và bạn sẽ có 0 lần lặp, liệu nó có hoạt động mà không có trường hợp đặc biệt không? Và những gì về chỉ 1 lần lặp, trong đó giới hạn thấp nhất cũng là giới hạn cao nhất cùng một lúc?

Kiểm tra các trường hợp cạnh (cả hai cạnh của mỗi cạnh) là những gì bạn nên làm khi viết vòng lặp và những gì bạn nên làm trong đánh giá mã.


1

Tôi sẽ cố gắng tránh xa các chủ đề được đề cập đến galore.

Các công cụ / mô hình tinh thần để tránh những sai lầm như vậy là gì?

Công cụ

Đối với tôi, công cụ lớn nhất để viết tốt hơn forwhilecác vòng lặp không phải là viết bất kỳ forhoặc whilevòng lặp nào cả.

Hầu hết các ngôn ngữ hiện đại cố gắng nhắm mục tiêu vấn đề này trong một số thời trang hoặc khác. Ví dụ, Java, trong khi có Iteratorquyền ngay từ đầu, vốn thường hơi khó sử dụng, đã đưa ra cú pháp rút gọn để sử dụng chúng dễ dàng hơn trong bản phát hành latler. C # cũng có chúng, v.v.

Ngôn ngữ hiện đang được ưa chuộng của tôi, Ruby, đã áp dụng phương pháp tiếp cận chức năng ( .each, .mapv.v.) toàn diện. Điều này rất mạnh mẽ. Tôi vừa mới đếm nhanh trong một số cơ sở mã Ruby tôi đang làm việc: trong khoảng 10.000 dòng mã, có 0 forvà khoảng 5 while.

Nếu tôi bị buộc phải chọn một ngôn ngữ mới, việc tìm kiếm các vòng lặp dựa trên chức năng / dữ liệu như thế sẽ rất cao trong danh sách ưu tiên.

Mô hình trí tuệ

Hãy nhớ rằng đó whilelà mức độ trừu tượng tối thiểu bạn có thể nhận được, chỉ cần một bước ở trên goto. Theo tôi, forlàm cho nó thậm chí còn tồi tệ hơn thay vì tốt hơn vì nó kết hợp cả ba phần của vòng lặp với nhau.

Vì vậy, nếu tôi đang ở một nơi xa xôi forđược sử dụng, thì tôi chắc chắn rằng cả 3 phần đều đơn giản và luôn giống nhau. Điều này có nghĩa là tôi sẽ viết

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

Nhưng không có gì phức tạp hơn. Có lẽ, đôi khi tôi sẽ đếm ngược, nhưng tôi sẽ cố hết sức để tránh nó.

Nếu sử dụng while, tôi tránh xa các shenannigans bên trong liên quan đến điều kiện vòng lặp. Bài kiểm tra bên trong while(...)sẽ đơn giản nhất có thể, và tôi sẽ tránh breaktốt nhất có thể. Ngoài ra, vòng lặp sẽ ngắn (đếm các dòng mã) và bất kỳ số lượng mã lớn hơn sẽ được tính ra.

Đặc biệt là nếu điều kiện thực tế phức tạp, tôi sẽ sử dụng "biến điều kiện" rất dễ phát hiện và không đặt điều kiện vào whilechính câu lệnh:

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

(Hoặc một cái gì đó như thế, trong các biện pháp chính xác, tất nhiên.)

Điều này cung cấp cho bạn một mô hình tinh thần rất dễ dàng đó là "biến này đang chạy từ 0 đến 10 đơn điệu" hoặc "vòng lặp này chạy cho đến khi biến đó là sai / đúng". Hầu hết các bộ não dường như có thể xử lý mức độ trừu tượng này tốt.

Mong rằng sẽ giúp.


1

Các vòng lặp ngược, đặc biệt, có thể khó lý do vì nhiều ngôn ngữ lập trình của chúng tôi thiên về lặp đi lặp lại, cả trong cú pháp vòng lặp chung và bằng cách sử dụng các khoảng nửa mở dựa trên không. Tôi không nói rằng việc các ngôn ngữ đưa ra những lựa chọn đó là sai; Tôi chỉ nói rằng những lựa chọn đó làm phức tạp suy nghĩ về các vòng lặp ngược.

Nói chung, hãy nhớ rằng một vòng lặp for chỉ là đường cú pháp được xây dựng xung quanh vòng lặp while:

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

tương đương với:

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

(có thể có thêm một lớp phạm vi để giữ các biến được khai báo trong bước init cục bộ vào vòng lặp).

Điều này tốt cho nhiều loại vòng lặp, nhưng bước cuối cùng sẽ rất khó xử khi bạn đi lùi. Khi làm việc ngược lại, tôi thấy việc bắt đầu chỉ mục vòng lặp với giá trị sau giá trị tôi muốn dễ dàng hơn và di chuyển stepphần này lên đầu vòng lặp, như sau:

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
}

hoặc, như một vòng lặp for:

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

Điều này có vẻ đáng sợ, vì biểu thức bước nằm trong cơ thể chứ không phải tiêu đề. Đó là một tác dụng phụ đáng tiếc của xu hướng chuyển tiếp vốn có trong cú pháp vòng lặp. Vì điều này, một số người sẽ cho rằng bạn thay vì làm điều này:

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

Nhưng đó là một thảm họa nếu biến chỉ mục của bạn là loại không dấu (vì nó có thể ở C hoặc C ++).

Với ý nghĩ này, hãy viết chức năng chèn của bạn.

  1. Vì chúng tôi sẽ làm việc lạc hậu, chúng tôi sẽ để chỉ mục vòng lặp là mục nhập sau vị trí mảng "hiện tại". Tôi sẽ thiết kế hàm để lấy kích thước của số nguyên thay vì chỉ mục cho phần tử cuối cùng bởi vì các phạm vi nửa mở là cách tự nhiên để biểu diễn các phạm vi trong hầu hết các ngôn ngữ lập trình và vì nó cho chúng ta một cách để biểu diễn một mảng trống mà không cần dùng đến đến một giá trị ma thuật như -1.

    function insert(array, size, value) {
      var j = size;
    
  2. Trong khi giá trị mới nhỏ hơn phần tử trước , hãy tiếp tục dịch chuyển. Tất nhiên, yếu tố trước đó có thể được kiểm tra chỉ nếu có một yếu tố trước đó, vì vậy chúng tôi đầu tiên phải kiểm tra rằng chúng tôi không phải ở đầu:

      while (j != 0 && value < array[j - 1]) {
        --j;  // now j become current
        array[j + 1] = array[j];
      }
    
  3. Điều này để lại jđúng nơi chúng ta muốn giá trị mới.

      array[j] = value; 
    };
    

Lập trình Ngọc trai của Jon Bentley đưa ra một lời giải thích rất rõ ràng về sắp xếp chèn (và các thuật toán khác), có thể giúp xây dựng các mô hình tinh thần của bạn cho các loại vấn đề này.


0

Bạn có đơn giản bối rối về những gì một forvòng lặp thực sự làm và cơ chế hoạt động của nó?

for(initialization; condition; increment*)
{
    body
}
  1. Đầu tiên, việc khởi tạo được thực thi
  2. Sau đó, điều kiện được kiểm tra
  3. Nếu điều kiện là đúng, cơ thể được chạy một lần. Nếu không goto # 6
  4. Mã gia tăng được thực thi
  5. Đi số 2
  6. Kết thúc vòng lặp

Nó có thể hữu ích cho bạn để viết lại mã này bằng cách sử dụng một cấu trúc khác. Đây là cùng một vòng lặp while:

initialization
while(condition)
{
    body
    increment
}

Dưới đây là một số gợi ý khác:

  • Bạn có thể sử dụng cấu trúc ngôn ngữ khác như vòng lặp foreach không? Điều này quan tâm đến điều kiện và bước tăng cho bạn.
  • Bạn có thể sử dụng chức năng Bản đồ hoặc Bộ lọc không? Một số ngôn ngữ có chức năng với các tên này lặp lại bên trong thông qua bộ sưu tập cho bạn. Bạn chỉ cần cung cấp các bộ sưu tập và cơ thể.
  • Bạn thực sự nên dành nhiều thời gian hơn để làm quen với forcác vòng lặp. Bạn sẽ sử dụng chúng mọi lúc. Tôi đề nghị bạn bước qua một vòng lặp for trong trình gỡ lỗi để xem cách nó thực thi.

* Lưu ý rằng trong khi tôi sử dụng thuật ngữ "gia tăng", thì đó chỉ là một số mã nằm sau phần thân và trước khi kiểm tra điều kiện. Nó có thể là một giảm hoặc không có gì cả.


1
Tại sao các downvote?
2023861

0

Cố gắng hiểu biết thêm

Đối với các thuật toán không tầm thường với các vòng lặp, bạn có thể thử phương pháp sau:

  1. Tạo một mảng cố định với 4 vị trí và đặt một số giá trị vào để mô phỏng vấn đề;
  2. Viết thuật toán của bạn để giải quyết vấn đề đã cho , không có bất kỳ vòng lặp nàovới các chỉ mục được mã hóa cứng ;
  3. Sau đó, thay thế các chỉ mục được mã hóa cứng trong mã của bạn bằng một số biến ihoặc j, và tăng / giảm các biến này khi cần thiết (nhưng vẫn không có bất kỳ vòng lặp nào);
  4. Viết lại mã của bạn và đặt các khối lặp đi lặp lại trong một vòng lặp , đáp ứng các điều kiện trước và sau;
  5. [ tùy chọn ] viết lại vòng lặp của bạn ở dạng bạn muốn (for / while / do while);
  6. Điều quan trọng nhất là viết thuật toán của bạn một cách chính xác; sau đó, bạn cấu trúc lại và tối ưu hóa mã / vòng lặp của bạn nếu cần thiết (nhưng điều này có thể làm cho mã không còn tầm thường nữa đối với người đọc)

Vấn đề của bạn

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

Viết phần thân của vòng lặp bằng tay nhiều lần

Chúng ta hãy sử dụng một mảng cố định với 4 vị trí và cố gắng viết thuật toán bằng tay, không có vòng lặp:

           //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

Viết lại, loại bỏ các giá trị được mã hóa cứng

//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

Dịch sang một vòng lặp

Với 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;
    }
}

Tái cấu trúc / viết lại / tối ưu hóa vòng lặp theo cách bạn muốn:

Với 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

với 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: mã giả định đầu vào là hợp lệ và mảng đó không chứa sự lặp lại;


-1

Đây là lý do tại sao tôi sẽ tránh việc viết các vòng lặp hoạt động trên các chỉ số thô, có lợi cho các hoạt động trừu tượng hơn, bất cứ khi nào có thể.

Trong trường hợp này, tôi sẽ làm một cái gì đó như thế này (bằng mã giả):

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

-3

Trong ví dụ của bạn, phần thân của vòng lặp khá rõ ràng. Và một điều khá rõ ràng là một số yếu tố phải được thay đổi vào cuối. Vì vậy, bạn viết mã, nhưng không có sự bắt đầu của vòng lặp, điều kiện kết thúc và bài tập cuối cùng.

Sau đó, bạn đi ra khỏi mã và tìm ra đó là động thái đầu tiên cần được thực hiện. Bạn thay đổi bắt đầu của vòng lặp để di chuyển đầu tiên là chính xác. Bạn đi ra khỏi mã một lần nữa và tìm ra đâu là động tác cuối cùng cần được thực hiện. Bạn thay đổi điều kiện kết thúc để bước cuối cùng sẽ chính xác. Và cuối cùng, bạn đi ra khỏi mã của mình và tìm ra nhiệm vụ cuối cùng phải là gì, và sửa mã cho phù hợp.


1
Bạn có thể vui lòng đưa ra một số ví dụ?
CodeYogi

OP đã có một ví dụ.
gnasher729

2
Ý anh là gì? Tôi là OP.
CodeYogi
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.