Điều gì thực sự là một deque trong STL?


192

Tôi đã xem xét các thùng chứa STL và cố gắng tìm ra chúng thực sự là gì (tức là cấu trúc dữ liệu được sử dụng) và deque đã ngăn tôi: Lúc đầu tôi nghĩ rằng đó là một danh sách liên kết kép, cho phép chèn và xóa từ cả hai đầu trong thời gian không đổi, nhưng tôi gặp rắc rối bởi lời hứa của nhà điều hành [] sẽ được thực hiện trong thời gian không đổi. Trong một danh sách được liên kết, truy cập tùy ý phải là O (n), phải không?

Và nếu đó là một mảng động, làm thế nào nó có thể thêm các phần tử trong thời gian không đổi? Cần phải đề cập rằng việc tái phân bổ có thể xảy ra và O (1) là chi phí khấu hao, giống như đối với một vectơ .

Vì vậy, tôi tự hỏi cấu trúc này là gì cho phép truy cập tùy ý trong thời gian liên tục, và đồng thời không bao giờ cần phải được chuyển đến một nơi mới lớn hơn.



1
@Graham Lần dequeue Giật là một tên gọi chung khác cho trò chơi dequeiên. Tôi vẫn chấp thuận bản chỉnh sửa vì từ điển deque, thường là tên chính tắc.
Konrad Rudolph

@Konrad Cảm ơn. Câu hỏi cụ thể là về deque C ++ STL, sử dụng chính tả ngắn hơn.
Graham Borland

2
dequelà viết tắt của hàng đợi kết thúc kép , mặc dù rõ ràng yêu cầu nghiêm ngặt về quyền truy cập O (1) vào các yếu tố trung gian là đặc biệt đối với C ++
Matthieu M.

Câu trả lời:


181

Một deque được định nghĩa một cách đệ quy: bên trong nó duy trì một hàng hai đầu của các khối có kích thước cố định. Mỗi khối là một vectơ và hàng đợi (Bản đồ bản đồ trong hình bên dưới) của chính các khối cũng là một vectơ.

sơ đồ bố trí bộ nhớ của một deque

Có một phân tích tuyệt vời về các đặc tính hiệu suất và cách so sánh với vectortại CodeProject .

Việc triển khai thư viện tiêu chuẩn GCC trong nội bộ sử dụng a T**để thể hiện bản đồ. Mỗi khối dữ liệu là một khối được T*phân bổ với một số kích thước cố định __deque_buf_size(phụ thuộc vào sizeof(T)).


27
Đó là định nghĩa của một deque như tôi đã học, nhưng theo cách này, nó không thể đảm bảo truy cập thời gian liên tục, do đó phải thiếu một cái gì đó.
stefaanv

14
@stefaanv, @Konrad: Việc triển khai C ++ mà tôi đã thấy đã sử dụng một mảng các con trỏ tới các mảng có kích thước cố định. Điều này có nghĩa là Push_front và Push_back không thực sự là thời gian không đổi, nhưng với các yếu tố tăng trưởng thông minh, bạn vẫn nhận được khấu hao liên tục, do đó, O (1) không quá sai lầm và trong thực tế, nó nhanh hơn so với vectơ vì bạn đang tráo đổi con trỏ đơn hơn là toàn bộ đối tượng (và ít con trỏ hơn đối tượng).
Matthieu M.

5
Truy cập liên tục vẫn có thể. Chỉ cần, nếu bạn cần phân bổ một khối mới ở phía trước, đẩy lùi một con trỏ mới trên vectơ chính và dịch chuyển tất cả các con trỏ.
Xèo

4
Nếu bản đồ (chính hàng đợi) là danh sách hai đầu, tôi không thấy cách nó có thể cho phép truy cập ngẫu nhiên O (1). Nó có thể được thực hiện như một bộ đệm tròn, cho phép thay đổi kích thước bộ đệm tròn hiệu quả hơn: Chỉ sao chép các con trỏ thay vì tất cả các thành phần trong hàng đợi. Dường như đó chỉ là một lợi ích nhỏ.
Wernight

