線性表——順序表的實現與講解(C++描述)

線性表

引言

新生安排體檢,爲了 便管理與統一數據,學校特意規定了排隊的方式,即按照學號排隊,誰在前誰在後,這都是規定好的,因此誰在誰不在,都是很是方便統計的,同窗們就像被一條線(學號)聯繫起來了,這種組織數據(同窗)的方式咱們能夠稱做線性表結構ios

定義

線性表:具備零個或多個(具備相同性質,屬於同一元素的)數據元素的有限序列算法

若將線性表記爲 ( a0 , a1 ,ai -1 ai ,ai +1 , ... , an - 1 , an )編程

  • 注意:i 是任意數字,只爲了說明相對位置,下標即其在線性表中的位置)數組

  • 前繼和後繼:因爲先後元素之間存在的是順序關係,因此除了首尾元素外,每一個元素均含有前驅後繼,簡單的理解就是前一個 元素和後一個元素微信

  • 空表:若是線性表中元素的個數 n 爲線性表長度,那麼 n = 0 的時候,線性表爲空curl

  • 首節點、尾節點: 上面表示中的 :a0 稱做首節點,an 稱做尾節點編程語言

抽象數據類型

  • 數據類型:一組性質相同的值的集合及定義在此集合上的一些操做的總稱函數

  • 抽象數據類型:是指一個數學模型及定義在該模型上的一組操做學習

關於數據類型咱們能夠舉這樣一個例子this

  • 例如:咱們經常用到的 整數型 浮點型 數據 這些都是數據的總稱,全部符合其性質特徵的均可以用其對應數據類型來定義,例如 520是一個知足整數特徵的數據,因此能夠賦值給 一個int型的變量 int love = 520;

像這些通常的數據類型一般在編程語言的內部定義封裝,直接提供給用戶,供其調用進行運算,而抽象數據類型通常由用戶本身根據已有的數據類型進行定義

抽象數據類型和高級編程語言中的數據類型其實是一個概念,但其含義要比普通的數據類型更加普遍、抽象

爲何說抽象呢?是由於它是咱們用戶爲了解決實際的問題,與描述顯示生活且現實生活中的實體所對應的一種數據類型,我能夠定義其存儲的結構,也能夠定義它所可以,或者說須要進行的一些操做,例如在員工表中,添加或刪除員工信息,這兩部分就組成了 「員工」 這個抽象的數據類型

大體流程就是:

  • A:通常用戶會編寫一個自定義數據類型做爲基礎類型

  • B:其中一些抽象操做就能夠定義爲該類型的成員函數,而後實現這些函數
  • C:若是對外的接口在公有域中,就能夠經過對象來調用這些操做了
  • 固然,咱們在使用抽象數據類型的時候,咱們更加註意數據自己的API描述,而不會關心數據的表示,這些都是實現該抽象數據類型的開發者應該考慮的事情

線性表分爲兩種——順序存儲結構和鏈式存儲結構,咱們先來學習第一種

順序存儲結構

什麼是順序存儲結構呢?

順序存儲結構:用一段地址連續的存儲單元依次存儲線性表的數據元素

怎麼理解這這種存儲方式呢?

例如在一個菜園子中,有一片空地,咱們在其中找一小塊種蔬菜,由於土地不夠平整疏鬆因此咱們須要耕地,同時將種子按照必定的順序種下去,這就是對錶的初始化

菜園子能夠理解爲內存空間,空地能夠理解爲可使用的內存空間,咱們經過種蔬菜種子的方式,將必定的內存空間所佔據,固然,這片空間中你所放置的數據元素都必須是相同類型的 也就是說都得是蔬菜種子,有時候有些種子被蟲子咬壞了,咱們就須要移除一些種子,買來之後再在空出來的位置中選地方種好,這也就是增長和刪除數元素

地址計算方式

