연결 목록은 어떤 상황에서 유용합니까?


110

대부분의 경우 사람들이 연결 목록을 사용하려고 시도하는 것은 나에게 좋지 않은 (또는 매우 가난한) 선택처럼 보입니다. 연결된 목록이 데이터 구조의 좋은 선택인지 아닌지에 대한 상황을 탐색하는 것이 유용 할 수 있습니다.

이상적으로는 데이터 구조를 선택하는 데 사용할 기준과 특정 상황에서 가장 잘 작동 할 수있는 데이터 구조에 대한 답변이 설명됩니다.

편집 : 나는 숫자뿐만 아니라 답변의 질에 매우 감동했습니다. 나는 하나만 받아 들일 수 있지만 조금 더 나은 것이 없었다면 받아 들일 가치가 있었을 것이라고 말해야 할 두세 가지가 더 있습니다. 단 한 쌍 (특히 내가 받아들이기로 한)만이 연결 목록이 실질적인 이점을 제공하는 상황을 지적했습니다. 나는 Steve Jessop이 하나가 아니라 세 가지 다른 답변을 내놓은 것에 대해 일종의 명예로운 언급을 할 가치가 있다고 생각합니다. 물론 답변이 아닌 댓글로만 게시 되었음에도 불구하고 Neil의 블로그 항목도 읽을만한 가치가 있다고 생각합니다. 유익 할뿐만 아니라 재미도 있습니다.


34
두 번째 단락에 대한 답은 한 학기 정도 걸립니다.
Seva Alekseyev

2
내 의견은 punchlet.wordpress.com/2009/12/27/letter-the-fourth를 참조하십시오 . 그리고 이것은 설문 조사 인 것처럼 보이므로 아마도 CW 일 것입니다.

1
@Neil, 멋지지만 CS Lewis가 승인 할 것 같지 않습니다.
Tom

@Neil : 설문 조사 같은데요. 대부분은 내가 적어도 합리적이라고 살 수있는 근거가있는 답변을 누군가가 생각 해낼 수 있는지 확인하려는 시도입니다. @Seva : 예, 다시 읽으면서 마지막 문장을 원래 의도했던 것보다 좀 더 일반적으로 만들었습니다.
Jerry Coffin

2
@Yar People (나를 포함해서 죄송합니다)은 트리처럼 FORTRAN IV (포인터 개념이 없음)와 같은 언어로 포인터없이 연결 목록을 구현하는 데 사용되었습니다. "실제"메모리 대신 배열을 사용했습니다.

답변:


40

동시 데이터 구조에 유용 할 수 있습니다. (이제 아래에 비 동시 실제 사용 샘플이 있습니다 . @Neil 이 FORTRAN을 언급하지 않았다면 존재하지 않을 것 입니다. ;-)

예를 들어 ConcurrentDictionary<TKey, TValue>.NET 4.0 RC에서는 연결 목록을 사용하여 해시 된 항목을 동일한 버킷에 연결합니다.

의 기본 데이터 구조 ConcurrentStack<T>도 링크 된 목록입니다.

ConcurrentStack<T>새로운 스레드 풀 의 기반 역할을하는 데이터 구조 중 하나입니다 (기본적으로 스택으로 구현 된 로컬 "큐"포함). (다른 주요지지 구조는 ConcurrentQueue<T>입니다.)

새로운 Thread Pool은 새로운 Task Parallel Library 의 작업 스케줄링을위한 기반을 제공합니다 .

따라서 그것들은 확실히 유용 할 수 있습니다. 연결 목록은 현재 적어도 하나의 훌륭한 신기술의 주요 지원 구조 중 하나입니다.

(단일 링크 된 목록은 주요 작업을 단일 CAS (+ 재시도) 로 수행 할 수 있기 때문에 이러한 경우에 잠금 없는 ( 잠금없는 것은 아님) 매력적인 선택이됩니다 . 최신 GC-d 환경에서) Java 및 .NET- ABA 문제 는 쉽게 피할 수 있습니다. 새로 생성 된 노드에 추가 한 항목을 랩핑하고 해당 노드를 재사용하지 마십시오. GC가 작업을 수행하도록합니다. ABA 문제에 대한 페이지는 잠금 구현도 제공합니다. 무료 스택-실제로 항목을 보유하는 (GC-ed) 노드가있는 .Net (& Java)에서 작동합니다.)

