12.29 奧卡姆剃刀:使代碼簡潔明瞭


奧卡姆剃刀:使代碼簡潔明瞭

背景

在我的Group Randomizer應用程序中,我的MVP(最低可行產品)的功能之一是創建保存組功能。 當用戶單擊"保存組"按鈕時,它將當前選項列表保存為新組。 但是,此功能的核心功能是,如果找到一個具有相同選項的現有保存組,而不考慮順序,則將顯示一條警報,指出具有您當前選項列表的組已經存在。

例如,"保存的組#1"具有人員列表,即"Sam"和"Bob"。 如果我當前的選項列表是"Bob"和"Sam",並且我嘗試將其另存為新組,則會彈出警報,提示"具有當前選項列表的已保存組已存在。 它是保存的組#1"。

對於本博客,我將介紹如何創建算法來創建此檢查功能以及Occam剃刀的應用。 如果您不熟悉Occam的剃刀,則Wikipedia的定義是:

奧卡姆(Occam)剃刀……是解決問題的原則,其中指出"不應在沒有必要的情況下增加實體"。

嘗試檢查組算法

下面的代碼是我第一次嘗試檢查組算法。

<code>function checkGroups(savedGroup, currentListOfOptions) {  savedGroupHash = {}  // populates the savedGroupHash based on the contents of the   savedGroup array parameter  for (let i = 0; i < savedGroup.length; i++){    savedGroupHash[savedGroup[i]] = true  }  // checks the savedGroupHash against the current list of options to check if savedGroupHash contains all of the list of options  for (let j = 0; j < currentListOfOptions.length; j++) {    if (!savedGroupHash.hasOwnProperty(currentListOfOptions[j])) {      return false    }  }  return true}/<code>


注意:此算法的參數均為數組。 每當下面提到"選項"時,它就是指它們各自數組中的元素。

為了提供有關如何使用該算法的總體信息,我創建了兩個for循環。 第一個for循環根據savedGroup參數的內容填充了saveGroupHash。 對於鍵值對,鍵是默認值為true的選項。 此值(或缺少該值)將在第二個for循環中用作布爾值。 在第二個for循環中,我對照了savedGroupHash檢查了currentListOfOptions參數中的每個選項。 只要currentListOfOptions中的選項未包含在savedGroupHash中,我將返回false,這將立即退出該函數。 如果不是,那麼最後一行將返回true,因為從第二個for循環開始它沒有返回false。

在檢查組算法中測試首次嘗試

根據我在上一個博客中進行正確測試的經驗,我決定創建一些測試用例以徹底測試我的算法。 在這種情況下,想到了三種情況。

  1. savedGroup和currentListOfOptions實際上具有完全相同的選項,但順序不同。 這應該返回true.
  2. savedGroup包含與currentListOfOptions相同的選項,但具有比currentListOfOptions更多的選項。 這應該返回false.
  3. currentListOfOptions包含與saveGroup相同的選項,但具有比saveGroup更多的選項。 這應該返回false。

在第一種情況下,我們使用saveGroup = ["Sam","Bob"]和currentListOfOptions = ["Bob"," Sam"]。 接下來,讓我們將這兩個變量作為參數輸入到算法中,然後控制檯記錄返回值。

const savedGroup = ["Sam", "Bob"]const currentListOfOptions = ["Bob", "Sam"]console.log(checkGroups(savedGroup, currentListOfOptions)) //true


看起來這已經通過了我們的第一個測試用例。 讓我們繼續第二個測試用例。

在第二種情況下,我們使用savedGroup = [" Sam"," Bob"," Jane"]和currentListOfOptions = [" Bob"," Sam"]。 接下來,讓我們將這兩個變量輸入算法,然後控制檯記錄返回值。

<code>const savedGroup = ["Sam", "Bob"]const currentListOfOptions = ["Bob", "Sam"]console.log(checkGroups(savedGroup, currentListOfOptions)) //true/<code>


看起來這沒有通過我們的第二個測試案例。 讓我們繼續第三個測試案例。

在第三種情況下,我們使用savedGroup = [" Sam"," Bob"]和currentListOfOptions = [" Bob"," Sam"," Jane"]。 接下來,讓我們將這兩個變量輸入算法,然後控制檯記錄返回值。

<code>const savedGroup = ["Sam", "Bob", "Jane"]const currentListOfOptions = ["Bob", "Sam"]console.log(checkGroups(savedGroup, currentListOfOptions)) //true/<code>

看起來這已經通過了我們的第三個測試用例。

嗯……看來此算法無法正常工作。 在分析完我的代碼後,看起來每個參數的長度差異可能會使其丟掉。 如果是這種情況,請更改算法以適應這種情況。

檢查組算法的第二次嘗試

下面的代碼是我第二次嘗試檢查組算法。

<code>function checkGroups(savedGroup, currentListOfOptions) {  const savedGroupHash = {}  for (let i = 0; i < savedGroup.length; i++) {    savedGroupHash[savedGroup[i]] = 1  }  for (let j = 0; j < currentListOfOptions.length; j++) {    if (savedGroupHash.hasOwnProperty(currentListOfOptions[j])) {      savedGroupHash[currentListOfOptions[j]] -= 1    } else {      savedGroupHash[currentListOfOptions[j]] = 1    }  }    let sum = Object.values(savedGroupHash).reduce( (acc, current) => acc += current)  if (sum === 0) {    //this means the same group already exists    return true  } else {    //this means the group does not already exist    return false  }}/<code>


為了提供對該算法的總體概述,有兩個for循環,一個累加器和一個if / else語句。 第一個for循環與第一個算法中的第一個for循環幾乎具有相同的功能,但是,它提供了默認值1。第二個for循環為currentListOfOptions中的每個選項操縱saveedGroupHash。 如果該選項位於SavedGroupHash中,則將值減1,結果為0;否則,添加一個新的鍵值對,其中鍵為選項本身,值為1,類似於第一個 for循環。

接下來,使用累加器,它將獲取saveGroupHash中所有值的總和。 sum的目的是幫助確定saveGroup數組和currentListOfOptions數組之間的長度是否存在差異。 我在if / else語句的條件比較中使用總和,以便返回true或false。

現在是令人不安的部分:測試此新算法。

在檢查組算法中測試第二次嘗試

為了保持一致性,我將使用與首次嘗試該算法時相同的測試用例。 為簡潔起見,我還將結合所有三個測試用例的代碼和結果。

<code>//Test Case #1const savedGroup = ["Sam", "Bob"]const currentListOfOptions = ["Bob", "Sam"]console.log(checkGroups(savedGroup, currentListOfOptions)) //true//Test Case #2const savedGroup = ["Sam", "Bob", "Jane"]const currentListOfOptions = ["Bob", "Sam"]console.log(checkGroups(savedGroup, currentListOfOptions)) //false//Test Case #3const savedGroup = ["Sam", "Bob"]const currentListOfOptions = ["Bob", "Sam", "Jane"]console.log(checkGroups(savedGroup, currentListOfOptions)) //false/<code>

是! 看起來該算法正在按預期工作! 我們終於可以稱之為一天了,但是…

奧卡姆剃刀

第二天,當我在Group Randomizer應用程序上工作時,我再次查看了檢查組算法。 我一直為這個算法的成功而苦惱(因為我花了1-2個小時來開發它),直到我意識到第二種算法完全是矯枉過正。 什麼意思?

讓我們再次看看測試用例,以及為什麼我改變了對算法的第一次嘗試。 導致測試失敗的根本原因的原始假設是每個參數的數組長度(如測試用例2所示)。 在該算法的第二次嘗試中,我操縱了savedGroupHash哈希值,以便跟蹤saveGroup和currentListOfOptions之間的數組長度差異。 總和是數組長度之間的差。 如果sum為0,則數組長度相同,並返回true。 其他任何值都將返回false。

換一種方式來看,我本質上是在檢查saveGroup和currentListOfOptions的長度是否相同。 由於我知道這兩個參數是數組類型,因此我只需在每個參數上調用.length即可確認它們的長度是否相等。 如果這些長度不一樣,我可以立即返回false。 下面的代碼表示此實現。

<code>if (savedGroup.length !== currentListOfOptions.length) {   return false}/<code>


現在,我可以使用上面的代碼修改算法的首次嘗試。

<code>function checkGroups(savedGroup, currentListOfOptions) {  //insert new code here  if (savedGroup.length !== currentListOfOptions.length) {    return false  }  savedGroupHash = {}  for (let i = 0; i < savedGroup.length; i++){    savedGroupHash[savedGroup[i]] = true  }  for (let j = 0; j < currentListOfOptions.length; j++) {    if (!savedGroupHash.hasOwnProperty(currentListOfOptions[j])) {      return false    }  }    return true}/<code>


您可能接下來會猜到,我使用此算法運行了所有三個測試用例,並通過了所有三個測試用例!

我能夠簡化檢查數組長度的代碼

<code>for (let j = 0; j < currentListOfOptions.length; j++) {    if (savedGroupHash.hasOwnProperty(currentListOfOptions[j])) {      savedGroupHash[currentListOfOptions[j]] -= 1    } else {      savedGroupHash[currentListOfOptions[j]] = 1    }  }    let sum = Object.values(savedGroupHash).reduce( (acc, current) => acc += current)if (sum === 0) {    //this means the same group already exists    return true  } else {    //this means the group does not already exist    return false  }}/<code>


<code>if (savedGroup.length !== currentListOfOptions.length) {   return false}/<code>


第一次嘗試檢查組算法的意外結果

在測試第一個未修改算法和第二個算法之間的過程中,我意識到了第一個未修改算法的實際作用。 儘管它沒有達到檢查組是否相同的目的,但它檢查了一個組currentListOfOptions是否是另一組saveGroup的子集。 我無意間創建了一種檢查子集的算法,而不是檢查整個集合的算法。 現在,如果將來需要檢查子集,則無需從頭開始重新創建算法,也不需要使用第一個未修改的算法來利用它。

重要要點

創建此算法時,我學了兩個主要課程。

  • 不要因為過於複雜而陷入困境。

在算法的第二次嘗試中,我一直在想著該算法必須包含某種複雜的解決方案,因為這不是一個共同的功能(可能不正確)。 如您所知,當可以用三行代碼(如果我不使用大括號的話,則只需一行)完成此錯誤時,我就以一種迂迴的方式解決了該錯誤。 如果您開始認為您的解決方案開始變得複雜,請退後一步,重新評估您要達到的目標,然後看看是否可以用其他方式表達它,以尋求更好的方法。

  • 研究算法實際上是有幫助的。

像許多有抱負的軟件開發人員一樣,我將研究不同的數據結構和算法,並在LeetCode等網站上練習求解算法。 起初,我並沒有真正瞭解研究這些算法的目的,因為我覺得我永遠不會在技術面試之外應用這些算法。 但是,在我第一次嘗試創建檢查組算法時,我憑直覺將哈希值用於比較目的。 當我想到為什麼這樣做時,我突然意識到這是因為使用哈希是解決LeetCode算法以將時間複雜度從O(n²)提升到O(n)的一種常用方法。 現在,我發現我最想不到的算法可能適用於我的代碼。

(本文翻譯自Samuel Guo的文章《Occam's Razor: Keeping the Code Short and Simple》,參考:https://levelup.gitconnected.com/occams-razor-keeping-the-code-short-and-simple-7ce4b60f87d4)


分享到:


相關文章: