C++應用程序性能優化(四)——C++經常使用數據結構性能分析

C++應用程序性能優化(四)——C++經常使用數據結構性能分析

本文將根據各類實用操做(遍歷、插入、刪除、排序、查找)並結合實例對經常使用數據結構進行性能分析。算法

1、經常使用數據結構簡介

一、數組

數組是最經常使用的一種線性表,對於靜態的或者預先能肯定大小的數據集合,採用數組進行存儲是最佳選擇。
數組的優勢一是查找方便,利用下標便可當即定位到所需的數據節點;二是添加或刪除元素時不會產生內存碎片;三是不須要考慮數據節點指針的存儲。然而,數組做爲一種靜態數據結構,存在內存使用率低、可擴展性差的缺點。不管數組中實際有多少元素,編譯器總會按照預先設定好的內存容量進行分配。若是超出邊界,則須要創建新的數組。數組

二、鏈表

鏈表是另外一種經常使用的線性表,一個鏈表就是一個由指針鏈接的數據鏈。每一個數據節點由指針域和數據域構成,指針通常指向鏈表中的下一個節點,若是節點是鏈表中的最後一個,則指針爲NULL。在雙向鏈表(Double Linked List)中,指針域還包括一個指向上一個數據節點的指針。在跳轉鏈表(Skip Linked List)中,指針域包含指向任意某個關聯向的指針。緩存

template <typename T>
class LinkedNode
{
public:
    LinkedNode(const T& e): pNext(NULL), pPrev(NULL)
    {
        data = e;
    }
    LinkedNode<T>* Next()const
    {
        return pNext;
    }
    LinkedNode<T>* Prev()const
    {
        return pPrev;
    }
private:
    T data;
    LinkedNode<T>* pNext;// 指向下一個數據節點的指針
    LinkedNode<T>* pPrev;// 指向上一個數據節點的指針
    LinkedNode<T>* pConnection;// 指向關聯節點的指針
};

與預先靜態分配好存儲空間的數組不一樣,鏈表的長度是可變的。只要內存空間足夠,程序就能持續爲鏈表插入新的數據項。數組中全部的數據項都被存放在一段連續的存儲空間中,鏈表中的數據項會被隨機分配到內存的某個位置。性能優化

三、哈希表

數組和鏈表有各自的優缺點,數組可以方便定位到任何數據項,但擴展性較差;鏈表則沒法提供快捷的數據項定位,但插入和刪除任意一個數據項都很簡單。當須要處理大規模的數據集合時,一般須要將數組和鏈表的優勢結合。經過結合數組和鏈表的優勢,哈希表可以達到較好的擴展性和較高的訪問效率。
雖然每一個開發者均可以構建本身的哈希表,但哈希表都有共同的基本結構,以下:
C++應用程序性能優化(四)——C++經常使用數據結構性能分析
哈希數組中每一個項都有指針指向一個小的鏈表,與某項相關的全部數據節點都會被存儲在鏈表中。當程序須要訪問某個數據節點時,不須要遍歷整個哈希表,而是先找到數組中的項,而後查詢子鏈表找到目標節點。每一個子鏈表稱爲一個桶(Bucket),如何定位一個存儲目標節點的桶,由數據節點的關鍵字域Key和哈希函數共同肯定,雖然存在多種映射方法,但實現哈希函數最經常使用的方法仍是除法映射。除法函數的形式以下:
F(k) = k % D
k是數據節點的關鍵字,D是預先設計的常量,F(k)是桶的序號(等同於哈希數組中每一個項的下標),哈希表實現以下:服務器

// 數據節點定義
template <class E, class Key>
class LinkNode
{
public:
    LinkNode(const E& e, const Key& k): pNext(NULL), pPrev(NULL)
    {
        data = e;
        key = k;
    }
    void setNextNode(LinkNode<E, Key>* next)
    {
        pNext = next;
    }
    LinkNode<E, Key>* Next()const
    {
        return pNext;
    }
    void setPrevNode(LinkNode<E, Key>* prev)
    {
        pPrev = prev;
    }
    LinkNode<E, Key>* Prev()const
    {
        return pPrev;
    }

