연결된 노드 구조를 변경할 수없는 실용적인 방법이 있습니까?


26

단독 연결 목록을 작성하기로 결정하고 내부 연결 노드 구조를 변경할 수 없도록 계획했습니다.

나는 걸림돌에 부딪쳤다. 이전 add작업 에서 다음과 같은 연결된 노드가 있다고 가정 해보십시오 .

1 -> 2 -> 3 -> 4

을 추가하고 싶다고 말하십시오 5.

이렇게하려면 노드 4가 변경 불가능하므로 새 복사본을 만들어야 4하지만 해당 next필드를를 포함하는 새 노드로 바꿔야 합니다 5. 문제는 지금, 3이전을 참조하는 것입니다 4. 추가되지 않은 것 5. 이제 copy를 복사 하고 사본 을 참조 3하기 위해 next필드를 교체 해야 4하지만 이제는 2이전을 참조합니다 3...

즉, 추가를 수행하려면 전체 목록을 복사해야합니다.

내 질문 :

  • 내 생각이 맞습니까? 전체 구조를 복사하지 않고 추가 할 수있는 방법이 있습니까?

  • 분명히 "유효한 Java"는 다음과 같은 권장 사항을 포함합니다.

    클래스를 변경 가능한 이유가없는 한 클래스는 변경할 수 없습니다 ...

    이것은 가변성에 대한 좋은 사례입니까?

나는 이것이 목록 자체에 대해 이야기하지 않기 때문에 이것이 제안 된 답변의 사본이라고 생각하지 않습니다. 인터페이스를 준수하기 위해 분명히 변경 가능해야합니다 (새로운 목록을 내부적으로 유지하고 게터를 통해 검색하지 않는 것). 두 번째 생각에도 약간의 돌연변이가 필요합니다. 최소한으로 유지됩니다. 목록의 내부를 변경할 수 없는지 여부에 대해 이야기하고 있습니다.