14
@JeremyWest Tại sao không? Truy cập được lập chỉ mục đi đến phần tử i% B-th trong khối i / B-th (kích thước khối B =), đó rõ ràng là O (1). Bạn có thể thêm một khối mới vào khấu hao O (1), do đó việc thêm các phần tử được khấu hao O (1) ở cuối. Thêm một phần tử mới ở đầu là O (1) trừ khi cần thêm một khối mới. Thêm một khối mới ở đầu không phải là O (1), đúng, đó là O (N) nhưng thực tế nó có một yếu tố không đổi rất nhỏ vì bạn chỉ cần di chuyển con trỏ N / B chứ không phải là yếu tố N.
Konrad Rudolph

22

Hãy tưởng tượng nó như một vectơ của vectơ. Chỉ có họ không phải std::vectorlà s tiêu chuẩn .

Các vectơ bên ngoài chứa con trỏ đến các vectơ bên trong. Khi công suất của nó được thay đổi thông qua việc phân bổ lại, thay vì phân bổ tất cả khoảng trống vào cuối std::vector, nó sẽ phân chia không gian trống thành các phần bằng nhau ở đầu và cuối của vectơ. Điều này cho phép push_frontpush_backtrên vectơ này cả hai xảy ra trong thời gian khấu hao O (1).

Hành vi vectơ bên trong cần thay đổi tùy thuộc vào việc nó ở phía trước hay phía sau của deque. Ở phía sau, nó có thể hoạt động như một tiêu chuẩn std::vectornơi nó phát triển ở cuối và push_backxuất hiện trong thời gian O (1). Ở phía trước, nó cần phải làm ngược lại, phát triển ở đầu với mỗi push_front. Trong thực tế, điều này có thể dễ dàng đạt được bằng cách thêm một con trỏ vào phần tử phía trước và hướng phát triển cùng với kích thước. Với sửa đổi đơn giản này push_frontcũng có thể là O (1) thời gian.

Truy cập vào bất kỳ phần tử nào đòi hỏi phải bù đắp và chia cho chỉ số vectơ bên ngoài thích hợp xảy ra trong O (1) và lập chỉ mục vào vectơ bên trong cũng là O (1). Điều này giả định rằng các vectơ bên trong đều có kích thước cố định, ngoại trừ các vectơ ở đầu hoặc cuối của deque.


1
Bạn có thể mô tả các vectơ bên trong có công suất
Caleth

18

deque = hàng đợi kết thúc

Một container có thể phát triển theo một trong hai hướng.

Deque thường được triển khai như một vectortrong số vectors(một danh sách các vectơ không thể cung cấp truy cập ngẫu nhiên theo thời gian liên tục). Trong khi kích thước của các vectơ thứ cấp phụ thuộc vào việc triển khai, một thuật toán phổ biến là sử dụng kích thước không đổi theo byte.


6
Nó không hoàn toàn vectơ trong nội bộ. Các cấu trúc bên trong có thể được phân bổ nhưng công suất không được sử dụng ở đầu cũng như cuối
Vịt Mooing

@MooingDuck: Đó là việc triển khai được xác định thực sự. Nó có thể là một mảng các mảng hoặc vectơ của vectơ hoặc bất cứ thứ gì có thể cung cấp hành vi và độ phức tạp được quy định bởi tiêu chuẩn.
Alok Lưu

1
@Als: Tôi không nghĩ arrayvề bất cứ điều gì hoặc vectorbất cứ điều gì có thể hứa hẹn sẽ được khấu hao O(1). Ít nhất bên trong của hai cấu trúc, phải có khả năng có một O(1)Push_front, điều mà không phải arraycũng không vectorthể đảm bảo.
Vịt Mooing

4
@MooingDuck yêu cầu đó dễ dàng được đáp ứng nếu đoạn đầu tiên phát triển từ trên xuống thay vì từ dưới lên. Rõ ràng là một tiêu chuẩn vectorkhông làm điều đó, nhưng đó là một sửa đổi đủ đơn giản để làm cho nó như vậy.
Đánh dấu tiền chuộc

