0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

一文帶你手撕 STL 容器源碼(下)

ss ? 來源:CSDN技術(shù)社區(qū) ? 作者:herongweiV ? 2021-04-30 16:12 ? 次閱讀

distance(begin(), end(), result);

return result;

}

size_type max_size() const { return size_type(-1); }

// 返回第一個元素的值

reference front() { return *begin(); }

const_reference front() const { return *begin(); }

// 返回最后一個元素的值

reference back() { return *(--end()); }

const_reference back() const { return *(--end()); }

// 交換

void swap(list《T, Alloc》& x) { __STD::swap(node, x.node); }

。。。

};

template 《class T, class Alloc》

inline void swap(list《T, Alloc》& x, list《T, Alloc》& y) {

x.swap(y);

}

list 的頭插和尾插

因為 list 是一個循環(huán)的雙鏈表, 所以 push 和 pop 就必須實現(xiàn)是在頭插入,刪除還是在尾插入和刪除。

在 list 中,push 操作都調(diào)用 insert 函數(shù), pop 操作都調(diào)用 erase 函數(shù)。

template 《class T, class Alloc = alloc》

class list {

。。。

// 直接在頭部或尾部插入

void push_front(const T& x) { insert(begin(), x); }

void push_back(const T& x) { insert(end(), x); }

// 直接在頭部或尾部刪除

void pop_front() { erase(begin()); }

void pop_back() {

iterator tmp = end();

erase(--tmp);

}

。。。

};

上面的兩個插入函數(shù)內(nèi)部調(diào)用的 insert 函數(shù)。

class list {

。。。

public

// 最基本的insert操作, 之插入一個元素

iterator insert(iterator position, const T& x) {

// 將元素插入指定位置的前一個地址

link_type tmp = create_node(x);

tmp-》next = position.node;

tmp-》prev = position.node-》prev;

(link_type(position.node-》prev))-》next = tmp;

position.node-》prev = tmp;

return tmp;

}

這里需要注意的是

節(jié)點實際是以 node 空節(jié)點開始的。

插入操作是將元素插入到指定位置的前一個地址進行插入的。

刪除操作

刪除元素的操作大都是由 erase 函數(shù)來實現(xiàn)的,其他的所有函數(shù)都是直接或間接調(diào)用 erase。

list 是鏈表,所以鏈表怎么實現(xiàn)刪除元素, list 就在怎么操作:很簡單,先保留前驅(qū)和后繼節(jié)點, 再調(diào)整指針位置即可。

由于它是雙向環(huán)狀鏈表,只要把邊界條件處理好,那么在頭部或者尾部插入元素操作幾乎是一樣的,同樣的道理,在頭部或者尾部刪除元素也是一樣的。

template 《class T, class Alloc = alloc》

class list {

。。。

iterator erase(iterator first, iterator last);

void clear();

// 參數(shù)是一個迭代器 修改該元素的前后指針指向再單獨釋放節(jié)點就行了

iterator erase(iterator position) {

link_type next_node = link_type(position.node-》next);

link_type prev_node = link_type(position.node-》prev);

prev_node-》next = next_node;

next_node-》prev = prev_node;

destroy_node(position.node);

return iterator(next_node);

}

。。。

};

。。。

}

list 內(nèi)部提供一種所謂的遷移操作(transfer):將某連續(xù)范圍的元素遷移到某個特定位置之前,技術(shù)上實現(xiàn)其實不難,就是節(jié)點之間的指針移動,只要明白了這個函數(shù)的原理,后面的 splice,sort,merge 函數(shù)也就一一知曉了,我們來看一下 transfer 的源碼:

template 《class T, class Alloc = alloc》

class list {

。。。

protected:

void transfer(iterator position, iterator first, iterator last) {

if (position != last) {

(*(link_type((*last.node).prev))).next = position.node;

(*(link_type((*first.node).prev))).next = last.node;

(*(link_type((*position.node).prev))).next = first.node;

link_type tmp = link_type((*position.node).prev);

(*position.node).prev = (*last.node).prev;

(*last.node).prev = (*first.node).prev;

(*first.node).prev = tmp;

}

}

。。。

};

上面代碼的七行分別對應(yīng)下圖的七個步驟,看明白應(yīng)該不難吧。

a938d4c0-a83e-11eb-9728-12bb97331649.png

另外 list 的其它的一些成員函數(shù)這里限于篇幅,就不貼出源碼了,簡單說一些注意點。

splice函數(shù): 將兩個鏈表進行合并:內(nèi)部就是調(diào)用的 transfer 函數(shù)。

merge 函數(shù): 將傳入的 list 鏈表 x 與原鏈表按從小到大合并到原鏈表中(前提是兩個鏈表都是已經(jīng)從小到大排序了)。 這里 merge 的核心就是 transfer 函數(shù)。