편집 : @Neil : 실제로 FORTRAN에 대해 언급 한 내용은 .NET에서 가장 많이 사용되고 남용되는 데이터 구조 인 일반 .NET generic에서 동일한 종류의 연결 목록을 찾을 수 있음을 상기시켜주었습니다 Dictionary<TKey, TValue>.

하나는 아니지만 많은 연결 목록이 배열에 저장됩니다.

  • 삽입 / 삭제시 많은 작은 할당 (해제)을 피합니다.
  • 배열이 순차적으로 채워지기 때문에 해시 테이블의 초기로드는 매우 빠릅니다 (CPU 캐시로 매우 잘 재생 됨).
  • 체이닝 해시 테이블은 메모리 측면에서 비싸다는 것은 말할 것도없고이 "트릭"은 x64에서 "포인터 크기"를 절반으로 줄입니다.

기본적으로 많은 연결 목록이 배열에 저장됩니다. (사용 된 각 버킷에 대해 하나씩) 재사용 가능한 노드의 무료 목록은 그들 사이에 "결합"됩니다 (삭제가있는 경우). 배열은 시작 / 재해시시 할당되고 체인 노드는 그 안에 보관됩니다. 삭제를 따르는 자유 포인터 (배열에 대한 인덱스 )도 있습니다 . ;-) 그래서-믿거 나 말거나-FORTRAN 기술은 여전히 ​​살아 있습니다. (... 그리고 가장 일반적으로 사용되는 .NET 데이터 구조 중 하나보다 다른 곳은 없습니다 ;-).


2
놓친 경우를 대비하여 Neil의 의견이 있습니다. "사람 (나를 포함하여 죄송합니다)은 트리처럼 FORTRAN IV (포인터 개념이 없음)와 같은 언어에서 포인터없이 연결 목록을 구현하는 데 사용되었습니다. . "실제"메모리 대신 배열을 사용했습니다. "
Andras Vass 2010 년

Dictionary.NET에서 훨씬 더 많이 저장하는 경우 "배열의 링크 된 목록"접근 방식을 추가해야합니다 . 그렇지 않으면 각 노드가 힙에 별도의 개체를 필요로하고 힙에 할당 된 모든 개체에 약간의 오버 헤드가 있습니다. ( en.csharp-online.net/Common_Type_System%E2%80%94Object_Layout )
Andras Vass 2010 년

std::list잠금이없는 다중 스레드 컨텍스트 에서는 C ++의 기본값 이 안전하지 않다는 것을 아는 것도 좋습니다 .
Mooing Duck 2013-08-26

49

링크 된 목록은 임의의 (컴파일시 알 수없는) 길이의 목록에서 많은 삽입 및 제거를 수행해야하지만 너무 많이 검색하지 않아야 할 때 매우 유용합니다.

목록 분할 및 결합 (양방향 연결)은 매우 효율적입니다.

또한 연결 목록을 결합 할 수도 있습니다. 예를 들어 트리 구조는 수평 연결 목록 (형제)을 함께 연결하는 "수직"연결 목록 (상위 / 하위 관계)으로 구현 될 수 있습니다.

이러한 목적으로 배열 기반 목록을 사용하면 심각한 제한이 있습니다.

  • 새 항목을 추가하면 어레이를 재 할당해야합니다 (또는 향후 확장을 허용하고 재 할당 횟수를 줄이는 데 필요한 것보다 더 많은 공간을 할당해야 함).
  • 항목을 제거하면 공간이 낭비되거나 재 할당이 필요합니다.
  • 끝을 제외한 모든 곳에 항목을 삽입하는 것은 많은 데이터를 한 위치 위로 복사 (재 할당 및)하는 것을 포함합니다.

