數(shù)據(jù)結(jié)構(gòu)與算法-C語言5-線性表之順序存儲結(jié)構(gòu)

數(shù)據(jù)結(jié)構(gòu)與算法-目錄

1、線性表的定義:有零個或多個數(shù)據(jù)元素組成的有序數(shù)列。

線性表是一種常用的數(shù)據(jù)結(jié)構(gòu)。在實際應用中,線性表都是以棧、隊列、字符串、數(shù)組等特殊線性表的形式來使用的。由于這些特殊線性表都具有各自的特性,因此,掌握這些特殊線性表的特性,對于數(shù)據(jù)運算的可靠性和提高操作效率都是至關重要的。

線性表是一個線性結(jié)構(gòu),它是一個含有n≥0個結(jié)點的有限序列,對于其中的結(jié)點,有且僅有一個開始結(jié)點沒有前驅(qū)但有一個后繼結(jié)點,有且僅有一個終端結(jié)點沒有后繼但有一個前驅(qū)結(jié)點,其它的結(jié)點都有且僅有一個前驅(qū)和一個后繼結(jié)點。

特征:
  • 集合中必存在唯一的一個“第一元素”;
  • 集合中必存在唯一的一個 “最后元素” ;
  • 除了第一個元素之外,均有唯一的前驅(qū)(前件)。
  • 除了最后一個元素之外,均有唯一的后繼(后件);
注意:

線性表強調(diào)是有限的,事實上無論計算機發(fā)展到多強大,它處理的元素個數(shù)是有限的。

用數(shù)學語言來定義:

若將線性表記為(a1,…,ai-1,ai,ai+1,…,an),則表中ai-1領先于ai,ai領先于ai+1,稱ai-1ai的直接前驅(qū)元素,ai+1ai的直接后繼元素。當i = 1,2,…,n-1時,ai有且僅有一個直接后繼,當i = 2 , 3 , … , n時,ai有且僅有一個直接前驅(qū)。

所以線性表元素的個數(shù)n(n ≥ 0)定義為線性表長度,當n=0時,稱為空表。

在非空表中的每個數(shù)據(jù)元素都有一個確定的位置,如a1是第一個數(shù)據(jù)元素,an是最后一個數(shù)據(jù)元素,ai是第i個數(shù)據(jù)元素,稱i為數(shù)據(jù)元素ai在線性表中的位序。

2、線性表的抽象數(shù)據(jù)類型

數(shù)據(jù)類型 : 是指一組性質(zhì)相同的值的集合及定義在此集合上的一些操作的總稱。
例如很多編程語言的整型、浮點型、字符型這些指的就是數(shù)據(jù)類型。

抽象:是指抽取出事物具有的普遍性的本質(zhì)。它要求抽出問題的特征而忽略非本質(zhì)的細節(jié),是對具體事物的一個概括。抽象是一種思考問題的方式,它隱藏了繁雜的細節(jié)。

我們對已有的數(shù)據(jù)類型進行抽象,就有了抽象數(shù)據(jù)類型。
抽象數(shù)據(jù)類型(Abstract Data Type,ADT) 是指一個數(shù)據(jù)模型及定義在該模型上的一組操作。
抽象數(shù)據(jù)類型的定義僅取決于它的一組邏輯性,而與其在計算機內(nèi)部如何表示和實現(xiàn)無關。

線性表的抽象數(shù)據(jù)類型定義如下:
ADT 抽象數(shù)據(jù)類型名
Data 數(shù)據(jù)元素之間邏輯關系的名稱
Operation 操作
endADT

ADT 線性表(List)

Data
線性表的數(shù)據(jù)對象集合為{a1,a2,....,an},每個元素的類型均為DataType。其中除了,第一個元素a1外,每一個元素有且只有一個直接前驅(qū)元素,除最后一個元素an外,每一個元素有且只有一個直接后繼元素。數(shù)據(jù)元素之間的關系是一對一的關系。


Operation
InitList(*L):初始化操作,建立一個空的線性表。
ListEmpty(L):若線性表為空,返回true,否則返回false。

ClearList(\*L):線性表清空。
GetElem(L,i,\*e):將線性表L中第i個位置元素返回給e。
LocateElem(L,e):在線性表L中查找與給定值e相等的元素,如果
    查找成功,返回該元素在表中的序列號;否則,返回0表示失敗。
ListInsert(\*L,i,e):在線性表的第i個位置插入元素e。
ListDelete(\*L,i,\*e):刪除線性表L中的第i個元素,并用e返回其值
ListLength(L):返回線性表L的元素個數(shù)。<br>

endADT

對于不同的應用,線性表的操作時不同的,上述操作時最基本的,問題中設計的關于線性表的更復雜操作,完全可以用這些基本操作的組合來實現(xiàn)。

比如,要實現(xiàn)兩個線性表集合A和B的并集操作。即要使得集合A = A ∪ B,說白了,就是把存在集合B中但并不存在中的數(shù)據(jù)元素插到A中即可。

