어떤 종류의 알고리즘이 세트를 필요로합니까?


10

첫 번째 프로그래밍 과정에서 나는 무언가의 중복을 제거하는 것과 같은 일을해야 할 때마다 세트를 사용해야한다고 들었습니다. 예 : 벡터에서 모든 복제본을 제거하려면 해당 벡터를 반복하고 각 요소를 세트에 추가하면 고유 한 상황이 발생합니다. 그러나 각 요소를 다른 벡터에 추가하고 요소가 이미 존재하는지 확인하여이를 수행 할 수도 있습니다. 사용 된 언어에 따라 성능에 차이가있을 수 있다고 가정합니다. 그러나 그 이외의 세트를 사용해야하는 이유가 있습니까?

기본적으로 어떤 종류의 알고리즘에 집합이 필요하며 다른 컨테이너 유형으로 수행해서는 안됩니까?


2
"세트"라는 용어를 사용할 때의 의미에 대해 더 구체적으로 설명 할 수 있습니까? C ++ 세트
Robert Harvey

예, 실제로 "집합"정의는 대부분의 언어에서 고유 한 요소 만 허용하는 컨테이너와 매우 유사합니다.
Floella

6
"각 요소를 다른 벡터에 추가하고 요소가 이미 존재하는지 확인"-이것은 직접 세트를 구현하는 것입니다. 손으로 직접 작성할 수있는 이유는 무엇입니까?
JacquesB

답변:


8

당신은 구체적으로 세트를 요구하고 있지만 귀하의 질문은 더 큰 개념, 추상화에 관한 것입니다. Java를 사용하는 경우 ArrayList를 대신 사용하십시오. 그러나 왜 거기서 멈추어야합니까? Vector가 필요한 것은 무엇입니까? 이 모든 작업을 배열로 수행 할 수 있습니다.

배열에 항목을 추가해야 할 때마다 단순히 모든 요소를 ​​반복하고 요소가 없으면 끝에 추가합니다. 그러나 실제로 배열에 공간이 있는지 먼저 확인해야합니다. 더 큰 새 배열을 작성하지 않고 기존 배열의 모든 기존 요소를 새 배열로 복사 한 다음 새 요소를 추가 할 수 있습니다. 물론 새 배열을 가리 키도록 이전 배열에 대한 모든 참조를 업데이트해야합니다. 다 했어요? 큰! 이제 우리는 무엇을 다시 달성하려고 했습니까?

또는 Set 인스턴스를 사용하고을 호출 할 수 있습니다 add(). 세트가 존재하는 이유는 많은 일반적인 문제에 유용한 추상화이기 때문입니다. 예를 들어, 항목을 추적하고 새로운 항목이 추가 될 때 반응한다고 가정 해 봅시다. add()세트를 호출 하면 세트가 수정되었는지 여부에 따라 반환 true되거나 반환 false됩니다. 프리미티브를 사용하여 직접 손으로 쓸 수는 있지만 왜?

실제로 List가 있고 중복을 제거하려는 경우가 있습니다. 제안하는 알고리즘은 기본적으로 가장 느린 방법입니다. 버킷을 만들거나 정렬하는 일반적인 두 가지 빠른 방법이 있습니다. 또는 해당 알고리즘 중 하나를 구현하는 세트에 추가 할 수도 있습니다.

경력 / 교육 초기에 이러한 알고리즘을 구축하고 이해하는 데 중점을두고 있으며이를 수행하는 것이 중요합니다. 그러나 이것이 전문 개발자가 일상적으로하는 일이 아닙니다. 이들은 이러한 접근 방식을 사용하여 훨씬 흥미로운 것을 구축하고 사전 구축되고 안정적인 구현을 사용하여 시간을 절약 할 수 있습니다.


23

사용 된 언어에 따라 성능에 차이가있을 수 있다고 가정합니다. 그러나 그 이외의 세트를 사용해야하는 이유가 있습니까?

예, (성능이 아닙니다.)

세트 를 사용하지 않으면 추가 코드를 작성해야하므로 세트를 사용하십시오 . 세트를 사용하면 쉽게 읽을 수 있습니다. 고유성 로직에 대한 모든 테스트는 생각할 필요가없는 다른 곳에 숨겨져 있습니다. 이미 테스트를 거친 장소에 있으며 작동한다는 것을 믿을 수 있습니다.

