面经(一)

1、LRU缓存到内存

leetcode146 https://leetcode-cn.com/problems/lru-cache/solution/lruhuan-cun-ji-zhi-by-leetcode-solution/

#include <unordered_map>
using namespace std;

struct DLinkedNode 
{
	int key, value;
	DLinkedNode* prev;
	DLinkedNode* next;
	DLinkedNode() : key(0), value(0), prev(nullptr), next(nullptr) {}
	DLinkedNode(int _key, int _value): key(_key), value(_value), prev(nullptr), next(nullptr){}
};


class LRUCache {
public:
	LRUCache(int _capacity):capacity(_capacity),size(0) {
		// 是由伪头部和伪尾部节点
		head = new DLinkedNode();
		tail = new DLinkedNode();
		head->next = tail;
		tail->prev = head;		
	}

	int get(int key) {
		if (!cache.count(key)) {
			return -1;
		}
		// 如果key存在,先通过哈希表定位,再移到头部
		DLinkedNode* node = cache[key];
		moveToHead(node);
		return node->value;
	}

	void put(int key, int value) {
		if (!cache.count(key)) {
			// 如果key不存在,创建一个新的节点
			DLinkedNode* node = new DLinkedNode(key, value);
			// 添加进哈希表
			cache[key] = node;
			// 添加至双向链表的头部
			addToHead(node);
			++size;
			if (size > capacity) {
				// 如果超出容量,删除双向链表的尾部节点
				DLinkedNode* removed = removeTail();
				// 删除哈希表中对应的项
				cache.erase(removed->key);
				//防止内存泄漏
				delete removed;
				--size;
			}
		}
		else {
			// 如果key存在,先通过哈希表定位,再修改value,并移到头部
			DLinkedNode* node = cache[key];
			node->value = value;
			moveToHead(node);
		}
	}
	void addToHead(DLinkedNode* node) {
		node->prev = head;
		node->next = head->next;
		head->next = node;
	}
	void removeNode(DLinkedNode* node) {
		node->prev->next = node->next;
		node->next->prev = node->prev;
	}
	void moveToHead(DLinkedNode* node) {
		removeNode(node);
		addToHead(node);
	}
	DLinkedNode* removeTail() {
		DLinkedNode* node = tail->prev;
		removeNode(node);
		return node;
	}
	
private:
	unordered_map<int, DLinkedNode*> cache;
	DLinkedNode* head;
	DLinkedNode* tail;
	int size;
	int capacity;

};
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCach* obj = new LRUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key, value);
*/
/*
复杂度分析
时间复杂度:对于put和get都是O(1)。
空间复杂度:O(capacity),因为哈希表和双向链表最多存储capacity+1个元素。
*/

 

 

 

2、最长回文字串

3、YUV格式

1、什么是YUV?
YUV是指亮度参量和色度参量分开表示的像素格式,其中“Y”表示明亮度(Luminance或Luma),也就是灰度值;而“U”和“V”表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。

与我们熟知的RGB类似,YUV也是一种颜色编码方法,主要用于电视系统以及模拟视频领域,它将亮度信息(Y)与色彩信息(UV)分离,没有UV信息一样能够显示完整的图像,只不过是黑白的,这样的设计很好的解决了彩色电视与黑白电视兼容的问题。并且,YUV不像RGB那样要求三个独立的视频信号同时传输,YUV通过一些压缩手段,在用YUV方式传输时,占用的频带就减小了很多。

有的地方还会用YCbCr或者YPbPr来表示,其实概念是一样的。在DVD中,色度信号被存储成Cb和Cr(C代表颜色,b代表蓝色,r代表红色)。

 

2、YUV压缩的基础
由于我们眼睛的视网膜杆细胞多于视网膜的锥细胞,而视网膜的杆细胞是识别亮度的,锥细胞是识别色度的,所以我们的眼睛对于明暗的分辨要比对颜色的分辨要精细,也就是我们眼睛对于亮度的敏感程度要大于色度的敏感程度。那么,我们在存储图像信息时,为了节约空间,就没有必要将所有的色度信息全部存储下来了。

 

3、YUV的存储格式
YUV的格式有两大类:planar和packed。

对于planar的YUV格式,先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是所有像素点的V。

对于packed的YUV格式,每个像素点的Y、U、V都是连续交叉存储的。

 

4、YUV的采样方式
YUV码流的存储格式与其采样方式有密切的关系,主流的采样方式有三种:YUV4:4:4、YUV4:2:2、YUV4:2:0。

