デザイン パターンの美しさ 67 - イテレーター パターン (その 2): 「スナップショット」機能をサポートするイテレーターをどのように設計して実装するか?

67 | イテレーターモード (パート 2): 「スナップショット」機能をサポートするイテレーターを設計および実装する方法は?

最後の 2 つのレッスンでは、反復子パターンの原則と実装を学び、コレクションを走査しながらコレクション要素を追加および削除する理由を分析し、予期しない結果と対処戦略をもたらしました。

今日は、この質問をもう一度見てみましょう: 「スナップショット」機能をサポートする反復子を実装するにはどうすればよいでしょうか? この質問は、反復子パターンの理解を深めるために、前のクラスの内容を拡張して考えることと見なすことができ、問題を分析して解決するための一種の演習でもあります。面接の質問でも練習問題でも構いませんので、私の説明を読む前に、スムーズに答えられるか試してみてください。

早速、今日から本格的に勉強を始めましょう!

問題の説明

まず問題の背景を紹介しましょう。「スナップショット」機能をサポートするイテレータ モードを実装するにはどうすればよいでしょうか。

この問題を理解する鍵は、「スナップショット」という言葉を理解することです。いわゆる「スナップショット」とは、コンテナーのイテレーターを作成するとき、コンテナーのスナップショット (スナップショット) を取得することと同等であることを意味します。コンテナー内の要素を追加または削除しても、スナップショット内の要素はそれに応じて変更されません。イテレーターがトラバースするオブジェクトは、コンテナーではなくスナップショットです。これにより、イテレーターのトラバーサル プロセス中にコンテナー内の要素を追加または削除することによって引き起こされる予期しない結果やエラーが回避されます。

次に、上記の段落を説明するために例を挙げましょう。具体的なコードは以下の通りです。3 つの要素 3、8、および 2 は、最初にコンテナー リストに格納されます。イテレータ iter1 が作成された後、コンテナ リストは要素 3 を削除し、要素 8 と 2 の 2 つだけを残しますが、iter1 によってトラバースされるオブジェクトはスナップショットであり、コンテナ リスト自体ではありません。したがって、走査の結果は 3、8、2 のままです。同様に、iter2 と iter3 もそれぞれのスナップショットでトラバースされ、出力結果がコード内のコメントに表示されます。

List<Integer> list = new ArrayList<>();
list.add(3);
list.add(8);
list.add(2);

Iterator<Integer> iter1 = list.iterator();//snapshot: 3, 8, 2
list.remove(new Integer(2));//list:3, 8
Iterator<Integer> iter2 = list.iterator();//snapshot: 3, 8
list.remove(new Integer(3));//list:8
Iterator<Integer> iter3 = list.iterator();//snapshot: 3

// 输出结果:3 8 2
while (iter1.hasNext()) {
  System.out.print(iter1.next() + " ");
}
System.out.println();

// 输出结果:3 8
while (iter2.hasNext()) {
  System.out.print(iter1.next() + " ");
}
System.out.println();

// 输出结果:8
while (iter3.hasNext()) {
  System.out.print(iter1.next() + " ");
}
System.out.println();

上記の機能を実装するとしたら、どのようにしますか? 以下は、この機能要件のスケルトン コードで、ArrayList と SnapshotArrayIterator の 2 つのクラスが含まれています。これら 2 つのクラスについては、必要な主要なインターフェイスをいくつか定義しただけで、完全なコード実装は提供しませんでした。あなたはそれを完璧にしようとすることができます, そして、以下の私の説明を読んでください.

public ArrayList<E> implements List<E> {
  // TODO: 成员变量、私有函数等随便你定义

  @Override
  public void add(E obj) {
    //TODO: 由你来完善
  }

  @Override
  public void remove(E obj) {
    // TODO: 由你来完善
  }

  @Override
  public Iterator<E> iterator() {
    return new SnapshotArrayIterator(this);
  }
}

public class SnapshotArrayIterator<E> implements Iterator<E> {
  // TODO: 成员变量、私有函数等随便你定义

  @Override
  public boolean hasNext() {
    // TODO: 由你来完善
  }

  @Override
  public E next() {//返回当前元素,并且游标后移一位
    // TODO: 由你来完善
  }
}

ソリューション 1

最初に最も簡単な解決策を見てみましょう。イテレータ クラスにメンバー変数のスナップショットを定義して、スナップショットを格納します。イテレーターが作成されるたびに、コンテナー内の要素のコピーがスナップショットにコピーされ、その後のトラバーサル操作は、イテレーター自体が保持するスナップショットに基づいて実行されます。具体的なコードの実装は次のとおりです。

public class SnapshotArrayIterator<E> implements Iterator<E> {
  private int cursor;
  private ArrayList<E> snapshot;