3
@ Mooing Duck, Cả Push_front và Push_back đều có thể dễ dàng thực hiện trong khấu hao O (1) với cấu trúc vectơ duy nhất. Nó chỉ là một chút kế toán của một bộ đệm tròn, không có gì hơn. Giả sử bạn có một vectơ thông thường có công suất 1000 với 100 phần tử ở vị trí 0 đến 99. Bây giờ, khi một lần đẩy xảy ra, bạn chỉ cần đẩy ở cuối, tức là ở vị trí 999, sau đó là 998 cho đến khi hai đầu gặp nhau. Sau đó, bạn phân bổ lại (với sự tăng trưởng theo cấp số nhân để đảm bảo số lần không đổi của amortizet) giống như bạn làm với một vectơ thông thường. Vì vậy, hiệu quả bạn chỉ cần một con trỏ bổ sung để đầu tiên el.
plamenko

14

(Đây là câu trả lời tôi đã đưa ra trong một chủ đề khác . Về cơ bản, tôi cho rằng việc triển khai thậm chí khá ngây thơ, sử dụng một cách duy nhất vector, tuân thủ các yêu cầu của "đẩy không khấu hao liên tục_ trước, sau}". Bạn có thể ngạc nhiên , và nghĩ rằng điều này là không thể, nhưng tôi đã tìm thấy những trích dẫn có liên quan khác trong tiêu chuẩn xác định bối cảnh một cách đáng ngạc nhiên. Xin hãy kiên nhẫn với tôi, nếu tôi mắc lỗi trong câu trả lời này, sẽ rất hữu ích để xác định những điều nào Tôi đã nói chính xác và nơi logic của tôi đã bị hỏng.)

Trong câu trả lời này, tôi không cố gắng xác định một triển khai tốt , tôi chỉ đang cố gắng giúp chúng tôi diễn giải các yêu cầu phức tạp trong tiêu chuẩn C ++. Tôi đang trích dẫn từ N3242 , theo Wikipedia , là tài liệu tiêu chuẩn hóa C ++ 11 miễn phí mới nhất. (Nó dường như được tổ chức khác với tiêu chuẩn cuối cùng và do đó tôi sẽ không trích dẫn số trang chính xác. Tất nhiên, các quy tắc này có thể đã thay đổi trong tiêu chuẩn cuối cùng, nhưng tôi không nghĩ điều đó đã xảy ra.)

A deque<T>có thể được thực hiện chính xác bằng cách sử dụng a vector<T*>. Tất cả các phần tử được sao chép vào heap và các con trỏ được lưu trữ trong một vectơ. (Thêm về vectơ sau).

Tại sao T*thay vì T? Bởi vì tiêu chuẩn đòi hỏi rằng

"Việc chèn vào một trong hai đầu của deque làm mất hiệu lực tất cả các trình lặp cho deque, nhưng không có tác dụng đối với tính hợp lệ của các tham chiếu đến các phần tử của deque. "

(nhấn mạnh của tôi). Việc T*giúp thỏa mãn điều đó. Nó cũng giúp chúng tôi đáp ứng điều này:

"Chèn một phần tử duy nhất ở đầu hoặc cuối của một deque luôn ..... gây ra một cuộc gọi đến một hàm tạo của T. "

Bây giờ cho bit (gây tranh cãi). Tại sao sử dụng một vectorđể lưu trữ T*? Nó cho chúng ta truy cập ngẫu nhiên, đó là một khởi đầu tốt. Hãy quên đi sự phức tạp của vectơ trong giây lát và xây dựng vấn đề này một cách cẩn thận:

Tiêu chuẩn nói về "số lượng hoạt động trên các đối tượng được chứa.". Đối với deque::push_frontđiều này rõ ràng là 1 bởi vì chính xác một Tđối tượng được xây dựng và không có Tđối tượng nào được đọc hoặc quét theo bất kỳ cách nào. Con số này, 1, rõ ràng là một hằng số và không phụ thuộc vào số lượng các đối tượng hiện có trong deque. Điều này cho phép chúng ta nói rằng:

'Đối với chúng tôi deque::push_front, số lượng hoạt động trên các đối tượng được chứa (Ts) là cố định và không phụ thuộc vào số lượng đối tượng đã có trong deque.'