5
문제가 감소 그래서, 때 순서에 의해 목록에 매우 많은 조회를하면 시퀀스의 중간에 삽입 및 제거를 많이 할 필요가 있지만? 연결 목록을 순회하는 것은 일반적으로 배열을 복사하는 것과 같거나 더 비싸기 때문에 배열에서 항목을 제거하고 삽입하는 것에 대해 말하는 모든 것은 목록의 임의 액세스만큼이나 나쁩니다. LRU 캐시는 제가 생각할 수있는 한 가지 예입니다. 중간에 많이 제거해야하지만 목록을 살펴볼 필요는 없습니다.
Steve Jessop

2
목록에 추가하려면 추가하는 모든 요소에 대한 메모리 할당이 포함됩니다. 이것은 매우 비용이 많이 드는 시스템 호출을 포함 할 수 있습니다. 어레이에 추가하려면 어레이를 확장해야하는 경우에만 이러한 호출이 필요합니다. 실제로 대부분의 언어 (정확히 이러한 이유로)에서 배열은 선호되는 데이터 구조이며 목록은 거의 사용되지 않습니다.

1
어느 쪽 이요? 그 할당은 놀랍도록 빠르다는 것이 분명합니다. 일반적으로 포인터에 개체 크기를 추가해야합니다. GC에 대한 총 오버 헤드가 낮습니까? 지난번에 실제 앱에서 측정을 시도했을 때 핵심은 프로세서가 어쨌든 유휴 상태 일 때 Java가 모든 작업을 수행하고 있었기 때문에 자연스럽게 보이는 성능에 큰 영향을 미치지 않았습니다. 사용량이 많은 CPU 벤치 마크에서 Java를 혼란스럽게하고 최악의 경우 할당 시간이 매우 나빠졌습니다. 그러나 이것은 수년 전의 일이며 세대 별 가비지 수집은 그 이후로 GC의 총 비용을 현저하게 줄였습니다.
Steve Jessop

1
@Steve : 목록과 배열간에 할당이 "동일"하다는 점에 대해 잘못 알고 있습니다. 목록에 메모리를 할당해야 할 때마다 작은 블록 (O (1)) 만 할당하면됩니다. 배열의 경우 전체 목록에 대해 충분히 큰 새 블록을 할당 한 다음 전체 목록 (O (n))을 복사해야합니다. 목록의 알려진 위치에 삽입하려면 고정 된 수의 포인터 (O (1))를 업데이트하지만 배열에 삽입하고 이후 항목을 한 위치 위로 복사하여 삽입 공간을 만들기 위해 O (n)합니다. 따라서 배열이 LL보다 훨씬 덜 효율적인 경우가 많이 있습니다.
Jason Williams

1
@Jerry : 이해합니다. 내 요점은 배열 재 할당 비용의 상당 부분이 메모리를 할당 하지 않고 전체 배열 내용 을 새 메모리 에 복사 해야한다는 것입니다 . 배열의 항목 0에 삽입하려면 전체 배열 내용 을 메모리의 한 위치 위로 복사 해야합니다 . 배열이 나쁘다는 말은 아닙니다. 랜덤 액세스가 필요하지 않은 상황이 있고 LL의 진정으로 일정한 시간 삽입 / 삭제 / 재 링크가 선호되는 상황이 있습니다.
Jason Williams

20

연결된 목록은 매우 유연합니다. 하나의 포인터를 수정하면 배열 목록에서 동일한 작업이 매우 비효율적 인 경우 대규모 변경을 수행 할 수 있습니다.


세트 나지도가 아닌 목록을 사용하는 이유에 대해 동기를 부여 할 수 있습니까?
patrik

14

배열은 연결된 목록이 일반적으로 비교되는 데이터 구조입니다.

일반적으로 연결된 목록은 목록 자체를 많이 수정해야 할 때 유용하지만 배열은 직접 요소 액세스의 목록보다 성능이 좋습니다.

