Měl jsem níže uvedený problém v testu kódování a dostal jsem 28/30 testů a 2 selhaly kvůli do vypršení časového limitu.
Problém
Vytvořili jste programovací jazyk a nyní jste se rozhodli přidat k němuhashmap
podporu. Bylo zjištěno, že v běžných programovacích jazycích je nemožné přidat číslo ke všem klíčům / hodnotámhashmap
. Takže jste se rozhodli implementovat vlastníhashmap
ve vašem novém jazyce s následujícími operacemi.
insert x y
– vložte a objekt pomocí klíčex
a hodnotyy
get x
– vrátí hodnotu objektu pomocí klíčex
addToKey x
– přidatx
ke všem klíčům v mapěaddToValue y
– přidaty
ke všem hodnotám v mapěVaším úkolem je implementovat tento
hashmap
, použít zadané dotazy a vyhledat součet všech výsledků proget
operací
Například
- Pro
queryType=["insert","insert","addToValue","addToKey","get"]
aquery=[[1,2],[2,3],[2],[1],[3]]
, výstup by měl být .
Vysvětlení
-
insert 1 2
– hashmap bude{1:2}
-
insert 2 3
– hashmap bude{1:2,2:3}
-
addToValue 2
– hashmap bude{1:4,2:5}
-
addToKey 1
– hashmap bude{2:4,3:5}
-
get 3
– hodnota je 5
vstup / Výstup
- [časový limit provedení] 3 sekundy (Java)
- [input] array.string queryType
pole typy dotazů. je zaručeno, že každáqueryType[i]
kterákoli z výše uvedených operací
1 < = queryType.length < = 10 ^ 5- [vstup] array.array.integer dotaz
Pole dotazů, kde je každý dotaz uveden dvěma čísly pro vložení a jedním číslem pro ostatní Klíčové hodnoty jsou v rozsahu [-10 ^ 9,10 ^ 9]
Níže je moje řešení v Java
long hashMap(String[] queryType, int[][] query) { long sum = 0; Integer currKey = 0; Integer currValue = 0; Map<Integer, Integer> values = new HashMap<>(); for (int i = 0; i < queryType.length; i++) { String currQuery = queryType[i]; switch (currQuery) { case "insert": HashMap<Integer, Integer> copiedValues = new HashMap<>(); if (currKey != 0 || currValue != 0) { Set<Integer> keys = values.keySet(); for (Integer key : keys) { copiedValues.put(key + currKey, values.get(key) + currValue); } values.clear(); values.putAll(copiedValues); currValue = 0; currKey = 0; } values.put(query[i][0], query[i][1]); break; case "addToValue": currValue += values.isEmpty() ? 0 : query[i][0]; break; case "addToKey": currKey += values.isEmpty() ? 0 : query[i][0]; break; case "get": copiedValues = new HashMap<>(); if (currKey != 0 || currValue != 0) { Set<Integer> keys = values.keySet(); for (Integer key : keys) { copiedValues.put(key + currKey, values.get(key) + currValue); } values.clear(); values.putAll(copiedValues); currValue = 0; currKey = 0; } sum += values.get(query[i][0]); } } return sum; }
Existuje nějaká jiná datová struktura, kterou mohu použít místo hashmap
nebo mohu vylepšit můj kód je lineárnější?
Komentáře
Odpovědět
Navrhuji, abyste si vytvořili vlastní OffsetIntegerMap
, který může mapovat mezi celými čísly a zpracovávat offset klíčů a hodnot.
You nemusíte nutně implementovat HashMap
od nuly, definujte si své vlastní omezené rozhraní a implementujte jej pomocí existujícího Map<Integer, Integer>
prostřednictvím kompozice.
Zpracováním posunů odděleně od klíčů a hodnot končí složitost operací posunutí O (1) místo O (n) při přepočtech a Map<>
operace put a get zůstanou na původním O (1).
Příklad “ OffsetIntegerMap
„:
import java.util.HashMap; import java.util.Map; public class OffsetIntegerMap { private final Map<Integer, Integer> actualMap; private int keyOffset = 0; private int valueOffset = 0; public OffsetIntegerMap() { actualMap = new HashMap<>(); } public int size() { return actualMap.size(); } public boolean isEmpty() { return actualMap.isEmpty(); } public boolean containsKey(int key) { var keyWithoutOffset = key - keyOffset; return actualMap.containsKey(keyWithoutOffset); } public boolean containsValue(int value) { var valueWithoutOffset = value - valueOffset; return actualMap.containsValue(valueWithoutOffset); } public Integer get(int key) { var keyWithoutOffset = key - keyOffset; var value = actualMap.get(keyWithoutOffset); if (value == null) return null; return value + valueOffset; } public Integer put(int key, int value) { var keyWithoutOffset = key - keyOffset; var valueWithoutOffset = value - valueOffset; var oldValue = actualMap.put(keyWithoutOffset, valueWithoutOffset); if (oldValue == null) return null; return oldValue + valueOffset; } public Integer remove(int key) { var keyWithoutOffset = key - keyOffset; var oldValue = actualMap.remove(keyWithoutOffset); if (oldValue == null) return null; return oldValue + valueOffset; } public void clear() { actualMap.clear(); keyOffset = 0; valueOffset = 0; } public int getKeyOffset() { return keyOffset; } public void setKeyOffset(int keyOffset) { this.keyOffset = keyOffset; } public int getValueOffset() { return valueOffset; } public void setValueOffset(int valueOffset) { this.valueOffset = valueOffset; } public void addToValues(int toAdd) { this.valueOffset += toAdd; } public void addToKeys(int toAdd) { this.keyOffset += toAdd; } }
Zapouzdřením ofsetové logiky se smyčka zpracování také stává mnohem jednodušší, aniž by co nejvíce refaktorovala ing:
static long hashMap(String[] queryType, int[][] query) { long sum = 0; var map = new OffsetIntegerMap(); for (int i = 0; i < queryType.length; i++) { String currQuery = queryType[i]; switch (currQuery) { case "insert": map.put(query[i][0], query[i][1]); break; case "addToValue": map.addToValues(query[i][0]); break; case "addToKey": map.addToKeys(query[i][0]); break; case "get": sum += map.get(query[i][0]); } } return sum; }
Komentáře
- Děkuji. Vypadá to na lepší implementaci. Zkontroluji tuto logiku.
- Tuto odpověď považuji za nejlepší. Použití třídy oddělené od testování všech případů.
keyWithoutOffset
avalueWithoutOffset
(myslím, že jsem viděl chybu v původním kódu w.r..t. k tomu). Jasné názvy (offset). Jen názvy metod jsou Map centrické místo těch v požadavcích. - Můžete použít příklad z otázky. Stačí nahradit
[]
znakem{}
.queryType
jeString[]
aquery
jeint[][]
. - Ah, přehlédl jsem to. A já ‚ jsem příliš zkažený kódováním výzevných webů, které mi právě “ spustit “ knoflík :-). Upraveno toto řešení do mé vlastní odpovědi.
- Ofset nebude stejný pro každý klíč v hashmapě! – začít se sadou klíčů (1,2,3) – přidat 10 ke všem klíčům, nyní sada klíčů je (10,11,12) – vložit nový klíč (5), nyní sada klíčů je (10,11,12,5) – přidat 10 na všechny klíče, nyní sada klíčů je (20,21,22,15). První 3 klíče tedy účinně obsahovaly offset 20, ale poslední klíč měl offset pouze 10 (tj. Přidání klíče provedené před vložením tohoto klíče (5) bude ignorováno).
Odpověď
Mám pro vás několik návrhů.
Extrahujte část logiky metod.
Ve vašem kód, když je dotaz insert
a get
, máte dva velké bloky kódu, které jsou podobné; můžete extrahovat na metodu a znovu ji použít v obou částech.
Navrhuji metodu, která vrací logickou hodnotu založenou na podmínce if
, takže budete schopen nastavit proměnné currValue
a currKey
na nulu.
long hashMap(String[] queryType, int[][] query) { //[...] switch (currQuery) { //[...] case "insert": if (didWeCopiedValuesToMap(currKey, currValue, values)) { currValue = 0; currKey = 0; } values.put(query[i][0], query[i][1]); break; //[...] } //[...] } private boolean didWeCopiedValuesToMap(Integer currKey, Integer currValue, Map<Integer, Integer> values, HashMap<Integer, Integer> copiedValues) { if (currKey != 0 || currValue != 0) { Set<Integer> keys = values.keySet(); for (Integer key : keys) { copiedValues.put(key + currKey, values.get(key) + currValue); } values.clear(); values.putAll(copiedValues); return true; } return false; }
Chcete-li zkontrolovat aktuální dotaz currQuery
, můžete každý z nich extrahovat metodou.
private boolean isGet(String currQuery) { return "get".equals(currQuery); } private boolean isAddToKey(String currQuery) { return "addToKey".equals(currQuery); } private boolean isAddToValue(String currQuery) { return "addToValue".equals(currQuery); } private boolean isInsert(String currQuery) { return "insert".equals(currQuery); }
Vždy, když je to možné, používejte primitiva
Když vězte, že není možné získat nulovou hodnotu s číslem, zkuste použít primitiva; zabírají méně paměti a jsou rychlejší než obalová třída.
Před
Integer currKey = 0; Integer currValue = 0;
Po
int currKey = 0; int currValue = 0;
Zkuste vložit méně kódu do switch
bloků
Podle mého názoru se kód stává méně čitelným, pokud jsou v přepínacím bloku více než 3 řádky kódů; Navrhuji, abyste jej převedli na is-else-if
. Díky této konverzi bude kód kratší a čitelnější.
Před
switch (currQuery) { case "insert": if (didWeCopiedValuesToMap(currKey, currValue, values)) { currValue = 0; currKey = 0; } values.put(query[i][0], query[i][1]); break; case "addToValue": currValue += values.isEmpty() ? 0 : query[i][0]; break; case "addToKey": currKey += values.isEmpty() ? 0 : query[i][0]; break; case "get": if (didWeCopiedValuesToMap(currKey, currValue, values)) { currValue = 0; currKey = 0; } sum += values.get(query[i][0]); }
Po
if ("insert".equals(currQuery)) { if (didWeCopiedValuesToMap(currKey, currValue, values)) { currValue = 0; currKey = 0; } values.put(query[i][0], query[i][1]); } else if ("addToValue".equals(currQuery)) { currValue += values.isEmpty() ? 0 : query[i][0]; } else if ("addToKey".equals(currQuery)) { currKey += values.isEmpty() ? 0 : query[i][0]; } else if ("get".equals(currQuery)) { if (didWeCopiedValuesToMap(currKey, currValue, values)) { currValue = 0; currKey = 0; } sum += values.get(query[i][0]); }
Refaktorovaný kód
long hashMap(String[] queryType, int[][] query) { long sum = 0; int currKey = 0; int currValue = 0; Map<Integer, Integer> values = new HashMap<>(); for (int i = 0; i < queryType.length; i++) { String currQuery = queryType[i]; if (isInsert(currQuery)) { if (didWeCopiedValuesToMap(currKey, currValue, values)) { currValue = 0; currKey = 0; } values.put(query[i][0], query[i][1]); } else if (isAddToValue(currQuery)) { currValue += values.isEmpty() ? 0 : query[i][0]; } else if (isAddToKey(currQuery)) { currKey += values.isEmpty() ? 0 : query[i][0]; } else if (isGet(currQuery)) { if (didWeCopiedValuesToMap(currKey, currValue, values)) { currValue = 0; currKey = 0; } sum += values.get(query[i][0]); } } return sum; } private boolean isGet(String currQuery) { return "get".equals(currQuery); } private boolean isAddToKey(String currQuery) { return "addToKey".equals(currQuery); } private boolean isAddToValue(String currQuery) { return "addToValue".equals(currQuery); } private boolean isInsert(String currQuery) { return "insert".equals(currQuery); } private boolean didWeCopiedValuesToMap(int currKey, int currValue, Map<Integer, Integer> values) { HashMap<Integer, Integer> copiedValues = new HashMap<>(); if (currKey != 0 || currValue != 0) { Set<Integer> keys = values.keySet(); for (Integer key : keys) { copiedValues.put(key + currKey, values.get(key) + currValue); } values.clear(); values.putAll(copiedValues); return true; } return false; }
odpověď
nejvíce nákladnou operací je addToKey x
, který přidává x ke všem klíčům v mapě, protože v podstatě musíte ve svém hashmap
a odstraňte starý vstupní klíč, hodnotu. Abyste se vyhnuli nutnosti ukládat do mezipaměti starou položku při iteraci mapy, můžete rozlišit dva případy:
x > 0, pokud tedy iterujete přes keyset
seřazeno sestupně není třeba ukládat do mezipaměti staré záznamy
x < 0, stejný přístup, ale keyset
je seřazeno vzestupně
Protože používáte hashmap
, není zaručeno žádné pořadí klíčů, takže potřebujete data struktura pro uložení objednaných klíčů před iterací nad klíči, jak je uvedeno níže:
private static void addtoKey(Map<Integer, Integer> map, int i) { if (i != 0) { List<Integer> list = new ArrayList<>(map.keySet()); if (i > 0) { Collections.sort(list, Collections.reverseOrder()); } else { Collections.sort(list); } for(int key : list) { map.put(key + i, map.get(key)); map.remove(key); } } }
Vyloučil jsem případ 0
protože map
zůstává nedotčen. Jiné operace nepotřebují pořadí klíčů a jak již bylo navrženo, mohlo by být lepší pokusit se izolovat každou operaci soukromou metodou.
Komentáře
- Děkuji @dariosicily za odpověď. Není ‚ t třídění pokaždé, když provádíme
addToKey
operace také nákladné? Nebo Mohu použít aSortedMap
k udržení sestupné objednávky. Stejně jakoSortedMap<Integer, Integer>values = new TreeMap<Integer, Integer>(Collections.reverseOrder());
- @Praveen jste vítáni. Ano je třídění pokaždé, ale s
ArrayList
po seřazení postupujete lineárně.Byl jsem usvědčen, že můžete použít pouzeHashMap
; pokud můžete použítTreeMap
místoHashMap
, můžete použít iterátor a reverzní iterátor a iterovat přesTreeMap
přímým způsobem.
Odpověď
Upravená verze Johnbotova odpověď bez extra třídy. Myslím, že ta extra třída je přehnaná a spíše odvádí pozornost od algoritmu, protože musím prohledávat hodně kódu (hodně z nich), abyste zjistili, co se děje. Není to taková extra třída, díky níž je smyčka zpracování mnohem jednodušší. Je to algoritmus.
Další změny:
-
keyOffset
mi není jasné, kterým směrem je to odsazeno, tak jsem to přejmenoval naaddedToKey
(podobně pro hodnotu). - Objednal operaci názvy jako ve specifikaci problému, a to jednak proto, aby zůstaly blízké specifikaci, jednak proto, že mi toto pořadí dává větší smysl.
- Zavedeno
args
pro uložení opakování kódu. - Použité
long
/Long
pro všechno, nejen pro součet. Koneckonců, přidání do klíčů / hodnot by mohlo způsobit jejich přetečení, pokud použijemeint
/Integer
.
static long hashMap(String[] queryType, int[][] query) { Map<Long, Long> map = new HashMap<>(); long sum = 0, addedToKey = 0, addedToValue = 0; for (int i = 0; i < query.length; i++) { int[] args = query[i]; switch (queryType[i]) { case "insert": map.put(args[0] - addedToKey, args[1] - addedToValue); break; case "get": sum += map.get(args[0] - addedToKey) + addedToValue; break; case "addToKey": addedToKey += args[0]; break; case "addToValue": addedToValue += args[0]; } } return sum; }
Komentáře
- Proč jednotně přidává
addedToKey
do klíč hodnoty ‚ nefunguje, ale odečte jej pro akceinsert
aget
práce?
Odpověď
Co takhle jen uložit hodnotu offsetu pro klíče a hodnoty a vytvořit obálkové metody kolem metody hashmaps get / put pro zohlednění tohoto posunu.
Map
pokaždé, když vytvoříteinsert
neboget
dotazy, pokud mi můžete vysvětlit, proč si toho vážím.