目录
链式表的分类方式
按节点连接方式分类
按存储结构分类
按功能扩展分类
带头双向循环动态链表模拟实现
链式表的分类方式
链式表(链表)根据不同的结构和特性,可以分为以下几类:
按节点连接方式分类
单向链表
每个节点包含数据和指向下一个节点的指针,只能单向遍历。- 优点:结构简单,内存占用较小。
- 缺点:无法反向遍历,删除节点需从头查找前驱节点。
双向链表
每个节点包含数据、指向下一个节点的指针和指向前一个节点的指针。- 优点:支持双向遍历,删除和插入操作更高效。
- 缺点:每个节点多占用一个指针空间,内存开销较大。
循环链表
尾节点指向头节点,形成闭环。可分为单向循环链表和双向循环链表。- 优点:适合环形数据处理(如轮询调度)。
- 缺点:需注意循环终止条件,避免无限循环。
按存储结构分类
静态链表
使用数组模拟链表,通过数组下标代替指针。- 优点:无需动态内存分配,适合嵌入式系统等受限环境。
- 缺点:容量固定,灵活性差。
动态链表
节点通过动态内存分配(如malloc)创建,内存可动态扩展。- 优点:灵活性强,内存利用率高。
- 缺点:需手动管理内存,易产生内存泄漏。
按功能扩展分类
带头节点的链表
在链表头部添加一个不存储数据的节点(头节点),简化插入/删除操作。- 优点:统一操作逻辑,避免对头节点的特殊处理。
不带头节点的链表
直接以第一个数据节点作为头节点。- 优点:节省一个节点的空间。
- 缺点:需单独处理头节点操作。
而今天我要模拟实现的是带头双向循环动态链表。
带头双向循环动态链表模拟实现
链表的结构结构体:
typedef int DCLDataType; typedef struct DCListNode { DCLDataType data; // 存储数据元素的值 struct DCListNode* prev; // 存放前驱结点的指针 struct DCListNode* next; // 存放后继结点的指针 }DCListNode;链表的初始化:
// 链表初始化 DCListNode* DCListInit() { DCListNode* head = (DCListNode*)malloc(sizeof(DCListNode)); if (head == NULL) { perror("malloc"); exit(1); } head->next = head->prev = head; return head; }注意点:在该链式表内,因为要实现的循环链表,所以需要在初始化的时候需要进行一个头尾相连向头指针。
链式表的销毁:
// 销毁链表 void DCListDestroy(DCListNode* L) { if (L == NULL) { return; } DCListNode* dest ,*head = L; L = L->next; while (L != head) { dest = L; L = L->next; free(dest); dest = NULL; } free(head); head = NULL; return; }因为是一个循环链表需要进行特殊的处理,先从链式表头指针往后的第一个节点进行销毁,遇到头指针停止循环销毁,对头指针单独销毁。
获取链式表的为序i节点:
// 获取链表的位序i的结点 DCListNode* DCListGetElem(DCListNode* L, int i) { if (i <= 0 || L == NULL)//错误操作,指针为空 { return NULL; } DCListNode* node = L->next; int I = 1; for (I = 1; I < i && L != node; I++) { node = node->next; } if (L == node)//超出链式表的最大访问,遇到 return NULL; return node; }1.判断头指针是否为空或者i小于等于0
2.声明节点跳过头指针,开始查找。
3.超出最大链式的极限返回NULL ,没有就返回node 节点
在pos位置后插入值为x的结点 和 头插 尾插
void DCListInsert(DCListNode* pos, DCLDataType x) { DCListNode* news = (DCListNode*)malloc(sizeof(DCListNode)); if (news == NULL) { perror("malloc"); exit(1); } news->next = pos->next; news->prev = pos; pos->next->prev = news; pos->next = news; news->data = x; return; } // 头插 void DCListPushFront(DCListNode* L, DCLDataType x) { assert(L != NULL); DCListInsert(L, x); return; } // 尾插 void DCListPushBack(DCListNode* L, DCLDataType x) { assert(L); DCListInsert(L->prev, x); return; }节点插入都要先申请一个节点,这个节点为了方便解释设为C点,要插入某个节点前面我们设A点下一个节点为B,节点的各个节点一定要遵循,A->B的节点最后改不然会导致找不到B点,或者可以用其他方式来实现,总之要保证B点有能够存取的地址的方式。
我的步骤就是先把申请来的新地址,先把新节点C的前后指针存入B和A,把B的后指针存入C,再把A的前指针存入C,再把C的data存入x。
头插和尾插都只需要把对应的地址传参即可,如果要实现对应位置插入就用刚才的DCListGetElem函数利用起来,就可以实现对应位置插入。
//对应位置插入 void DCListPushNode(DCListNode* L, DCLDataType x,int i) { DCListNode* node = DCListGetElem(L, i); assert(node); DCListInsert(node, x); return; }删除pos位置的结点头删和尾删
// 删除pos位置的结点 void DCListDelete(DCListNode* pos) { assert(pos != NULL); if (pos == pos->next) { return; } pos->next->prev = pos->prev; pos->prev->next = pos->next; free(pos); pos = NULL; return; } // 头删 void DCListPopFront(DCListNode* L) { assert(L != NULL); DCListDelete(L->next); return; } // 尾删 void DCListPopBack(DCListNode* L) { assert(L != NULL); DCListDelete(&L->prev); return; }对应位置删除只关注前后指针前后指针从新链接就可以。当前位置的前指针节点的后指针 改成 当前位置的指针的后一个节点;当前位置的后指针节点的前指针 改成 当前位置的指针的前一个节点;这样及保证前后指针完美链接,也可以不用声明多的变量,只用pos 进行空间释放。
注意点:
头删时把头指针的往后一个节点进行释放,而不是头指针释放,所以传参传L->next;尾删就是把头指针的L->prev 进行释放。原则就是保证删除的不是头指针,只有销毁才需要把头指针进行销毁。
打印输出:
// 打印链表中的元素 void DCListPrint(DCListNode* L) { assert(L!=NULL); DCListNode* node = L->next; while (L!=node) { printf("%d -> ",node->data); node = node->next; } printf("\n"); return; }模拟实现
void List_test() { DCListNode* head = DCListInit(); DCListPushBack(head, 1); DCListPushBack(head, 2); DCListPushBack(head, 3); DCListPushBack(head, 4); DCListPushBack(head, 5); DCListPushBack(head, 6); DCListPrint(head); DCListPushFront(head, 2); DCListPushFront(head, 3); DCListPushFront(head, 4); DCListPushFront(head, 5); DCListPushFront(head, 6); DCListPrint(head); DCListPopFront(head); DCListPrint(head); DCListPopFront(head); DCListPrint(head); DCListPopFront(head); DCListPrint(head); DCListDestroy(head); return; } int main() { List_test(); return 0; }感谢观看!
悠仁さん