그렇게하려면 자신의 코드를 작성해야하며 걱정해야합니다. 블리. 누가하고 싶어?

기본적으로 어떤 종류의 알고리즘에 집합이 필요하며 다른 컨테이너 유형으로 수행해서는 안됩니까?

"다른 컨테이너 유형으로는 수행 할 수 없습니다"라는 알고리즘이 없습니다. 집합을 활용할 수있는 알고리즘이 있습니다. 추가 코드를 작성할 필요가 없을 때 좋습니다.

이제 이와 관련하여 특별히 설정 한 것은 없습니다. 항상 필요에 가장 잘 맞는 컬렉션을 사용해야합니다. 자바에서는이 그림이 그 결정을 내리는 데 도움이된다는 것을 알았습니다. 세 가지 종류의 세트가 있음을 알 수 있습니다.

여기에 이미지 설명을 입력하십시오

그리고 @germi가 올바르게 지적했듯이, 작업에 올바른 컬렉션을 사용하면 다른 사람들이 코드를 쉽게 읽을 수 있습니다.


6
이미 언급했지만 세트를 사용하면 다른 사람들이 코드를 쉽게 추론 할 수 있습니다. 그들은 고유 한 항목 만 포함한다는 것을 알기 위해 채워지는 방법을 볼 필요가 없습니다.
germi

14

그러나 각 요소를 다른 벡터에 추가하고 요소가 이미 존재하는지 확인하여이를 수행 할 수도 있습니다.

그렇게 하면 벡터 데이터 구조 위에 집합의 의미를 구현합니다 . 오류를 포함 할 수있는 추가 코드를 작성 중이며 항목이 많으면 결과가 매우 느려집니다.

기존의 테스트되고 효율적인 집합 구현을 사용하여 왜 그렇게 하시겠습니까?


6

실제 엔터티를 나타내는 소프트웨어 엔터티는 종종 논리적으로 설정됩니다. 예를 들어 자동차를 생각해보십시오. 자동차에는 고유 식별자가 있으며 자동차 그룹이 세트를 구성합니다. 집합 개념은 프로그램이 알고 있고 데이터 값을 제한하는 것이 매우 중요하다는 자동차 수집에 대한 제약 조건으로 사용됩니다.

또한 세트에는 대수가 매우 잘 정의되어 있습니다. George가 소유 한 자동차 세트와 Alice가 소유 한 세트가있는 경우 George와 Alice가 동일한 자동차를 소유하더라도 유니온은 분명히 George와 Alice가 소유 한 세트입니다. 따라서 집합을 사용해야하는 알고리즘은 관련된 엔터티의 논리가 집합 특성을 나타내는 알고리즘입니다. 그것은 매우 일반적인 것으로 판명되었습니다.

세트가 구현되는 방법과 고유성 제약이 보장되는 방법은 또 다른 문제입니다. 하나는 세트가 로직에 매우 기본적이라는 점에서 중복을 제거하는 세트 로직에 대한 적절한 구현을 찾을 수 있기를 희망하지만 구현을 직접 수행하더라도 고유성 보장은 세트에 항목을 삽입하는 데 내재되어 있습니다. "요소가 이미 있는지 확인"하지 않아도됩니다.


"이미 존재하는지 확인"은 종종 중복 제거에 필수적입니다. 종종 개체는 데이터에서 생성됩니다. 또한 동일한 데이터로 객체를 생성하는 사람이 동일한 데이터에 대해 하나의 객체 만 재사용하기를 원합니다. 따라서 새 객체를 만들고 세트에 있는지 확인하고 객체가 있으면 세트에서 가져 오거나 그렇지 않으면 객체를 삽입합니다. 객체를 방금 삽입 한 경우에도 여전히 동일한 객체가 많이 있습니다.
gnasher729

1
@ gnasher729 Set 구현 자의 책임은 존재 여부를 검사하는 것을 포함하지만 Set 사용자 는 Set에 for 1..100: set.insert(10)10 개만 있음을 알 수 있습니다.
Caleth