從定義中咱們能夠知道 這種存儲方式,存儲的數據是連續的,並且相同類型,因此每個數據元素佔據的存儲空間是一致的,假設每一個數據 佔據 L個存儲單元那麼咱們能夠的出這樣的結論公式

$$Loc(a_i) = Loc(a_1) + (i -1)*L$$

  • i 表明所求元素的下標
  • 也就是單位長度乘以對應的個數

線性表的抽象數據類型

#ifndef _LIST_H_
#define _LIST_H_
#include<iostream>
using namespace std;

class outOfRange{};
class badSize{};
template<class T>
class List {
public:
    // 清空線性表
    virtual void clear()=0;
    // 判空,表空返回true,非空返回false
    virtual bool empty()const=0;
    // 求線性表的長度
    virtual int size()const=0;
    // 在線性表中,位序爲i[0..n]的位置插入元素value
    virtual void insert(int i,const T &value)=0;
    // 在線性表中,位序爲i[0..n-1]的位置刪除元素
    virtual void remove(int i)=0;
    // 在線性表中,查找值爲value的元素第一次出現的位序
    virtual int search(const T&value)const=0;
    // 在線性表中,查找位序爲i的元素並返回其值
    virtual T visit(int i)const=0;
    // 遍歷線性表
    virtual void traverse()const=0;
    // 逆置線性表
    virtual void inverse()=0;                   
    virtual ~List(){};
};

/*自定義異常處理類*/ 


class outOfRange :public exception {  //用於檢查範圍的有效性
public:
    const char* what() const throw() {
        return "ERROR! OUT OF RANGE.\n";
    }
};

class badSize :public exception {   //用於檢查長度的有效性
public:
    const char* what() const throw() {
        return "ERROR! BAD SIZE.\n";
    }
};

#endif

在上面線性表的抽象數據類型中,定義了一些經常使用的方法,咱們能夠在其中根據須要,增刪函數

有了這樣的抽象數據類型List 咱們就能夠寫出線性表其下的順序結構和鏈式結構表的定義寫出來

異常語句說明:若是new在調用分配器分配存儲空間的時候出現了錯誤(錯誤信息被保存了一下),就會catch到一個bad_alloc類型的異常,其中的what函數,就是提取這個錯誤的基本信息的,就是一串文字,應該是const char*或者string

順序表——順序存儲結構的定義

#ifndef _SEQLIST_H_
#define _SEQLIST_H_
#include "List.h"
#include<iostream>
using namespace std;

//celemType爲順序表存儲的元素類型
template <class elemType>
class seqList: public List<elemType> { 
private:
    // 利用數組存儲數據元素
    elemType *data;
    // 當前順序表中存儲的元素個數
    int curLength;
    // 順序表的最大長度
    int maxSize;
    // 表滿時擴大表空間
    void resize();                          
public:
    // 構造函數
    seqList(int initSize = 10);             
    // 拷貝構造函數
    seqList(seqList & sl);
    // 析構函數
    ~seqList()  {delete [] data;}
    // 清空表,只需修改curLength
    void clear()  {curLength = 0;}
    // 判空
    bool empty()const{return curLength == 0;}
    // 返回順序表的當前存儲元素的個數
    int size() const  {return curLength;}
    // 在位置i上插入一個元素value,表的長度增1
    void insert(int i,const elemType &value);
    // 刪除位置i上的元素value,若刪除位置合法,表的長度減1 
    void remove(int i);
    // 查找值爲value的元素第一次出現的位序
    int search(const elemType &value) const ;
    // 訪問位序爲i的元素值,「位序」0表示第一個元素,相似於數組下標
    elemType visit(int i) const;            
    // 遍歷順序表
    void traverse() const;
    // 逆置順序表
    void inverse();                         
    bool Union(seqList<elemType> &B);
};

順序表基本運算的實現

(一) 構造函數

在構造函數中,咱們須要完成這個空順序表的初始化,即建立出一張空的順序表

