コーディングテストで以下の問題が発生し、28/30のテストに合格し、2つが失敗しました。タイムアウトになります。
問題
プログラミング言語を作成し、それにhashmap
サポートを追加することにしました。一般的なプログラミング言語では、すべてのhashmap
キー/値に数値を追加することは不可能であることがわかりました。そのため、次の操作を使用して、新しい言語で独自のhashmap
を実装することにしました。
insert x y
-キーx
と値y
get x
-キーx
addToKey x
を持つオブジェクトの値を返します-マップ内のすべてのキーにaddToValue y
-マップ内のすべての値にy
を追加しますタスクは、この
hashmap
を実装し、指定されたクエリを適用して、を見つけることです。get
操作のすべての結果の合計
例
-
queryType=["insert","insert","addToValue","addToKey","get"]
およびquery=[[1,2],[2,3],[2],[1],[3]]
の場合、出力は。
説明
-
insert 1 2
-ハッシュマップは{1:2}
-
insert 2 3
-ハッシュマップは{1:2,2:3}
-
addToValue 2
-ハッシュマップは -
addToKey 1
-ハッシュマップは{2:4,3:5}
-
-値は5です
入力/出力
- [実行時間制限] 3秒(Java)
- [input] array.string queryType
の配列クエリタイプ。queryType[i]
上記の操作のいずれか1つ
1 < = queryType.length < = 10 ^ 5- [input] array.array.integer query
クエリの配列。各クエリは挿入用に2つの数字で、その他の場合は1つの数字で示されます。キー値の範囲は[-10 ^ 9,10 ^ 9]
以下は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; }
hashmap
の代わりに使用できる他のデータ構造はありますか、または改善できますかコードをより直線的にしますか?
コメント
回答
整数間でマッピングし、キーと値のオフセットを処理できる独自のOffsetIntegerMap
を作成することをお勧めします。
あなた必ずしも最初からHashMap
を実装する必要はありません。独自の制限付きインターフェースを定義し、コンポジションを通じて既存のMap<Integer, Integer>
で実装します。
キーと値とは別にオフセットを処理することにより、オフセット操作の複雑さは、再計算とMap<>
putおよびget操作は元のO(1)のままです。
例” 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; } }
オフセットロジックをカプセル化することにより、処理ループも、ほとんどリファクタリングすることなく、はるかに単純になります。 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; }
コメント
- ありがとうございます。より良い実装のように見えます。このロジックを確認します。
- この回答が一番上にあります。すべてのケースのテストとは別のクラスを使用する。
keyWithoutOffset
およびvalueWithoutOffset
(元のコードにバグが見られたと思いますw.r..t。)。明確な名前(オフセット)。メソッド名だけが、要件のメソッド名ではなくマップ中心です - 質問の例を使用できます。
[]
を{}
に置き換えるだけです。queryType
はString[]
であり、query
はint[][]
。 - ああ、それを見落としていました。そして、私は’チャレンジサイトをコーディングすることで甘やかされすぎて、”実行”ボタン:-)。このソリューションを今すぐ自分の答えに変更しました。
- オフセットは、ハッシュマップのすべてのキーで同じになるわけではありません。 -キーセット(1,2,3)で開始-すべてのキーに10を追加すると、キーセットは(10,11,12)になります-新しいキー(5)を挿入し、キーセットは(10,11,12,5)になります-追加すべてのキーに対して10、現在のキーセットは(20,21,22,15)です。したがって、最初の3つのキーには実質的にオフセット20が追加されましたが、最後のキーにはオフセットが10しかありませんでした(つまり、このキー(5)が挿入される前に行われたキーの追加は無視されます)。
回答
提案があります。
ロジックの一部をメソッドに抽出します。
コード、クエリがinsert
とget
の場合、類似した2つの大きなコードブロックがあります。メソッドに抽出して、両方のセクションでメソッドを再利用できます。
if
条件に基づいてブール値を返すメソッドをお勧めします。これにより、次のようになります。 currValue
変数とcurrKey
変数をゼロに設定できます。
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; }
また、現在のクエリcurrQuery
を確認するには、メソッドでそれぞれを抽出できます。
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); }
可能な場合は常にプリミティブを使用する
数値でnull値を取得することは不可能であることを知っているので、プリミティブを使用してみてください。プリミティブはメモリを消費せず、ラッパークラスよりも高速です。
Before
Integer currKey = 0; Integer currValue = 0;
後
int currKey = 0; int currValue = 0;
switch
ブロックに入れるコードを減らしてください
私の意見では、スイッチブロックに3行を超えるコードがあると、コードが読みにくくなります。 is-else-if
に変換することをお勧めします。この変換により、コードが短くなり、読みやすくなります。
Before
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]); }
後
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]); }
リファクタリングされたコード
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; }
回答
最も多いコストのかかる操作は、マップ内のすべてのキーにxを追加するaddToKey x
です。これは、hashmap
そして古いエントリキーvalueを削除します。マップを反復処理するときに古いエントリをキャッシュする必要がないように、2つのケースを区別できます。
x > 0の場合、反復処理を行うとkeyset
降順で、古いエントリをキャッシュする必要はありません
x < 0、同じアプローチですが、keyset
は昇順で並べられます
hashmap
を使用しているため、キーの順序は保証されていないため、データが必要です以下のようにキーを反復処理する前に、順序付けするキーを格納する構造:
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); } } }
ケースを除外しました0
map
は変更されていないためです。他の操作ではキーの順序は必要ありません。すでに提案したように、すべての操作をプライベートメソッドで分離することをお勧めします。
コメント
- 答えてくれてありがとう@dariosicily。
addToKey
の操作を行う間、毎回ソートするのはコストもかかりますか?または使用できますか?SortedMap
は、挿入順序を降順に保ちます。たとえば、SortedMap<Integer, Integer>values = new TreeMap<Integer, Integer>(Collections.reverseOrder());
- @Praveenどういたしまして。はい。毎回並べ替えますが、並べ替え後に
ArrayList
を使用すると、直線的に進みます。HashMap
しか使用できないと有罪判決を受けました。HashMap
の代わりにTreeMap
を使用できる場合は、イテレータと逆イテレータを使用して、簡単に。
回答
ジョンボットの答え 追加のクラスなし。多くのコードを検索する必要があるため、追加のクラスはやり過ぎで、アルゴリズムの邪魔になると思います。 (その多くは定型文です)何が起こっているかを確認します。処理ループをはるかに単純にするのは、その余分なクラスではありません。それはアルゴリズムです。
さらなる変更:
-
keyOffset
がどの方向にオフセットされているかわからないため、名前をaddedToKey
に変更しました(値も同様)。 - 操作を注文しました。問題の仕様と同じように名前を付けます。仕様に近づけるためと、その順序が私にとってより理にかなっているためです。
- コードの繰り返しを節約するために
args
を導入しました。 - 合計だけでなくすべてに
long
/Long
を使用しました。結局のところ、キー/値に追加すると、int
/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; }
コメント
-
addedToKey
を均一に追加する理由value ‘キーは機能しませんが、insert
およびget
アクションではキーを減算します動作しますか?
回答
キーと値のオフセット値を格納し、ラッパーメソッドを構築するのはどうですか?ハッシュマップは、このオフセットを説明するためのget / putメソッドを取得します。
insert
を作成するたびに新しいMap
を作成する理由がわかりません。get
クエリ、私が感謝する理由を説明していただければ。