사용자는 10 개의 동일한 객체 그룹에 100 개의 서로 다른 객체를 만들 수 있습니다. 삽입 한 후에는 세트에 10 개의 객체가 있지만 100 개의 객체가 여전히 떠 다닙니다. 중복 제거는 세트에 10 개의 객체가 있음을 의미하며 모두 10 개의 객체를 사용합니다. 분명히 테스트가 필요하지 않습니다. 객체를 제공하고 일치하는 객체를 세트로 반환하는 함수가 필요합니다.
gnasher729

4

성능 특성 (매우 중요하고 쉽게 무시해서는 안 됨) 외에도 세트는 추상 컬렉션으로 매우 중요합니다.

Array를 사용하여 Set 동작을 에뮬레이션 할 수 있습니까 (성능 무시)? 네 그럼요! 삽입 할 때마다 요소가 이미 배열에 있는지 확인한 다음 아직 찾지 못한 경우에만 요소를 추가 할 수 있습니다. 그러나 그것은 당신이 의식적으로 알고, Array-Psuedo-Set에 삽입 할 때마다 기억해야 할 것입니다. 오, 먼저 중복 검사를하지 않고 직접 한 번 삽입 했습니까? Welp, 배열이 불변성을 깨뜨 렸습니다 (모든 요소가 독특하고 동등하게 중복이 없음).

그래서 당신은 그 문제를 해결하기 위해 무엇을 할 것입니까? 새로운 데이터 유형을 생성하고 호출하면 (예 PsuedoSet:) 내부 배열을 래핑하고 insert공개적으로 작업을 노출하여 요소의 고유성을 강화합니다. 랩핑 된 배열은이 공용 insertAPI 를 통해서만 액세스 할 수 있으므로 중복이 발생하지 않도록 보장합니다. 이제 contains검사 성능을 향상시키기 위해 해싱을 추가 하면 조만간 full-out 구현을 구현할 수 Set있습니다.

나는 또한 진술에 응답하고 후속 질문을 할 것입니다.

첫 번째 프로그래밍 과정에서 여러 개의 정렬 된 요소를 저장하는 것과 같은 일을해야 할 때마다 Array를 사용해야한다고 들었습니다. 예 : 동료 이름 모음을 저장합니다. 그러나 원시 메모리를 할당하고 시작 포인터 + 주어진 오프셋에 의해 주어진 메모리 주소의 값을 설정하여 그렇게 할 수도 있습니다.

원시 포인터와 고정 오프셋을 사용하여 배열을 모방 할 수 있습니까? 네 그럼요! 삽입 할 때마다 오프셋이 작업중인 할당 된 메모리의 끝에서 벗어나지 않는지 확인할 수 있습니다. 그러나 그것은 당신이 의식적으로 알고, Pseudo-Array에 삽입 할 때마다 기억해야 할 것입니다. 오, 먼저 오프셋을 확인하지 않고 직접 한 번 삽입 했습니까? 웰프, 당신의 이름을 가진 세그먼테이션 결함이 있습니다!

그래서 당신은 그 문제를 해결하기 위해 무엇을 할 것입니까? PsuedoArray포인터와 크기를 감싸고, insert오퍼레이션을 공개적으로 노출시키는 새로운 데이터 유형을 호출 (예 :) 하면 오프셋이 크기를 초과하지 않도록 강제 할 수 있습니다. 랩핑 된 데이터는이 공용 insertAPI 를 통해서만 액세스 할 수 있으므로 버퍼 오버 플로우가 발생하지 않도록 보장합니다. 이제 다른 편리한 기능 (배열 크기 조정, 요소 삭제 등)을 추가하면 조만간 full-out 구현을 구현할 수 Array있습니다.


3

모든 종류의 집합 기반 알고리즘이 있습니다. 특히 교차 집합과 집합 조합을 수행하고 결과를 집합으로 설정해야하는 경우에 특히 유용합니다.

집합 기반 알고리즘은 다양한 경로 찾기 알고리즘 등에 많이 사용됩니다.

고정 이론에 대한 입문서는 다음 링크를 확인하십시오. http://people.umass.edu/partee/NZ_2006/Set%20Theory%20Basics.pdf

