1. 程式人生 > >資料結構——線性表 (順序表、單鏈表、靜態連結串列、迴圈連結串列、雙向連結串列)

資料結構——線性表 (順序表、單鏈表、靜態連結串列、迴圈連結串列、雙向連結串列)

提示:以下內容不適合零基礎人員,僅供筆者複習之用。

一、線性結構的基本特徵:
1.集合中必存在唯一的一個“第一元素”;
2.集合中必存在唯一的一個 “最後元素”;
3.除最後元素在外,均有 唯一的後繼;
4.除第一元素之外,均有 唯一的前驅。
如:java中的List介面,就是線性表。ArrayList就是順序線性表,LinkedList就是連結串列線性表。

二、線性表的基本操作:
1.InitList(*L): 初始化操作,建立一個空的線性表L。
2.ListEmpty(L): 判斷線性表是否為空表,若線性表為空,返回true,否則返回false。
3.ClearList(*L): 將線性表清空。
4.GetElem(L,i,*e): 將線性表L中的第i個位置元素值返回給e。
5.LocateElem(L,e): 線上性表L中查詢與給定值e相等的元素,如果查詢成功,返回該元素在表中序號表示成功;否則,返回0表示失敗。
6.ListInsert(*L,i,e): 線上性表L中第i個位置插入新元素e。
7.ListDelete(*L,i,*e): 刪除線性表L中第i個位置元素,並用e返回其值。
8.ListLength(L): 返回線性表L的元素個數。
——對於不同的應用,線性表的基本操作是不同的,上述操作是最基本的。對於實際問題中涉及的關於線性表的更復雜操作,完全可以用這些基本操作的組合來實現。

三、兩種不同的線性表
我們知道,資料結構分為邏輯結構物理結構,邏輯結構分為集合結構、線性結構、樹形結構和圖形結構四大類。物理結構分為順序儲存結構和鏈式儲存結構。
線性表是線性結構的一種,那麼線性表當然也有物理結構,也就是說,線性表有兩種,分別是順序結構的線性表(叫做順序表)和鏈式結構的線性表(叫做連結串列)。

3.1 順序儲存結構的線性表
3.1.1 定義
指的是用一段地址連續的儲存單元依次儲存線性表的資料元素。和陣列不一樣,陣列的長度是存放線性表的儲存空間的長度,儲存分配後這個量一般是不變的。線性表是線性表中資料元素的個數,隨著插入和刪除的操作,長度會變。所以,這裡要區分兩個概念,即陣列長度和線性表的長度

是不一樣的。在任意時刻,線性表的長度應該小於等於陣列的長度

3.1.2 儲存方式
因為每個資料元素的型別都相同,所以可以使用一維陣列來實現。結構程式碼如下:

//線性表的順序儲存結構  
#define MAXSIZE 20;//儲存空間初始分配量為20  
typedef int ElemType;//資料型別為int  
type struct  
{  
    ElemType data[MAXSIZE];//陣列儲存資料元素  
    int length;//線性表長度  
}SqList; 

這裡可以看到,順序儲存結構需要三個屬性:

  • 儲存空間的起始位置:陣列data,它的儲存位置就是儲存空間的儲存位置。
  • 線性表的最大儲存容量:陣列長度MaxSize。
  • 線性表的當前長度:length。

3.1.3 地址計算方法
這裡寫圖片描述
若每個儲存元素佔用c個儲存單元,那麼線性表中元素的位置可以由此計算出:
這裡寫圖片描述
通過這個公式,可隨時算出線性表中任意位置的地址,使用相同的時間。它的存取時間效能為O(1),這一特點的儲存結構稱之為隨機存取結構。

3.1.4 操作
獲取元素:

#define MAXSIZE 20  //儲存空間初始分配量
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

typedef int Status;

typedef int ElemType; //ElemType型別根據實際情況而定,這裡設為int

Status GetElem(SqList L, int i, ElemType *e){//獲取元素
    if (L.length == 0 || i<1 || i>L.length){
        return ERROR;
    }
    *e = L.data[i - 1];
    return OK;
}

