1. 程式人生 > >資料結構--佇列-泛型OC&C++混編-泛型程式設計

資料結構--佇列-泛型OC&C++混編-泛型程式設計

在這篇文章裡, 您可以學習到:

  1. 資料結構簡介
  2. 資料結構的邏輯結構和物理結構
  3. 佇列
  4. OC和C++在Xcode中的混編
  5. 泛型程式設計思想
  6. 泛型程式設計實現迴圈佇列和連結串列佇列
  7. 部落格中使用的圖片均來自網路

一.資料結構簡介

資料結構是計算機儲存、組織資料的方式。資料結構是指相互之間存在一種或多種特定關係的資料元素的集合。通常情況下,精心選擇的資料結構可以帶來更高的執行或者儲存效率。資料結構往往同高效的檢索演算法和索引技術有關。

幫你理解

  1. 資料結構的實現與語言無關, 它可以被任何語言實現.
  2. 一個好的抽象, 可以適用普遍資料型別. 除了特定場合和教學示例外, 你不能只支援某種特定的資料型別.

談到資料結構, 一定會涉及到的兩個概念: 資料 和 資料之間的關係.

  • 資料: 我們前面提到, 資料結構不分資料型別, 這個資料可以是任何資料, 你可以把它想象成是一個收納盒, 盒子內可以放任何東西.

  • 關係: 按照相面的比喻, 關係就是指這些收納盒之間的對映關係. 比如我規定一種關係: 把這些盒子排成一列, 對於中間的某個盒子來講, 它的前面和後面都有一個和它緊挨的盒子. 我們可以說, 這個盒子和它前面和後面的盒子有關係, 和它不相鄰的盒子沒有關係. 那麼我們可以把這樣的模型剝離出來, 得到一種資料結構 – 線性結構, 又叫線性表.

推理
按照這種抽象模型的過程, 我們可以把下面的關係抽離出另一種資料結構.
file-listfile-list
還可以繼續抽象, 元素之間的關係式任意的, 就得到了網狀結構, 也就是我們所說的

.
file-listfile-list

抽象出狀結構之後, 我們基本已經無法再從對應關係上來細分了, 不信您就想想, 你所成想到的關係, 全部包含在這幾種抽象中了

嗎?

當然不是, 空間還能抽象到11維呢!

事實上, 上述資料結構的共性–每個元素至少與其他元素建立了聯絡. 這種規則是人為規定的, 沒人要求我們一定這樣抽象資料, 所以, 我們可以假設所有的盒子之間兩兩都沒有關係, 所有盒子都是獨立的, 就像一堆硬幣, 我們把這種資料結構成為集合:

file-listfile-list

至此, 我們得到了四種結構

線性表: 資料結構中的元素存在一對一的相互關係
樹: 資料結構中的元素存在一對多的相互關係
圖: 資料結構中的元素存在多對多的相互關係
雜湊(集合): 資料結構中的元素之間除了“同屬一個集合” 的相互關係外,別無其他關係

  • 注意: 資料結構只有這四種嗎?
    現實世界中的結構千變萬化,沒有通用結構可以解決所有問題。甚至一個小問題,某個適應此類問題很優秀的結構卻未必就適合它。
    我們提煉的4種結構, 相對來講是最具普遍性的, 能適應現實世界的大多數場景.
    你可以提煉你自己的資料結構.

二. 資料結構的邏輯結構和物理結構

書承上文
資料結構指同一資料元素類中各資料元素之間存在的關係。資料結構分別為邏輯結構儲存結構(物理結構)和資料的運算. 資料的邏輯結構是從具體問題抽象出來的數學模型,是描述資料元素及其關係的數學特性的,有時就把邏輯結構簡稱為資料結構.

邏輯結構: 我們上面贅述了那麼多, 都是描述的邏輯結構(不涉及實現問題, 只是描述它們的關係).
儲存結構(物理結構): 真正有關資料在計算機中是如何儲存方式.

