コーディングテストで以下の問題が発生し、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]]の場合、出力は。

説明

  1. insert 1 2-ハッシュマップは{1:2}
  2. insert 2 3-ハッシュマップは{1:2,2:3}
  3. addToValue 2-ハッシュマップは
  4. 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の代わりに使用できる他のデータ構造はありますか、または改善できますかコードをより直線的にしますか?

コメント

  • コードレビューへようこそ。 ‘ insertを作成するたびに新しいMapを作成する理由がわかりません。 getクエリ、私が感謝する理由を説明していただければ。
  • @dariosicily、それは私が’キーまたはマップの更新中に既存の値を上書きしたい。例:{2:3,3:1}の場合、キー1と値1を追加する場合、最初の反復では{3:4}になります。ここでは、次のキーと値のペアである実際の3:1を失います。要するに、キーと値のペアの上書き/衝突を避けるためです。
  • ありがとう、今私はそれを手に入れました。

回答

整数間でマッピングし、キーと値のオフセットを処理できる独自の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。)。明確な名前(オフセット)。メソッド名だけが、要件のメソッド名ではなくマップ中心です
  • 質問の例を使用できます。 []{}に置き換えるだけです。 queryTypeString[]であり、queryint[][]
  • ああ、それを見落としていました。そして、私は’チャレンジサイトをコーディングすることで甘やかされすぎて、”実行”ボタン:-)。このソリューションを今すぐ自分の答えに変更しました。
  • オフセットは、ハッシュマップのすべてのキーで同じになるわけではありません。 -キーセット(1,2,3)で開始-すべてのキーに10を追加すると、キーセットは(10,11,12)になります-新しいキー(5)を挿入し、キーセットは(10,11,12,5)になります-追加すべてのキーに対して10、現在のキーセットは(20,21,22,15)です。したがって、最初の3つのキーには実質的にオフセット20が追加されましたが、最後のキーにはオフセットが10しかありませんでした(つまり、このキー(5)が挿入される前に行われたキーの追加は無視されます)。

回答

提案があります。

ロジックの一部をメソッドに抽出します。

コード、クエリがinsertgetの場合、類似した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メソッドを取得します。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です