提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
一、单链表的问题及思考
在上一节,我们学习了单链表,了解了单链表相比于顺序表有更好的存储功能,但是,单链表仍然存在一些缺陷:
- 例如尾删,任意位置插入,任意位置删除的时间复杂度但是O(N)
- 查找数据时不能从后往前,只能从头又开始遍历查找尾。
- 找不到前驱,都等等。
这时,就可以引入一个新的链表:带哨兵位头节点的双向带头循环链表,这里简称双链表。
提示:以下是本篇文章正文内容,
二、链表结构分析
实际中要实现的链表的结构非常多样,以下情况组合起来就有8种链表结构:
- 单向、双向
- 带头、不带头
- 循环、非循环
但是我们一般常用的就是无头单向非循环链表和带头双向循环链表。
-
无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。 -
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。
- 值得注意的是,带头的链表那个“头(Head)”习惯称为“哨兵位”,一般不存储数据。所以不要尝试将链表的长度存储到“头“中。主要原因是我们并不清楚这个链表存储的是什么数据,如果是float,double,struct等类型,那存储的长度还能行吗?
三、双向链表的表示和实现
1.函数声明
代码如下(示例):
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);
void ListInsert(ListNode* pos, LTDataType x);
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);
ListInsert(phead, x);
}
双向链表头插: ListPushFront
void ListPushFront(ListNode* phead, LTDataType x)
{
assert(phead);
ListInsert(phead->next, x);
}
双向链表头删:ListPopFront
void ListPopFront(ListNode* phead)
{
assert(phead);
ListErase(phead->next);
}
双向链表尾删: ListPopBack
void ListPopBack(ListNode* phead)
{
assert(phead);
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
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* newnode = BuyListNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
双向链表删除pos位置的节点:ListErase
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),是存储数据最优的模式。
|