세트 시맨틱이 필요한 경우 세트를 사용하십시오. 어떤 단계에서 벡터 /리스트를 제거하는 것을 잊어 버렸기 때문에 가짜 복제로 인한 버그를 피할 수 있으며 벡터 /리스트를 지속적으로 정리하여 할 수있는 것보다 빠릅니다.


1

실제로 표준 세트 컨테이너는 대부분 쓸모가 없으며 배열을 사용하는 것을 선호하지만 다른 방식으로 수행합니다.

집합 교차점을 계산하기 위해 첫 번째 배열을 반복하고 요소를 단일 비트로 표시합니다. 그런 다음 두 번째 배열을 반복하고 표시된 요소를 찾습니다. Voila, 해시 테이블보다 작업 및 메모리가 훨씬 적은 선형 시간으로 교차점을 설정하십시오. 내 코드베이스는 요소를 복제하지 않고 요소 색인을 중심으로 회전하고 (요소 자체가 아닌 요소에 대한 인덱스를 복제 함) 정렬 할 필요가 거의 없지만 몇 년 동안 세트 데이터 구조를 사용하지 않았습니다. 결과.

또한 요소가 그러한 목적으로 데이터 필드를 제공하지 않는 경우에도 사용하는 악의적 인 비트 인증 C 코드가 있습니다. 트래버스 된 요소를 표시하기 위해 가장 중요한 비트 (사용하지 않은 비트)를 설정하여 요소 자체의 메모리를 사용합니다. 실제로는 거의 조립 수준에서 작업하지 않는 한 그렇게하지 마십시오. 그러나 요소가 순회에 특정 필드를 제공하지 않는 경우에도 적용 할 수있는 방법을 언급하고 싶었습니다. 특정 비트는 사용되지 않습니다. 내 딩키 i7에서 1 초 이내에 2 억 개의 요소 (2.4 기가 데이터)에 대한 설정된 교차점을 계산할 수 있습니다. std::set같은 시간에 각각 1 억 개의 요소를 포함하는 두 인스턴스 간에 교차를 설정하십시오 . 가까이 가지도 않습니다.

옆으로 ...

그러나 각 요소를 다른 벡터에 추가하고 요소가 이미 존재하는지 확인하여이를 수행 할 수도 있습니다.

새로운 벡터에 요소가 이미 존재하는지 확인하는 것은 일반적으로 선형 시간 연산이 될 것이며, 이는 설정된 교차점 자체를 2 차 연산으로 만듭니다 (폭발적인 작업량이 입력 크기가 클수록). 평범한 오래된 벡터 또는 배열을 사용하고 확장 가능한 방식으로 수행하려는 경우 위의 기술을 권장합니다.

기본적으로 어떤 종류의 알고리즘에 집합이 필요하며 다른 컨테이너 유형으로 수행해서는 안됩니까?

컨테이너 작업 수준에서 (세트 작업을 효율적으로 제공하기 위해 특별히 구현 된 데이터 구조에서와 같이) 의견에 대해 편견을 갖고 있지만 개념 수준에서 설정 논리를 필요로하는 것이 많이 있습니다. 예를 들어, 게임 세계에서 비행과 수영을 모두 할 수있는 생물을 찾고 싶다고하는데 한 세트 (실제로 세트 컨테이너를 사용하는지 여부)와 다른 생물에서 수영 할 수있는 생물을 가지고 있다고 가정 해 봅시다. . 이 경우 교차로를 설정해야합니다. 날 수 있거나 마법 같은 생물을 원한다면, 집합 조합을 사용합니다. 물론 이것을 구현하기 위해 실제로 세트 컨테이너가 필요하지 않으며 가장 최적의 구현은 일반적으로 세트로 특별히 설계된 컨테이너가 필요하지 않거나 원하지 않습니다.

탄젠트 해제

