C++面试基础知识整理(8)

标准模板库

STL基本组成

  • 容器、迭代器、仿函数、算法、分配器、配接器
  • 他们之间的关系:分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配接器用来套接适配仿函数
组件 描述
容器(Containers) 容器是用来管理某一类对象的集合。C++ 提供了各种不同类型的容器,比如 deque、list、vector、map 等。
算法(Algorithms) 算法作用于容器。它们提供了执行各种操作的方式,包括对容器内容执行初始化、排序、搜索和转换等操作。
迭代器(iterators) 迭代器用于遍历对象集合的元素。这些集合可能是容器,也可能是容器的子集。

动态数组实现原理

// 改变数组容量的大小
    void resize(int newCapacity)
    {
    
    
        assert(newCapacity>=size);
        
        T *newData=new T[newCapacity];
        
        for(int i=0;i<size;i++)
            newData[i]=data[i];
        deleta[] data;
        
        data=newData;
        capacity=newCapacity;
    }
    // 向数组中添加一个元素
    void push_bakc(T e)
    {
    
    
        if(size == capacity)
            resize(2*capacity);// 改变数组的容量为原来的两倍
        data[size++]=e;
    }
    // 从数组中删除一个元素
    T pop_back()
    {
    
    
    	assert(size>0);
        T ret=data[size-1];
        size--;
        if(size==capacity/4)
            resize(capacity/2);// 改变数组的容量为原来的四分之一
        return ret;
    }
  • 当数组中元素的个数等于数组的容量时,此时若再添加一个元素,则会重新分配内存,将数组的容量改变为原来的两倍,并将数组中的元素赋值到新数组中,以实现动态数组。

  • 前n次赋值的时间复杂度为n,最后一次赋值的时间复杂度也为n,所以均摊时间复杂度为2n/(n+1)=2,为O(1).

  • 当从数组中删除元素时,若数组中元素的个数仅等于原来的1/2,就改变数组的容量,虽然删除元素的均摊时间复杂度仍为O(1),但在此时若重复进行插入与删除操作,会不断地为数组分配内存,使数组容量变为原来的两倍或1/2,会使得均摊复杂度变为O(n),导致复杂度的震荡

  • 为避免复杂度震荡,应当在数组中元素的个数等于原来的1/4时,再改变数组的容量为原来的1/2,如上面代码所示。

vector和list

  • 底层结构

    • vector的底层结构是动态顺序表,在内存中是一段连续的空间。
    • list的底层结构是带头节点的双向循环链表,在内存中不是一段连续的空间。
  • 随机访问[]

    • vector支持随机访问,可以利用下标精准定位到一个元素上,访问某个元素的时间复杂度是O(1)。
    • list不支持随机访问,要想访问list中的某个元素只能是从前向后或从后向前依次遍历,时间复杂度是O(N)。
  • 插入和删除

    • vector任意位置插入和删除的效率低,因为它每插入一个元素(尾插除外),都需要搬移数据,时间复杂度是O(N),而且插入还有可能要增容,这样一来还要开辟新空间,导致效率低下。
    • list任意位置插入和删除的效率高,他不需要搬移元素,只需要改变插入或删除位置的前后两个节点的指向即可,时间复杂度为O(1)。
  • 适用场景

    • vector适合需要高效率存储,需要随机访问,并且不关心插入和删除效率的场景。
    • list适合有大量的插入和删除操作,并且不关心随机访问的场景。

vector迭代器失效

  • 失效的两种情况:
    • 当插入元素后,如果储存空间重新分配,则原迭代器指向的内存不再是vector,导致失效。
    • 当删除元素时,后面所有的元素会向前移动一个位置,导致迭代器指向的下一个位置是未知内存。
int main()
{
    
    
	vector<int> vec(5, 0);
	cout << &vec[0] << endl; // 打印数组元素首地址

	for (int i = 0; i < vec.size(); i++)
	{
    
    
		vec[i] = i;
	}

	vector<int>::iterator iter = vec.begin();
	
    // 插入元素时,迭代器失效
	vec.push_back(0);
	cout << &vec[0] << endl;// 数组扩容后,首地址已经改变
	//cout << *iter << endl;

	// 删除元素时迭代器失效
	for (iter = vec.begin(); iter != vec.end();)
	{
    
    
		if (*iter == 3) {
    
    
			//vec.erase(iter);// 如果不给iter重新复制,则迭代器会失效
			iter = vec.erase(iter);
		}
		cout << *iter << endl;
		iter++;
	}

	return 0;
}