下面三个图比较直观的显示了三种采样方式。其中黑点表示采样像素点的Y分量,空心圆表示采样像素点的UV分量。

5、存储方式
<1>YUYV格式(属于YUV422)

YUYV是YUV422采样的存储格式的一种,相邻的两个Y公用其相邻的两个Cb(U)、Cr(V)。对于像素点Y’00、Y’01而言,其Cb、Cr的值均为Cb00、Cr00,其他的像素点的YUV取值依次类推。

<2>UYVY格式(属于YUV422)

<3>YUV422P(属于YUV422)

YUV422P是一种Plane模式,即planar模式,并不是像上面YUV数据交错存储,而是先存储所有的Y分量,然后存储所有的U(Cb)分量,最后存储所有的V(Cr)分量。其每一个像素点的YUV值提取方法也是遵循YUV422格式的最基本提取方法,即两个Y共用一个UV。比如,对于像素点Y’00、Y’01而言,其Cb、Cr的值均为Cb00、Cr00。

<4>YUV420sp

<5>YUV420p

<6>YV12、YU12格式(属于YUV420)

YU12(又称I420)和YV12属于YUV420格式,也是一种Plane模式,将Y、U、V分量分别打包,依次存储。其没一个像素点的YUV数据提取都遵循YUV420格式的提取方式,即4个Y分量共用一组UV。如上图中,Y’00、Y’01、Y’10、Y’11共用Cr00、Cb00,其他以此类推。

注意,YU12与YV12的区别在于是先存U还是先存V。对于YU12来说,存储顺序是YUV,即YCbCr;对于YV12来说,存储顺序是YVU,即YCrCb。所以上图就是YV12了。

<7>NV12、NV21(属于YUV420)

NV21、NV12都属于YUV420格式,是一种two-plane模式,即Y和UV分为两个Plane,但是UV(CbCr)为交错存储,而不是分为三个plane。其提取方式与上面一种类似,即Y’00、Y’01、Y’10、Y’11共用Cr00、Cb00。

注意,NV21与NV12的区别在于,在UV交替的存储中,NV12是UV(CbCr)交替存储,NV21是VU(CrCb)交替存储,所以上图显示的是NV21。而且NV12是IOS的模式,NV21是Android的模式。


参考:YUV格式详解

4、SIFT特征

1.SIFT概述

SIFT的全称是Scale Invariant Feature Transform,是一种具有尺度不变性和光照不变性的特征描述子,也同时是一套特征提取的理论,首次由D. G. Lowe于2004年以《Distinctive Image Features from Scale-Invariant Keypoints[J]》发表于IJCV中。开源算法库OpenCV中进行了实现、扩展和使用。SIFT特征对旋转、尺度缩放、亮度变化等保持不变性,是一种非常稳定的局部特征。

依据原始论文,SIFT算法流程分为4个部分:

1、DoG尺度空间构造(Scale-space extrema detection)

2、关键点搜索与定位(Keypoint localization)

3、方向赋值(Orientation assignment)

4、关键点描述(Keypoint descriptor)

此外再添加第5个部分,用于说明一些需要特殊说明的重要内容。类似游戏攻略的主线和分线。

2.1"尺度"简介

图像处理中的"尺度"可以理解为图像的模糊程度,类似眼睛近视的度数。尺度越大细节越少,SIFT特征希望提取所有尺度上的信息,也就是无论图像是否经过放大缩小都能够提取特征。这种思考,是和人的生理特征类似的,比如我们即使是在模糊的情况下仍然能够识别物体的种类,生理上人体对物体的识别和分类和其尺度(模糊程度)没有直接关系。

高斯函数在图像处理中得到了广泛的运用,源于其本身具有的优良特性(高斯函数的优良特性放在6.1节);在图像处理中对图像构建尺度空间的方法,是使用不同sigma值的高斯核对图像进行卷积操作(注意,sigma就是“尺度”)。

G(x,y,\sigma) 是高斯核,其数学表述为:

G(x,y,\sigma)=\frac{1}{2\pi\sigma^2}e^{-(x^2+y^2)/2\sigma^2}

整个高斯卷积表示为:

其中是原图像,*是卷积符号,对应尺度下的尺度图像,这里简单了解一下,我们看一下在OpenCV中实现不同sigma的高斯卷积的代码和结果

参考:SIFT特征原理简析(HELU版)

