std :: list :: reverse에 왜 O (n) 복잡성이 있습니까?


192

std::listC ++ 표준 라이브러리 의 클래스에 대한 역 기능 이 선형 런타임을 갖는 이유는 무엇 입니까? 이중 연결 목록의 경우 역 기능이 O (1)이어야한다고 생각합니다.

이중으로 연결된 목록을 바꾸려면 머리와 꼬리 포인터를 전환하면됩니다.


18
사람들이 왜이 질문을 거절하는지 이해하지 못합니다. 물어 보는 것은 매우 합리적인 질문입니다. 이중 연결 목록을 되돌리려면 O (1) 시간이 걸립니다.
호기심

46
불행히도 일부 사람들은 "질문이 좋다"라는 개념과 "질문이 좋은 생각이있다"라는 개념을 혼동합니다. 나는 기본적으로 "내 이해가 일반적으로 받아 들여지는 관행과 다른 것처럼 보이며,이 갈등을 해결하도록 도와주십시오"라는 질문을 좋아합니다. 다른 사람들은 "99.9999 %의 처리 낭비이고, 생각조차하지 않는"접근 방식을 취하는 것으로 보입니다. 그것이 위안이라면, 나는 훨씬 더 적게 공감당했습니다!
corsiKa

4
예,이 질문은 품질면에서 엄청난 양의 다운 보트를 얻었습니다. 아마도 Blindy의 대답을 누가 찬성했는지와 동일합니다. 공평하게, "이중 연결리스트를 뒤집는 것은 단지 머리와 꼬리 포인터를 바꾸는 것을 포함해야한다"는 것은 일반적으로 모든 사람들이 고등학교에서 배운 표준 링크리스트 나 사람들이 사용하는 많은 구현에 대해서는 사실이 아닙니다. SO 질문에 대한 사람들의 즉각적인 직감 반응에서 많은 시간이 공감각 / 감독 결정을 내립니다. 당신이 그 문장에서 더 명확하거나 생략했다면, 당신은 덜 공언을 덜 받았을 것입니다.
Chris Beck

3
또는 @Curious : 증명해야 할 부담은 @Curious : ideone.com/c1HebO에서 이중 연결 목록 구현을 마무리했습니다 . Reverse함수가 O (1)에서 어떻게 구현 될지 예상 할 수 있습니까 ?
CompuChip

2
@CompuChip : 실제로 구현에 따라 그렇지 않을 수도 있습니다. 사용할 포인터를 알기 위해 여분의 부울이 필요하지 않습니다. 당신을 가리키는 포인터를 사용하십시오 .XOR 링크 목록에서는 자동으로 수행 할 수 있습니다. 따라서 목록이 구현되는 방법에 따라 다르며 OP 문을 명확히 할 수 있습니다.
Matthieu M.

답변:


187

가설 적 reverse으로 O (1) 일 수 있습니다 . 링크 된 목록의 방향이 현재 목록이 작성된 원래의 방향과 같은지 또는 반대인지를 나타내는 부울 목록 멤버가있을 수 있습니다 (가설 ​​적으로).

불행히도, 기본적으로 다른 작업의 성능이 저하됩니다 (점근선 런타임을 변경하지 않아도). 각 작업에서, 링크의 "다음"또는 "이전"포인터를 따를 지 여부를 고려하기 위해 부울을 참조해야합니다.

이것은 비교적 드문 작업으로 간주되었으므로 표준 (구현을 지시하지 않고 복잡성 만)은 복잡성이 선형 일 수 있다고 명시했습니다. 따라서 "다음"포인터는 항상 같은 방향을 분명하게 의미하여 일반적인 작업 속도를 높입니다.


29
@MooseBoys : 나는 당신의 비유에 동의하지 않습니다. 차이점은이 목록의 경우, 구현은 제공 할 수 있습니다 reverseO(1)복잡성 다른 작업의 큰 - 오에 영향을주지 않고 이 부울 플래그 트릭을 사용하여. 그러나 실제로 모든 작업에서 추가 분기는 기술적으로 O (1)이더라도 비용이 많이 듭니다. 반대로 sortO (1)이고 다른 모든 작업의 ​​비용이 같은 목록 구조를 만들 수 없습니다 . 문제는 요점은 O(1)큰 O에만 관심이 있다면 무료로 리버스 할 수 있다는 것입니다. 왜 그렇게하지 않았습니까?
Chris Beck

9
XOR 연결 목록을 사용한 경우 반전은 일정한 시간이됩니다. 반복자는 더 클 것이고, 증가 / 감소는 약간 더 계산 비용이 많이 듭니다. 그것은 모든 종류의 링크 된 목록 에 대해 피할 수없는 메모리 액세스로 인해 왜소 할 수 있습니다 .
중복 제거기

