第二章 線性表

線性表的類型定義

線性表是n個數據元素的有限序列。在稍復雜的線性表中,一個數據元素可以由若干個數據項組成,此時把數據元素稱為記錄,含有大量記錄的線性表稱為文件。例如:

姓名 年齡 學號 成績
張一 19 001 100
王二 19 002 95
  • 每一行稱作數據元素
  • 姓名、年齡、學號和成績稱為數據項

由此可見,線性表中數據元素可以是各式各樣的,但同一線性表中的元素必定具有相同特性。相鄰數據元素之間存在著序偶關系。除首尾,每個元素都有唯一前驅和唯一后繼。

  • 抽象數據類型線性表的定義
ADT List{
    數據對象:D = {ai | ai ∈ ElemSet, i =1,2,  ... , n, n≥0}
    數據關系:R1 = {<ai-1,ai> | ai-1, ai∈D, i=2 ,3, ..., n}
    基本操作:
        InitList( &L )
            操作結果:構造一個空的線性表L。
        DestroyList(&L )
            初始條件:線性表L已存在
            操作結果:銷毀線性表L
        ClearList(& L )
            初始條件:線性表L已存在
            操作結果:將L重置為空表
        ListEmpty( L )  //判斷是否為空
            初始條件:線性表L已存在
            操作結果:若L為空表,則返回True,否則返回False
        ListLength( L )
            初始條件:線性表L已存在
            操作結果:返回線性表L中元素個數
        GetElem( L, i, &e )
            初始條件:線性表L已存在
            操作結果:用e返回L中第i個元素
        LocateElem( L, e, compare() )
            初始條件:線性表L已存在,compare()是數據元素判定函數
            操作結果:返回L中第一個與e滿足關系compare()的數據元素的位序。若不存在則返回0
        PriorElem( L, cur_e, &pre_e )
            初始條件:線性表L已存在
            操作結果:若cur_e是L的數據元素且不是第一個,則用pre_e返回其前驅;否則操作失敗,pre_e無定義
        NextElem( L, cur_e, &next_e )
            初始條件:線性表L已存在
            操作結果:若cur_e是L的數據元素且不是最后一個,則用next_e返回其后繼;否則操作失敗,pre_e無定義
        ListInsert(& L, i ,e )
            初始條件:線性表L已存在, 1<= i <= ListLength(L)+1
            操作結果:在L的第i個位置之前插入e,L的長度自增一
        ListDelete(& L , i , &e)
            初始條件:線性表L已存在且非空,1<= i <= ListLength(L)
            操作結果:刪除L中第i個元素,并用e返回其值,L長度減一
        ListTraverse( L, visit() )
            初始條件:線性表L已存在
            操作結果:對L中每個數據元素調用visit(), 一旦visit()失敗,則操作失敗
}ADT List
/*
對抽象類型線性表,還可以進行一些更為復雜的操作。如,合并或拆分表等
*/
算法 2.1——歸并線性表
  • 歸并
    用LA和LB表示A和B兩個集合,現要求一個新的集合A = A∪B。即擴大LA,將存在于LB而不存在于LA的元素插入到LA中
  • 算法實現
void union(List &La, List Lb){
    La_len = ListLength(La);Lb_len = ListLength(Lb);
    for (i=1; i<=Lb_len; ++i){
        GetElem(Lb, i, e);  //遍歷Lb中元素
        if( !ListLocate(La, e, equal) ){  //判斷e是否在La中
            ListInsert(La, ++La_len, e );  //將e插入至La末尾
        }
    }
}
算法2.2——歸并排序
  • 歸并排序
    線性表La和Lb中元素按值非遞減有序排列,要求將La和Lb合并,并保證值非遞減。
    如La = (1,2,3,3,5);Lb = (2,2,4,8)
    歸并為:Lc = (1,2,2,2,3,3,4,5,8)

