ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++ : 手写简化版 list 完整实现与高频面试题

C++ : 手写简化版 list 完整实现与高频面试题 一、手写简化版 list 完整实现list的核心架构双向循环链表 哨兵节点 双向迭代器覆盖双端增、删、任意位置插入、删除、链表拼接等核心能力。#includeiostream#includecassert#includeutility#includeiterator// 链表节点结构体前驱指针 后继指针 数据templatetypenameTstructListNode{ListNode*prev;ListNode*next;T data;// 支持原地构造元素templatetypename...ArgsexplicitListNode(Args...args):prev(nullptr),next(nullptr),data(std::forwardArgs(args)...){}};// 双向迭代器封装节点指针实现迭代器语义templatetypenameTclassListIterator{public:// 迭代器类型定义符合 STL 规范usingiterator_categorystd::bidirectional_iterator_tag;usingvalue_typeT;usingdifference_typeptrdiff_t;usingpointerT*;usingreferenceT;ListNodeT*node;explicitListIterator(ListNodeT*n):node(n){}// 解引用与成员访问referenceoperator*()const{returnnode-data;}pointeroperator-()const{return(node-data);}// 前置自增ListIteratoroperator(){nodenode-next;return*this;}// 后置自增ListIteratoroperator(int){ListIterator tmp*this;*this;returntmp;}// 前置自减ListIteratoroperator--(){nodenode-prev;return*this;}// 后置自减ListIteratoroperator--(int){ListIterator tmp*this;--*this;returntmp;}// 相等性比较booloperator(constListIteratorother)const{returnnodeother.node;}booloperator!(constListIteratorother)const{returnnode!other.node;}};// 简化版 list 容器templatetypenameTclassMyList{private:ListNodeT*sentinel;// 哨兵节点哑节点不存有效数据size_t _size;// 元素总数保证 O(1) 求大小public:usingiteratorListIteratorT;// 默认构造初始化哨兵形成自环MyList():_size(0){sentinelnewListNodeT();sentinel-prevsentinel;sentinel-nextsentinel;}// 析构释放所有节点 哨兵~MyList(){clear();deletesentinel;sentinelnullptr;}// 清空所有元素恢复空链表状态voidclear(){ListNodeT*cursentinel-next;while(cur!sentinel){ListNodeT*tmpcur;curcur-next;deletetmp;}sentinel-prevsentinel;sentinel-nextsentinel;_size0;}// 迭代器接口 iteratorbegin(){returniterator(sentinel-next);}iteratorend(){returniterator(sentinel);}// 容量接口 boolempty()const{return_size0;}size_tsize()const{return_size;}// 元素访问 Tfront(){assert(!empty()front called on empty list);returnsentinel-next-data;}Tback(){assert(!empty()back called on empty list);returnsentinel-prev-data;}// 核心插入操作 // 在 pos 位置之前插入元素返回指向新元素的迭代器iteratorinsert(iterator pos,constTvalue){ListNodeT*new_nodenewListNodeT(value);ListNodeT*curpos.node;ListNodeT*prev_nodecur-prev;// 四步指针操作完成插入头尾位置逻辑完全一致new_node-prevprev_node;new_node-nextcur;prev_node-nextnew_node;cur-prevnew_node;_size;returniterator(new_node);}voidpush_back(constTvalue){insert(end(),value);}voidpush_front(constTvalue){insert(begin(),value);}// 核心删除操作 // 删除 pos 位置的元素返回指向被删元素下一个的迭代器iteratorerase(iterator pos){assert(pos!end()erase called on end iterator);ListNodeT*curpos.node;ListNodeT*prev_nodecur-prev;ListNodeT*next_nodecur-next;// 前后节点直接互连跳过被删节点prev_node-nextnext_node;next_node-prevprev_node;deletecur;--_size;returniterator(next_node);}voidpop_back(){assert(!empty()pop_back called on empty list);erase(--end());}voidpop_front(){assert(!empty()pop_front called on empty list);erase(begin());}// 链表专属splice 拼接 // 将 other 整个链表的节点移动到 pos 位置之前other 变为空// 零拷贝、零构造析构纯指针操作voidsplice(iterator pos,MyListother){if(other.empty())return;if(thisother)return;// 自身拼接无意义ListNodeT*other_headother.sentinel-next;ListNodeT*other_tailother.sentinel-prev;ListNodeT*pos_nodepos.node;ListNodeT*pos_prevpos_node-prev;// 断开 other 的哨兵恢复空链表状态other.sentinel-nextother.sentinel;other.sentinel-prevother.sentinel;// 将 other 的节点接入当前链表pos_prev-nextother_head;other_head-prevpos_prev;other_tail-nextpos_node;pos_node-prevother_tail;// 更新计数_sizeother._size;other._size0;}// 链表的翻转voidreverse()noexcept{// reverse sequence// 空链表或者仅有一个元素无需反转if(_size1)return;ListNodeT*cursentinel-next;while(cur!sentinel){ListNodeT*next_nodecur-next;// 临时存储下一个元素std::swap(cur-prev,cur-next);// 交换当前元素的前后指针指向curnext_node;// 向下一个元素偏移}//交换哨兵元素的前后指针std::swap(sentinel-next,sentinel-prev);}};// 测试用例 intmain(){MyListintlst;// 测试双端插入for(inti1;i5;i)lst.push_back(i);// 尾插 1 2 3 4 5for(inti0;i-2;--i)lst.push_front(i);// 头插 -2 -1 0std::cout双端插入后元素总数lst.size()\n;std::cout顺序遍历;for(autoitlst.begin();it!lst.end();it){std::cout*it ;}std::cout\n\n;// 测试中间插入与删除autoitlst.begin();it;it;// 指向第3个元素lst.insert(it,100);// 在第3个位置前插入100std::cout中间插入100后;for(intx:lst)std::coutx ;std::cout\n;lst.erase(--lst.end());// 删除最后一个元素std::cout删除尾元素后;for(intx:lst)std::coutx ;std::cout\n\n;// 测试 splice 拼接MyListintother;other.push_back(10);other.push_back(20);other.push_back(30);lst.splice(lst.begin(),other);std::coutsplice拼接后当前list;for(intx:lst)std::coutx ;std::cout\n拼接后other是否为空std::boolalphaother.empty()\n;lst.reverse();std::coutreverse翻转后当前list:;for(intx:lst)std::coutx ;std::coutstd::endl;return0;}说明本实现为教学简化版与标准库的差异主要在于未使用分配器、未实现 const 迭代器与异常安全核心指针操作、哨兵设计、迭代器逻辑与 STL 源码完全对齐。二、list 高频面试题与标准答案基础概念类STL list 的底层数据结构是什么答底层是双向循环链表并通过哨兵节点哑节点统一边界处理。每个节点包含前驱指针、后继指针和数据元素容器本身只持有哨兵节点指针哨兵的 next 指向头节点、prev 指向尾节点首尾相连形成闭环。list 属于什么迭代器类型支持哪些操作答双向迭代器bidirectional_iterator仅支持前后逐个移动、--不支持随机跳跃、-、下标访问因此无法直接使用std::sort等要求随机访问迭代器的算法。list 是顺序容器吗和 vector、deque 的本质区别是什么答list 是顺序容器元素按插入顺序线性排列。本质区别在于内存布局vector整块连续内存随机访问极快中间插删慢deque分段连续内存双端插删快支持随机访问但常数开销大list离散的链表节点不支持随机访问已知迭代器时任意位置插删 O(1)底层原理类为什么 list 要使用哨兵节点有什么好处答哨兵节点是不存储有效数据的虚拟节点始终存在并首尾相连形成闭环。核心好处消除边界特判头插、尾插、中间插入的指针操作逻辑完全一致不需要单独处理空链表、头节点、尾节点等边界场景简化迭代器语义end()直接指向哨兵天然匹配“最后一个元素的下一个位置”减少代码分支降低出错概率list 的size()是 O(1) 还是 O(n)答C11 标准强制要求所有容器的size()为 O(1)因此主流实现GCC libstdc 等都会在 list 中维护一个计数成员变量插入删除时同步更新。C11 之前的旧实现没有 size 成员调用size()需要遍历整个链表计数时间复杂度 O(n)。详细说明 list 的迭代器失效规则答list 是所有 STL 容器中迭代器稳定性最强的插入操作所有已有迭代器、引用、指针全部保持有效不受任何影响删除操作仅被删除节点对应的迭代器、引用、指针失效其余节点全部不受影响根本原因是链表增删只修改节点指针不移动已有节点的内存地址。list 的splice操作是什么时间复杂度是多少答splice是 list 独有的链表拼接能力可以将另一个 list 的单个节点、区间或整个链表直接移动到当前 list 的指定位置。全程纯指针操作不拷贝元素零构造析构开销。时间复杂度整个链表转移、单个节点转移O(1)区间转移O(k)k 为区间元素个数用于统计数量更新 size 成员C11 之前没有 size 成员所有 splice 均为 O(1)。为什么 list 要自己实现sort、merge、unique等成员函数答分为两类原因通用算法无法使用std::sort要求随机访问迭代器list 只有双向迭代器不满足要求通用算法效率更低std::merge、std::remove等基于“拷贝/赋值元素”实现而 list 可以直接移动节点指针零拷贝性能更高因此 list 专门实现了成员版本充分发挥链表结构的优势。list::sort的底层实现是什么答主流 STL 实现采用迭代版自底向上归并排序空间复杂度 O(1)仅维护固定大小的指针数组不额外分配节点时间复杂度 O(n log n)是稳定排序。它维护一个指针数组每个位置对应一条长度为 2^i 的有序子链表逐个将节点合并进对应层级最终拼接出完整有序链表。对比与选型类工程上为什么默认优先用 vector 而不是 list答核心原因是缓存友好性的巨大差距vector 的连续内存可以被 CPU 缓存预取遍历命中率极高访问速度快list 的节点散落在堆内存中地址不连续遍历时频繁触发缓存未命中访问主存的耗时是缓存的几十上百倍大多数场景下“找到插入位置的遍历开销”已经抵消了链表插删 O(1) 的理论优势甚至更慢。因此工程上默认优先使用 vector实测证明 list 更优时再切换。什么场景下适合使用 list答满足以下条件时可考虑 list频繁在中间位置插入删除且能快速获取插入位置的迭代器如配合哈希表做索引需要保证元素地址/引用长期稳定不能因增删失效需要频繁进行节点拼接、转移如 LRU 缓存实现元素体积大、拷贝成本极高插删移动代价远大于遍历开销list 和 forward_list 有什么区别答forward_list是 C11 引入的单链表与 list 的核心差异list 是双向链表支持双向迭代头尾增删均为 O(1)forward_list 是单向链表仅支持正向遍历没有size()成员保证零额外开销只支持头插和指定位置之后的插入forward_list 更轻量内存开销更小但功能受限适合极致追求空间、仅需单向操作的场景细节与坑点类空链表的begin()和end()是什么关系答二者相等。空链表中哨兵节点的prev和next都指向自己因此begin()指向哨兵的 next即哨兵自身end()也指向哨兵完全符合 STL 迭代器语义。对 list 执行sort后迭代器会失效吗答不会失效。list::sort通过修改节点指针重排链表不移动、不拷贝元素本身所有节点的内存地址不变因此迭代器、引用、指针全部保持有效仅元素顺序发生变化。list 每个节点的额外内存开销是多少答64 位系统下每个节点包含两个指针prev next共 16 字节的额外开销再加上数据本身的大小。若存储int4 字节额外开销高达 300%内存利用率极低若存储大对象如几百字节的结构体指针开销占比可忽略。
返回列表