C++之STL標準模板庫——從入門到精通

STL的本質

通俗說:STL是Standard Template Library(標準模板庫),是高效的C++程序庫,其採用泛型編程思想對常見數據結構(順序表,鏈表,棧和隊列,堆,二叉樹,哈希)和算法(查找、排序、集合、數值運算…)等進行封裝,裏面處處體現着泛型編程程序設計思想以及設計模式,已被集成到C++標準程序庫中
具體說:STL中包含了容器適配器算法迭代器仿函數以及空間配置器
STL設計理念:追求代碼高複用性以及運行速度的高效率,在實現時使用了許多技術。

STL的六大組件

容器

STL中的容器,可以劃分爲兩大類:序列式容器和關聯式容器。
在這裏插入圖片描述

list類介紹
string類介紹
vector類介紹
deque類介紹
stack類和queue類
map和set類的概念及使用
AVL樹和紅黑樹(map和set的底層實現)

算法

算法:問題的求解步驟,以有限的步驟,解決數學或邏輯中的問題。

STL常用算法:
1. accumulate

該算法作用是對區間中的元素進行累加。

#include <numeric>
// 對[first, last)區間中元素在init的基礎上進行累加
template <class InputIterator, class T>
T accumulate ( InputIterator first, InputIterator last,
				 T init );
 
// 對[fist, last)區間中元素在init的基礎上按照binary_op指定的操作進行累加
template <class InputIterator, class T, 
				class BinaryOperation>
T accumulate ( InputIterator first, InputIterator last,
		 T init,BinaryOperation binary_op );
#include <numeric>
#include <vector>
 
struct Mul2 
{
    int operator()(int x, int y) { return x + 2 * y; }
};
 
int main()
{
    // 對區間中的元素進行累加
    vector<int> v{ 10, 20, 30 };
    cout << accumulate(v.begin(), v.end(), 0)<<endl;
 
    // 對區間中的每個元素乘2,然後累加
    cout << accumulate(v.begin(), v.end(), 0, Mul2()) << endl;
    return 0;
}

在這裏插入圖片描述

2. count與count_if

該算法的作用是統計區間中某個元素出現的次數。

// 統計value在區間[first,last)中出現的次數
template <class InputIterator, class T>
ptrdiff_t count ( InputIterator first, InputIterator last,
					 const T& value )
{
    ptrdiff_t ret=0;
    while (first != last) 
    	if (*first++ == value) 
    	++ret;
    return ret;
}
 
// 統計滿足pred條件的元素在[first, last)中的個數
template <class InputIterator, class Predicate>
ptrdiff_t count_if ( InputIterator first, InputIterator last, 
					Predicate pred )
{
    ptrdiff_t ret=0;
    while (first != last)
    	if (pred(*first++))
    	++ret;
    return ret;
}
#include <algorithm>
#include <vector>
 
bool IsOdd(int i) 
{ return ((i % 2) == 1); }
 
int main() 
{
    // 統計10在v1中出現的次數
    vector<int> v1{ 10, 20, 30, 30, 20, 10, 10, 20 };
    cout << count(v1.begin(), v1.end(), 10) << endl;
 
    // 統計v2中有多少個偶數
    vector<int> v2{0,1,2,3,4,5,6,7,8,9};
    cout << count_if(v2.begin(), v2.end(), IsOdd) << endl;
    return 0;
}

在這裏插入圖片描述

3. find、find_if

該算法的作用是找元素在區間中第一次出現的位置

// 在[first, last)中查找value第一次出現的位置,找到返回該元素的位置,否則返回last
// 時間複雜度O(N)
template<class InputIterator, class T>
InputIterator find(InputIterator first, InputIterator last, 
					const T& value)
{
	for (; first != last; first++) 
		if (*first == value) 
		break;
	return first;
}

// 在[first, last)中查找滿足pred條件的元素第一次出現的位置,找到返回該位置,否則返回last
// 時間複雜度O(N)
template<class InputIterator, class Predicate>
InputIterator find_if(InputIterator first, InputIterator last,
					 Predicate pred)
{
	for (; first != last; first++) 
		if (pred(*first)) 
		break;
	return first;
}
4. max和min

max返回兩個元素中較大值,min返回兩個元素中較小值。

template <class T> 
const T& max(const T& a, const T& b)
{
  return (a<b)?b:a;
} 
 
template <class T> 
const T& min(const T& a, const T& b) 
{
  return !(b<a)?a:b;
} 
5. merge

該算法作用將兩個有序序列合併成一個有序序列, 使用時必須包含頭文件。

template <class InputIterator1, class InputIterator2, 
			class OutputIterator>
