Ich hatte das folgende Problem in einem Codierungstest und bekam 28/30 Testdurchläufe und 2 fehlgeschlagen zu einer Zeitüberschreitung.
Problem
Sie haben eine Programmiersprache erstellt und nun beschlossen,hashmap
Unterstützung hinzuzufügen. Es wurde festgestellt, dass es in gängigen Programmiersprachen unmöglich ist, allenhashmap
Schlüsseln / Werten eine Zahl hinzuzufügen. Sie haben sich daher entschlossen, Ihre eigenehashmap
in Ihrer neuen Sprache mit den folgenden Operationen zu implementieren.
insert x y
– Einfügen und Objektieren mit Schlüsselx
und Werty
get x
– gibt den Wert eines Objekts mit dem Schlüsselx
addToKey x
zurück – addiertx
zu allen Schlüsseln in der KarteaddToValue y
– fügey
zu allen Werten in der KarteIhre Aufgabe ist es, diese
hashmap
zu implementieren, die angegebenen Abfragen anzuwenden und die zu finden Summe aller Ergebnisse fürget
Operationen
Zum Beispiel
- Für
queryType=["insert","insert","addToValue","addToKey","get"]
undquery=[[1,2],[2,3],[2],[1],[3]]
sollte die Ausgabe .
Erläuterung
-
insert 1 2
– Die Hashmap lautet{1:2}
-
insert 2 3
– Hashmap ist{1:2,2:3}
-
addToValue 2
– Hashmap ist{1:4,2:5}
-
addToKey 1
– Die Hashmap lautet{2:4,3:5}
-
get 3
– Wert ist 5
Eingabe / Ausgabe
- [Ausführungszeitlimit] 3 Sekunden (Java)
- [Eingabe] array.string queryType
Array von Abfragetypen. Es ist garantiert, dass jedequeryType[i]
eine der oben genannten Operationen
1 < = queryType.length < = 10 ^ 5- [Eingabe] array.array.integer-Abfrage
Array von Abfragen, wobei jede Abfrage durch 2 Zahlen zum Einfügen und eine Zahl für andere erwähnt wird. Die Schlüsselwerte liegen im Bereich [-10 ^ 9,10 ^ 9]
Nachfolgend ist meine Lösung in 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; }
Gibt es eine andere Datenstruktur, die ich anstelle von hashmap
verwenden kann, oder kann ich mich verbessern? Mein Code soll linearer sein?
Kommentare
- Willkommen bei Code Review. Ich verstehe ‚ nicht, warum Sie jedes Mal, wenn Sie
insert
oder erstellen, eine neueMap
erstellenget
fragt, ob Sie mir erklären können, warum ich es schätze. - @dariosicily, weil ich nicht ‚ bin Sie möchten den vorhandenen Wert nicht überschreiben, während Sie einen Schlüssel oder eine Karte aktualisieren. Beispiel: Wenn Sie für {2: 3,3: 1} Schlüssel 1 und Wert 1 hinzufügen möchten, wird es in der ersten Iteration zu {3: 4}. Hier werde ich das tatsächliche 3: 1 verlieren, das das nächste Schlüsselwertpaar ist. Kurz gesagt, um ein Überschreiben / eine Kollision von Schlüsselwertpaaren zu vermeiden.
- Danke, jetzt habe ich es.
Antwort
Ich würde vorschlagen, dass Sie Ihre eigene OffsetIntegerMap
erstellen, die zwischen ganzen Zahlen abbilden und einen Versatz für die Schlüssel und Werte verarbeiten kann.
Sie Sie müssen die HashMap
nicht unbedingt von Grund auf neu implementieren, Ihre eigene eingeschränkte Schnittstelle definieren und sie mit einer vorhandenen Map<Integer, Integer>
durch Komposition implementieren.
Durch die getrennte Behandlung der Offsets von den Schlüsseln und Werten wird die Komplexität der Offset-Operationen bei Neuberechnungen zu O (1) anstelle von O (n) und Map<>
Put- und Get-Operationen bleiben auf ihrem ursprünglichen O (1).
Ein Beispiel für ein “ 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; } }
Durch die Kapselung der Offset-Logik wird die Verarbeitungsschleife auch viel einfacher, ohne viel von irgendetwas umzugestalten 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; }
Kommentare
- Vielen Dank. Sieht nach einer besseren Implementierung aus. Ich werde diese Logik überprüfen.
- Ich finde diese Antwort oben. Verwenden einer Klasse, die vom Testen aller Fälle getrennt ist.
keyWithoutOffset
undvalueWithoutOffset
(Ich glaube, ich habe einen Fehler im Originalcode w.r..t. dazu gesehen). Die eindeutigen Namen (Offset). Nur die Methodennamen sind Map-zentriert anstelle der in den Anforderungen angegebenen. - Sie können das Beispiel aus der Frage verwenden. Ersetzen Sie einfach
[]
durch{}
.queryType
istString[]
undquery
istint[][]
. - Ah, das habe ich übersehen. Und ich ‚ bin zu verwöhnt, indem ich Challenge-Sites codiere, die mir nur einen “ Run “ geben Taste :-). Diese Lösung wurde jetzt in meine eigene Antwort geändert.
- Der Versatz ist nicht für jeden Schlüssel in der Hashmap gleich! – Beginnen Sie mit dem Keyset (1,2,3) – fügen Sie 10 zu allen Schlüsseln hinzu, jetzt ist das Keyset (10,11,12) – fügen Sie den neuen Schlüssel (5) ein, jetzt ist das Keyset (10,11,12,5) – fügen Sie hinzu 10 für alle Tasten, jetzt ist der Keyset (20,21,22,15). Den ersten 3 Schlüsseln wurde also effektiv ein Versatz von 20 hinzugefügt, aber der letzte Schlüssel hatte nur einen Versatz von 10 (dh das Hinzufügen von Schlüsseln, die vor dem Einfügen dieses Schlüssels (5) vorgenommen wurden, wird ignoriert).
Antwort
Ich habe einige Vorschläge für Sie.
Extrahieren Sie einen Teil der Logik in Methoden.
In Ihrem Code: Wenn die Abfrage insert
und get
lautet, haben Sie zwei große Codeblöcke, die ähnlich sind. Sie können in eine Methode extrahieren und die Methode in beiden Abschnitten wiederverwenden.
Ich schlage eine Methode vor, die einen Booleschen Wert basierend auf der Bedingung if
zurückgibt Die Variablen currValue
und currKey
können auf Null gesetzt werden.
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; }
Um die aktuelle Abfrage currQuery
zu überprüfen, können Sie jede dieser Abfragen in einer Methode extrahieren.
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); }
Verwenden Sie nach Möglichkeit immer die Grundelemente
Wenn Sie wissen, dass es unmöglich ist, mit der Zahl einen Nullwert zu erhalten, versuchen Sie, die Grundelemente zu verwenden; sie benötigen weniger Speicher und sind schneller als die Wrapper-Klasse.
Vor
Integer currKey = 0; Integer currValue = 0;
Nach
int currKey = 0; int currValue = 0;
Versuchen Sie, weniger Code in switch
-Blöcke
einzufügen Meiner Meinung nach wird der Code weniger lesbar, wenn ein Schaltblock mehr als 3 Codezeilen enthält. Ich schlage vor, dass Sie es in eine is-else-if
konvertieren. Durch diese Konvertierung wird der Code kürzer und lesbarer.
Vor
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]); }
Nach
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]); }
Refactored Code
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; }
Antwort
Am häufigsten Eine teure Operation ist die addToKey x
, die allen Schlüsseln in der Karte x hinzufügt, da Sie im Wesentlichen einen neuen Eingabeschlüssel mit dem Wert + x in Ihrer hashmap
und lösche den alten Eingabeschlüssel, Wert. Um zu vermeiden, dass der alte Eintrag beim Durchlaufen der Karte zwischengespeichert werden muss, können Sie zwei Fälle unterscheiden:
x > 0. Wenn Sie dann über a iterieren keyset
in absteigender Reihenfolge geordnet, müssen die alten Einträge nicht zwischengespeichert werden.
x < 0, der gleiche Ansatz, aber der keyset
ist aufsteigend geordnet
Da Sie hashmap
verwenden, ist keine Schlüsselreihenfolge garantiert, sodass Sie Daten benötigen Struktur zum Speichern der zu bestellenden Schlüssel, bevor die folgenden Schlüssel durchlaufen werden:
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); } } }
Ich habe den Fall 0
ausgeschlossen weil map
unberührt bleibt. Andere Operationen benötigen keine Reihenfolge der Schlüssel und wie bereits vorgeschlagen, könnte es besser sein, jede Operation in einer privaten Methode zu isolieren.
Kommentare
- Vielen Dank an @dariosicily für die Antwort. Ist ‚ nicht jedes Mal zu sortieren, während die
addToKey
-Operation ebenfalls kostspielig ist? Oder kann ich sie verwenden? aSortedMap
, um die Einfügereihenfolge absteigend zu halten.SortedMap<Integer, Integer>values = new TreeMap<Integer, Integer>(Collections.reverseOrder());
- @Praveen Gern geschehen. Ja, das ist es Jedes Mal sortieren, aber mit
ArrayList
nach dem Sortieren gehen Sie linear vor.Ich wurde verurteilt, dass Sie nurHashMap
verwenden können. Wenn SieTreeMap
anstelle vonHashMap
verwenden können, können Sie einen Iterator und einen umgekehrten Iterator verwenden und über Ihren auf einfache Weise.
Antwort
Geänderte Version von Johnbots Antwort ohne eine zusätzliche Klasse. Ich denke, die zusätzliche Klasse ist übertrieben und lenkt eher vom Algorithmus ab, da ich viel Code durchsuchen muss (viel davon Boilerplate) um zu sehen, was los ist. Es ist nicht diese zusätzliche Klasse, die die Verarbeitungsschleife viel einfacher macht. Es ist der Algorithmus.
Weitere Änderungen:
-
keyOffset
ist mir nicht klar, in welche Richtung es versetzt ist, deshalb habe ich das inaddedToKey
umbenannt (ebenfalls für Wert). - Die Operation wurde angeordnet Namen wie in der Problemspezifikation, sowohl um nahe an der Spezifikation zu bleiben als auch weil diese Reihenfolge für mich sinnvoller ist.
-
args
eingeführt, um einige Code-Wiederholungen zu sparen. - Verwendet
long
/Long
für alles, nicht nur für die Summe. Wenn Sie die Schlüssel / Werte hinzufügen, können sie schließlich überlaufen, wenn Sie nurint
/Integer
verwenden.
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; }
Kommentare
- Warum wird
addedToKey
einheitlich zum hinzugefügt Der Schlüssel von value ‚ funktioniert nicht, subtrahiert ihn jedoch für die Aktioneninsert
undget
Arbeit?
Antwort
Wie wäre es, wenn Sie nur einen Versatzwert für Schlüssel und Werte speichern und Wrapper-Methoden um das herum erstellen Hashmaps erhalten / setzen Methoden, um diesen Offset zu berücksichtigen.