지미 제임스로부터이 교차로 접근에 관해 좋은 질문을 받았습니다. 그것은 주제를 다소 벗어 났지만 오, 더 많은 사람들 이이 기본 침입 방식을 사용하여 교차를 설정하여 균형 잡힌 이진 트리 및 해시 테이블과 같은 전체 보조 구조를 설정 작업의 목적으로 작성하지 않도록하는 데 관심이 있습니다. 언급 한 바와 같이, 근본적인 요구 사항은리스트가 얕은 카피 요소를 색인화하거나 정렬되지 않은 첫 번째 정렬되지 않은리스트 또는 배열을 통과하거나 "두 번째에서 픽업 할 때"표시 될 수있는 공유 요소를 가리 키도록하는 것입니다. 두 번째 목록을 통과하십시오.

그러나 다음과 같은 경우에는 요소를 건드리지 않고도 멀티 스레딩 컨텍스트에서도 실질적으로 달성 할 수 있습니다.

  1. 두 집계에는 요소에 대한 인덱스가 포함됩니다.
  2. 지수의 범위는 너무 크지 않으며 (예 : [0, 2 ^ 26), 수십억 이상이 아님) 합리적으로 밀집되어 있습니다.

이를 통해 집합 연산을 위해 병렬 배열 (요소 당 하나의 비트) 만 사용할 수 있습니다. 도표:

여기에 이미지 설명을 입력하십시오

스레드 동기화는 풀에서 병렬 비트 배열을 가져 와서 풀로 다시 릴리스 할 때만 필요합니다 (범위를 벗어날 때 암시 적으로 수행됨). 설정 작업을 수행하기위한 실제 두 루프에는 스레드 동기화가 필요하지 않습니다. 스레드가 비트를 로컬로 할당하고 해제 할 수있는 경우 병렬 비트 풀을 사용할 필요조차 없지만, 비트 풀은 중앙 요소가 자주 참조되는 이런 종류의 데이터 표현에 맞는 코드베이스에서 패턴을 일반화하는 데 편리 할 수 ​​있습니다. 각 스레드가 효율적인 메모리 관리를 방해하지 않도록 인덱스별로 내 영역의 주요 예는 엔터티 구성 요소 시스템 및 인덱스 메쉬 표현입니다. 둘 다 종종 교집합이 필요하며 중앙에 저장된 모든 것을 참조하는 경향이 있습니다 (ECS와 정점, 모서리,

인덱스가 밀집되어 있고 드문 드문 흩어져있는 경우에도 병렬 비트 / 부울 배열의 합리적인 스파 스 구현 (해당 512 비트 청크 (풀린 노드 당 64 바이트, 512 개의 연속 인덱스를 나타내는 노드) 만 저장)과 같이 적용 할 수 있습니다. ) 및 완전히 빈 연속 블록 할당을 건너 뜁니다. 중앙 데이터 구조가 요소 자체에 의해 거의 사용되지 않는 경우 이미 이와 같은 것을 사용하고있을 가능성이 있습니다.

여기에 이미지 설명을 입력하십시오

... 희소 비트 세트가 병렬 비트 배열로 사용되는 비슷한 아이디어. 이러한 구조는 새로운 불변 ​​복사를 만들기 위해 깊게 복사 할 필요가없는 청키 블록을 얕게 복사하기 쉽기 때문에 불변성에 적합합니다.

평균적인 기계에서이 접근 방식을 사용하여 수억 개의 요소들 사이의 교차점을 1 초 이내에 완료 할 수 있으며, 이는 단일 스레드 내에 있습니다.

클라이언트가 결과 교차점에 대한 요소 목록이 필요하지 않은 경우 절반 미만으로 수행 할 수 있습니다. 예를 들어 두 목록에서 찾은 요소에 일부 논리 만 적용하려는 경우에는 통과 할 수 있습니다 함수 포인터 또는 functor 또는 delegate 또는 교차하는 요소의 범위를 처리하기 위해 다시 호출되는 모든 것. 이 효과에 뭔가 :

// 'func' receives a range of indices to
// process.
set_intersection(func):
{
    parallel_bits = bit_pool.acquire()

    // Mark the indices found in the first list.
    for each index in list1:
        parallel_bits[index] = 1

    // Look for the first element in the second list 
    // that intersects.
    first = -1
    for each index in list2:
    {
         if parallel_bits[index] == 1:
         {
              first = index
              break
         }
    }

    // Look for elements that don't intersect in the second
    // list to call func for each range of elements that do
    // intersect.
    for each index in list2 starting from first:
    {
        if parallel_bits[index] != 1:
        {
             func(first, index)
             first = index
        }
    }
    If first != list2.num-1:
        func(first, list2.num)
}