    E& getData()const
    {
        return data;
    }
    Key& getKey()const
    {
        return key;
    }
private:
    // 指針域
    LinkNode<E, Key>* pNext;
    LinkNode<E, Key>* pPrev;
    // 數據域
    E data;// 數據
    Key key;//關鍵字
};

// 哈希表定義
template <class E, class Key>
class HashTable
{
private:
    typedef LinkNode<E, Key>* LinkNodePtr;
    LinkNodePtr* hashArray;// 哈希數組
    int size;// 哈希數組大小
public:
    HashTable(int n = 100);
    ~HashTable();
    bool Insert(const E& data);
    bool Delete(const Key& k);
    bool Search(const Key& k, E& ret)const;
private:
    LinkNodePtr searchNode()const;
    // 哈希函數
    int HashFunc(const Key& k)
    {
        return k % size;
    }

};
// 哈希表的構造函數
template <class E, class Key>
HashTable<E, Key>::HashTable(int n)
{
    size = n;
    hashArray = new LinkNodePtr[size];
    memset(hashArray, 0, size * sizeof(LinkNodePtr));
}
// 哈希表的析構函數S
template <class E, class Key>
HashTable<E, Key>::~HashTable()
{
    for(int i = 0; i < size; i++)
    {
        if(hashArray[i] != NULL)
        {
            // 釋放每一個桶的內存
            LinkNodePtr p = hashArray[i];
            while(p)
            {
                LinkNodePtr toDel = p;
                p = p->Next();
                delete toDel;
            }
        }
    }
    delete [] hashArray;
}

分析代碼,哈希函數決定了一個哈希表的效率和性能。
當F(k)=k時,哈希表中的每一個桶僅有一個節點,哈希表是一個一維數組,雖然每一個數據節點的指針會形成必定的內存空間浪費,但查找效率最高(時間複雜度O(1))。
當F(k)=c時,哈希表全部的節點存放在一個桶中,哈希表退化爲鏈表,同時還加上一個多餘的、基本爲空的數組,查找一個節點的時間效率爲O(n),效率最低。
所以,構建一個理想的哈希表須要儘量的使用讓數據節點分配更均勻的哈希函數,同時哈希表的數據結構也是影響其性能的一個重要因素。例如,桶的數量太少會形成巨大的鏈表,致使查找效率低下,太多的桶則會致使內存浪費。所以,在設計和實現哈希表前,須要分析數據節點的關鍵值,根據其分佈來決定須要造多大的哈希數組和使用什麼樣的哈希函數。
哈希表的實現中,數據節點的組織方式多種多樣,並不侷限於鏈表,桶能夠是一棵樹,也能夠是一個哈希表。數據結構

四、二叉樹

二叉樹是一種經常使用數據結構,開發人員一般熟知二叉查找樹。在一棵二叉查找樹中,全部節點的左子節點的關鍵值都小於等於自己,而右子節點的關鍵值大於等於自己。因爲平衡二叉查找樹與有序數組的折半查找算法原理相同,因此查詢效率要遠高於鏈表(O(Log2n)),而鏈表爲O(n)。但因爲樹中每一個節點都要保存兩個指向子節點的指針,空間代價要遠高於單向鏈表和數組,而且樹中每一個節點的內存分配是不連續的,致使內存碎片化。但二叉樹在插入、刪除以及查找等操做上的良好表現使其成爲最經常使用的數據結構之一。二叉樹的鏈表實現以下:ide

template <class T>
class TreeNode
{
public:
    TreeNode(const TreeNode& e): left(NULL), right(NULL)
    {
        data = e;
    }
    TreeNode<T>* Left()const
    {
        return left;
    }
    TreeNode<T>* Right()const
    {
        return right;
    }
private:
    T data;
    TreeNode<T>* left;
    TreeNode<T>* right;
};

2、數據結構的遍歷操做

一、數組的遍歷

遍歷數組的操做很簡單,不管是順序仍是逆序均可以遍歷數組,也能夠任意位置開始遍歷數組。函數

二、鏈表的遍歷

跟蹤指針便能完成鏈表的遍歷:性能

LinkNode<E>* pNode = pFirst;
    while(pNode)
    {
        pNode = pNode->Next();
        // do something
    }