다음은 상대 작업 비용 (n = 목록 / 배열 길이)과 비교하여 목록 및 배열에서 수행 할 수있는 작업 목록입니다.

  • 요소 추가 :
    • 목록에서는 새 요소와 리디렉션 포인터에 메모리를 할당하기 만하면됩니다. O (1)
    • 어레이에서는 어레이를 재배치해야합니다. 의 위에)
  • 요소 제거
    • 목록에서 포인터를 리디렉션합니다. O (1).
    • 배열에서 제거 할 요소가 배열의 첫 번째 또는 마지막 요소가 아닌 경우 배열을 재배치하는 데 O (n) 시간을 소비합니다. 그렇지 않으면 단순히 포인터를 배열의 시작으로 재배치하거나 배열 길이를 줄일 수 있습니다.
  • 알려진 위치에서 요소 가져 오기 :
    • 목록에서 첫 번째 요소에서 특정 위치의 요소로 목록을 걸어야합니다. 최악의 경우 : O (n)
    • 배열에서 요소에 즉시 액세스 할 수 있습니다. O (1)

이것은이 두 가지 인기 있고 기본적인 데이터 구조에 대한 매우 낮은 수준의 비교이며 목록 자체를 많이 수정해야하는 상황에서 목록이 더 잘 수행된다는 것을 알 수 있습니다 (요소 제거 또는 추가). 반면에 배열의 요소에 직접 액세스해야하는 경우 배열이 목록보다 성능이 좋습니다.

메모리 할당의 관점에서 볼 때 모든 요소를 ​​나란히 둘 필요가 없기 때문에 목록이 더 좋습니다. 반면에 다음 (또는 이전) 요소에 대한 포인터를 저장하는 데는 (약간) 오버 헤드가 있습니다.

이러한 차이점을 아는 것은 개발자가 구현에서 목록과 배열 중에서 선택하는 데 중요합니다.

이것은 목록과 배열의 비교입니다. 여기에보고 된 문제에 대한 좋은 해결책이 있습니다 (예 : SkipLists, Dynamic Arrays 등). 이 답변에서는 모든 프로그래머가 알아야 할 기본 데이터 구조를 고려했습니다.


이것은 목록의 좋은 구현과 끔찍한 배열 구현의 경우 다소 사실입니다. 대부분의 어레이 구현은 사용자가 인정하는 것보다 훨씬 더 정교합니다. 그리고 동적 메모리 할당이 얼마나 비싸다는 것을 이해하지 못한다고 생각합니다.

이 답변은 데이터 구조 대학 과정의 프로그램을 다루지 않습니다. 이것은 당신, 나 그리고 대부분의 사람들이 아는 방식으로 구현 된 연결된 목록과 배열을 고려하여 작성된 비교입니다. 기하학적으로 확장되는 배열, 건너 뛰기 목록 등은 내가 알고 사용하고 연구하는 솔루션이지만 더 깊은 설명이 필요하며 스택 오버플로 답변에 맞지 않습니다.
Andrea Zilio

1
"메모리 할당의 관점에서 보면 모든 요소를 ​​나란히 배치 할 필요가 없기 때문에 목록이 더 좋습니다." 반대로, 인접한 컨테이너는 요소를 나란히 유지 하기 때문에 더 좋습니다 . 현대 컴퓨터에서는 데이터 지역성이 가장 중요합니다. 메모리에서 뛰어 다니는 모든 것이 캐시 성능을 저하시키고 C ++ std::vector와 같은 연결 목록보다 C ++와 같은 동적 배열을 사용하여 더 빠르게 수행하는 (효과적으로) 임의의 위치에 요소를 삽입하는 프로그램으로 이어집니다 std::list. 목록이 너무 비싸요.
David Stone

@DavidStone 아마도 나는 충분히 명확하지 않았지만 그 문장으로 나는 요소를 저장하기 위해 연속적인 공간이 필요하지 않다는 사실을 언급했습니다. 특히 너무 작지 않은 것을 저장하고 사용 가능한 메모리가 제한되어있는 경우 데이터를 저장할 수 있는 연속 여유 공간 이 충분하지 않을 수 있지만 대신 목록을 사용하여 데이터를 맞출 수 있습니다 (포인터 오버 헤드가 있더라도 ... 그들이 차지하는 공간과 당신이 언급 한 성능 문제로 인해). 더 명확하게 답변을 업데이트해야합니다.
Andrea Zilio

4

