开发成长之路(4)-- C++从入门到开发(C++知名库:STL入门·容器

STL,虽然是一套程序库,但却不仅仅是一套一般印象中的程序库,而是一个具有划时代意义的、有着深厚理论基础的发明。

说是软件组件史上的一大突破,也当之无愧。

为了建立数据结构与算法的一套标准,降低其间的耦合关系,以及提升各自的交互性、弹性、独立性,C++社群中诞生了STL.

STL是一个开源项目,所以有很多个版本。我讲解及使用的是SGI STL版本,不论是符号命名,还是编码风格上,这个版本的可读性非常高。


STL可不止有容器

对于大部分接触过STL的人来说,对于STL的印象应该是极好的,不过大部分人可能也是简单的将容器和STL的全部画起了等号,最多再加上算法,毕竟我们使用STL常用到的也就那两套头文件。说实话我也前也是这么认为的。

其实STL提供了六大组件,容器和算法只是其中一部分,它们分别是:

容器、算法、迭代器、仿函数、配接器、配置器。

这些组件都是什么?

不要急,就算知道也再看一遍吧。

  • 容器

  • 各种数据结构,如Vector、List、Map,用于存放数据。

  • 算法

  • 各种常见算法如:排序、增删查等。从实现来看,STL算法属于泛型函数。

  • 迭代器

  • 很惊奇,迭代器不属于容器,也不属于算法。

  • 扮演起容器与算法之间的“粘合剂”,是“泛型指针”。

  • 原生指针可以作为一种迭代器,不过迭代器一般是以智能指针的形式存在的。

  • 仿真函数

  • 行为类似函数,从实现来看是一种重载了operator()的类或模板类。

  • 函数指针可视为狭义上的仿真函数。

  • 配接器

  • 说来话长,一种用于修饰容器、迭代器、仿真函数的东西。

  • 配置器

  • 空间配置与管理,如果要深入了解STL代码,则这一块将会是奠基石一般的存在。


来看一下STL六大组件联合工作的图示:

在这里插入图片描述


STL的序列式容器容器


源码之前,了无秘密

曾经面试官问过我这么一个问题:请你描述一下,STL中的所有容器,它们的底层实现机制、它们增删查改的时间复杂度是多少。

当时回答的迷迷糊糊的。本篇,就围绕这个话题展开。

Vector

什么是Vector?可以理解为是动态数组。

Vector所采用的数据结构非常简单,连续线性空间。

template <class T,class Alloc * alloc> //模板,后面会专门出一篇写C++的模板编程

class vector{

···

protected:

iterator start; //表示目前使用空间的头

iterator finish; //表示目前使用空间的尾

iterator end_of_storage; //表示目前可用的空间的尾

···

}

为了降低空间配置的时间成本,vector实际配置的大小可能会比客户端需求的量更大一些,以备将来扩充的可能。

看图:

在这里插入图片描述

运用这三个算子,可以很轻易的实现一些功能:

template <class T,class Alloc * alloc>

class vector{

···

public:

iterator begin(){return start;}

iterator end(){return finish;}

size_type size() const{return size_type(end() - begin());}

size_type capacity() const{return size_type(end_of_storage - begin());} //还剩多少空间

bool empty() const{return begin() == end();}

reference operator[](size_type n){return *(begin()+n);} //下标取值法

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

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

//返回首尾地址

···

}


再来看一些常用函数的底层实现:

void push_back(const T& x){

if(finish != end_of_storage){ //还有备用空间

construct(finish,x); //一个插入函数,暂时知道这个就够了,偏底层

++finish;

}

else{ //空间不够用了

insert_aux(end(),x); //更底层了,看上面那张图

}

}

注意:一旦引起空间重新配置,指向原Vector的所有迭代器都将失效!!!


pop_back() 删除末端元素:

void pop_back(){

–finish;

destroy(finish); //这个destory后面讲空间配置器的时候会讲到

//就是这么简单

}

erase() 清除(first,last)中所有元素:

先看图:

在这里插入图片描述

iterator erase(iterase first,iterase last){

iterator i = copy(last,finish,first) //copy后面的篇章会有,先克服一下困难

destroy(i,finish);

finish = finish - (last - first);

return first;

}

erase() 清除某个位置上的元素:

iterator erase(iterator position){

if(position +1 != end())

copy(position +1,finish,position)

–finish;

destroy(finish);

return position;

}

clear() 方法:

void clear(){

erase(begin(),end());

}


insert() 插入操作:

这个操作的代码实在是太多的底层细节了,还是看图吧:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


List

Vector可以用数组的知识来覆盖,那么List,就用链表的知识来覆盖吧。

这里要注意:

list对于任何元素的插入和删除,永远都是常数时间,我也得去一探究竟啦,我当初回答错了。

先看看数据结构:

template

struct __list_node{

typedef void* void_pointer;

void_pointer prev;

void_pointer next;

T data;

}

双向链表!!!

list的迭代器和vector的不同,它的要求更高一些。因为链表使用的存储空间往往是零零散散的,所以list的迭代器必须有能力在杂乱的存储空间中快速的跳转。

相对于Vector,List还有一个优势,就是不论如何的插入和接合操作,都不会造成原有的List迭代器失效。List的删除操作也只有指向那个被删除的元素的迭代器失效,其它迭代器不会受影响。

在这里插入图片描述

SGI list不仅仅是一个双向链表,还是个环状双向链表,所以它只需要一个指针,便可以完整的表现链表。

template <class T,class Alloc = alloc> //缺省使用alloc作为配置器

class list{

protected:

typedef __list_node list_node;

public:

typedef list_node* link_type;

protected:

link_type node; //只要一个指针,便可以表示整个循环链表

如果让指针node指向刻意置于尾端的一个空白节点,node便能符合STL对于“前闭后开”的区间要求,成为list迭代器。

iterator begin(){return (link_type)((*node).next);}

iterator end(){return node;}

bool empty() const{return node->next == node;}

size_type size() const{

size_type result = 0;

disance(begin(),end(),result); //一个全局函数

return result;

}

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

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

在这里插入图片描述


关于list的增删改查,其实跟链表也就差不多了。


deque


vector是单向开口的连续线性空间,deque则是一种双向开口的连续线性空间。

所谓双向开口,也就是说可以在头尾两端进行插入和删除操作。

vector当然也可以做头部操作,但是其头部操作效率奇差!!!

无法被接受。

(这点以前居然都没有发现!!!)

在这里插入图片描述

deque没有所谓容量的观念,因为它是动态的以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。因此,deque没有必要提供所谓的空间保留功能。

但是呢,为什么我们更多的选用vector而非deque呢?因为它的指针实在是太麻烦了。我们后面就知道了。

除非必要,我们应尽可能的选择使用vector而非deque。对deque进行的排序操作,为了最高效率,可以将deque完整的复制到一个vector身上,将vector排序后,再复制回deque。

不要被事务的表面现象锁迷惑,你看它是分段连续线性空间,就以为它是vector和list的结合体,取长补短?其实不然。

为了维持整体连续的假象,数据结构的设计及迭代器前进后退等操作都颇为繁琐。


deque的中控器

deque采用一块所谓的map映射,来看吧:

template <class T,class Alloc = alloc,size_t BufSiz = 0>

class deque{

public:

typedef T value_type;

typedef value_type* pointer;

···

protected:

//元素的指针的指针

typedef pointer* map_pointer;

protected:

map_pointer map;

//指向map,map是块连续空间,其内的每个元素都是一个指针,指向一块缓冲区

size_type map_size;

//map内可容纳多少指针

}

在这里插入图片描述

在这里插入图片描述

看得我尴尬症都犯了。

此外,deque还维护start、finish两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素(的下一个位置)。

此外,它当然也必须记住目前map的大小,一旦map的空间不足,必须要重新配置一个更大的map。


stack – 栈


什么是栈?怎么说呢,我觉得我真应该先写数据结构专栏。失策失策!!!

栈是一种先进后出的数据结构,它只有一个接口。

只能从一端加入元素,从那一端移除元素,所以并不被允许有其他方法来存取元素。

换言之,stack不允许有遍历行为。

将元素推入stack的方式称为push,将元素退出stack的操作称为pop。

在这里插入图片描述

以某种既有容器作为底部结构,将其接口改变,使之符合“先进后出”的特性,形成一个stack,是很容易做到的。

deque是双向开口数据结构,若以deque为底部结构并封闭其头端开口。

便轻而易举形成了一个stack、

(不知道为什么,我觉得好糟糕哦,vector是不能做吗?)

还是等我过两篇写数据结构的时候再说吧。


除deque之外,list也是双向开口的数据结构。以list为底的stack被称作链栈。

嘿我就挺纳闷儿,为什么就非要双向开口的数据结构???


queue – 队列


队列,是一种先进先出结构,只能从一端加入元素,从另一端移除元素,所以并不被允许有其他方法来存取元素。

换言之,queue不允许有遍历行为。

那这个跟上面的stack其实没多大区别,只不过一个是后进先出,一个是先进先出的罢了。那为什么也要双向开口的数据结构呢?


heap是什么


heap并不属于STL容器组件,它是个“幕后白手”,扮演priority queue的助手。

顾名思义,那个queue允许用户以任何次序插入数据,但是在插入的时候会根据优先级进行排序,以保证取出的时候是按照优先级排序的。

如果以List作为这个优先级队列的底层机制,那么排序将会很麻烦,如果以二叉搜索树的话,未免大材小用了。

而难度夹在中间的binary heap便是不二人选了。

所谓binary heap,就是一种完全二叉树,整棵树除了底层节点外,都是填满的,从左至右又不得又间隙。

苍白无力的文字啊,来看张图实在:

在这里插入图片描述

简单明了吧,可以用想象下面有一个数组来存储所有节点,以树根节点作为数组的[0]位置,可以发现,任何一个节点 [i] 的左子节点必位于 [2i] 处,其右子节点必位于 [2i+1] 处。

而任何一个节点 [k],其父节点必位于 [k/2] 处。

通过这简单的规则,咱就种了一棵树,完全二叉树。

而这颗二叉树需要能动态的增加节点,所以采用vector作为这棵树的底层土壤是个理想的选择。

根据元素排列方式,heap可以分为max-heap和min-heap。STL供应的是max-heap,最大值在头结点。

heap算法

push_heap算法(尾端插入元素)

本来是自己画了图,但是理解了书中的图之后,发现他的图更有一番风味。

在这里插入图片描述

原先我也疑惑于为何同一级中左边的节点会比右边节点大,后来我想明白了。

在插入过程中,这个顺序被打乱是难以避免的,况且这个排序于取出数据并无影响,所以没必要在做额外工作对树的底层做那么精细的排序。

如果还是不理解,先看下去,慢慢的就会茅塞顿开。

在尾部插入时,总是将节点插入到最底层的最右节点,不管你要插入的数据右多大,见上图第一个步骤。

插入之后执行上溯操作,将新节点拿来与父节点进行比较,如果“青出于蓝胜于蓝”,那么将父子节点互换位置。见上图第二个步骤。

之后持续执行上一个步骤,直到不再互换位置。见上图三、四个步骤。

至于下面被打乱的顺序,不用担心,乱中有序。

正是由于这波操作,使得同一级会出现左边的节点比右边的大的情况。

下面来看一下算法的实现细节:

//该函数接受两个迭代器,用来表现一个heap底部容器的头尾,并且新元素已经插入到底部容器的最尾端。

template

inline void push_heap(RandomAccessIterator first,RandomAccessIterator last)

{

__push_heap_aux(first,last,distance_type(first),value_type(first));/这俩type在上一篇提到了,不知道也就算了吧,毕竟上一篇也不短/

}

template <class RandomAccessIterator,class Distance,class T>

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

{

__push_heap(first,Distance((last-first)-1),Distance(0),T(*(last - 1))); //(last-first)-1,容器最尾端

}

template <class RandomAccessIterator,class Distance,class T>

void __push_heap(RandomAccessIterator first,Distance holeIndex,Distance topIndex,T value)

{

Distance parent = (holeIndex - 1)/2; //找出父节点

while(holeIndex > topIndex && *(first+parent)<value) //尚未到达顶端,且父节点小于新值,这个循环将父值不断下调

{

*(first + holeIndex) = *(first + parent); //令新值为父

holeIndex = parent; //调节洞号,向上提升至父节点

parent = (holeIndex -1)/2; //新洞的父节点

}

*(first + holeIndex) = value; //令洞值为新值,完成插入操作

}

看下来,果然会茅舍顿开吧。

pop_heap算法(头部插入元素)

看完上面的插入,可能会有人觉得这样打乱顺序的话取出会有问题,其实会有吗?不知道,看下去。

在这里插入图片描述

还是用书上的图啊。

取出元素时,首先将1根节点拿下来,留下一个洞洞,见上图第一步到第二步。

还要将当前树的最后一个节点拿下来,并将根节点放到尾节点在容器中的位置。见上图步骤二。

接下来将尾节点和原根节点的两个子节点比较大小,将大的那个推上根节点。见上图步骤三。同样留下一个洞洞。

循环这个“向下流放”的过程,直到原尾结点插入树中或者到了最底层。见上图步骤四。

看懂了这个图之后我们来看算法的实现细节:

template

inline void pop_heap(RandomAccessIterator first,RandomAccessIterator last)

{

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

}

template <class RandomAccessIterator,class T>

inline void __pop_heap_aux(RandomAccessIterator first,RandomAccessIterator last,Distance*,T*)

{

__pop_heap(first,last-1,last-1,T(*(last - 1)),diatance_type(first));
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

面试前一定少不了刷题,为了方便大家复习,我分享一波个人整理的面试大全宝典

  • Java核心知识整理

2020年五面蚂蚁、三面拼多多、字节跳动最终拿offer入职拼多多

Java核心知识

  • Spring全家桶(实战系列)

2020年五面蚂蚁、三面拼多多、字节跳动最终拿offer入职拼多多

  • 其他电子书资料

2020年五面蚂蚁、三面拼多多、字节跳动最终拿offer入职拼多多

Step3:刷题

既然是要面试,那么就少不了刷题,实际上春节回家后,哪儿也去不了,我自己是刷了不少面试题的,所以在面试过程中才能够做到心中有数,基本上会清楚面试过程中会问到哪些知识点,高频题又有哪些,所以刷题是面试前期准备过程中非常重要的一点。

以下是我私藏的面试题库:

2020年五面蚂蚁、三面拼多多、字节跳动最终拿offer入职拼多多
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
4v0PUd-1713672560048)]

[外链图片转存中…(img-ih7kt5xt-1713672560048)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

面试前一定少不了刷题,为了方便大家复习,我分享一波个人整理的面试大全宝典

  • Java核心知识整理

[外链图片转存中…(img-GdgBpLG2-1713672560048)]

Java核心知识

  • Spring全家桶(实战系列)

[外链图片转存中…(img-3qBZT3Mk-1713672560048)]

  • 其他电子书资料

[外链图片转存中…(img-MdI9mBTk-1713672560048)]

Step3:刷题

既然是要面试,那么就少不了刷题,实际上春节回家后,哪儿也去不了,我自己是刷了不少面试题的,所以在面试过程中才能够做到心中有数,基本上会清楚面试过程中会问到哪些知识点,高频题又有哪些,所以刷题是面试前期准备过程中非常重要的一点。

以下是我私藏的面试题库:

[外链图片转存中…(img-JXscrHXc-1713672560049)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 25
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值