带头双向循环链表的实现
文章目录
- 带头双向循环链表的实现
- 一、模型构建
- 二、代码实现(接口函数以及测试用例)
- ①初始化 + Create函数
- ②尾插
- ③尾删
- ④头插
- ⑤头删
- ⑥查找
- ⑦在pos位置前插入
- 新尾插
- 新头插
- ⑧删除pos位
- 新尾删
- 新头删
- ⑨销毁链表
- ⑩打印链表
- ⑪测试用例
- 三、链表和顺序表的对比
一、模型构建
每一个结点都有一个前置指针,前置指针存上一个结点的地址,一个后置指针存下一个结点的地址,还有一个存储数据的变量。尾的下一个就是头,头的上一个就是尾。
并且首尾相连还带有哨兵位头结点。
充分体现出了双向带头循环。
如果要是链表为空,那应该是什么样子的呢?
phead->next = phead, phead->prev = phead.
二、代码实现(接口函数以及测试用例)
①初始化 + Create函数
因为双向带头循环链表无论如何都是有一个哨兵位phead的结点,所以我们初始化的时候不能像创建顺序表和链表一样把指针设置为NULL之后就OK啦,需要用malloc函数去开辟一块空间。
LT* CreateNode(LTDataType x)
{
LT* newnode = (LT*)malloc(sizeof(LT));
if (newnode == NULL)
{
printf("malloc");
exit(-1);
}
newnode->x = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
LT* LTIni()
{
LT* newnode = CreateNode(-1);
LT* phead = newnode;
phead->next = phead;
phead->prev = phead;
return phead;
}
②尾插
void LTPushBack(LT* phead, LTDataType x)
{
assert(phead);
LT* tail = phead->prev;
LT* newnode = CreateNode(x);
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
找尾很方便,直接就是phead->prev
③尾删
void LTPopBack(LT* phead)
{
assert(phead);
assert(phead->next != phead);
LT* tail = phead->prev;
LT* newtail = tail->prev;
newtail->next = phead;
phead->prev = newtail;
free(tail);
tail = NULL;
}
④头插
void LTPushFront(LT* phead, LTDataType x)
{
assert(phead);
LT* newnode = CreateNode(x);
LT* tmp = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = tmp;
tmp->prev = newnode;
}
⑤头删
void LTPopFront(LT* phead)
{
assert(phead);
assert(phead->next != phead);
LT* node = phead->next;
phead->next = node->next;
node->next->prev = phead;
free(node);
node = NULL;
}
⑥查找
LT* LTFind(LT* phead, LTDataType x)
{
assert(phead);
LT* cur = phead->next;
while (cur != phead)
{
if (cur->x == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
⑦在pos位置前插入
void LTInsert(LTDataType x, LT* pos)//在pos位置前插入一个结点
{
assert(pos);
LT* tmp = pos->prev;
LT* newnode = CreateNode(x);
tmp->next = newnode;
newnode->prev = tmp;
newnode->next = pos;
pos->prev = newnode;
}
大家仔细观察一下如果pos = phead是一个什么情况呢?
我画图为大家解释一下:
由此可见,如果要是pos = phead,那插入pos位之前就相当于是尾插,
而如果想实现头插,我们也只需把pos位设置成phead->next就可以啦。
新尾插
void LTPushBack(LT* phead, LTDataType x)
{
assert(phead);
LTInsert(x, phead);
}
新头插
void LTPopBack(LT* phead LTDataType x)
{
assert(phead);
LTInsert(x, phead->next);
}
⑧删除pos位
void LTErase1(LT* pos)//删除pos位置前的结点
{
assert(pos);
LT* tmp = pos->prev;
tmp->prev->next = pos;
pos->prev = tmp->prev;
free(tmp);
tmp = NULL;
}
void LTErase2(LT* pos)
{
assert(pos);
LT* tmp1 = pos->next;
LT* tmp2 = pos->prev;
tmp2->next = tmp1;
tmp1->prev = tmp2;
free(pos);
//pos = NULL;
}
LTErase1函数其实并不是很重要,让我们一起来看LTErase2函数,最后这步pos是否设置NULL,其实影响都不是很大,因为形参的改变是不会影响实参的,所以这个时候其实实际的pos已经成为了一个野指针了,我们就需要在工程里使用他的地方后面自己设置成NULL。
例如:
但是在此处个人认为在这个函数里是不是加上这句断言会更好一些呀
assert(pos != phead)
新尾删
void LTPopBack(LT* phead)
{
assert(phead);
assert(phead->next != phead);
LTErase2(phead->prev);
}
新头删
void PopFront(LT* phead)
{
assert(phead);
assert(phead->next);
LTErase2(phead->next);
}
⑨销毁链表
void LTDestroy(LT* phead)
{
assert(phead);
LT* cur = phead->next;
while (cur != phead)
{
LT* next = cur->next;
free(cur);
cur = next;
}
free(phead);
//phead = NULL;//外面自己置空,形参的改变不影响实参。
}
销毁和删除一样,动态开辟的内存的指针都需要在外面重新置空
⑩打印链表
void LTPrint(LT* phead)
{
assert(phead);
LT* cur = phead->next;
printf("<=>哨兵位<=>");
while (cur != phead)
{
printf("%d<=>", cur->x);
cur = cur->next;
}
printf("\n");
}
⑪测试用例
#define _CRT_SECURE_NO_WARNINGS
#include "List.h"
void test1()
{
LT* listnode = LTIni();
LTPushBack(listnode, 1);
LTPushBack(listnode, 2);
LTPushBack(listnode, 3);
LTPushBack(listnode, 4);
LTPushBack(listnode, 5);
LTPrint(listnode);
LTPopBack(listnode);
LTPopBack(listnode);
LTPopBack(listnode);
LTPopBack(listnode);
LTPopBack(listnode);
LTPrint(listnode);
LTPushFront(listnode, 1);
LTPushFront(listnode, 2);
LTPushFront(listnode, 3);
LTPushFront(listnode, 4);
LTPushFront(listnode, 5);
LTPrint(listnode);
LTPopFront(listnode);
LTPopFront(listnode);
LTPopFront(listnode);
LTPopFront(listnode);
LTPopFront(listnode);
LTPrint(listnode);
}
void test2()
{
LT* listnode = LTIni();
LTPushBack(listnode, 1);
LTPushBack(listnode, 2);
LTPushBack(listnode, 3);
LTPushBack(listnode, 4);
LTPushBack(listnode, 5);
LTPrint(listnode);
LT* findnode = LTFind(listnode, 5);
LTInsert(100, findnode);
LTInsert(100, listnode);
LTPrint(listnode);
LTErase1(listnode);
listnode = NULL;
LTErase1(findnode);
findnode = NULL;
LTPrint(listnode);
LTErase2(findnode);
findnode = NULL;
LTPrint(listnode);
}
void test3()
{
LT* listnode = LTIni();
LTPushBack(listnode, 1);
LTPushBack(listnode, 2);
LTPushBack(listnode, 3);
LTPushBack(listnode, 4);
LTPushBack(listnode, 5);
//LTErase2(listnode);
//listnode = NULL;//这样已经避免了不能传入哨兵位。
LTPrint(listnode);
LTDestroy(listnode);
listnode = NULL;
}
int main()
{
//test1();
//test2();
test3();
return 0;
}
三、链表和顺序表的对比
顺序表 | 链表(主要是双向带头循环链表) |
---|---|
支持下标随机访问, 便于排序 | 下标随机访问不方便,不便于随机排序 |
空间连续,便于缓存命中 | |
头部,尾部插删效率低,需要挪动数据 | 在知道结点地址的情况下,任意位置的插删都是O(1) |
空间不够需要扩容,扩容有一定的消耗且有一定的空间浪费 | 按需申请释放,合理利用空间,不存在浪费。 |