또는이 효과에 무언가. 첫 번째 다이어그램에서 의사 코드의 가장 비싼 부분은 intersection.append(index)두 번째 루프 std::vector에 있으며 작은 목록의 크기에 미리 예약되어있는 경우에도 적용됩니다 .

모든 것을 딥 카피하면 어떻게됩니까?

그만 해요! 교차점을 설정해야하는 경우 교차 할 데이터를 복제하고 있음을 의미합니다. 아주 작은 객체조차도 32 비트 인덱스보다 작을 수 있습니다. 실제로 인스턴스화 된 ~ 43 억 개 이상의 요소가 필요하지 않으면 요소의 주소 지정 범위를 2 ^ 32 (2 ^ 32 바이트가 아닌 2 ^ 32 요소)로 줄일 수 있습니다.이 시점에서 완전히 다른 솔루션이 필요합니다 ( 메모리에 설정된 컨테이너를 사용하지 않는 것이 확실합니다).

주요 경기

요소가 동일하지 않지만 일치하는 키를 가질 수있는 작업을 설정해야하는 경우는 어떻습니까? 이 경우 위와 동일한 아이디어입니다. 각 고유 키를 인덱스에 매핑하면됩니다. 예를 들어 키가 문자열 인 경우, 인터 닝 된 문자열이이를 수행 할 수 있습니다. 이 경우 문자열 키를 32 비트 인덱스에 매핑하기 위해 trie 또는 해시 테이블과 같은 멋진 데이터 구조가 필요하지만 결과 32 비트 인덱스에 대해 설정 작업을 수행하기 위해 이러한 구조가 필요하지 않습니다.

기계의 전체 주소 범위가 아닌 매우 합리적인 범위의 요소에 대한 색인으로 작업 할 수있을 때 매우 저렴하고 간단한 알고리즘 솔루션과 데이터 구조가 많이 열립니다. 따라서 가치가있는 것보다 더 많습니다 각 고유 키에 대한 고유 색인을 얻을 수 있습니다.

나는 인덱스를 좋아한다!

나는 피자와 맥주만큼이나 인덱스를 좋아합니다. 내가 20 대에있을 때, 나는 실제로 C ++에 들어가서 모든 종류의 표준 호환 데이터 구조 (컴파일 타임에 범위 ctor에서 채우기 ctor를 명확하게하는 데 필요한 트릭 포함)를 설계하기 시작했습니다. 돌이켜 보면 그것은 많은 시간 낭비였습니다.

데이터베이스를 배열로 중앙 집중식으로 저장하고 조각화하고 기계의 전체 주소 지정 가능 범위에 걸쳐 저장하는 대신 요소를 색인화하는 데 데이터베이스를 돌리면 알고리즘 및 데이터 구조 가능성의 세계를 탐색 할 수 있습니다. 주변에 오래된 일반 회전 컨테이너와 알고리즘 설계 int또는 int32_t. 그리고 최종 결과가 한 데이터 구조에서 다른 데이터 구조로 다른 데이터 구조로 다른 요소로 다른 요소로 지속적으로 전송하지 않는 경우 훨씬 더 효율적이고 유지 관리하기 쉽다는 것을 알았습니다.

일부 고유 한 값에 T고유 인덱스가 있고 중앙 배열에 인스턴스가 있다고 가정 할 수있는 사용 사례 예 :

인덱스에 대한 부호없는 정수와 잘 작동하는 멀티 스레드 기수 정렬 . 실제로 다중 스레드 기수 정렬을 사용하면 Intel의 병렬 정렬과 같은 1 억 개의 요소를 정렬하는 데 약 1/10의 시간이 걸리며 Intel은 이미 std::sort큰 입력 보다 4 배 빠릅니다 . 물론 인텔은 비교 기반 정렬이기 때문에 훨씬 유연하며 사전을 정렬 할 수 있으므로 사과를 오렌지와 비교합니다. 그러나 캐시 친화적 인 메모리 액세스 패턴을 달성하거나 복제본을 빠르게 필터링하기 위해 기수 정렬 패스를 수행 할 수있는 것처럼 종종 오렌지 만 필요합니다.