雙向鏈表能夠支持順序和逆序遍歷,跳轉鏈表經過過濾某些無用節點能夠實現快速遍歷。優化

三、哈希表的遍歷

若是預先知道全部節點的Key值,能夠經過Key值和哈希函數找到每個非空的桶,而後遍歷桶的鏈表。不然只能經過遍歷哈希數組的方式遍歷每一個桶。

for(int i = 0; i < size; i++)
    {
        LinkNodePtr pNode = hashArray[i];
        while(pNode) != NULL)
        {
            // do something
            pNode = pNode->Next();
        }
    }

四、二叉樹的遍歷

遍歷二叉樹由三種方式:前序,中序,後序,三種遍歷方式的遞歸實現以下:

// 前序遍歷
template <class E>
void PreTraverse(TreeNode<E>* pNode)
{
    if(pNode != NULL)
    {
        // do something
        doSothing(pNode);
        PreTraverse(pNode->Left());
        PreTraverse(pNode->Right());
    }
}

// 中序遍歷
template <class E>
void InTraverse(TreeNode<E>* pNode)
{
    if(pNode != NULL)
    {
        InTraverse(pNode->Left());
        // do something
        doSothing(pNode);
        InTraverse(pNode->Right());
    }
}

// 後序遍歷
template <class E>
void PostTraverse(TreeNode<E>* pNode)
{
    if(pNode != NULL)
    {
        PostTraverse(pNode->Left());
        PostTraverse(pNode->Right());
        // do something
        doSothing(pNode);
    }
}

使用遞歸方式對二叉樹進行遍歷的缺點主要是隨着樹的深度增長,程序對函數棧空間的使用愈來愈多,因爲棧空間的大小有限,遞歸方式遍歷可能會致使內存耗盡。解決辦法主要有兩個:一是使用非遞歸算法實現前序、中序、後序遍歷,即仿照遞歸算法執行時函數工做棧的變化情況,創建一個棧對當前遍歷路徑上的節點進行記錄,根據棧頂元素是否存在左右節點的不一樣狀況,決定下一步操做(將子節點入棧或當前節點退棧),從而完成二叉樹的遍歷;二是使用線索二叉樹,即根據遍歷規則,在每一個葉子節點增長指向後續節點的指針。

3、數據結構的插入操做

一、數組的插入

因爲數組中的全部數據節點都保存在連續的內存中,因此插入新的節點須要移動插入位置以後的全部節點以騰出空間,才能正確地將新節點複製到插入位置。若是剛好數組已滿,還須要從新創建一個新的容量更大的數組,將原數組的全部節點拷貝到新數組,所以數組的插入操做與其它數據結構相比,時間複雜度更高。
若是向一個未滿的數組插入節點,最好的狀況是插入到數組的末尾,時間複雜度是O(1),最壞狀況是插入到數組頭部,須要移動數組的全部節點,時間複雜度是O(n)。
若是向一個滿數組插入節點,一般作法是先建立一個更大的數組,而後將原數組的全部節點拷貝到新數組,同時插入新節點,最後刪除元數組,時間複雜度爲O(n)。在刪除元數組以前,兩個數組必須並存一段時間,空間開銷較大。

二、鏈表的插入

在鏈表中插入一個新節點很簡單,對於單鏈表只須要修改插入位置以前節點的pNext指針使其指向本節點,而後將本節點的pNext指針指向下一個節點便可(對於鏈表頭不存在上一個節點,對於鏈表尾不存在下一個節點)。對於雙向鏈表和跳轉鏈表,須要修改相關節點的指針。鏈表的插入操做與長度無關,時間複雜度爲O(1),固然鏈表的插入操做一般會伴隨鏈表插入節點位置的定位,須要必定時間。

三、哈希表的插入

在哈希表中插入一個節點須要完成兩部操做,定位桶並向鏈表插入節點。

template <class E, class Key>
bool HashTable<E, Key>::Insert(const E& data)
{
    Key k = data;// 提取關鍵字
    // 建立一個新節點
    LinkNodePtr pNew = new LinkNodePtr(data, k);
    int index = HashFunc(k);//定位桶
    LinkNodePtr p = hashArray[index];
    // 若是是空桶,直接插入
    if(NULL == p)
    {
        hashArray[index] = pNew;
        return true;
    }
    // 在表頭插入節點
    hashArray[index] = pNew;
    pNew->SetNextNode(p);
    p->SetPrevNode(pNew);
    return true;
}