Java에서 드문 불변 콜렉션은 던지기 위해 재정의 된 메소드를 변경하거나 CopyOnWritexxx멀티 스레딩에 사용되는 엄청나게 비싼 클래스입니다. 컬렉션이 실제로 불변이 될 것으로 기대하는 사람은 아무도 없습니다 (
분명한 점이

9
(견적에 대한 주석.) 큰 인용문은 종종 크게 오해됩니다. 불변성은 그 이점 때문에 ValueObject에 적용되어야 하지만, Java로 개발하는 경우, 불변성이 예상되는 곳에 불변성을 사용하는 것은 비현실적입니다. 대부분의 클래스에는 변경 불가능한 특정 측면과 요구 사항에 따라 변경 가능해야하는 특정 측면이 있습니다.
rwong

2
관련 (아마도 중복) : 컬렉션 객체가 더 잘 변하지 않는가? "성능 오버 헤드없이이 콜렉션 (클래스 LinkedList)을 불변 콜렉션으로 도입 할 수 있습니까?"
gnat

왜 불변 링크리스트를 만들겠습니까? 연결된 목록의 가장 큰 장점은 가변성입니다. 배열을 사용하는 것이 더 나을까요?
Pieter B

3
귀하의 요구 사항을 이해하도록 도와 주실 수 있습니까? 변경하려는 불변 목록을 원하십니까? 모순이 아닙니까? 목록의 "내부"란 무엇을 의미합니까? 이전에 추가 한 노드는 나중에 수정할 수없고 목록의 마지막 노드에만 추가 할 수 있어야합니까?
null

답변:


46

기능적 언어로 된 목록을 사용하면 거의 항상 머리와 꼬리, 첫 번째 요소 및 나머지 목록으로 작업합니다. 추가 할 때 전체 목록 (또는 연결된 목록과 정확하게 일치하지 않는 다른 게으른 데이터 구조)을 복사해야하기 때문에 앞에 추가하는 것이 훨씬 일반적입니다.

명령형 언어에서는 추가적으로 의미가 더 자연스럽게 느껴지고 이전 버전의 목록에 대한 참조를 무효화하지 않아도되므로 추가가 훨씬 일반적입니다.

앞에 붙일 때 전체 목록을 복사하지 않아도되는 이유의 예로 다음과 같은 사항을 고려하십시오.

2 -> 3 -> 4

앞에 붙인 1다면 :

1 -> 2 -> 3 -> 4

그러나 2목록이 변경 불가능하고 링크가 한 방향으로 만 이동하기 때문에 다른 사람이 목록의 헤드로 참조를 보유하고 있는지 여부는 중요하지 않습니다 . 에 1대한 참조가있는 경우조차도 말할 수있는 방법이 없습니다 2. 이제 5목록 중 하나에 목록 을 추가 한 경우 전체 목록의 사본을 만들어야합니다. 그렇지 않으면 다른 목록에도 나타납니다.


아, 왜 선행에 사본이 필요하지 않은지에 대한 좋은 지적. 나는 그것을 생각하지 않았다.
Carcigenicate

17

올바로 노드를 변경하지 않으려면 추가시 전체 목록을 복사해야합니다. 우리 next는 (현재) 두 번째-마지막 노드의 next포인터 를 설정해야합니다.이 노드는 불변 설정에서 새 노드를 생성 한 다음 세 번째-마지막 노드 의 포인터 등 을 설정해야합니다 .

여기서 핵심 문제는 불변성이 아니며 append작업이 잘못되어 있다고 생각하지 않습니다 . 둘 다 각각의 영역에서 완벽하게 괜찮습니다. 그것들을 섞는 것은 나쁘다 : 불변 목록에 대한 자연적인 (효율적인) 인터페이스는 목록의 앞 부분에서 조작을 강조했지만, 변하기 쉬운 목록의 경우 처음부터 끝까지 항목을 연속적으로 추가하여 목록을 작성하는 것이 더 자연 스럽습니다.

그러므로 나는 당신이 당신의 마음을 구성 할 것을 제안합니다 : 당신은 임시 인터페이스 또는 지속적인 인터페이스를 원하십니까? 대부분의 작업은 새 목록을 생성하고 수정되지 않은 버전에 액세스 가능 (지속적)으로 두거나 다음과 같은 코드를 작성합니까 (일시적) :

list.append(x);
list.prepend(y);

두 가지 선택 모두 훌륭하지만 구현시 인터페이스를 반영해야합니다. 영구적 인 데이터 구조는 변경 불가능한 노드의 이점을 얻는 반면, 임시 데이터 구조는 암시 적으로 수행하는 성능 약속을 실제로 이행하기 위해 내부 변경이 필요합니다. java.util.List그리고 다른 인터페이스는 아주 드물다 : 불변 목록에 그것들을 구현하는 것은 부적절하고 실제로 성능 위험이있다. 변경 가능한 데이터 구조에 대한 좋은 알고리즘은 변경 불가능한 데이터 구조에 대한 좋은 알고리즘과는 상당히 다르므로 변경 불가능한 데이터 구조를 변경 가능한 데이터 구조 (또는 그 반대)로 드레싱하면 잘못된 알고리즘이 필요합니다.

퍼시 스턴트리스트에는 몇 가지 단점이 있지만 (효율적인 추가가 아님), 기능적으로 프로그래밍 할 때 심각한 문제가 될 필요는 없습니다. 사고 방식을 바꾸고 map또는 fold( 또는 두 개의 상대적으로 원시적 인 함수의 이름을 지정 하기 위해) ) 또는 반복적 으로 추가 하여 또한,이 데이터 구조 만 사용하도록 강요하는 사람은 없습니다. 또한 퍼시 스턴트리스트는 다른 워크로드에 몇 가지 장점이 있습니다. 테일은 꼬리를 공유하므로 메모리를 절약 할 수 있습니다.


4

단독으로 연결된 목록이 있으면 앞면과 뒷면보다 더 많은 경우 앞면과 함께 작업합니다.