노드 당 힙 할당없이 링크 된 목록, 트리, 그래프, 별도의 체인 해시 테이블 등과 같은 링크 된 구조를 빌드 할 수 있습니다 . 요소와 병렬로 노드를 대량으로 할당하고 인덱스와 함께 연결할 수 있습니다. 노드 자체는 다음 노드에 대한 32 비트 색인이되어 다음과 같이 큰 배열로 저장됩니다.

여기에 이미지 설명을 입력하십시오

병렬 처리에 적합합니다. 연결된 구조는 병렬 처리에 적합하지 않습니다. 왜냐하면 배열을 통한 병렬 for 루프를 수행하는 것과는 반대로 트리 또는 연결된 목록 순회에서 병렬 처리를 시도하는 것이 어색하기 때문에 어색합니다. 인덱스 / 중앙 배열 표현을 사용하면 항상 해당 중앙 배열로 가서 모든 것을 병렬 병렬 루프로 처리 할 수 ​​있습니다. 일부만 처리하려는 경우에도이 방법으로 처리 할 수있는 모든 요소의 중앙 배열이 항상 있습니다 (중앙 배열을 통한 캐시 친화적 액세스를 위해 기수 정렬 목록으로 색인화 된 요소를 처리 할 수 ​​있음).

상수 시간에 즉시 각 요소에 데이터를 연결할 수 있습니다 . 위의 병렬 비트 배열의 경우와 같이 병렬 데이터를 요소에 쉽고 저렴하게 연결하여 임시 처리 할 수 ​​있습니다. 여기에는 임시 데이터 이외의 사용 사례가 있습니다. 예를 들어, 메쉬 시스템은 사용자가 원하는만큼 UV 맵을 메쉬에 첨부 할 수 있도록 할 수 있습니다. 그러한 경우, 우리는 AoS 접근 방식을 사용하여 모든 단일 정점과 얼굴에 얼마나 많은 UV 맵이 있는지 하드 코딩 할 수 없습니다. 우리는 그러한 데이터를 즉석에서 연관시킬 수 있어야하며, 병렬 배열은 거기에서 편리하고 모든 종류의 정교한 연관 컨테이너, 심지어 해시 테이블보다 훨씬 저렴합니다.

물론 병렬 배열은 오류가 발생하기 쉬운 병렬 배열이 서로 동기화되어 있기 때문에 눈살을 찌푸립니다. 예를 들어 "root"배열에서 인덱스 7의 요소를 제거 할 때마다 "children"에 대해서도 동일한 작업을 수행해야합니다. 그러나 대부분의 언어에서이 개념을 범용 컨테이너로 일반화하는 것은 쉬운 일이므로 병렬 배열을 서로 동기화하기위한 까다로운 논리는 전체 코드베이스의 한 곳에만 있으면되며 이러한 병렬 배열 컨테이너는 이후 삽입시 회수 할 배열의 빈 공간에 많은 메모리를 낭비하지 않도록 위의 희소 배열 구현을 사용하십시오.

더 정교함 : 스파 스 비트 셋 트리

좋아, 나는 냉소적이라고 생각하는 것을 좀 더 정교하게 요청했지만 어쨌든 그렇게 재미있을 것입니다! 사람들이이 아이디어를 완전히 새로운 수준으로 끌어 내려면 N + M 요소를 선형 적으로 반복하지 않고도 교차로를 설정하는 것이 가능합니다. 이것은 내가 오랫동안 사용하고 기본적으로 모델로 사용했던 최고의 데이터 구조입니다 set<int>.

여기에 이미지 설명을 입력하십시오

두리스트의 각 요소를 검사하지 않고도 세트 교차를 수행 할 수있는 이유는 계층의 루트에있는 단일 세트 비트가 세트에 백만 개의 인접한 요소가 있음을 나타낼 수 있기 때문입니다. 1 비트 만 검사하면 범위에있는 N 개의 인덱스가 [first,first+N)세트에 있고 N이 매우 큰 수임을 알 수 있습니다.