哈希表插入操做的時間複雜度爲O(1),若是桶的鏈表是有序的,須要花時間定位鏈表中插入的位置,若是鏈表長度爲M,則時間複雜度爲O(M)。

四、二叉樹的插入

二叉樹的結構直接影響插入操做的效率,對於平衡二叉查找樹,插入節點的時間複雜度爲O(Log2N)。對於非平衡二叉樹,插入節點的時間複雜度比較高,在最壞狀況下,非平衡二叉樹全部的left節點都爲NULL,二叉樹退化爲鏈表,插入節點新節點的時間複雜度爲O(n)。
當節點數量不少時,對平衡二叉樹中進行插入操做的效率要遠高於非平衡二叉樹。工程開發中,一般避免非平衡二叉樹的出現,或是將非平衡二叉樹轉換爲平衡二叉樹。簡單作法以下:
(1)中序遍歷非平衡二叉樹,在一個數組中保存全部的節點的指針。
(2)因爲數組中全部元素都是有序排列的,可使用折半查找遍歷數組,自上而下逐層構建平衡二叉樹。

4、數據結構的刪除操做

一、數組的刪除

從數組中刪除節點,若是須要數組沒有空洞,須要在刪除節點後將其後全部節點向前移動。最壞狀況下(刪除首節點),時間複雜度爲O(n),最好狀況下(刪除尾節點),時間複雜度爲O(1)。
在某些場合(如動態數組),當刪除完成後若是數組中存在大量空閒位置,則須要縮小數組,即建立一個較小的新數組,將原數組中全部節點拷貝到新數組,再將原數組刪除。所以,會致使較大的空間與時間開銷,應謹慎設置數組的大小,即要儘可能避免內存空間的浪費也要減小數組的放大或縮小操做。一般,每當須要刪除數組中的某個節點時,並不將其真正刪除,而是在節點的位置設計一個標記位bDelete,將其設置爲true,同時禁止其它程序使用本節點,待數組中須要刪除的節點達到必定閾值時,再統一刪除,避免屢次移動節點操做,下降時間複雜度。

二、鏈表的刪除

鏈表中刪除節點的操做,直接將被刪除節點的上一節點的指針指向被刪除節點的下一節點便可,刪除操做的時間複雜度是O(1)。

三、哈希表的刪除

從哈希表中刪除一個節點的操做以下:首先經過哈希函數和鏈表遍歷(桶由鏈表實現)找到待刪除節點,而後刪除節點並從新設置前向和後向指針。若是被刪除節點是桶的首節點,則將桶的頭指針指向後續節點。

template <class E, class Key>
bool HashTable<E, Key>::Delete(const Key& k)
{
    // 找到關鍵值匹配的節點
    LinkNodePtr p = SearchNode(k);

    if(NULL == p)
    {
        return false;
    }
    // 修改前向節點和後向節點的指針
    LinkNodePtr pPrev = p->Prev();
    if(pPrev)
    {
        LinkNodePtr pNext = p->Next();
        if(pNext)
        {
            pNext->SetPrevNode(pPrev);
            pPrev->SetNextNode(pNext);
        }
        else
        {
            // 若是前向節點爲NULL,則當前節點p爲首節點
            // 修改哈希數組中的節點的指針,使其指向後向節點。
            int index = HashFunc(k);
            hashArray[index] = p->Next();
            if(p->Next() != NULL)
            {
                p->Next()->SetPrevNode(NULL);
            }
        }
    }
    delete p;
    return true;
}

四、二叉樹的刪除

從二叉樹刪除一個節點須要根據狀況討論:
(1)若是節點是葉子節點,直接刪除。
(2)若是刪除節點僅有一個子節點,則將子節點替換被刪除節點。
(3)若是刪除節點的左右子節點都存在,因爲每一個子節點均可能有本身的子樹,須要找到子樹中合適的節點,並將其立爲新的根節點,並整合兩棵子樹,從新加入到原二叉樹。

5、數據結構的排序操做

一、數組的排序

數組的排序包括冒泡、選擇、插入等排序方法。