reverse 函數(shù): 實現(xiàn)將鏈表翻轉(zhuǎn)的功能:主要是 list 的迭代器基本不會改變的特點, 將每一個元素一個個插入到 begin 之前。

sort 函數(shù): list 這個容器居然還自己實現(xiàn)一個排序,看一眼源碼就發(fā)現(xiàn)其實內(nèi)部調(diào)用的 merge 函數(shù),用了一個數(shù)組鏈表用來存儲 2^i 個元素, 當(dāng)上一個元素存儲滿了之后繼續(xù)往下一個鏈表存儲, 最后將所有的鏈表進行 merge歸并(合并), 從而實現(xiàn)了鏈表的排序。

賦值操作: 需要考慮兩個鏈表的實際大小不一樣時的操作:如果原鏈表大 : 復(fù)制完后要刪除掉原鏈表多余的元素;如果原鏈表小 : 復(fù)制完后要還要將x鏈表的剩余元素以插入的方式插入到原鏈表中。

resize 操作: 重新修改 list 的大小,傳入一個 new_size,如果鏈表舊長度大于 new_size 的大小, 那就刪除后面多余的節(jié)點。

clear 操作: 清除所有節(jié)點:遍歷每一個節(jié)點,銷毀(析構(gòu)并釋放)一個節(jié)點。

remove 操作: 清除指定值的元素:遍歷每一個節(jié)點,找到就移除。

unique 操作: 清除數(shù)值相同的連續(xù)元素,注意只有“連續(xù)而相同的元素”,才會被移除剩一個:遍歷每一個節(jié)點,如果在此區(qū)間段有相同的元素就移除之。

感興趣的讀者可以自行去閱讀源碼體會。

list 總結(jié)

我們來總結(jié)一下。

list 是一種雙向鏈表。每個結(jié)點都包含一個數(shù)據(jù)域、一個前驅(qū)指針 prev 和一個后驅(qū)指針 next。

由于其鏈表特性,實現(xiàn)同樣的操作,相對于 STL 中的通用算法, list 的成員函數(shù)通常有更高的效率,內(nèi)部僅需做一些指針的操作,因此盡可能選擇 list 成員函數(shù)。

優(yōu)點

在內(nèi)部方便進行插入刪除操作。

可在兩端進行push和pop操作。

缺點

不支持隨機訪問,即下標(biāo)操作和.at()。

相對于 vector 占用內(nèi)存多。

deque下面到了本篇最硬核的內(nèi)容了,接下來我們學(xué)習(xí)一下 雙端隊列 deque 。

deque 的功能很強大。

首先來一張圖吧。

a947f626-a83e-11eb-9728-12bb97331649.png

上面就是 deque 的示例圖,deque 和 vector 的最大差異一在于 deque 允許常數(shù)時間內(nèi)對頭端或尾端進行元素的插入或移除操作。

二在于 deque 沒有所謂的容量概念,因為它是動態(tài)地以分段連續(xù)空間組合而成隨時可以增加一塊新的空間并拼接起來。

雖然 deque 也提供 隨機訪問的迭代器,但它的迭代器和前面兩種容器的都不一樣,其設(shè)計相當(dāng)復(fù)雜度和精妙,因此,會對各種運算產(chǎn)生一定影響,除非必要,盡可能的選擇使用 vector 而非 deque。一一來探究下吧。

deque 的中控器

deque 在邏輯上看起來是連續(xù)空間,內(nèi)部確實是由一段一段的定量連續(xù)空間構(gòu)成。

一旦有必要在 deque 的前端或尾端增加新空間,deque 便會配置一段定量的連續(xù)空間,串接在整個 deque 的頭部或尾部。

設(shè)計 deque 的大師們,想必設(shè)計的時候遇到的最大挑戰(zhàn)就是如何在這些分段的定量連續(xù)空間上,還能維護其整體連續(xù)的假象,并提供其隨機存取的接口,從而避開了像 vector 那樣的“重新配置-復(fù)制-釋放”開銷三部曲。

這樣一來,雖然開銷降低,卻提高了復(fù)雜的迭代器架構(gòu)。

因此 deque 數(shù)據(jù)結(jié)構(gòu)的設(shè)計和迭代器前進或后退等操作都非常復(fù)雜。

deque 采用一塊所謂的 map (注意不是 STL 里面的 map 容器)作為中控器,其實就是一小塊連續(xù)空間,其中的每個元素都是指針,指向另外一段較大的連續(xù)線性空間,稱之為緩沖區(qū)。在后面我們看到,緩沖區(qū)才是 deque 的儲存空間主體。

#ifndef __STL_NON_TYPE_TMPL_PARAM_BUGtemplate 《class T, class Ref, class Ptr, size_t BufSiz》

class deque {public:

typedef T value_type;

typedef value_type* pointer;

。。。

protected:

typedef pointer** map_pointer;

map_pointer map;//指向 map,map 是連續(xù)空間,其內(nèi)的每個元素都是一個指針。

size_type map_size;

。。。

};