相對于一個排序一個長表,將兩個短表排序后歸并的效率要更高一些

  • 問題解析
    將Lc設置為空表,然后將La和Lb中元素逐個插入Lc即可。插入元素滿足c = a≤ b?a:b,其中a,b分別屬于La和Lb

  • 算法實現

void MergeList(List La, List Lb, List &Lc){
    InitList(Lc);
    i = j = 1; k = 0;
    La_len = ListLength(La);
    Lb_len = ListLength(Lb);
    while((i<=La_len)&&(j<=Lb_len)){  //當La和Lb均非空時
        GetElem(La, i, ai);
        GetElem(Lb, j, bj);
        if(ai <= bj){
            ListInsert(Lc, ++k, ai);
            ++i;
        }
        else{
            ListInsert(Lc, ++k, bj);
            ++j;
        }
    }
    while(i <= La_len){  //La非空時
        GetElem(La, i++, e);
        ListInsert(Lc, ++k, e);
    }
    while(j <= Lb_len){
        GetElem(Lb, j++, e);
        ListInsert(Lc, ++k, e);
    }
}

上述兩個算法的時間復雜度取決于抽象數據類型List定義中基本操作的執(zhí)行時間。如GetElem(定位)和ListInsert與表長無關,因此時間復雜度為O(1),LocateElem(逐個元素操作)與表長有關,則算法2.1時間復雜度為O(ListLength(La) * ListLength(Lb)),2.2為O(ListLength(La) + ListLength(Lb))。

線性表的順序表示和實現


線性表的順序表示指的是用一組地址連續(xù)的存儲單元依次存儲線性表的數據元素。他的特點是,為表中相鄰的元素a1和a2賦以相鄰的存儲位置LOC(a1)和LOC(a2)。換句話說,以元素在計算機內“物理位置相鄰”來表示線性表中數據元素之間的邏輯關系。
假設每個元素需占用m個存儲單元,并以所占的第一個單元的存儲地址作為元素的存儲位置。則線性表的第i個元素ai的存儲位置滿足LOC(ai) = LOC(a1) + (i-1)*m。式中LOC(a1)稱作線性表的起始位置基地址。
順序表只要確定了存儲的起始位置,就可以隨機存取線性表中任一數據元素,因此順序存儲結構是一種隨機存取的存儲結構。

  • C語言中常用一維動態(tài)數組描述數據結構中的順序存儲結構。描述如下
//---線性表的動態(tài)分配順序存儲結構---
#define LIST_INIT_SIZE    100  //線性表存儲空間初始分配量
#define LISTINCREMENT    10  //線性表存儲空間分配增量
typedef struct{
    ElemType    *elem;  //存儲空間基地址,等價于數組名
    int         length;  //當前長度
    int         listsize;  //當前分配的存儲容量
}SqList;
  • typedef是自定義數據類型 Sqlist是自定義數據類型名

順序表的初始化操作就是

  1. 為順序表分配一個預定義大小(LIST_INIT_SIZE)的數組空間
  2. 將線性表當前長度設為“0”(具體參見算法2.3)
  3. 一旦因插入元素導致空間不足(大于listsize)時,可進行再分配,即為順序表增加一個大小為存儲LISTINCREMENT個數據元素的空間
  • 上述代碼等價于
struct List{
    ElemType* elem;
    int length;
    int listsize;
}
#typedef List SqList;
算法 2.3——初始化線性表
  • 算法實現
Status InitList_Sq(SqList &L){ //SqList是自定義的數據類型
    L.elem = (ElemType*)malloc(LIST_INIT_SIZE * sizeof(ElemType));// malloc分配一個滿足他的存儲空間,并返回指向它的指針
    if(! L.elem)  {exit(OVERFLOW);}  //存儲分配失敗,溢出
    L.length = 0;  //空表長度為0
    L.listsize = LIST_INIT_SIZE;  //初始存儲容量
    return OK;
}

順序存儲結構下線性表元素的插入與刪除

由于邏輯上相鄰的數據元素在物理位置上也是相鄰的,因此必須移動元素才能反應這個邏輯關系的變化。