這個’儲存’是指在記憶體中的存在形式(並非外設, 如硬碟/U盤/光碟).

假設我們有10個盒子, 它們是線性結構, 這10個盒子應該如何擺放呢?

既然是線性結構, 難道不是依次擺放嗎? 一個挨著一個的樣子.

您要是這麼想得話, 說明您沒有明白物理結構和邏輯結構的區別.
假如我們把盒子分別放到不同的地方, 然後在每個盒子上寫明: 它上一個盒子是放在哪裡, 下一個盒子放在哪裡.
那麼您是不是可以根據盒子上的位置找到下一個盒子呢? 當然可以了.

這就是邏輯結構(線性關係)和物理結構(擺放位置)的區別.

事實上, 我們在記憶體中存放資料的格式確實有兩種: 順序儲存鏈式儲存

順序儲存: 顧名思義, 資料放在一塊兒連續的記憶體空間, 工作指標的移動可以很方便的找到其他元素(我說的是其他元素, 而不是下一個元素, 因為這樣的儲存方式也不一定完全為線性表儲存)
鏈式儲存: 所有元素均任意存放在記憶體中, 元素之間是通過元素內的指標互相尋找對方的.

三.佇列

佇列是一種特殊的線性表,特殊之處在於它只允許在表的 前端(head) 進行刪除操作,而在表的 後端(tail) 進行插入操作,佇列是一種操作受限制的線性表。進行插入操作的端稱為 隊尾 ,進行刪除操作的端稱為 隊頭

佇列特點是先進先出,後進後出.

file-listfile-list

佇列的定義, 除了點名了它元素之間的邏輯關係, 也定義了它的部分演算法.

演算法與資料結構密不可分, 它依附於資料結構而存在, 對於任何演算法的編寫,必須依賴一個已經存在的資料結構來對它進行操作,資料結構成為演算法的操作物件,這也是為什麼演算法和資料結構兩門分類不分家的概念,演算法在沒有資料結構的情況下,沒有任何存在的意義. 而資料結構沒有演算法就等於是一個屍體而沒有靈魂

對於一個線性表來說, 必須實現下面5種基本方法才能被稱為是佇列:

  • 初始化佇列:Init_Queue(q) ,初始條件:隊q 不存在。操作結果:構造了一個空隊
  • 入隊操作: In_Queue(q,x),初始條件: 隊q 存在。操作結果:對已存在的佇列q,插入一個元素x 到隊尾,隊發生變化
  • 出隊操作: Out_Queue(q,x),初始條件: 隊q 存在且非空,操作結果: 刪除隊首元素,並返回其值,隊發生變化
  • 讀隊頭元素:Front_Queue(q,x),初始條件: 隊q 存在且非空,操作結果: 讀隊頭元素,並返回其值,隊不變
  • 讀隊頭元素:Front_Queue(q,x),初始條件: 隊q 存在且非空,操作結果: 讀隊頭元素,並返回其值,隊不變

以上只是要求實現的基本方法, 你可以為佇列新增其他的方法,我們規定上面的方法是普世大多數應用場景, 如果你的需求特殊, 完全可以進行擴充套件.

  • 佇列的實現方式

    佇列是一種受限制的線性表, 所以它的實現和線性表的實現一樣, 可以分為 順序儲存鏈式儲存

    佇列的順序儲存分為兩種: 順序佇列和迴圈佇列, 具體我們在下面詳解.
    佇列的鏈式儲存是一種連結串列實現.

  • 順序佇列

    建立順序佇列結構必須為其靜態分配或動態申請 一片連續的儲存空間 ,並設定兩個指標(下標)進行管理。一個是隊頭指標head,它指向隊頭元素;另一個是隊尾指標tail,它指向下一個入隊元素的儲存位置.