단일 연결 목록은 셀 할당 자 또는 개체 풀의 사용 가능한 목록에 적합합니다.

  1. 스택 만 필요하므로 단일 연결 목록이면 충분합니다.
  2. 모든 것이 이미 노드로 나뉩니다. 셀이 포인터를 포함 할만큼 충분히 큰 경우 침입 목록 노드에 대한 할당 오버 헤드가 없습니다.
  3. 벡터 또는 데크는 블록 당 포인터 하나의 오버 헤드를 부과합니다. 이는 처음 힙을 만들 때 모든 셀이 무료이므로 선행 비용이 발생한다는 점을 고려할 때 중요합니다. 최악의 경우 셀당 메모리 요구 사항이 두 배가됩니다.

음, 동의했습니다. 하지만 실제로 그런 것을 만드는 프로그래머는 얼마나 될까요? 대부분은 단순히 std :: list 등이 제공하는 것을 다시 구현하고 있습니다. 그리고 실제로 "침투 적"은 일반적으로 주어진 것과는 약간 다른 의미를 갖습니다. 즉, 가능한 각 목록 요소에는 데이터와 분리 된 포인터가 포함되어 있습니다.

1
얼마나? 0 이상, 백만 미만 ;-) Jerry의 질문은 "목록의 좋은 사용을 제공하십시오"또는 "모든 프로그래머가 매일 사용하는 목록의 좋은 사용을 제공하십시오"또는 그 사이의 어떤 것입니까? 목록 요소 인 객체 내에 포함 된 목록 노드에 대해 "침투 적"외에는 다른 이름을 알지 못합니다. (C 용어로) 공용체의 일부인지 여부입니다. 요점 3은 C, C ++, 어셈블러에 적합한 언어에만 적용됩니다. 자바 불량.
Steve Jessop

4

이중 연결 목록은 요소 (Java의 LinkedHashMap)에 대한 순서도 정의하는 해시 맵의 순서를 정의하는 데 적합합니다.

  1. 관련 벡터 또는 데크보다 더 많은 메모리 오버 헤드 (1 대신 포인터 2 개), 삽입 / 제거 성능이 향상됩니다.
  2. 어쨌든 해시 항목을위한 노드가 필요하므로 할당 오버 헤드가 없습니다.
  3. 참조 위치는 벡터 또는 포인터 데크와 비교할 때 추가 문제가 아닙니다. 각 객체를 어느 쪽이든 메모리로 가져와야하기 때문입니다.

물론 LRU 캐시가 더 정교하고 조정 가능한 것에 비해 처음에는 좋은 아이디어인지에 대해 논쟁 할 수 있지만, 하나를 갖게된다면 꽤 괜찮은 구현입니다. 모든 읽기 액세스에서 벡터 또는 deque에서 중간에서 삭제 및 끝에서 추가를 수행하고 싶지는 않지만 일반적으로 노드를 꼬리로 이동하는 것이 좋습니다.


4

연결된 목록은 데이터가 저장되는 위치를 제어 할 수 없지만 어떻게 든 한 개체에서 다음 개체로 이동해야하는 경우 자연스러운 선택 중 하나입니다.

예를 들어 C ++에서 메모리 추적을 구현할 때 (새로 만들기 / 삭제 대체) 어떤 포인터가 해제되었는지 추적하는 제어 데이터 구조가 필요하며,이를 완전히 구현해야합니다. 대안은 각 데이터 청크의 시작 부분에 연결 목록을 할당하고 추가하는 것입니다.

delete가 호출 될 때 목록에서 자신이 어디에 있는지 항상 즉시 알기 때문에 O (1)의 메모리를 쉽게 포기할 수 있습니다. 또한 방금 malloc 된 새 청크를 추가하는 것은 O (1)에 있습니다. 이 경우 목록을 걷는 것은 거의 필요하지 않으므로 O (n) 비용은 여기서 문제가되지 않습니다 (구조물을 걷는 것은 O (n)입니다).


3

고속 푸시, 팝 및 회전이 필요할 때 유용하며 O (n) 인덱싱을 신경 쓰지 않습니다.


deque와 비교하여 C ++ 연결 목록에 시간을 낭비한 적이 있습니까?