template <typename T>
void Swap(T& a, T& b)
{
  T temp;
  temp = a;
  a = b;
  b = temp;
}

冒泡排序實現:

/**********************************************
* 排序方式:冒泡排序
* array:序列
* len:序列中元素個數
* min2max:按從小到大進行排序
* *******************************************/
template <typename T>
static void Bubble(T array[], int len, bool min2max = true)
{
    bool exchange = true;
    //遍歷全部元素
    for(int i = 0; (i < len) && exchange; i++)
    {
            exchange = false;
            //將尾部元素與前面的每一個元素做比較交換
            for(int j = len - 1; j > i; j--)
            {
                    if(min2max?(array[j] < array[j-1]):(array[j] > array[j-1]))
                    {
                            //交換元素位置
                            Swap(array[j], array[j-1]);
                            exchange = true;
                    }
            }
    }
}

冒泡排序的時間複雜度爲O(n^2),冒泡排序是穩定的排序方法。
選擇排序實現:

/******************************************
* 排序方式:選擇排序
* array:序列
* len:序列中元素個數
* min2max:按從小到大進行排序
* ***************************************/
template <typename T>
void Select(T array[], int len, bool min2max = true)
{
 for(int i = 0; i < len; i++)
 {
     int min = i;//從第i個元素開始
     //對待排序的元素進行比較
     for(int j = i + 1; j < len; j++)
     {
         //按排序的方式選擇比較方式
         if(min2max?(array[min] > array[j]):(array[min] < array[j]))
         {
             min = j;
         }
     }
     if(min != i)
     {
        //元素交換
        Swap(array[i], array[min]);
     }
 }
}

選擇排序的時間複雜度爲O(n^2),選擇排序是不穩定的排序方法。
插入排序實現:

/******************************************
 * 排序方式:選擇排序
 * array:序列
 * len:序列中元素個數
 * min2max:按從小到大進行排序
 * ***************************************/
template <typename T>
void Select(T array[], int len, bool min2max = true)
{
  for(int i = 0; i < len; i++)
  {
      int min = i;//從第i個元素開始
      //對待排序的元素進行比較
      for(int j = i + 1; j < len; j++)
      {
          //按排序的方式選擇比較方式
          if(min2max?(array[min] > array[j]):(array[min] < array[j]))
          {
              min = j;
          }
      }
      if(min != i)
      {
         //元素交換
         Swap(array[i], array[min]);
      }
  }
}

插入排序的時間複雜度爲O(n^2),插入排序是穩定的排序方法。

二、鏈表的排序

雖然鏈表在插入和刪除操做上性能優越,但排序複雜度卻很高,尤爲是單向鏈表。因爲鏈表中訪問某個節點須要依賴其它節點,不能根據下標直接定位到任意一項,所以節點定位的時間複雜度爲O(N),排序效率低下。
工程開發中,可使用數組鏈表,當須要排序時構造一個數組,存放鏈表中每一個節點的指針。在排序過程當中經過數組定位每一個節點,並實現節點的交換。
鏈表數組爲直接訪問鏈表的節點提供了便利,可是使用空間換時間的方法,若是但願獲得一個有序鏈表,最好是在構建鏈表時將每一個節點插入到合適的位置。

三、哈希表的排序

因爲採用哈希函數訪問每一個桶,所以哈希表中對哈希數組排序毫無心義,但具體節點的定位須要經過查詢每一個桶鏈表完成(桶由鏈表實現),將桶的鏈表排序能夠提升節點的查詢效率。

四、二叉樹的排序

對於二叉查找樹,其自己是有序的,中序遍歷能夠獲得二叉查找樹有序的節點輸出。對於未排序的二叉樹,全部節點被隨機組織,定位節點的時間複雜度爲O(N)。

6、數據結構的查找操做

一、數組的查找

數組的最大優勢是能夠經過下標任意的訪問節點,而不須要藉助指針、索引或遍歷,時間複雜度爲O(1)。對於下標未知的狀況查找節點,則只能遍歷數組,時間複雜度爲O(N)。對於有序數組,最好的查找算法是二分查找法。

