【數據結構與算法】-(4)雙向鏈表和雙向循環鏈表

1、概念

雙向鏈表函數

是在單鏈表的每一個節點中,再設置一個指向其前驅結點的指針域。工具

因此在雙向鏈表中的結點有兩個指針域,一個指向直接後繼,另外一個指向直接前驅。性能

雙向鏈表

雙向循環鏈表:ui

與雙向鏈表相比,雙向循環鏈表的尾結點的next指向頭結點,頭結點的prior 指向尾結點,造成一個循環。以下圖所示:spa

雙向鏈表

建立代碼以下:指針

#define ElemType int;
typedef struct DulNode {
   	ElemType data;
		struct DulNode *prior /*直接前驅指針*/ struct DulNode *next /*直接後繼指針*/ } DulNode, *DuLinkList;
複製代碼

2、雙向鏈表的操做

2.1 建立鏈表

建立雙向鏈表的步驟以下:code

  1. 建立空鏈表L,以及結點*L
  2. 指定一個尾結點爲p
  3. 建立臨時結點temp,並進行數值給定
  4. 將當前尾結點ptemp進行雙線連接連接
    1. tempp 的後繼
    2. ptemp 的前驅
  5. temp 賦值給pp依然爲鏈表的尾結點。
// ① 建立*L 指向頭結點
    *L = (LinkList)malloc(sizeof(Node));
    if (*L == NULL) return ERROR;
    (*L)->prior = NULL;
    (*L)->next = NULL;
    (*L)->data = -1;
    
    //② 新建一個尾結點 p
    LinkList p = *L;
    for(int i=0; i < 10;i++){
        
        // ③ 建立1個臨時的結點
        LinkList temp = (LinkList)malloc(sizeof(Node));
        temp->prior = NULL;
        temp->next = NULL;
        temp->data = i;
        
        // ④ 爲新增的結點創建雙向鏈表關係
          // 1 temp 是p的後繼
        p->next = temp;
          // 2 temp 的前驅是p
        temp->prior = p;
     // ⑤ p 要記錄最後的結點的位置,方便下一次插入 
        p = p->next;
        
    }
複製代碼

2.2 增長結點

向雙向鏈表種添加結點的步驟和單向鏈表添加結點相似,只是多了一步連接前驅的工做。cdn

步驟以下:blog

  1. 新建目標結點temp
  2. 建立指針 p ,指向鏈表的頭結點
  3. 經過循環便利,將 p 向後移,找到插入位置 i 的結點
    1. 【判斷】若是插入位置超出鏈表自己長度,跳出
    2. 【判斷】若是 p 爲鏈表尾部,只需作 ptemp首尾相連
  4. 找到i 結點後,分兩步對ptemp 進行首尾相連
    1. 將原pnextprior 指向 目標結點temp
    2. 將目標結點的next 指向 pnext
    3. pnext 指向目標結點 temp
    4. 將 目標結點 tempprior 指向 p

**注意:**這裏的第四步中,第1,2兩步必須先於3,4兩步執行,不然先將ptemp,關聯上,會致使原pnext 丟失,成爲野指針。源碼

畫個圖表示一下流程:

雙向鏈表-插入結點

Status ListInsert(LinkList *L, int i, ElemType data){
    
    //1. 插入的位置不合法 爲0或者爲負數
    if(i < 1) return ERROR;
    
    //2. 新建結點
    LinkList temp = (LinkList)malloc(sizeof(Node));
    temp->data = data;
    temp->prior = NULL;
    temp->next = NULL;
    
    //3.將p指向頭結點!
    LinkList p = *L;
    
    //4. 找到插入位置i直接的結點
    for(int j = 1; j < i && p;j++)
        p = p->next;
    
    //5. 若是插入的位置超過鏈表自己的長度
    if(p == NULL){
        return  ERROR;
    }
    //6. 判斷插入位置是否爲鏈表尾部;
    if (p->next == NULL) {
        
        p->next = temp;
        temp->prior = p;
    }
  	else
    {
        //1️⃣ 將p->next 結點的前驅prior = temp
        p->next->prior = temp;
        //2️⃣ 將temp->next 指向原來的p->next
        temp->next = p->next;
        //3️⃣ p->next 更新成新建立的temp
        p->next = temp;
        //4️⃣ 新建立的temp前驅 = p
        temp->prior = p;
    }
    
    return  OK;
}
複製代碼