参考:SIFT特征

5、自然数欧拉角旋转矩阵

6、SVM核函数

7、STL vector内存清空,clear()并没有清空内存。

1、为什么需要主动释放vector内存

vector其中一个特点:内存空间只会增长,不会减小,援引C++ Primer:为了支持快速的随机访问,vector容器的元素以连续方式存放,每一个元素都紧挨着前一个元素存储。设想一下,当vector添加一个元素时,为了满足连续存放这个特性,都需要重新分配空间、拷贝元素、撤销旧空间,这样性能难以接受。因此STL实现者在对vector进行内存分配时,其实际分配的容量要比当前所需的空间多一些。就是说,vector容器预留了一些额外的存储区,用于存放新添加的元素,这样就不必为每个新元素重新分配整个容器的内存空间。

在调用push_back时,每次执行push_back操作,相当于底层的数组实现要重新分配大小;这种实现体现到vector实现就是每当push_back一个元素,都要重新分配一个大一个元素的存储,然后将原来的元素拷贝到新的存储,之后在拷贝push_back的元素,最后要析构原有的vector并释放原有的内存。


2、怎么释放vector的内存

A、对于数据量不大的vector,没有必要自己主动释放vector,一切都交给操作系统。

B、但是对于大量数据的vector,在vector里面的数据被删除后,主动去释放vector的内存就变得很有必要了!

由于vector的内存占用空间只增不减,比如你首先分配了10000个字节,然后erase掉后面9999个,留下一个有效元素,但是内存占用仍为10000个。所有内存空间是在vector析构时候才能被系统回收。empty()用来检测容器是否为空的,clear()可以清空所有元素。但是即使clear(),vector所占用的内存空间依然如故,无法保证内存的回收。如果需要空间动态缩小,可以考虑使用deque。如果vector,可以用swap()来帮助你释放内存。

就像前面所说的,vector的内存空间是只增加不减少的,我们常用的操作clear()和erase(),实际上只是减少了size(),清除了数据,并不会减少capacity,所以内存空间没有减少。那么如何释放内存空间呢,正确的做法是swap()操作。

标准模板如下

template < class T >  
void ClearVector( vector< T >& vt )   
{  
    vector< T > vtTemp;   
    veTemp.swap( vt );  
}  

也可以简单使用以下操作

vector<Point>().swap(pointVec); //或者pointVec.swap(vector<Point> ())  

参考:C++ 如何快速清空vector以及释放vector内存?

参考:vector内存分配和回收机制

8、全特化,偏特化

模板为什么要特化,因为编译器认为,对于特定的类型,如果你能对某一功能更好的实现,那么就该听你的。

模板分为类模板与函数模板,特化分为全特化与偏特化。全特化就是限定死模板实现的具体类型,偏特化就是如果这个模板有多个类型,那么只限定其中的一部分。

template<typename T1, typename T2>
class Test
{
public:
    Test(T1 i,T2 j):a(i),b(j){cout<<"模板类"<<endl;}
private:
    T1 a;
    T2 b;
};

template<>
class Test<int , char>
{
public:
    Test(int i, char j):a(i),b(j){cout<<"全特化"<<endl;}
private:
    int a;
    char b;
};

template <typename T2>
class Test<char, T2>
{
public:
    Test(char i, T2 j):a(i),b(j){cout<<"偏特化"<<endl;}
private:
    char a;
    T2 b;
};

那么下面3句依次调用类模板、全特化与偏特化:

Test<double , double> t1(0.1,0.2);  
Test<int , char> t2(1,'A');  
Test<char, bool> t3('A',true);  
//依次打印:
//类模板
//全特化
//偏特化

另外函数模板只能全特化不能偏特化

从上面的例子可以看出:

全特化,模板的参数列表为空,表示该特化版本没有模板参数,全部被特化了。
偏特化,模板参数列表中包含未特化的模板参数,同时指定特化额类型,比如上述中char为T1的特化类型。

9、模板泛型

模板是一种对类型进行参数化的工具,模板是泛型编程的基础,而泛型编程指的就是编写与类型无关的代码,是C++中一种常见的代码复用方式。模板分为模板函数模板类;模板函数针对参数类型不同的函数;模板类主要针对数据成员和成员函数类型不同的类。

简单的提及了模板的概念,那么模板究竟是怎样实现的呢?我们先举一个模板函数的例子,比如在c语言和c++中使用频率相当之高的swap函数,以前我们写的swap函数通常是针对某种特定类型的,有了模板,我们便可以写出这样的swap函数:

#include<iostream>
using namespace std;
template<class T>
void Swap(T* x,T* y)
{
	T tmp;
	tmp=*x;
	*x=*y;
	*y=tmp;
}
int main()
{
	int a=10;
	int b=20;
	double c=2.0;
	double d=2.4;
	Swap(&a,&b);
	Swap(&c,&d);
	printf("a=%d  b=%d\n",a,b);
	printf("c=%f  d=%f\n",c,d);
	system("pause");
	return 0;
}

用模板实现的这个Swap函数实现了与类型无关的效果,例如:可以交换double类型数据的交换,也可以实现int ,float一些其他类型的交换,很大程度上减少了重复性代码的编写。

注意:模板的声明和定义只能放在全局,命名空间或者类范围内进行,即不能在局部范围或者函数内部进行,比如不能在main函数中声明和定义一个模板。

模板的一般格式为:template<class 形参名1,class 形参名2,...>

                                返回类型  函数名(参数列表){函数体}

其中template和class是关键字,class可以用typename关键字替代,一般情况下template和class没有什么区别。<>里面的参数叫做模板形参,模板形参和函数形参很像,但是模板形参不能为空,一旦声明了模板函数就可以使用模板函数的形参名声明类中的成员变量和成员函数,即可以在该函数中使用内置类型的地方都可以使用模板形参名。模板形参在调用该模板函数时根据其提供的模板实参类型来初始化模板形参,也就是说一旦编译器确定了实际的模板实参类型就可以称它实例化了函数模板的一个实例。
 

模板类

类模板的一般形式为:

template<class 形参1,class 形参2...>
class 类名
{
};

类模板和函数模板一样都是以template开始后接上模板形参列表组成,模板形参不能为空。一旦声明了类模板就可以使用类模板的形参名声明类中的成员变量和成员函数。

例如:

template<class T>
class A
{
public:
A(T a);//构造
A(const A<T>& a);//拷贝构造
A<T>& operator=(const A<T>& a);//赋值运算符的重载
private:
T _a;
};

这里注意区分模板类的类名和类型:其中A<T>为模板类的类型,A为类名,一般来说,在模板类中,只有拷贝构造函数 和构造函数名必须为类名,其他一般均为类型。模板类实现完成之后,模板类对象可以这样创建:类名<类型>  对象名;比如上面的模板类A,则可以创建一个类型为int的模板类对象a(A<int> a;);在类A后面跟上一个<>并在里面填上相应的类型,这样的话凡是用到模板形参的地方都会被int替换,当有两个模板参数时可以这样实例化:类名<类型1,类型2>  对象名;例如:A<int,double> a;类型之间用逗号分隔即可。

参考:C++编程之模板与泛型

10、STL有哪些迭代器

根据STL中的分类,iterator包括:

  • 输入迭代器(Input Iterator):通过对输入迭代器解除引用,它将引用对象,而对象可能位于集合中。最严格的输入迭代只能以只读方式访问对象。例如:istream。 
  • 输出迭代器(Output Iterator):该类迭代器和Input Iterator极其相似,也只能单步向前迭代元素,不同的是该类迭代器对元素只有写的权力。例如:ostream, inserter。 

以上两种基本迭代器可进一步分为三类:

  • 前向迭代器(Forward Iterator):该类迭代器可以在一个正确的区间中进行读写操作,它拥有Input Iterator的所有特性,和Output Iterator的部分特性,以及单步向前迭代元素的能力。
  • 双向迭代器(Bidirectional Iterator):该类迭代器是在Forward Iterator的基础上提供了单步向后迭代元素的能力。例如:list, set, multiset, map, multimap。
  • 随机迭代器(Random Access Iterator):该类迭代器能完成上面所有迭代器的工作,它自己独有的特性就是可以像指针那样进行算术计算,而不是仅仅只有单步向前或向后迭代。例如:vector, deque, string, array。 

1 Input Iterators
Input Iterator只能逐元素的向前遍历,而且对元素是只读的,只能读取元素一次。通常这种情况发生在从标准输入设备(通常是键盘)读取数据时。

下面是Input Iterator的可用操作列表:
*iter: 只读访问对应的元素 

iter->member: 只读访问对应元素的成员 
++iter: 向前遍历一步(返回最新的位置) 
iter++: 向前遍历一步(返回原先的位置) 
iter1 == iter2: 判断两个迭代器是否相等 
iter1 != iter2:判断两个迭代器是否不等 
TYPE(iter): 复制迭代器 