점령 된 인덱스를 트래버스 할 때 실제로 이것을 루프 최적화 프로그램으로 사용합니다. 세트에 8 백만 개의 인덱스가 있다고 가정하기 때문입니다. 일반적으로이 경우 메모리의 8 백만 정수에 액세스해야합니다. 이것으로 잠재적으로 몇 비트를 검사하고 점유 된 인덱스의 인덱스 범위를 통해 루프를 돌릴 수 있습니다. 또한, 인덱스의 범위는 정렬 된 순서로되어있어 원래의 요소 데이터에 액세스하는 데 사용되는 정렬되지 않은 인덱스 배열을 반복하는 것과는 대조적으로 캐시 친화적 인 순차 액세스가 가능합니다. 물론이 기법은 매우 드문 경우에 더 나 빠지며 최악의 시나리오는 모든 단일 인덱스가 짝수 (또는 홀수) 인 경우가 많으며,이 경우 연속 영역이 전혀 없습니다. 그러나 내 유스 케이스에서는 최소한


2
"집합 교차점을 계산하려면 첫 번째 배열을 반복하고 요소를 단일 비트로 표시합니다. 그런 다음 두 번째 배열을 반복하여 표시된 요소를 찾습니다." 두 번째 배열의 어디에 표시합니까?
JimmyJames

1
아시다시피, 데이터를 각 값을 나타내는 단일 객체로 '인터 닝'하고 있습니다. 집합에 대한 사용 사례의 하위 집합에 대한 흥미로운 기술입니다. 이 접근법을 자신의 세트 클래스에서 조작으로 구현하지 않는 이유는 없습니다.
JimmyJames

2
"어떤 경우에는 캡슐화를 위반하는 침입 솔루션입니다 ..."일단 당신이 의미하는 바를 알아 냈을 때, 그것은 나에게 발생했지만 그럴 필요는 없다고 생각합니다. 이 동작을 관리하는 클래스가있는 경우 인덱스 객체는 모든 요소 데이터와 독립적이며 컬렉션 유형의 모든 인스턴스에서 공유 될 수 있습니다. 즉, 하나의 마스터 데이터 세트가있을 때 각 인스턴스는 마스터 세트를 다시 가리 킵니다. 멀티 스레딩은 더 복잡해야하지만 관리가 가능하다고 생각합니다.
JimmyJames

1
이것이 데이터베이스 솔루션에 잠재적으로 유용한 것처럼 보이지만 이런 식으로 구현되었는지 여부는 알 수 없습니다. 이것을 넣어 주셔서 감사합니다. 당신은 내 마음을 작동시켰다.
JimmyJames

1
좀 더 자세히 설명해 주시겠습니까? ;) 시간이 좀 있으면 확인해 보겠습니다.
JimmyJames

-1

n 개의 요소를 포함하는 집합에 다른 요소 X가 포함되어 있는지 확인하려면 일반적으로 일정한 시간이 걸립니다. n 개의 요소를 포함하는 배열에 다른 요소가 포함되어 있는지 확인하려면 X는 일반적으로 O (n) 시간이 걸립니다. 나쁘지만 n 개의 항목에서 복제본을 제거하려면 갑자기 O (n ^ 2) 대신 O (n)이 걸립니다. 100,000 개의 항목이 컴퓨터를 무릎에 꿇게합니다.

그리고 더 많은 이유를 묻고 있습니까? "촬영 외에도 저녁을 즐기 셨나요, 링컨 부인?"


2
다시 읽어보고 싶을 것 같습니다. O (n² ) 시간 대신 O (n) 시간을 취하는 것이 일반적으로 좋은 것으로 간주됩니다.
JimmyJames

이 글을 읽는 동안 머리에 서 있었을까요? OP는 "어떻게 어레이를 가져 가지 않느냐"고 물었다.
gnasher729

2
왜 O (n²)에서 O (n)으로 갈 때 '컴퓨터가 무릎을 꿇을 것'입니까? 나는 수업 시간에 그것을 놓쳤을 것입니다.
JimmyJames
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.