C++中的deque

1. 什么是 Deque?

  • 核心概念: Deque 是 “Double-Ended Queue”(雙端隊列)的縮寫。你可以把它想象成一個可以在兩端(頭部和尾部)高效地進行添加或刪除操作的線性數據結構。
  • 關鍵特性:
    • 雙端操作: 這是 deque 最顯著的特點。你可以在隊列的開頭(front)結尾(back) 快速地插入(push_front, push_back)或刪除(pop_front, pop_back)元素。
    • 順序容器: 元素在 deque 中是按順序存儲的,每個元素都有一個確定的位置(索引)。你可以像數組或 vector 一樣,通過下標 operator[]at()隨機訪問任意位置的元素(效率接近 O(1),但比 vector 稍慢一點,后面解釋原因)。
    • 動態大小: 和 vector、list 一樣,deque 可以根據需要動態地增長或縮小,你不需要預先指定其大小。
  • 形象比喻: 想象一個特殊的書架(或者更準確地說,一組小書架拼成的大書架)
    • 你既可以從書架的最左邊(front) 快速地拿走或放入一本書。
    • 也可以從書架的最右邊(back) 快速地拿走或放入一本書。
    • 你還可以直接走到書架中間某個編號的位置(索引),快速找到并取閱(訪問)那本書。
    • 當一個小書架放滿了,管理員可以輕松地在最左邊或最右邊拼接一個新的空小書架,而不需要把整個大書架的書都重新整理一遍(這是它和 vector 擴容的關鍵區別)。

2. Deque 的內部實現機制(理解其高效性的關鍵)