其示例圖如下:deque 的結(jié)構(gòu)設(shè)計中,map 和 node-buffer 的關(guān)系如下:

a9702790-a83e-11eb-9728-12bb97331649.png

deque 的迭代器

deque 是分段連續(xù)空間,維持其“整體連續(xù)”假象的任務(wù),就靠它的迭代器來實現(xiàn),也就是 operator++ 和 operator-- 兩個運算子上面。

在看源碼之前,我們可以思考一下,如果讓你來設(shè)計,你覺得 deque 的迭代器應(yīng)該具備什么樣的結(jié)構(gòu)和功能呢?

首先第一點,我們能想到的是,既然是分段連續(xù),迭代器應(yīng)該能指出當(dāng)前的連續(xù)空間在哪里;

其次,第二點因為緩沖區(qū)有邊界,迭代器還應(yīng)該要能判斷,當(dāng)前是否處于所在緩沖區(qū)的邊緣,如果是,一旦前進或后退,就必須跳轉(zhuǎn)到下一個或上一個緩沖區(qū);

第三點,也就是實現(xiàn)前面兩種情況的前提,迭代器必須能隨時控制中控器。

有了這樣的思想準(zhǔn)備之后,我們再來看源碼,就顯得容易理解一些了。

template 《class T, class Ref, class Ptr, size_t BufSiz》

struct __deque_iterator {

// 迭代器定義

typedef __deque_iterator《T, T&, T*, BufSiz》 iterator;

typedef __deque_iterator《T, const T&, const T*, BufSiz》 const_iterator;

static size_t buffer_size() {return __deque_buf_size(BufSiz, sizeof(T)); }

// deque是random_access_iterator_tag類型

typedef random_access_iterator_tag iterator_category;

// 基本類型的定義, 滿足traits編程

typedef T value_type;

typedef Ptr pointer;

typedef Ref reference;

typedef size_t size_type;

typedef ptrdiff_t difference_type;

// node

typedef T** map_pointer;

map_pointer node;

typedef __deque_iterator self;

。。。

};

deque 的每一個緩沖區(qū)由設(shè)計了三個迭代器(為什么這樣設(shè)計?)

struct __deque_iterator {

。。。

typedef T value_type;

T* cur;

T* first;

T* last;

typedef T** map_pointer;

map_pointer node;

。。。

};

對啊,它為什么要這樣設(shè)計呢?回到前面我們剛才說的,因為它是分段連續(xù)的空間。

下圖描繪了deque 的中控器、緩沖區(qū)、迭代器之間的相互關(guān)系:

ac76aef0-a83e-11eb-9728-12bb97331649.png

看明白了嗎,每一段都指向一個緩沖區(qū) buffer,而緩沖區(qū)是需要知道每個元素的位置的,所以需要這些迭代器去訪問。

其中cur 表示當(dāng)前所指的位置;

first 表示當(dāng)前數(shù)組中頭的位置;

last 表示當(dāng)前數(shù)組中尾的位置。

這樣就方便管理,需要注意的是 deque 的空間是由 map 管理的, 它是一個指向指針的指針, 所以三個參數(shù)都是指向當(dāng)前的數(shù)組,但這樣的數(shù)組可能有多個,只是每個數(shù)組都管理這3個變量。

那么,緩沖區(qū)大小是誰來決定的呢?這里呢,用來決定緩沖區(qū)大小的是一個全局函數(shù):

inline size_t __deque_buf_size(size_t n, size_t sz) {

return n != 0 ? n : (sz 《 512 ? size_t(512 / sz): size_t(1));

}

//如果 n 不為0,則返回 n,表示緩沖區(qū)大小由用戶自定義//如果 n == 0,表示 緩沖區(qū)大小默認(rèn)值//如果 sz = (元素大小 sizeof(value_type)) 小于 512 則返回 521/sz//如果 sz 不小于 512 則返回 1

我們來舉例說明一下:

假設(shè)我們現(xiàn)在構(gòu)造了一個 int 類型的 deque,設(shè)置緩沖區(qū)大小等于 32,這樣一來,每個緩沖區(qū)可以容納 32/sizeof(int) = 4 個元素。經(jīng)過一番操作之后,deque 現(xiàn)在有 20 個元素了,那么成員函數(shù) begin() 和 end() 返回的兩個迭代器應(yīng)該是怎樣的呢?

如下圖所示:

ac870dea-a83e-11eb-9728-12bb97331649.png

20 個元素需要 20/(sizeof(int)) = 3 個緩沖區(qū)。

所以 map 運用了三個節(jié)點,迭代器 start 內(nèi)的 cur 指針指向緩沖區(qū)的第一個元素,迭代器 finish 內(nèi)的 cur 指針指向緩沖區(qū)的最后一個元素(的下一個位置)。

注意,最后一個緩沖區(qū)尚有備用空間,如果之后還有新元素插入,則直接插入到備用空間。

deque 迭代器的操作

主要就是兩種:前進和后退。

operator++ 操作代表是需要切換到下一個元素,這里需要先切換再判斷是否已經(jīng)到達緩沖區(qū)的末尾。

self& operator++() {

++cur; //切換至下一個元素

if (cur == last) { //如果已經(jīng)到達所在緩沖區(qū)的末尾

set_node(node+1); //切換下一個節(jié)點

cur = first;

}

return *this;

}

operator-- 操作代表切換到上一個元素所在的位置,需要先判斷是否到達緩沖區(qū)的頭部,再后退。

self& operator--() {

if (cur == first) { //如果已經(jīng)到達所在緩沖區(qū)的頭部

set_node(node - 1); //切換前一個節(jié)點的最后一個元素

cur = last;

}

--cur; //切換前一個元素

return *this;

} //結(jié)合前面的分段連續(xù)空間,你在想一想這樣的設(shè)計是不是合理呢?

deque 的構(gòu)造和析構(gòu)函數(shù)

deque 的構(gòu)造函數(shù)有多個重載函數(shù),接受大部分不同的參數(shù)類型,基本上每一個構(gòu)造函數(shù)都會調(diào)用 create_map_and_nodes,這就是構(gòu)造函數(shù)的核心,后面我們來分析這個函數(shù)的實現(xiàn)。

template 《class T, class Alloc = alloc, size_t BufSiz = 0》

class deque {

。。。

public: // Basic types

deque() : start(), finish(), map(0), map_size(0){

create_map_and_nodes(0);

} // 默認(rèn)構(gòu)造函數(shù)

deque(const deque& x) : start(), finish(), map(0), map_size(0) {

create_map_and_nodes(x.size());

__STL_TRY {

uninitialized_copy(x.begin(), x.end(), start);

}

__STL_UNWIND(destroy_map_and_nodes());

}

// 接受 n:初始化大小, value:初始化的值

deque(size_type n, const value_type& value) : start(), finish(), map(0), map_size(0) {

fill_initialize(n, value);

}

deque(int n, const value_type& value) : start(), finish(), map(0), map_size(0) {

fill_initialize(n, value);

}

deque(long n, const value_type& value) : start(), finish(), map(0), map_size(0){

fill_initialize(n, value);

}

。。。

下面我們來看一下 deque 的中控器是如何配置的。

void deque《T,Alloc,BufSize》::create_map_and_nodes(size_type_num_elements) {

//需要節(jié)點數(shù)= (每個元素/每個緩沖區(qū)可容納的元素個數(shù)+1)

//如果剛好整除,多配一個節(jié)點

size_type num_nodes = num_elements / buffer_size() + 1;

//一個 map 要管理幾個節(jié)點,最少 8 個,最多是需要節(jié)點數(shù)+2

map_size = max(initial_map_size(), num_nodes + 2);

map = map_allocator::allocate(map_size);

// 計算出數(shù)組的頭前面留出來的位置保存并在nstart.

map_pointer nstart = map + (map_size - num_nodes) / 2;

map_pointer nfinish = nstart + num_nodes - 1;

map_pointer cur;//指向所擁有的節(jié)點的最中央位置

。。。

}

注意了,看了源碼之后才知道:deque 的 begin 和 end 不是一開始就是指向 map 中控器里開頭和結(jié)尾的,而是指向所擁有的節(jié)點的最中央位置。

這樣帶來的好處是可以使得頭尾兩邊擴充的可能性和一樣大,換句話來說,因為 deque 是頭尾插入都是 O(1), 所以 deque 在頭和尾都留有空間方便頭尾插入。

那么,什么時候 map 中控器 本身需要調(diào)整大小呢?觸發(fā)條件在于 reserve_map_at_back 和 reserve_map_at_front 這兩個函數(shù)來判斷,實際操作由 reallocate_map 來執(zhí)行。

那 reallocate_map 又是如何操作的呢?這里先留個懸念。

// 如果 map 尾端的節(jié)點備用空間不足,符合條件就配置一個新的map(配置更大的,拷貝原來的,釋放原來的)void reserve_map_at_back (size_type nodes_to_add = 1) {

if (nodes_to_add + 1 》 map_size - (finish.node - map))

reallocate_map(nodes_to_add, false);

}

// 如果 map 前端的節(jié)點備用空間不足,符合條件就配置一個新的map(配置更大的,拷貝原來的,釋放原來的)void reserve_map_at_front (size_type nodes_to_add = 1) {

if (nodes_to_add 》 start.node - map)

reallocate_map(nodes_to_add, true);

}

deque 的插入元素和刪除元素

因為 deque 的是能夠雙向操作,所以其 push 和 pop 操作都類似于 list 都可以直接有對應(yīng)的操作,需要注意的是 list 是鏈表,并不會涉及到界線的判斷, 而deque 是由數(shù)組來存儲的,就需要隨時對界線進行判斷。

push 實現(xiàn)

template 《class T, class Alloc = alloc, size_t BufSiz = 0》

class deque {

。。。

public: // push_* and pop_*

// 對尾進行插入

// 判斷函數(shù)是否達到了數(shù)組尾部。 沒有達到就直接進行插入

void push_back(const value_type& t) {

if (finish.cur != finish.last - 1) {

construct(finish.cur, t);

++finish.cur;

}

else

push_back_aux(t);

}

// 對頭進行插入

// 判斷函數(shù)是否達到了數(shù)組頭部。 沒有達到就直接進行插入

void push_front(const value_type& t) {

if (start.cur != start.first) {

construct(start.cur - 1, t);

--start.cur;

}

else

push_front_aux(t);

}

。。。

};

pop 實現(xiàn)

template 《class T, class Alloc = alloc, size_t BufSiz = 0》

class deque {

。。。

public:

// 對尾部進行操作

// 判斷是否達到數(shù)組的頭部。 沒有到達就直接釋放

void pop_back() {

if (finish.cur != finish.first) {

--finish.cur;

destroy(finish.cur);

}

else

pop_back_aux();

}

// 對頭部進行操作

// 判斷是否達到數(shù)組的尾部。 沒有到達就直接釋放

void pop_front() {

if (start.cur != start.last - 1) {

destroy(start.cur);

++start.cur;

}

else

pop_front_aux();

}

。。。

};

pop 和 push 都先調(diào)用了 reserve_map_at_XX 函數(shù),這些函數(shù)主要是為了判斷前后空間是否足夠。

刪除操作

不知道還記得,最開始構(gòu)造函數(shù)調(diào)用 create_map_and_nodes 函數(shù),考慮到 deque 實現(xiàn)前后插入時間復(fù)雜度為O(1),保證了在前后留出了空間,所以 push 和 pop 都可以在前面的數(shù)組進行操作。

現(xiàn)在就來看 erase,因為 deque 是由數(shù)組構(gòu)成,所以地址空間是連續(xù)的,刪除也就像 vector一樣,要移動所有的元素。

deque 為了保證效率盡可能的高,就判斷刪除的位置是中間偏后還是中間偏前來進行移動。

template 《class T, class Alloc = alloc, size_t BufSiz = 0》

class deque {

。。。

public: // erase

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;

}

// 范圍刪除, 實際也是調(diào)用上面的erase函數(shù)。

iterator erase(iterator first, iterator last);

void clear();

。。。

};

最后講一下 insert 函數(shù)

deque 源碼的基本每一個insert 重載函數(shù)都會調(diào)用了 insert_auto判斷插入的位置離頭還是尾比較近。

如果離頭進:則先將頭往前移動,調(diào)整將要移動的距離,用 copy 進行調(diào)整。

如果離尾近:則將尾往前移動,調(diào)整將要移動的距離,用 copy 進行調(diào)整。

注意 : push_back 是先執(zhí)行構(gòu)造在移動 node,而 push_front 是先移動 node 在進行構(gòu)造,實現(xiàn)的差異主要是 finish 是指向最后一個元素的后一個地址而 first指 向的就只第一個元素的地址,下面 pop 也是一樣的。

deque 源碼里還有一些其它的成員函數(shù),限于篇幅,這里就不貼出來了,簡單的過一遍

reallocate_map:判斷中控器的容量是否夠用,如果不夠用,申請更大的空間,拷貝元素過去,修改 map 和 start,finish 的指向。

fill_initialize 函數(shù):申請空間,對每個空間進行初始化,最后一個數(shù)組單獨處理。畢竟最后一個數(shù)組一般不是會全部填充滿。

clear 函數(shù):刪除所有元素,分兩步執(zhí)行:

首先從第二個數(shù)組開始到倒數(shù)第二個數(shù)組一次性全部刪除,這樣做是考慮到中間的數(shù)組肯定都是滿的,前后兩個數(shù)組就不一定是填充滿的,最后刪除前后兩個數(shù)組的元素。

deque 的 swap 操作:只是交換了 start, finish, map,并沒有交換所有的元素。

resize 函數(shù): 重新將 deque 進行調(diào)整, 實現(xiàn)與 list一樣的。

析構(gòu)函數(shù): 分步釋放內(nèi)存。

deque 總結(jié)

deque 其實是在功能上合并了 vector 和 list。

優(yōu)點:

1、隨機訪問方便,即支持 [ ] 操作符和 vector.at();

2、在內(nèi)部方便的進行插入和刪除操作;

3、可在兩端進行 push、pop

缺點:因為涉及比較復(fù)雜,采用分段連續(xù)空間,所以占用內(nèi)存相對多。

使用區(qū)別:

1、如果你需要高效的隨即存取,而不在乎插入和刪除的效率,使用 vector。

2、如果你需要大量的插入和刪除,而不關(guān)心隨機存取,則應(yīng)使用 list。

3、如果你需要隨機存取,而且關(guān)心兩端數(shù)據(jù)的插入和刪除,則應(yīng)使用 deque 。

棧和隊列以 deque 為底層容器的適配器

最后要介紹的三種常用的數(shù)據(jù)結(jié)構(gòu),準(zhǔn)確來說其實是一種適配器,底層都是已其它容器為基準(zhǔn)。

棧-stack:先入后出,只允許在棧頂添加和刪除元素,稱為出棧和入棧。

隊列-queue:先入先出,在隊首取元素,在隊尾添加元素,稱為出隊和入隊。

ac941e72-a83e-11eb-9728-12bb97331649.png

優(yōu)先隊列-priority_queue:帶權(quán)值的隊列。

常見棧的應(yīng)用場景包括括號問題的求解,表達式的轉(zhuǎn)換和求值,函數(shù)調(diào)用和遞歸實現(xiàn),深度優(yōu)先遍歷 DFS 等;

常見的隊列的應(yīng)用場景包括計算機系統(tǒng)中各種資源的管理,消息緩沖隊列的管理和廣度優(yōu)先遍歷 BFS 等。

翻一下源碼,就知道 stack 和 queue 的底層其實就是使用 deque,用 deque 為底層容器封裝。

stack 的源碼:

#ifndef __STL_LIMITED_DEFAULT_TEMPLATEStemplate 《class T, class Sequence = deque《T》 》

#else

template 《class T, class Sequence》

#endif