OutputIterator merge ( InputIterator1 first1,
	InputIterator1 last1,
	InputIterator2 first2,
	InputIterator2 last2,
    OutputIterator result )
{
    while (true) 
    {
      *result++ = (*first2<*first1)? *first2++ : *first1++;
       if (first1==last1) return copy(first2,last2,result);
       if (first2==last2) return copy(first1,last1,result);
  }
} 
int main()
{
	vector<int> v{ 2, 6, 5, 8 };
	list<int> L{ 9, 3, 0, 5, 7 };

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

	vector<int> vRet(v.size() + L.size());
	merge(v.begin(), v.end(), L.begin(), L.end(), vRet.begin());

	for (auto e : vRet)
		cout << e << " ";
	cout << endl;
	system("pause");
	return 0;
}

在這裏插入圖片描述
注意:

  1. 使用時必須保證區間有序
  2. 時間複雜度爲O(M+N)
6. partial_sort

該算法的作用是:找TOPK

// 在區間[first, last)中找前middle-first個最小的元素,並存儲在[first, middle)中
template <class RandomAccessIterator>
void partial_sort(RandomAccessIterator first, 
				RandomAccessIterator middle,
  				RandomAccessIterator last);
 
// 在[first, last)中找前middle-first個最大或者最小的元素,並存儲在[first, middle)中
template <class RandomAccessIterator, class Compare>
void partial_sort(RandomAccessIterator first, 
					RandomAccessIterator middle,
                    RandomAccessIterator last, 
                    Compare comp);

partial_sort的實現原理是:對原始容器內區間爲[first, middle)的元素執行make_heap()操作構造一個最大堆,然後拿[middle, last)中的每個元素和first進行比較,first內的元素爲堆內的最大值。如果小於該最大值,則互換元素位置,並對[first, middle)內的元素進行調整,使其保持最大堆序。比較完之後在對[first, middle)內的元素做一次對排序sort_heap()操作,使其按增序排列
注意,堆序和增序是不同的。因此該算法的功能實際就是:TOP-K

int main()
{
	// 找該區間中前4個最小的元素, 元素最終存儲在v1的前4個位置
	vector<int> v1{ 4, 1, 8, 0, 5};
	partial_sort(v1.begin(), v1.begin() + 4, v1.end());
	// 找該區間中前4個最大的元素, 元素最終存儲在v1的前4個位置
	vector<int> v2{ 4, 1, 8, 0, 5};
	partial_sort(v2.begin(), v2.begin() + 4, v2.end(), greater<int>());
	system("pause");
	return 0;
}

在這裏插入圖片描述

7. partition

該算法的作用是按照條件對區間中的元素進行劃分,使用時必須包含頭文件。

template <class BidirectionalIterator, class Predicate>
BidirectionalIterator partition(BidirectionalIterator first,
					BidirectionalIterator last, 
					Predicate pred)
{
     while (true)
     {
        while (first!=last && pred(*first)) ++first;
        if (first==last--) break;
        while (first!=last && !pred(*last)) --last;
        if (first==last) break;
        swap (*first++,*last);
      }
      return first;
}
bool IsOdd(int i) 
{ return (i % 2) == 1; }
 
int main() 
{
    vector<int> v{0,1,2,3,4,5,6,7,8,9};
    // 將區間中元素分割成奇數和偶數兩部分
    auto div = partition(v.begin(), v.end(), IsOdd);
 
    // 打印[begin, div)的元素
    for (auto it = v.begin(); it != div; ++it)
        cout << *it <<" ";
    cout << endl;
 
    // 打印[div, end)的元素
    for (auto it = div; it != v.end(); ++it)
        cout << " " << *it;
    cout << endl;
 
    return 0;
}

在這裏插入圖片描述

8. reverse

該算法的作用是對區間中的元素進行逆置,使用時必須包含頭文件。

template <class BidirectionalIterator>
void reverse ( BidirectionalIterator first, BidirectionalIterator last)
{
  while ((first!=last)&&(first!=--last))
    swap (*first++,*last);
}
9. sort

排序在實際應用中需要經常用到,而在目前的排序中,快排平均情況下是性能最好的一種排序,但是快排也有其自身的短板,比如說:元素接近有序、元素量比較大的情況下,直接使用快排時,堪稱一場災難。因此STL中sort算法並沒有直接使用快排,而是針對各種情況進行了綜合考慮。下面關於sort函數分點進行說明:

  1. sort函數提供了兩個版本
    • sort(first, last):默認按照小於方式排序,排序結果爲升序,一般用排內置類型數據
    • sort(first, last, comp):可以通過comp更改元素比較方式,即可以指定排序的結果爲升序或者降序,一般以仿函數對象和函數指針的方式提供
  2. sort並不是一種排序算法,而是將多個排序算法混合而成
  3. 當元素個數少於__stl_threshold閾值時(16),使用直接插入排序處理
  4. 當元素個數超過__stl_threshold時,考慮是否能用快排的方式排序,因爲當元素數量達到一定程度,遞歸式的快排可能會導致棧溢出而崩,因此:
  • 通過__lg函數判斷遞歸的深度