算法 2.4——插入
  • 插入:在第i(1≤ i≤ n)個元素之前插入一個元素時,需將第n至第i(共n-i+1)個元素向后移動一個位置
  • 算法實現
Status ListInsert_Sq (SqList& L, int i, ElemType e){
    //在順序表L第i個位置之前插入新元素e
    //i的合法值為 1≤ i≤ ListLength_Sq(L)+1
    if (i<1 || i>= L.length + 1)  
        return ERROR;
    if (L.length > L.listsize){    // 當前長度大于當前分配的存儲容量,即當前存儲空間已滿 增加分配
        newbase = (ElemType* )realloc(L.elem, (L.listsize+LISTCREMENT)*sizeof(ElemType));
        if(!newbase)  
            exit(OVERFLOW);  //若重新分配失敗,證明溢出
        L.elem = newbase;  //新基地址,相當于數組名
        L.listsize += LISTINCREMENT;  //增加存儲容量
    }
    q = &(L.elem[i-1])  //q是插入位置,elem等價于數組名
    for (p = &(L.elem[L.length-1]); p>= q; --p){  //從末尾開始,將q及以后的所有元素向右移動
        *(p+1) = *p;
    }
    *q = e;  //插入e
    ++L.length;  //表長增一
    return OK;
}// ListInsert_Sq
算法 2.5——刪除
  • 刪除:刪除第i(1≤ i≤ n)個元素時,需將第i至第n(共n-i)個元素向前移動一個位置
  • 算法實現
Status ListDelete_Sq(SqList& L, int i, ElemType &e){  //用e返回被刪除的元素
    //i的合法值為1<= i <= L.length,與插入操作不同
    if(i<1 || i>L.length)
        return ERROR;
    p = &(L.elem[i-1]);
    e = *p;
    q = &(L.elem[L.length-1]);  //等價于 q = L.elem+L.length-1
    for(p ; p<= q; p++ ){
        *(p-1) = *p;
    }
    --L.length;
} //ListDelete_Sq

插入與刪除算法時間主要耗費在移動元素上,故移動元素的操作為基本操作,而移動元素的個數取決于插入或刪除的位置。

插入和刪除元素的時間復雜度
  • 插入元素時間復雜度:假設Pi是在第i個元素元素之前插入一個元素的概率,則在長度為n的線性表中插入一個元素時所需移動元素的期望值(平均次數)為:
    (1)
  • 刪除元素時間復雜度:假設Qi是刪除第i個元素的概率,則在長度為n的線性表中刪除一個元素時所需移動元素的期望值(平均次數)為:


    (2)

    不失一般性,可以假定在線性表的任何位置上插入或刪除元素都是等概率的,即



    則式(1)和式(2)可分別簡化為
    (3)

    (4)

由此可見,平均需要移動約一半的元素。若表長為n,算法ListInsert_Sq和ListDelete_Sq的時間復雜度為O(n)

算法 2.6——順序表的union
  • 歸并(順序表):基本操作是“進行兩個元素的比較”,若L中存在與e相同的元素ai,則比較次數為1<= i <= L.length,否則為表長,即算法LocateElem_Sq的時間復雜度為O(L.length),所以對于La和Lb而言,其時間復雜度為O(La.length*Lb.length)
  • 算法實現
int LocateElem_Sq(SqList L, ElemType e, Status ( *compare)(ElemType, ElemType)){
    //在順序線性表L中查找第1個與e滿足compare()的位序
    //若找到則返回位序,否則返回0
    i = 1;
    np = L.elem;    //數組名,第一個元素存儲位置
    while (i <=  L.length && !( *compare) ( *p++, e)){  //判斷是否存在滿足compare函數的元素
        i++;
    }
    if( i <= L.length){
        return i;
    }
    return 0;
}
算法 2.7——順序表的歸并
  • 歸并排序(順序表) 顯然順序表歸并的基本操作是“元素賦值”,算法時間復雜度為O(La.length + Lb.length)。已知La和Lb中元素單調非減排列,現要求構造Lc歸并排列。
  • 算法實現