  public SnapshotArrayIterator(ArrayList<E> arrayList) {
    this.cursor = 0;
    this.snapshot = new ArrayList<>();
    this.snapshot.addAll(arrayList);
  }

  @Override
  public boolean hasNext() {
    return cursor < snapshot.size();
  }

  @Override
  public E next() {
    E currentItem = snapshot.get(cursor);
    cursor++;
    return currentItem;
  }
}

このソリューションはシンプルですが、少し高価です。イテレータが作成されるたびに、データのコピーをスナップショットにコピーする必要があるため、メモリ消費量が増加します。コンテナーに要素を同時にトラバースする複数の反復子がある場合、データの複数のコピーがメモリに格納されます。ただし、幸いなことに、Java でのコピーは浅いコピーです。つまり、コンテナー内のオブジェクトは実際には複数のコピーではなく、オブジェクトの参照のみがコピーされます。ディープコピーとシャローコピーについては講義47で詳しく説明していますので、もう一度戻って見てください。

コンテナーをコピーせずにスナップショットをサポートする方法はありますか?

ソリューション 2

2 番目のソリューションをもう一度見てみましょう。

コンテナ内の要素ごとに 2 つのタイムスタンプを保存できます。1 つはタイムスタンプ addTimestamp を追加するためのもので、もう 1 つはタイムスタンプ delTimestamp を削除するためのものです。要素がコレクションに追加されると、addTimestamp を現在の時刻に設定し、delTimestamp を最大の長整数値 (Long.MAX_VALUE) に設定します。要素が削除されると、delTimestamp を現在の時刻に更新し、要素が削除されたことを示します。

これは単に削除をマークしているだけであり、実際にコンテナーから削除するわけではないことに注意してください。

同時に、各反復子は、反復子に対応するスナップショットの作成タイムスタンプである、反復子作成タイムスタンプ snapshotTimestamp も保存します。イテレーターを使用してコンテナーをトラバースする場合、addTimestamp<snapshotTimestamp<delTimestamp を満たす要素のみがイテレーターに属するスナップショットになります。

要素の addTimestamp>snapshotTimestamp の場合、反復子の作成後に要素が追加され、反復子のスナップショットに属していないことを意味します。要素の delTimestamp<snapshotTimestamp の場合、要素が反復子の作成前に削除され、スナップショットに属していないことを意味します。所属するイテレータ このイテレータのスナップショット。

このように、スナップショット機能は、コンテナーをコピーすることなく、タイムスタンプの助けを借りてコンテナー自体で実現されます。具体的なコードの実装は次のとおりです。ArrayList の拡張については考慮していないことに注意してください. 興味がある場合は、自分で改善できます.

public class ArrayList<E> implements List<E> {
  private static final int DEFAULT_CAPACITY = 10;

  private int actualSize; //不包含标记删除元素
  private int totalSize; //包含标记删除元素

  private Object[] elements;
  private long[] addTimestamps;
  private long[] delTimestamps;

  public ArrayList() {
    this.elements = new Object[DEFAULT_CAPACITY];
    this.addTimestamps = new long[DEFAULT_CAPACITY];
    this.delTimestamps = new long[DEFAULT_CAPACITY];
    this.totalSize = 0;
    this.actualSize = 0;
  }

  @Override
  public void add(E obj) {
    elements[totalSize] = obj;
    addTimestamps[totalSize] = System.currentTimeMillis();
    delTimestamps[totalSize] = Long.MAX_VALUE;
    totalSize++;
    actualSize++;
  }

  @Override
  public void remove(E obj) {
    for (int i = 0; i < totalSize; ++i) {
      if (elements[i].equals(obj)) {
        delTimestamps[i] = System.currentTimeMillis();
        actualSize--;
      }
    }
  }

  public int actualSize() {
    return this.actualSize;
  }

  public int totalSize() {
    return this.totalSize;
  }

  public E get(int i) {
    if (i >= totalSize) {
      throw new IndexOutOfBoundsException();
    }
    return (E)elements[i];
  }

  public long getAddTimestamp(int i) {
    if (i >= totalSize) {
      throw new IndexOutOfBoundsException();
    }
    return addTimestamps[i];
  }

  public long getDelTimestamp(int i) {
    if (i >= totalSize) {
      throw new IndexOutOfBoundsException();
    }
    return delTimestamps[i];
  }
}

public class SnapshotArrayIterator<E> implements Iterator<E> {
  private long snapshotTimestamp;
  private int cursorInAll; // 在整个容器中的下标,而非快照中的下标
  private int leftCount; // 快照中还有几个元素未被遍历
  private ArrayList<E> arrayList;