class stack {public:

typedef typename Sequence::value_type value_type;

typedef typename Sequence::size_type size_type;

typedef typename Sequence::reference reference;

typedef typename Sequence::const_reference const_reference;

protected:

Sequence c;

queue 的源碼:

#ifndef __STL_LIMITED_DEFAULT_TEMPLATEStemplate 《class T, class Sequence = deque《T》 》

#else

template 《class T, class Sequence》

#endif

class queue {public:

typedef typename Sequence::value_type value_type;

typedef typename Sequence::size_type size_type;

typedef typename Sequence::reference reference;

typedef typename Sequence::const_reference const_reference;

protected:

Sequence c;

heap堆最后我們來看一下,heap ,heap 并不是一個容器,所以他沒有實現(xiàn)自己的迭代器,也就沒有遍歷操作,它只是一種算法。

push_heap 插入元素

插入函數(shù)是 push_heap, heap 只接受 RandomAccessIterator 類型的迭代器。

template 《class RandomAccessIterator》

inline void push_heap(RandomAccessIterator first, RandomAccessIterator last) {

__push_heap_aux(first, last, distance_type(first), value_type(first));

}

template 《class RandomAccessIterator, class Distance, class T》

inline void __push_heap_aux(RandomAccessIterator first, RandomAccessIterator last, Distance*, T*) {

// 這里傳入的是兩個迭代器的長度, 0, 還有最后一個數(shù)據(jù)

__push_heap(first, Distance((last - first) - 1), Distance(0), T(*(last - 1)));

}

pop_heap 刪除元素

pop 操作其實并沒有真正意義去刪除數(shù)據(jù),而是將數(shù)據(jù)放在最后,只是沒有指向最后的元素而已,這里使用 arrary 也可以,畢竟沒有對數(shù)組的大小進行調(diào)整。

pop 的實現(xiàn)有兩種,這里都羅列了出來,另一個傳入的是 cmp 仿函數(shù)。

template 《class RandomAccessIterator, class Compare》

inline void pop_heap(RandomAccessIterator first, RandomAccessIterator last,

Compare comp) {

__pop_heap_aux(first, last, value_type(first), comp);

}

template 《class RandomAccessIterator, class T, class Compare》

inline void __pop_heap_aux(RandomAccessIterator first,

RandomAccessIterator last, T*, Compare comp) {

__pop_heap(first, last - 1, last - 1, T(*(last - 1)), comp,

distance_type(first));

}

template 《class RandomAccessIterator, class T, class Compare, class Distance》

inline void __pop_heap(RandomAccessIterator first, RandomAccessIterator last,

RandomAccessIterator result, T value, Compare comp,

Distance*) {

*result = *first;

__adjust_heap(first, Distance(0), Distance(last - first), value, comp);

}

template 《class RandomAccessIterator, class T, class Distance》

inline void __pop_heap(RandomAccessIterator first, RandomAccessIterator last,

RandomAccessIterator result, T value, Distance*) {

*result = *first; // 因為這里是大根堆, 所以first的值就是最大值, 先將最大值保存。

__adjust_heap(first, Distance(0), Distance(last - first), value);

}

make_heap 將數(shù)組變成堆存放

template 《class RandomAccessIterator》

inline void make_heap(RandomAccessIterator first, RandomAccessIterator last) {

__make_heap(first, last, value_type(first), distance_type(first));

}

template 《class RandomAccessIterator, class T, class Distance》

void __make_heap(RandomAccessIterator first, RandomAccessIterator last, T*,

Distance*) {

if (last - first 《 2) return;

// 計算長度, 并找出中間的根值

Distance len = last - first;

Distance parent = (len - 2)/2;

while (true) {

// 一個個進行調(diào)整, 放到后面

__adjust_heap(first, parent, len, T(*(first + parent)));

if (parent == 0) return;

parent--;

}

}

sort_heap 實現(xiàn)堆排序

其實就是每次將第一位數(shù)據(jù)彈出從而實現(xiàn)排序功。

template 《class RandomAccessIterator》

void sort_heap(RandomAccessIterator first, RandomAccessIterator last) {

while (last - first 》 1) pop_heap(first, last--);

}

template 《class RandomAccessIterator, class Compare》

void sort_heap(RandomAccessIterator first, RandomAccessIterator last,

Compare comp) {

while (last - first 》 1) pop_heap(first, last--, comp);

}

priority_queue 優(yōu)先隊列最后我們來看一下 priority_queue。

上一節(jié)分析 heap 其實就是為 priority_queue 做準(zhǔn)備,priority_queue 是一個優(yōu)先級隊列,是帶權(quán)值的, 支持插入和刪除操作,其只能從尾部插入,頭部刪除, 并且其順序也并非是根據(jù)加入的順序排列的。

priority_queue 因為也是隊列的一種體現(xiàn), 所以也就跟隊列一樣不能直接的遍歷數(shù)組,也就沒有迭代器。

priority_queue 本身也不算是一個容器,它是以 vector 為容器以 heap為數(shù)據(jù)操作的配置器。

類型定義

#ifndef __STL_LIMITED_DEFAULT_TEMPLATEStemplate 《class T, class Sequence = vector《T》,

class Compare = less《typename Sequence::value_type》 》

#else

template 《class T, class Sequence, class Compare》

#endif

class priority_queue {public:

// 符合traits編程規(guī)范

typedef typename Sequence::value_type value_type;

typedef typename Sequence::size_type size_type;

typedef typename Sequence::reference reference;

typedef typename Sequence::const_reference const_reference;

protected:

Sequence c; // 定義vector容器的對象

Compare comp; // 定義比較函數(shù)(偽函數(shù))

。。。

};

屬性獲取

priority_queue 只有簡單的 3 個屬性獲取的函數(shù), 其本身的操作也很簡單, 只是實現(xiàn)依賴了 vector 和 heap 就變得比較復(fù)雜。

class priority_queue {

。。。

public:

bool empty() const { return c.empty(); }

size_type size() const { return c.size(); }

const_reference top() const { return c.front(); }

。。。

};

push 和 pop 實現(xiàn)

push 和 pop 具體都是采用的 heap 算法。

priority_queue 本身實現(xiàn)是很復(fù)雜的,但是當(dāng)我們已經(jīng)了解過 vector,heap 之后再來看,它其實就簡單了。

就是將 vector 作為容器, heap 作為算法來操作的配置器,這也體現(xiàn)了 STL 的靈活性: 通過各個容器與算法的結(jié)合就能實現(xiàn)另一種功能。

最后,來自實踐生產(chǎn)環(huán)境的一個體會:上面所列的所有容器的一個原則:為了避免拷貝開銷,不要直接把大的對象直接往里塞,而是使用指針。

編輯:jq

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 數(shù)據(jù)
    +關(guān)注

    關(guān)注

    8

    文章

    6715

    瀏覽量

    88316
  • STL
    STL
    +關(guān)注

    關(guān)注

    0

    文章

    85

    瀏覽量

    18260
  • MAP
    MAP
    +關(guān)注

    關(guān)注

    0

    文章

    48

    瀏覽量

    15106
收藏 人收藏

    評論

    相關(guān)推薦

    HarmonyOS開發(fā):【基于命令行(獲取源碼)】

    在Ubuntu環(huán)境通過以下步驟獲取OpenHarmony源碼。
    的頭像 發(fā)表于 04-25 22:08 ?301次閱讀
    HarmonyOS開發(fā):【基于命令行(獲取<b class='flag-5'>源碼</b>)】

    容器隔膜般是什么,超級電容器隔膜的作用

    容器隔膜是電容器的關(guān)鍵組件之,主要用于隔離電容器的兩個電極,防止電極之間的直接接觸而發(fā)生短路,同時允許離子通過以完成電荷的存儲和釋放過程。這種隔膜通常非常薄,但必須具備
    的頭像 發(fā)表于 04-12 14:31 ?888次閱讀

    帶你了解PWM原理、頻率與占空比

    ~5V之間任意大?。┑哪M電壓。比方說 占空比為50% 那就是高電平時間半,低電平時間半,在定的頻率,就可以得到模擬的2.5V輸出電壓 那么75%的占空比 得到的電壓就是3.7
    發(fā)表于 03-27 14:12