2 Output Iterators
Output iterator跟Input Iterator相对应,只能逐元素向前遍历,而且对元素是只写的(*iter操作不能作为右值,只能作为左值),只能写入元素一次。通常这种情况发生在向标准输出设备(屏幕或者打印机)写入数据时,或者利用inserter向容器中追加新元素时。

//下面是Output Iterator的可用操作列表:
*iter = value: 向对应的元素写入新值 
++iter: 向前遍历一步(返回最新的位置) 
iter++: 向前遍历一步(返回原先的位置) 
TYPE(iter): 复制迭代器 

3 Forward Iterators
Forward Iterator是Input Iterator和Output Iterator的结合,虽然也只能逐元素向前遍历,但可以对元素进行读写操作。下面看Forward Iterator的可用操作列表:

*iter:  
iter->member:  
++iter:  
iter++:  
iter1 == iter2:  
iter1 != iter2:  
TYPE():  
TYPE(iter):  
iter1 = iter2:  跟Input Iterator和Output Iterator不同的是,Forward Iterator可以对同一元素访问多次。


下面我们特别关注一下Forward Iterator和Output Iterator的区别:
(1)对于Output Iterator,写入数据时不检查目标容器是否到达结束位置是正确的做法,比如下面循环对于Output Iterator是成立的:

//ok for output iterator

//error for forward iterator
while(true) 
{
    *pos = foo();
    ++pos;
}


(2)对于Forward Iterator,则必须保证访问元素的有效性,那么上面形式对Forward Iterator来说是错误的,因为当碰到容器end()位置时,导致不确定的后果。对于Forward Interator,上面形式必须修改为这样:

while(pos != col1.end()) 
{
    *pos = foo();
    ++pos;
}

4 Bidirectional Iterators
双向迭代器行为特征类似于Forward Iterator,只是额外增加了一个逐元素向后遍历的能力。所以对于双向迭代器可用的操作,除了包含Forward Iterator的所有操作外,多了一组向后遍历的操作:

--iter: 向后遍历一步(返回最新的位置) 
iter--: 向后遍历一步(返回原有的位置)

 5 Random Access Iterators
随机访问迭代器除了有双向迭代器的能力特征外,还可以进行元素随机访问。所以对于随机访问迭代器,增加了关于“迭代器运算”的一些操作。下面是除了双向迭代器的所有操作外,额外的操作列表:

iter[n]: 直接访问索引为n的元素 
iter+=n: 向前或向后(n为负数)遍历n个元素 
iter-=n: 先后或向前(n为负数)遍历n个元素 
iter+n: 返回当前位置后面第n个元素的iterator位置 
n+iter: 同上 
iter-n: 返回当前位置前面第n个元素的iterator位置 
iter1-iter2: 返回iter1和iter2之间的距离(distance) 
iter1<iter2: 判断iter1是否在iter2之前 
iter1>iter2: 判断iter1是否在iter2之后 
iter1<=iter2: 判断iter1是否不再iter2之后 
iter1>=iter2: 判断iter1是否不再iter2之前

参考:STL里面的五种迭代器

参考:C++中STL各个迭代器详解

11、多继承内存分布

具体看参考,只记录一下结论

11.1、普通类的内存排布方式,成员变量依据声明的顺序进行排列,类内偏移从0开始,普通成员函数不占内存空间。这里没有考虑char等成员变量的字节对齐方式等问题。

11.2、编译器是将虚表指针{vfptr}放在类内存的开始处,从图可知其偏移量为0,接着放置类其他成员变量。

11.3、在非虚继承下:
(1) 派生类会继承基类的全部,包括虚基指针。
(2) 派生类和基类会各自维护一个虚函数表,他们不相同,不是同一张表。

11.4、在虚继承下:
(1) 派生类会继承基类的全部,包括虚基指针。但是不是将其放在类内存的地址偏移0处。派生类会新生成虚指针,放在类内存地址偏移量为0处。
(2) 派生类有2个虚指针,对应有两张虚函数表。

 

参考:C++ 类 内存分布 虚函数 单继承 多继承

12、cast有哪四种

C++各cast类型转换

static_cast基础类型(基础类型指针不行)和具有继承关系的类对象指针/引用互转。
dynamic_cast只能转换具有继承关系指针或引用,且只能由子类转为父类(类型安全)
const_cast增加或去除变量的const属性
reinterpret_cast强制转换所有类型

 