file-listfile-list

  • 每次在隊尾插入一個元素是,tail增1
  • 每次隊頭刪除一個元素時,head增1。
  • 隨著插入和刪除操作的進行,佇列元素的個數不斷變化,佇列所佔的儲存空間也在為佇列結構所分配的連續空間中移動。
  • 當head==tail時,佇列中沒有任何元素,稱為 空隊
  • 當tail增加到指向分配的連續空間之外時,佇列無法再插入新元素.成為 滿隊
  • 這時往往還有大量可用空間未被佔用,這些空間是已經出隊的佇列元素曾經佔用過得儲存單元。這種現象叫做 “溢位”

事實上, 順序佇列有三種溢位:

  • “下溢”現象:當佇列為空時,做出隊運算產生的溢位現象。“下溢”是正常現象,常用作程式控制轉移的條件.
  • “真上溢”現象:當佇列滿時,做進棧運算產生空間溢位的現象。“真上溢”是一種出錯狀態,應設法避免.
  • “假上溢”現象:由於入隊和出隊操作中,頭尾指標只增加不減小,致使被刪元素的空間永遠無法重新利用。當佇列中實際的元素個數遠遠小於向量空間的規模時,也可能由於尾指標已超越向量空間的上界而不能做入隊操作。該現象稱為”假上溢”現象.

“下溢”和”真上溢”是我們無法避免的, 他們本身也不屬於異常現象. 但是”假上溢”會使我們程式發生異常,我們要避免它.
如何利用使用過的空間呢?

  • 迴圈佇列

為充分利用空間,克服”假溢位”現象的方法是:將空間想象為一個首尾相接的圓環,並稱這種空間為迴圈佇列。迴圈佇列可以有效的使用空間.

file-listfile-list

file-listfile-list

從圖中我們可以看到

  • 當有元素入隊時, tail(此時head和tail不是指標, 而是下標), 向後移動一位. tail 永遠在隊尾元素的下一個位置.
  • 如果tail的值超過我們的空間總大小, 則對tail對陣列長度取模. 使得tail永遠不會越界.
  • 出隊時, 返回head下標所在的值, 之後head向後移動一位, 與tail一樣, 一旦超出陣列長度, 則對其取模.
  • 開始時, head和tail指向同一位置, 此時佇列為空.
  • 當滿隊時, tail移動到head位置, 此時 滿隊.

如果按照上述邏輯, 在判斷滿隊時, 因為tail和head均指向同一位置, 所以我們不能作出區分. 在這裡, 判斷滿隊的邏輯發生了問題.

  • 判斷滿隊我們有兩種方案:

1.通過一個計數器,記錄元素個數, 當元素個數與陣列最大值相等,則為滿列;
2.少用一個儲存空間,也就是陣列的最後一個存數空間不用,當(tail+1)%maxsiz = head時,佇列滿;

我們會在後面的示例中演示迴圈佇列的Demo, 這個Demo採用的是第一種方案.

  • 佇列的連結串列

在佇列的形成過程中,可以利用線性連結串列的原理,來生成一個佇列.
新元素(等待進入佇列的元素)總是被插入到連結串列的尾部,而讀取的時候總是從連結串列的頭部開始讀取。每次讀取一個元素,釋放一個元素。所謂的動態建立,動態釋放。因而也不存在溢位等問題。由於連結串列由結構體間接而成,遍歷也方便。

佇列的連結串列實現原理圖:

file-listfile-list

插入資料時:

file-listfile-list

因為連結串列的儲存方式資料動態申請空間, 插入元素的時候才會申請空間, 所以不存在滿隊的情況

  • 你可以人為的為連結串列佇列設定一個上限. 一旦達到這個上限, 則滿隊.

後面的demo回詳細的為您解釋基本演算法的實現.

四.OC與C++的混編

蘋果的Objective-C編譯器允許使用者在同一個原始檔裡自由地混合使用C++和Objective-C,混編後的語言叫Objective-C++。有了它,你就可以在Objective-C應用程式中使用已有的C++類庫。

五.泛型程式設計

您肯定遇到過如下情況, 編寫好的函式因為型別原因, 往往要重寫好多次, 造成程式碼重複, 如下面的例子:

// 一個返回兩個整型數的和 addInt函式

1
2
3
4
int addInt(int a, int b)
{
    return a+b;
}

// 一個返回兩個浮點數的和 addFloat函式

1
2
3
4
float addFloat(float a, float b)
{
    return a+b;
}

上面的程式碼, 除了返回值和引數的型別不同, 邏輯完全一樣, 對於這種情況, C++率先提出了泛型程式設計的概念.

泛型即是指具有在多種資料型別上皆可操作的含意,與模板有些相似。

泛型程式設計讓你編寫完全一般化並可重複使用的演算法,其效率與針對某特定資料型別而設計的演算法相同。
關於泛型的理解, 您可以理解為”將數型別做為引數傳遞進來”
泛型程式設計(Generic Programming)最初提出時的動機很簡單直接:發明一種語言機制,能夠幫助實現一個通用的標準容器庫。

我們看一個例子, 下面的程式碼使用C++語法:

1
2
3
4
template <typename T> struct Node{
    T data;      //資料域
    Node* next; //指標域
};

上述程式碼中, 我們定義了一個結構體, 結構體中有兩個成員變數 data 和 next

next:指向自身結構體型別的指標.
data:的型別是T, 這個T是什麼呢?

template < typename T >: 這條語句表示T的型別定義還沒有給出,需要在使用Node型別時, 通過”<>” 符號來指明 typename T 的型別.

1
Node<int> node;
  • 注意, 上述程式碼可以寫成
    1
    2
    3
    4
    5
    
    template <typename T> 
    struct Node{
        T data;      //資料域
        Node* next; //指標域
    };
    

template 是來修飾 struct Node 的, 您不能在他們中間插入任何程式碼, 比如:

  • 錯誤的示例:
    1
    2
    3
    4
    5
    6
    
    template <typename T> 
    int a = 0;  // 這裡是錯誤的
    struct Node{
        T data;      //資料域
        Node * next; //指標域
    };
    

前面說到, 資料結構是不分語言和型別的, 我們能不能寫一個佇列, 適配所有的資料型別呢? 向佇列插入int, float, double, char都可以處理, 甚至物件也能放進去.
我們引入泛型的概念就是為此目的.

STL模板
  • 我們能想到這種思路, 前輩們早就想到了, 事實上, STL即為用泛型程式設計思想實現的一系列資料結構和演算法的封裝, 我們下面要寫的例子, 實際上在C++的模板庫中早已為我們寫好. STL模板庫有17個頭檔案,6個大部分.
  • STL可分為容器(containers)、迭代器(iterators)、空間配置器(allocator)、配接器(adapters)、演算法(algorithms)、仿函式(functors)六個部分。