template <class E>
int BinSearch(E array[], const E& value, int start, int end)
{
    if(end - start < 0)
    {
        return INVALID_INPUT;
    }
    if(value == array[start])
    {
        return start;
    }
    if(value == array[end])
    {
        return end;
    }
    while(end > start + 1)
    {
        int temp  = (end + start) / 2;
        if(value == array[temp])
        {
            return temp;
        }
        if(array[temp] < value)
        {
            start = temp;
        }
        else
        {
            end = temp;
        }
    }
    return -1;
}

折半查找的時間複雜度是O(Log2N),與二叉樹查詢效率相同。
對於亂序數組,只能經過遍歷方法查找節點,工程開發中一般設置一個標識變量保存更新節點的下標,執行查詢時從標識變量標記的下標開始遍歷數組,執行效率比從頭開始要高。

二、鏈表的查找

對於單向鏈表,最差狀況下須要遍歷整個鏈表才能找到須要的節點,時間複雜度爲O(N)。
對於有序鏈表,能夠預先獲取某些節點的數據,能夠選擇與目標數據最接近的一個節點查找,效率取決於已知節點在鏈表中的分佈,對於雙向有序鏈表效率會更高,若是正中節點已知,則查詢的時間複雜度爲O(N/2)。
對於跳轉鏈表,若是預先可以根據鏈表中節點之間的關係創建指針關聯,查詢效率將大大提升。

三、哈希表的查找

哈希表中查詢的效率與桶的數據結構有關。桶由鏈表實現,則查詢效率和鏈表長度有關,時間複雜度爲O(M)。查找算法實現以下:

template <class E, class Key>
bool HashTable<E, Key>::SearchNode(const Key& k)const
{
    int index = HashFunc(k);
    // 空桶,直接返回
    if(NULL == hashArray[index])
        return NULL;
    // 遍歷桶的鏈表,若是由匹配節點,直接返回。
    LinkNodePtr p = hashArray[index];
    while(p)
    {
        if(k == p->GetKey())
            return p;
        p = p->Next();
    }
}

四、二叉樹的查找

在二叉樹中查找節點與樹的形狀有關。對於平衡二叉樹,查找效率爲O(Log2N);對於徹底不平衡的二叉樹,查找效率爲O(N);
工程開發中,一般須要構建儘可能平衡的二叉樹以提升查詢效率,但平衡二叉樹受插入、刪除操做影響很大,插入或刪除節點後須要調整二叉樹的結構,一般,當二叉樹的插入、刪除操做不少時,不須要在每次插入、刪除操做後都調整平衡度,而是在密集的查詢操做前統一調整一次。

7、動態數組的實現及分析

一、動態數組簡介

工程開發中,數組是經常使用數據結構,若是在編譯時就知道數組全部的維數,則能夠靜態定義數組。靜態定義數組後,數組在內存中佔據的空間大小和位置是固定的,若是定義的是全局數組,編譯器將在靜態數據區爲數組分配空間,若是是局部數組,編譯器將在棧上爲數組分配空間。但若是預先沒法知道數組的維數,程序只有在運行時才知道須要分配多大的數組,此時C++編譯器能夠在堆上爲數組動態分配空間。
動態數組的優勢以下:
(1)可分配空間較大。棧的大小都有限制,Linux系統可使用ulimit -s查看,一般爲8K。開發者雖然能夠設置,但因爲須要保證程序運行效率,一般不宜太大。堆空間的一般可供分配內存比較大,達到GB級別。
(2)使用靈活。開發人員能夠根據實際須要決定數組的大小和維數。
動態數組的缺點以下:
(1)空間分配效率比靜態數組低。靜態數組通常由棧分配空間,動態數組通常由堆分配空間。棧是機器系統提供的數據結構,計算機會在底層爲棧提供支持,即分配專門的寄存器存放棧的地址,壓棧和出棧都有專門的機器指令執行,於是棧的效率比較高。堆由C++函數庫提供,其內存分配機制比棧要複雜得多,爲了分配一塊內存,庫函數會按照必定的算法在堆內存內搜索可用的足夠大小的空間,若是發現空間不夠,將調用內核方法去增長程序數據段的存儲空間,從而程序就有機會分配足夠大的內存。所以堆的效率要比棧低。
(2)容易形成內存泄露。動態內存須要開發人員手工分配和釋放內存,容易因爲開發人員的疏忽形成內存泄露。

