1. 程式人生 > >【C++】STL之list學習

【C++】STL之list學習

list學習總結

一. list是什麼?

1.list概述

  • list是可以在常數時間範圍內在任意位置進行插入和刪除序列式容器,並且該容器可以前後雙向迭代
  • list的底層是雙向連結串列結構,雙向連結串列中每個元素儲存在互不相關的獨立節點中,在節點中通過指標指向前一個
    元素和後一個元素。
  • listforward_list非常相似,最主要的不同在於forward_list單鏈表,只能單向迭代

2.list相對其他容器的優缺點

  • 與其他的序列式容器相比(array,vector,deque)list通常在任意位置進行插入、移除元素的執行效率更好,一般在常數時間內。
  • list和forward_list最大的缺陷不支援任意位置的隨機訪問。比如:要訪問list第6個元素,必須從已知的位置(比如頭部或者尾部)迭代到該位置,在這段位置上迭代需要線性的時間開銷;list還需要一些額外的空間,以儲存每個節點的相關聯資訊(指向前一個結點或後一個結點的指標域)
  • list相對於vector還有一個好處就是空間的利用率比較高。每刪除或者插入一個元素,就配置或釋放一個空間。

3.list的資料結構

SGI版本的list不僅僅是一個雙向連結串列,而且還是一個環狀雙向連結串列,也就是一條雙向迴圈連結串列
在這裡插入圖片描述

注:本圖擷取自《STL原始碼剖析一書》

二.list的使用(常見使用介面)

1.list的常見建構函式

  • list():構造空的list
  • list (size_type n, const value_type& val = value_type()):構造一個含有nval值的list
  • list (const list& x):拷貝建構函式
  • list (InputIterator first, InputIterator last):迭代器區間構造
