디자인 패턴의 아름다움 67-반복자 패턴(2부): "스냅샷" 기능을 지원하는 반복자를 디자인하고 구현하는 방법은 무엇입니까?

67 | 반복자 모드(2부): "스냅샷" 기능을 지원하는 반복자를 설계하고 구현하는 방법은 무엇입니까?

지난 2개의 강의에서는 반복자 패턴의 원리와 구현에 대해 배웠고, 컬렉션을 순회하면서 컬렉션 요소를 추가하고 삭제하는 이유를 분석하여 예상치 못한 결과와 대처 전략을 도출했습니다.

오늘은 이 질문을 다시 살펴보겠습니다. "스냅샷" 기능을 지원하는 반복자를 구현하는 방법은 무엇입니까? 이 질문은 반복자 패턴에 대한 이해를 돕기 위한 이전 수업의 내용에 대한 확장된 사고라고 볼 수 있으며, 문제를 분석하고 해결하는 일종의 연습이기도 합니다. 면접질문이나 연습문제로 받아들일 수 있으니 제 설명을 읽기 전에 매끄럽게 대답할 수 있는지 확인해보세요.

더 이상 고민하지 않고 오늘의 공부를 공식적으로 시작합시다!

문제 설명

먼저 문제의 배경인 "스냅샷" 기능을 지원하는 반복자 모드를 구현하는 방법을 소개하겠습니다.

이 문제를 이해하는 열쇠는 "스냅샷"이라는 단어를 이해하는 것입니다. 소위 "스냅샷"은 컨테이너에 대한 반복자를 생성할 때 컨테이너의 스냅샷(스냅샷)을 찍는 것과 동일함을 의미합니다. 컨테이너에 요소를 추가하거나 삭제하더라도 스냅샷의 요소는 이에 따라 변경되지 않습니다. 반복자가 순회하는 객체는 컨테이너가 아닌 스냅샷으로, 반복자 순회 프로세스 중에 컨테이너에 요소를 추가하거나 삭제하여 발생하는 예기치 않은 결과나 오류를 방지합니다.

다음으로 위 단락을 설명하기 위해 예를 들어 보겠습니다. 구체적인 코드는 다음과 같습니다. 세 개의 요소 3, 8 및 2가 컨테이너 목록에 초기에 저장됩니다. 반복자 iter1이 생성된 후 컨테이너 목록은 요소 3을 삭제하고 두 개의 요소 8과 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라는 두 클래스를 포함하는 이 기능 요구 사항에 대한 스켈레톤 코드입니다. 이 두 클래스에 대해 몇 가지 필요한 핵심 인터페이스만 정의했으며 완전한 코드 구현을 제공하지 않았습니다. 당신은 그것을 완벽하게 시도한 다음 아래 내 설명을 읽을 수 있습니다.

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

두 번째 솔루션을 다시 살펴보겠습니다.

컨테이너의 각 요소에 대해 두 개의 타임스탬프를 저장할 수 있습니다. 하나는 타임스탬프 addTimestamp를 추가하는 것이고 다른 하나는 타임스탬프 delTimestamp를 삭제하는 것입니다. 요소가 컬렉션에 추가되면 addTimestamp를 현재 시간으로 설정하고 delTimestamp를 최대 긴 정수 값(Long.MAX_VALUE)으로 설정합니다. 요소가 삭제되면 delTimestamp를 현재 시간으로 업데이트하여 삭제되었음을 나타냅니다.

이는 컨테이너에서 실제로 삭제하는 것이 아니라 삭제를 표시하는 것입니다.

동시에 각 반복자는 반복자에 해당하는 스냅샷의 생성 타임스탬프인 반복자 생성 타임스탬프 snapshotTimestamp도 저장합니다. 반복자를 사용하여 컨테이너를 순회하는 경우 addTimestamp<snapshotTimestamp<delTimestamp를 만족하는 요소만 반복자에 속하는 스냅샷입니다.

요소의 addTimestamp>snapshotTimestamp인 경우 iterator가 생성된 후에 요소가 추가되어 iterator의 스냅샷에 속하지 않음을 의미하고, 요소의 delTimestamp<snapshotTimestamp인 경우 iterator가 생성되기 전에 요소가 삭제되어 더 이상 존재하지 않음을 의미합니다. 반복자에 속함 이 반복자의 스냅샷입니다.

이러한 방식으로 스냅샷 기능은 컨테이너를 복사하지 않고 타임스탬프의 도움으로 컨테이너 자체에서 실현됩니다. 구체적인 코드 구현은 다음과 같습니다. 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의 맨 아래 계층은 원래 빠른 임의 액세스를 지원할 수 있는 배열의 데이터 구조에 의존하고 O(1) 시간 복잡도 내에서 첨자 i로 요소를 얻을 수 있지만 이제 데이터 삭제는 실제 삭제가 아니라 표시만 됩니다. 타임스탬프 삭제로 인해 아래 첨자에 따른 빠른 랜덤 액세스 지원이 불가능합니다. 배열에 대한 임의 액세스에 대한 지식이 없는 경우 "The Beauty of Data Structures and Algorithms" 칼럼을 읽을 수 있으며 여기서는 설명하지 않겠습니다.