六. 泛型程式設計實現迴圈佇列和連結串列佇列

  • 1.新建工程
    file-listfile-list
    file-listfile-list

  • 2.新建C++檔案, 我們沒有建立標頭檔案
    file-listfile-list
    file-listfile-list

  • 3.建立完成後, 手動修改檔案型別為.mm型別.
    file-listfile-list

  • 4.迴圈佇列的實現
    我們在LinkListByArray.mm中寫下面程式碼, 注意, 這個是C++語法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    
    //
    //  LinkListByArray.cpp
    //  QueueByLinkList
    //
    //  Created by LO3G on 15/11/29.
    //  Copyright © 2015年 LO3G. All rights reserved.
    //
    
    #include <iostream>
    
    using namespace std;
    
    template <typename T>
    class LinkListByArray {
    private:
    
        // 指向佇列首地址, 該地址永遠指向連續記憶體空間的首地址而不會發生變化
        T * arrayPtr; 
    
        // 隊頭下標, 注意型別是個整型
        int head;
    
        // 隊尾下標
        int tail;     
        int maxCnt;
    
    public:
        // 隊列當前元素個數, 將這個屬性設為共有, 是為了外界能直接訪問佇列個數.
        // c++沒有oc中"屬性"概念, 所有成員變數的語義需要用方法來控制.
        int count;
    
        // 初始化
        LinkListByArray(int cnt);
    
        // 入隊
        void pushItem(T item);
    
        // 出隊
        T popItem();
    
        // 空隊
        bool isEmpty();
        // 滿隊
        bool isFull();
    
        // peek隊頭元素
        T peekHead();
    
        // 列印佇列
        void display();
    };
    
    // 初始化
    template <typename T>
    LinkListByArray<T>::LinkListByArray(int cnt)
    {
        // 引數cnt表示想要建立佇列的最大長度. 
        maxCnt = cnt; 
    
        // 按照型別T*個數的方式, 在堆區內為佇列開闢空間.
        arrayPtr = (T *)malloc(cnt*sizeof(T)); 
    
        // 頭和尾下標均指向陣列的第一個位置.
        head = 0; 
        tail = 0;
    
        // 佇列內元素個數在初始化時為0.
        count = 0; 
    }
    
    // 空隊
    template <typename T>
    bool LinkListByArray<T>::isEmpty()
    {
        // 這裡使用一個計數器記錄佇列元素個數
        if (count == 0) {  
            cout << "空隊" << endl;
            return true;
        }else
        {
            return false;
        }
    }
    // 滿隊
    template <typename T>
    bool LinkListByArray<T>::isFull()
    {
        if (count == maxCnt) {
            cout << "滿隊" << endl;
            return true;
        }else
        {
            return false;
        }
    }
    
    // 入隊
    template <typename T>
    void LinkListByArray<T>::pushItem(T item)
    {
        // 如果滿隊的情況, 不能入隊了, 直接返回.
        if (isFull()) { 
            return;
        }
        // 入隊操作只在佇列尾部
        arrayPtr[tail] = item;  
    
        // 注意, 這裡為tail增加時, 要對其取模, 保證它永遠不會大過陣列總長度.
        tail = (tail + 1) % maxCnt; 
    
        // 入隊成功, 將元素個數記錄+1.
        count++; 
    }
    
    // 出隊
    template <typename T>
    T LinkListByArray<T>::popItem()
    {
        // 如果空隊, 則不能返回任何元素, 返回NULL作為標記, 外部需判斷
        if (isEmpty()) {  
            return NULL;
        }
    
        // 出隊時, 宣告一個臨時變數做一次值拷貝
        T tmp = arrayPtr[head];
    
        // 之後將head向後移動一位, 注意, 這裡也要對head取模, 保證其值不會大於陣列總長度.
        head = (head + 1) % maxCnt;  
        count--;
    
        return tmp;
    }
    
    // 列印佇列
    template <typename T>
    void LinkListByArray<T>::display()
    {
        for (int i = 0; i <= count - 1; i++) {
            cout << arrayPtr[(head + i) % maxCnt] << ' ';
        }
        cout << endl;
    }
    

main.mm中呼叫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//
//  main.mm
//  QueueByLinkList
//
//  Created by LO3G on 15/11/27.
//  Copyright © 2015年 LO3G. All rights reserved.
//
#import <iostream>
#import "LinkListByArray.mm"

using namespace std;

int main(int argc, const char * argv[]) {

    // 初始化一個4個元素個數的佇列
    // 注意<char>指明佇列內元素型別為char型, 這裡你可以寫任何型別, 但是需入隊時型別一致.
    LinkListByArray<char> queueArray(4);  

    queueArray.pushItem('a');  // 入隊一個元素'a'
    queueArray.pushItem('b');
    queueArray.pushItem('c');
    queueArray.pushItem('d');

    // 出隊, 我們沒有用變數接受, 出隊的元素被我們弄丟了.
    queueArray.popItem();     

    queueArray.pushItem('a');
    queueArray.pushItem('b');
    queueArray.pushItem('c');
    queueArray.pushItem('d');

    queueArray.popItem();
    queueArray.pushItem('a');
    que