1. 程式人生 > >帶你深入理解STL之List容器

帶你深入理解STL之List容器

上一篇部落格中介紹的vector和陣列類似,它擁有一段連續的記憶體空間,並且起始地址不變,很好的支援了隨機存取,但由於是連續空間,所以在中間進行插入、刪除等操作時都造成了記憶體塊的拷貝和移動,另外在記憶體空間不足時還需要重新申請一塊大記憶體來進行記憶體的拷貝。為了克服這些缺陷,STL定義了另一種容器List,它對於資料插入和刪除的時間複雜度均為O(1),而且再記憶體方面不用頻繁的拷貝轉移。下面,就一起來看看List的原始碼實現吧!

List概述

List和Vector都是STL的序列式容器,唯一不同的地方就在於:Vector是一段連續的記憶體空間,List則是一段不連續的記憶體空間,相比於Vector來說,List在每次插入和刪除的時候,只需要配置或釋放一個元素空間,對於任何位置的插入和刪除操作,List永遠能做到常數時間。但是,List由於不連續的記憶體空間,導致不支援隨機定址,所以尺有所長寸有所短,在程式中選擇使用那種容器還要視元素的構造複雜度和存取行為而定。

List的節點

List的節點結構如下:

template <class T>
struct __list_node
{
  typedef void* void_pointer; 
  void_pointer next;    //型別為void*,也可以設為__list_node<T>*
  void_pointer prev;
  T data;
};

從節點結構可以看出,List就是一個雙向連結串列,其結構如下圖所示:

List

List的迭代器

在Vector中,由於是連續的儲存空間,支援隨機存取,所以其迭代器可以直接用普通指標代替。但是,在List中行不通。List必須有能力指向List的節點,並有能力進行正確的遞增、遞減、取值和成員存取等操作。

List是一個雙向連結串列,迭代器必須具備前移、後退的能力,所以List的迭代器是一個Bidirectional Iterator!在Vector中如果進行插入和刪除操作後迭代器會失效,List有一個重要的性質就是插入和接合操作都不會造成原有的List迭代器失效。而且,再刪除一個節點時,也僅有指向被刪除元素的那個迭代器失效,其他迭代器不受任何影響。下面來看看List迭代器的原始碼。

template<class T, class Ref, class Ptr>
struct __list_iterator
{
  typedef __list_iterator<T, T&, T*>             iterator;   // 支援Iterator_traits
typedef __list_iterator<T, const T&, const T*> const_iterator; typedef __list_iterator<T, Ref, Ptr> self; // 以下為支援Iterator_traits而定義的一些型別 typedef bidirectional_iterator_tag iterator_category; //List的迭代器型別為雙向迭代器 typedef T value_type; typedef Ptr pointer; typedef Ref reference; typedef size_t size_type; typedef ptrdiff_t difference_type; typedef __list_node<T>* link_type; // 這個是迭代器實際管理的資源指標 link_type node; // 迭代器建構函式 __list_iterator(link_type x) : node(x) {} __list_iterator() {} __list_iterator(const iterator& x) : node(x.node) {} // 在STL演算法中需要迭代器提供支援 bool operator==(const self& x) const { return node == x.node; } bool operator!=(const self& x) const { return node != x.node; } // 過載operator *, 返回實際維護的資料 reference operator*() const { return (*node).data; } // 成員呼叫操作符 pointer operator->() const { return &(operator*()); } // 字首自加 self& operator++() { node = (link_type)((*node).next); return *this; } // 字尾自加, 需要先產生自身的一個副本, 然會再對自身操作, 最後返回副本 self operator++(int) { self tmp = *this; ++*this; return tmp; } self& operator--() { node = (link_type)((*node).prev); return *this; } self operator--(int) { self tmp = *this; --*this; return tmp; } }

List的迭代器實現了==,!=,++,–,取值和成員呼叫等操作,由於是存放在不連續的記憶體空間,所以並不支援vector那樣的p+n的操作。

List的資料結構

List的資料結構個List的節點資料結構是分開定義的,SGI的List不僅是一個雙向連結串列,而且還是一個環狀雙向連結串列,所以它只需要一個指標,就能完整表現一個連結串列。

template <class T, class Alloc = alloc>
class list
{
protected:
  typedef void* void_pointer;
  typedef __list_node<T> list_node;

  // 這個提供STL標準的allocator介面
  typedef simple_alloc<list_node, Alloc> list_node_allocator;

  // 連結串列的頭結點,並不存放資料
  link_type node;