插入元素:

  • 如果插入位置不合理,丟擲異常
  • 如果線性表長度大於等於陣列長度,則丟擲異常或者動態增加容量
  • 從最後一個元素開始向前遍歷到第i個位置,分別將它們向後移動一個位置
  • 將要插入元素填入位置i處
  • 表長度加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不在範圍內時
        return ERROR;
    }

    if (i <= L.length){//若插入資料的位置不在表尾
        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;
}

刪除元素:

  • 如果刪除位置不合理,丟擲異常
  • 取出刪除元素
  • 從刪除元素位置開始遍歷到最後一個元素位置,分別將它們向前移動一個位置
  • 表長減1
Status ListDelete(SqList L, int i, ElemType *e){//刪除操作
    int k;

    if (L.length==0){//線性表為空
        return ERROR;
    }

    if (i<1 || i>L.length + 1){//刪除位置不正確
        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;
}

3.1.5 時間複雜度
在存、讀資料時,不管是哪個位置,時間複雜度都是O(1);而插入或刪除操作時,時間複雜度都是O(n)。

3.1.6 優缺點
優點:

  • 無須為表示表中元素之間的邏輯關係而增加額外的儲存空間
  • 可以快速地存取表中任一位置的元素

缺點:

  • 插入和刪除操作需要移動大量元素
  • 當線性表長度變化較大時,難以確定儲存空間的容量
  • 造成儲存空間的“碎片”

3.2 鏈式儲存結構的線性表
3.2.1 定義
單鏈表:n個結點(ai的儲存映像,每個結點中只包括一個指標域)連結成一個連結串列,即為線性表(a1,a2,….an)的鏈式儲存結構。
頭指標:連結串列中第一個結點的儲存位置。
這裡寫圖片描述
頭結點:有時為了便於操作,在單鏈表的第一個結點前附設一個結點,稱為頭結點。頭結點的資料域可以不存資訊,可以存線性表的長度等附加資訊,頭結點的指標域指向第一個結點的指標。
這裡寫圖片描述

頭指標和頭結點的區別
頭指標

  • 頭指標是指連結串列指向第一個結點的指標,若連結串列有頭結點,則是指向頭結點的指標
  • 頭指標具有標識作用,常用頭指標冠以連結串列的名字
  • 無論連結串列是否為空,頭指標均不為空。頭指標是連結串列的必要元素

頭結點

  • 頭結點是為了操作的統一和方便設立的,在第一個元素的結點之前,其資料域一般無意義(或存放連結串列長度)。
  • 有了頭結點,對在第一個元素結點前插入結點和刪除第一結點,其操作與其他結點的操作就統一了
  • 頭結點不一定是連結串列必須要素

3.2.2 線性錶鏈式儲存結構

typedef struct Node{//線性表的單鏈表儲存結構
    ElemType data;
    struct Node *next;
}Node;

typedef struct Node *LinkList;//定義LinkList

這裡寫圖片描述

3.2.3 單鏈表的讀取

  • 宣告一個結點p指向連結串列第一個結點,初始化j從1開始
  • 當j < i時,就遍歷連結串列,讓p的指標向後移動,不斷指向下一結點,j累加1
  • 若到連結串列末尾p為空,則說明第i個元素不存在
  • 否則查詢成功,返回結點p的資料
/* 初始條件:順序線性表L已存在,1≤i≤ListLength(L) */
/* 操作結果:用e返回L中第i個數據元素的值 */
Status GetElem(LinkList L,int i,ElemType *e)
{
    int j;
    LinkList p;     /* 宣告一結點p */
    p = L->next;        /* 讓p指向連結串列L的第一個結點 */
    j = 1;      /*  j為計數器 */
    while (p && j<i)  /* p不為空或者計數器j還沒有等於i時,迴圈繼續 */
    {   
        p = p->next;  /* 讓p指向下一個結點 */
        ++j;
    }
    if ( !p || j>i ) 
        return ERROR;  /*  第i個元素不存在 */
    *e = p->data;   /*  取第i個元素的資料 */
    return OK;
}

說白了,就是從頭開始找,直到第i個元素為止。最好情況的時間複雜度為O(1),最壞情況的時間複雜度為O(n)

3.2.4 單鏈表的插入
這裡寫圖片描述
① s->next = p->next;
② p->next = s;
注意以上順序不能顛倒,否則p->next給覆蓋成s的地址了,這樣,a(i+1)結點就沒有了上級。

演算法思路:

  • 宣告一個結點p指向連結串列第一個結點,初始化j從1開始
  • 當j<1時,就遍歷連結串列,讓p的指標向後移動,不斷指向下一結點,j累加1
  • 若到連結串列末尾p為空,則說明第i個元素不存在
  • 否則查詢成功,在系統中新生成一個空結點s
  • 將資料元素e賦值為s->data
  • 單鏈表的插入標準語句s->next=p->next;p->next=s;
  • 返回成功
/* 初始條件:順序線性表L已存在,1≤i≤ListLength(L), */
/* 操作結果:在L中第i個位置之前插入新的資料元素e,L的長度加1 */
Status ListInsert(LinkList *L,int i,ElemType e)
{ 
    int j;
    LinkList p,s;
    p = *L;   
    j = 1;
    while (p && j < i)     /* 尋找第i個結點 */
    {
        p = p->next;
        ++j;
    } 
    if (!p || j > i) 
        return ERROR;   /* 第i個元素不存在 */
    s = (LinkList)malloc(sizeof(Node));  /*  生成新結點(C語言標準函式) */
    s->data = e;  
    s->next = p->next;      /* 將p的後繼結點賦值給s的後繼  */
    p->next = s;          /* 將s賦值給p的後繼 */
    return OK;
}

3.2.5 單鏈表的刪除
這裡寫圖片描述
實際上就是一步,p->next=p->next->next;用q取代p->next,即是:
q=p->next;
p->next=q->next;

演算法思路:

  • 宣告一個結點p指向連結串列第一個結點,初始化j從1開始
  • 當j<1時,就遍歷連結串列,讓p的指標向後移動,不斷指向下一個結點,j累加1
  • 若到連結串列末尾p為空,則說明第i個元素不存在
  • 否則查詢成功,將要刪除的結點p->next賦值給q
  • 單鏈表的刪除標準語句p->next=q->next;
  • 將q結點中的資料賦值給e,作為返回
  • 釋放q結點
  • 返回成功
/* 初始條件:順序線性表L已存在,1≤i≤ListLength(L) */
/* 操作結果:刪除L的第i個數據元素,並用e返回其值,L的長度減1 */
Status ListDelete(LinkList *L,int i,ElemType *e) 
{ 
    int j;
    LinkList p,q;
    p = *L;
    j = 1;
    while (p->next && j < i)    /* 遍歷尋找第i個元素 */
    {
        p = p->next;
        ++j;
    }
    if (!(p->next) || j > i) 
        return ERROR;           /* 第i個元素不存在 */
    q = p->next;
    p->next = q->next;          /* 將q的後繼賦值給p的後繼 */
    *e = q->data;               /* 將q結點中的資料給e */
    free(q);                    /* 讓系統回收此結點,釋放記憶體 */
    return OK;
}

3.2.6 單鏈表操作的時間複雜度
分析單鏈表的插入和刪除演算法,第一步就是遍歷查詢到第i個元素;第二步就是插入和刪除元素。容易看出,它們的時間複雜度都是O(n),如果在不知道第i個元素的指標位置,單鏈表資料結構在插入和刪除操作上,與線性表的順序儲存結構是沒有太大優勢的。但如果,我們希望從第i個位置,插入10個元素,對於順序儲存結構意味著,每一次插入都需要移動n-i個元素,每次都是O(n),而單鏈表,我們只需要在第一次時,找到第i個位置的指標,此時為O(n),接下來只是簡單地通過賦值移動指標而已,時間複雜度都是O(1)。顯然,對於插入或刪除資料越頻繁的操作,單鏈表的效率優勢就越是明顯

3.2.7 單鏈表的整表建立
因為單鏈表佔用空間的大小和位置是不需要預先分配劃定的,可以根據系統的實際情況和需求即時生成。其建立的過程就是一個動態生成連結串列的過程,即從“空表”的初始狀態起,依次建立各元素結點,並逐個插入連結串列。
演算法思路:

  • 宣告一結點p和計數器變數i
  • 初始化一空連結串列L
  • 讓L的頭結點的指標指向NULL,即建立一個帶頭結點的單鏈表
  • 迴圈:
    生成一個新結點賦值給p
    隨機生成一數字賦值給p的資料域p->data
    將p插入到頭結點與前一新結點之間
/*  隨機產生n個元素的值,建立帶表頭結點的單鏈線性表L(頭插法) */
void CreateListHead(LinkList *L, int n) 
{
    LinkList p;
    int i;
    srand(time(0));                         /* 初始化隨機數種子 */
    *L = (LinkList)malloc(sizeof(Node));
    (*L)->next = NULL;                      /*  先建立一個帶頭結點的單鏈表 */
    for (i=0; i<n; i++) 
    {
        p = (LinkList)malloc(sizeof(Node)); /*  生成新結點 */
        p->data = rand()%100+1;             /*  隨機生成100以內的數字 */
        p->next = (*L)->next;    
        (*L)->next = p;                     /*  插入到表頭 */
    }
}

以上是使用頭插法實現,還可以使用尾插法實現,即按排隊順序,先來後到,每次加入的新結點都插在終端結點後面:

/*  隨機產生n個元素的值,建立帶表頭結點的單鏈線性表L(尾插法) */
void CreateListTail(LinkList *L, int n) 
{
    LinkList p,r;
    int i;
    srand(time(0));                      /* 初始化隨機數種子 */
    *L = (LinkList)malloc(sizeof(Node)); /* L為整個線性表 */
    r=*L;                                /* r為指向尾部的結點 */
    for (i=0; i<n; i++) 
    {
        p = (Node *)malloc(sizeof(Node)); /*  生成新結點 */
        p->data = rand()%100+1;           /*  隨機生成100以內的數字 */
        r->next=p;                        /* 將表尾終端結點的指標指向新結點 */
        r = p;                            /* 將當前的新結點定義為表尾終端結點 */
    }
    r->next = NULL;                       /* 表示當前連結串列結束 */
}

3.2.8 單鏈表的整表刪除
演算法思路:

  • 宣告一結點p和q
  • 將第一個結點賦值給p
  • 迴圈:
    將下一結點賦值給q
    釋放p
    將q賦值給p
/* 初始條件:順序線性表L已存在。操作結果:將L重置為空表 */
Status ClearList(LinkList *L)
{ 
    LinkList p,q;
    p=(*L)->next;           /*  p指向第一個結點 */
    while(p)                /*  沒到表尾 */
    {
        q=p->next;
        free(p);
        p=q;
    }
    (*L)->next=NULL;        /* 頭結點指標域為空 */
    return OK;
}

提示:q變數很重要,不能直接free(p);因為:p是一個結點,除了資料域,還有指標域。free(p);是對整個結點進行刪除和記憶體釋放的工作。而變數q的作用是,使得下一個結點得到了記錄,以便於釋放當前結點後,把下一結點拿回來補充。(類似皇帝的遺囑)

3.3 單鏈表結構與順序儲存結構優缺點
對單鏈表結構和順序儲存作對比:
這裡寫圖片描述
經分析,可得出一些經驗結論:

  • 若線性表需要頻繁查詢,很少進行插入、刪除操作,宜採用順序儲存結構。若需要頻繁插入和刪除時,宜採用單鏈表結構。比如遊戲開發中,使用者註冊的個人資訊,除註冊時插入資料外,絕大多數是讀取,所以應該考慮順序儲存結構。而玩家的武器或裝備列表,可能隨時增加或減少,這時可以考慮單鏈表結構。
  • 當線性表中的元素個數變化較大或根本不知道有多大時,最好用單鏈表結構,這樣可以不需要考慮儲存空間的大小問題。而事先知道線性表的大致長度,比如一年12個月,這種用順序結構效率會高很多。

3.4 靜態連結串列(連結串列的遊標實現)
陣列描述的連結串列叫做靜態連結串列。陣列的每個下表都對應一個data和一個cur,資料域data用於存放資料元素,cur相當於單鏈表中的next指標,存放該元素後繼在陣列中的下標。

/* 線性表的靜態連結串列儲存結構 */
#define MAXSIZE 1000 /* 儲存空間初始分配量 */

typedef int Status;           /* Status是函式的型別,其值是函式結果狀態程式碼,如OK等 */
typedef char ElemType;        /* ElemType型別根據實際情況而定,這裡假設為char */

/* 線性表的靜態連結串列儲存結構 */
typedef struct 
{
    ElemType data;
    int cur;  /* 遊標(Cursor) ,為0時表示無指向 */
} Component,StaticLinkList[MAXSIZE];

補充概念:備用連結串列——未被使用的陣列元素。
靜態連結串列特點:

  • 陣列的第一個元素,即下標為0的元素的cur就存放備用連結串列的第一個結點的下標;
  • 陣列的最後一個元素的cur則存放第一個有數值的元素的下標,相當於單鏈表中頭結點的作用,當整個連結串列為空時為0。

這裡寫圖片描述
3.4.1 初始化:

/* 將一維陣列space中各分量鏈成一個備用連結串列,space[0].cur為頭指標,"0"表示空指標 */
Status InitList(StaticLinkList space) 
{
    int i;
    for (i=0; i<MAXSIZE-1; i++)  
        space[i].cur = i+1;
    space[MAXSIZE-1].cur = 0; /* 目前靜態連結串列為空,最後一個元素的cur為0 */
    return OK;
}

假設已存入甲、乙、丁、戊、己、庚等資料,則儲存分配示意如下:
這裡寫圖片描述

3.4.2 插入:
靜態連結串列中要解決的是,如何用靜態模擬動態連結串列結構的儲存空間的分配,需要時申請,無用時釋放。可將所有未被使用的及已被刪除的分量用遊標鏈成一個備用的連結串列,每當進行插入時,便可以從備用連結串列上取得第一個結點作為待插入的新結點。

/* 若備用空間連結串列非空,則返回分配的結點下標,否則返回0 */
int Malloc_SSL(StaticLinkList space) 
{ 
    int i = space[0].cur;                   /* 當前陣列第一個元素的cur存的值 */
                                            /* 就是要返回的第一個備用空閒的下標 */
    if (space[0]. cur)         
        space[0]. cur = space[i].cur;       /* 由於要拿出一個分量來使用了, */
                                            /* 所以我們就得把它的下一個 */
                                            /* 分量用來做備用 */
    return i;
}

這段程式碼用於返回一個下標值,即陣列頭元素的cur存的第一個空閒的下標,如上圖的話,應該返回7。
如果在上述儲存內容中繼續插入丙,步驟是,先把丙放在位置7,把乙的cur改為7,再把丙的cur改為3,這樣就完成了插入。

/*  在L中第i個元素之前插入新的資料元素e   */
Status ListInsert(StaticLinkList L, int i, ElemType e)   
{  
    int j, k, l;   
    k = MAXSIZE - 1;   /* 注意k首先是最後一個元素的下標 */
    if (i < 1 || i > ListLength(L) + 1)   
        return ERROR;   
    j = Malloc_SSL(L);   /* 獲得空閒分量的下標 */
    if (j)   
    {   
        L[j].data = e;   /* 將資料賦值給此分量的data */
        for(l = 1; l <= i - 1; l++)   /* 找到第i個元素之前的位置 */
           k = L[k].cur;           
        L[j].cur = L[k].cur;    /* (邏輯重點)把第i個元素之前的cur賦值給新元素的cur */
        L[k].cur = j;           /* (邏輯重點)把新元素的下標賦值給第i個元素之前元素的cur */
        return OK;   
    }   
    return ERROR;   
}

呼叫時輸入i為3,
第4行k=MAXSIZE-1=999。
第7行,j=7。此時下標為0的cur也因為7要被佔用而更改備用連結串列的值為8。(Malloc_SSL方法內更新備用連結串列首結點位置
第11-12行,for迴圈l由1到2,執行兩次,程式碼k=L[k].cur;使得k=999,得到k=L[999].cur=1,再得到k=L[i].cur=2。
第13行,L[j].cur=L[k].cur;因j=7,而k=2得到L[7].cur=L[2].cur=3。就是剛說的把丙的cur改為3。
第14行,L[k].cur=j;意思就是L[2].cur=7。也就是把乙的cur改為指向丙的下標7。
這裡寫圖片描述

3.4.3 刪除:
如果要刪除甲,這個位置就空出來,有新元素進來就優先考慮這裡,所以原來的第一個空位分量,即下標是8 的分量要降級了(後退為備用連結串列的第二個結點),把8給“甲”所在下標為1的分量的cur,也就是space[1].cur=space[0].cur=8,而space[0].cur=k=1就是讓這個刪除的位置稱為第一個優先空位,把它存入第一個元素的cur中,如圖:
這裡寫圖片描述
相關程式碼描述:

/*  將下標為k的空閒結點回收到備用連結串列 */
void Free_SSL(StaticLinkList space, int k) 
{  
    space[k].cur = space[0].cur;    /* 把第一個元素的cur值賦給要刪除的分量cur */
    space[0].cur = k;               /* 把要刪除的分量下標賦值給第一個元素的cur */
}

/*  刪除在L中第i個數據元素   */
Status ListDelete(StaticLinkList L, int i)   
{ 
    int j, k;   
    if (i < 1 || i > ListLength(L))   
        return ERROR;   
    k = MAXSIZE - 1;   
    for (j = 1; j <= i - 1; j++)   
        k = L[k].cur;   
    j = L[k].cur;   
    L[k].cur = L[j].cur;   
    Free_SSL(L, j);   
    return OK;   
} 

3.4.4 靜態連結串列優缺點:
這裡寫圖片描述
總的說,靜態連結串列是為了給沒有指標的高階語言設計的一種實現單鏈表能力的方法。雖使用較少,但思考方式比較巧妙,思想值得借鑑。

3.5 迴圈連結串列
將單鏈表中終端結點的指標端由空指標改為指向頭結點,就使整個單鏈表形成一個環,這種頭尾相接的單鏈表稱為單迴圈連結串列,簡稱迴圈連結串列。
迴圈連結串列解決了一個問題:如何從當中一個結點出發,訪問到連結串列的全部結點。
為使空連結串列與非空連結串列處理一致,通常設定一個頭結點。
迴圈連結串列帶有頭結點的空連結串列如圖:
這裡寫圖片描述
非空的迴圈連結串列如圖:
這裡寫圖片描述
迴圈連結串列和單鏈表的主要差異就在於迴圈的條件判斷上,原來是p->next是否為空,現在是p->next不等於頭結點,則迴圈未結束。
如果用頭指標表示迴圈連結串列,則需O(n)時間找到最後一個結點。若改用尾指標表示迴圈連結串列,此時查詢開始結點和終端結點都很方便了。(這是由迴圈連結串列的特點決定的)如圖:
這裡寫圖片描述
此時若尾指標用rear指示,則查詢終端結點時間是O(1),而開始結點,其實就是rear->next->next,其時間複雜也為O(1)。

3.5.1 迴圈連結串列的合併:
合併時,有了尾指標就非常簡單了。如圖:
這裡寫圖片描述
操作如下:
這裡寫圖片描述

p=rearA->next;    /*儲存A表的頭結點,即①*/
rearA->next=rearB->next->next;    /*將本是指向B表的第一個結點(不是頭結點)賦值給reaA->next,即②*/
rearB->next=p;    /*將原A表的頭結點賦值給rearB->next,即③*/
free(p);    /*釋放p*/

以上程式碼free(p);出自書中,筆者認為有誤,應該是釋放rearB->next。敬請讀者發表意見。

3.6 雙向連結串列
在單鏈表的每個結點中,再設定一個指向其前驅結點的指標域。所以在雙向連結串列中的結點都有兩個指標域,一個指向直接後繼,另一個指向直接前驅。
雙向連結串列的迴圈帶頭結點的空連結串列如圖:
這裡寫圖片描述
非空的迴圈的帶頭結點的雙向連結串列如圖:
這裡寫圖片描述
其中某個結點的前驅的後繼是自身,後繼的前驅也是自身。

3.6.1 插入
注意順序:
這裡寫圖片描述
順序是:先搞定s的前驅和後繼,再搞定後結點的前驅,最後解決前結點的後繼
s->prior=p;//①
s->next=p->next;//②
p->next->prior=s;//③
p->next=s;//④

3.6.2 刪除
這裡寫圖片描述
p->prior->next=p->next;//①
p->next->prior=p->prior;//②

3.6.3 特點
由於多了prior指標,對於插入和刪除操作要注意。因為每個結點是兩份指標,所以在空間上是要佔用略多。不過因為良好的對稱性,使得對某個結點的前後結點的操作帶來了方便,可以有效提高演算法的時間效能。即,用空間換時間

3.7 總結
線性表
—順序儲存結構
—鏈式儲存結構(單鏈表、靜態連結串列、迴圈連結串列、雙向連結串列)

參考:
《大話資料結構》