void MergeList_Sq( SqList La, SqList Lb, SqList& Lc){
    //用Lc返回新的列表
    pa = La.elem;  pb = Lb.elem;
    //需要為Lc的三個屬性都賦值 
    Lc.listsize = Lc.length = La.length + Lb.length;  //偽代碼連續(xù)賦值
    pc = Lc.elem =  (ElemType* )malloc(sizeof(ElemType )* Lc.listsize);
    if(! pc)
        exit(OVERFLOW);
    pa_last = La.elem + La.length -1;    //取末尾位置
    pb_last = Lb.elem + Lb.length -1;
    while( pa <= pa_last && pb <= pb_last ){
        if(*pa >= *pb)
            *pc++ = *pb++;
        else
            *pc++ = *pa++;
    }
    while (pa <= La.length){
        *pc++ = *pa++;
    }
    while (pb <= Lb.length){
        *pc++ = *pb++;
    }
}  //MergeList_Sq

順序表的優(yōu)缺點

  • 優(yōu)點
    1. 節(jié)省存儲空間
    2. 對第i個節(jié)點操作易于實現
    3. 容易查找一個節(jié)點的前驅和后繼
  • 缺點
    1. 插入和刪除操作困難

線性表的鏈式表示和實現


不要求邏輯上相鄰的元素在物理位置上也相鄰
一個線性表由若干個結點組成,每個結點至少含有兩個域:數據域(信息域)和指針域(鏈域),這樣的結點形成存儲的線性表稱作鏈表
在使用鏈表時,關心的只有它所表示的線性表中數據元素之間的邏輯順序,而不是在存儲器中的實際位置。

線性鏈表(單鏈表)

  • 邏輯關系
    數據元素之間的邏輯關系是由結點中的指針指示的。指針為數據元素之間的邏輯關系的映像,邏輯上相鄰的兩個數據元素其存儲的物理位置不要求緊鄰。
  • 頭結點
    本身不存放數據,指針域指向第一個元素的地址。使對第一個元素的操作與對其他元素的操作保持一致。用頭指針指向頭結點。
  • 結構示意圖


結構指針
typedef struct Lnode{
    //單鏈表存儲結構
    ElemType data;  //數據域
    struct Lnode *next; //單鏈表指針域
}LNode, *LinkList;  // 此行直接定義了 鏈LNode 和 頭指針LinkList 類型名

LinkList L; //LinkList是指針類型的struct Lnode,L為單鏈表的頭指針

鏈表的嵌套定義:嵌套定義的前提是已知所需分配的空間。鏈表可嵌套定義是因為所有指針變量所需空間大小是確定的,不受指針基類型的影響

算法 2.8——GetElem在單鏈表中的實現
  • 在單鏈表中取元素:在單鏈表中,取得第i個元素的信息,必須從頭指針出發(fā)尋找。
  • 算法實現
Status GetElem_L(LinkList L, int i, ElemType& e){  
    //假設L是帶頭結點鏈表的頭指針, 用e返回獲得的結果
    p = L->next; j =1;  //初始化指針,使p指向第一個結點;用j作為計數器
    while(p && j<i){
        p = p->next;
        ++j;
    }
    if( !p||j>i)
        return ERROR;
    e = p -> data;
    return OK:
}

GetElem時間復雜度

while循環(huán)體中的語句頻度和被查元素在表中位置有關,若1< i≤ n則頻度為 i-1,否則為n。因此時間復雜度為線性O(n)。

算法 2.9——單鏈表插入新結點
  • 插入: 為在a和b元素中插入新元素x,根據插入操作的邏輯定義,首先要修改結點a中的指針域,令其指向結點x,而結點x的指針域指向結點b。假設s為指向結點x的指針,則上述操作的語句描述為: s->next = p->next; p->next = s
  • 插入元素原理圖


  • 算法實現
