插入函数
在pos位置前插入,这个pos的地址是由LTFind函数返回的
如果我们直接就将新的节点newnode插入到pos前面位置,它们之间的链接操作有一定的顺序,如果随意改变链接关系是会出问题的
所以我们可以用一个指针posprev去保存pos->prev的地址,然后再链接posprev、newnode和 pos间的链接关系,这时的链接关系的改变不需要按照一定的顺序,按照逻辑随意修改
void LTInsert(LTNode* pos, LTDataType x) { assert(pos); LTNode* posprev = pos->prev; LTNode* newnode = BuyNewNode(x); posprev->next = newnode; newnode->prev = posprev; newnode->next = pos; pos->prev = newnode; }
我们可以用这个删除函数与实现头插和尾插
尾插:因为在pos前插入,头节点的前一个结点就是尾结点,所以调用LTInsert函数时传的地址是phead
void LTPushBack(LTNode* phead, LTDataType x) { assert(phead); LTInsert(phead, x); //调用LTInsert函数尾插 }
头插:phead->next的前插入就是头插,所以传参为phead->next
void LTPushFront(LTNode* phead, LTDataType x) { assert(phead); LTInsert(phead->next, x); //调用LTInsert函数头插 }
删除函数
这里的操作也如上个函数一样,用一个指针posprev保存pos->prev的地址,用指针posnext保存pos->next的地址,然后链接posprev和posnext
随后free掉pos即可
void LTErase(LTNode* pos) { assert(pos); LTNode* posprev = pos->prev; LTNode* posnext = pos->next; posprev->next = posnext; posnext->prev = posprev; free(pos); pos = NULL; }
这个函数还有个瑕疵就是:
如果pos传的是哨兵位头节点的地址,那么删除操作就会出错
这里如果想检查,就需要传一个参数,用来比较pos和头节点地址是否相同,但是没有必要
在后面C++中会有更好的写法
同样,可以调用LTErase去实现头删和尾删
头删:
void LTPopFront(LTNode* phead) { assert(phead); assert(!LTEmpty(phead)); LTErase(phead->next); }
尾删:
void LTPopBack(LTNode* phead) { assert(phead); assert(!LTEmpty(phead)); LTErase(phead->prev); }
销毁函数
void LTDestroy(LTNode* phead) { assert(phead); LTNode* del = phead->next; LTNode* nex = del->next; while (del!= phead) { free(del); del = nex; nex = nex->next; } free(phead); phead = NULL; }
1
带头双向循环链表的特点
在前面实现的过程中可以发现:
以往单链表的尾删、删除和插入操作的时间复杂度是 O(n),因为需要找尾或找前一个结点,但是带头双向循环链表完美得解决了这个问题,我们可以通过phead->prev找到尾,通过某一结点的prev找到前一个结点,所以在带头双向循环链表中,尾删和插入的时间复杂度为O(1)
在单链表的头插、尾插、头删、尾删等函数中,传的参数有二级指针,不容易理解,但是带头双向循环链表有头节点,传参是头节点的地址,因为在函数中是不会改变这个地址的指向,所以不用传二级指针
链表和顺序表的对比
这两个结构各有优势,很难说谁更优,这两个结构相辅相成
顺序表:
物理上存储空间连续
支持随机访问
任意位置插入或者删除元素可能需要搬移元素,效率低O(N)
连续物理空间,空间不够后需要增容。增容有一定程度的消耗
链表:
物理上存储空间不连续,但是逻辑上连续
不支持随机访问
任意位置插入删除效率高
按需申请释放空间