이제 이 문제를 해결하는 방법을 살펴보겠습니다. 컨테이너가 스냅샷 순회와 임의 액세스를 모두 지원하도록 하시겠습니까?

해결책은 어렵지 않으니 약간의 힌트를 드리겠습니다. ArrayList에 두 개의 배열을 저장할 수 있습니다. 마크 삭제를 지원하는 것은 스냅샷 순회 기능을 구현하는 데 사용되고, 마크 삭제를 지원하지 않는 것(즉, 삭제할 데이터가 어레이에서 직접 제거됨)은 임의 액세스를 지원하는 데 사용됩니다. 여기에 해당 코드를 제공하지 않겠습니다. 관심이 있으시면 직접 구현할 수 있습니다.

주요 검토

자, 오늘의 내용은 여기까지입니다. 집중해야 할 부분을 함께 요약하고 복습해 봅시다.

오늘 우리는 "스냅샷" 기능을 지원하는 반복자를 구현하는 방법에 대해 이야기했습니다. 실제로 이 문제 자체는 학습의 초점이 아닙니다. 실제 프로젝트 개발에서는 이러한 요구 사항을 거의 만나지 않기 때문입니다. 그래서 오늘의 내용을 바탕으로 너무 많은 요약을 하고 싶지 않습니다. 하고 싶은 말이 있는데 왜 오늘의 내용을 이야기해야 할까요?

사실 이 레슨의 내용을 익히기 위해 앞에서 뒤로 읽기만 하고 이해하면 괜찮다고 느끼면 이득이 거의 제로에 가깝습니다. 생각하는 질문이나 인터뷰 질문으로 받아들이는 것이 좋은 학습 방법입니다.내 설명을 읽기 전에 스스로 해결 방법을 생각하고 솔루션을 코드로 구현 한 다음 내 설명과 차이점을 살펴보십시오. 이 과정은 문제를 분석하고 해결하는 능력, 코드 설계 능력, 코딩 능력에 가장 중요한 연습이며 이것이 우리 기사의 의미입니다. 소위 "지식은 죽었고 능력은 산다"는 진리입니다.

사실 이 섹션의 내용뿐 아니라 전체 칼럼의 연구도 이와 같다.

"데이터 구조와 알고리즘의 아름다움"이라는 칼럼에서 동급생이 제 칼럼을 여러 번 읽고 거의 모든 내용을 이해했다고 말한 적이 있습니다. 면접관은 그에게 실제 개발과 결합된 알고리즘 질문을 주었지만 여전히 그때 머리가 멍하니 이 칼럼을 공부하고 알고리즘 인터뷰를 해보고 싶으면 뭘 배워야 하는지 물어보는데 추천하는 책은 없나요? .

그의 인터뷰 질문을 읽은 후 나는 내 칼럼의 지식이 완전히 풀릴 수 있다는 것을 알았고, 칼럼에서 유사한 문제가 언급되었지만 비즈니스 배경이 변경되었습니다. 그가 답을 할 수 없었던 이유는 여전히 지식을 문제 해결로 전환하는 능력이 없었기 때문입니다. 왜냐하면 그는 수동적으로 "볼" 뿐이고 결코 능동적으로 "생각"하지 않았기 때문입니다. 지식만 습득하고 능력을 발휘하지 못했고, 여전히 스스로 분석하고 생각하고 실용적인 문제를 해결할 수 없었습니다 .

그에게 내 제안은 칼럼의 모든 시작 질문을 인터뷰 질문으로 취급하고 스스로 생각한 다음 답변을 살펴보는 것입니다. 이런 식으로 칼럼 전체를 학습한 후에는 자신의 능력에 대해 더 많은 훈련을 받게 될 것이고, 알고리즘 인터뷰를 만났을 때 어떤 아이디어도 잃지 않을 것입니다. 같은 방식으로 "디자인 패턴의 아름다움" 칼럼을 공부할 때도 마찬가지입니다.

수업 토론

오늘 언급한 두 번째 솔루션에서 삭제된 요소는 삭제 표시만 됩니다. 반복자가 사용되지 않더라도 삭제된 요소는 배열에서 실제로 제거되지 않으므로 불필요한 메모리 사용이 발생합니다. 이 문제에 대해 추가 최적화 방법이 있습니까?

메시지를 남기고 나와 함께 생각을 공유하는 것을 환영합니다. 얻은 것이 있다면 이 기사를 친구들과 공유할 수 있습니다.

추천

출처blog.csdn.net/fegus/article/details/130519327