STL之deque容器的實現架構

來源:互聯網
上載者:User

標籤:des   style   class   blog   code   http   

說明:本文僅供學習交流,轉載請標明出處,歡迎轉載!

       vector底層採用的是一個數組來實現,list底層採用的是一個環形的雙向鏈表實現,而deque則採用的是兩者相結合,所謂結合,並不是兩種資料結構的結合,而是某些效能上的結合。我們知道,vector支援隨機訪問,而list支援常量時間的刪除,deque支援的是隨機訪問以及首尾元素的刪除

        deque是double ended queue的縮寫,讀作deck。首先我們用一個圖來說明deque底層所採用的資料結構。


        這個資料結構有一種似曾相識的感覺吧!沒錯,你好像在哪裡看過:你可以將這個資料結構變相地看成是一個二維數組,可將map看成是該二維數組的數組名;當然,你也可以將該資料結構與當初我們學習OS時的記憶體配置中的分頁式儲存管理相結合。這樣,你會發現,其實知識都是相同的,這就是知識運用能力和遷移能力的體現吧。

        好了,說了那麼多,下面我們給出deque容器的設計思路吧。

        (1)迭代器的設計。與list容器相類似的地方是,我們需要重新規划下deque迭代器的定義,從中我們也很容易可以看出,deque容器採用的就是一種分段連續的儲存空間,既然是分段連續,那麼支援隨機訪問的deque容器就需要對迭代器做特殊地處理,首先我們給出迭代器的設計圖


       從可以看出,deque的迭代器由四個屬性群組成,這四個屬性群組成了我們隨機訪問一個容器內元素的必要成分。我們再介紹利用該迭代器隨機訪問容器內的元素時,先介紹下我們是如何隨機訪問一個二維數組a[i][j]的。我們知道,對一個二維數組,我們首先需要找到第一維i的入口地址,找到該入口地址後,我們再去找對應j的地址,所以對於數組a[i][j]的另外一種訪問方式是*(*(a+i)+j),當然我們再訪問這個一個數組元素的時候,一定要注意不要越界,否則會引起嚴重的運行錯誤。根據這一點,我們每次訪問一個數組元素的時候,都會判斷該元素的下標是否越界。類似地,deque容器的迭代器每一個組成部分都有著重要的不可缺少的作用。中,node用於指向的“第一維”的某個位置,該位置對應要訪問元素的入口地址,剩下的三個屬性則用於“第二維”,[first,last)規定了訪問本區間元素的邊界條件,而curr則指向實際我們要訪問的元素。當然,所有對該類迭代器的訪問操作都必須建立在相應運算子的重載上。下面我們給出deque迭代器的實現架構:

/***********deque迭代器的設計***********/template<class T,class Ref=T&,class Ptr=T*,size_t Bufsize=0>//Bufsize表示緩衝區的大小struct _deque_iterator{typedef _deque_iterator<T,T&,T*,Bufsize> iterator;//定義迭代器的類型typedef _deque_iterator<T,const T&,const T*,Bufsize> const_iterator;//定義常迭代器的類型,可以看出只是對引用和指標加以const限定static size_t buffer_size();//返回緩衝區的大小,以個為單位/************迭代器的幾個類型別名*********/typedef random_access_iterator_tag iterator_category;//deque迭代器的tag為隨機訪問迭代器typedef T value_type;//元素類型typedef Ptr pointer;//指標類型typedef Ref reference;//參考型別typedef size_t size_type;//大小類型typedef ptrdiff_t difference_type;//指標差實值型別typedef T** map_pointer;//想到二維數組的數組名就是一個二級指標    map_pointer node;//node可以看成是第一維    /*************deque迭代器包含的幾種重要的屬性******/T *first;//緩衝區開始處,可看成是第二維第一個元素的位置T *last;//緩衝區末端的下一個位置,第二維的最後元素的下一個位置T *curr;//可以看成是原生指標,表示當前的元素位置//如果是第一個塊緩衝區,則指向當前實際的元素位置;//如果是最後一個緩衝區,則指向當前實際元素的下一個位置,符合STL中[...)的原則/************迭代器的操作*************/reference operator *()const;//return *current;定義解引用操作reference operator ->()const;//return current;定義箭頭操作符difference_type operator-(const iterator & x)const;//定義兩迭代器相減的操作typedef iterator self;bool operator==(const self &x)const;//判斷兩個迭代器是否相等bool operator!=(const self &x)const;//判斷兩個迭代器是否不相等bool operator<(const self &x)const;//先比較第一維,相同則再比較第二維void set_node(map_pointer new_node);//將當前的迭代器設定為new_node,主要是設定node、first、last屬性的值self& operator++();//定義前置自加,++p,返回引用,因為是return *thisself operator++(int);//定義後置自加,p++,返回非引用,因為是return temp,不能返回局部對象的引用self& operator--();//定義前置自減,--p,返回引用self operator--(int);//定義後置自減,p--,返回非引用self& operator+=(difference_type n);//將本迭代器自加n步,隨機訪問迭代器的屬性self& operator-=(difference_type n);//本迭代器自減n步,隨機訪問迭代器的屬性self operator-(difference_type n);//返回一個將本迭代器減n後的temp值,隨機訪問迭代器的屬性reference operator[](difference_type n)const;//定義下表訪問功能,隨機訪問迭代器的屬性};
       deque容器的兩個重要的迭代器

        1. start:綁定到第一個有效map結點和該結點對應的緩衝區。當然STL將有效元素防止中間,預留空間放兩邊,這樣做是為了更好地實現首尾的操作。

        2.finish:綁定到最後一個有效map結點(不是下一個位置哈,別被end()給弄傻了)和該結點對應的緩衝區。

        具體如所示:


---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


    總結:1.deque容器所有緩衝區的容量都是一樣的,範圍都是:[first,last)。

             2.deque容器中,必定包括兩個迭代器,分別是:start和finish, map元素個數(第一維)>=2時,start綁定到第一個有效map結點及其對應的緩衝區,finish當定到最後一個有效map結點及其對應的緩衝區。

             3.start.curr和finish.cur嚴格遵守STL的原則,左閉右開元組[...),意思是說:start.cur指向的是其所綁定緩衝區第一個有效元素,指標start.cur的移動方向是first<-------last;而finish.cur指向的是其所綁定緩衝區的最後一個有效元素的下一個位置,指標finish.cur的移動方向是first------->last。記住:start.cur指向的永遠是一個含有實際值的位置,而finish.cur永遠指向的是一個空位置。

            4.start所綁定的緩衝區已滿的條件是start.cur=start.first,而當該緩衝區為空白start.cur=start.last時,start會自動綁定到別的緩衝區(刪除最後一個有效元素時發生);finish所綁定的緩衝區已滿的條件finish.cur=finish.last,為空白的條件是finish.cur=cur.first。

     (2)deque容器的設計。最上面的那個給出了deque容器的結構,我要說明的是,與vector相比,deque容器多出了pop_front()和push_fron()操作這兩個首部的增刪操作;而與list相比,deque多出了對容器元素的隨機訪問功能。然而,任何方便的操作都需要背後複雜資料結構的支撐,與前兩個容器相比,deque容器在中間插入和刪除操作是相當複雜的。在正式給出deque的代碼架構之前,我首先介紹下deque容器的插入操作和刪除操作中,內部所做的事情。

        deque容器的插入操作

        deque容器的插入操作分三種情況。

      (1)在容器的首部插入,push_front(const T &t)。先找到start綁定的緩衝區,判斷該緩衝區是否還有空閑位置(若start.curr==start.first,則表示沒有空閑位置,否則表示有。因為頭部的curr指向的是含有實際值的位置,而尾部的curr指向的是含有實際值的下一個位置,這符合stl的規則,即左閉由開的原則,[..)),如果有則在該空閑位置插入元素t,同時更新start.curr,若沒有空閑位置可以插入元素t,則在start.node(這是第一維)的前一個位置申請一個緩衝區,同時將start綁定到該新緩衝區(即設定start的first,last),並將start.cur指向新申請緩衝區的最後一個有效位置(即star.cur=star.last-1)。

    (2)在容器的尾部插入,push_back(const T& t)。先找到finish所綁定的緩衝區,判斷該緩衝區是否有空閑位置(若finish.curr==finish.last,則表示沒有空閑位置,否則表示有。因為finish的curr指向的是含有實際值的下一個位置,last表示最後一個實際值的下一個位置,若兩者相等,則表示最後一個空閑位置用完了)。

     (3)在容器的指定位置前插入元素,insert(iter,t)、insert(iter,n,t)、insert(iter,b,e)。這裡,我們將容器空間分成三個部分(前面部分,iter,後面部分),在插入之前,我們需要比較前面部分元素的個數與後面部分元素的個數,由於指定位置的插入必然會引起元素的移動(首、尾插入除外),那麼我們移動的是前後兩部分中,元素個數比較少的那部分元素,這樣做的效率會更高,STL就是凡事以效率為前提

       需要特別指的注意的是,在首部或尾部的插入操作中,有可能引發在插入端沒有空餘的map空間了,這個時候我們需要特別注意,STL的做法並不是因為該操作端沒有閒置空間了就直接重新申請新的空間,而是先判斷下另一端是否有足夠的空間(SGI STL的判斷標準是 map_size>2*已使用的map個數),如果空間足夠,則不需要重新分配map,只需要移動下原來已有map元素的指標的位置即可。如果不夠,則重新分配(三部曲:申請空間、複製元素、刪除舊空間)。

         deque容器的刪除操作

      (1)刪除首部元素,pop_front()。若start所綁定的緩衝區上有多個(兩個及以上)元素,此時只需將第一個有效元素析構即可destroy(start.curr),析構完後再將start.cur++;若start所綁定的緩衝區上只有一個元素,即start.curr==start.last-1,則將該元素析構後destory(start.cur),需要先將start綁定的緩衝區的空間收回deallocate_node(start.first),再將start重新綁定map結點(相當於第一維)和緩衝區(相當於第二維)start.set_node(start.node+1),start.cur=start.first。

     (2)刪除尾部元素,pop_back()。若finish所綁定的緩衝區上有1個或多個元素,此時只需將最後一個有效元素析構即可destroy(--finish.curr);若start所綁定的緩衝區是空緩衝區,即finish.curr==finish.first,則先將該空緩衝區的空間釋放掉deallocate_node(finish.first),再將finish綁定到上一個map結點和緩衝區finish.set_node(finish.node-1),finish.cur=finish.last-1,最後將最後一個元素析構destroy(finish.cur)。

     總結:start的cur一定是指向一個有效元素,而finish的cur一定是指向最後一個有效元素的下一個位置,也就是說finish的cur指向的一定是一個空位置。所以,start.cur所綁定的緩衝區為空白時,一定要重新為其綁定一個新的緩衝區,不過有沒有其他動作請求;而即便finish.cur所綁定的緩衝區為空白時,如果沒有其他動作的請求,無需為finish綁定新的緩衝區。

     (3)指定位置刪除元素,erase(iter),erase(b,e)。這裡我只想提下與插入操作相似的點,那就是刪除元素之後,一定會涉及到移動操作,是移動左邊部分的元素還是右邊部分的元素呢?這取決於兩者元素的個數。STL從效率的角度考慮,只會移動元素個數少的那部分。

       下面總結下使得deque迭代器失效的操作。

       使deque迭代器失效的操作

       1.在deque的首部或尾部插入操作時,通常情況下迭代器是不會失效的,但是也有例外。例外情況是,當在容器的首部或尾部做頻繁的插入操作使得deque的兩端的預留空間用得差不多時(還記得上面的判斷條件SGI STL的判斷標準是 map_size>2*已使用的map個數嗎)會引起記憶體空間的重新設定(三部曲),這種特殊情況下容器內所有的迭代器都將失效。

       2.在deque的首部或尾部做刪除操作時,除了被刪除元素所對應的迭代器外,其他任何元素對應的迭代器都不會失效。

       3.在deque的中間做插入或刪除操作時,可能會使得被插入位置或刪除位元置的左邊所有迭代器失效,也可能使得其右邊的所有迭代器失效,具體那邊的迭代器失效得視左右兩邊元素的個數而定,元素個數少的那邊的所有迭代器都會失效,而元素個數多的那邊的所有迭代器都不會失效。這裡也是我跟C++primer不一樣的地方,《C++primer 第4版 中文版》P282中提到,對於deque容器,如果刪除時不包含第一個元素或最後一個元素,那麼該queue容器相關的所有迭代器都會失效,我覺得這句話是錯誤的,應該視具體情況而定,正如我分析的那樣。

      最後,給出deque容器的實現架構:

/***************deque容器的定義**********************/template<class T,class Alloc=alloc,size_t Buffsiz=0>class deque{public:/***********定義公有訪問屬性****************/typedef T value_type;//定義元素類型typedef value_type *pointer;//定義指標類型typedef T& reference;//定義參考型別typedef ptrdiff_t difference_type;//定義迭代器才差實值型別typedef size_t size_type;//定義大小類型typedef _deque_iterator<T,T&,T*,BuffSiz> iterator;//迭代器類型        typedef _deque_iterator<T,const T&,const T*,BuffSiz>const_iterator;//指向常量的迭代器類型typedef deque<T,Alloc,Buffsize> self;//定義容器類型/***********建構函式/解構函式*****************/deque();//無參建構函式,將其map和map_size設定為0deque(const self & deq);//定義複製建構函式deque(Input_Iterator b,Input_Iterator e);//[b,e)可以是任意容器的迭代器deque(size_type n,const T &t);//用n個初值t去建立容器deque(size_type n);//建立含有n個元素的容器~deque();//解構函式的定義/*************插入操作*********************/void push_back(const T &);//後插入void push_front(const T &);//前插入iterator insert(iterator iter,const T &t);//在iter前插入,返回新插入元素的位置void insert(iterator iter,iterator b,iterator e);//將[b,e)範圍內的元素插入到iter之前void insert(iterator iter,size_type n,const T& t);//在iter前插入n個初值為t的元素/*************刪除操作********************/iterator erase(iterator iter);//刪除iter所指向元素,返回該元素對應的下一個元素的迭代器iterator erase(iterator b,iterator e);//刪除[b,e)範圍內的元素,返回原先e的位置void clear();//刪除容器內的所有元素void pop_back();//返回容器內最後一個有效元素void pop_front();//返回容器內第一個有效元素/*************大小操作*******************/size_type size()const;//返回容器內元素的格式size_type max_size()const;//返回容器可容納的最多元素的個數bool empty()const;//判斷容器是否為空白void resize(size_type n);//將容器的大小設定為nvoid resize(size_type n,const T t);//將容器的大小設定為n,容需要新添加元素,則其初值為t/*************訪問操作*****************/iterator begin()const;//返回頭指標iterator end()const;//返回末端元素的下一個位置iterator rbegin()const;//返回最後一個有效元素iterator rend()const;//返回第一個有效元素的前一個位置reference front()const;//返回第一個元素的引用reference back();//返回最後一個元素的引用reference operator [](size_type i);//返回第i個元素的引用refrence at(size_type i);//返回第i個元素的引用protected:typedef T** map_pointer;//定義指向map類型(相當於第一維)的類型別名iterator start;//指向map(第一維)的第一個元素iterator finish;//指向map(第二維)的最後一個有效元素map_pointer map;//相當於二維數組的首地址size_type map_size;//定義第一維的元素個數};

參考文獻:

[1]《STL源碼剖析 侯捷》

[2]《C++ primer 第4版》

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.