内核链表和普通链表的区别:
1.内核链表的结构
从结构上我们就知道为什么内核链表的数据可以不受限制,我们将节点的指针和数据分离,就可以在大的结构体里存放不同的数据,而普通链表指针数据是在一块的无法改变数据类型。
指针是独立在一个结构体里的,因此他可以有多个头多个尾,因此他可以存在于多个链表中
2.内核链表代码实现
在实现过程中思路相同的我就不多讲了,完成内核链表经典的几个操作,由于带哨兵位的链表使我们更容易操作,因此我们书写的是带哨兵位的双向循环内核链表。
内核链表,顾名思义我们针对链表的操作都是针对核也就是指针域的那个结构体。
2.1内核链表创建
通过结构我们知道内核链表用大结构体包含了一个存储指针的结构体,要注意的是大结构体里的指针域结构体是一个普通变量,不可以是指针类型!因此他的结构如下:
typedef int DataTay;
//链表的指针域//核心
struct HeadAndTail
{
struct HeadAndTail* next;
struct HeadAndTail* prev;
};
//链表节点
typedef struct Node
{
DataTay data;
struct HeadAndTail ptr;//这里只能是普通变量不可以是指针,如果是指针那结构上有什么变化?
}Node, * P_Node;
2.2内核链表初始化
初始化我们做了以下步骤:
我们先要在堆开辟一个节点的空间,存入我们想要存储的数据,让节点内核指针prev指向内核地址,next也指向内核地址。
void ListHeadInit(struct HeadAndTail* NewSpace)
{
NewSpace->next = NewSpace;
NewSpace->prev = NewSpace;
}
P_Node GetNodeSpace(DataTay Data)
{
P_Node NewSpace = (P_Node)calloc(1, sizeof(Node));
printf("成功获取新的节点\n");
NewSpace->data = Data;
printf("成功获取小结构体节点地址:%x\n", &NewSpace->ptr);
return NewSpace;
}
//创建一个节点并初始化,返回该节点的地址。
P_Node ListNodeInit(DataTay Data)
{
P_Node NewNodeAddr = GetNodeSpace(Data);
ListHeadInit(&NewNodeAddr->ptr);//指向的是核的地址
return NewNodeAddr;
}
2.3头插
我们保持一个理念,一切的指向操作都是针对这个核,我们在书写过程中基本是不会出错的!
头插思路:用头结点获取到头结点核地址和头结点下一个节点的核地址,我们新创建的节点就可以前指向头节点的核地址,尾指向头结点下一个节点的核地址,再让头结点next指向新的核地址,让头结点下个节点的核的prev指向新的节点核地址,接完成了头插,尾插思路是相同的我就不在多说了。
//头插函数
void DLinkListPushFront(P_Node StLinkListHead, DataTay x)
{
struct HeadAndTail* Head = &StLinkListHead->ptr;
struct HeadAndTail* HeadNext= StLinkListHead->ptr.next;
struct HeadAndTail* NewHead = &(GetNodeSpace(x)->ptr);
NewHead->next = HeadNext;//新的节点下一个指向头结点
NewHead->prev = Head;//新节点的前一个节点是尾节点
Head->next = NewHead;//找到尾节点,头结点的前一个节点就是尾节点,尾节点的下一个节点就是新节点
HeadNext->prev = NewHead;//头节点的前一个指向新节点
}
2.4头删
头删的思路就是要删除的节点(释放节点空间),再将断开处链接起来,就OK。
//头删
int DelListHead(P_Node StLinkListHead)
{
struct HeadAndTail* Head = &StLinkListHead->ptr;
struct HeadAndTail* HeadNext_Next = StLinkListHead->ptr.next->next;
if (StLinkListHead->ptr.next == &StLinkListHead->ptr)
{
printf("链表为空");
return -1;
}
else
{
P_Node DelNodeAddr = GetP_NodeAddrforHeadNodeAddr(Head->next);
Head->next = HeadNext_Next;//找到尾节点,头结点的前一个节点就是尾节点,尾节点的下一个节点就是新节点
HeadNext_Next->prev = Head;//头节点的前一个指向新节点
free(DelNodeAddr);
return 0;
}
}
2.5查找链表中数据的地址
这个非常重要,这是我们根据内核地址获取到大结构体地址从而根据大结构体遍历数据的关键!
这里我就啰嗦的补充一下C语言相关知识点!
2.5.1指针类型强制转换
我们定义了一个int类型的指针,这个指针变量减一的含义是什么?地址向下偏移4个字节。
那char类型的指针呢?不再多说。
2.5.2结构体占用内存的大小计算(了解)
对齐数:结构体成员大小与系统默认对齐数比较,小的为最终对齐数
第一个成员变量从结构体入口地址开始存储
之后所有的成员变量都要以入口地址为基准,在对齐数的整数倍的偏移地址处开始存储
结构体的总大小为系统默认对齐数的整数倍
2.5.3根据核地址计算节点入口地址
我们将0强制转换成节点类型的指针,再获取到内核地址,这样我们就直接得到了内核地址到节点入口地址的偏移量。我们再用知道的小结构体地址减去偏移量就得到了大结构体地址!
注意:这个公式每个强制类型转换都是有意义的,缺一不可。
//根据小结构体地址计算大结构体的地址
P_Node GetP_NodeAddrforHeadNodeAddr(struct HeadAndTail* SmallStrAddr)
{
//获取偏移量 //再根据偏移量得到大结构体地址
return (P_Node)((char*)SmallStrAddr - (unsigned long)(&((P_Node)0)->ptr));
}
2.6遍历链表
//打印链表的所有值
int PrintfDlist(P_Node HeadNodeAddr)
{
struct HeadAndTail* Newhead = &(HeadNodeAddr->ptr);
struct HeadAndTail* NewheadNext = HeadNodeAddr->ptr.next;
P_Node NewNode;
int i = 0;
if (HeadNodeAddr->ptr.next == &HeadNodeAddr->ptr)
{
printf("链表为空");
return -1;
}
else
{
for (; NewheadNext != Newhead; NewheadNext = NewheadNext->next)
{
NewNode = GetP_NodeAddrforHeadNodeAddr(NewheadNext);
printf("第%d个节点的值为:%d\n", i,NewNode->data);
i++;
}
return 0;
}
}
3.总结
内核链表最强的的地方在于,它可以存储任意类型的数据,我们只需要在节点里加上自己设定变量类型的标识,在遍历时我们获取节点的数据类型就可以进行打印
并且内核指针可以有多对,这样这个节点可以存在于多个链表中。