【数据结构】线性表之“双链表(带头循环双向链表)”

发布于:2025-05-26 ⋅ 阅读:(24) ⋅ 点赞:(0)

- 第 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不是NULL
    
    	DListNode* 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不是NULL
    	if (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不是NULL
    	if (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); // 避免空指针,检查头指针是否为NULL
    	DListNode* 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 ~ 5
	printf("尾插1 2 3 4 5\n");
	for (int i = 1; i <= 5; i++) {
		DListPushBack(L, i); // 每次尾插
	}
	DListPrint(L); // 打印链表

	// 头插 1 ~ 5
	printf("头插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 之前插入 33
	printf("在 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不是NULL

	DListNode* 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不是NULL
	if (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不是NULL
	if (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); // 避免空指针,检查头指针是否为NULL
	DListNode* 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"); // 最后打印结束
}

网站公告

今日签到

点亮在社区的每一天
去签到