Tất nhiên, số lượng các hoạt động trên T*sẽ không được thực hiện tốt. Khi vector<T*>phát triển quá lớn, nó sẽ được phân bổ lại và nhiều T*s sẽ được sao chép xung quanh. Vì vậy, có, số lượng các hoạt động trên T*sẽ thay đổi rất nhiều, nhưng số lượng các hoạt động trên Tsẽ không bị ảnh hưởng.

Tại sao chúng ta quan tâm đến sự khác biệt này giữa đếm hoạt động Tvà đếm hoạt động trên T*? Đó là bởi vì tiêu chuẩn nói:

Tất cả các yêu cầu phức tạp trong mệnh đề này chỉ được nêu dưới dạng số lượng thao tác trên các đối tượng được chứa.

Đối với deque, các đối tượng được chứa là T, không phải T*, nghĩa là chúng ta có thể bỏ qua mọi thao tác sao chép (hoặc reallocs) a T*.

Tôi đã không nói nhiều về cách một vectơ sẽ hành xử trong một deque. Có lẽ chúng ta sẽ giải thích nó như một bộ đệm tròn (với vectơ luôn chiếm tối đa capacity(), và sau đó phân bổ lại mọi thứ thành một bộ đệm lớn hơn khi vectơ đầy. Các chi tiết không quan trọng.

Trong một vài đoạn cuối, chúng tôi đã phân tích deque::push_frontvà mối quan hệ giữa số lượng đối tượng trong deque đã có và số lượng các hoạt động được thực hiện bởi Push_front trên các T-objects có chứa . Và chúng tôi thấy họ độc lập với nhau. Vì các tiêu chuẩn bắt buộc rằng độ phức tạp là về mặt hoạt động T, nên chúng ta có thể nói điều này có độ phức tạp không đổi.

Có, Hoạt động-On-T * -Complexity được khấu hao (do vector), nhưng chúng tôi chỉ quan tâm đến Độ phức tạp của hoạt động và điều này là không đổi (không khấu hao).

Sự phức tạp của vector :: push_back hoặc vector :: push_front không liên quan trong việc thực hiện này; những cân nhắc liên quan đến các hoạt động trên T*và do đó không liên quan. Nếu tiêu chuẩn đề cập đến khái niệm lý thuyết 'thông thường' về độ phức tạp, thì họ sẽ không giới hạn rõ ràng về "số lượng thao tác trên các đối tượng được chứa". Tôi có diễn giải quá mức câu đó không?


8
Có vẻ như rất nhiều gian lận với tôi! Khi bạn chỉ định mức độ phức tạp của một thao tác, bạn không thực hiện nó trên một phần dữ liệu: bạn muốn có ý tưởng về thời gian chạy dự kiến ​​của hoạt động bạn đang gọi, bất kể hoạt động trên đó là gì. Nếu tôi tuân theo logic của bạn về các thao tác trên T, điều đó có nghĩa là bạn có thể kiểm tra xem giá trị của mỗi T * có phải là số nguyên tố mỗi khi một thao tác được thực hiện và vẫn tuân thủ tiêu chuẩn vì bạn không chạm vào Ts. Bạn có thể chỉ định nơi trích dẫn của bạn đến từ đâu?
Zonko

2
Tôi nghĩ rằng các nhà văn tiêu chuẩn biết rằng họ không thể sử dụng lý thuyết phức tạp thông thường bởi vì chúng ta không có một hệ thống được chỉ định đầy đủ, nơi chúng ta biết, ví dụ, sự phức tạp của việc cấp phát bộ nhớ. Thật không thực tế khi giả vờ rằng bộ nhớ có thể được phân bổ cho một thành viên mới của listbất kể kích thước hiện tại của danh sách; nếu danh sách quá lớn, việc phân bổ sẽ chậm hoặc sẽ thất bại. Do đó, theo như tôi thấy, ủy ban đã đưa ra quyết định chỉ xác định các hoạt động có thể được tính và đo lường khách quan. (PS: Tôi có một lý thuyết khác về điều này cho câu trả lời khác.)
Aaron McDaid

Tôi khá chắc chắn O(n)có nghĩa là số lượng các hoạt động tỷ lệ thuận với số lượng các yếu tố. IE, số lượng hoạt động meta. Nếu không, sẽ không có ý nghĩa để giới hạn tra cứu O(1). Ergo, danh sách liên kết không đủ điều kiện.
Vịt Mooing

8
Đây là một cách giải thích rất thú vị, nhưng theo logic này, một con trỏ cũng listcó thể được thực hiện như một vectorcon trỏ (việc chèn vào giữa sẽ dẫn đến một lời gọi hàm tạo sao chép duy nhất , bất kể kích thước danh sách và việc O(N)xáo trộn con trỏ có thể bị bỏ qua vì chúng không phải là hoạt động trên T).
Mankude

1
Đây là cách lập pháp ngôn ngữ tốt (mặc dù tôi sẽ không cố gắng nói rõ liệu nó có thực sự đúng hay không nếu có một số điểm tinh tế trong tiêu chuẩn cấm thực hiện này). Nhưng đó không phải là thông tin hữu ích trong thực tế, bởi vì (1) các triển khai phổ biến không thực hiện dequetheo cách này và (2) "gian lận" theo cách này (ngay cả khi được tiêu chuẩn cho phép) khi tính toán phức tạp thuật toán không hữu ích trong việc viết chương trình hiệu quả .
Kyle Strand

13

Từ tổng quan, bạn có thể nghĩ dequenhư mộtdouble-ended queue

tổng quan về deque

Các dữ liệu dequeđược lưu trữ bởi các vectơ kích thước cố định, đó là

được trỏ bởi một map(cũng là một đoạn của vectơ, nhưng kích thước của nó có thể thay đổi)

cấu trúc nội bộ deque

Mã phần chính của deque iteratornhư sau:

/*
buff_size is the length of the chunk
*/
template <class T, size_t buff_size>
struct __deque_iterator{
    typedef __deque_iterator<T, buff_size>              iterator;
    typedef T**                                         map_pointer;

    // pointer to the chunk
    T* cur;       
    T* first;     // the begin of the chunk
    T* last;      // the end of the chunk

    //because the pointer may skip to other chunk
    //so this pointer to the map
    map_pointer node;    // pointer to the map
}

Mã phần chính của dequenhư sau:

/*
buff_size is the length of the chunk
*/
template<typename T, size_t buff_size = 0>
class deque{
    public:
        typedef T              value_type;
        typedef T&            reference;
        typedef T*            pointer;
        typedef __deque_iterator<T, buff_size> iterator;

        typedef size_t        size_type;
        typedef ptrdiff_t     difference_type;

    protected:
        typedef pointer*      map_pointer;

        // allocate memory for the chunk 
        typedef allocator<value_type> dataAllocator;

        // allocate memory for map 
        typedef allocator<pointer>    mapAllocator;

    private:
        //data members

        iterator start;
        iterator finish;

        map_pointer map;
        size_type   map_size;
}

Dưới đây tôi sẽ cung cấp cho bạn mã cốt lõi của deque, chủ yếu là về ba phần:

  1. vòng lặp

  2. Cách xây dựng một deque

1. trình vòng lặp ( __deque_iterator)

Vấn đề chính của iterator là, khi ++, - iterator, nó có thể bỏ qua phần khác (nếu nó trỏ đến cạnh của chunk). Ví dụ, có ba khối dữ liệu: chunk 1, chunk 2, chunk 3.

Các pointer1con trỏ đến điểm bắt đầu chunk 2, khi toán tử --pointernó sẽ trỏ đến cuối chunk 1, để đến pointer2.

nhập mô tả hình ảnh ở đây

Dưới đây tôi sẽ cung cấp cho các chức năng chính của __deque_iterator:

Đầu tiên, bỏ qua bất kỳ đoạn nào:

void set_node(map_pointer new_node){
    node = new_node;
    first = *new_node;
    last = first + chunk_size();
}

Lưu ý rằng, chunk_size()hàm tính kích thước khối, bạn có thể nghĩ nó trả về 8 để đơn giản hóa ở đây.

operator* lấy dữ liệu trong khối

reference operator*()const{
    return *cur;
}

operator++, --

// hình thức tiền tố của sự gia tăng

self& operator++(){
    ++cur;
    if (cur == last){      //if it reach the end of the chunk
        set_node(node + 1);//skip to the next chunk
        cur = first;
    }
    return *this;
}

// postfix forms of increment
self operator++(int){
    self tmp = *this;
    ++*this;//invoke prefix ++
    return tmp;
}
self& operator--(){
    if(cur == first){      // if it pointer to the begin of the chunk
        set_node(node - 1);//skip to the prev chunk
        cur = last;
    }
    --cur;
    return *this;
}

self operator--(int){
    self tmp = *this;
    --*this;
    return tmp;
}
iterator bỏ qua n bước / truy cập ngẫu nhiên
self& operator+=(difference_type n){ // n can be postive or negative
    difference_type offset = n + (cur - first);
    if(offset >=0 && offset < difference_type(buffer_size())){
        // in the same chunk
        cur += n;
    }else{//not in the same chunk
        difference_type node_offset;
        if (offset > 0){
            node_offset = offset / difference_type(chunk_size());
        }else{
            node_offset = -((-offset - 1) / difference_type(chunk_size())) - 1 ;
        }
        // skip to the new chunk
        set_node(node + node_offset);
        // set new cur
        cur = first + (offset - node_offset * chunk_size());
    }

    return *this;
}

// skip n steps
self operator+(difference_type n)const{
    self tmp = *this;
    return tmp+= n; //reuse  operator +=
}

self& operator-=(difference_type n){
    return *this += -n; //reuse operator +=
}

self operator-(difference_type n)const{
    self tmp = *this;
    return tmp -= n; //reuse operator +=
}

// random access (iterator can skip n steps)
// invoke operator + ,operator *
reference operator[](difference_type n)const{
    return *(*this + n);
}

2. Cách xây dựng một deque

chức năng chung của deque

iterator begin(){return start;}
iterator end(){return finish;}

reference front(){
    //invoke __deque_iterator operator*
    // return start's member *cur
    return *start;
}

reference back(){
    // cna't use *finish
    iterator tmp = finish;
    --tmp; 
    return *tmp; //return finish's  *cur
}

reference operator[](size_type n){
    //random access, use __deque_iterator operator[]
    return start[n];
}


template<typename T, size_t buff_size>
deque<T, buff_size>::deque(size_t n, const value_type& value){
    fill_initialize(n, value);
}

template<typename T, size_t buff_size>
void deque<T, buff_size>::fill_initialize(size_t n, const value_type& value){
    // allocate memory for map and chunk
    // initialize pointer
    create_map_and_nodes(n);

    // initialize value for the chunks
    for (map_pointer cur = start.node; cur < finish.node; ++cur) {
        initialized_fill_n(*cur, chunk_size(), value);
    }

    // the end chunk may have space node, which don't need have initialize value
    initialized_fill_n(finish.first, finish.cur - finish.first, value);
}

template<typename T, size_t buff_size>
void deque<T, buff_size>::create_map_and_nodes(size_t num_elements){
    // the needed map node = (elements nums / chunk length) + 1
    size_type num_nodes = num_elements / chunk_size() + 1;

    // map node num。min num is  8 ,max num is "needed size + 2"
    map_size = std::max(8, num_nodes + 2);
    // allocate map array
    map = mapAllocator::allocate(map_size);

    // tmp_start,tmp_finish poniters to the center range of map
    map_pointer tmp_start  = map + (map_size - num_nodes) / 2;
    map_pointer tmp_finish = tmp_start + num_nodes - 1;

    // allocate memory for the chunk pointered by map node
    for (map_pointer cur = tmp_start; cur <= tmp_finish; ++cur) {
        *cur = dataAllocator::allocate(chunk_size());
    }

    // set start and end iterator
    start.set_node(tmp_start);
    start.cur = start.first;

    finish.set_node(tmp_finish);
    finish.cur = finish.first + num_elements % chunk_size();
}

Giả sử i_dequecó 20 phần tử int 0~19có kích thước khối là 8 và bây giờ đẩy 3 phần tử (0, 1, 2) thành i_deque:

i_deque.push_back(0);
i_deque.push_back(1);
i_deque.push_back(2);

Đó là cấu trúc bên trong như dưới đây:

nhập mô tả hình ảnh ở đây

Sau đó đẩy lại, nó sẽ gọi phân bổ đoạn mới:

push_back(3)

nhập mô tả hình ảnh ở đây

Nếu chúng ta push_front, nó sẽ phân bổ đoạn mới trước khi thắngstart

nhập mô tả hình ảnh ở đây

Lưu ý khi push_backphần tử vào deque, nếu tất cả các bản đồ và khối được điền, nó sẽ gây ra phân bổ bản đồ mới và điều chỉnh các đoạn. Nhưng đoạn mã trên có thể đủ để bạn hiểu deque.


Bạn đã đề cập, "Lưu ý khi phần tử Push_back thành deque, nếu tất cả các bản đồ và khối được lấp đầy, nó sẽ gây ra việc phân bổ bản đồ mới và điều chỉnh các đoạn". Tôi tự hỏi tại sao tiêu chuẩn C ++ nói "[26.3.8.4.3] Việc chèn một phần tử duy nhất ở đầu hoặc cuối của một deque luôn mất thời gian không đổi" trong N4713. Phân bổ một mớ dữ liệu mất nhiều thời gian hơn. Không?
HCSF

7

Tôi đã đọc "Cấu trúc dữ liệu và thuật toán trong C ++" của Adam Drozdek, và thấy điều này hữu ích. HTH.

Một khía cạnh rất thú vị của STL deque là việc thực hiện nó. Một deque STL không được triển khai như một danh sách được liên kết mà là một mảng các con trỏ tới các khối hoặc mảng dữ liệu. Số lượng khối thay đổi linh hoạt tùy thuộc vào nhu cầu lưu trữ và kích thước của mảng con trỏ thay đổi tương ứng.

Bạn có thể nhận thấy ở giữa là mảng các con trỏ tới dữ liệu (khối bên phải) và bạn cũng có thể nhận thấy rằng mảng ở giữa đang thay đổi động.

Một hình ảnh đáng giá ngàn lời nói.

nhập mô tả hình ảnh ở đây


1
Cảm ơn bạn đã giới thiệu một cuốn sách. Tôi đọc dequephần này và nó khá hay.
Rick

@Rick rất vui khi nghe điều đó. Tôi nhớ rằng việc đào sâu vào deque tại một số điểm vì tôi không thể hiểu làm thế nào bạn có thể có quyền truy cập ngẫu nhiên (toán tử []) trong O (1). Cũng chứng minh rằng (đẩy / pop) _ (trở lại / phía trước) đã phân bổ độ phức tạp O (1) là một 'khoảnh khắc aha' thú vị.
Keloo

6

Mặc dù tiêu chuẩn không bắt buộc bất kỳ triển khai cụ thể nào (chỉ truy cập ngẫu nhiên theo thời gian không đổi), một deque thường được triển khai như một tập hợp các "trang" bộ nhớ liền kề. Các trang mới được phân bổ khi cần thiết, nhưng bạn vẫn có quyền truy cập ngẫu nhiên. Không giống như std::vector, bạn không hứa rằng dữ liệu được lưu trữ liên tục, nhưng giống như vectơ, việc chèn vào ở giữa đòi hỏi rất nhiều sự di chuyển.


4
hoặc xóa ở giữa đòi hỏi rất nhiều di dời
Mark Hendrickson

Nếu insertyêu cầu nhiều di dời, làm thế nào thí nghiệm 4 ở đây cho thấy sự khác biệt đáng kinh ngạc giữa vector::insert()deque::insert()?
Bula

1
@Bula: Có lẽ do thông tin sai lệch của các chi tiết? Độ phức tạp của chèn deque là "tuyến tính trong số lượng phần tử được chèn cộng với ít hơn khoảng cách đến điểm bắt đầu và kết thúc của deque." Để cảm thấy chi phí này, bạn cần chèn vào giữa hiện tại; đó là những gì điểm chuẩn của bạn đang làm?
Kerrek SB

@KerrekSB: bài viết có điểm chuẩn đã được tham khảo trong câu trả lời của Konrad ở trên. Thật ra tôi không để ý phần bình luận của bài viết dưới đây. Trong chủ đề 'Nhưng deque có thời gian chèn tuyến tính?' tác giả đã đề cập rằng ông đã sử dụng chèn ở vị trí 100 thông qua tất cả các bài kiểm tra, điều này làm cho kết quả dễ hiểu hơn một chút.
Bula
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.