3
@IlyaPopov : 모든 노드에 실제로 이것이 필요합니까? 사용자는 목록 노드 자체에 대한 질문을하지 않고 기본 목록 본문 만 묻습니다. 따라서 사용자가 호출하는 모든 메소드에서 부울에 액세스하는 것이 쉽습니다. 예를 들어 목록이 반전되면 반복자가 무효화되는 규칙을 만들거나 반복자와 함께 부울 사본을 저장할 수 있습니다. 그래서 나는 그것이 큰 O에 영향을 미치지 않을 것이라고 생각합니다. 인정할 것입니다. 사양을 한 줄씩 살펴 보지 않았습니다. :)
Chris Beck

4
@ 케빈 : 흠, 뭐? 어쨌든 두 개의 포인터를 직접 xor 할 수 없으므로 먼저 정수로 변환해야합니다 (명백하게 유형 std::uintptr_t. 그런 다음 xor 할 수 있습니다)
Deduplicator

3
@Kevin, C ++로 XOR 링크리스트를 만들 수있다. 실제로 이런 종류의 포스터-자식 언어이다. 아무것도 사용하지 않아도 배열에 std::uintptr_t캐스트 char한 다음 구성 요소를 XOR 할 수 있습니다. 속도는 느리지 만 100 % 휴대 가능합니다. 아마도 두 구현 중에서 선택하고 두 번째 구현 uintptr_t이 누락 된 경우 두 번째 구현 만 사용할 수 있습니다 . 어떤 이는이 답변에 설명되어있는 경우 : stackoverflow.com/questions/14243971/...
크리스 벡

61

목록에 각 노드 의“ ”및“ ”포인터 의 의미를 바꿀 수 있는 플래그가 저장되어 있으면 O (1) 일 있습니다 . 목록을 되 돌리는 것이 빈번한 작업이라면 실제로 그러한 추가가 유용 할 수 있으며 현재 표준에 의해 구현이 금지 되는 이유를 모르겠습니다 . 그러나 이러한 플래그를 사용 하면 목록의 일반적인 순회 가 더 비쌉니다 (상수 요인에 의해서만)prevnext

current = current->next;

operator++리스트 반복자, 당신은 얻을 것

if (reversed)
  current = current->prev;
else
  current = current->next;

쉽게 추가하기로 결정한 것이 아닙니다. 목록이 일반적으로 반대보다 훨씬 더 자주 통과한다는 점을 감안할 때 표준 이이 기술 을 요구 하는 것은 매우 현명하지 않습니다 . 따라서, 역동 작은 선형 복잡성을 가질 수있다. 그러나 앞에서 언급했듯이 tO (1) ⇒ tO ( n )은 기술적으로“최적화”구현이 허용됩니다.

Java 또는 이와 유사한 배경에서 온 경우 반복자가 매번 플래그를 확인해야하는 이유가 궁금 할 수 있습니다. 우리는 대신에 두 가지 반복자 유형 공통 기본 형식에서 파생 된 모두를 가지고 있고,이 수 없습니다 std::list::beginstd::list::rbegin다형 적절한 반복자를 반환? 가능하면 반복자를 진행시키는 것은 간접적 인 (인라인하기 어려운) 함수 호출이므로 모든 것이 더 나빠질 것입니다. Java에서는 어쨌든이 가격을 정기적으로 지불하지만 성능이 중요 할 때 많은 사람들이 C ++에 도달하는 이유 중 하나입니다.

주석에서 Benjamin Lindley 가 지적한 것처럼 reverse반복자를 무효화 할 수 없기 때문에 표준에 의해 허용되는 유일한 접근 방식은 반복자 내부의 목록에 포인터를 저장하여 이중 간접 메모리 액세스를 유발하는 것으로 보입니다.


7
@galinette : std::list::reverse반복자를 무효화하지 않습니다.
Benjamin Lindley

1
@galinette 죄송합니다 . 귀하가 작성한 " 노드 당 플래그"와는 반대로 " 반복자 별 플래그"로 이전 주석을 잘못 읽었 습니다. 물론 노드 당 플래그는 역순 회적이므로 다시 통과하기 위해 선형 순회를 수행해야합니다.
5gon12eder 2019

2
@ 5gon12eder : 매우 손실 된 비용으로 분기를 제거 할 수 있습니다 : nextprev포인터를 배열에 저장하고 방향을 a 0또는 로 저장하십시오 1. 앞으로 반복하려면 따라 pointers[direction]하고 반대로 반복하십시오 pointers[1-direction](또는 그 반대). 이것은 여전히 오버 헤드의 작은 비트를 추가 할 수 있지만 것입니다 아마 지점 미만.
Jerry Coffin