Status ListInsert_L (LinkList& L, int i, ElemType e){
    //在帶頭結點的單鏈表L中第i個位置前插入元素e
    p = L; j = 0;  //初始化p為頭指針
    while( p && j < i-1){  //尋找 i-1結點
        p = p->next;  
        ++j;
    }
    if(!p || j>i-1){
        return ERROR;
    }
    s = (ElemType* ) malloc( sizeof( ElemType));
    s -> data = e;  s -> next = p -> next;
    p -> next = s;
    return OK;
}

算法 2.10——單鏈表刪除結點
  • 刪除元素原理圖

  • 算法實現

Status ListDelete_L(LinkList& l, int i, ElemType& e){
    //刪除單鏈表L第i個元素并用e返回值
    p = L; j = 0;
    while(p ->next && j<i-1 ){  //尋找第i個結點,并令p指向其前驅
        p = p->next;
        ++j;
    }
    if ( !( p->next) || j>i-1)  //插入位置不合法
        return ERROR;
    q = p->next; p->next = q->next;  //刪除結點
    e = q->data;
    free(q);  //釋放被刪除的結點
    return OK;
}

二者的時間復雜的都是O(n)

malloc函數

假設p是LinkList類型變量,則執(zhí)行p = (LinkList) malloc (sizeof ( LNode))語句的含義是,生成一個LNode型的結點,并將其起始位置賦給p。反之free( p)是回收一個LNode型結點,回收后的空間可以備作再次生成節(jié)點的空間

算法 2.11——逆向建立單鏈表
  • 逆向建立單鏈表:從表尾到表頭,時間復雜度為O(n)
  • 算法實現
void CreateList_L( LinkList& L, int n){
    //逆序輸入n個元素的值,建立帶表頭節(jié)點的單鏈表L
    L = (LinkList )malloc( sizeof( LNode));  
    L -> next = NULL;  //建立一個帶有頭結點的空表
    for ( i=n; i>0; --i){
        p = (LinkList) malloc (sizeof ( LNode));  
        scanf(& p->data);        
        p->next = L->next;
        L->next = p;  //逆序插入結點
    }
}

算法 2.12——合成有序鏈表
  • 合成:鏈表的歸并排列。
  • 算法實現