关注点:

1、清楚地知道要转变的变量,转换前是什么类型,转换后是什么类型,以及转换后有什么后果。

2、一般情况下,不建议类型转换,避免进行类型转换。

参考:C++中四种cast的用法及区别

参考:

13、ncnn等框架

轻量级推理框架及其特点:

 

 

14、VGG等深度学习发展

15、池化作用、不同卷积核混用原因、空洞卷积

16、STL中的容器

各大容器的特点:
1.可以用下标访问的容器有(既可以插入也可以赋值):vector、deque、map;

特别要注意一下,vector和deque如果没有预先指定大小,是不能用下标法插入元素的!

2. 序列式容器才可以在容器初始化的时候制定大小,关联式容器不行;

3.注意,关联容器的迭代器不支持it+n操作,仅支持it++操作。
 

16.1、顺序性容器(vector、deque、list)

一、vector

当需要使用数组的情况下,可以考虑使用vector

 1.特点:

 (1) 一个动态分配的数组(当数组空间内存不足时,都会执行: 分配新空间-复制元素-释放原空间);

 (2) 当删除元素时,不会释放限制的空间,所以向量容器的容量(capacity)大于向量容器的大小(size);

 (3) 对于删除或插入操作,执行效率不高,越靠后插入或删除执行效率越高;

 (4) 高效的随机访问的容器。

 

 2.创建vecotr对象:

 (1) vector<int> v1;

 (2) vector<int> v2(10);  

 3.基本操作:
 

 v.capacity();  //容器容量

 v.size();      //容器大小

 v.at(int idx); //用法和[]运算符相同

 v.push_back(); //尾部插入

 v.pop_back();  //尾部删除

 v.front();     //获取头部元素

 v.back();      //获取尾部元素

 v.begin();     //头元素的迭代器

 v.end();       //尾部元素的迭代器

 v.insert(pos,elem); //pos是vector的插入元素的位置

 v.insert(pos, n, elem) //在位置pos上插入n个元素elem

 v.insert(pos, begin, end);

 v.erase(pos);   //移除pos位置上的元素,返回下一个数据的位置

 v.erase(begin, end); //移除[begin, end)区间的数据,返回下一个元素的位置

 reverse(pos1, pos2); //将vector中的pos1~pos2的元素逆序存储

二分查找
 

#include<iostream>

#include<algorithm>

#include<vector>

#include<deque>

#include<map>

#include<cstring>

using namespace std;

int  main()

{
    vector<int> v(10);

    int num;

    vector<int>::iterator beg = v.begin();

    vector<int>::iterator end = v.end();

    vector<int>::iterator mid = v.begin() + (end - beg) / 2;

    for (int i = 0; i < 10; i++)

    {
         v[i] = i;
    }

    cin >> num;

    sort(v.begin(), v.end());

    while (*mid != num &&  beg <= end)

    {
         if (num < *mid)

         {
             end = mid;
         }

         else

         {
             beg = mid + 1;
         }

         mid = beg + (end - beg) / 2;

    }

    if (*mid == num)

    {
         cout << "Find" << endl;
    }

    else

    {
         cout << "Not Find" << endl;
    }

    return 0;

}

 二、deque

1.特点:

(1) deque(double-ended queue 双端队列);

(2) 具有分段数组、索引数组, 分段数组是存储数据的,索引数组是存储每段数组的首地址;

(3) 向两端插入元素效率较高!

    (若向两端插入元素,如果两端的分段数组未满,既可插入;如果两端的分段数组已满,

    则创建新的分段函数,并把分段数组的首地址存储到deque容器中即可)。

    中间插入元素效率较低!

 

2. 创建deque对象

(1) deque<int> d1;

(2) deque<int> d2(10);

3. 基本操作:

(1) 元素访问:

d[i];

d.at[i];

d.front();

d.back();

d.begin();

d.end();

(2) 添加元素:

d.push_back();

d.push_front();

d.insert(pos,elem); //pos是vector的插入元素的位置

d.insert(pos, n, elem) //在位置pos上插入n个元素elem

d.insert(pos, begin, end);

(3) 删除元素:

d.pop_back();

d.pop_front();

d.erase(pos);   //移除pos位置上的元素,返回下一个数据的位置

d.erase(begin, end); //移除[begin, end)区间的数据,返回下一个元素的位置