void test1()
{
	list<int> l1;//空構造
	list<int> l2(4, 10);//構造4個值為10的結點
	list<int> l3(l2);//拷貝構造
	list<int> l4(l2.begin(), l2.end());//使用l2的迭代器區間[begin,end)構造
	
	//C++11語法糖遍歷
	for (auto& e : l4)
	{
		cout << e << " ";
	}
	cout << endl;

	int arr[] = { 1, 2, 3, 4 };//使用陣列為迭代器區間構造
	list<int> l5(arr, arr + (sizeof(arr) / sizeof(int)));
	//迭代器遍歷
	list<int>::iterator it = l5.begin();
	while (it != l5.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

2.list常見的iterator

2.1 常見介面

  • begin():返回第一個元素的迭代器
  • end():返回最後一個元素下一個位置的迭代器
  • rbegin():返回第一個位置的反向迭代器,既end() - 1
  • rend():返回最後一個元素下一個位置的reverse_iterator,即end()位置
  • cbegin() (C++11):返回第一個元素的const_iterator
  • cend() (C++11):返回最後一個元素下一個位置的const_iterator

2.2 迭代器在list的位置

在這裡插入圖片描述

  • begin與end正向迭代器,對迭代器執行++操作,迭代器向後移動
  • rbegin()與rend()反向迭代器,對迭代器執行++操作,迭代器向前移動
  • cbegin與cendconst正向迭代器,與begin和end不同的是:該迭代器指向節點中的元素值不能修改
  • crbegin與crendconst的反向迭代器,與rbegin和rend不同的是:該迭代器指向節點中的元素值不能修改

2.3 iterator的使用

void test2()
{
	int arr[] = { 1, 2, 3, 4 };
	list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
	//正向迭代器 1 2 3 4
	list<int>::iterator it = l1.begin();
	while (it != l1.end())
	{
		//*it *= 2;可以修改
		cout << *it << " ";
		++it;
	}
	cout << endl;
	//正向const迭代器 1 2 3 4(但是*it不能修改)
	list<int>::const_iterator cit = l1.cbegin();
	while (cit != l1.cend())
	{
		//*cit *= 2; 不能修改
		cout << *cit << " ";
		++cit;
	}
	cout << endl;
	//反向迭代器 4 3 2 1
	list<int>::reverse_iterator rit = l1.rbegin();
	while (rit != l1.rend())
	{
		//*rit *= 2; 可以修改
		cout << *rit << " ";
		++rit;
	}
	cout << endl;
	//反向const迭代器 4 3 2 1
	list<int>::const_reverse_iterator crit = l1.crbegin();
	while (crit != l1.crend())
	{
		//*crit *= 2; 不可以修改
		cout << *crit << " ";
		++crit;
	}
	cout << endl;
}

3.list的size和empty介面

  • bool empty() const:檢測list是否為空,是返回true,否則返回false
  • size_t size() const:返回list中有效節點的個數
void test3()
{
	int arr[] = { 1, 2, 3, 4 };
	list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
	int size = l1.size();
	//輸出l1的大小
	cout << size << endl;
	//判斷l1是否為空,不為空遍歷列印
	if (l1.empty())
	{
		cout << "list為空" << endl;
	}
	else
	{
		for (auto e : l1)
		{
			cout << e << " ";
		}
		cout << " ";
	}
}

4.list獲取資料元素(front&&back)

  • reference front():返回list第一個結點值的引用
  • const_reference front() const:返回list的第一個節點中值的const引用
  • reference back():返回list的最後一個節點中值的引用
  • const_reference back() const:返回list的最後一個節點中值的const引用
void test4()
{
   int arr[] = { 1, 2, 3, 4 };
   list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
   l1.front() = 10;//將l1中的第一個資料改為10
   const int& x = l1.back();//獲取l1中最後一個結點值的const引用
   //x = 10;const引用不可改變
   cout << x << endl;
}

5.list的增刪改查介面

5.1 介面說明

  • void push_front (const value_type& val):在list第一個結點前插入值為 val的結點

  • void pop_front():刪除list中第一個結點

  • void push_back (const value_type& val):在list最後一個結點後插入值為val的結點

  • void pop_back():刪除list中最後一個結點

  • iterator insert (iterator position, const value_type& val):在list position 位置中插 入值為val的結點

  • void insert (iterator position, size_type n, const value_type& val):在position位置插入值為val的n個結點

  • void insert (iterator position, InputIterator first, InputIterator last):在list position位置插入 [first, last)區間中元素

  • iterator erase (iterator position):刪除position位置上的結點

  • iterator erase (iterator first, iterator last): 刪除list中[first, last)區間中的元素

  • void swap (list& x):交換兩條list中的結點

  • void resize (size_type n, value_type val = value_type()):將list中有效元素個數改變 到n個,多出的元素用val 填充

  • void clear():清空list中的有效元素

下邊3個介面為C++11新特性的介面:

  • template <class... Args> void emplace_front (Args&&... args):在list中的第一個結點前根據引數構造新的結點
  • template <class... Args> void emplace_back (Args&&... args):在list最後一個結點後根據引數直接構造新的結點
  • template <class... Args> iterator emplace( const_iterator position, Args&&... args):在list的任意位置根據引數直接構造新的結點
/*測試emplace_back、emplace_front、emplace */
class Date {
public:    
	Date(int year = 1900, int month = 1, int day = 1) 
		: _year(year)
		, _month(month)
		, _day(day)    
	{ 
		cout << "Date(int, int, int):" << this << endl; 
	}

	Date(const Date& d) 
		: _year(d._year)
		, _month(d._month)
		, _day(d._day)    
	{ 
		cout << "Date(const Date&):" << this << endl; 
	}

private:    
	int _year;    
	int _month;    
	int _day;
};
//push_back尾插:先構造好元素,然後將元素拷貝到節點中,插入時先調建構函式,再調拷貝建構函式 
//emplace_back尾插:先構造節點,然後呼叫建構函式在節點中直接構造物件 
//emplace_back比push_back更高效,少了一次拷貝建構函式的呼叫 
void test8()
{
	list<Date> l;    
	Date d(2018, 10, 20);    
	l.push_back(d); //構造--拷貝構造
	l.emplace_back(2018, 10, 21);  //插入時直接構造 
	l.emplace_front(2018, 10, 19); //插入時直接構造 
}

5.2 測試功能

/*測試push_front、push_back、pop_front、pop_back*/
void test5()
{
	int arr[] = { 1, 2, 3, 4 };
	list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
	l1.push_front(0);
	l1.push_back(5);
	PrintList(l1);//0 1 2 3 4 5

	l1.pop_front();
	l1.pop_back();
	PrintList(l1);//1 2 3 4 
}
/*測試insert、erase*/
void test6()
{
	int arr[] = { 1, 2, 3, 4 };
	list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
	
	//獲得值為3的迭代器
	list<int>::iterator pos = find(l1.begin(), l1.end(), 3);
	
	//在pos前插入值為0的結點
	l1.insert(pos, 0);
	PrintList(l1);//1 2 0 3 4 
	
	//在pos前插入5個值為8的結點
	l1.insert(pos, 2, 8);
	PrintList(l1);//1 2 0 8 8 3 4

	//在pos前插入[v.begin(), v.end)區間中的結點
	vector<int> v(2, 7);
	l1.insert(pos, v.begin(), v.end());
	PrintList(l1);//1 2 0 8 8 7 7 3 4

	//刪除pos位置的元素
	l1.erase(pos);
	PrintList(l1);//1 2 0 8 8 7 7 4

	//刪除迭代器區間的值
	l1.erase(l1.begin() , l1.end());
	PrintList(l1);//list為空
}
/* 測試resize、swap、clear */
void test7()
{
	int arr[] = { 1, 2, 3, 4 };
	list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
	PrintList(l1);//1 2 3 4

	//將l1中元素個數增加到10個,多出的元素用預設值填充    
	//如果list中放置的是內建型別,預設值為0
	//如果list中放置自定義型別元素,呼叫預設建構函式
	l1.resize(6);
	PrintList(l1);//1 2 3 4 0 0

	//將l1中的元素增加到8個,多出的元素用8來填充
	l1.resize(8, 8);
	PrintList(l1);//1 2 3 4 0 0 8 8

	//將l1中的元素減少到5個 
	l1.resize(5);
	PrintList(l1);//1 2 3 4 0

	// 用vector中的元素來構造list    
	vector<int> v{ 4, 5, 6 };    
	list<int> l2(v.begin(), v.end());    
	PrintList(l2);//4 5 6

	//交換l1和l2的元素
	l1.swap(l2);
	PrintList(l1);//4 5 6
	PrintList(l2);//1 2 3 4 0

	// 將l2中的元素清空    
	l2.clear();    
	cout << l2.size() << endl;//0
	PrintList(l2);//空
}

6.list的迭代器失效問題

vector的insert和erase迭代器都會失效,vector在insert時會出現擴容的問題,所以使用迭代器時可能會出現野指標問題,vector在erase時由於vs的檢查機制,雖然將pos迭代器位置的資料刪除後,會將後邊的元素移動到前邊,不會存在野指標,但是vs編譯器會檢查出來,這個迭代器已經失效。相對而說,listinsert不會失效,因為list不存在擴容的問題,但是erase也會存在失效,失效的只是指向被刪除節點的迭代器,其他迭代器不會受到影響。例如:(1-2-3,刪除2之後,1-3,迭代器依舊指向2,但是2已經被刪除了,所以會出現類似野指標的問題)

void test9()
{
	int arr[] = { 1, 2, 3, 4 };
	list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));

	list<int>::iterator it = l1.begin();
	while (it != l1.end())
	{
		// erase()函式執行後,it所指向的節點已被刪除
		//因此it無效,在下一次使用it時,必須先給其賦值
		//l1.erase(it);
		//++it;這樣做編譯器報錯迭代器失效

		/*可以這麼修改*/
		//it = l1.erase(it);//erase刪除之後,返回下一個位置的迭代器
		l1.erase(it++);//也可以這麼寫,相當於it = l1.erase(it);
	}
}

