Hackerrank: Memecahkan rekor

Oct 22 2020

Saya belajar Clojure dan saya peringkat n00b dalam hal itu, mencoba belajar dari buku dan tutorial online (tapi kadang-kadang saya khawatir saya mengambil kebiasaan buruk atau setidaknya tidak semua kebiasaan baik). Untuk latihan saya melakukan masalah Breaking the Records di Hackerrank.

TL; DR deskripsi masalah:

Untuk daftar skor (dalam urutan historis), hitung berapa kali skor terbaik sebelumnya dilampaui dan skor terburuk sebelumnya dilampaui.

Dalam bahasa yang berulang, cukup mudah untuk hanya mengulang-ulang daftar ; tetapi Clojure tidak melakukan iterasi dan saya memutuskan untuk mengatasi masalah ini untuk latihan membangun solusi rekursif (ujung ekor). Untuk memudahkan diri saya sendiri, pertama-tama saya melakukan solusi rekursif di Java, yang kemudian saya terjemahkan. Secara keseluruhan, ini adalah fungsi rekursif yang cukup sederhana tanpa melibatkan operasi roket.

Jelas kode saya berfungsi, seperti yang ditunjukkan oleh tes unit yang disertakan. Namun kekhawatiran saya adalah sebagai berikut:

  • Ketika diletakkan di sebelah kode Java, keduanya terlihat cukup mirip. Apakah saya mengikuti pemrograman Clojure "idiomatik", atau itu hanya "transliterasi kata demi kata" yang kikuk?
  • Adakah area yang bisa lebih kompak dan / atau lebih mudah dipahami dengan menggunakan konstruksi Clojure yang berbeda?

Masukan kritis Anda akan sangat dihargai - termasuk mengenai pengujian unit , karena itu bisa dibilang bagian penting dari pemrograman, yang ingin saya pelajari dan praktikkan secara paralel.

Kode:

(ns hackerrank.breaking-records
  (:require [clojure.test :refer :all]))

(defrecord Record [min max countworse countbetter])

(defn recalc-record [rec newscore]
  (Record.
    (min newscore (:min rec))
    (max newscore (:max rec))
    (+ (:countworse rec) (if (> (:min rec) newscore) 1  0))
    (+ (:countbetter rec) (if (< (:max rec) newscore) 1  0))))

(defn accumulate [curr-record remaining-scores]
  (if (nil? (second remaining-scores))
    curr-record
    (recur (recalc-record curr-record (second remaining-scores)) (rest remaining-scores)))
)

(defn breaking-records [scores]
  (let [result (accumulate (Record. (first scores) (first scores) 0 0) scores)]
    (list (:countbetter result) (:countworse result))))

(deftest test-records
  (testing "edge cases"
    (is (= '(0 0) (breaking-records '())) "no games played yet")
    (is (= '(0 0) (breaking-records '(5))) "single game"))
  (testing "hackerrank examples"
    (is (= '(2 4) (breaking-records '(10 5 20 20 4 5 2 25 1))))
    (is (= '(4 0) (breaking-records '(3 4 21 36 10 28 35 5 24 42)))))
)

Jawaban

2 AlanThompson Oct 23 2020 at 02:14

Saya menulis ulang solusi Anda untuk menggunakan fitur Clojure yang lebih khas. Saat Anda mengulang data dan perlu melacak status terakumulasi, hal itu sulit dikalahkan loop/recur. Contoh pertama:

(ns tst.demo.core
  (:use clojure.test))

(defn breaking-records
  [scores]
  ; this loop has 5 variables. Init all of them
  (loop [low         (first scores)
         high        (first scores)
         nworse      0
         nbetter     0
         score-pairs (partition 2 1 scores)]
    (if (empty? score-pairs)
      {:nbetter nbetter :nworse nworse}
      (let [curr-score-pair (first score-pairs)
            new-score       (second curr-score-pair)]
        ; start the next iteration with modified versions of the 5 loop vars
        (recur
          (min new-score low)
          (max new-score high)
          (if (< new-score low)
            (inc nworse)
            nworse)
          (if (< high new-score)
            (inc nbetter)
            nbetter)
          (rest score-pairs))))))

dan tes unit:

(deftest test-records
  (testing "edge cases"
    (is (= (breaking-records []) {:nbetter 0 :nworse 0}) "no games played yet")
    (is (= (breaking-records [5]) {:nbetter 0 :nworse 0}) "single game"))
  (testing "hackerrank examples"
    (is (= (breaking-records [10 5 20 20 4 5 2 25 1]) {:nbetter 2 :nworse 4}))
    (is (= (breaking-records [3 4 21 36 10 28 35 5 24 42]) {:nbetter 4 :nworse 0}))))

; ***** NOTE: it's much easier to use vectors like [1 2 3] instead of a quoted list `(1 2 3)

Silakan lihat daftar dokumentasi ini , khususnya. CheatSheet Clojure. Juga, proyek template secara keseluruhan menunjukkan bagaimana saya suka menyusun sesuatu. :)

Fungsi yang paling membantu adalah partition. Lihat dokumennya .


Refaktor ringan

Anda dapat menyederhanakannya sedikit dan membuatnya sedikit lebih ringkas dengan menggunakan fungsi yang lebih terspesialisasi seperti reducedan cond->. Versi ini menggunakan peta untuk menahan status dan reducemelakukan perulangan:

(defn breaking-records
  [scores]
  (let [state-init     {:low     (first scores)
                        :high    (first scores)
                        :nworse  0
                        :nbetter 0}
        accum-stats-fn (fn [state score-pair]
                         ; Use map destructuring to pull out the 4 state variables
                         (let [{:keys [low high nworse nbetter]} state 
                               new-score (second score-pair)
                               state-new {:low     (min new-score low)
                                          :high    (max new-score high)
                                          :nworse  (cond-> nworse
                                                     (< new-score low) (inc))
                                          :nbetter (cond-> nbetter
                                                     (< high new-score) (inc))}]
                           state-new))
        state-final    (reduce accum-stats-fn
                         state-init
                         (partition 2 1 scores))
        result         (select-keys state-final [:nworse :nbetter])]
    result))