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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章