template <class Size>
inline Size __lg(Size n) 
{ 
    Size k;
    for (k = 0; n > 1; n >>= 1) ++k;
    return k;
}
  • 如果遞歸的深度沒有超過2* 時,則使用快排方式排序,注意:快排時使用到了三數取中法預防分割後一邊沒有數據的極端情況
  • 如果遞歸深度超過2* 時,說明數據量大,遞歸層次太深,可能會導致棧溢出,此時使用堆排序處理。
10. unique

該函數的作用是刪除區間中相鄰的重複性元素,確保元素唯一性,注意在使用前要保證區間中的元素是有序的,才能達到真正的去重。

// 元素不相等時,用後一個將前一個元素覆蓋掉
template <class ForwardIterator>
ForwardIterator unique(ForwardIterator first, 
						ForwardIterator last);

// 如果元素不滿足pred條件時,用後一個將前一個覆蓋掉
template <class ForwardIterator, 
class BinaryPredicate>ForwardIteratorunique(ForwardIterator first, 
	ForwardIterator last,
	BinaryPredicate pred);

template <class ForwardIterator>
ForwardIterator unique(ForwardIterator first, 
						ForwardIterator last)
{
	ForwardIterator result = first;
	while (++first != last)
	{
		if (!(*result == *first))  // or: if (!pred(*result,*first)) for the pred 
			version
			*(++result) = *first;
	}
	return ++result;
}

注意:

  1. 該函數並不是將重複性的元素刪除掉,而是用後面不重複的元素將前面重複的元素覆蓋掉了。
  2. 返回值是一個迭代器,指向的是去重後容器中不重複最後一個元素的下一個位置。
  3. 該函數需要配合erase才能真正的將元素刪除掉
#include <algorithm>
#include <vector>