這是理解 deque 為何能在兩端高效操作,以及其隨機訪問性能、內存特性的核心。典型的 STL 實現(如 GCC 的 libstdc++ 或 Clang 的 libc++)采用一種叫做 “分塊數組” 或 “塊鏈” 的策略:

  • 不是一整塊連續內存: 不像 std::vector 總是試圖維護一整塊大的連續內存空間。
  • 由多個固定大小的 “塊”(Chunks / Blocks) 組成: Deque 內部管理著一個數組(通常叫 mapblock map,這個數組的每個元素是一個指針,指向一個固定大小的內存塊。
  • 塊內連續,塊間不連續: 每個內存塊內部是連續存儲元素的。但是,不同的內存塊在物理內存上不一定是連續的。這些塊通過 map 數組組織起來。
  • map 數組本身也是動態數組: 這個指向塊的指針數組 (map) 本身也是一個動態數組(通常實現為 vector)。當 deque 增長導致需要更多的塊時,這個 map 數組也可能需要擴容(重新分配和復制指針),但這比 vector 擴容(復制所有元素)開銷小得多。
  • 維護關鍵指針/迭代器: Deque 對象內部維護幾個關鍵信息:
    • 指向 map 數組的指針。
    • map 數組的大小(能容納多少塊指針)。
    • 指向第一個元素所在塊的指針(start 迭代器或類似結構,包含塊指針、當前塊內起始位置)。
    • 指向最后一個元素所在塊的下一個位置的指針(finish 迭代器或類似結構,包含塊指針、當前塊內結束位置)。
    • 通常還會緩存第一個和最后一個元素在各自塊內的具體位置。

大家可以通過下面這3幅圖來大概理解一下deque的結構:

在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述

為什么這種結構支持高效的雙端操作?

  • 在頭部插入(push_front):
    1. 檢查第一個塊 start 指向的塊是否還有空間(在頭部方向)。
    2. 如果有空間: 直接在那個塊的空位(start 指針前面)放入新元素,更新 start 的位置信息。非常快 (O(1))
    3. 如果沒空間:map 數組的前端(或找到一個空閑位置)分配一個新塊,將新元素放入新塊的末尾位置(因為新塊要加在最前面),然后更新 mapstart 指向這個新塊。如果 map 數組前端沒空間了,可能需要重新分配并復制 map(這個開銷相對小,因為只復制指針)。均攤復雜度接近 O(1)
  • 在尾部插入(push_back):
    1. 檢查最后一個塊 finish 指向的塊是否還有空間(在尾部方向)。
    2. 如果有空間: 直接在那個塊的空位放入新元素,更新 finish 的位置信息。非常快 (O(1))
    3. 如果沒空間:map 數組的后端分配一個新塊,將新元素放入新塊的開頭位置,更新 mapfinish。同樣,map 擴容開銷較小。均攤復雜度接近 O(1)
  • 在頭部刪除(pop_front):
    1. 直接移除 start 當前指向的元素。
    2. 更新 start 指向下一個位置(或下一個塊的開頭,如果當前塊刪空了)。
    3. 如果當前塊變空,可以釋放它(可選,實現可能緩存空塊)。O(1)
  • 在尾部刪除(pop_back):
    1. 直接移除 finish 前一個位置的元素(finish 通常指向最后一個元素的下一個位置)。
    2. 更新 finish 指向前一個位置(或前一個塊的末尾,如果當前塊刪空了)。
    3. 同樣,空塊可能被釋放或緩存。O(1)

為什么隨機訪問是 O(1)(接近)?

  1. 計算目標元素位置: 假設每個塊固定大小為 block_size (例如 512 字節或存 16/32/64 個元素,取決于元素大小和實現)。
  2. 定位塊: 要訪問索引 i 處的元素:
    • 首先計算它在哪個塊: block_index = (i + start_block_offset) / block_size (需要知道 start 塊在 map 中的位置和 start 在它自己塊內的偏移)。
    • 然后在 map[block_index] 指向的塊內找到偏移: offset_in_block = (i + start_block_offset) % block_size
  3. 直接訪問: 通過 map[block_index] + offset_in_block 直接訪問元素。
  4. 開銷: 這個過程涉及幾次整數除法和取模運算(現代 CPU 很快),以及兩次指針解引用(訪問 map 數組,再訪問具體塊)。這比 vector 的一次指針偏移訪問(start_pointer + i * element_size略慢,但復雜度仍然是常數時間 O(1)。這是“接近 O(1)”的含義,實際常數因子比 vector 大。

3. Deque 的核心操作與復雜度

操作函數時間復雜度說明
頭部插入push_front(value)均攤 O(1)在開頭添加一個元素。非常高效。
尾部插入push_back(value)均攤 O(1)在末尾添加一個元素。非常高效。
頭部刪除pop_front()O(1)移除開頭元素。非常高效。
尾部刪除pop_back()O(1)移除末尾元素。非常高效。
隨機訪問operator[], atO(1)通過索引訪問元素。效率接近 vector,常數因子略高。at 會進行邊界檢查。
訪問頭部元素front()O(1)返回開頭元素的引用。
訪問尾部元素back()O(1)返回末尾元素的引用。
中間插入insert(pos, value)O(n)在迭代器 pos 指定的位置插入元素。需要移動后續元素,效率較低。
中間刪除erase(pos)O(n)刪除迭代器 pos 指定的元素。需要移動后續元素,效率較低。
獲取大小size()O(1)返回當前元素數量(通常內部維護)。
檢查是否空empty()O(1)檢查 deque 是否為空。
清空clear()O(n)刪除所有元素。通常會釋放所有內存塊(實現可能緩存空塊)。

4. Deque 的迭代器失效規則(重要!)

理解迭代器何時失效對安全使用容器至關重要:

  • 插入(push_front, push_back, insert):
    • push_front / push_back通常只使所有迭代器失效(但指向元素的引用和指針通常仍然有效!)。 為什么?因為 map 數組可能重新分配,導致指向塊的指針改變。不過,重要提示:大多數現代實現保證 push_front/push_back 不會使指向已有元素的引用和指針失效(失效的是迭代器本身,因為它可能存儲了塊指針和位置信息)。這是標準允許但不強制的,主流實現(libstdc++, libc++)都提供了這個保證。如果插入導致新塊分配,map 重分配,則所有迭代器失效(引用/指針仍然指向有效元素)。如果沒導致新塊/map重分配,可能只有end()迭代器失效(實現細節)。最安全保守的做法是:在push_front/push_back后,假設所有迭代器可能失效(除非你明確知道當前實現和容量情況)。引用和指針通常安全。
    • insert(pos, value) (在中間插入): 使所有指向插入位置之后(包括pos本身)的迭代器、引用和指針失效。 因為需要移動后面的元素。
  • 刪除(pop_front, pop_back, erase):
    • pop_front使指向被刪除元素(第一個元素)的迭代器、引用和指針失效。 front() 返回的引用也失效。其他迭代器/引用/指針通常安全。
    • pop_back使指向被刪除元素(最后一個元素)的迭代器、引用和指針失效。 back() 返回的引用也失效。其他迭代器/引用/指針通常安全。
    • erase(pos) (在中間刪除): 使所有指向被刪除元素及其之后位置的迭代器、引用和指針失效。 因為需要移動后面的元素填補空缺。
  • swap 交換兩個 deque 后,兩個 deque 的所有迭代器、引用和指針都會交換它們的有效性。原來指向 deque A 的迭代器現在指向 deque B 中的對應元素(如果還有效的話),反之亦然。
  • clear 所有迭代器、引用和指針失效。

總結關鍵點: 在 deque 中間進行插入或刪除操作是昂貴的(O(n))且會使相關迭代器失效。在兩端操作非常高效,并且通常不會使指向已有元素的引用和指針失效(迭代器本身需要謹慎對待,特別是push_front/push_back之后)。

5. Deque 的典型應用場景

  • 雙端隊列操作: 這是最直接的用途,當你需要一個隊列,但頻繁需要同時在隊頭和隊尾進行操作時。例如:
    • 任務調度器: 一些系統可能從隊列頭部取任務執行,但也允許高優先級任務插入到隊列頭部(push_front)。
    • 撤銷/重做(Undo/Redo)歷史記錄: 新操作push_back,撤銷時pop_back,重做可能需要訪問尾部附近。
    • 滑動窗口算法: 需要高效地移除窗口左端元素(pop_front)和添加右端元素(push_back),同時可能需要隨機訪問窗口內的任意元素來計算最大值、最小值、平均值等。
  • 需要高效隨機訪問,且插入主要在兩端: 如果你需要一個支持隨機訪問(像數組)的容器,但插入和刪除操作主要發生在開頭或結尾,而不是中間,那么 deque 是比 vector 更好的選擇。Vector 在頭部插入/刪除是 O(n) 的災難。
  • 替代 vector 的謹慎選擇: 當你對 vector 在尾部插入導致擴容并復制所有元素的性能開銷非常敏感,并且你同時需要在頭部進行插入操作時。Deque 擴容(添加新塊)的開銷更小、更平攤。但要注意 deque 的隨機訪問稍慢且內存占用通常更大。

6. Deque vs. Vector vs. List

特性std::dequestd::vectorstd::list
內部結構分塊數組 (塊鏈)單一動態數組雙向鏈表
內存連續性塊內連續,塊間不連續完全連續完全不連續
隨機訪問O(1) (接近 vector)O(1) (最快)O(n) (慢)
頭部插入/刪除O(1) (高效)O(n) (低效, 需要移動所有元素)O(1) (高效)
尾部插入/刪除O(1) (高效)均攤 O(1) (高效, 除非擴容)O(1) (高效)
中間插入/刪除O(n) (需要移動元素)O(n) (需要移動元素)O(1) (已知位置迭代器)
迭代器失效 (尾部插入)可能失效 (若導致map重分配)可能失效 (若擴容)通常不失效
迭代器失效 (頭部插入)可能失效 (若導致map重分配)總是失效通常不失效
內存使用較高 (塊指針數組+可能空余空間)較低 (但尾部可能有預留空間)較高 (每個元素需額外指針開銷)
緩存友好性中等 (塊內友好)非常好 (整個數組連續) (元素分散)
主要優勢高效雙端操作 + 不錯隨機訪問極致隨機訪問 + 尾部高效插入任意位置高效插入/刪除
主要劣勢中間操作慢,內存開銷稍大頭部/中間插入刪除慢,擴容代價大隨機訪問慢,內存開銷大

7. 簡單代碼示例

#include 
#include 
#include int main() {// 1. 創建 dequestd::deque myDeque = {1, 2, 3}; // {1, 2, 3}// 2. 雙端插入myDeque.push_front(0); // {0, 1, 2, 3}myDeque.push_back(4);  // {0, 1, 2, 3, 4}// 3. 訪問頭部和尾部std::cout << "Front: " << myDeque.front() << std::endl; // 0std::cout << "Back: " << myDeque.back() << std::endl;   // 4// 4. 隨機訪問 (索引 2)std::cout << "Element at index 2: " << myDeque[2] << std::endl; // 2// 安全隨機訪問 (索引 2)std::cout << "Element at index 2 (using at): " << myDeque.at(2) << std::endl; // 2// 5. 雙端刪除myDeque.pop_front(); // {1, 2, 3, 4}myDeque.pop_back();  // {1, 2, 3}// 6. 遍歷 (使用迭代器)std::cout << "Deque contents: ";for (auto it = myDeque.begin(); it != myDeque.end(); ++it) {std::cout << *it << " ";}std::cout << std::endl; // Output: 1 2 3// 7. 中間插入 (效率較低!)auto it = myDeque.begin() + 1; // 指向索引1的元素 '2'myDeque.insert(it, 99);       // {1, 99, 2, 3}// 8. 中間刪除 (效率較低!)it = myDeque.begin() + 2;     // 指向索引2的元素 '2' (在99之后)myDeque.erase(it);            // {1, 99, 3}// 9. 輸出最終結果std::cout << "Final deque: ";for (int num : myDeque) { // 范圍for循環std::cout << num << " ";}std::cout << std::endl; // Output: 1 99 3return 0;
}

總結

  • Deque 的核心價值在于雙端操作的高效性 (O(1)) 和良好的隨機訪問能力 (O(1))。 這是它在 STL 容器家族中的獨特定位。
  • 理解其分塊存儲機制是理解其性能特征(為什么兩端快、隨機訪問接近 vector、中間慢)、內存占用和迭代器失效規則的關鍵。
  • 優先在兩端操作。 避免在 deque 中間頻繁插入或刪除,這比在 list 中做同樣操作代價高得多(O(n) vs O(1))。如果需要頻繁在中間操作,std::list 是更合適的選擇。
  • 隨機訪問很快,但比 vector 略慢。 如果你需要極致的隨機訪問速度,并且插入刪除只在尾部進行,std::vector 仍然是最優選擇。
  • 注意內存使用。 Deque 通常比 vector 占用更多內存,因為它有管理塊的開銷(指針數組)和塊內可能的空間浪費。
  • 小心迭代器失效。 特別是在進行 push_front/push_back(可能使所有迭代器失效)和中間插入刪除(使局部迭代器失效)之后。引用和指針指向的元素內容在兩端操作后通常仍然有效,這是 deque 一個非常有用且重要的特性。
  • 明智選擇容器: 沒有“最好”的容器,只有“最合適”的。根據你的具體需求(操作的位置、頻率、是否需要隨機訪問、內存限制、迭代器穩定性要求)來權衡選擇 vector、deque 或 list。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/90520.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/90520.shtml
英文地址,請注明出處:http://en.pswp.cn/web/90520.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

GNU到底是什么,與Unix和Linux是什么關系

GNU&#xff08;發音為 /ɡnu?/&#xff0c;類似“革奴”&#xff09;是一個自由軟件操作系統項目&#xff0c;由理查德斯托曼&#xff08;Richard Stallman&#xff09;于1983年發起&#xff0c;目標是創建一個完全由自由軟件組成的類Unix操作系統。它的名字是一個遞歸縮寫&a…

雙指針算法介紹及使用(下)

在上一篇文章中我們已經對雙指針有了一定了解&#xff0c;接下來我們通過題目來對雙指針進行更好的理解。 1. leetcode 202. 快樂數 這道題使用的方法是快慢指針&#xff0c; 比如說一個數X&#xff0c;那么創建兩個變量X1和X2&#xff0c;然后X1每次變化兩次&#xff0c;X2變化…

Elasticsearch整合:Repository+RestClient雙模式查詢優化

Elasticsearch整合&#xff1a;RepositoryRestClient雙模式查詢優化Elasticsearch 雙模式查詢優化&#xff1a;Repository RestClient 整合指南一、架構設計&#xff1a;雙模式協同工作流二、Repository 模式&#xff1a;快速開發最佳實踐2.1 基礎配置2.2 高級特性&#xff1a…

Elasticsearch 高級查詢語法 Query DSL 實戰指南

目錄 1、DSL 概述 1.1 DSL按照查詢的結構層次劃分 1.2 DSL按照檢索功能的用途和特性劃分 1.3 示例數據準備 2、match_all ——匹配所有文檔 3、精確匹配 3.1 term——單字段精確匹配查詢 3.2 terms——多值精確匹配 3.3 range——范圍查詢 3.4 exists——是否存在查詢…

DNS 服務正反向解析與 Web 集成實戰:從配置到驗證全流程

DNS 服務正反向解析配置全流程指南 一、前言 在網絡環境中&#xff0c;DNS&#xff08;Domain Name System&#xff09;服務起著至關重要的作用&#xff0c;它負責將域名解析為 IP 地址&#xff0c;以及將 IP 地址反向解析為域名。本文將詳細介紹如何配置 DNS 服務的正反向解析…

2025.07.25【宏基因組】|PathoScope 安裝與使用指南

PathoScope 安裝與使用指南&#xff1a;微生物組數據分析利器 作為一名生物信息工程師&#xff0c;在微生物組數據分析中&#xff0c;我們常常需要高效、準確的工具來鑒定和量化樣本中的微生物組成。PathoScope 正是這樣一款強大的工具&#xff0c;它能夠幫助我們從高通量測序…

AI結對編程:分布式團隊的集體記憶外腦

AI結對編程:分布式團隊的集體記憶外腦 “當新人通過AI瞬間掌握三年積累的業務規則時,傳統‘傳幫帶’模式正式宣告過時——分布式團隊最珍貴的資產不再是代碼,而是被AI固化的集體經驗。” 一、人腦的帶寬困局 柏林新人加入新加坡支付團隊,面臨恐怖的知識迷宮: - …

棧----1.有效的括號

20. 有效的括號 - 力扣&#xff08;LeetCode&#xff09; /** 括號特性: 左括號必定先出現,每個左括號都需要一個右括號與之匹配,后出現的左括號先匹配 解法: 依據后出現的左括號先匹配,很容易聯想到棧,即后進先出 遍歷字符串,遇到左括號就在棧中添加一個對應的右括號 遇到右括…

數據報表怎么自動填寫內容?總結了幾個方法

你有沒有遇到過這種情況&#xff1f;月底趕銷售報告&#xff0c;Excel里密密麻麻的數據要往Word里搬&#xff0c;光是復制粘貼就折騰半小時&#xff0c;好不容易搞完&#xff0c;老板突然說數據有更新…得&#xff0c;全白干&#xff01;更崩潰的是&#xff0c;這種重復勞動每個…

構造函數是否可以聲明成虛函數?

構造函數&#xff08;constructor&#xff09;不能被聲明為虛函數。? 原因解釋 構造函數的主要職責是創建并初始化對象本身&#xff0c;而虛函數機制是基于 虛表指針&#xff08;vptr&#xff09; 的&#xff0c;它只有在對象構造完成之后才會起作用。 所以&#xff1a; 在構造…

【Rust線程池】如何構建Rust線程池、Rayon線程池用法詳細解析

?? 歡迎大家來到景天科技苑?? &#x1f388;&#x1f388; 養成好習慣&#xff0c;先贊后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者簡介&#xff1a;景天科技苑 &#x1f3c6;《頭銜》&#xff1a;大廠架構師&#xff0c;華為云開發者社區專家博主&#xff0c;…

CAN總線網絡的參數協同:從一致性要求到容差邊界

CAN總線網絡的參數協同&#xff1a;從一致性要求到容差邊界 一、引言&#xff1a;CAN總線的“隱形契約”二、CAN通信的核心參數&#xff1a;不止于波特率三、參數一致性的必要性&#xff1a;為何波特率相同仍會失敗&#xff1f;四、容差范圍的科學界定&#xff1a;從理論計算到…

Activity 啟動模式

如何指定 Activity 的啟動模式&#xff1f;在 AndroidMainfest.xml 中通過給 <activity> 標簽指定 android:lauchMode 來選擇啟動模式。4種啟動模式standard&#xff08;默認&#xff09;&#xff1a;每當啟動一個 Activity&#xff0c;都會創建一個新的實例壓入返回棧。…

7·22勝算云AI日報:OpenAI再擴容且與英國政府簽訂三年AI計劃、字節GR-3、微軟Culture計劃、國數局數據基地

OpenAI Oracle&#xff1a;4.5 GW「Stargate II」再擴容&#xff0c;AI 電力版圖重排 7 月 22 日&#xff0c;OpenAI 與 Oracle 聯合公布“Stargate II”計劃&#xff1a;雙方將在美國多地追加 4.5 GW 超算級電力與冷卻配套&#xff0c;使 Stargate 系列園區總規模躍升至 5 GW…

【優選算法】鏈表

目錄鏈表常用的技巧和操作1、常用技巧2、常用操作一、[兩數相加](https://leetcode.cn/problems/add-two-numbers/description/)二、[兩兩交換鏈表中的節點](https://leetcode.cn/problems/swap-nodes-in-pairs/description/)三、[重排鏈表](https://leetcode.cn/problems/reor…

制造業新突破:AR 培訓系統助力復雜操作輕松上手?

在制造業&#xff0c;生產設備復雜、操作流程繁瑣&#xff0c;新員工掌握操作技能不易。比如汽車制造企業的發動機裝配環節&#xff0c;涉及眾多精密零部件安裝&#xff0c;對安裝順序、位置精度要求嚴格&#xff0c;一點小失誤都可能影響發動機性能甚至引發質量問題。過去新員…

《計算機網絡》實驗報告八 加密、數字簽名與證書

目 錄 1、實驗目的 2、實驗環境 3、實驗內容 3.1 對稱加密 3.2 散列函數 3.3 非對稱加密 3.4 數字簽名 3.5 證書 4、實驗結果與分析 4.1 對稱加密 4.2 散列函數 4.3 非對稱加密 4.4 數字簽名 4.5 證書 5、實驗小結 5.1 問題與解決辦法&#xff1a; 5.2 心得體…

MySQL(157)如何分析和優化存儲過程?

分析和優化存儲過程是數據庫性能優化的重要環節。通過對存儲過程進行分析和優化&#xff0c;可以提高數據庫操作的執行效率&#xff0c;減少資源消耗&#xff0c;改善系統整體性能。以下是詳細的步驟和代碼示例&#xff0c;介紹如何分析和優化 MySQL 存儲過程。 一、分析存儲過…

基于深度學習的胸部 X 光圖像肺炎分類系統(一)

本文先重點介紹了過采樣的原理是實現。 由于醫學數據相對缺乏&#xff0c;過采樣是解決數據問題的方法之一。 后續寫一篇搭建神經網絡的說明 目錄 概述 導入必要的庫 數據加載和預處理函數 處理樣本不均衡函數 構建改進的 CNN 模型函數 主函數 數據生成器generator&…

【PGCCC】在 Postgres 中構建復制安全的 LSM 樹

在原生 Postgres 實現中&#xff0c;全文搜索由B 樹或GIN&#xff08;廣義倒排索引&#xff09;結構支持。這些索引針對相對快速的查找進行了優化&#xff0c;但受限于 B 樹的寫入吞吐量。 當我們構建pg_searchPostgres 搜索和分析擴展時&#xff0c;我們的優先級有所不同。為了…