1. 程式人生 > >STL原始碼剖析(四)序列式容器--deque

STL原始碼剖析(四)序列式容器--deque

文章目錄

1. 關於deque

1.1 deque概述

  • deque,即我們常說的佇列,是一個雙向開口的連續線性空間,可以在頭尾兩端分別做元素的插入和刪除操作
  • deque由一段段的定量連續空間組成,稍後再介紹

1.2 deque與vector區別

1. deque允許於常數時間內對起頭端進行元素的插入或移除操作
2. deque沒有所謂的容量觀念
3. 因為deque需要維持其整體連續的假象,導致其實現起來比較繁瑣,為了提高效率,儘可能選擇vector而不是deque

2. deque構成

– 剛剛說到,deque是由一段段的定量連續空間組成,那麼怎樣設計才能使得deque看起來整體連續呢?
答案是採用一塊map(此map非STLmap)作為主控。map是一小塊連續空間,其中每個元素都是指標,指向緩衝區,此緩衝區負責儲存deque的元素

在這裡插入圖片描述

3. deque的迭代器

  • deque迭代器必須要能夠指出緩衝區所在位置,其次要能夠判斷是否在緩衝區的邊緣,還須時刻掌握管控中心map
template <class T, class Ref, class Ptr, size_t BufSize>
struct __deque_iterator {
	//...
	static size_t buffer_size() { return  __deque_buf_size(BufSize, sizeof(T)); }  //定義緩衝區大小
	//...
	typedef T** map_pointer;    //管控中心
T* cur; //此迭代器所指緩衝區中現行元素 T* first; //此迭代器所指緩衝區的頭 T* last; //此迭代器所指緩衝區的尾 map_pointer node; //指向管控中心 //... }; //如果n不為0,表示由使用者自己設定緩衝區大小,就返回n //如果n為0,表示採用預設值: // 如果sz(元素大小)小於512,則緩衝區大小為512 / sz // 如果sz不小於512,則返回1 inline size_t __deque_buf_size(size_t n, size_t sz) { return n != 0 ? n : (sz < 512 ? szie_t(512 / sz) : size_t(1)); }
  • 如下圖所示,一個迭代器的構造:
    在這裡插入圖片描述
  • deque迭代器設計即,將所支援的操作運算子過載,比較簡單,就不詳細分析了

4. deque構造與記憶體管理

deque,除了維護指向map的指標外,也維護start、finish兩個迭代器,分別指向第一緩衝區的第一個元素和最後緩衝區的最後一個元素,當map所提供的節點不足時,就必須重新分配更大的一塊map

  • 以實際例子來解析deque的構造與記憶體管理:
#include <iostream>
#include <deque>
#include <algorithm>
using namespace std; 

int main(int argc, char** argv) {
 	deque<int> ideq(20,9);    //1
	cout << "size = " << ideq.size() << endl;         //size = 20 
	//為每個元素設定初值  
	for (int i = 0;i < ideq.size(); ++i)
		ideq[i] = i;
	//輸出每個元素 
	for (int i = 0; i < ideq.size(); ++i)
		cout << ideq[i] << ' ';                   //0 1 2 ... 19
	cout << endl;
	//在尾端新增3個元素 
	for (int i = 0;i < 3; ++i)       
		ideq.push_back(i);
	//輸出所有元素 
	for (int i = 0; i < ideq.size(); ++i)
		cout << ideq[i] << ' ';                   //0 1 2 ...19 0 1 2
	cout << endl;
	cout << "size = " << ideq.size() << endl;     //size = 23
	//在頭部增加一個元素 
	ideq.push_front(99);
	for (int i = 0; i < ideq.size(); ++i)
		cout << ideq[i] << ' ';                   //99 0 1 2 ... 19 0 1 2
	cout << endl;
	cout << "size = " << ideq.size() << endl;     //size = 24
	return 0;
}

4.1 deque構造

當建立一個deque時,呼叫deque的建構函式:

deque(int n, const value_type& value)
	: start(), finsh(), map(0), map_size(0)
{
	fill_initialize(n, value);   //負責安排deque的資料結構,並設定元素初值
}
//fill_initialize函式實現
template <class T, class Alloc, size_t BufSzie>
void deque<T,Alloc, BufSize>::fill_initialize(size_type n, const value_type& value) {
	create_map_and_nodes(n);     //負責安排deque的結構 
	map_pointer = cur;
	__STL_TRY {
		//為每個節點設定初始值 
		for (cur = start.node; cur < finish.node; ++cur){
			uninitialized_fill(*cur, *cur + buffer_size(), value);
		}
		//最後一個節點的尾端可能有備用空間,不必設初值 
		uninitialized_fill(finish.first, finish.cur, value); 
	}
	catch(...) {
		//...
	}
}
//create_map_and_nodes函式實現
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::create_map_and_nodes(size_type num_elements)
{
    // 需要的結點數, 元素個數 / 每個緩衝區能容納的元素數 + 1
  // 這裡如果能整除,會多分配一個
  size_type num_nodes = num_elements / buffer_size() + 1;
 
  // map要維護的結點, 這裡最小的值為8,最多為所需節點數+2,前後各留一個以便擴充
  map_size = max(initial_map_size(), num_nodes + 2);
  // 呼叫deque專屬空間配置器,配置map空間
  map = map_allocator::allocate(map_size);
 
  // 將[nstart, nfinish)區間設定在map的中間,
  // 這樣就能保證前後增長而儘可能減少map的重新分配次數
  map_pointer nstart = map + (map_size - num_nodes) / 2;
  map_pointer nfinish = nstart + num_nodes - 1;
 
  // 分配結點空間
  map_pointer cur;
  __STL_TRY {
    for (cur = nstart; cur <= nfinish; ++cur)
        // 為每一個map指標指向的緩衝區的每一個元素分配記憶體空間 
      *cur = allocate_node();
  }
 
  // 維護指標狀態,為deque的兩個迭代器start和finish賦初值
  start.set_node(nstart);
  finish.set_node(nfinish);
  start.cur = start.first;
  finish.cur = finish.first + num_elements % buffer_size();
}

4.2 push_back元素操作

先來簡短分析一下push_back的步驟