프롤로그 및 하스켈과 같은 기능적 언어는 전면 요소와 나머지 배열을 얻는 쉬운 방법을 제공합니다. 뒷면에 추가하는 것은 각 노드를 복사하는 O (n) 작업입니다.


1
하스켈을 사용했습니다. Afaik은 게으름을 사용하여 문제의 일부를 우회합니다. 나는 그것이 List인터페이스에 의해 기대되는 것이라고 생각했기 때문에 추가를하고 있다 (나는 거기에 잘못 될 수있다). 나는 그 비트가 실제로 질문에 대답한다고 생각하지 않습니다. 전체 목록을 여전히 복사해야합니다. 전체 순회가 필요하지 않기 때문에 마지막 요소에 더 빨리 액세스 할 수 있습니다.
Carcigenicate

기본 데이터 구조 / 알고리즘과 일치하지 않는 인터페이스에 따라 클래스를 만드는 것은 고통과 비 효율성에 대한 초대입니다.
ratchet freak

JavaDocs는 "append"라는 단어를 명시 적으로 사용합니다. 당신은 구현을 위해 그것을 무시하는 것이 낫다는 것을 말하고 있습니까?
Carcigenicate

2
@Carcigenicate 아니오 불변의 노드가있는 단일 연결리스트를 틀에 맞추려고 시도하는 것은 실수라고 말하는 것입니다.java.util.List
ratchet freak

1
게으름으로 인해 더 이상 연결된 목록이 없습니다. 목록이 없으므로 돌연변이가 없습니다 (추가). 대신 요청시 다음 항목을 생성하는 코드가 있습니다. 그러나 목록이 사용되는 모든 곳에서 사용할 수는 없습니다. 즉, 인수로 전달 된 목록을 변경할 것으로 예상 되는 메소드를 Java로 작성하는 경우 먼저 해당 목록을 변경할 수 있어야합니다. 생성기 방식을 사용하려면 코드를 완전히 재구성하고 재구성하여 목록을 물리적으로 제거 할 수 있어야합니다.
rwong

4

다른 사람들이 지적했듯이 추가 작업을 수행 할 때 불변의 단일 링크 목록은 전체 목록을 복사해야한다는 것이 맞습니다.

cons(선행) 연산의 관점에서 알고리즘 구현의 해결 방법을 사용한 다음 최종 목록을 한 번 뒤집을 수 있습니다. 여전히 목록을 한 번 복사해야하지만 복잡성 오버 헤드는 목록의 길이에 비례하지만 추가를 반복해서 사용하면 2 차 복잡성을 쉽게 얻을 수 있습니다.

차이점 목록 (예 : 여기 참조 )은 흥미로운 대안입니다. 차이 목록은 목록을 감싸고 일정한 시간에 추가 작업을 제공합니다. 추가해야하는 한 기본적으로 랩퍼를 사용한 다음 완료되면 목록으로 다시 변환합니다. 이것은 a StringBuilder를 사용하여 문자열을 구성 할 때 수행하는 작업과 비슷 하며 결국 String호출 하여 (불변!) 결과를 얻습니다 toString. 한 가지 차이점은 a StringBuilder는 변경할 수 있지만 차이 목록은 변경할 수 없다는 것 입니다. 또한 차이 목록을 다시 목록으로 변환 할 때 여전히 전체 새 목록을 구성해야하지만 다시 한 번만 수행하면됩니다.

DListHaskell 's와 유사한 인터페이스를 제공 하는 불변 클래스 를 구현하는 것은 매우 쉬워야합니다 Data.DList.


4

Immutable.js 의 제작자 Lee Byron의 2015 React conf 에서이 훌륭한 비디오 를 시청해야합니다 . 내용을 복제 하지 않는 효율적인 불변 목록을 구현하는 방법을 이해하기위한 포인터와 구조를 제공합니다 . 기본 아이디어는 다음과 같습니다.-두 개의 목록이 동일한 노드 (동일한 값, 동일한 다음 노드)를 사용하고 동일한 노드가 사용되는 한-목록이 다를 때 분기 노드에 구조가 생성되어 각 목록의 다음 특정 노드