  public SnapshotArrayIterator(ArrayList<E> arrayList) {
    this.snapshotTimestamp = System.currentTimeMillis();
    this.cursorInAll = 0;
    this.leftCount = arrayList.actualSize();;
    this.arrayList = arrayList;

    justNext(); // 先跳到这个迭代器快照的第一个元素
  }

  @Override
  public boolean hasNext() {
    return this.leftCount >= 0; // 注意是>=, 而非>
  }

  @Override
  public E next() {
    E currentItem = arrayList.get(cursorInAll);
    justNext();
    return currentItem;
  }

  private void justNext() {
    while (cursorInAll < arrayList.totalSize()) {
      long addTimestamp = arrayList.getAddTimestamp(cursorInAll);
      long delTimestamp = arrayList.getDelTimestamp(cursorInAll);
      if (snapshotTimestamp > addTimestamp && snapshotTimestamp < delTimestamp) {
        leftCount--;
        break;
      }
      cursorInAll++;
    }
  }
}

実際、上記の解決策は、ある問題を解決して別の問題を導入することと同じです。ArrayList の最下層は配列のデータ構造に依存しており、元々高速なランダム アクセスをサポートし、添字 i を持つ要素を O(1) 時間の複雑さで取得できますが、現在、データの削除は実際の削除ではなく、マークされているだけです。タイムスタンプによる削除。これにより、添字による高速ランダム アクセスをサポートできなくなります。配列へのランダム アクセスの知識がない場合は、私のコラム「データ構造とアルゴリズムの美しさ」を参照してください。ここでは説明しません。

では、この問題を解決する方法を見てみましょう: コンテナーがスナップショット トラバーサルとランダム アクセスの両方をサポートするようにしますか?

解決策は難しくありません。少しヒントを与えます。ArrayList には 2 つの配列を格納できます。スナップショットトラバーサル機能を実装するためにマーク削除をサポートするものと、ランダムアクセスをサポートするためにマーク削除をサポートしない (つまり、削除するデータがアレイから直接削除される) ものを使用します。対応するコードはここでは示しません。興味がある場合は、自分で実装できます。

キーレビュー

では、本日の内容は以上です。集中する必要があることをまとめて一緒に確認しましょう。

今日は、「スナップショット」機能をサポートする反復子を実装する方法について話しました。実際、この問題自体は学習の焦点では​​ありません。実際のプロジェクト開発では、そのような要件に遭遇することはほとんどないからです。ですから、今日の内容に基づいて、あまり多くの要約を作成したくありません。伝えたいことがあるのですが、なぜ今日の内容について話したいのですか?

実際、このレッスンの内容を理解するには、最初から最後まで読んで、理解できれば大丈夫だと感じれば、得られるものはほとんどゼロです。良い学習方法は、思考問題や面接の質問として理解することです.私の説明を読む前に、自分で解決する方法を考え、コードで解決策を実装してから、私の説明との違いを見てください. このプロセスは、問題を分析して解決する能力、コード設計能力、およびコーディング能力にとって最も価値のある演習であり、これが私たちの記事の意味です. いわゆる「知は死に、能力は生」は真実です。

実際、このセクションの内容だけでなく、コラム全体の考察もこのようなものです。

「データ構造とアルゴリズムの美しさ」というコラムで、クラスメートが私のコラムを何度も読んでほとんどの内容を理解したと言ったことがあります.その時、彼は頭が真っ白で、このコラムを勉強した後にアルゴリズムのインタビューに対処したい場合は何を学べばいいのか、何かお勧めの本はありますか? .

彼のインタビューの質問を読んだ後、私のコラムの知識は完全に解決できることがわかりました. また、同様の問題がコラムで言及されていますが、ビジネスの背景は異なります. 彼が答えられなかったのは、知識を問題解決に変換する能力がまだ彼にはなかったためです。彼は受動的に「見る」だけで、能動的に「考える」ことはありませんでした。知識を習得しただけで、能力を発揮できず、実際の問題を自分で分析し、考え、解決することができませんでした

彼への私の提案は、コラムの最初の質問をすべてインタビューの質問として扱い、自分で考えてから、答えを見ることです。このように、コラム全体を学習した後は、自分の能力についてより多くのトレーニングを受けることができ、アルゴリズムの面接に遭遇したときにアイデアを失うことはありません. 同じように、「デザインパターンの美しさ」というコラムを勉強することについても同じことが言えるはずです。

クラスディスカッション

今日言及された 2 番目の解決策では、削除された要素のみが削除対象としてマークされます。イテレータが使用されていない場合でも、削除された要素は実際には配列から削除されないため、不要なメモリ使用量が発生します。この問題に対して、さらに最適化する方法はありますか?

メッセージを残して、あなたの考えを私と共有してください。何かを得た場合は、この記事を友達と共有してください。

おすすめ

転載: blog.csdn.net/fegus/article/details/130519327