二、動態數組實現

在實時視頻系統中,視頻服務器承擔視頻數據的緩存和轉發工做。通常,服務器爲每臺攝像機開闢必定大小且獨立的緩存區。視頻幀被寫入此緩存區後,服務器在某一時刻再將其讀出,並向客戶端轉發。視頻幀的轉發是臨時的,因爲緩存區大小有限而視頻數據源源不斷,因此一幀數據在被寫入事後,過一段時間並會被新來的視頻幀所覆蓋。視頻幀的緩存時間由緩存區和視頻幀的長度決定。
因爲視頻幀數據量巨大,而一臺服務器一般須要支持幾十臺甚至數百臺攝像機,緩存結構的設計是系統的重要部分。一方面,若是預先分配固定數量的內存,運行時再也不增長、刪除,則服務器只能支持必定數量的攝像機,靈活性小;另外一方面,因爲視頻服務器程序在啓動時將佔據一大塊內存,將致使系統總體性能降低,所以考慮使用動態數組實現視頻緩存。
首先,服務器中爲每臺攝像機分配一個必定大小的緩存塊,由類CamBlock實現。每一個CamBlock對象中有兩個動態數組,分別存放視頻數據的_data和存放視頻幀索引信息的_frameIndex。每當程序在內存中緩存(讀取)一個視頻幀時,對應的CamBlock對象將根據視頻幀索引表_frameIndex找到視頻幀在_data中的存放位置,而後將數據寫入或讀出。_data是一個循環隊列,通常根據FIFO進行讀取,即若是有新幀進入隊列,程序會在_data中最近寫入幀的末尾開始複製,若是超出數組長度,則從頭覆蓋。
C++應用程序性能優化(四)——C++經常使用數據結構性能分析

// 視頻幀的數據結構
typedef struct
{
    unsigned short idCamera;// 攝像機ID
    unsigned long length;// 數據長度
    unsigned short width;// 圖像寬度
    unsigned short height;// 圖像高度
    unsigned char* data; // 圖像數據地址
} Frame;

// 單臺攝像機的緩存塊數據結構
class CamBlock
{
public:
    CamBlock(int id, unsigned long len, unsigned short numFrames):
        _data(NULL), _length(0), _idCamera(-1), _numFrames(0)
    {
        // 確保緩存區大小不超過閾值
        if(len > MAX_LENGTH || numFrames > MAX_FRAMES)
        {
            throw;
        }
        try
        {
            // 爲幀索引表分配空間
            _frameIndex = new Frame[numFrames];
            // 爲攝像機分配指定大小的內存
            _data = new unsigned char[len];
        }
        catch(...)
        {
            throw;
        }
        memset(this, 0, len);
        _length = len;
        _idCamera = id;
        _numFrames = numFrames;

    }
    ~CamBlcok()
    {
        delete [] _frameIndex;
        delete [] _data;
    }
    // 根據索引表將視頻幀存入緩存
    bool SaveFrame(const Frame* frame);
    // 根據索引表定位到某一幀,讀取
    bool ReadFrame(Frame* frame);
private:
    Frame* _frameIndex;// 幀索引表
    unsigned char* _data;//存放圖像數據的緩存區
    unsigned long _length;// 緩存區大小
    unsigned short _idCamera;// 攝像機ID
    unsigned short _numFrames;//可存放幀的數量
    unsigned long _lastFrameIndex;//最後一幀的位置
};

爲了管理每臺攝像機獨立的內存塊,快速定位到任意一臺攝像機的緩存,甚至任意一幀,須要創建索引表CameraArray來管理全部的CamBlock對象。

class CameraArray
{
    typedef CamBlock BlockPtr;
    BlockPtr* cameraBufs;// 攝像機視頻緩存
    unsigned short cameraNum;// 當前已經鏈接的攝像機臺數
    unsigned short maxNum;//cameraBufs容量
    unsigned short increaseNum;//cameraBufs的增量
public:
    CameraArray(unsigned short max, unsigned short inc);
    ~CameraArray();
    // 插入一臺攝像機
    CamBlock* InsertBlock(unsigned short idCam, unsigned long size, unsigned short numFrames);
    // 刪除一臺攝像機
    bool RemoveBlock(unsigned short idCam);
private:
    // 根據攝像機ID返回其在數組的索引
    unsigned short GetPosition(unsigned short idCam);
};