三、list


1. 特点:

(1) 双向链表

2.创建对象:

list<int> L1;

list<int> L2(10);

3.基本操作:

(1) 元素访问:

lt.front();

lt.back();

lt.begin();

lt.end();

(2) 添加元素:

lt.push_back();
lt.push_front();
lt.insert(pos, elem);
lt.insert(pos, n , elem);
lt.insert(pos, begin, end);
lt.pop_back();
lt.pop_front();
lt.erase(begin, end);
lt.erase(elem);

(3)sort()函数、merge()函数、splice()函数:

sort()函数就是对list中的元素进行排序;

merge()函数的功能是:将两个容器合并,合并成功后会按从小到大的顺序排列;

比如:lt1.merge(lt2); lt1容器中的元素全都合并到容器lt2中。

splice()函数的功能是:可以指定合并位置,但是不能自动排序!
 

16.2、关联容器(map、set)

关联容器与序列容器有着根本性的不同,序列容器的元素是按照在容器中的位置来顺序保存和访问的,而关联容器的元素是按关键元素来保存和访问的。关联容器支持高效的关键字查找与访问。两个主要的关联容器类型是map与set。

1.set
1.1 简介:set里面每个元素只存有一个key,它支持高效的关键字查询操作。set对应数学中的“集合”。

1.2 特点:

  • 储存同一类型的数据元素(这点和vector、queue等其他容器相同)
  • 每个元素的值都唯一(没有重复的元素)
  • 根据元素的值自动排列大小(有序性)
  • 无法直接修改元素
  • 高效的插入删除操作
     

1.3 声明:set<T> a

set<int> a;

1.4 常用函数

以下设 set<T> a,其中a是T类型的set容器。


1.6 插入元素:

  • a.insert(x) :其中a为set<T>型容器,x为T型变量  
 set<int> a={0,1,2,9};
 a.insert(6);
 for(auto it = a.begin();it != a.end();it++)    cout << *it;//输出01269
  • a.insert(first,second):其中first为指向区间左侧的迭代器,second为指向右侧的迭代器。作用是将first到second区间内元素插入到a(左闭右开)。
set<int> a = {0,1,2,9};
set<int> b = {3,4,5};
auto first = b.begin();
auto second = b.end();
a.insert(first,second);
for(auto it = a.begin();it != a.end();it++)    cout << *it;


插入元素会自动插入到合适的位置,使整个集合有序

1.7 删除元素:

a.erase(x):删除建值为x的元素
a.erase(first,second):删除first到second区间内的元素(左闭右开)
a.erase(iterator):删除迭代器指向的元素
  • set中的删除操作是不进行任何的错误检查的,比如定位器的是否合法等等,所以用的时候自己一定要注意。

1.8 lower_bound 和 upper_bound 迭代器:

  • lower_bound(x1):返回第一个不小于键参数x1的元素的迭代器
  • upper_bound(x2):返回最后一个大于键参数x2的元素的迭代器
  • 由以上俩个函数,可以得到一个目标区间,即包含集合中从'x1'到'x2'的所有元素
#include<iostream>
#include<set>
#include<algorithm>
using namespace std;
int main()
{
    set<int> a = {0,1,2,5,9};
    auto it2 = a.lower_bound(2);//返回指向第一个大于等于x的元素的迭代器
    auto it = a.upper_bound(2);//返回指向第一个大于x的元素的迭代器
    cout << *it2 << endl;//输出为2
    cout << *it << endl;//输出为5
    return 0;
} 

1.9 set_union() 与 set_intersection()

set_union():对集合取并集

set_union()函数接受5个迭代器参数。前两个迭代器定义了第一个集合的区间,接下来的俩个迭代器定义了第二个集合的区间,最后一个迭代器是输出迭代器,指出将结果集合复制到什么位置。例如:要将A与B的集合复制到C中,可以这样写:

#include<iostream>
#include<set>
#include<algorithm>
using namespace std;
int main()
{
    set<int> A = {1,2,3}, B= {2,4,5},C;
    set_union(A.begin(),A.end(),B.begin(),B.end(),
            insert_iterator<set<int> >(C,C.begin()));
    for(auto it = C.begin();it != C.end();it++)
        cout << *it <<" ";
    return 0;
} 

注意:

其中第五个参数不能写C.begin(),原因有两个:首先,关联集合将建看作常量,所以C.begin()返回的迭代器是常量迭代器,不能作为输出迭代器(详情请参考迭代器相关概念)。其次,与copy()相同,set_union()将覆盖容器中已有的数据,并且要求容器用足够的空间容纳新信息,而C不满足,因为它是空的。

解决方法:可以创建一个匿名的insert_iterator,将信息复制给C。如上述代码所为。另一种方法如下:

set_union(A.begin(),A.end(),B.begin(),B.end(),
        inserter(C,C.begin()));//调用inserter
set_intersection():对集合取交集,它的接口与set_union()相同。

附:使用set_union()和set_intersection()还有另一种技巧。由于需要五个迭代器,看起来会很累赘和麻烦,如果多次使用会增加出错的几率,所以我们可以试试用宏定义的方法来简化代码。如下:

#include<iostream>
#include<set>
#include<algorithm>
using namespace std;
#define ALL(x) x.begin(),x.end()
#define INS(x) inserter(x,x.begin())
int main()
{
    set<int> A = {1,2,3}, B= {2,4,5},C;
    set_union(ALL(A),ALL(B),INS(C));
    for(auto it = C.begin();it != C.end();it++)
        cout << *it <<" ";
    return 0;
} 


其中使用到了宏定义。

1.10 set的几个问题:

(1)为何map和set的插入删除效率比用其他序列容器高?

因为对于关联容器来说,不需要做内存拷贝和内存移动。set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。因此插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点也OK了。这里的一切操作就是指针换来换去,和内存移动没有关系。

(2)为何每次insert之后,以前保存的iterator不会失效?

iterator这里就相当于指向节点的指针,内存没有变,指向内存的指针怎么会失效呢(当然被删除的那个元素本身已经失效了)。相对于vector来说,每一次删除和插入,指针都有可能失效,调用push_back在尾部插入也是如此。因为为了保证内部数据的连续存放,iterator指向的那块内存在删除和插入过程中可能已经被其他内存覆盖或者内存已经被释放了。即使时push_back的时候,容器内部空间可能不够,需要一块新的更大的内存,只有把以前的内存释放,申请新的更大的内存,复制已有的数据元素到新的内存,最后把需要插入的元素放到最后,那么以前的内存指针自然就不可用了。特别时在和find等算法在一起使用的时候,牢记这个原则:不要使用过期的iterator。

(3)当数据元素增多时,set的插入和搜索速度变化如何?

如果你知道log2的关系你应该就彻底了解这个答案。在set中查找是使用二分查找,也就是说,如果有16个元素,最多需要比较4次就能找到结果,有32个元素,最多比较5次。那么有10000个呢?最多比较的次数为log10000,最多为14次,如果是20000个元素呢?最多不过15次。看见了吧,当数据量增大一倍的时候,搜索次数只不过多了1次,多了1/14的搜索时间而已。你明白这个道理后,就可以安心往里面放入元素了。
 

 

参考:https://blog.csdn.net/weixin_41162823/article/details/80185444?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522161853469616780274116838%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=161853469616780274116838&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-80185444.first_rank_v2_pc_rank_v29&utm_term=%E5%85%B3%E8%81%94%E5%AE%B9%E5%99%A8

16.3、容器适配器(queue、stack)

STL 提供了 3 种容器适配器,分别为 stack 栈适配器、queue 队列适配器以及 priority_queue 优先权队列适配器。其中,各适配器所使用的默认基础容器以及可供用户选择的基础容器,如表 1 所示。

表 1 STL 容器适配器及其基础容器
容器适配器基础容器筛选条件默认使用的基础容器
stack 基础容器需包含以下成员函数:
  • empty()
  • size()
  • back()
  • push_back()
  • pop_back()
满足条件的基础容器有 vector、deque、list。
deque
queue基础容器需包含以下成员函数:
  • empty()
  • size()
  • front()
  • back()
  • push_back()
  • pop_front()
满足条件的基础容器有 deque、list。
deque
priority_queue基础容器需包含以下成员函数:
  • empty()
  • size()
  • front()
  • push_back()
  • pop_back()
满足条件的基础容器有vector、deque。
vector


不同场景下,由于不同的序列式容器其底层采用的数据结构不同,因此容器适配器的执行效率也不尽相同。但通常情况下,使用默认的基础容器即可。当然,我们也可以手动修改,具体的修改容器适配器基础容器的方法,后续讲解具体的容器适配器会详细介绍。

 

参考:C++STL容器总结

17、相机及其参数

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

落花逐流水

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值