三.list的模擬實現

1.list的結點類

list本身list的結點是兩個不同的結構,要實現list,必須先要實現它的結點類。要實現的是一個雙向帶頭迴圈連結串列,所以結點設計必須包括三個域,既prev指標域(指向前一個幾點)、next指標域(指向後一個結點)、data(資料域)

template<class T>
  //為什麼不用struct而用class?
  //如果不使用訪問限定符使用struct,struct預設訪問限定符為public
  //如果使用訪問此限定符使用class,class預設訪問限定符為class
  struct ListNode
  {
    //建構函式(T()相當於是一個匿名物件)
    ListNode(const T& data = T())
     :_data(data)
     ,_next(nullptr)
     ,_prev(nullptr)
    {}
    
	//不需要過載解構函式,直接使用預設生成的析構即可,因為成員變數只是3個指標
	
    T _data; //資料域
    ListNode<T>* _next;//指向前一個結點的指標
    ListNode<T>* _prev;//指向回一個結點的指標
  };

2.list的迭代器

list不能像vector一樣以一個原生指標作為迭代器,因為它的結點不能保證在儲存空間中連續存在list迭代器必須要能夠指向list的結點,並且有能力進行正確的遞增、遞減、取值、成員取用等操作。遞增操作時指向下一個結點,遞減操作時指向下一個操作,取值時取的是結點的資料值,成員取用的是結點的成員。所以我們應該講上述的成員和操作封裝在一個類中。我們可以將原生態指標進行封裝,因迭代器的使用形式與指標完全相同,因此,在自定義的類中必須實現以下方法:

  • 指標可以解引用,迭代器的類中必須過載operator*()
  • 指標可以++向後移動,迭代器類中必須過載operator++()operator++(int)operator--()和operator--(int)
  • 迭代器需要進行是否相等的比較,因此還需要過載operator==()與operator!=()
/**
   * 模擬實現list的迭代器類
   */ 
  template<class T>
  struct ListIterator
  {
    typedef ListNode<T> Node;
    typedef ListIterator<T> iterator;

    //構造
    ListIterator(Node* node)
      :_node(node)
    {}
    //拷貝構造
    ListIterator(const iterator& i)
      :_node(i._node)
    {}

    /**
     * 過載迭代器的解引用、++、!=、==、--等
     */ 
    // *it
    T& operator*()
    {
      return _node -> _data;
    }
    // ++it 
    iterator operator++()
    {
      _node = _node -> _next;
      return *this;
    }
    // it++
    iterator operator++(int)
    {
      iterator tmp(*this);
      _node = _node -> _next;
      return tmp;
    }
    // --it 
    iterator operator--()
    {
      _node = _node -> _prev;
      return *this;
    }
    // it--
    iterator operator--(int)
    {
      iterator tmp(*this);
      _node = _node -> _prev;
      return tmp;
    }
    // it1 != it2
    bool operator!=(const iterator& it)
    {
      return _node != it._node;
    }
    // it1 == it2
    bool operator==(const iterator& it)
    {
      return _node == it._node;
    }
    Node* _node;
  };

3.list建構函式

list的常見建構函式在上邊的已經提及,可以根據其功能模擬實現它:

 //迭代器定義為和庫一樣的名字可以支援語法糖
    typedef ListIterator<T><