deque

  • deque双端队列,由一段一段的定量连续空间构成。一旦要在 deque 的前端和尾端增加新空间,便配置一段定量连续空间,串在整个 deque 的头端或尾端。
  • 因此不论在尾部或头部安插元素都十分迅速。 在中间部分安插元素则比较费时,因为必须移动其它元素。
  • 优点:支持随机访问,即 [] 操作和 .at(),所以查询效率高;可在双端进行 pop,push。
  • 缺点:不适合中间插入删除操作;占用内存多。
  • 适用场景:适用于既要频繁随机存取,又要关心两端数据的插入与删除的场景。

set

  • set集合由红黑树实现,内部元素依据其值自动排序,每个元素值只能出现一次,不允许重复。
  • map 和 set 的插入删除效率比用其他序列容器高,因为对于关联容器来说,不需要做内存拷贝和内存移动。
  • 优点:使用平衡二叉树实现,便于元素查找(时间复杂度为O(logN)),且保持了元素的唯一性,以及能自动排序。
  • 缺点:每次插入值的时候,都需要调整红黑树,效率有一定影响。
  • 适用场景:适用于经常查找一个元素是否在某群集中且需要排序的场景。

map

  • map 由红黑树实现,其元素都是 “键值/实值” 所形成的一个对组。内部元素根据键值自动排序。每个键值只能出现一次,不允许重复。
  • 优点:使用平衡二叉树实现,便于元素查找,且能把一个值映射成另一个值,可以创建字典。
  • 缺点:每次插入值的时候,都需要调整红黑树,效率有一定影响。
  • 适用场景:适用于需要存储一个数据字典,并要求方便地根据key找value的场景。

map和set的区别

  • set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。原因是map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。
  • map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map应用中需要慎用。如果find能解决需要,尽可能用find。

map和unordered_map的区别

  • map,其底层是基于红黑树实现的

    • 优点:
      • 有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作
      • map的查找、删除、增加等一系列操作时间复杂度稳定,都为logn
    • 缺点:
      • 查找、删除、增加等操作平均时间复杂度较慢,与n相关
  • unordered_map,其底层是一个哈希表

    • 优点如下:
      • 查找、删除、添加的速度快,时间复杂度为常数级O©
    • 缺点如下:
      • 因为unordered_map内部基于哈希表,以(key,value)对的形式存储,因此空间占用率高
      • Unordered_map的查找、删除、添加的时间复杂度不稳定,平均为O©,取决于哈希函数。极端情况下可能为O(n)

allocator分配器

作用

  • 一般情况下,内存分配主要使用new和delete,但是new将内存分配和对象构造组合在了一起,delete将对象析构和内存释放组合在了一起。一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费。
  • 当分配一大块内存时,我们通常计划在这块内存上按需构造对象。在此情况下,我们希望将内存分配和对象构造分离。
  • **allocator允许内存分配和对象初始化的分离。**它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。

使用

int test_allocator_1()
{
    
    
	std::allocator<std::string> alloc; // 可以分配string的allocator对象
	
    // 内存分配
    int n = 5;
	auto const p = alloc.allocate(n); // 分配n个未初始化的string
 	
    // 对象初始化
	auto q = p; // q指向最后构造的元素之后的位置
	alloc.construct(q++); // *q为空字符串
	alloc.construct(q++, 10, 'c'); // *q为cccccccccc
	alloc.construct(q++, "hi"); // *q为hi
 
	std::cout << *p << std::endl; // 正确:使用string的输出运算符
	//std::cout << *q << std::endl; // 灾难:q指向未构造的内存
	std::cout << p[0] << std::endl;
	std::cout << p[1] << std::endl;
	std::cout << p[2] << std::endl;
 	
    // 对象析构
	while (q != p) {
    
    
		alloc.destroy(--q); // 释放我们真正构造的string
	}
 	
    // 内存释放
	alloc.deallocate(p, n);
 
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_34731182/article/details/113432859