假設我們La表示集合A,Lb表示集合B

示例如下:
//將所有的在線性表Lb中但不在La中的元素插入到La中
void unionL(List *La , List Lb)
{
    int La_len,Lb_len,i;
    ElemType e;
    La_len = ListLength(*La);
    Lb_len = ListLength(*Lb);
    for(i = 0 ;i ≤ Lb;i++)
    {
        GetElem(Lb,i,*e);//取出Lb中第i個數(shù)據(jù)元素賦給e
        if(!LocateElem(*La,e))//La中不存在和e元素相同的數(shù)據(jù)元素
        {
            ListInsert(La,++La_len,e);//插入
        }
    }
}

3、線性表的順序存儲結(jié)構(gòu)

線性表有兩種物理存儲結(jié)構(gòu):順序存儲結(jié)構(gòu)和鏈式存儲結(jié)構(gòu)。

3.1、順序存儲結(jié)構(gòu)定義

線性表的順序存儲結(jié)構(gòu),指定的是用一段地址連續(xù)的存儲單元一次存儲線性表的數(shù)據(jù)元素。

3.2、順序存儲結(jié)構(gòu)方式

線性表的順序存儲方式,說白了,就是在內(nèi)存中找了一塊地方,把一定內(nèi)存空間占了,然后把相同數(shù)據(jù)類型的數(shù)據(jù)元素一次存在在里面。既然線性表的數(shù)據(jù)元素的類型都相同,所以用C語言的一維數(shù)組來實現(xiàn)順序存儲結(jié)構(gòu),即把第一個數(shù)據(jù)元素存儲到數(shù)組下表為0的位置中,接著把線性表相鄰的元素存儲在數(shù)組中相鄰的位置。

為了建立一個線性表,要在內(nèi)存中找一塊地,于是這塊地的第一個位置就非常關鍵,它是存儲空間的起始位置。

線性表中,我們估算這個線性表的最大存儲容量,建立一個數(shù)組,數(shù)組的長度就是最大存儲容量。

我們已經(jīng)有了起始位置,也有了最大的容量,于是我們可以在里面增加數(shù)據(jù)了。隨著數(shù)據(jù)的插入,我們線性表的長度開始變大,不過線性表的當前長度不能超過存儲容量,即數(shù)組的長度。

接下來看線性表順序存儲的結(jié)構(gòu)代碼:

#define MAXSIZE 20
typedef int ElemType;
typedef struct
{
  ElemType data[MAXSIZE];
  int length;   //線性表當前長度
}SqList;

順序存儲結(jié)構(gòu)封裝需要三個屬性:

  • 存儲空間的起始位置,數(shù)組data,它的存儲位置就是線性表存儲空間的存儲位置。
  • 線性表的中最大存儲容量,數(shù)組的長度MaxSize。
  • 線性表的當前長度:length。
    注意,數(shù)組的長度與線性表的當前長度需要區(qū)分一下:數(shù)組的長度是存放線性表的存儲區(qū)間的總長度,一般初始化后不變。而線性表的當前長度是線性表中元素的個數(shù),是會變化的。
3.3、數(shù)組長度與線性表長度區(qū)別

數(shù)組長度是存放線性表的存儲空間的長度,存儲空間分配完一般是不變的。

線性表長度是線性表中元素數(shù)據(jù)的個數(shù),隨著線性表插入和刪除操作的進行,這個量是變化的。

在任意時刻,線性表的長度應該小于等于數(shù)組的長度。

3.4、地址計算方法

線性表的起始是從1開始的,可數(shù)組卻是從0開始開始第一個下標的,于是線性表中第i個元素,存儲在數(shù)組小標為i-1的位置。
用數(shù)組存儲順序表意味著要分配固定長度的數(shù)組空間,由于線性表中可以進行插入和刪除操作,因此分配的數(shù)組空間要大于等于當前線性表的長度。
由于每個數(shù)組元素,不管它是整形、實型還是字符型,它都是需要占用一定的存儲空間。
假設占用的是c個存儲單元,那么線性表中第i+1個元素的存儲位置和第i個元素的存儲位置的關系是(LOC表示獲得存儲位置的函數(shù)):

LOC(ai+1) = LOC(ai) + c;

所以對于第i個數(shù)據(jù)元素ai的存儲位置可以由a1推算出:

LOC(ai) = LOC(ai) + (i-1)*c

通過這個公式,隨時可以算出線性表中任意位置的地址,不管他是第一個還是最后一個,都是相同的事件。那么我們對每個線性表位置的存入或者取出數(shù)據(jù),對于計算機來說都是相等的時間,也就是一個常數(shù),因此我們算法中學到的時間復雜度的概念來說,它的存取時間的性能為O(1)。我們通常把具有這一特點的存儲結(jié)構(gòu)稱為隨機存取結(jié)構(gòu)。

4、順序存儲結(jié)構(gòu)的插入與刪除