2.3 刪除結點

刪除鏈表中的結點分兩種:刪除指定位置結點和刪除指定元素的結點。其思路都是一致的,遍歷鏈表中的元素,找到指定元素,並進行刪除。主要流程與刪除單向鏈表的邏輯相似,只是多了一個移除前驅結點的操做。

刪除結點的通俗理解:就比如員工離職前,必要的一步就是工做交接,告訴你們接下來工做時誰來接手,鍋該由誰來背,公司才能正常運行;不然你一拍屁股刪庫跑路了,公司可就熱鬧了,你們都抓瞎了,這樣就亂套了。

2.3.1 刪除指定位置結點

流程以下:

  1. 給定一個工具結點p,指向鏈表的頭結點
  2. 依次循環查找,將 p 指向刪除位的前一個
  3. 建立臨時結點 temp 指向要刪除的結點,並把該結點 data 賦值給返回的 *e
  4. 待刪除結點上一結點的 next 指向 temp 的後一個結點
  5. 待刪除結點是否鏈尾結點?
    • 並不是鏈尾:將待刪除結點 temp 的下一結點的 prior 指向工具結點p
    • 是鏈尾:不做處理
  6. 釋放待刪除結點 temp

總結一下,核心操做就兩步:

  • 將目標上一個結點的 next 指給下一個
  • 將目標的下一個結點的 prior 指給上個結點

有圖有真相:

雙向鏈表-刪除指定位置結點

貼一下實操的代碼以下:

Status DeleteVeryNode(LinkList *L, int i, ElemType *e){
    int k = 1;
    LinkList p = (*L);
    //1.判斷雙向鏈表是否爲空,若是爲空則返回ERROR;
    if (*L == NULL) {
        return ERROR;
    }
    //2. 將指針p移動到刪除元素位置前一個
    while (k < i && p != NULL) {
        p = p->next;
        k++;
    }
    //3.若是k>i 或者 p == NULL 則返回ERROR
    if (k>i || p == NULL) {
        return  ERROR;
    }
    //4.建立臨時指針temp 指向要刪除的結點,並將要刪除的結點的data 賦值給*e,帶回到main函數
    LinkList temp = p->next;
    *e = temp->data;
    //5. p->next 等於要刪除的結點的下一個結點
    p->next = temp->next;
    //6. 若是刪除結點的下一個結點不爲空,則將將要刪除的下一個結點的前驅指針賦值p;
    if (temp->next != NULL) {
        temp->next->prior = p;
    }
    //7.刪除temp結點
    free(temp);
    return OK;
}
複製代碼

2.3.2 刪除指定元素的結點

刪除指定元素的結點,會更簡單,只須要遍歷循環,找到相應的結點後,依次對前驅點和後繼點進行從新配置。

步驟以下:

  1. 建立鏈表L, 將p 指向首結點。
  2. 遍歷鏈表L,判斷給定元素 datap-> data是否相等,相等即找到目標結點
  3. 修改目標結點的前驅結點後繼指針,指向目標結點下一個結點(交代後事....)
  4. 若刪除結點非尾結點:修改目標結點後繼結點的前驅指針 prior,指向目標的上一個結點。
  5. 釋放被刪除的結點 p

雙向鏈表-刪除指定元素結點

貼一下實操的代碼以下:

Status DeleteDefinedNode(LinkList *L, int data){
    // 1. 建立鏈表,以及頭結點 p
    LinkList p = *L;
    //1.遍歷雙向循環鏈表
    while (p) {
        //2.判斷當前結點的數據域和data是否相等,若相等則刪除該結點
        if (p->data == data) {
            //修改被刪除結點的前驅結點的後繼指針
            p->prior->next = p->next;
            //修改被刪除結點的後繼結點的前驅指針
            if(p->next != NULL){
                p->next->prior = p->prior;
            }
            //釋放被刪除結點p
            free(p);
            //退出循環
            break;
        }
        //沒有找到該結點,則繼續移動指針p
        p = p->next;
    }
    return OK;
}
複製代碼

2.4 查詢結點

  1. 建立鏈表L, 將p 指向首結點。
  2. 遍歷鏈表L,判斷給定元素 datap-> data是否相等,
    1. 相等即找到目標結點,跳出。
    2. 不然繼續循環,將 p 移動到下一個結點
int selectElem(LinkList L,ElemType elem){
    LinkList p = L->next;
    int i = 1;
    while (p) {
        if (p->data == elem) {
            return i;
        }
        i++;
        p = p->next;
    }
    return  -1;
}
複製代碼

2. 5 更新結點

更新結點,只須要遍歷循環鏈表,找到序號內的結點,將其數據域 data 替換爲新的數據

Status replaceLinkList(LinkList *L,int index,ElemType newElem){
    LinkList p = (*L)->next;
    
    for (int i = 1; i < index; i++) {
        p = p->next;
    }
    
    p->data = newElem;
    return OK;
}
複製代碼

3、雙向循環鏈表的操做

3.1 建立鏈表

雙向循環鏈表的建立步驟,與雙向鏈表相似。差異在於:多了將尾結點的 next 指向 頭結點,而頭結點的 prior 指向 尾結點。

具體步驟:

  1. 建立空鏈表L,以及結點*L,使得其前驅和後繼都指向本身
  2. 指定一個尾結點爲p
  3. 建立臨時結點temp,並進行數值給定
  4. 將當前尾結點ptemp進行雙線連接連接
    1. tempp 的後繼
    2. ptemp 的前驅
    3. temp 的後繼是 p
    4. p 的前驅是新建的 temp
  5. temp 賦值給pp依然爲鏈表的尾結點,方便下次插入新結點

實現源碼以下:

Status creatCircularLinkList(LinkList *L){
    // 1 建立空鏈表
    *L = (LinkList)malloc(sizeof(Node));
    if (*L == NULL) {
        return ERROR;
    }
    (*L)->next = (*L);
    (*L)->prior = (*L);
    // 指定一個尾結點爲 p
    LinkList p = *L;
    for(int i=0; i < 10;i++){
        //1.建立1個臨時的結點
        LinkList temp = (LinkList)malloc(sizeof(Node));
        temp->data = i;
        //2.爲新增的結點創建雙向鏈表關係
        //① temp 是p的後繼
        p->next = temp;
        //② temp 的前驅是p
        temp->prior = p;
        //③ temp的後繼是*L
        temp->next = (*L);
        //④ p 的前驅是新建的temp
        p->prior = temp;
        //⑤ p 要記錄最後的結點的位置,方便下一次插入
        p = p->next;
    }
    return OK;
}
複製代碼

3.2 增長結點

與雙向鏈表類似,區別在於雙向循環鏈表因爲有首位域,在找到指定位置後,須要先將插入結點的priornext 與 先後創建關係,以後再考慮前結點的 next連接,最後考慮的是 目標結點的next

具體步驟以下:

  1. 新建目標結點temp
  2. 建立指針 p ,指向鏈表的頭結點
  3. 經過循環便利,將 p 向後移,找到插入位置 i 的結點
    1. 【判斷】若是插入位置超出鏈表自己長度,跳出
    2. 【判斷】若是 pnext指向 頭結點跳出
  4. 找到i 結點後,分兩步對ptemp 進行首尾相連
    1. tempprior 指向 p
    2. tempnext 指向 pnext
    3. pnext 指向目標結點 temp
    4. 判斷 temp 是不是尾結點
      1. 若是是:將頭結點的 prior 指向 temp
      2. 若是否:將目標結點 tempprior 指向 p