    帶你了解紅墨水實驗!

    、什么是紅墨水實驗? 將焊點置于紅色墨水或染料中, 讓紅墨水或染料滲入焊點的裂紋之中,干燥后將焊點強行分離, 焊點般會從薄弱的環(huán)節(jié)(裂紋處)開裂。 因此,紅墨水實驗可以通過檢查開裂處界面的染色
    的頭像 發(fā)表于 02-26 11:24 ?1686次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>帶你</b>了解紅墨水實驗!

    什么情況容器會被擊穿

    容器種常見的電子元件,廣泛應(yīng)用于各個領(lǐng)域。然而,在特定條件,電容器可能會發(fā)生擊穿現(xiàn)象,導(dǎo)致其無法正常工作甚至損壞。那么,在什么情況
    的頭像 發(fā)表于 02-19 14:11 ?1934次閱讀

    干式電容器什么情況會爆炸?

    干式電容器種常見的電子元件,但在特定情況可能會發(fā)生爆炸。那么,干式電容器在哪些情況會發(fā)生爆炸呢?
    的頭像 發(fā)表于 12-15 14:20 ?588次閱讀

    薄膜電容器可以在高海拔工況運行嗎?

    薄膜電容器種非常常見的電容器類型,具有體積小、重量輕、頻率響應(yīng)快等優(yōu)點,因此在很多電子產(chǎn)品中廣泛應(yīng)用。但是,在高海拔工況,薄膜電容器
    的頭像 發(fā)表于 12-14 14:40 ?734次閱讀

    帶你了解 DAC

    了解 DAC
    的頭像 發(fā)表于 12-07 15:10 ?7968次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>帶你</b>了解 DAC

    Linux是如何對容器的進程進行CPU限制的,底層是如何工作的?

    現(xiàn)在很多公司的服務(wù)都是跑在容器,我來問幾個容器 CPU 相關(guān)的問題,看大家對天天在用的技術(shù)是否熟悉。
    的頭像 發(fā)表于 11-29 14:31 ?1052次閱讀
    Linux是如何對<b class='flag-5'>容器</b><b class='flag-5'>下</b>的進程進行CPU限制的,底層是如何工作的?

    STL內(nèi)容介紹

    1 什么是STL? STL(Standard Template Library),即標(biāo)準(zhǔn)模板庫,是個具有工業(yè)強度的,高效的C++程序庫。它被容納于C++標(biāo)準(zhǔn)程序庫(C++ Standard
    的頭像 發(fā)表于 11-13 11:32 ?637次閱讀
    <b class='flag-5'>STL</b>內(nèi)容介紹

    C++中STL容器中的常見容器及基本操作

    、什么是容器? 所謂容器,就是可以承載,包含元素的個器件,它是STL六大組件之,是
    的頭像 發(fā)表于 11-10 11:23 ?385次閱讀
    C++中<b class='flag-5'>STL</b><b class='flag-5'>容器</b>中的常見<b class='flag-5'>容器</b>及基本操作

    MCUXpresso IDE源碼制作成Lib庫方法及其與IAR,MDK差異

    MCUXpresso IDE源碼制作成Lib庫方法及其與IAR,MDK差異
    的頭像 發(fā)表于 11-07 17:13 ?1009次閱讀
    MCUXpresso IDE<b class='flag-5'>下</b>將<b class='flag-5'>源碼</b>制作成Lib庫方法及其與IAR,MDK差異

    Ubuntu系統(tǒng)編譯OpenCV4.8源碼記錄

    支持,所以就用這個開發(fā)板給大家演示一下如何在烏班圖系統(tǒng)編譯OpenCV4.8源碼與如何編譯執(zhí)行OpenCV C++應(yīng)用。
    的頭像 發(fā)表于 10-27 16:07 ?1363次閱讀
    Ubuntu系統(tǒng)<b class='flag-5'>下</b>編譯OpenCV4.8<b class='flag-5'>源碼</b>記錄

    帶你了解工業(yè)CT

    ,大多數(shù)人很少接觸到工業(yè)CT。CT在醫(yī)學(xué)和我們的日常生活中占有如此重要的地位。那么什么是工業(yè)CT,它起什么作用,主要應(yīng)用在哪些領(lǐng)域,以及目前的發(fā)展現(xiàn)狀。怎么。本文將對這些問題逐進行簡要分析 1. 對于第個問題,我們先來討論一下
    的頭像 發(fā)表于 10-25 14:37 ?1087次閱讀

    使用STL函數(shù)控制傳送帶

    要創(chuàng)建 STL 函數(shù)塊“STL-Conveyor”,請按以下步驟操作
    的頭像 發(fā)表于 10-12 16:00 ?487次閱讀
    使用<b class='flag-5'>STL</b>函數(shù)控制傳送帶