4.1、獲得元素操作

對于線性的順序存儲結(jié)構(gòu)來說,我們要實現(xiàn)GetItem操作,即將線性表L中的第i個位置元素返回,其實是非常簡單的。就程序而言,只要第i個元素在下標的范圍內(nèi),就是把數(shù)組第i-1個下標值返回即可。

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status
//Status是函數(shù)的類型,其值是函數(shù)結(jié)果狀態(tài)代碼,如OK等
//初始條件:順序線性表L已經(jīng)存在,1 <=i <= ListLength(L)
//操作結(jié)果:用e反回L中第i個元素的值。
Status GeElem(SqList L,int i,ElemType *e){
if(L.length==0 || i < 1 || i >L.length){
  return ERROR;
}
*e = L.data[i-1];
return OK;
}

注意這里返回值類型Status是一個整型,返回OK代表1,ERROR代表0。

4.2、插入操作

線性表的順序存儲結(jié)構(gòu)具有隨機存儲結(jié)構(gòu)的特點,時間復雜度為O(1);
如果我們要實現(xiàn)ListInsert(*L,i,e),即在線性表L中的第i個位置插入新元素e,應該如何操作?
所以插入算法操作:

  1. 如果插入位置不合理,拋出異常;
  2. 如果線性表長度大于等于數(shù)組長度,則拋出異常或動態(tài)增加數(shù)組容量;
  3. 從最后一個元素開始向前遍歷到第i個位置,分別將它們都向后移動一個位置。
  4. 將要插入元素填入位置i處
  5. 線性表長+1
    實現(xiàn)代碼如下:
//初始條件:順序線性表L已存在,1 ≤ i ≤ ListLength(L)
//操作結(jié)果:在L的第i個位置插入新的數(shù)據(jù)元素e,L的長度加1
Status ListInsert(SqList *L,int i,ElemType e)
{
    int k;
    if(L->length == MAXSIZE)//當線性表已滿
        return ERROR;
    if(i < 1 || i >L->length + 1)//當i不在范圍內(nèi)時
    {
        return ERROR;
    }
    if(i <= L->length)//若插入數(shù)據(jù)位置不在表尾
    {
        for(k = L->length-1;k > i-1;k--)
        {
            L->data[k + 1] = L->data[k];
        }
    }
    L->data[i - 1] = e;//將新元素插入
    L->length++;
    return OK;
}
4.3、刪除操作

刪除算法的思路:

  1. 如果刪除位置不合理,拋出異常
  2. 取出刪除元素
  3. 從刪除元素位置開始遍歷到最后一個元素位置,分別將它們向前移動一個位置
  4. 表長減一

實現(xiàn)代碼如下:

//初始條件:順序線性表L已經(jīng)存在,1 <= i <= ListLength(L)
//操作結(jié)果:刪除L的第i個元素,并用e返回其值,L的長度減1
Status ListDelete(SqList *L ,int i , ElemType *e)
{
    int k;
    if(L->length == 0)//線性表為空
        return ERROR;
    if(i < 1 || i > L->length)//刪除位置不正確
        return ERROR;
    *e = L->data[i-1];
    if(i < L->length)
    {
        for(k = i;k < L->length;k++)
            L->data[k - 1] = L->data[k];
    }
    L->length--;
    return OK;
}

現(xiàn)在,我們來分析一下,插入和刪除的事件復雜度。
現(xiàn)在我們來看最好的情況,如果一個元素要插入到最后一個位置,或者刪除最后一個位置,此時時間復雜度為O(1),因為不需要移動元素的。

最壞的情況呢,如果元素要插入到第一個位置或者刪除第一個元素,此時時間復雜度是多少呢?那就意味著所有元素向后或者向前,所以這個時間復雜度為O(n)。

至于平均的情況,由于元素插入到第i個位置,或者刪除第i個元素,需要移動n - i個元素,每個位置插入或刪除元素的可能性是相同的,也就是位置靠前,移動元素多,位置靠后,移動元素少。最終平均移動次數(shù)和最中間那個元素的移動次數(shù)相等,為(n - 1)/ 2。

根據(jù)時間復雜度的推導,平均時間復雜度還是O(n)。

這說明:

線性表的順序存儲結(jié)構(gòu),在存、讀數(shù)據(jù)時,不管是哪個位置,時間復雜度都是O(1);而插入或刪除時,時間復雜度都是O(n)。這就說明,它比較適合元素個數(shù)不太變化,而更多是存取數(shù)據(jù)的應用。

4.4、線性表順序存儲結(jié)構(gòu)的優(yōu)缺點
優(yōu)點:
  • 無須為表中元素之間的邏輯關系而增加額外的存儲空間

  • 可以快速地存取表中任一位置的元素

缺點:
  • 插入和刪除需要移動大量元素

  • 當線性表長度變化較大時,難以確定存儲空間的容量

  • 造成存儲空間的“碎片”

特別感謝:
魚C工作室小甲魚

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容