[GeekBand] STL vector 查找拷貝操做效率分析

本文參考文獻::GeekBand課堂內容,授課老師:張文傑html

            :C++ Primer 11 中文版(第五版)ios

                   :網絡資料: 葉卡同窗的部落格  http://www.leavesite.com/算法

                                                             http://blog.sina.com.cn/s/blog_a2a6dd380102w73e.htmlwindows

 

1、關於Vector的基本概念及相關算法簡介

一、什麼是vector?數組

  簡單的理解:數組!網絡

     進一步的理解:變長一維的動態數組,連續存放的內存塊,堆內分配內存。支持[]操做(一會就會用到),支持下標操做。數據結構

     再進一步的理解:vector表示對象的集合,集合中每一個對象都有與之對應的索引(理解爲下標,這裏能夠保存不少元素的對象,包括不限於,如int string、或者本身定義的類的對象等等。app

 

二、Vector的初始化less

有幾種基本初始化方法:函數

vector<T> v1 ;          //構造函數
vector<T> v2(v1) ;    //拷貝構造函數
vector<T> v2 =v1 ;   //拷貝賦值
vector<T> v1{ 0, 0, 30, 20, 0, 0, 0, 0, 10, 0 };   //初始化

 本文中採用最基本的方法進行初始化。

三、Vector基本操做

vector<int>  vec;
vector<int> vec2;
vec.push_back(t);  //向v的尾端添加元素t
vec.empty();
vec.size();
vec == vec2;  //相等相似的操做都有

4、迭代器

簡單的理解:相似於指針。以下面的一句話,迭代器有一個優勢,那就是全部的標準庫容器均可以使用迭代器,具備極強的通用性。

 

vector<int>::iterator result = find_if(Vec1.begin(), Vec1.end(), bind2nd(not_equal_to<int>(), unexpectedInt));

 

5、本文中可能會用到的一些(算法)操做:

一、函數 :not_equal_to,重載了操做符(),判斷左右兩個數是否相等,不等返回值 TRUE、即1,相等返回 FALSE、即0.

    相似的函數有equal_to 

template<class _Ty = void>
    struct not_equal_to
        : public binary_function<_Ty, _Ty, bool>
    {    // functor for operator!=
    bool operator()(const _Ty& _Left, const _Ty& _Right) const
        {    // apply operator!= to operands
        return (_Left != _Right);
        }
    };

 

二、bind1st 和 bind2nd

bind1st(const Fn2& Func,const Ty& left)  :1st指:傳進來的參數應該放左邊,也就是第一位
bind2nd(const Fn2& Func,const Ty& right) :2nd指:傳進來的參數應該放右邊,也就是第二位

簡單的兩個例子:

#include "stdafx.h"
#include <vector>
#include <functional>
#include <algorithm>
#include <iostream>
#include <iterator>
#include<windows.h>
#include <Mmsystem.h>

using namespace std;

void ShowArray(vector<int> &Vec)
{
    vector<int>::iterator it = Vec.begin();
    //it 是一個地址  
    while (it < Vec.end())
    {
        cout << *it << endl;
        it++;
    }

};
int _tmain(int argc, _TCHAR* argv[])
{
    int a[] = { 1, 2, 100, 200 };
    std::vector<int> arr(a, a + 4);

    // 移除全部小於100的元素
    arr.erase(std::remove_if(arr.begin(), arr.end(),
        std::bind2nd(std::less< int>(), 100)), arr.end());
    ShowArray(arr);
    /**************************************/
    printf("*******************************\n");
    int b[] = { 1, 2, 100, 200 };
    std::vector<int> arr2(b, b + 4);
    // 移除全部大於100的元素
    arr2.erase(std::remove_if(arr2.begin(), arr2.end(),
        std::bind1st(std::less< int>(), 99)), arr2.end());
    ShowArray(arr2);
}

本例中由於僅判斷是否爲0 ,全部採用bind1st 和 bind2nd都同樣。

 

三、remove_copy_if

remove_copy_if() 函數原型

template<class _InIt,class _OutIt,class _Pr> 
  inline _OutIt remove_copy_if(_InIt _First, _InIt _Last,
        _OutIt _Dest, _Pr _Pred)
    {    
// copy omitting each element satisfying _Pred
    _DEBUG_RANGE(_First, _Last);
    _DEBUG_POINTER(_Dest);
    _DEBUG_POINTER(_Pred);
    return (_Remove_copy_if(_Unchecked(_First), _Unchecked(_Last),
        _Dest, _Pred,
        _Is_checked(_Dest)));
    }

 

remove_copy_if()的思考方式和copy_if()相反,若IsNotZero為true,則不copy,若為false,則copy。

 

remove_copy_if(Vec1.begin(), Vec1.end(), back_inserter(Vec2), IsNotZero);

此時要求: 當unexpectedInt 爲0時,返回值爲TRUE,不進行拷貝;當unexpectedInt 不爲0時,返回值爲FALSE,則進行copy。

bool IsNotZero(int unexpectedInt)
{
    return (unexpectedInt == 0);
}

 

 

2、三種不一樣方法來實現將查找拷貝操做

完成代碼以下: 開發環境 VS2013 IDE

// Vector.cpp : 定義控制檯應用程序的入口點。
//


/*
問題:
給定一個 vector:v1 = [0, 0, 30, 20, 0, 0, 0, 0, 10, 0],
但願經過 not_equal_to 算法找到到不爲零的元素,並複製到另外一個 vector: v2
*/

/*
要點一:
    第一步、利用not_equal_to函數進行數值比較,區分vector某一元素是不是非0數據
    第二步、查找全部的非0元素
    第三步、將全部非0元素拷貝到v2中來
要點二:效率問題
    測試結果:
    利用下標耗費時間最少,運行速度比較快,但不通用(vector能夠利用下標)。
    利用迭代器耗費時間較多,但更爲通用。
    利用C++ 11 remove_copy_if() algorithm 進行分析

總結:
    C++ 11 remove_copy_if() algorithm 代碼最少,效率最高。

*/


#include "stdafx.h"
#include <vector>
#include <functional>
#include <algorithm>
#include <iostream>
#include <iterator>
#include<windows.h>
#include <Mmsystem.h>
using namespace std;
#pragma comment( lib,"winmm.lib" )


//利用下標的方法

void FiltArray0(vector<int> &Vec1, vector<int> &Vec2, const int unexpectedInt)
{
    //測試時間
    LARGE_INTEGER litmp;
    LONGLONG qt1, qt2;
    double dft, dff, dfm;
    QueryPerformanceFrequency(&litmp);//得到時鐘頻率
    dff = (double)litmp.QuadPart;
    QueryPerformanceCounter(&litmp);//得到初始值
    //測試時間開始
    qt1 = litmp.QuadPart;

    int size = Vec1.size();
    for (int i = 0; i<size; i++)
    {
        if (not_equal_to<int>()(Vec1[i], unexpectedInt))
        {
            Vec2.push_back(Vec1[i]);
        }
        else
            continue;
    }

    QueryPerformanceCounter(&litmp);//得到終止值
    qt2 = litmp.QuadPart;
    dfm = (double)(qt2 - qt1);
    dft = dfm / dff;//得到對應的時間值
    cout<<"下標方法測試時間爲:" << dft << endl;

}




//使用迭代器
void FiltArray1(vector<int> &Vec1, vector<int> &Vec2, const int unexpectedInt)
{
    //測試時間
    LARGE_INTEGER litmp;
    LONGLONG qt1, qt2;
    double dft, dff, dfm;
    QueryPerformanceFrequency(&litmp);//得到時鐘頻率
    dff = (double)litmp.QuadPart;
    QueryPerformanceCounter(&litmp);//得到初始值
    //測試時間開始
    qt1 = litmp.QuadPart;

    //查找第一個不爲0的數值
    vector<int>::iterator result = find_if(Vec1.begin(), Vec1.end(), bind2nd(not_equal_to<int>(), unexpectedInt));
    while (result != Vec1.end())
    {
        Vec2.push_back(*result);
        //result結果的下一位開始查找不爲0的數
        result = find_if(result + 1, Vec1.end(), bind2nd(not_equal_to<int>(), unexpectedInt));
    }

    QueryPerformanceCounter(&litmp);//得到終止值
    qt2 = litmp.QuadPart;
    dfm = (double)(qt2 - qt1);
    dft = dfm / dff;//得到對應的時間值
    cout << "迭代器方法測試時間爲:" << dft << endl;


}


bool IsNotZero(int unexpectedInt)
{
    return (unexpectedInt == 0);
}

void FiltArray2(vector<int> &Vec1, vector<int> &Vec2, const int unexpectedInt)
{

    //測試時間
    LARGE_INTEGER litmp;
    LONGLONG qt1, qt2;
    double dft, dff, dfm;
    QueryPerformanceFrequency(&litmp);//得到時鐘頻率
    dff = (double)litmp.QuadPart;
    QueryPerformanceCounter(&litmp);//得到初始值
    //測試時間開始
    qt1 = litmp.QuadPart;

    // C++ 11 裏的函數
    //《effective STL》 :儘可能用算法替代手寫循環;查找少不了循環遍歷
    remove_copy_if(Vec1.begin(), Vec1.end(), back_inserter(Vec2), IsNotZero);

    QueryPerformanceCounter(&litmp);//得到終止值
    qt2 = litmp.QuadPart;
    dfm = (double)(qt2 - qt1);
    dft = dfm / dff;//得到對應的時間值
    cout << "利用拷貝算法測試時間爲:" << dft << endl;

}





void ShowArray(vector<int> &Vec)
{
    vector<int>::iterator it = Vec.begin();
    //it 是一個地址  
    while (it < Vec.end())
    {
        cout << *it << endl;
        it++;
    }


}

void ClearArray(vector<int> &Vec)
{
    vector<int>::iterator it = Vec.begin();
    //清空數據
    while (it < Vec.end())
    {
        it = Vec.erase(it);
    }

}

int main()
{  
    vector<int> v1{ 0, 0, 30, 20, 0, 0, 0, 0, 10, 0 };
    vector<int> v2;
    const int unexpectedInt = 0;
    /*
    方案一:   利用數組下標sanzho
    */
    FiltArray0(v1, v2, unexpectedInt);
    cout << "利用數組下標方案,V2中數據爲:" << endl;
    ShowArray(v2);
    /*
    方案二:   利用迭代器
    */
    ClearArray(v2);
    FiltArray1(v1, v2, unexpectedInt);
    cout << "利用迭代器方案,V2中數據爲:" << endl;
    ShowArray(v2);
    /*
    方案三:    利用拷貝算法
    */
    ClearArray(v2);
    FiltArray2(v1, v2, unexpectedInt);
    cout << "利用拷貝算法,V2中數據爲:" << endl;
    ShowArray(v2);
    return 0;
}

 

 

3、效率對比

運行了幾回,來觀察實際運行時間。

 

 

等等。綜合發現DEBUG模式下。

  第三種方案的運行時間最長,代碼量最少。

  第二種方案的運行時間最長,更爲通用。

  第一種方案的運行時間居中,但不通用。

 

Release模式下,數據以下圖所示:

 

 

 

 

 

 

數據整理,對好比下表所示:

  下標操做 迭代器操做 remove_copy_if()算法操做
Debug統計數據一 0.000015762 0.0000395882 0.00000733115
Debug統計數據二 0.00000879738 0.000023093 0.00000806426
Debug統計數據三 0.00000879738 0.0000373889 0.00000733115
Release模式一 0.00000146623 0 0
Release模式二 0.00000146623 0.000000366557 0.000000366557
Release模式三 0.00000146623 0.00000109967 0.000000366557

 

對比發現,Release版本通過優化後,模式使用迭代器耗費的時間下降了很多。此時居然比下標運行時間還要短!

 

4、進一步的思考與總結

1)效率相比本身手寫更高;STL的代碼都是C++專家寫出來的,專家寫出來的代碼在效率上很難超越; 
2)千萬注意要使用++iter 不能使用iter++,iter++ 是先拷貝一份值,再進行++,效率很低;

