提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
一、单链表的问题及思考
在上一节,我们学习了单链表,了解了单链表相比于顺序表有更好的存储功能,但是,单链表仍然存在一些缺陷:
- 例如尾删,任意位置插入,任意位置删除的时间复杂度但是O(N)
- 查找数据时不能从后往前,只能从头又开始遍历查找尾。
- 找不到前驱,都等等。
这时,就可以引入一个新的链表:带哨兵位头节点的双向带头循环链表,这里简称双链表。
提示:以下是本篇文章正文内容,
二、链表结构分析
实际中要实现的链表的结构非常多样,以下情况组合起来就有8种链表结构:
- 单向、双向
- 带头、不带头
- 循环、非循环
但是我们一般常用的就是无头单向非循环链表和带头双向循环链表。
-
无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
-
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。
- 值得注意的是,带头的链表那个“头(Head)”习惯称为“哨兵位”,一般不存储数据。所以不要尝试将链表的长度存储到“头“中。主要原因是我们并不清楚这个链表存储的是什么数据,如果是float,double,struct等类型,那存储的长度还能行吗?
三、双向链表的表示和实现
1.函数声明
代码如下(示例):
// 2、带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
LTDataType _data;
struct ListNode* _next;
struct ListNode* _prev;
}ListNode;
// 创建返回链表的头结点.
ListNode* ListInit();
// 双向链表销毁
void ListDestory(ListNode* plist);
// 双向链表打印
void ListPrint(ListNode* plist);
// 双向链表尾插
void ListPushBack(ListNode* plist, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* plist);
// 双向链表头插
void ListPushFront(ListNode* plist, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* plist);
// 双向链表查找
ListNode* ListFind(ListNode* plist, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
2.接口函数详解
代码如下(示例):
创建返回链表的头结点:ListInit
//扩容函数
ListNode* BuyListNode(LTDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
// 创建返回链表的头结点
ListNode* ListInit()
{
ListNode* phead = BuyListNode(0);
phead->next = phead;
phead->prev = phead;
return phead;
}
双向链表销毁:ListDestory
// 双向链表销毁
void ListDestory(ListNode* phead)
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)
{
ListNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
phead = NULL;
}
双向链表打印:ListPrint
// 双向链表打印
void ListPrint(ListNode* phead)
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
双向链表尾插:ListPushBack
// 双向链表尾插
void ListPushBack(ListNode* phead, LTDataType x)
{
assert(phead);
//ListNode* tail = phead->prev;
//ListNode* newnode = BuyListNode(x);
//tail->next = newnode;
//newnode->prev = tail;
//newnode->next = phead;
//phead->prev = newnode;
ListInsert(phead, x);
}
双向链表头插: ListPushFront
// 双向链表头插
//void ListPushFront(ListNode* phead, LTDataType x)
//{
// assert(phead);
//
// ListNode* first = phead->next;
// ListNode* newnode = BuyListNode(x);
//
// // phead newnode first
// phead->next = newnode;
// newnode->prev = phead;
// newnode->next = first;
// first->prev = newnode;
//}
void ListPushFront(ListNode* phead, LTDataType x)
{
assert(phead);
//ListNode* newnode = BuyListNode(x);
//newnode->next = phead->next;
//phead->next->prev = newnode;
//phead->next = newnode;
//newnode->prev = phead;
ListInsert(phead->next, x);
}
双向链表头删:ListPopFront
// 双向链表头删
void ListPopFront(ListNode* phead)
{
assert(phead);
//assert(phead->next != phead);
//ListNode* first = phead->next;
//ListNode* second = first->next;
//phead->next = second;
//second->prev = phead;
//free(first);
//first = NULL;
ListErase(phead->next);
}
双向链表尾删: ListPopBack
// 双向链表尾删
void ListPopBack(ListNode* phead)
{
assert(phead);
/*assert(phead->next != phead);
ListNode* tail = phead->prev;
ListNode* prev = tail->prev;
prev->next = phead;
phead->prev = prev;
free(tail);
tail = NULL;*/
ListErase(phead->prev);
}
双向链表查找:ListFind
// 双向链表查找
ListNode* ListFind(ListNode* phead, LTDataType x)
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
双向链表在pos的前面进行插入x:ListInsert
// 双向链表在pos的前面进行插入x
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* newnode = BuyListNode(x);
// prev newnode pos
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
双向链表删除pos位置的节点:ListErase
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
}
- 在上述函数中,可以先写在随机位置插入,在随机位置删除的函数,然后在头插头删,尾插尾删可以复用。如上述函数中,注释的部分可以用ListInsert,ListErase函数代替。
- 在这个哨兵位头节点的双向带头循环链表中,无论是插入数据,还是删除数据,都非常方便,时间复杂度都是O(1),是存储数据最优的模式。
四、顺序表和链表的区别和联系
顺序表:
优点:
空间连续、支持随机访问
缺点:
- 中间或前面部分的插入删除时间复杂度O(N)
- 增容的代价比较大。
链表:
缺点:
以节点为单位存储,不支持随机访问
优点:
- 任意位置插入删除时间复杂度为O(1)
- 没有增容消耗,按需申请节点空间,不用了直接释放。
实际上,顺序表和链表属于优缺点相互弥补的关系。