C++:04.容器+迭代器+空间配置器

容器:顺序容器/关联容器

顺序容器:

向量容器vector、双端队列 deque、双向链表 list。

元素在容器中的位置同元素的值无关,即容器不是排序的。

vector 是可变长的动态数组。#include <vector>。 随机访问、内存是连续的、方便排序、二分搜索。可以嵌套形成二维动态数组vector<vector<int> > v(3); //v有3个元素,每个元素都是vector<int> 容器。当然也可也vector<list<int>> vec3;

list是双向链表容器,不循环。#include <list>。查找速度慢,但插入删除快。不支持随机访问,所以STL中算法sort无法对list排序。但list容器引入了sort成员函数来完成排序。

deque双端队列容器。#include <deque>。vector头插较慢,尾插较快。deque头尾都挺快。首先动态开辟二维数组,并且还有一个小块的一维数组(最开始开辟能存放两个指针的空间)用指针来管理这个二维数组。当扩容的时候首先扩容这个一维的,再2倍扩容存放数据的空间,并且是再原有的基础上前面放一块,后面放一块。从而保证始终都可以头查和尾插。但因为是动态开辟的空间,所以并不连续。

关联容器:

关联容器分为两种:一种基于红黑树的有序容器,另一种基于哈希表的无序容器

基于红黑树的有序容器 查询  O(log2n)

默认情况下,从小到大排序

基于哈希表的无序容器  查询 O(1)
set:                集合                 不允许key重复 unordered_set:                 集合                 不允许key重复
multiset:         多重集合          允许key重复 unordered_multiset:         多重集合           允许key重复
map:               映射表             不允许key重复 unordered_map:               映射表              不允许key重复
multimap:        多重映射表       允许key重复 unordered_multimap:        多重映射表       允许key重复

map和set的插入删除效率比用其他序列容器高:因为对于关联容器来说,不需要做内存拷贝和内存移动。set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。因此插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点也OK了。

 map:

在map中存放的是键值对。

在定义的时候可以这样定义:
    map<int, string> map1; 即将int与string绑定。
插入:    
    map1.insert(make_pair(1060, "张三")); 使用make_pair函数,map里放的类型是pair类型
    map1[1022] = "张琪"; 这样也可以插入

注意:operator[] 副作用
    string name1 = map1[1022]; 如果这样使用查询操作,如果没有的话会自己插入一个,并且第二个元素为空。

map可以解决找出海量数据中出现次数最多的元素的问题。

海量数据处理方法:分治法(大文件分成内存能够加载的多个小文件 哈希映射) + 哈希统计  +   小跟堆结构

容器适配器:

栈 stack、队列 queue、优先级队列 priority_queue。容器适配器没有迭代器,没有底层数据结构。容器适配器,就是对容器的一个封装。

eg:栈stack和队列queue底下使用的就是deque实现。

为啥用deque不用vector :初期开辟的空间多,效率高(vector最开始只开辟1字节,后进行2倍扩容)。 并且再扩容的时候,deque只需要复制地址,而vector需要拷贝数据。

优先队列priority_queue选的是vector,vector可以计算下标,deque不连续,无法计算。

容器都是类模板。它们实例化后就成为容器类。用容器类定义的对象称为容器对象。

stack(deque) : 栈  操作有:push  pop  empty  size  top  
stack<int> s1;
stack<int, vector<int>> s2;???????

queue(deque) : 队列  push  pop  empty  size  front  back/rear
priority_queue(vector) : 优先级队列 push  pop  empty  size  top

补充:

对于vector来说,最开始只开辟了1字节,后进行2倍扩容。效率不高。所以提供了两个函数reserve() 和resize()。

vector<int> vec1, vec2;

vec1.resize(100);  // 不仅会开辟内存,还给内存上添加了元素
vec2.reserve(100); // 只开辟空间

迭代器:正向/反向迭代器

要访问容器中的元素,需要通过“迭代器(iterator)”进行。迭代器相当于容器和操纵容器的算法之间的中介。迭代器可以指向容器中某个元素,通过*可以访问容器元素,所以与指针类似。

迭代器的设计:每个数据类型都有自己的迭代器,并不是共用一个迭代器。