3)經過分析,用algorithm+functional進行遍歷效率最高。並且 下標索引的方式老是會效率高於迭代器方式。

那麼爲何迭代器速率比較慢呢?
其中的一位網友的解釋:std::vector::end()的原型
    iterator end() _NOEXCEPT
        {    // return iterator for end of mutable sequence
        return (iterator(this->_Mylast, this));
        }

    const_iterator end() const _NOEXCEPT
        {    // return iterator for end of nonmutable sequence
        return (const_iterator(this->_Mylast, this));
        }

 

在Debug模式下,每次判斷itr != Vec.end()的時候,都要進行從新構造一個迭代器並進行返回,這樣固然下降的效率。
 
但同時,迭代器具備良好的通用性,在效率要求不是那麼高的狀況下,其實用哪一個都無所謂!
 
 
4)push_back耗費時間複雜度分析,有一篇文章解釋的清晰明瞭,估計看了就沒明白爲何時間會差距這麼大。
   (轉帖) http://blog.sina.com.cn/s/blog_a2a6dd380102w73e.html
內容以下:

      vectorSTL中的一種序列式容器,採用的數據結構爲線性連續空間,它以兩個迭代器 start 和 finish 分別指向配置得來的連續空間中目前已被使用的範圍,並以迭代器end_of_storage 指向整塊連續空間(含備用空間)的尾端,結構以下所示:

    template Alloc = alloc>

    class vector {

  ​  ...

    protected:

        iterator start;                     // 表示目前使用空間的頭

        iterator finish;                   // 表示目前使用空間的尾

        iterator end_of_storage;  // 表示可用空間的尾​

     ...};​

     咱們在使用 vector 時​,最常使用的操做恐怕就是插入操做了(push_back),那麼當執行該操做時,該函數都作了哪些工做呢?

    該函數首先檢查是否還有備用空間,若是有就直接在備用空間上構造元素,並調整迭代器 finish,使 vector 變大。若是沒有備用空間了,就擴充空間,從新配置、移動數據,釋放原空間。​

    其中​判斷是否有備用空間,就是判斷  finish 是否與 end_of_storage 相等.若是 

finish != end_of_storage,說明還有備用空間,不然已無備用空間。

    當執行 push_back 操做,該 vector 須要分配更多空間時,它的容量(capacity)會增大到原來的 倍。​如今咱們來均攤分析方法來計算 push_back 操做的時間複雜度。

    假定有 n 個元素,倍增因子爲 m。那麼完成這 n 個元素往一個 vector 中的push_back​操做,須要從新分配內存的次數大約爲 logm(n),第 i 次從新分配將會致使複製 m^i (也就是當前的vector.size() 大小)箇舊空間中元素,所以 n 次 push_back 操做所花費的總時間約爲 n*m/(m - 1):

 

時間複雜度計算
 

    很明顯這是一個等比數列.那麼 n 個元素,n 次操做,每一次操做須要花費時間爲 m / (m - 1),這是一個常量.

    因此,咱們採用均攤分析的方法可知,vector 中 push_back 操做的時間複雜度爲常量時間.​

 
 
 
 
 
 
 
 
 
 
 
           
                                             2016.08.22



 

 

 

 

 

 

 

 

                                  

 

相關文章
相關標籤/搜索