@ 닐 : 내가 가지고 있다고 말할 수 없습니다.
Ignacio Vazquez-Abrams

@Neil : C ++가 다른 컨테이너 (진실에서 멀지 않은)보다 느리게 만들기 위해 의도적으로 연결된 목록 클래스를 방해했다면 언어에 구애받지 않는 질문과 무슨 관련이 있습니까? 침입 연결 목록은 여전히 ​​연결 목록입니다.
Steve Jessop

@Steve C ++는 언어입니다. 나는 그것이 어떻게 의지를 가질 수 있는지 볼 수 없습니다. C ++위원회의 구성원이 연결 목록 (많은 작업에서 논리적으로 느려 야 함)을 어떻게 든 파괴했다고 제안하는 경우, 죄인을 지명하십시오!

3
실제로 방해가되지는 않습니다. 외부 목록 노드에는 장점이 있지만 성능은 그중 하나가 아닙니다. 그러나 당신이 알고있는 것과 똑같은 것을 절충 할 때 모든 사람들은 확실히 알고 있었는데, 그것은 .NET을 잘 사용하기가 매우 어렵다는 것입니다 std::list. 침입 목록은 컨테이너 요소에 대한 최소 요구 사항이라는 C ++ 철학과 맞지 않습니다.
Steve Jessop

3

단일 연결 목록은 함수형 프로그래밍 언어에서 일반적인 "목록"데이터 유형의 명백한 구현입니다.

  1. 머리에 추가 빠르고이며, (append (list x) (L))그리고 (append (list y) (L))거의 모든 데이터를 공유 할 수 있습니다. 쓰기가없는 언어로 쓰기시 복사 할 필요가 없습니다. 기능적인 프로그래머는이를 활용하는 방법을 알고 있습니다.
  2. 꼬리에 추가하는 것은 안타깝게도 느리지 만 다른 구현도 마찬가지입니다.

이에 비해 벡터 또는 deque는 일반적으로 양쪽 끝에 추가하는 속도가 느리기 때문에 전체 목록 (벡터) 또는 인덱스 블록과 데이터 블록의 복사본을 가져와야합니다 (적어도 두 개의 개별 추가 예제에서는). (deque)에 추가됩니다. 실제로, 어떤 이유로 꼬리에 추가해야하는 큰 목록의 deque에 대해 할 말이있을 수 있습니다. 판단 할 함수 프로그래밍에 대해 충분히 알지 못합니다.


3

연결 목록의 좋은 사용 예는 목록 요소가 매우 큰 경우입니다. 한 두 개만 동시에 CPU 캐시에 들어갈 수있을만큼 충분히 큽니다. 이 시점에서 반복을위한 벡터 또는 배열과 같은 연속적인 블록 컨테이너가 갖는 이점은 다소 무효화되며, 많은 삽입 및 제거가 실시간으로 발생하는 경우 성능 이점이 가능할 수 있습니다.


2

내 경험상 희소 행렬과 피보나치 힙을 구현했습니다. 연결된 목록을 사용하면 이러한 데이터 구조의 전체 구조를 더 잘 제어 할 수 있습니다. 희소 행렬이 연결 목록을 사용하여 가장 잘 구현되는지 확실하지 않지만 더 나은 방법이있을 수 있지만 학부 CS에서 연결 목록을 사용하여 희소 행렬의 안팎을 배우는 데 실제로 도움이되었습니다.


1

목록에서 사소하게 O (1)이고 다른 데이터 구조에서 O (1)에서 구현하기 매우 어려운 두 가지 보완 작업이 있습니다. 요소의 순서를 유지해야한다고 가정하고 임의 위치에서 요소를 제거하고 삽입하는 것입니다.

해시 맵은 분명히 O (1)에서 삽입 및 삭제를 수행 할 수 있지만 요소를 순서대로 반복 할 수는 없습니다.

위의 사실을 감안할 때 해시 맵을 연결 목록과 결합하여 멋진 LRU 캐시를 만들 수 있습니다. 고정 된 수의 키-값 쌍을 저장하고 가장 최근에 액세스 한 키를 삭제하여 새 키를위한 공간을 만드는 맵입니다.