template <class elemType>
seqList<elemType>::seqList(int initSize) { 

    if(initSize <= 0) throw badSize();
    maxSize = initSize;
    data = new elemType[maxSize];
    curLength = 0;
                        
}

在這裏咱們注意區分 initSize 和 curLenght 這兩個變量

  • initSize :初始化 (指定) 數組長度
    • 數組長度是存放線性表的存儲空間的長度,通常來講這個值是固定的,可是爲了知足須要不少狀況下,咱們會選擇動態的分配數組,即定義擴容機制,雖然很方便,可是確帶來了效率的損失,咱們在擴容的函數中會再提到這一問題
  • curLenght:線性表長度,即數據元素的個數

(二) 拷貝構造函數

template <class elemType>
seqList<elemType>::seqList(seqList & sl) { 

    maxSize = sl.maxSize;
    curLength = sl.curLength;
    data = new elemType[maxSize];
    for(int i = 0; i < curLength; ++i)
        data[i] = sl.data[i];
    
}

(三) 插入

咱們下面來談一個很是經常使用的操做——插入操做,接着用咱們一開始的例子,學校安排體檢,你們自覺的按照學號順訊排好了隊伍,可是遲到的某個學生Z和認識前面隊伍中的C同窗,過去想套近乎,插個隊,若是該同窗贊成了,這意味着原來C同窗前面的人變成了Z,B同窗後面的人也從C變成了Z同窗,同時從所插入位置後面的全部同窗都須要向後移動一個位置,後面的同窗莫名其妙的就退後了一個位置

咱們來想一下如何用代碼實現它呢,而且有些什麼須要特別考慮到的事情呢?

  • 一、插入元素位置的合法以及有效性
    • 插入的有效範圍:[0,curLength] 說明:curLength:當前有效位置
  • 二、檢查是否表滿,表滿不能繼續添加,不然發生溢出錯誤
    • A:不執行操做,報錯退出 (爲避免能夠將數組初始大小設置大一些)
    • B:動態擴容,擴大數組容量 (下例採用)
  • 三、首尾節點的特殊插入狀況考慮
  • 四、移動方向
    • 利用循環,從表尾開始逐次移動,若是從插入位置開始,會將後面的未移動元素覆蓋掉
template <class elemType>
void seqList<elemType>::insert(int i, const elemType &value) { 
    
    //合法的插入範圍爲【0..curlength】
    if (i < 0 || i > curLength) throw outOfRange(); 
    //表滿,擴大數組容量
    if (curLength == maxSize) resize();         
    for (int j = curLength; j > i; j--)
        //下標在【curlength-1..i】範圍內的元素日後移動一步
        data[j] = data[j - 1];
    //將值爲value的元素放入位序爲i的位置
    data[i] = value;
    //表長增長
    ++curLength;    

}

(四) 刪除

既然理解了插入操做,趁熱打鐵,先認識一下對應的刪除操做,這個操做是什麼流程呢?仍是上面的例子,插隊後的同窗被管理人員發現,不得不離開隊伍,這樣剛纔被迫集體後移的那些同窗就都又向前移動了一步,固然刪除位置的先後繼關係也發生了改變

與插入相同,它又有什麼注意之處呢?

  • 一、刪除元素位置的合法以及有效性
    • 刪除的有效範圍:[0,curLength - 1]
    • i < 0 || i > curLength- 1隱性的解決了判斷空表的問題
  • 二、移動方向
    • 利用循環,從刪除元素的位置後開始逐次前移
template <class elemType>
void seqList<elemType>::remove(int i) { 
    
    //合法的刪除範圍
    if(i < 0 || i > curLength- 1) throw outOfRange();  
    for(int j = i; j < curLength - 1; j++)
        data[j] = data[j+1];
    --curLength; 
}

(五) 擴容操做

還記得嗎,咱們在構造函數中,定義了數組的長度
seqList<elemType>::seqList(int initSize) { 代碼內容}

同時咱們將這個初始化的指定參數值作爲了 數組的長度