迭代器的目的:是用户脱离容器。不需要关心底层实现。

迭代器的定义:迭代器类型设计成了容器类型的嵌套类型。

正向迭代器:容器类名::iterator  迭代器名;

反向迭代器:容器类名::reverse_iterator  迭代器名;

正反迭代器的区别:

  • 正向迭代器进行++操作时,迭代器会指向容器中的后一个元素;调用begin函数返回第一个元素的迭代器,从前向后遍历。
  • 反向迭代器进行++操作时,迭代器会指向容器中的前一个元素;调用rbegin函数返回最后一个元素的迭代器,从后向前遍历。

按迭代器的功能分类:正向迭代器,反向迭代器(可以使用--运算符),随机访问迭代器(可以使用+=,<等。还支持随机访问p[i])。

vector 随机访问
deque 随机访问
list 双向
set / multiset 双向
map / multimap 双向

迭代器失效问题:

1、使用erase函数删除迭代器中的一个元素,导致迭代器失效。

正常思路:(删除容器中的偶数)
for (it1 = vec.begin(); it1 != vec.end(); ++it1)
{
    if (*it1 % 2 == 0)
    {
        vec.erase(it1);
    }
}

 正确的使用方式:erase函数是有返回值的。返回被删除元素的下一个元素的迭代器。

for (it1 = vec.begin(); it1 != vec.end(); )
{
    if (*it1 % 2 == 0)
    {
        it1 = vec.erase(it1); 利用返回值,保证迭代器不会失效
    }
    else
    {
        ++it1;
    }
}

 2、使用insert函数插入一个元素,导致迭代器失效。

错误代码就不演示了!正确使用如下:

在所有奇数前插入0
for (it1 = vec.begin(); it1 != vec.end(); ++it1) 3、再加1,指向第二个奇数
{
    if (*it1 % 2 != 0)
    {
        it1 = vec.insert(it1, 0); 1、跟erase一样有返回值,返回的是插入的元素的迭代器。也就是指向了0.
        ++it1; 2、加1指向了插入的0元素之前的那个元素
    }
}

 嗯,这里看了一篇博客,引用一下:https://blog.csdn.net/codercong/article/details/52065130

 C++ Primier的总结

关于容器的迭代器失效的问题,C++ Primier用了一小节作了总结:

(1)增加元素到容器后

对于vector和string,如果容器内存被重新分配,iterators,pointers,references失效;如果没有重新分配,那么插入点之前的iterator有效,插入点之后的iterator失效;

对于deque,如果插入点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,deque的迭代器失效,但reference和pointers有效;

对于list和forward_list,所有的iterator,pointer和refercnce有效。

(2)从容器中移除元素后

对于vector和string,插入点之前的iterators,pointers,references有效;off-the-end迭代器总是失效的;

对于deque,如果插入点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,off-the-end失效,其他的iterators,pointers,references有效;

对于list和forward_list,所有的iterator,pointer和refercnce有效。

(3)在循环中refresh迭代器

当处理vector,string,deque时,当在一个循环中可能增加或移除元素时,要考虑到迭代器可能会失效的问题。我们一定要refresh迭代器。


迭代器的辅助函数

#include <algorithm>  //使用STL算法需要包含此头文件

STL 中有用于操作迭代器的三个函数模板,它们是:

  • advance(p, n):使迭代器 p 向前或向后移动 n 个元素。
  • distance(p, q):计算两个迭代器之间的距离,即迭代器 p 经过多少次 + + 操作后和迭代器 q 相等。如果调用时 p 已经指向 q 的后面,则这个函数会陷入死循环。
  • iter_swap(p, q):用于交换两个迭代器 p、q 指向的值。

空间配置器 :Allocator

由于new关键字在申请对象内存的时候不仅会申请内存还会构造很多对象,但很多时候,这些对象我们其实并不需要使用。我们只需要在使用这个空间的时候在构造对象。
所以我们需要将开辟内存(allocate)和构造对象(construst)分开使用。就有了Allocator空间配置器。(delete同理)

实现简单的向量容器+迭代器+空间配置器

template<typename T>
struct Allocator
{
	// allocate开辟内存
	T* allocate(size_t size)
	{
		return (T*)malloc(size);  malloc只申请空间,并不构造对象
	}

