std::list
C ++ 표준 라이브러리 의 클래스에 대한 역 기능 이 선형 런타임을 갖는 이유는 무엇 입니까? 이중 연결 목록의 경우 역 기능이 O (1)이어야한다고 생각합니다.
이중으로 연결된 목록을 바꾸려면 머리와 꼬리 포인터를 전환하면됩니다.
std::list
C ++ 표준 라이브러리 의 클래스에 대한 역 기능 이 선형 런타임을 갖는 이유는 무엇 입니까? 이중 연결 목록의 경우 역 기능이 O (1)이어야한다고 생각합니다.
이중으로 연결된 목록을 바꾸려면 머리와 꼬리 포인터를 전환하면됩니다.
답변:
가설 적 reverse
으로 O (1) 일 수 있습니다 . 링크 된 목록의 방향이 현재 목록이 작성된 원래의 방향과 같은지 또는 반대인지를 나타내는 부울 목록 멤버가있을 수 있습니다 (가설 적으로).
불행히도, 기본적으로 다른 작업의 성능이 저하됩니다 (점근선 런타임을 변경하지 않아도). 각 작업에서, 링크의 "다음"또는 "이전"포인터를 따를 지 여부를 고려하기 위해 부울을 참조해야합니다.
이것은 비교적 드문 작업으로 간주되었으므로 표준 (구현을 지시하지 않고 복잡성 만)은 복잡성이 선형 일 수 있다고 명시했습니다. 따라서 "다음"포인터는 항상 같은 방향을 분명하게 의미하여 일반적인 작업 속도를 높입니다.
reverse
와 O(1)
복잡성 다른 작업의 큰 - 오에 영향을주지 않고 이 부울 플래그 트릭을 사용하여. 그러나 실제로 모든 작업에서 추가 분기는 기술적으로 O (1)이더라도 비용이 많이 듭니다. 반대로 sort
O (1)이고 다른 모든 작업의 비용이 같은 목록 구조를 만들 수 없습니다 . 문제는 요점은 O(1)
큰 O에만 관심이 있다면 무료로 리버스 할 수 있다는 것입니다. 왜 그렇게하지 않았습니까?
std::uintptr_t
. 그런 다음 xor 할 수 있습니다)
std::uintptr_t
캐스트 char
한 다음 구성 요소를 XOR 할 수 있습니다. 속도는 느리지 만 100 % 휴대 가능합니다. 아마도 두 구현 중에서 선택하고 두 번째 구현 uintptr_t
이 누락 된 경우 두 번째 구현 만 사용할 수 있습니다 . 어떤 이는이 답변에 설명되어있는 경우 : stackoverflow.com/questions/14243971/...
목록에 각 노드 의“ ”및“ ”포인터 의 의미를 바꿀 수 있는 플래그가 저장되어 있으면 O (1) 일 수 있습니다 . 목록을 되 돌리는 것이 빈번한 작업이라면 실제로 그러한 추가가 유용 할 수 있으며 현재 표준에 의해 구현이 금지 되는 이유를 모르겠습니다 . 그러나 이러한 플래그를 사용 하면 목록의 일반적인 순회 가 더 비쌉니다 (상수 요인에 의해서만)prev
next
current = current->next;
에 operator++
리스트 반복자, 당신은 얻을 것
if (reversed)
current = current->prev;
else
current = current->next;
쉽게 추가하기로 결정한 것이 아닙니다. 목록이 일반적으로 반대보다 훨씬 더 자주 통과한다는 점을 감안할 때 표준 이이 기술 을 요구 하는 것은 매우 현명하지 않습니다 . 따라서, 역동 작은 선형 복잡성을 가질 수있다. 그러나 앞에서 언급했듯이 t ∈ O (1) ⇒ t ∈ O ( n )은 기술적으로“최적화”구현이 허용됩니다.
Java 또는 이와 유사한 배경에서 온 경우 반복자가 매번 플래그를 확인해야하는 이유가 궁금 할 수 있습니다. 우리는 대신에 두 가지 반복자 유형 공통 기본 형식에서 파생 된 모두를 가지고 있고,이 수 없습니다 std::list::begin
및 std::list::rbegin
다형 적절한 반복자를 반환? 가능하면 반복자를 진행시키는 것은 간접적 인 (인라인하기 어려운) 함수 호출이므로 모든 것이 더 나빠질 것입니다. Java에서는 어쨌든이 가격을 정기적으로 지불하지만 성능이 중요 할 때 많은 사람들이 C ++에 도달하는 이유 중 하나입니다.
주석에서 Benjamin Lindley 가 지적한 것처럼 reverse
반복자를 무효화 할 수 없기 때문에 표준에 의해 허용되는 유일한 접근 방식은 반복자 내부의 목록에 포인터를 저장하여 이중 간접 메모리 액세스를 유발하는 것으로 보입니다.
std::list::reverse
반복자를 무효화하지 않습니다.
next
및 prev
포인터를 배열에 저장하고 방향을 a 0
또는 로 저장하십시오 1
. 앞으로 반복하려면 따라 pointers[direction]
하고 반대로 반복하십시오 pointers[1-direction]
(또는 그 반대). 이것은 여전히 오버 헤드의 작은 비트를 추가 할 수 있지만 것입니다 아마 지점 미만.
swap()
상수 시간으로 지정되며 반복자를 무효화하지 않습니다.
양방향 반복자를 지원하는 모든 컨테이너에는 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.
모든 노드를 순회 n
하고 데이터를 업데이트해야 하기 때문에 (업데이트 단계는 실제로 O(1)
) 이것은 전체 작업을 O(n*1) = O(n)
합니다.