maxSize = initSize;

爲何咱們不直接指定構造函數中的參數爲 maxSize呢?

從變量名能夠看出這是爲了說明初始值和最大值不是同一個數據,也能夠說是爲了擴容作準備,

爲何要擴容呢?

數組中存放着線性表,可是若是線性表的長度(數據元素的個數)達到了數組長度會怎麼樣?很顯然咱們已經沒有多餘的空間進行例如插入這種操做,也稱做表滿了,因此咱們定義一個擴容的操做,當涉及到可能表滿的狀況,就執行擴容操做

擴容是否是最好的方式?

雖然數組看起來有一絲不太靈光,可是數組確實也是存儲對象或者數據的有效方式,咱們也推薦這種方式,可是因爲其長度固定,致使它在不少時候會受到一些限制,就例如咱們上面的表滿問題,那麼如何解決呢?方法之一就是咱們設置初始值比實際值多一些,可是因爲實際值每每會有一些波動,就會致使佔用過多的內存空間形成浪費,或者仍發生表滿問題,爲了解決實際問題,很顯然仍是擴容更加符合須要,可是代價就是必定的效率損失

數組就是一個簡單的線性序列,這使得元素訪問很是快速。可是爲這種速度所付出的代價是數組對象的大小被固定,而且在其生命週期中不可改變

咱們看一下擴容的基本原理你就知道緣由了!

擴容思想:

因爲數組空間在內存中是必須連續的,所以,擴大數組空間的操做須要從新申請一個規模更大的新數組,將原有數組的內容複製到新數組中,釋放原有數組空間,將新數組做爲線性表的存儲區

因此爲了實現空間的自動分配,儘管咱們仍是會首選動態擴容的方式,可是這種彈性顯然須要必定的開銷

template <class elemType>
void seqList<elemType>::resize() { 

    elemType *p = data;
    maxSize *= 2;
    data = new elemType[maxSize];
    for(int i = 0; i < curLength; ++i)
        data[i] = p[i];
    delete[] p; 
 
}

(六) 按值查找元素

順序查找值爲value的元素第一次出現的位置,只須要遍歷線性表中的每個元素數據,依次與指定value值比較

  • 相同:返回值的位序
    • 注意查詢的有效範圍
  • 找不到或錯誤:返回 -1

template<class elemType>
int seqList<elemType>::search(const elemType & value) const
{

    for(int i = 0; i < curLength; i++)
        if(value == data[i])return i;
    return - 1;

}

(七) 按位置(下標)查找元素

這個就真的很簡單了,直接返回結果便可

template<class elemType>
elemType seqList<elemType>::visit(int i) const {

    return data[i];                                                                   
    
}

(八) 遍歷元素

遍歷是什麼意思呢?遍歷其實就是每個元素都訪問一次,從頭至尾過一遍,因此咱們就能夠利用遍歷實現查詢,或者輸出等功能,若是表是空表,就輸出信息提示,而且注意遍歷的有效範圍是[0,最後一個元素 - 1]

template<class elemType>
void seqList<elemType>::traverse()const {

    if (empty())
        cout << "is empty" << endl; 
    else {
        cout << "output element:\n";
        //依次訪問順序表中的全部元素
        for (int i = 0; i < curLength; i++) 
            cout << data[i] << " ";
        cout << endl;
    }
                    
}

(九) 逆置運算

逆置運算顧名思義 ,就是將線性表中的數據顛倒一下,也就是說首元素和尾元素調換位置,而後就是第二個元素和倒數第二個元素調換,接着向中間以對爲單位繼續調換,也能夠稱做收尾對稱交換,須要注意的就是循環的次數僅僅是線性表長度的一半而已

template<class elemType>
void seqList<elemType>::inverse() {
    
    elemType tem;
    for(int i = 0; i < curLength/2; i++) {
        //調換的具體方式,能夠設置一箇中間值
        tem = data[i];
        //對稱的兩個數據
        data[i] = data[curLength - i -1];
        data[curLength - i -1] = tem;
    }
        
}

