【數據結構】線性表之“雙鏈表(帶頭循環雙向鏈表)”

- 第 99 篇 -
Date: 2025 - 05 - 25
Author: 鄭龍浩/仟墨
【數據結構】
續上一篇: 線性表之“單鏈表”

文章目錄

  • “雙鏈表(帶頭雙向循環鏈表)” 的實現:
  • 分步解釋所有函數:
  • test.c
  • DListNode.h
  • DListNode.c

“雙鏈表(帶頭雙向循環鏈表)” 的實現:

  • 帶頭:帶頭結點
  • 循環:鏈表中的結點是循環的,頭結點前驅是最后一個結點,最后一個結點后繼是頭結點
  • 雙向:每一個結點都有存儲前驅結點和后繼結點的指針域

分步解釋所有函數:

  1. 申請新的結點

    封裝這個函數是因為在后邊插入的時候多次使用了下面的代碼,造成了代碼冗余,所以將申請新結點的代碼封裝起來,用的時候直接調用即可

    1. malloc申請新的空間,寬度為 sizef(DListNode)
    2. 判斷malloc內存分配是否成功,失敗則退出(概論很小)
    3. 設置結點數據域,將元素 x 存入該結點的 data
    4. 初始化指針域,前驅后繼全部都指向自己(完成閉環)
    5. 返回申請的結點的地址
    // 1 申請新的結點
    DListNode* BuyDListNode(ElemType x) {DListNode* NewNode = (DListNode*)malloc(sizeof(DListNode)); // 申請新的結點if (NewNode == NULL) {printf("內存申請失敗!\n");exit(EXIT_FAILURE);}NewNode->data = x; // 存儲數據NewNode->prev = NewNode;  // 初始時指向自己NewNode->next = NewNode;  // 初始時指向自己return NewNode;
    }
    
  2. 帶頭結點的雙向循環鏈表初始化 – 創建并返回頭結點,即返回哨兵結點

    這個函數的作用和 BuyDListNode 很像,但是意思上不一樣!

    • DListInit() 有很強的語義約束 –> 專門用于初始化,而非開辟新的結點的內存空間

    • 返回的頭結點指針具有特殊作用(哨兵結點),不是普通節點(普通結點的data存儲數據,而哨兵結點的data不存儲數據,默認為0

      返回創建的頭結點指針,該頭結點代表一個空鏈表

    • 寫函數的時候,可以調用申請結點的函數來實現頭節點的創建

    步驟:

    1. 調用 BuyDListNode(0) 創建頭結點(data 無實際意義)
    2. 由于 BuyDListNode 已讓 prevnext 指向自己,無需額外操作
    3. 返回頭結點指針
    // 2 帶頭結點的雙向循環鏈表初始化 -- 創建并返回頭結點,即返回哨兵結點
    DListNode* DListInit() {DListNode* NewNode = BuyDListNode(0); // 申請新的結點 // 頭結點的 data 無意義,僅作哨兵// 因為BuyDListNode(0_中已經指向了自己所以下面代碼可寫可不寫//NewNode->prev = NewNode; // 新結點前驅指向自己//NewNode->next = NewNode; // 新結點后繼指向自己return NewNode; // 返回頭結點(哨兵結點)
    }
    
  3. 雙向鏈表清除 – 只釋放頭節點以外的結點

    清除的意思是保留雙鏈表本身,但是雙鏈表中保存數據的結點要清除(不清除頭結點)

    步驟:

    1. 檢查頭結點有效性(assert),若phead中存儲的地址為NULL,則試圖訪問phead->next會導致程序崩潰,所以需要攔截非法操作
    2. 釋放哨兵結點以外的結點(存儲數據的結點)
    3. 初始化指針域,將哨兵結點的前驅和后繼全部指向自己,完成閉環
    // 3 雙向鏈表清除 -- 只釋放頭節點以外的結點
    void DListClear(DListNode* phead) {assert(phead); // 防止空指針DListNode* cur = phead->next; // 存放當前的結點的地址while (cur != phead) { // 釋放處頭結點(哨兵結點)以外的結點DListNode* next = cur->next; // 保存下一個結點地址(防止釋放后地址丟失)free(cur); // 釋放當前結點內存空間cur = next; // 指向下一個結點}cur->next = cur->prev = cur;
    }
    
  4. 雙向鏈表銷毀

    DListClear基礎上增加頭結點內存釋放

    但是銷毀的時候要用到二級指針,因為二級指針才能將存放頭結點地址的二級指針變量置為NULL

    步驟:

    1. 檢查頭結點有效性(assert),phead中存儲的地址為NULL,則試圖訪問phead->next會導致程序崩潰,所以需要攔截非法操作
    2. 釋放哨兵結點以外的結點(存儲數據的結點),使用DListClear
    3. 釋放頭結點內存空間
    4. phead初始化,即phead中存儲的地址為NULL
    // 4 雙向鏈表銷毀 -- 銷毀要用雙指針
    void DListDestroy(DListNode** phead) {assert(*phead); // 防止空指針DListClear(*phead); // 只釋放頭節點以外的結點free(*phead); // 釋放頭結點*phead = NULL; // 重新置為NULL
    }
    
  5. 頭插

    頭插操作是在 哨兵結點(頭結點)與 第一個結點之間插入一個新的結點,并且將新的結點與頭結點和原第一個結點連接起來

    步驟:

    1. 檢查頭結點有效性(assert),若phead中存儲的地址為NULL,則試圖訪問phead->next會導致程序崩潰,所以需要攔截非法操作
    2. 申請新的結點
    3. 新結點前驅指向頭結點
    4. 新節點后繼指向原第一個結點
    5. 原第一結點前驅指向新的結點
    6. 頭結點后繼指向新的結點
    // 5 頭插
    void DListPushFront(DListNode* phead, ElemType x) {assert(phead != NULL); // 避免空指針,確保phead不是NULL// 因為帶頭雙向循環鏈表是有頭結點的,而頭結點不存儲數據始終為NULL,即不存在頭結點給頭結點添加數據的情況DListNode* NewNode = BuyDListNode(x); // 申請新的結點// 頭插就是讓新的結點插到 “哨兵結點(頭結點)”與“第一個結點”之間DListNode* first = phead->next; // 表示第一個結點NewNode->prev = phead;	// 1 新結點前驅指向頭結點NewNode->next = first;	// 2 新節點后繼指向原第一個結點first->prev = NewNode;	// 3 原第一結點前驅指向新的結點phead->next = NewNode;	// 4 頭結點后繼指向新的結點
    }
    
  6. 尾插

    尾插操作就是在最后一個結點后邊插入一個新的結點,并且將新節點與原最后一個結點和頭結點連接起來

    步驟:

    1. 檢查頭結點有效性(assert),若phead中存儲的地址為NULL,則試圖訪問phead->next會導致程序崩潰,所以需要攔截非法操作
    2. 申請新的結點
    3. 新節點前驅指向尾節點
    4. 新節點后繼指向頭節點
    5. 尾節點后繼指向新節點
    6. 頭節點前驅指向新節點
    // 6 尾插
    void DListPushBack(DListNode* phead, ElemType x) {assert(phead != NULL); // 避免空指針,確保phead不是NULLDListNode* NewNode = BuyDListNode(x); // 申請新的結點// 尾插就是將新的結點放到 tail 的后邊DListNode* tail = phead->prev; // 尾結點NewNode->prev = tail;	// 1 新節點前驅指向尾節點NewNode->next = phead;	// 2 新節點后繼指向頭節點tail->next = NewNode;	// 3 尾節點后繼指向新節點phead->prev = NewNode;	// 4 頭節點前驅指向新節點
    }
    
  7. 頭刪

    頭刪就是將第一個結點(不是刪除哨兵結點)的內存空間釋放,并且將頭結點與原第二個結點連接起來

    步驟:

    1. 檢查頭結點有效性(assert),若phead中存儲的地址為NULL,則試圖訪問phead->next會導致程序崩潰,所以需要攔截非法操作
    2. 檢查鏈表是否只有一個頭結點(哨兵結點),如果是的話,則不刪除,直接return
    3. 頭結點后繼指向第二個結點
    4. 第二個結點前驅指向頭結點
    5. 釋放第一個結點
    // 7 頭刪
    void DListPopFront(DListNode* phead) {assert(phead != NULL); // 避免空指針,確保phead不是NULLif (phead->next == phead) return; // 如果只有頭結點(哨兵結點),頭結點中不存儲data,不刪除,直接返回// 切記:頭刪不是刪除“頭結點(哨兵結點)”,而是刪除第一個結點DListNode* first = phead->next; // 第一個結點(保護要釋放的結點的地址,不然重新指向以后,地址丟失)DListNode* second = first->next; // 第二個結點phead->next = second;	// 1 頭結點后繼指向第二個結點second->prev = phead;	// 2 第二個結點前驅指向頭結點free(first);			// 3 釋放第一個結點
    }
    
  8. 尾刪

    尾刪就是將最后一個結點內存空間釋放, 并且將原倒數第二個結點與頭結點連接起來

    步驟:

    1. 檢查頭結點有效性(assert),若phead中存儲的地址為NULL,則試圖訪問phead->next會導致程序崩潰,所以需要攔截非法操作
    2. 檢查鏈表是否只有一個頭結點(哨兵結點),如果是的話,則不刪除,直接return
    3. 頭結點前驅指向倒數第二個結點
    4. 倒數第二個結點后繼指向頭結點
    5. 釋放尾結點內存空間
    // 8 尾刪
    void DListPopBack(DListNode* phead) {assert(phead != NULL); // 避免空指針,確保phead不是NULLif (phead->next == phead) return; // 如果只有頭結點(哨兵結點),頭結點中不存儲data,不刪除,直接返回DListNode* tail = phead->prev; // 尾結點(保護要釋放的結點的地址,不然重新指向以后,地址丟失)DListNode* TailPrev = tail->prev; // 倒數第二個結點phead->prev = TailPrev;	// 1 頭結點前驅指向倒數第二個結點TailPrev->next = phead;	// 2 倒數第二個結點后繼指向頭結點free(tail);				// 3 釋放尾結點
    }
    
  9. 查找 找到返回結點地址,找不到返回NULL

    從第一個結點(不是哨兵結點)到 最后一個結點找結點

    步驟:

    1. 檢查頭結點有效性(assert),若phead中存儲的地址為NULL,則試圖訪問phead->next會導致程序崩潰,所以需要攔截非法操作
    2. 從第一個結點找到最后一個結點,如果找到了datax的結點,就返回該結點的地址
    3. 沒找到則返回NULL
    // 9 查找 找到返回結點地址,找不到返回NULL
    DListNode* DListFind(DListNode* phead, ElemType x) {assert(phead); // 避免空指針,確保phead不是NULL// 切記:頭結點不存儲數據,查找無需找頭結點DListNode* cur = phead->next;// 從第一個結點到最后一個結點 (結束條件為:cur 走到了頭結點)while (cur != phead) {if (cur->data == x) {return cur; // 找到了,返回cur地址}cur = cur->next; // 指向下一個結點}return NULL; // 若沒找到,則返回NULL
    }
    
  10. 在 pos 的前面進行插入

    pos 前面插入 數據為 x 的結點,就是在 pos 前驅與 pos 之間插入數據,并將新節點 與 pos 前驅后繼連接起來

    步驟:

    1. 檢查頭結點有效性(assert),若phead中存儲的地址為NULL,則試圖訪問phead->next會導致程序崩潰,所以需要攔截非法操作
    2. 驗證pos是否屬于某個鏈表(通過檢查閉環)
    3. 申請新的結點
    4. 新結點的前驅指向 原pos前驅結點
    5. 新節點的后繼指向 pos結點
    6. 原pos的前驅結點后繼指向 新的結點
    7. 原pos的前驅指向 新的結點
    // 10 在pos的前面進行插入(不包括頭結點,因為頭結點不存儲數據)
    void DListInsert(DListNode* pos, ElemType x) {assert(pos != NULL); // 避免空指針,檢查pos指針是否為NULL// 驗證pos是否屬于某個鏈表(通過檢查閉環)assert(pos->next != pos && pos->prev != pos);  // 確保不是孤立節點DListNode* NewNode = BuyDListNode(x); // 申請新的結點// pos 前面插入 數據為 x 的結點,就是在 pos 前驅與 pos 之間插入數據DListNode* PosPrev = pos->prev; // pos 的前驅結點NewNode->prev = PosPrev;	// 1 新結點的前驅指向 原pos前驅結點NewNode->next = pos;	// 2 新節點的后繼指向 pos結點PosPrev->next = NewNode;	// 3 原pos的前驅結點后繼指向 新的結點pos->prev = NewNode;		// 4 原pos的前驅指向 新的結點
    }
    
  11. 刪除 pos 位置的節點

    刪除pos結點就是將pos的前驅結點和后繼結點連接起來,并且釋放pos結點

    步驟:

    1. 檢查頭結點有效性(assert),若phead中存儲的地址為NULL,則試圖訪問phead->next會導致程序崩潰,所以需要攔截非法操作
    2. 驗證pos是否屬于某個鏈表(通過檢查閉環)
    3. 新結點的前驅指向 原pos前驅結點
    4. 新節點的后繼指向 pos結點
    5. 原pos的前驅結點后繼指向 新的結點
    6. 原pos的前驅指向 新的結點
    // 11 刪除pos位置的結點(不包括頭結點,因為頭結點不存儲數據)
    void DListErase(DListNode* pos) {assert(pos != NULL); // 避免空指針,檢查pos指針是否為NULL && 斷言指向頭結點(哨兵結點)// 驗證pos是否屬于某個鏈表(通過檢查閉環)assert(pos->next != pos && pos->prev != pos);  // 確保不是孤立節點// 刪除pos結點就是將pos的前驅結點和后繼結點連接起來,并且釋放pos結點pos->prev->next = pos->next;	// 1 pos的前驅結點后繼指向pos的后繼結點pos->next->prev = pos->prev;	// 2 pos的后繼結點前驅指向pos的前驅結點free(pos);						// 3 釋放 pos 結點的內存空間
    }
    
  12. 打印

    從第一個結點打印到最后一個結點

    步驟:

    1. 檢查頭結點有效性(assert),若phead中存儲的地址為NULL,則試圖訪問phead->next會導致程序崩潰,所以需要攔截非法操作
    2. 驗證pos是否屬于某個鏈表(通過檢查閉環)
      1. 檢查是否只有一個結點(哨兵結點),如果是的話則打印Empty List,并且跳過后邊代碼,直接return
    3. 從第一個結點打印到最后一個結點(結束條件為cur為哨兵結點「頭結點」)
    4. 打印結尾HEAD
    // 12 打印
    void DListPrint(DListNode* phead) {assert(phead); // 避免空指針,檢查頭指針是否為NULLDListNode* cur = phead->next; // 存儲當前結點地址// 若是頭結點if (phead->next == phead) {printf("Empty List\n");return;}// 從第一個結點打印到最后一個結點(結束條件為cur為哨兵結點「頭結點」)while (cur != phead) {printf("%d -> ", cur->data); // 打印當前結點的數據cur = cur->next; // 指向下一個結點}printf("HEAD\n"); // 最后打印結束
    }
    

test.c

#define _CRT_SECURE_NO_WARNINGS
#include "DListNode.h"
void Check1();
int main(void) {Check1();return 0;
}// 測試1 
void Check1() {// 創建鏈表DListNode* L = DListInit(); // 新建鏈表 -- 必須要讓L頭結點為DListInit(),如果指向NULL會越界訪問// 尾插 1 ~ 5printf("尾插1 2 3 4 5\n");for (int i = 1; i <= 5; i++) {DListPushBack(L, i); // 每次尾插}DListPrint(L); // 打印鏈表// 頭插 1 ~ 5printf("頭插11 22 33 44 55\n");for (int i = 1; i <= 5; i++) {DListPushFront(L, i*10 + i); // 每次頭插}DListPrint(L); // 打印鏈表// 頭刪 5 次printf("頭刪 5 次\n");for (int i = 1; i <= 5; i++) {DListPopFront(L); // 每次頭刪}DListPrint(L); // 打印鏈表// 尾刪 2 次printf("尾刪 2 次\n");for (int i = 1; i <= 2; i++) {DListPopBack(L); // 每次尾插}DListPrint(L); // 打印鏈表// 在 3 之前插入 33printf("在 3 之前插入 33\n");DListInsert(DListFind(L, 3), 23);DListPrint(L); // 打印鏈表// 刪除數據為 23 的結點printf("刪除數據為 23 的結點\n");DListErase(DListFind(L, 23));DListPrint(L); // 打印鏈表// 清除雙鏈表(保留哨兵)printf("清除雙鏈表(保留哨兵)\n");DListClear(L);if (L->next == L && L->prev == L) {printf("鏈表已經清除\n");}// 銷毀雙鏈表printf("銷毀雙鏈表(不保留哨兵)\n");DListDestroy(&L);if (L == NULL) {printf("鏈表已經銷毀\n");}
}

DListNode.h

#define _CRT_SECURE_NO_WARNINGS
#include "stdio.h"
#include "stdlib.h"
#include "assert.h"
typedef int ElemType;
typedef struct DListNode {ElemType data; // 數據struct DListNode* prev; // 指向前驅節點(前驅結點的地址)struct DListNode* next; // 指向后驅結點(后驅結點的地址)
}DListNode;// 申請新的結點
DListNode* BuyDListNode(ElemType x);
// 帶頭結點的雙向循環鏈表初始化 -- 創建并返回頭結點,即返回哨兵結點
DListNode* DListInit();
// 雙向鏈表清除 -- 只釋放頭節點以外的結點
void DListClear(DListNode* phead);
// 雙向鏈表銷毀
void DListDestroy(DListNode** phead);
// 頭刪頭插要重新更新頭指針
// 頭插
void DListPushFront(DListNode* phead, ElemType x);
// 尾插
void DListPushBack(DListNode* phead, ElemType x);
// 頭刪
void DListPopFront(DListNode* phead);
// 尾刪
void DListPopBack(DListNode* phead);
// 查找 找到返回結點地址,找不到返回NULL
DListNode* DListFind(DListNode* phead, ElemType x);
// 在phead的前面進行插入
void DListInsert(DListNode* pos, ElemType x);
// 刪除phead位置的節點
void DListErase(DListNode* pos);
// 打印
void DListPrint(DListNode* phead);

DListNode.c

#define _CRT_SECURE_NO_WARNINGS
#include "DListNode.h"// 1 申請新的結點
DListNode* BuyDListNode(ElemType x) {DListNode* NewNode = (DListNode*)malloc(sizeof(DListNode)); // 申請新的結點if (NewNode == NULL) {printf("內存申請失敗!\n");exit(EXIT_FAILURE);}NewNode->data = x; // 存儲數據NewNode->prev = NewNode;  // 初始時指向自己NewNode->next = NewNode;  // 初始時指向自己return NewNode;
}
// 2 帶頭結點的雙向循環鏈表初始化 -- 創建并返回頭結點,即返回哨兵結點
DListNode* DListInit() {DListNode* NewNode = BuyDListNode(0); // 申請新的結點 // 頭結點的 data 無意義,僅作哨兵// 因為BuyDListNode(0_中已經指向了自己所以下面代碼可寫可不寫//NewNode->prev = NewNode; // 新結點前驅指向自己//NewNode->next = NewNode; // 新結點后繼指向自己return NewNode; // 返回頭結點(哨兵結點)
}
// 3 雙向鏈表清除 -- 只釋放頭節點以外的結點
void DListClear(DListNode* phead) {assert(phead); // 防止空指針DListNode* cur = phead->next; // 存放當前的結點的地址while (cur != phead) { // 釋放處頭結點(哨兵結點)以外的結點DListNode* next = cur->next; // 保存下一個結點地址(防止釋放后地址丟失)free(cur); // 釋放當前結點內存空間cur = next; // 指向下一個結點}cur->next = cur->prev = cur;
}
// 4 雙向鏈表銷毀 -- 銷毀要用雙指針
void DListDestroy(DListNode** phead) {assert(*phead); // 防止空指針DListClear(*phead); // 只釋放頭節點以外的結點free(*phead); // 釋放頭結點*phead = NULL; // 重新置為NULL
}
// 5 頭插
void DListPushFront(DListNode* phead, ElemType x) {assert(phead != NULL); // 避免空指針,確保phead不是NULL// 因為帶頭雙向循環鏈表是有頭結點的,而頭結點不存儲數據始終為NULL,即不存在頭結點給頭結點添加數據的情況DListNode* NewNode = BuyDListNode(x); // 申請新的結點// 頭插就是讓新的結點插到 “哨兵結點(頭結點)”與“第一個結點”之間DListNode* first = phead->next; // 表示第一個結點NewNode->prev = phead;	// 1 新結點前驅指向頭結點NewNode->next = first;	// 2 新節點后繼指向原第一個結點first->prev = NewNode;	// 3 原第一結點前驅指向新的結點phead->next = NewNode;	// 4 頭結點后繼指向新的結點
}
// 6 尾插
void DListPushBack(DListNode* phead, ElemType x) {assert(phead != NULL); // 避免空指針,確保phead不是NULLDListNode* NewNode = BuyDListNode(x); // 申請新的結點// 尾插就是將新的結點放到 tail 的后邊DListNode* tail = phead->prev; // 尾結點NewNode->prev = tail;	// 1 新節點前驅指向尾節點NewNode->next = phead;	// 2 新節點后繼指向頭節點tail->next = NewNode;	// 3 尾節點后繼指向新節點phead->prev = NewNode;	// 4 頭節點前驅指向新節點
}
// 7 頭刪
void DListPopFront(DListNode* phead) {assert(phead != NULL); // 避免空指針,確保phead不是NULLif (phead->next == phead) return; // 如果只有頭結點(哨兵結點),頭結點中不存儲data,不刪除,直接返回// 切記:頭刪不是刪除“頭結點(哨兵結點)”,而是刪除第一個結點DListNode* first = phead->next; // 第一個結點(保護要釋放的結點的地址,不然重新指向以后,地址丟失)DListNode* second = first->next; // 第二個結點phead->next = second;	// 1 頭結點后繼指向第二個結點second->prev = phead;	// 2 第二個結點前驅指向頭結點free(first);			// 3 釋放第一個結點
}
// 8 尾刪
void DListPopBack(DListNode* phead) {assert(phead != NULL); // 避免空指針,確保phead不是NULLif (phead->next == phead) return; // 如果只有頭結點(哨兵結點),頭結點中不存儲data,不刪除,直接返回DListNode* tail = phead->prev; // 尾結點(保護要釋放的結點的地址,不然重新指向以后,地址丟失)DListNode* TailPrev = tail->prev; // 倒數第二個結點phead->prev = TailPrev;	// 1 頭結點前驅指向倒數第二個結點TailPrev->next = phead;	// 2 倒數第二個結點后繼指向頭結點free(tail);				// 3 釋放尾結點
}
// 9 查找 找到返回結點地址,找不到返回NULL
DListNode* DListFind(DListNode* phead, ElemType x) {assert(phead); // 避免空指針,確保phead不是NULL// 切記:頭結點不存儲數據,查找無需找頭結點DListNode* cur = phead->next;// 從第一個結點到最后一個結點 (結束條件為:cur 走到了頭結點)while (cur != phead) {if (cur->data == x) {return cur; // 找到了,返回cur地址}cur = cur->next; // 指向下一個結點}return NULL; // 若沒找到,則返回NULL
}
// 10 在pos的前面進行插入(不包括頭結點,因為頭結點不存儲數據)
void DListInsert(DListNode* pos, ElemType x) {assert(pos != NULL); // 避免空指針,檢查pos指針是否為NULL// 驗證pos是否屬于某個鏈表(通過檢查閉環)assert(pos->next != pos && pos->prev != pos);  // 確保不是孤立節點DListNode* NewNode = BuyDListNode(x); // 申請新的結點// pos 前面插入 數據為 x 的結點,就是在 pos 前驅與 pos 之間插入數據DListNode* PosPrev = pos->prev; // pos 的前驅結點NewNode->prev = PosPrev;	// 1 新結點的前驅指向 原pos前驅結點NewNode->next = pos;	// 2 新節點的后繼指向 pos結點PosPrev->next = NewNode;	// 3 原pos的前驅結點后繼指向 新的結點pos->prev = NewNode;		// 4 原pos的前驅指向 新的結點
}
// 11 刪除pos位置的結點(不包括頭結點,因為頭結點不存儲數據)
void DListErase(DListNode* pos) {assert(pos != NULL); // 避免空指針,檢查pos指針是否為NULL && 斷言指向頭結點(哨兵結點)// 驗證pos是否屬于某個鏈表(通過檢查閉環)assert(pos->next != pos && pos->prev != pos);  // 確保不是孤立節點// 刪除pos結點就是將pos的前驅結點和后繼結點連接起來,并且釋放pos結點pos->prev->next = pos->next;	// 1 pos的前驅結點后繼指向pos的后繼結點pos->next->prev = pos->prev;	// 2 pos的后繼結點前驅指向pos的前驅結點free(pos);						// 3 釋放 pos 結點的內存空間
}
// 12 打印
void DListPrint(DListNode* phead) {assert(phead); // 避免空指針,檢查頭指針是否為NULLDListNode* cur = phead->next; // 存儲當前結點地址// 若是頭結點if (phead->next == phead) {printf("Empty List\n");return;}// 從第一個結點打印到最后一個結點(結束條件為cur為哨兵結點「頭結點」)while (cur != phead) {printf("%d -> ", cur->data); // 打印當前結點的數據cur = cur->next; // 指向下一個結點}printf("HEAD\n"); // 最后打印結束
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/907395.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/907395.shtml
英文地址,請注明出處:http://en.pswp.cn/news/907395.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【學習筆記】Transformer

學習的博客&#xff08;在此致謝&#xff09;&#xff1a; 初識CV - Transformer模型詳解&#xff08;圖解最完整版&#xff09; 1 整體結構 Transformer由Encoder和Decoder組成&#xff0c;分別包含6個block。 Transformer的工作流程大體如下&#xff1a; 獲取每個單詞的em…

[MMU]IOMMU的主要職能及詳細的驗證方案

IOMMU的主要職能及詳細的驗證方案 摘要&#xff1a;IOMMU&#xff08;Input/Output Memory Management Unit&#xff09;是一種硬件組件&#xff0c;負責管理I/O設備對內存的直接訪問&#xff08;DMA&#xff0c;Direct Memory Access&#xff09;&#xff0c;其主要作用是提供…

動物類 如何使用Yolov11訓練使用牛羊數據集 實現對牛羊進行檢測數據集

牛羊檢測數據集 3700張 平視視角牛羊檢測 帶標注 voc yolo 牛羊檢測數據集 3700張 牛羊檢測平視 帶標注 voc yolo 分類名: (圖片張數&#xff0c;標注個數) cattle: (1395&#xff0c;4309) sheep: (2393&#xff0c;1 1205) 總數: (3791&#xff0c; 15514) 總類(nc): 2類 以…

搭建frp內網穿透

前言 內網穿透的原理我就不多說了哈&#xff0c;既然會看到我這篇文章&#xff0c;想必都知道內網穿透是做什么的吧 frp分為服務端和客戶端&#xff0c;服務端一般是搭在公網服務器中&#xff0c;客戶端一般搭在本地或者局域網&#xff0c;需要提前在服務端搭好ftp server&am…

Tailwind CSS 實戰,基于 Kooboo 構建 AI 對話框頁面(四):語音識別輸入功能

基于前三章的內容&#xff0c;開發AI 對話框語音識別輸入功能&#xff1a; Tailwind css實戰&#xff0c;基于Kooboo構建AI對話框頁面&#xff08;一&#xff09;-CSDN博客 Tailwind css實戰&#xff0c;基于Kooboo構建AI對話框頁面&#xff08;二&#xff09;&#xff1a;實…

ollama list模型列表獲取 接口代碼

ollama list模型列表獲取 接口代碼 curl http://localhost:11434/v1/modelscoding package hcx.ollama;/*** ClassName DockerOllamaList* Description TODO* Author dell* Date 2025/5/26 11:31* Version 1.0**/import java.io.BufferedReader; import java.io.InputStreamR…

ISOLAR軟件生成報錯處理(五)

錯誤1 An error has occurred. See error log for more details. java.lang.NullPointerException 這東西不用管&#xff0c;不影響生成 錯誤2 Description Resource Path Location Type Target ARObject: <xxxx> CompuMethod used for floating-point data conversi…

前端開發定時,ES學習,java集合

1.前端vue3加入定時任務&#xff1a; import { onMounted, ref,onUnmounted } from vue;//初始化&#xff0c;結束調用部分引用let timer: any;//定時器onMounted(async () > {timer setInterval(() > {open()//需要定時的任務}, 60000)//一分鐘調用一次}); onUnmounte…

Photoshop2025(PS2025)軟件及安裝教程

在數字圖像編輯領域&#xff0c;Adobe Photoshop 一直是無可爭議的王者。如今&#xff0c;Photoshop 2025 重磅登場&#xff0c;再次為我們帶來了驚喜與變革&#xff0c;進一步鞏固了它在行業中的領先地位。 Photoshop 2025 在人工智能方面的升級令人矚目。其全新的 “Magic Se…

【SQL Server Management Studio 連接時遇到的一個錯誤】

第一次用SQL Server Management Studio啟動之后第一步就是要建立連接 但是不知道Server Name要填什么&#xff0c;看了網上的教程說是要找到下面這個注冊表中對應的實例名稱填上去&#xff0c;或者前面加localhost 但是好像都沒有用&#xff0c;一直遇到報錯如下&#xff1a;…

高等數學基礎(向量矩陣及其創建和特殊的矩陣)

向量 向量是機器學習最底層的組成部分, 也是基礎數據的表示形式, 線性代數通過將研究對象拓展到向量, 對多維數據進行統一研究, 而進化出的方法方便我們可以研究和解決真實世界中的問題 標量 標量也稱為"無向量", 使用一個單獨的數表示數值大小, 可以有正負之分, …

IBM DB2數據庫管理工具IBM Data Studio

一、介紹 IBM Data Studio 是 IBM 提供的一個集成開發環境&#xff08;IDE&#xff09;&#xff0c;用于支持數據管理、開發、優化和管理數據庫應用程序&#xff0c;特別是在 IBM Db2 和其他數據庫平臺上。它提供了許多功能&#xff0c;以幫助開發人員和數據庫管理員提高生產力…

Java異常處理的全面指南

Java異常處理的全面指南 一、Java異常的基礎概念1.1 什么是異常1.2 異常類的層次結構 二、Java異常的處理方式2.1 try-catch塊2.2 throws關鍵字2.3 throw關鍵字 三、自定義異常3.1 自定義受檢異常3.2 自定義非受檢異常 四、Java異常處理的最佳實踐4.1 捕獲合適粒度的異常4.2 避…

MediaMtx開源項目學習

這個博客主要記錄MediaMtx開源項目學習記錄,主要包括下載、推流(攝像頭,MP4)、MediaMtx如何使用api去添加推流,最后自定義播放器,播放推流后的視頻流,自定義Video播放器博客地址 1 下載 MediaMTX MediaMTX 提供了預編譯的二進制文件,您可以從其 GitHub 頁面下載: Gi…

【unity游戲開發——編輯器擴展】EditorApplication公共類處理編輯器生命周期事件、播放模式控制以及各種編輯器狀態查詢

注意&#xff1a;考慮到編輯器擴展的內容比較多&#xff0c;我將編輯器擴展的內容分開&#xff0c;并全部整合放在【unity游戲開發——編輯器擴展】專欄里&#xff0c;感興趣的小伙伴可以前往逐一查看學習。 文章目錄 前言一、監聽編輯器事件1、常用編輯器事件2、示例監聽播放模…

Spring Boot+Activiti7入坑指南初階版

介紹  Activiti 是一個輕量級工作流程和業務流程管理 (BPM) 平臺,面向業務人員、開發人員和系統管理員。其核心是一個超快且堅如磐石的 Java BPMN 2 流程引擎。它是開源的,并根據 Apache 許可證分發。Activiti 可以在任何 Java 應用程序、服務器、集群或云中運行。它與 Spri…

VoltAgent 是一個開源 TypeScript 框架,用于構建和編排 AI 代理

?一、軟件介紹 文末提供程序和源碼下載 VoltAgent 是一個開源 TypeScript 框架&#xff0c;用于構建和編排 AI 代理 二、什么是 VoltAgent&#xff1f; AI 代理框架提供了構建由自主代理提供支持的應用程序所需的基礎結構和工具。這些代理通常由大型語言模型 &#xff08;&am…

《仿盒馬》app開發技術分享-- 訂單詳情頁(端云一體)

開發準備 在之前的章節中我們實現了訂單的提交&#xff0c;以及提交之后跳轉到確認訂單頁面&#xff0c;在確認訂單頁面我們添加了一個入口&#xff0c;這個入口是查詢訂單&#xff0c;當我們點擊入口時&#xff0c;我們需要跳轉到一個新的界面&#xff0c;這個界面通過接收上…

傳統項目管理總拖延?Scrum敏捷全流程拆解

在互聯網高速發展的時代&#xff0c;企業競爭的核心要素正逐漸向 "速度" 傾斜。市場環境瞬息萬變&#xff0c;用戶需求呈現出多元化、動態化的顯著特征&#xff0c;而傳統管理模式固有的滯后性與僵化性&#xff0c;已難以匹配快速迭代的市場需求。在此背景下&#xf…

GelSight Mini觸覺傳感器:7μm精度+3D 映射,賦能具身智能精密操作

GelSight Mini 高分辨率視觸覺傳感器采用先進的光學成像與觸覺感知技術&#xff0c;賦予機器人接近人類的觸覺能力。該設備可捕捉物體表面微觀細節&#xff0c;并生成高精度的2D/3D數字映射&#xff0c;幫助機器人識別形狀、紋理及接觸力&#xff0c;從而執行更復雜、精準的操作…