目录
链式表的分类方式
链式表(链表)根据不同的结构和特性,可以分为以下几类:
按节点连接方式分类
-
单向链表
每个节点包含数据和指向下一个节点的指针,只能单向遍历。
- 优点:结构简单,内存占用较小。
- 缺点:无法反向遍历,删除节点需从头查找前驱节点。
-
双向链表
每个节点包含数据、指向下一个节点的指针和指向前一个节点的指针。
- 优点:支持双向遍历,删除和插入操作更高效。
- 缺点:每个节点多占用一个指针空间,内存开销较大。
-
循环链表
尾节点指向头节点,形成闭环。可分为单向循环链表和双向循环链表。
- 优点:适合环形数据处理(如轮询调度)。
- 缺点:需注意循环终止条件,避免无限循环。
按存储结构分类
-
静态链表
使用数组模拟链表,通过数组下标代替指针。
- 优点:无需动态内存分配,适合嵌入式系统等受限环境。
- 缺点:容量固定,灵活性差。
-
动态链表
节点通过动态内存分配(如
malloc)创建,内存可动态扩展。- 优点:灵活性强,内存利用率高。
- 缺点:需手动管理内存,易产生内存泄漏。
按功能扩展分类
-
带头节点的链表
在链表头部添加一个不存储数据的节点(头节点),简化插入/删除操作。
- 优点:统一操作逻辑,避免对头节点的特殊处理。
-
不带头节点的链表
直接以第一个数据节点作为头节点。
- 优点:节省一个节点的空间。
- 缺点:需单独处理头节点操作。
而今天我要模拟实现的是带头双向循环动态链表。
带头双向循环动态链表模拟实现
链表的结构结构体:
cpp
typedef int DCLDataType;
typedef struct DCListNode {
DCLDataType data; // 存储数据元素的值
struct DCListNode* prev; // 存放前驱结点的指针
struct DCListNode* next; // 存放后继结点的指针
}DCListNode;
链表的初始化:
cpp
// 链表初始化
DCListNode* DCListInit()
{
DCListNode* head = (DCListNode*)malloc(sizeof(DCListNode));
if (head == NULL)
{
perror("malloc");
exit(1);
}
head->next = head->prev = head;
return head;
}
注意点:在该链式表内,因为要实现的循环链表,所以需要在初始化的时候需要进行一个头尾相连向头指针。
链式表的销毁:
cpp
// 销毁链表
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节点:
cpp
// 获取链表的位序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的结点 和 头插 尾插
cpp
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函数利用起来,就可以实现对应位置插入。
cpp
//对应位置插入
void DCListPushNode(DCListNode* L, DCLDataType x,int i)
{
DCListNode* node = DCListGetElem(L, i);
assert(node);
DCListInsert(node, x);
return;
}
删除pos位置的结点头删和尾删
cpp
// 删除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 进行释放。原则就是保证删除的不是头指针,只有销毁才需要把头指针进行销毁。
打印输出:
cpp
// 打印链表中的元素
void DCListPrint(DCListNode* L)
{
assert(L!=NULL);
DCListNode* node = L->next;
while (L!=node)
{
printf("%d -> ",node->data);
node = node->next;
}
printf("\n");
return;
}
模拟实现
cpp
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;
}

感谢观看!
悠仁さん