해시 맵의 항목에는 연결된 목록 노드에 대한 포인터가 있어야합니다. 해시 맵에 액세스 할 때 연결 목록 노드는 현재 위치에서 연결이 해제되고 목록의 선두로 이동합니다 (연결 목록의 경우 O (1), yay!). 최근에 가장 적게 사용 된 요소를 제거해야하는 경우 목록의 꼬리에서 하나를 삭제해야합니다 (꼬리 노드에 대한 포인터를 유지한다고 가정하면 다시 O (1)). 관련 해시 맵 항목 (따라서 백 링크는 해시 맵에 대한 목록이 필요합니다.)


1

연결 목록은 반복과 연동되는 부분을 포함하는 시스템의 도메인 기반 디자인 스타일 구현에서 매우 유용 할 수 있습니다.

떠오르는 예는 매달린 사슬을 모델링하는 경우 일 수 있습니다. 특정 링크의 장력이 무엇인지 알고 싶다면 인터페이스에 "명백한"가중치에 대한 게터가 포함될 수 있습니다. 구현에는 다음 링크에 겉보기 가중치를 요청한 다음 자체 가중치를 결과에 추가하는 링크가 포함됩니다. 이렇게하면 체인 클라이언트의 단일 호출로 하단까지 전체 길이가 평가됩니다.

자연어처럼 읽는 코드의 지지자이기 때문에 프로그래머가 체인 링크에 얼마나 많은 무게를 지니는지 물어볼 수있는 방법이 마음에 듭니다. 또한 링크 구현의 경계 내에서 이러한 속성 자식을 계산하는 문제를 유지하여 체인 가중치 계산 서비스의 필요성을 제거합니다. "


1

메시 및 이미지 처리, 물리 엔진 및 레이트 레이싱과 같은 성능이 중요한 분야에서 작동하는 연결 목록에 대해 내가 찾은 가장 유용한 사례 중 하나는 연결 목록을 사용하여 실제로 참조 위치를 개선하고 힙 할당을 줄이며 때로는 메모리 사용을 간단한 대안.

이제는 링크드리스트가 그 반대를 자주하는 것으로 악명이 높기 때문에 모든 것을 할 수 있다는 완전한 모순처럼 보일 수 있지만, 각리스트 노드는 우리가 허용 할 수있는 고정 된 크기와 정렬 요구 사항을 가지고 있다는 점에서 고유 한 속성을 가지고 있습니다. 그것들은 연속적으로 저장되고 가변 크기의 것은 불가능한 방식으로 일정한 시간에 제거됩니다.

결과적으로 백만 개의 중첩 된 가변 길이 하위 시퀀스를 포함하는 가변 길이 시퀀스를 저장하는 것과 유사한 것을 수행하려는 경우를 살펴 보겠습니다. 구체적인 예는 100 만 개의 다각형 (일부 삼각형, 일부 사각형, 일부 오각형, 일부 육각형 등)을 저장하는 색인화 된 메시이며, 때로는 메시의 어느 곳에서나 다각형이 제거되고 때로는 기존 다각형에 정점을 삽입하기 위해 다각형이 다시 작성되거나 하나를 제거하십시오. 이 경우 백만 개의 tiny를 저장하면 std::vectors모든 단일 벡터에 대한 힙 할당과 잠재적으로 폭발적인 메모리 사용에 직면하게됩니다. 백만 명의 작은 사람 SmallVectors은 일반적인 경우만큼이 문제를 겪지 않을 수 있지만 별도로 힙 할당되지 않은 사전 할당 된 버퍼는 여전히 폭발적인 메모리 사용을 유발할 수 있습니다.

여기서 문제는 백만 개의 std::vector인스턴스가 백만 개의 가변 길이 사물을 저장하려고한다는 것입니다. 가변 길이 사물은 힙의 다른 곳에 콘텐츠를 저장하지 않으면 연속적으로 저장되고 일정 시간 (적어도 매우 복잡한 할당자가없는 간단한 방식으로)에서 제거 될 수 없기 때문에 힙 할당을 원하는 경향이 있습니다.