Status LinkCircularListInsert(LinkList *L, int index, ElemType e){
   
    //1. 建立指針p,指向雙向鏈表頭
    LinkList p = (*L);
    int i = 1;
    
    //2.雙向循環鏈表爲空,則返回error
    if(*L == NULL) return ERROR;
   
    //3.找到插入前一個位置上的結點p
    while (i < index && p->next != *L) {
        p = p->next;
        i++;
    }
    
    //4.若是i>index 則返回error
    if (i > index)  return ERROR;
    
    //5.建立新結點temp
    LinkList temp = (LinkList)malloc(sizeof(Node));
    
    //6.temp 結點爲空,則返回error
    if (temp == NULL) return ERROR;
    
    //7.將生成的新結點temp數據域賦值e.
    temp->data = e;
    
    //8.將結點temp 的前驅結點爲p;
    temp->prior = p;
    //9.temp的後繼結點指向p->next;
    temp->next = p->next;
    //10.p的後繼結點爲新結點temp;
    p->next = temp;
    
    //若是temp 結點不是最後一個結點
    if (*L != temp->next) {
        //11.temp節點的下一個結點的前驅爲temp 結點
        temp->next->prior = temp;
    }else{
        (*L)->prior = temp;
        
    }
    return OK;
}
複製代碼

3.3 刪除結點

雙向循環鏈表的結點刪除,比雙向鏈表的簡單,由於鏈表首尾相連的特性,不須要考慮是否爲尾結點或者頭結點。

流程以下:

  1. 給定一個工具結點p,指向鏈表的頭結點
  2. 依次循環查找,將 p 指向刪除位的前一個
  3. 建立臨時結點 temp 指向要刪除的結點,並把該結點 data 賦值給返回的 *e
  4. 待刪除結點上一結點的 next 指向 temp 的後一個結點
  5. 釋放待刪除結點 temp

總結一下,核心操做就兩步:

  • 將目標上一個結點的 next 指給下一個
  • 將目標的下一個結點的 prior 指給上個結點
Status LinkListDelete(LinkList *L,int index,ElemType *e){
    int i = 1;
    LinkList temp = (*L)->next;
    if (*L == NULL) {
        return  ERROR;
    }
    //①.若是刪除到只剩下首元結點了,則直接將*L置空;
    if(temp->next == *L){
        free(*L);
        (*L) = NULL;
        return OK;
    }
    //1.找到要刪除的結點
    while (i < index) {
        temp = temp->next;
        i++;
    }
    //2.給e賦值要刪除結點的數據域
    *e = temp->data;
    //3.修改被刪除結點的前驅結點的後繼指針 
    temp->prior->next = temp->next;
    //4.修改被刪除結點的後繼結點的前驅指針
    temp->next->prior = temp->prior;
    //5. 刪除結點temp
    free(temp);
    return OK;
}
複製代碼

3.4 查詢結點

與雙向鏈表相同,更新結點,只須要遍歷循環鏈表,找到序號內的結點,將其數據域 data 替換爲新的數據

代碼參見 2.5

4、總結

4.1 線性表結構

線性表·結構

4.2 順序表與鏈表比較

順序結構的線性表與鏈式結構的線性表比較起來,

從空間性能上比較

  1. 存儲空間分配

  2. 存儲密度的大小

    存儲密度 = \frac{數據元素自己佔用存儲量}{結點結構佔用的存儲量}

時間性能比較:

  1. 存儲元素的效率
  2. 插入和刪除操做的效率

4.3 其餘

雙向鏈表比起單向鏈表來講,增長了prior 這一指針域,能夠在查找和刪除時,極其迅速的操做,無須作更多的判斷,極大地提升了運算效率。同時在操做時,須要注意指針修改順序,以避免造成野指針。

相關文章
相關標籤/搜索