  1. 呼叫push_back,如若最後一個緩衝區尚有兩個以上的備用空間,則直接在備用空間構造新元素
  2. 只剩一個元素備用空間時,呼叫push_back_aux配置新緩衝區
  3. 呼叫push_back_aux,先行呼叫reserve_map_at_back()判斷是否需要更換map,如果需要則呼叫reallocate_map()進行map的更換,不需要則什麼也不做
  4. 在新緩衝區中構造元素並更改finish狀態
  • 以下是push_back函式實現:
public:
	void push_back(const value_type& t) {
		if (finish.cur != finish.last - 1) {    //如果最後一個緩衝區還有兩個以上的元素備用空間
			construct(finish.cur, t);    //則直接在備用空間構造元素
			++finish.cur;       //調整最後緩衝區使用狀態
		}
		else   //即只有一個元素備用空間呼叫該函式
			push_back_aux(t);   //配置一塊新緩衝區
	}
//只剩一個元素備用空間
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::push_back_aux(const value_type& t)
{
  value_type t_copy = t;
  // 判斷是否需要調整map空間
  reserve_map_at_back();
  *(finish.node + 1) = allocate_node(); // 配置一塊新的緩衝區
  __STL_TRY {
    construct(finish.cur, t_copy);  // 構造新加入的元素
    finish.set_node(finish.node + 1);   // 調整finish
    finish.cur = finish.first;       //令cur指向新緩衝區
  }
  __STL_UNWIND(deallocate_node(*(finish.node + 1)));
}
//reverse_map_at_back()函式實現:
void reverse_map_at_back(size_type nodes_to_add = 1)
{
	if (nodes_to_add + 1 > map_size  - (finish.node - map)  //map尾端備用空間節點不足,則呼叫下面函式更換map
		reallocate_map(nodes_to_add, false);   //稍後解析
}

4.3 push_front元素操作

先來簡短分析一下push_front的步驟

  1. 呼叫push_front,如若第一緩衝區尚有備用空間,則直接在備用空間構造新元素
  2. 沒有元素備用空間時,呼叫push_front_aux配置新緩衝區
  3. 呼叫push_front_aux,先行呼叫reserve_map_at_front()判斷是否需要更換map,如果需要則呼叫reallocate_map()進行map的更換,不需要則什麼也不做
  4. 在新緩衝區中構造元素並更改start狀態
public:
	void push_front(const value_type& t)  {
		if (start.cur != start.first)  {     //尚有備用空間
			construct(start.cur - 1, t);   //直接構造元素
			--start.cur;         //調整緩衝區使用狀態
		}
		else
			push_front_aux(t);   //第一緩衝區無備用空間則呼叫
	}
//沒有備用空間
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::push_front_aux(const value_type& t)
{
 	value_type t_copy = t;
  	reserve_map_at_front();   // 同push_back(),檢查是否需要調整map
  	*(start.node - 1) = allocate_node();  // 配置一塊新的緩衝區
  	__STL_TRY {
   		 start.set_node(start.node - 1); // 調整start
   		 start.cur = start.last - 1;
  		  construct(start.cur, t_copy);
 	 }
  	catch (...) {
   		  start.set_node(start.node + 1);
 		  start.cur = start.first;
		  deallocate_node(*(start.node - 1));
  		  throw;
	}
}
//reserve_map_at_front函式實現
void reverse_map_at_front(size_type nodes_to_add = 1)  {
	if (nodes_to_add > start.node - map)  //map前端節點不足則呼叫下面函式進行更換map
		reallocate_map(nodes_to_add, true);
}

4.4 reallocate_map函式實現

主要分兩種情況進行分析:
1. 如果只是一端的節點使用完了,而另一端還剩餘很多空間,則在原map進行調整
2. 實在沒有空間可供調整,那就配置一塊新map,將原map的內容拷貝過來,釋放掉原來map的空間

  • 下面是函式實現:
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::reallocate_map(size_type nodes_to_add,
    bool add_at_front)
{
  size_type old_num_nodes = finish.node - start.node + 1;  //原節點數
  size_type new_num_nodes = old_num_nodes + nodes_to_add;   //新節點數
 
  map_pointer new_nstart;
  // 此處為了防止出現一端已經用完,另一端卻還有很多剩餘的情況
  if (map_size > 2 * new_num_nodes) {
    // 調整新的map中的起始點
    new_nstart = map + (map_size - new_num_nodes) / 2
                 + (add_at_front ? nodes_to_add : 0);  //根據傳入的bool值進行調整
    // 如果前端剩餘很多
    if (new_nstart < start.node)
      copy(start.node, finish.node + 1, new_nstart);
    else   // 尾端剩餘很多
      copy_backward(start.node, finish.node + 1, new_nstart + old_num_nodes);
  }
  else {    // map不夠用了,就需要配置一塊更大的map
    size_type new_map_size = map_size + max(map_size, nodes_to_add) + 2;
    // 配置一塊大的map
    map_pointer new_map = map_allocator::allocate(new_map_size);
    // 始終要使start和finish處在map空間的中間
    new_nstart = new_map + (new_map_size - new_num_nodes) / 2
                 + (add_at_front ? nodes_to_add : 0);
    // 拷貝到新的map空間中去
    copy(start.node, finish.node + 1, new_nstart);
    // 釋放舊的空間
    map_allocator::deallocate(map, map_size);
    // 設定map的起始地址和大小
    map = new_map;
    map_size = new_map_size;
  }
  // 調整新的start和finish
  start.set_node(new_nstart);
  finish.set_node(new_nstart + old_num_nodes - 1);
}

5. deque的元素操作

  • 以下是一些元素操作,push_back與push_front上述已經解析了,所以在這就不再重複提及:
操作 功能
pop_back 將尾端元素去掉,若最後緩衝區沒有元素則會呼叫pop_back_aux進行緩衝區的釋放
pop_front 將首部元素去掉,若第一緩衝區只有一個元素則呼叫pop_front_aux進行緩衝區的釋放
clear 用來清除整個deque,會保留一個緩衝區
erase 1.清除某個元素,返回被刪元素位置的迭代器 2.用來清除[first,last)之間的元素
insert 1.若插入點是deque的最前端,交給push_front去做 2.如果插入點是最尾端,則交給push_back去做 3. 否則交給insert_aux

5.1 關於erase

首先理清以下erase的思路:
erase單個元素:
1. 如果清除點之前的元素較少,則向後移動清除點之前的元素
2. 如果清除點之後的元素較少,則向前移動清除點之後的元素

erase[first,last)區間元素:
1. 清除的是整個deque,則呼叫clear
2. 清除的是部分空間:
2.1 如果清除點之前的元素較少,則向後移動清除點之前的元素
2.2 如果清除點之後的元素較少,則向前移動清除點之後的元素

  • 以下是函式實現:
//清除單個元素
iterator erase(iterator pos)
{
  iterator next = pos;
  ++next;
 
  // 計算待擦除點前的元素個數
  difference_type index = pos - start;
 
  // 判斷待擦除結點前後元素的個數, 哪部分少就移動哪部分
  if (index < (size() >> 1))
  {
    // 前面部分的元素少
    copy_backward(start, pos, next);  
    pop_front();
  }
  // 後面部分的元素少
  else {
    copy(next, finish, pos); 
    pop_back();
  }
  return start + index;
}
//清除一個區間
template <class T, class Alloc, size_t BufSize>
deque<T, Alloc, BufSize>::iterator
deque<T, Alloc, BufSize>::erase(iterator first, iterator last)
{
  if (first == start && last == finish) {   // 需要擦除整個deque
    clear();
    return finish;
  }
  else {
    difference_type n = last - first;   // 清除區間的長度
    difference_type elems_before = first - start;  // 待清除區間前方的元素個數
    if (elems_before < (size() - n) / 2) {  // 如果前方的元素個數較少
      copy_backward(start, first, last);    // 向後移動前方元素
      iterator new_start = start + n;     // 調整新的起始點
      destroy(start, new_start);          // 全域性函式,析構節點元素
      for (map_pointer cur = start.node; cur < new_start.node; ++cur)
        data_allocator::deallocate(*cur, buffer_size());   // 釋放緩衝區空間
      start = new_start;
    }
    else {    // 後方元素比較少的情況
      copy(last, finish, first);    // 向前移動後方元素
      iterator new_finish = finish - n; // 調整新的finish迭代器
      destroy(new_finish, finish);      // 全域性函式,析構節點元素
      for (map_pointer cur = new_finish.node + 1; cur <= finish.node; ++cur)
        data_allocator::deallocate(*cur, buffer_size());  // 釋放緩衝區空間
      finish = new_finish;
    }
    return start + elems_before;
  }
}

5.2 關於insert_aux

實現原理:
1. 判斷插入點之前的元素個數
2. 如插入點之前的元素個數較少,則將插入點之前的元素(包括插入點的元素)向後移動一位,將新元素x插入到插入點
3. 如插入點之後的元素個數較少,則將插入點之後的元素(包括插入點的元素)向前移動一位,將新元素x插入到插入點

  • 以下是函式實現:
template <class T, class Alloc, size_t BufSize>
typename deque<T, Alloc, BufSize>::iterator
deque<T, Alloc, BufSize>::insert_aux(iterator pos, const value_type& x)
{
  difference_type index = pos - start;  // 插入元素前面的元素個數
  value_type x_copy = x;
 
  if (index < size() / 2) {  // 如果前端的元素比較少
    push_front(front());  // 在最前面插入一個與第一個元素一樣的數
    iterator front1 = start;  // 記錄起始點
    ++front1;
    iterator front2 = front1; 
    ++front2;
    pos = start + index;
    iterator pos1 = pos;
    ++pos1;
    copy(front2, pos1, front1); // 拷貝空間,將[front2,pos1)拷貝到front1
  }
  else {   // 後端的元素比較少,原理同上
    push_back(back());
    iterator back1 = finish;
    --back1;
    iterator back2 = back1;
    --back2;
    pos = start + index;
    copy_backward(pos, back2, back1);  //拷貝空間,將[pos, back2]拷貝到back1
  }
  *pos = x_copy;
  return pos
            
           

相關推薦

STL原始碼剖析序列容器--deque

文章目錄 1. 關於deque 1.1 deque概述 1.2 deque與vector區別 2. deque構成 3. deque的迭代器 4. deque構造與記憶體管理 4.1 deque構造

STL原始碼剖析序列容器--slist

文章目錄 1. slist概述 2. slist的節點及迭代器 2.1節點設計: 2.2 迭代器設計: 3. 資料結構 4. 元素操作 1. slist概述 slist,即我們資料結構課程中

STL原始碼剖析序列容器--list

文章目錄 1. 關於list 1.1 首先在介紹list之前先來觀察一下list的節點結構: 1.2 與vector的區別 2. list的迭代器 3. list的資料結構 4. list記憶體構造 5. list

STL 原始碼剖析序列容器--vector

1. 寫在前面 之後的幾篇主要記錄一些關於序列式容器中重要的一些知識點以及疑難雜症,並不特別詳細分析。 2. vector 2.1 與array的區別 array是靜態空間,一旦配置就不能改變,要更換時需要自己來更改:首先配置一塊新空間,將元素從舊址搬到

STL原始碼剖析序列容器vector,list

容器分為: 序列式容器:array,vector(用演算法呈現heap(由heap實現priority-queue)),list,deque(配接器(stack,queue)) 關聯式容器:RB-tree(set,map,multiset,multimap),hashtab

STL原始碼剖析關聯容器--【set、multiset、map、multimap】

文章目錄 1. 寫在前面 2. set 2.1 set性質 2.2 set實現 2.3 multiset 3. map 3.1 map性質 3.2 pair 3.3 map實現

STL原始碼剖析heap--priority_queue

文章目錄 1. heap概述 2. 為何選擇heap作為priority queue的底層機制? 3. binary heap 4. heap演算法 5. priority_queue 1. heap概述 heap,

STL原始碼剖析容器介面卡--stack、queue

文章目錄 1. 寫在前面 2. stack--queue 2.1 satck概述 2.1.1 stack實現 2.1.2 stack迭代器 2.2 queue概述 2.2.1 qu

未完成STL學習筆記3序列容器 Sequence Containers

1. vector vector與陣列array十分相似,但array是靜態空間,而vector是動態空間,可以通過內部機制自行擴充空間,具有很好的靈活性。 其實現的關鍵在於對大小的控制和重新配置時的資料移動效率。 vector的型別定義如下: templat

STL原始碼剖析迭代器與traits程式設計

文章目錄 1. 迭代器概念 1.1 基本概念 1.2 迭代器設計理念 2. 引出traits程式設計 3. traits程式設計 3.1 traits程式設計技術 3.2 partial special

STL原始碼剖析空間配置器

歡迎大家來訪二笙的小房子,一同學習分享生活! 文章目錄 1. 寫在前面 2. SGI空間配置器 2.1 SGI標準空間配置器 2.2 SGI特殊的空間配置器,std::alloc 2.3 構造和析構基本工具 2.4 空間

STL原始碼剖析【hash_set、hash_map】

hash_set 與set區別及聯絡 與set大同小異,set以RB-tree作為其底層,而hash_set以hash table作為其底層 兩者都是藉由其底層操作完成我們所看到的那些操作 二者最大的不同在於set的底層RB-tree有自動排序功能,所以反映在se

STL原始碼剖析hashtable

文章目錄 1. hashtable概述 1.1 線性探測 1.2 二次探測 1.3 開鏈法 2. hashtable的桶與節點 3. hashtable迭代器 4. hashtable資料結構 5. has

STL原始碼剖析

opp(Object-Oriented Programming)vs GP(Generic Programming) OPP:企圖將datas和methods分開來 GP:企圖將datas和methods分開來 分開的好處: (1)containers和algorith

STL原始碼剖析

歡迎大家來訪二笙的小房子,一同學習分享生活! 寫在前面 學習STL,瞭解STL的歷史與發展,深度剖析STL原始碼,提高自己的程式設計能力!!! 1.瞭解STL 1.1 STL概述 STL誕生:為了建立資料結構和演算法的一套標準,並且降低其間的耦合關係以提

Lua原始碼剖析

前面三篇請看我前面的 blog 這篇主要來分析lua的虛擬機器的實現,我看的程式碼依舊是5.1 因此首先從luaL_loadfile開始,這個函式我們知道是在當前的lua state載入一個lua檔案,其中第二個引數就是filename。 其中LoadF結構很簡單,它用來表示一個load file: stru

STL原始碼剖析

演算法 從語言的角度看: 容器 Container 是一個class template 演算法 Algorithm 是一個function template 迭代器 Iterator 是一個class template 函式式 Functor 是一個class template 介面

verilog入門-----表達

開始 處理 lec parameter spa 方式 param mage ... 表達式有操作數和操作符組成。 操作數 操作數分為:常數、參數、線網、寄存器、位選擇、部分選擇、存儲器單元、函數調用 常數:表達時間中的整數值可被解釋為有符號數或無符號數。如果表達式中是

Flume NG原始碼分析使用ExecSource從本地日誌檔案中收集日誌

常見的日誌收集方式有兩種,一種是經由本地日誌檔案做媒介,非同步地傳送到遠端日誌倉庫,一種是基於RPC方式的同步日誌收集,直接傳送到遠端日誌倉庫。這篇講講Flume NG如何從本地日誌檔案中收集日誌。 ExecSource是用來執行本地shell命令,並把本地日誌檔案中的資料封裝成Event

OpenCV學習筆記30KAZE 演算法原理與原始碼分析KAZE特徵的效能分析與比較

      KAZE系列筆記: 1.  OpenCV學習筆記(27)KAZE 演算法原理與原始碼分析(一)非線性擴散濾波 2.  OpenCV學習筆記(28)KAZE 演算法原理與原始碼分析(二)非線性尺度空間構