Tive o problema abaixo em um teste de codificação e obtive 28/30 aprovações e 2 reprovações devido para um tempo limite.
Problema
Você criou uma linguagem de programação e agora decidiu adicionarhashmap
suporte a ela. Foi descoberto que em linguagens de programação comuns, é impossível adicionar um número a todas ashashmap
chaves / valores. Portanto, você decidiu implementar seu própriohashmap
em seu novo idioma com as seguintes operações.
insert x y
– inserir e objeto com chavex
e valory
get x
– retorna o valor de um objeto com a chavex
addToKey x
– adicionex
para todas as chaves no mapaaddToValue y
– adicioney
para todos os valores no mapaSua tarefa é implementar isso
hashmap
, aplicar as consultas fornecidas e encontrar o soma de todos os resultados deget
operações
Por exemplo
- Para
queryType=["insert","insert","addToValue","addToKey","get"]
equery=[[1,2],[2,3],[2],[1],[3]]
, a saída deve ser .
Explicação
-
insert 1 2
– o hashmap será{1:2}
-
insert 2 3
– o hashmap será{1:2,2:3}
-
addToValue 2
– o hashmap será{1:4,2:5}
-
addToKey 1
– o hashmap será{2:4,3:5}
-
get 3
– o valor é 5
Input / Saída
- [limite de tempo de execução] 3 segundos (Java)
- [input] array.string queryType
Matriz de tipos de consulta. é garantido que cadaqueryType[i]
qualquer uma das operações mencionadas acima
1 < = queryType.length < = 10 ^ 5- [input] array.array.integer query
Matriz de consultas, em que cada consulta é mencionada por 2 números para inserção e um número para outros Os valores-chave estão no intervalo [-10 ^ 9,10 ^ 9]
Abaixo está minha solução em 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; }
Existe alguma outra estrutura de dados que eu possa usar em vez de hashmap
ou Posso melhorar meu código para ser mais linear?
Comentários
Resposta
Eu sugiro que você crie seu próprio OffsetIntegerMap
que pode mapear entre inteiros e lidar com um deslocamento nas chaves e valores.
Você não precisa necessariamente implementar HashMap
do zero, defina sua própria interface limitada e implemente-a com um Map<Integer, Integer>
existente por meio de composição.
Ao lidar com os deslocamentos separadamente das chaves e valores, a complexidade das operações de deslocamento termina em O (1) em vez de O (n) ao fazer recálculos e Map<>
as operações colocar e obter permanecem em seu O (1) original.
Um exemplo de ” 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; } }
Encapsulando a lógica de deslocamento, o loop de processamento também se torna muito mais simples sem refatorar muito de nada 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; }
Comentários
- Obrigado. Parece uma implementação melhor. Vou verificar essa lógica.
- Acho esta resposta no topo. Usar uma classe separada do teste de todos os casos.
keyWithoutOffset
evalueWithoutOffset
(acho que vi um bug no código original w.r..t. para isso). Os nomes claros (deslocamento). Apenas os nomes dos métodos são centrados no mapa ao invés daqueles nos requisitos - Você pode usar o exemplo da questão. Basta substituir
[]
por{}
.queryType
éString[]
equery
éint[][]
. - Ah, esqueci isso. E eu ‘ estou muito estragado por codificar sites de desafio apenas me dando uma ” Executar ” botão :-). Modifiquei esta solução em minha própria resposta agora.
- O deslocamento não será o mesmo para todas as chaves no hashmap! – começar com o conjunto de chaves (1,2,3) – adicionar 10 a todas as chaves, agora o conjunto de chaves é (10,11,12) – inserir a nova chave (5), agora o conjunto de chaves é (10,11,12,5) – adicionar 10 para todas as chaves, agora o conjunto de chaves é (20,21,22,15). Portanto, as 3 primeiras chaves efetivamente tiveram o deslocamento 20 adicionado a elas, mas a última chave teve apenas o deslocamento 10 (ou seja, as adições de chave feitas antes da inserção desta chave (5) serão ignoradas).
Resposta
Tenho algumas sugestões para você.
Extraia um pouco da lógica dos métodos.
Em seu código, quando a consulta é insert
e get
, você tem dois grandes blocos de código que são semelhantes; você pode extrair para um método e reutilizar o método em ambas as seções.
Eu sugiro um método que retorne um booleano baseado na condição if
, então você será capaz de definir as variáveis currValue
e currKey
como zero.
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; }
Além disso, para verificar a consulta atual currQuery
, você pode extrair cada um deles em um método.
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); }
Sempre use os primitivos quando possível
Quando você saiba que é impossível obter um valor nulo com o número, tente usar os primitivos; eles ocupam menos memória e são mais rápidos do que a classe wrapper.
Antes
Integer currKey = 0; Integer currValue = 0;
Depois
int currKey = 0; int currValue = 0;
Tente colocar menos código em switch
blocos
Em minha opinião, o código se torna menos legível quando há mais de 3 linhas de códigos em um bloco de switch; Eu sugiro que você o converta em um is-else-if
. Essa conversão tornará o código mais curto e legível.
Antes
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]); }
Após
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]); }
Código refatorado
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; }
Resposta
O máximo operação cara é a addToKey x
que adiciona x a todas as chaves no mapa, porque substancialmente você tem que criar uma nova chave de entrada, valor + x em seu hashmap
e exclua a chave de entrada antiga, valor. Para evitar a necessidade de armazenar em cache a entrada antiga durante a iteração no mapa, você pode distinguir dois casos:
x > 0, então se você tiver iterado sobre um keyset
decrescente ordenado não há necessidade de armazenar em cache as entradas antigas
x < 0, mesma abordagem, mas o keyset
está em ordem crescente
Como você está usando hashmap
, não há ordem de chave garantida, então você precisa de dados estrutura para armazenar as chaves a serem ordenadas, antes de iterar sobre as chaves como a seguir:
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); } } }
Excluí o caso 0
porque map
permanece intocado. Outras operações não precisam da ordem das chaves e, conforme já sugerido, seria melhor tentar isolar todas as operações em um método privado.
Comentários
- Obrigado @dariosicily pela resposta. Isn ‘ t classificar todas as vezes ao fazer
addToKey
operação também é caro ?. Ou posso usar aSortedMap
para manter a ordem de inserção decrescente. Por exemplo,SortedMap<Integer, Integer>values = new TreeMap<Integer, Integer>(Collections.reverseOrder());
- @Praveen Você é bem-vindo. Sim, é classificando sempre, mas com
ArrayList
após a classificação, você procederá de forma linear.Fui condenado por você só poderia usarHashMap
; se você pode usarTreeMap
em vez deHashMap
, você pode usar um iterador e um iterador reverso e iterar sobre seuTreeMap
de maneira direta.
Resposta
Versão modificada de Resposta de Johnbot” sem uma classe extra. Eu acho que a classe extra é um exagero e me distrai do algoritmo, já que tenho que pesquisar muitos códigos (muito boilerplate) para ver o que está acontecendo. Não é essa classe extra que torna o loop de processamento muito mais simples. É o algoritmo.
Outras mudanças:
-
keyOffset
não está claro para mim em qual direção ele está deslocado, então renomei isso paraaddedToKey
(o mesmo para o valor). - Ordenou a operação nomes como na especificação do problema, tanto para ficar perto da especificação quanto porque essa ordem faz mais sentido para mim.
- Introduzido
args
para economizar algumas repetições de código. - Usado
long
/Long
para tudo, não apenas para a soma. Afinal, adicionar às chaves / valores pode sobrecarregá-los se apenas usarmosint
/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; }
Comentários
- Por que adicionar uniformemente
addedToKey
ao a chave do valor ‘ s não funciona, mas subtraí-la para as açõesinsert
eget
sim funciona?
Resposta
Que tal apenas armazenar um valor de deslocamento para chaves e valores e construir métodos de wrapper em torno do métodos get / put hashmaps para compensar esse deslocamento.
Map
toda vez que você fazinsert
ouget
consultas, se você puder me explicar por que eu aprecio isso.