반응 튜토리얼 의이 이미지는 깨진 영어보다 명확 할 수 있습니다.여기에 이미지 설명을 입력하십시오


2

엄밀히 Java는 아니지만 Scala로 작성된 불변의 고성능 인덱스 데이터 구조에 대해이 기사를 읽는 것이 좋습니다.

http://www.codecommit.com/blog/scala/implementing-persistent-vectors-in-scala

스칼라 데이터 구조이기 때문에 Java에서도 사용할 수 있습니다 (약간 더 자세하게). Clojure에서 사용 가능한 데이터 구조를 기반으로하며 더 많은 "기본"Java 라이브러리도 제공합니다.

또한 불변의 데이터 구조 를 구성 하는 것에 대한 참고 사항 : 일반적으로 원하는 것은 일종의 "빌더"입니다. "빌더"는 요소를 단일 스레드 내에서 추가하여 "공사중"데이터 구조를 "돌연변이"할 수 있습니다. 추가를 마치면 .build()or 와 같은 "언더 구성"개체에 대한 메서드를 호출하여 개체 .result()를 "빌드"하여 불변의 데이터 구조를 제공하여 안전하게 공유 할 수 있습니다.


1
에서 PersistentVector의 자바 포트에 대한 질문이 있습니다 stackoverflow.com/questions/15997996/...
James_pic

1

때로는 도움이 될 수있는 접근 방식에는 두 개의 클래스 목록 유지 객체, 즉 순방향 final연결 목록의 첫 번째 노드를 참조 하는 순방향 목록 객체와 초기 null이 아닌 비 final참조 객체가있는 것입니다. -null)은 동일한 항목을 반대 순서로 보유하는 리버스리스트 오브젝트와 final리버스 링크 된리스트의 마지막 항목을 참조 하는 리버스리스트 오브젝트와 초기 널이 아닌 비 최종 참조 ( -null)은 동일한 항목을 반대 순서로 보유하는 정방향 목록 객체를 식별합니다.

항목을 정방향 목록에 추가하거나 항목을 역방향 목록에 추가하면 final참조로 식별 된 노드에 연결되는 새 노드를 만들고 원본과 동일한 유형의 새 목록 객체를 만들면됩니다.final 참조 그 새로운 노드에.

항목을 순방향 목록에 추가하거나 역방향 목록 앞에 추가하려면 반대 유형의 목록이 있어야합니다. 특정 목록으로 처음 수행되면 반대 유형의 새 객체를 작성하고 참조를 저장해야합니다. 작업을 반복하면 목록을 다시 사용해야합니다.

목록 객체의 외부 상태는 반대 유형 목록에 대한 참조가 null인지 또는 반대 순서 목록을 식별하는지에 관계없이 동일한 것으로 간주됩니다. final멀티 스레드 코드를 사용하는 경우에도 모든 목록 객체에는 변수가 있으므로 변수를 만들 필요가 없습니다 .final 내용의 전체 사본에 대한 참조를 .

한 스레드의 코드가 목록의 역 사본을 작성하고 캐시하고 해당 사본이 캐시되는 필드가 일시적이지 않은 경우 다른 스레드의 코드가 캐시 된 목록을 볼 수는 없지만 유일한 부작용은 발생할 수 있습니다 그로부터 다른 스레드는 목록의 다른 역 사본을 작성하는 추가 작업을 수행해야한다는 것입니다. 이러한 조치는 효율성을 최악 volatile으로 악화 시키지만 정확성에는 영향을 미치지 않으며 변수가 자체적으로 효율성 장애를 발생시키기 때문에 변수가 비 휘발성이고 가끔 중복되는 작업의 가능성을 수용하는 것이 좋습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.