	// deallocate释放内存
	void deallocate(void *ptr)
	{
		free(ptr);  只释放空间
	}

	// construct构造对象
	void construct(void *ptr, const T &val)
	{
		// new   定位new 
		new (ptr) T(val);  表示在ptr指向的内存,构造一个值为val的对象
	}

	//destroy析构对象   
	void destroy(T *ptr)
	{
		ptr->~T();  只析构对象,并不释放内存。构造函数不能自己单独调用,析构函数可以
	}
};

template<typename T>
class Vector
{
public:
	Vector(int size=10):mSize(size),mCur(0)
	{
		//mpVec = new T [size];
		mpVec = allocator.allocate(size * sizeof(T));

	}
	~Vector()
	{
		//delete[] mpVec;
		for(int i = 0 ; i < mCur ; ++i)
		{
			allocator.destroy(mpVec + i);
		}
		allocator.deallocate(mpVec);
		mpVec = NULL;
	}
	Vector(const Vector &src)
	{
		mSize = src.mSize;
		mCur = src.mCur;
		//mpVec = new T [src.mSize];
		mpVec = allocator.allocate(mCur * sizeof(T));
		for(int i = 0;i < src.mCur;++i)  由于使用类模板,所以不能使用memcpy,防止浅拷贝的发生。
		{
			//mpVec[i] = src.mpVec[i];
			allocator.constructa(mpVec + i, src.mpVec[i]);
		}
	}
	Vector<T>& operator=(const Vector<T> &src)  只要返回的东西还活着就返回引用。
                                 返回引用就可以当作左值(不是立即数,立即数是通过寄存器返回的)。
	{
		if(mpVec == src.mpVec)
		{
			return *this;
		}
		//delete[] mpVec;
		for(int i = 0 ; i < mCur ; ++i)
		{
			allocator.destroy(mpVec + i);
		}
		//mpVec = new T [src.mSize];
		mpVec = allocator.allocate(src.mSize * sizeof(T));
		for(int i = 0;i < src.mCur;++i)
		{
			//mpVec[i] = src.mpVec[i];
			allocator.constructa(mpVec + i, src.mpVec[i]);
		}
		mSize = src.mSize;
		mCur = src.mCur;
		return *this;
	}

	int operator [] (int i)
	{
		return mpVec[i];
	}

	void push_back(const T &val)  // 从末尾给向量容器添加元素
	{
		if(mCur == mSize)
		{
			reSize();
		}
		//mpVec[mCur++] = val;
		allocator.construct(mpVec + mCur,val);
		mCur++;
	}

	void pop_back() // 从末尾删除向量容器的元素
	{
		if(mCur == 0)
		{
			return ;
		}
		mCur--;
		allocator.destroy(mpVec + mCur);
	}

	给向量容器Vector实现迭代器
	class iterator
	{
	public:
		iterator(T *pos):mp(pos)
		{}
		bool operator != (const iterator &src)
		{
			return mp != src.mp;
		}
		void operator ++ ()
		{
			mp++;
		}
		T operator * ()
		{
			return *mp;
		}
	private:
		T *mp;
	};
	iterator begin()// 返回首元素迭代器
	{
		return iterator(mpVec);
	}
	iterator end() // 返回末尾后继位置的迭代器
	{
		return iterator(mpVec + mCur);
	}
private:
	T *mpVec;
	int mSize;  // 扩容的总大小
	int mCur;   // 当前元素的个数
	Allocator<T> allocator;//空间配置器

	friend ostream& operator<<(ostream &out, const Vector<T> &src);
	void reSize() // 向量容器扩容函数,默认2倍扩容
	{
		T *tmp = new T [mSize * 2];
		mSize *= 2;
		for(int i = 0;i < mCur;++i)
		{
			tmp[i] = mpVec[i];
		}
		delete[] mpVec;
		mpVec = tmp;
	}
};

template<ypename T>
ostream& operator<<(ostream &out, const Vector<T> &src)
{
	out << src.mpVec;
	return out;
}

猜你喜欢

转载自blog.csdn.net/qq_41214278/article/details/83690655