void MergeList_L(LinkList La, LinkList Lb, LinkList &Lc){
    // 已知La和Lb為 內容元素非減的 單鏈表
    pa = La->next; pb = Lb->next; //使pa和pb指向第一個元素
    pc = Lc = La; //用La的頭節(jié)點作為Lc的頭節(jié)點
    while (pa && pb){
        if(pa->data <= pb->data){
            pc->next = pa;  //將pa插入到pc后
            pc = pa;  //平移pc指針,應該等價于pc = pc->next;
            pa = pa->next;  //平移pa指針
        }
        else{
            pc->next = pb;
            pc = pb;
            pb = pb->next;
    }
    pc -> next = pa? pa:pb;
    free(Lb); //不可釋放La,因為此時La和Lc共用一個地址。
} //MergeLsit_L

有時可以借用一維數組來描述線性鏈表,結構如下

//------線性表的靜態(tài)單鏈表存儲結構------
#define MAXSIZE 100
typedef struct{
    ElemType elem;
    int  cur;
} component, SLinkList[MAXSIZE];

此種類型在不設“指針”的情況下使用鏈表結構。
上述結構中,數組的一個分量代表一個節(jié)點,同時用游標cur代替指針指示結點在數組中的相對位置。數組的第零分量可以看成是頭節(jié)點,其指針域指示鏈表的第一個結點。這種存儲結構仍需預先分配出一個較大的空間,但在插入和刪除元素時不需要修改移動元素,只需要修改指針,仍具有鏈式存儲結構的主要優(yōu)點。我們稱這種存儲方式為靜態(tài)鏈表。
下圖展示了 在SUN后插入GUO并刪除ZHOU的操作。

假設S為SLinkList型變量,則S[0].cur指示第一個結點在數組中的位置。設 i = S[0].cur; 則S[i].cur表示第二個結點在數組中的位置。i = S[i].cur實為指針的后移(等價于 p = p -> next)

算法 2.13——靜態(tài)鏈表定位函數
  • 定位:查找第一個值為e的元素。若找到則返回位序,否則返回0
  • 算法實現
int LocateElem_SL(SLinkList L, ElemType e){
    i = S[0].cur; 
    while (i && S[i].data !=e){
        i = S[i].cur;  //遍歷,之道數據域等于e
    }
    return i;
}  // LocateElem_SL

類似的,靜態(tài)鏈表可以實現插入和刪除操作。所不同的是,malloc和free兩個函數需要自己編寫。為了分辨哪些分量未被使用,可以將所有未被使用過的以及被刪除的分量用游標構成一個備用的鏈表,每當進行插入操作時從備用鏈表上取得第一個結點作為待插入的新結點;反之將刪除下來的新結點插入到備用鏈表后。

算法 2.14——靜態(tài)鏈表的初始化
  • 算法實現
void InitSpace_SL(SLinkList &space){
    // space[0].cur作為頭指針
    for ( i=0; i<MAXSIZE-1; ++i){
        space[i].cur = i+1;
    }
    space[MAXSIZE-1].cur=0;
}
算法 2.15——靜態(tài)鏈表分配空間
  • 分配:在插入新結點,給新結點分配空間實際上就是為結點從備用鏈表中分配一個結點下標
  • 算法實現
int Malloc_SL(SLinkList &space){
    // 若備用鏈表非空則返回分配的結點下標,否則返回0
    i = space[0].cur;
    if(space[0].cur){  // 如果備用鏈表非空
        space[0].cur = space[i].cur;  // 連接備用鏈表頭結點和第二個結點
    }
    return i;
} // Malloc_SL
算法 2.16——靜態(tài)鏈表回收結點
  • 將結點回收,即將該結點放回備用鏈表中
  • 算法實現
void Free_SL(SLinkList &space, int k){
    // 將下標為k的空閑結點回收到備用鏈表
    space[k].cur = space[0].cur;
    space[0].cur = k;
}   
算法 2.17——集合運算(A/B)∪(B/A)
  • 對稱差:假設由終端輸入集合元素,先建立表示集合A的靜態(tài)鏈表S,而后在輸入集合B的元素的同時查找S表,若存在和B相同的元素,則從S表中刪除,否則插入S表
  • 算法實現
void difference_SL(SLinkList &space, int &S){
    // 依次輸入A和B的元素,在一堆數組space中建立表示(A/B)∪(B/A)
    // 的靜態(tài)鏈表,S為其頭指針。假設備用空間足夠大,space[0].cur為其頭指針。
    InitSpace_SL(space);
    S = Malloc_SL(space); // 生成S的頭結點
    r = S; // r指向S的末尾
    scanf(m,n); // 輸入A和B的元素個數
    for(j = 1; j <= m; ++j){
        i = Malloc_SL(space); // 分配結點
        scanf(space[i].data);
        space[r].cur = i; r = i; // 插入到表尾
    }  //for 
    space[r].cur = 0; //偽結點的指針為空
    for(j = 1; j <= n; ++j){
        scanf(b); p = S;
        k = space[S].cur; // k指向A中的第一個結點
        while(k != space[r].cur && space[k].data != b){
            p = k; k = space[k].cur; 
        }  //while
        if( k == space[r].cur){ // 當前表中不存在該元素,插入在r所指結點之后,且r的位置不變
            i = Malloc_SL(space);
            space[i].data = b;
            space[i].cur = space[r].cur;
            space[r].cur = i;
        } //if
        else{
             space[p].cur = space[k].cur;
             Free_SL(space,k);
             if (r == k)  //若刪除的是r所指結點,則需修改尾指針 
                 r = p;
        }  //else
    }  //for
}  //difference
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容