(十) 合併順序表

如今給出兩個線性表,表A和表B,其中的元素均爲正序存儲,如何能夠合併兩個表,放於A表中,可是表中的元素仍然保證正序存儲

算法思想:咱們分別設置三個指針,分別表明了A B C,C 表明新表,咱們分別讓三個指針指向三個表的末尾,將A表和B表的尾元素進行比較,而後將大的移入新A表中,而後將大的元素所在線性表的指針和新表的指針,前移一位 ,這樣A和B表繼續比較元素大小,重複操做,直到一方表空,將還有剩餘的那個表的剩餘元素移入新A表中

template<class elemType>
bool seqList<elemType>::Union(seqList<elemType> &B) {   

    int m, n, k, i, j;  
    //當前對象爲線性表A
    //m,n分別爲線性表A和B的長度
    m = this->curLength;                          
    n = B.curLength;
    //k爲結果線性表的工做指針(下標)新A表中
    k = n + m - 1;  
    //i,j分別爲線性表A和B的工做指針(下標)
    i = m - 1, j = n - 1;
    //判斷表A空間是否足夠大,不夠則擴容
    if (m + n > this->maxSize)                    
        resize();
    //合併順序表,直到一個表爲空
    while (i >= 0 && j >= 0)                      
        if (data[i] >= B.data[j])
            data[k--] = data[i--];
        //默認當前對象,this指針可省略
        else data[k--] = B.data[j--];             
    //將表B中的剩餘元素複製到表A中
    while (j >= 0)                                
        data[k--] = B.data[j--];
    //修改表A長度
    curLength = m + n;                            
    return true;
     
}

順序表的優缺點

優勢:

  1. 邏輯與物理順序一致,順序表可以按照下標直接快速的存取元素
  2. 無須爲了表示表中元素之間的邏輯關係而增長額外的存儲空間

缺點:

  1. 線性表長度須要初始定義,經常難以肯定存儲空間的容量,因此只能以下降效率的代價使用擴容機制

  2. 插入和刪除操做須要移動大量的元素,效率較低

時間複雜度證實

讀取:

還記的這個公式嗎?

$$Loc(a_i) = Loc(a_1) + (i -1)*L$$

經過這個公式咱們能夠在任什麼時候候計算出線性表中任意位置的地址,而且對於計算機所使用的時間都是相同的,即一個常數,這也就意味着,它的時間複雜度爲 O(1)

插入和刪除:

咱們以插入爲例子

  • 首先最好的狀況是這樣的,元素在末尾的位置插入,這樣不管該元素進行什麼操做,均不會對其餘元素產生什麼影響,因此它的時間複雜度爲 O(1)

  • 那麼最壞的狀況又是這樣的,元素正好插入到第一個位置上,這就意味着後面的全部元素所有須要移動一個位置,因此時間複雜度爲 O(n)

  • 平均的狀況呢,因爲在每個位置插入的機率都是相同的,而插入越靠前移動的元素越多,因此平均狀況就與中間那個值的必定次數相等,爲 (n - 1) / 2 ,平均時間複雜度仍是 O(n)

總結:

讀取數據的時候,它的時間複雜度爲 O(1),插入和刪除數據的時候,它的時間複雜度爲 O(n),因此線性表中的順序表更加適合處理一些元素個數比較穩定,查詢讀取多的問題

結尾:

若是文章中有什麼不足,或者錯誤的地方,歡迎你們留言分享想法,感謝朋友們的支持!

若是能幫到你的話,那就來關注我吧!若是您更喜歡微信文章的閱讀方式,能夠關注個人公衆號

在這裏的咱們素不相識,卻都在爲了本身的夢而努力 ❤

一個堅持推送原創開發技術文章的公衆號:理想二旬不止

相關文章
相關標籤/搜索