CameraArray::CameraArray(unsigned short max, unsigned short inc):
    cameraBufs(NULL), cameraNum(0), maxNum(0), increaseNum(0)
{
    // 若是參數越界,拋出異常
    if(max > MAX_CAMERAS || inc > MAX_INCREMENTS)
        throw;
    try
    {
        cameraBufs = new BlockPtr[max];
    }
    catch(...)
    {
        throw;
    }
    maxNum = max;
    increaseNum = inc;
}

CameraArray::~CameraArray()
{
    for(int i = 0; i < cameraNum; i++)
    {
        delete cameraBufs[i];
    }
    delete [] cameraBufs;
}

一般,會爲每一個攝像機安排一個整型的ID,在CameraArray中,程序按照ID遞增的順序排列每一個攝像機的CamBlock對象以方便查詢。當一個新的攝像機接入系統時,程序會根據它的ID在CameraArray中找到一個合適的位置,而後利用相應位置的指針建立一個新的CamBlock對象;當某個攝像機斷開鏈接,程序也會根據它的ID,找到對應的CamBlock緩存塊,並將其刪除。

CamBlock* CameraArray::InsertBlock(unsigned short idCam, unsigned long size,
                                   unsigned short numFrames)
{
    // 在數組中找到合適的插入位置
    int pos = GetPosition(idCam);
    // 若是已經達到數組邊界,須要擴大數組
    if(cameraNum == maxNum)
    {
        // 定義新的數組指針,指定其維數
        BlockPtr* newBufs = NULL;
        try
        {
            BlockPtr* newBufs = new BlockPtr[maxNum + increaseNum];
        }
        catch(...)
        {
            throw;
        }
        // 將原數組內容拷貝到新數組
        memcpy(newBufs, cameraBufs, maxNum * sizeof(BlockPtr));
        // 釋放原數組的內存
        delete [] cameraBufs;
        maxNum += increaseNum;
        // 更新數組指針
        cameraBufs = newBufs;
    }
    if(pos != cameraNum)
    {
        // 在數組中插入一個塊,須要將其後全部指針位置後移
        memmov(cameraBufs + pos + 1, cameraBufs + pos, (cameraNum - pos) * sizeof(BlockPtr));
    }
    ++cameraNum;
    CamBlock* newBlock = new CamBlock(idCam, size, numFrames);
    cameraBufs[pos] = newBlock;
    return cameraBufs[pos];
}

若是接入系統的攝像機數量超出了最初建立CameraArray的設計容量,則考慮到系統的可擴展性,只要硬件條件容許,須要增長cameraBufs的長度。

bool CameraArray::RemoveBlock(unsigned short idCam)
{
    if(cameraNum < 1)
        return false;
    // 在數組中找到要刪除的攝像機的緩存區的位置
    int pos = GetPosition(idCam);
    cameraNum--;
    BlockPtr deleteBlock = cameraBufs[pos];
    delete deleteBlock;
    if(pos != cameraNum)
    {
        // 將pos後全部指針位置前移
        memmov(cameraBufs + pos, cameraBufs + pos + 1, (cameraNum - pos) * sizeof(BlockPtr));
    }
    // 若是數組中有過多空閒的位置,進行釋放
    if(maxNum - cameraNum > increaseNum)
    {
        // 從新計算數組的長度
        unsigned short len = (cameraNum / increaseNum + 1) * increaseNum;
        // 定義新的數組指針
        BlockPtr* newBufs = NULL;
        try
        {
            newBufs = new BlockPtr[len];
        }
        catch(...)
        {
            throw;
        }
        // 將原數組的數據拷貝到新的數組
        memcpy(newBufs, cameraBufs, cameraNum * sizeof(BlockPtr));
        delete cameraBufs;
        cameraBufs = newBufs;
        maxNum = len;
    }
    return true;
}

若是刪除一臺攝像機時,發現數組空間有過多空閒空間,則須要釋放相應空閒空間。

相關文章
相關標籤/搜索