双向循环链表对比单链表,他的实现会更简单,不需要考虑太多
双向带头循环链表的结构体声明
typedef int DLTDataType;
typedef struct DListNode
{
//节点里面包含两个指针,一个是指向他上一个节点的指针prev
//另外一个就是指向他下一个节点的指针next
struct DListNode* prev;
struct DListNode* next;
DLTDataType data;
}DLTNode;
首先我们需要先初始化
DLTNode* DListNodeInit(void)
{
//初始化创建一个头节点
DLTNode* guard = (DLTNode*)malloc(sizeof(DLTNode));
if(guard == NULL)
{
perror("malloc fail");
exit(-1);
}
//这个头节点的prev和next都指向他自己
guard->next = guard;
guard->prev = guard;
//最后我们以返回的形式,传回去头节点head
return guard;
}
接下来是打印这个链表
void DListNodePrint(DLTNode* phead)
{
assert(phead);
//由于我们的头节点不保存数据,所以我们从头节点的下一个节点开始打印数据
DLTNode* cur=phead->next;
printf("phead<=>");
//对于双向带头循环链表的判断停止,不是判断null了,由于是循环的,他的尾会跟头是连着的,所以判断 //结束的是,cur 不等于头就代表这个链表都访问结束了
while(cur != phead)
{
printf("%d<=>",cur->data);
cur = cur->next;
}
printf("
");
}
由于每次数据的插入都需要创建一个节点,所以我们创建一个函数,用来创建这个节点、
DLTNode* BuyDListNode(DLTDataType x)
{
DLTNode* node = (DLTNode*)malloc(sizeof(DLTNode));
if(node == NULL)
{
perror("BuyDListNode fail");
exit(-1);
}
//每个新节点的头指向自己,尾也是指向自己
node->data = x;
node->next = node;
node->prev =node;
return node;
}
数据的尾插
void DListNodePushBack(DLTNode* phead,DLTDataType x)
{
assert(phead);
DLTNode* newnode = BuyDListNode(x);
DLTNode* tail = phead->prev;
//接下来是节点之间的指向关系了,由于我们有尾节点和头节点,所以不用考虑指向的先后顺序
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
newnode->next = phead;
//这个是在pos的位置前插入一个节点的函数接口,在下面会介绍到
//DListNodeInsert(phead,x);
}
数据的头插
void DListNodePushFront(DLTNode* phead,DLTDataType x)
{
assert(phead);
// DLTNode* newnode=BuyDListNode(x);
//第一种头插,需要考虑连接顺序
// phead->next->prev = newnode;
// newnode->prev = phead;
// newnode->next = phead->next;
// phead->next = newnode;
//第二种头插,我们有了头节点和下一节点的地址了,不需要考虑连接顺序
// DLTNode* first = phead->next;
// phead->next = newnode;
// newnode->prev = phead;
// first->prev = newnode;
// newnode->next = first;
//这个是在pos之前插入一个节点的函数接口,下面会有介绍
//DListNodeInsert(phead->next,x);
}
数据的尾删
void DListNodePopBack(DLTNode* phead)
{
assert(phead);
//在删除之前,我们需要先判断一下,这个链表是否为空,为空就没有必要删了
assert(!DListEmpty(phead));
//下面是删除的指向步骤
DLTNode* del = phead->prev;
DLTNode* prev = del->prev;
prev->next = phead;
phead->prev = prev;
free(del);
del = NULL;
}
数据的头删
void DListNodePopFront(DLTNode* phead)
{
assert(phead);
//数据的头删跟尾删是类似的,也是要先判断这个链表是否为空
assert(!DListEmpty(phead));
DLTNode* del=phead->next;
DLTNode* next = del->next;
next->prev = phead;
phead->next = next;
free(del);
del = NULL;
}
查找数据所在的节点地址,如果找到了返回这个节点的地址,如果没有找到就返回null
DLTNode* FindDListNode(DLTNode* phead,DLTDataType x)
{
assert(phead);
DLTNode* cur = phead->next;
while(cur != phead)
{
if(cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
接下来是可以代替头插,尾插的函数,在指定的地址前插入数据,有了这个函数,我们可以不用写头插,尾插,头插和尾插的函数接口可以改为 这是尾插 DListNodeInsert(phead,x);
这是头插 DListNodeInsert(phead->next,x); 直接放在对应的函数接口里面就行,原先的头插尾插代码都可以注释掉
void DListNodeInsert(DLTNode* pos, DLTDataType x)
{
assert(pos);
DLTNode* prev = pos->prev;
DLTNode* newnode = BuyDListNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
当然,有了在pos前插入数据,也会有在pos之前删除数据的
void DListNodeErase(DLTNode* pos)
{
assert(pos);
DLTNode* prev = pos->prev;
DLTNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
//由于不是用二级指针,这里,无法更改这个指针指向的地址置空
// pos = NULL;
}
接下来是最后一个函数接口,就是销毁这个链表
void DListNodeDestory(DLTNode* phead)
{
assert(phead);
DLTNode* cur = phead;
while(cur != phead)
{
DLTNode* next = cur->next;
free(cur);
cur=next;
}
free(phead);
}
到此就是一个双向带头循环链表接口;
接下来的是动态顺序表和单链表之间对比的优势和劣势;
顺序表:他在物理上的结构就是一窜连续的地址,我们可以通过下标对其进行随机访问,尾插是O(1)的,头插就差一些是O(n)的,这是一个动态的顺序表,他是可以扩容的,用realloc进行扩容的时候,会有两种情况,一种是这个顺序表后面刚好有他需要扩容的空间,一种是没有
如果有直接把所需要的空间给到顺序表进行扩容,如果没有就需要在内存的其他空间进行寻找,把数据都搬过去,所以异地扩容就比较消耗时间了,相对的数据的存储是连续的,也是有好处的,我们的CPU在读取数据之前都会在寄存器上读取,寄存器又在我们的内存上拿数据,他拿数据不是只拿一个的,他会把这个数据的周边几个数据一起拿走,至于拿几个,这个要看型号,数据存储在连续的地址,会提高寄存器的命中率,减少寄存器多次到内存取数据;
单链表:单链表是按需创建空间的,不存在浪费,但他每次都需要重新创建空间,所以跟顺序表对比上,并没有多少优势,就是用空间换取时间,在现在这个社会内存没有时间重要了,单链表的头插是O(1)的,尾插是O(n)的,刚好跟顺序表是相反的,对于具体选择单链表好,还是顺序表,要看使用场景了,如果知道大概的数据大小,那无疑用顺序表是比较好的,如果数据是需要频繁的插入和删除,那单链表会比顺序表好。