대신 다음과 같이하면됩니다.

struct FaceVertex
{
    // Points to next vertex in polygon or -1
    // if we're at the end of the polygon.
    int next;
    ...
};

struct Polygon
{
     // Points to first vertex in polygon.
    int first_vertex;
    ...
};

struct Mesh
{
    // Stores all the face vertices for all polygons.
    std::vector<FaceVertex> fvs;

    // Stores all the polygons.
    std::vector<Polygon> polys;
};

... 그런 다음 힙 할당 및 캐시 누락 수를 크게 줄였습니다. 액세스하는 모든 단일 다각형에 대해 힙 할당 및 잠재적으로 강제적 인 캐시 미스를 요구하는 대신, 이제 전체 메시에 저장된 두 벡터 중 하나가 용량을 초과 할 때만 힙 할당이 필요합니다 (상각 된 비용). 한 정점에서 다음 정점으로 이동하는 걸음으로 인해 캐시 미스가 발생할 수 있지만 노드가 연속적으로 저장되고 인접한 정점이있을 가능성이 있기 때문에 모든 단일 다각형이 별도의 동적 배열을 저장하는 것보다 적은 경우가 많습니다. 제거하기 전에 액세스 할 수 있습니다 (특히 많은 다각형이 한 번에 모두 정점을 추가하므로 다각형 정점의 사자가 완벽하게 연속되도록합니다).

다음은 또 다른 예입니다.

여기에 이미지 설명 입력

... 그리드 셀을 사용하여 매 프레임마다 이동하는 1,600 만 개의 입자에 대해 입자-입자 충돌을 가속화합니다. 이 입자 그리드 예제에서 연결 목록을 사용하면 3 개의 인덱스 만 변경하여 한 그리드 셀에서 다른 그리드 셀로 입자를 이동할 수 있습니다. 벡터에서 지우고 다른 벡터로 푸시하는 것은 훨씬 더 비싸고 더 많은 힙 할당을 도입 할 수 있습니다. 연결 목록은 또한 셀의 메모리를 32 비트로 줄입니다. 구현에 따라 벡터는 빈 벡터에 대해 32 바이트를 취할 수있는 지점에 동적 배열을 미리 할당 할 수 있습니다. 약 백만 개의 그리드 셀이 있다면 그것은 상당한 차이입니다.

... 그리고 이것은 내가 요즘 가장 유용한 연결 목록을 찾는 곳이며, 특히 32 비트 인덱스가 64 비트 컴퓨터에서 링크의 메모리 요구 사항을 절반으로 줄이기 때문에 "인덱싱 된 연결 목록"다양성이 유용하다는 것을 알게되었습니다. 노드는 배열에 연속적으로 저장됩니다.

종종 나는 그것들을 색인 된 자유 목록과 결합하여 언제 어디서나 일정한 시간 제거 및 삽입을 허용합니다.

여기에 이미지 설명 입력

이 경우 next인덱스는 노드가 제거 된 경우 다음 사용 가능한 인덱스를 가리키고 노드가 제거되지 않은 경우 다음 사용 된 인덱스를 가리 킵니다.

그리고 이것이 제가 요즘 연결 목록에 대해 찾은 최고의 사용 사례입니다. 예를 들어 각각 4 개의 요소를 평균하는 백만 개의 가변 길이 하위 시퀀스를 저장하려고 할 때 (하지만 때로는 요소가 제거되고 이러한 하위 시퀀스 중 하나에 추가되는 경우도 있음) 연결 목록을 사용하면 4 백만 개를 저장할 수 있습니다. 각각 개별적으로 힙 할당되는 1 백만 개의 컨테이너 대신 연결된 목록 노드 : 하나의 거대 벡터, 즉 백만 개의 작은 벡터가 아닙니다.


0

나는 과거에 C / C ++ 애플리케이션에서 연결 목록 (이중 연결 목록 포함)을 사용했습니다. 이것은 .NET과 심지어 stl 이전이었습니다.

필요한 모든 순회 코드가 Linq 확장 메서드를 통해 제공되기 때문에 지금은 .NET 언어로 연결된 목록을 사용하지 않을 것입니다.

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