  //....以下還有一堆List的操作函式
}
STL的List

List建構函式

List提供了一個空建構函式,如下:

list() { empty_initialize(); }

// 用於空連結串列的建立
void empty_initialize()
{
  node = get_node();
  node->next = node;  // 前置節點指向自己
  node->prev = node;  // 後置節點指向自己
}

另外,List還提供了帶參的建構函式,支援如下初始化操作:

List<int> myList(5,1); // 初始化5個1的連結串列,{1,1,1,1,1}

其建構函式原始碼如下:

// 帶參建構函式
list(size_type n, const T& value) { fill_initialize(n, value); }

// 建立值為value共n個結點的連結串列
void fill_initialize(size_type n, const T& value)
{
  empty_initialize();  // 先建立一個空連結串列
  insert(begin(), n, value); // 插入n個值為value的節點
}

// 在指定位置插入n個值為x的節點
void insert(iterator pos, int n, const T& x)
{
  insert(pos, (size_type)n, x);
}

// 在position前插入n個值為x的元素
template <class T, class Alloc>
void list<T, Alloc>::insert(iterator position, size_type n, const T& x)
{
  for ( ; n > 0; --n)
    insert(position, x);
}

// 好吧,到這裡才是真正的插入操作
// 很簡單的雙向連結串列插入操作
iterator insert(iterator position, const T& x)
{
  link_type tmp = create_node(x);
  tmp->next = position.node;
  tmp->prev = position.node->prev;
  (link_type(position.node->prev))->next = tmp;
  position.node->prev = tmp;
  return tmp;
}

STL的List提供了很多種建構函式,此處我列舉了其中一個,以此為例。

List的其他操作函式

get_node

此函式用來配置一個節點。

// 配置一個節點並返回
link_type get_node() { 
    return list_node_allocator::allocate(); 
}

put_node

此函式用來釋放一個節點。

// 釋放指定結點, 不進行析構, 析構交給全域性的destroy,
void put_node(link_type p) {
    list_node_allocator::deallocate(p); 
}

create_node

此函式用來配置並構造一個節點,並初始化其值

// 配置一個節點,並初始化其值為x
link_type create_node(const T& x)
{
  link_type p = get_node();
  construct(&p->data, x);   //全域性函式
  return p;
}

destory_node

此函式用來析構一個節點。

// 析構結點元素, 並釋放記憶體
void destroy_node(link_type p)
{
    destroy(&p->data);  //全域性函式
    put_node(p);
}

insert

此函式用來在制定位置插入一個節點(上面提到過這個函式,這裡重複一下,List的主要插入工作都交給這個函式),該函式是一個過載函式,其有多種形式。

// 好吧,到這裡才是真正的插入操作
// 很簡單的雙向連結串列插入操作
iterator insert(iterator position, const T& x)
{
  link_type tmp = create_node(x);
  tmp->next = position.node;
  tmp->prev = position.node->prev;
  (link_type(position.node->prev))->next = tmp;
  position.node->prev = tmp;
  return tmp;
}
// 其還有如下多種形式的過載函式
// 在[first,last]區間內插入元素
template <class T, class Alloc> template <class InputIterator>
void list<T, Alloc>::insert(iterator position,
                            InputIterator first, InputIterator last)
{
  for ( ; first != last; ++first)
    insert(position, *first);
}
// 在position位置插入元素,元素呼叫該型別預設建構函式
iterator insert(iterator position) { return insert(position, T()); }

push_back

在尾部插入元素,有了上面的insert函式之後,push_back就比較容易實現了。

// 在連結串列最後插入結點
void push_back(const T& x) { insert(end(), x); }

pop_front

// 在連結串列前端插入結點
void push_front(const T& x) { insert(begin(), x); }

earse

移除迭代器所指的元素

// 擦除指定結點
iterator erase(iterator position)
{
    // 雙向連結串列移除節點的操作
    link_type next_node = link_type(position.node->next);
    link_type prev_node = link_type(position.node->prev);
    prev_node->next = next_node;
    next_node->prev = prev_node;
    destroy_node(position.node);
    return iterator(next_node);
}

// 上述函式還有一個過載版本,移除區間內所有的節點
// 擦除[first, last)間的結點
template <class T, class Alloc>
list<T, Alloc>::iterator list<T, Alloc>::erase(iterator first, iterator last)
{
    while (first != last) erase(first++);
    return last;
}

pop_front

移除頭節點元素,有了上述的erase函式,就很方便的實現了。

// 刪除連結串列第一個結點
void pop_front() { erase(begin()); }

pop_back

移除連結串列中最後一個元素

// 刪除連結串列最後一個結點
void pop_back()
{
    iterator tmp = end();
    erase(--tmp);
}

clear

清除連結串列中的所有節點,也就是一個一個的清除

// 銷燬所有結點, 將連結串列置空
template <class T, class Alloc>
void list<T, Alloc>::clear()
{
  link_type cur = (link_type) node->next;
  while (cur != node) { //遍歷每一個節點
    link_type tmp = cur;
    cur = (link_type) cur->next;
    destroy_node(tmp);
  }
  node->next = node;// 移除後注意要保持連結串列是一個迴圈連結串列
  node->prev = node;
}

remove

將連結串列中值為value的節點移除

// 移除特定值的所有結點
// 時間複雜度O(n)
template <class T, class Alloc>
void list<T, Alloc>::remove(const T& value)
{
    iterator first = begin();
    iterator last = end();
    while (first != last) { //保證連結串列非空
        iterator next = first;
        ++next;
        if (*first == value) erase(first);  //擦除該節點
        first = next;
    }
}

transfer

將某段連續範圍內的元素遷移到指定位置。(非公開介面)

void transfer(iterator position, iterator first, iterator last)
{
    if (position != last)
    {
        (*(link_type((*last.node).prev))).next = position.node;
        (*(link_type((*first.node).prev))).next = last.node;
        (*(link_type((*position.node).prev))).next = first.node;
        link_type tmp = link_type((*position.node).prev);
        (*position.node).prev = (*last.node).prev;
        (*last.node).prev = (*first.node).prev;
        (*first.node).prev = tmp;
    }
}

這裡借用侯捷先生的《STL原始碼剖析》中的一幅圖來說明這個過程。

Transfer函式

splice

List提供的接合函式是Splice,上述transfer是非公開的函式。splice函式有如下幾個版本:

// 將連結串列x移動到position之前
void splice(iterator position, list& x)
{
    if (!x.empty())
        transfer(position, x.begin(), x.end()); //僅僅呼叫了transfer函式
}

// 將連結串列中i指向的內容移動到position之前
void splice(iterator position, list&, iterator i)
{
    iterator j = i;
    ++j;
    if (position == i || position == j) return;
    transfer(position, i, j);
}

// 將[first, last}元素移動到position之前
void splice(iterator position, list&, iterator first, iterator last)
{
    if (first != last)
        transfer(position, first, last);
}

merge

此函式用來合併兩個連結串列,這裡兩個連結串列必須是已拍好序的。

// 假設當前容器和x都已序, 保證兩容器合併後仍然有序
template <class T, class Alloc>
void list<T, Alloc>::merge(list<T, Alloc>& x)
{
  iterator first1 = begin();
  iterator last1 = end();
  iterator first2 = x.begin();
  iterator last2 = x.end();
  while (first1 != last1 && first2 != last2)
    if (*first2 < *first1) {
      iterator next = first2;
      transfer(first1, first2, ++next); //將first2節點遷移到first1之後
      first2 = next;
    }
    else
      ++first1;
  if (first2 != last2) transfer(last1, first2, last2);  //如果first2還有剩餘的,直接接合再連結串列1尾部
}

reverse

此函式用來反轉連結串列,其具體實現如下:

// 將連結串列倒置
template <class T, class Alloc>
void list<T, Alloc>::reverse()
{
  if (node->next == node || link_type(node->next)->next == node) return;
  iterator first = begin();
  ++first;
  while (first != end()) {
    iterator old = first;   // 取出一個節點
    ++first;
    transfer(begin(), old, first);  // 插入到begin()之後
  }
}

sort

此函式對連結串列進行升序排序,具體實現如下:

// 按照升序排序
template <class T, class Alloc>
void list<T, Alloc>::sort()
{
  if (node->next == node || link_type(node->next)->next == node) return;
  list<T, Alloc> carry;
  list<T, Alloc> counter[64];
  int fill = 0;
  while (!empty()) {
    // 從連結串列中取出一個節點
    carry.splice(carry.begin(), *this, begin());
    int i = 0;
    // 把carry中的新元素和counter中的結果逐一進行歸併
    while (i < fill && !counter[i].empty()) {
      counter[i].merge(carry);
      carry.swap(counter[i++]);
    }
    // 把歸併後的結果存放在counter[i]中
    carry.swap(counter[i]);
    // 已經達到2*fill,fill自增1
    if (i == fill) ++fill;
  }
  // 將counter中的所有元素進行歸併
  for (int i = 1; i < fill; ++i) counter[i].merge(counter[i - 1]);
  // 將counter連結串列和本連結串列進行交換
  swap(counter[fill - 1]);
}

// 交換本連結串列和連結串列x
void swap(list<T, Alloc>& x) { 
    swap(node, x.node); 
}

這裡可以舉個例子來說明一下這個過程:(以連結串列5,3,6,4,7,9,1,2,8)

carry每次從陣列中取一個數,然後歸併到counter陣列中,該演算法最多隻能排序2的64次方個數。

STL的sort過程

《STL原始碼剖析》中寫到此處是快速排序,其實我覺得應該是歸併排序。

後記

list連結串列中提供了很多操作函式,看完原始碼感覺自己重新複習了一遍資料結構裡的常用連結串列操作,同時也掌握了STL連結串列的常用函式。受益匪淺!!另外,sort不知道是侯捷先生的筆誤還是什麼,怎麼看都不像是快速排序啊。最後,大家有什麼疑惑可以再下方留言。

參考:

  • 侯捷先生的《STL的原始碼剖析》