4
반복자 안에 목록에 대한 포인터를 저장할 수 없습니다. swap()상수 시간으로 지정되며 반복자를 무효화하지 않습니다.
Tavian Barnes

1
@TavianBarnes 젠장! 그럼, 삼중 간접 지정은… (실제로 삼중이 아닙니다. 동적으로 할당 된 객체에 플래그를 저장해야하지만 반복자의 포인터는 물론 목록을 간접적으로 지정하는 대신 해당 객체를 직접 가리킬 수 있습니다.)
5gon12eder

37

양방향 반복자를 지원하는 모든 컨테이너에는 rbegin () 및 rend ()라는 개념이 있으므로이 질문에 대한 답이 없습니까?

이터레이터를 되돌리고이를 통해 컨테이너에 액세스하는 프록시를 작성하는 것은 쉽지 않습니다.

이 비 작동은 실제로 O (1)입니다.

같은 :

#include <iostream>
#include <list>
#include <string>
#include <iterator>

template<class Container>
struct reverse_proxy
{
    reverse_proxy(Container& c)
    : _c(c)
    {}

    auto begin() { return std::make_reverse_iterator(std::end(_c)); }
    auto end() { return std::make_reverse_iterator(std::begin(_c)); }

    auto begin() const { return std::make_reverse_iterator(std::end(_c)); }
    auto end() const { return std::make_reverse_iterator(std::begin(_c)); }

    Container& _c;
};

template<class Container>
auto reversed(Container& c)
{
    return reverse_proxy<Container>(c);
}

int main()
{
    using namespace std;
    list<string> l { "the", "cat", "sat", "on", "the", "mat" };

    auto r = reversed(l);
    copy(begin(r), end(r), ostream_iterator<string>(cout, "\n"));

    return 0;
}

예상 출력 :

mat
the
on
sat
cat
the

이를 감안할 때 표준위원회는 컨테이너가 필요하지 않기 때문에 컨테이너의 O (1) 역 순서를 의무화하는 데 시간이 걸리지 않았으며 표준 라이브러리는 엄격하게 필요한 것만 지시하는 원칙에 따라 크게 구축되었습니다. 복제 방지.

그냥 내 2c.


18

모든 노드를 순회 n하고 데이터를 업데이트해야 하기 때문에 (업데이트 단계는 실제로 O(1)) 이것은 전체 작업을 O(n*1) = O(n)합니다.


27
모든 항목 간의 링크도 업데이트해야하기 때문입니다. 종이 한 장을 꺼내서 다운 보트 대신 꺼내십시오.
Blindy

11
왜 그렇게 확신하는지 물어보십시오. 당신은 우리 시간을 낭비하고 있습니다.
Blindy

28
@Curious Doubly 연결된리스트의 노드는 방향 감각이 있습니다. 거기에서 이유.
구두

11
@Blindy 좋은 답변이 완성되어야합니다. 따라서 "종이를 꺼내서 꺼내십시오"는 정답의 필수 구성 요소가되어서는 안됩니다. 정답이 아닌 답변은 공감할 수 있습니다.
RM

7
@ 신발 : 그들은해야합니까? XOR 연계리스트 등을 조사하십시오.
중복 제거기

2

또한 모든 노드에 대해 이전 및 다음 포인터를 교체합니다. 그것이 선형이 필요한 이유입니다. 이 LL을 사용하는 함수가 LL에 대한 정보를 정상적으로 액세스하는지 또는 역방향으로 액세스하는지와 같이 입력으로 가져 오면 O (1)에서 수행 할 수 있습니다.


1

알고리즘 설명 만. 요소가있는 배열이 있다고 가정하면 반전해야합니다. 기본 아이디어는 첫 번째 위치의 요소를 마지막 위치로 변경하고 두 번째 위치의 요소를 두 번째 위치로 변경하는 등 각 요소를 반복하는 것입니다. 배열의 중간에 도달하면 모든 요소가 변경되므로 (n / 2) 반복에서 O (n)으로 간주됩니다.


1

목록을 역순으로 복사해야하기 때문에 O (n)입니다. 각 개별 항목 작업은 O (1)이지만 전체 목록에 n 개가 있습니다.

물론 새로운리스트를위한 공간을 설정하고 나중에 포인터를 변경하는 등의 상수 시간 연산이 있습니다. O 표기법은 1 차 n 인자를 포함하면 개별 상수를 고려하지 않습니다.

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