int main()
{
	vector<int> v{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
	auto it = unique(v.begin(), v.end());

	for (auto e : v)
		cout << e << " ";
	cout << endl;

	/*
	從打印的結果可以看出:
	1. unique並沒有將所有重複的元素刪除掉,而值刪除了一個9,因爲unique刪除的是相鄰的重複
	元素,而上述元素中只有一個9重複相鄰
	2. unique刪除時只是用後面元素將前面重複位置覆蓋掉了,並沒有達到真正刪除,若要真正刪
	除,還需要erase配合
	*/
	v.erase(it, v.end());

	// 如果想將區間中所有重複性的元素刪除掉,可以先對區間中的元素進行排序
	for (auto e : v)
		cout << e << " ";
	cout << endl;

	// 先對區間中的元素進行排序,另重複的元素放在相鄰位置
	sort(v.begin(), v.end());
	for (auto e : v)
		cout << e << " ";
	cout << endl;

	// 使用unique將重複的元素覆蓋掉
	it = unique(v.begin(), v.end());

	// 將後面無效的元素移除
	v.erase(it, v.end());
	for (auto e : v)
		cout << e << " ";
	cout << endl;
	return 0;
}
11. next_permutation和pre_permutation

next_permutation是獲取一個排序的下一個排列,可以遍歷全排列,prev_permutation剛好相反,獲取一個排列的前一個排列, 使用時必須包含頭文件
對序列 {a, b, c},每一個元素都比後面的小,按照字典序列,固定a之後,a比bc都小,c比b大,它的下一個序列即爲{a, c, b},而{a, c, b}的上一個序列即爲{a, b, c},同理可以推出所有的六個序列爲:{a, b,c}、{a, c, b}、{b, a, c}、{b, c, a}、{c, a, b}、{c, b, a},其中{a, b, c}沒有上一個元素,{c, b, a}沒有下一個元素。
注意:使用時,必須保證序列是有序的。

#include <algorithm>
#include <vector>
#include <functional>

int main()
{
	// 因爲next_permutation函數是按照大於字典序獲取下一個排列組合的
	// 因此在排序時必須保證序列是升序的
	vector<int> v = { 4, 1, 2, 3 };
	sort(v.begin(), v.end());
	do
	{
		cout << v[0] << " " << v[1] << " " << v[2] << " " << v[3] << endl;
	} while (next_permutation(v.begin(), v.end()));
	cout << endl;

	// 因爲prev_permutation函數是按照小於字典序獲取下一個排列組合的
	// 因此在排序時必須保證序列是降序的
	//sort(v.begin(), v.end());
	sort(v.begin(), v.end(), greater<int>());
	do
	{
		cout << v[0] << " " << v[1] << " " << v[2] << " " << v[3] << endl;
	} while (prev_permutation(v.begin(), v.end()));
	return 0;
}

迭代器

什麼是迭代器

迭代器是一種設計模式,讓用戶通過特定的接口訪問容器的數據,不需要了解容器內部的底層數據結構
C++中迭代器本質:是一個指針,讓該指針按照具體的結構去操作容器中的數據。

爲什麼需要迭代器

通過前面算法的學習瞭解到:STL中算法分爲容器相關聯與通用算法。所謂通用算法,即與具體的數據結構無關,比如:

template<class InputIterator, class T>
InputIterator find ( InputIterator first, 
					InputIterator last, 
					const T& value )
{
    for ( ;first!=last; first++) 
        if ( *first==value )
            break;
    
    return first;
}

vector的底層結構:
在這裏插入圖片描述
list的底層結構
在這裏插入圖片描述
map的底層結構:
在這裏插入圖片描述
迭代器實現原理
容器底層結構不同,導致其實現原理不同,容器迭代器的設計,必須結合具體容器的底層數據結構。比如:

  1. vector
    因爲vector底層結構爲一段連續空間,迭代器前後移動時比較容易實現,因此vector的迭代器實際是對原生態指針的封裝,即:
typedef T* iterator
  1. list
    list底層結構爲帶頭結點的雙向循環鏈表,迭代器在移動時,只能按照鏈表的結構前後依次移動,因此鏈表的迭代器需要對原生態的指針進行封裝,因爲當對迭代器++時,應該通過節點中的next指針域找到
    下一個節點。

如果迭代器不能直接使用原生態指針操作底層數據時,必須要對指針進行封裝,在封裝時需要提供以下方法:

  1. 迭代器能夠像指針一樣方式進行使用:重載
pointer operator*() / reference operator->()
  1. 能夠讓迭代器移動
    向後移動:
self& operator++() / self operator++(int)

向前移動:

self& operator--() / self operator--(int) 

(注意:有些容器不能向前移動,比如forward_list)

  1. 支持比較-因爲在遍歷時需要知道是否移動到區間的末尾
bool operator!=(const self& it)const / bool operator==(const self& it)const

適配器

適配器:又接着配接器,是一種設計模式,簡單的說:需要的東西就在眼前,但是由於接口不對而無法使用,需要對其接口進行轉化以方便使用。即:將一個類的接口轉換成用戶希望的另一個類的接口,使原本接口不兼容的類可以一起工作
STL中適配器總共有三種類型:

  • 容器適配器-stack和queue
    stack的特性是後進先出,queue的特性爲先進先出,該種特性deque的接口完全滿足,因此stack和queue在底層將deque容器中的接口進行了封裝。
template < class T, class Container = deque<T> >
class stack { ... };
 
template < class T, class Container = deque<T> > 
class queue { ... };
  • 迭代器適配器-反向迭代器
    反向迭代器++和–操作剛好和正向迭代器相反,因此:反向迭代器只需將正向迭代器進行重新封裝即可。
  • 函數適配器

仿函數

仿函數:一種具有函數特徵的對象,調用者可以像函數一樣使用該對象 ,爲了能夠“行爲類似函數”,該對象所在類必須自定義函數調用運算符operator(),重載該運算符後,就可在仿函數對象的後面加上一對小括號,以此調用仿函數所定義的operator()操作,就其行爲而言,“仿函數”一次更切貼。
仿函數一般配合算法,作用就是:提高算法的靈活性。

#include <vector>
#include <algorithm>
class Mul2
{
public:
	void operator()(int& data)
	{
		data <<= 1;
	}
};

class Mod3
{
public:
	bool operator()(int data)
	{
		return 0 == data % 3;
	}
};

int main()
{
	// 給容器中每個元素乘2
	vector<int> v{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	for_each(v.begin(), v.end(), Mul2());
	for (auto e : v)
		cout << e << " ";
	cout << endl;

	// 刪除容器中3的倍數
	auto pos = remove_if(v.begin(), v.end(), Mod3());
	v.erase(pos, v.end());

	// 將容器中的元素打印出來
	// 注意:對於功能簡單的操作,可以使用C++11提供的lambda表達式來代替
	// lambda表達式實現簡單,其在底層與仿函數的原理相同,編譯器會將lambda表達式轉換爲仿函數
	for_each(v.begin(), v.end(), [](int data){cout << data << " "; });
	cout << endl;
	return 0;
}

空間配置器

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章