数据结构世界——双向链表(Double Linked List)
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:单链表和双向带头循环链表
头节点
,实际为哨兵位
,哨兵位节点不存储任何有效元素,只是站在这里“放哨的”哨兵位
存在的意义: 遍历循环链表避免死循环ps:这里的“带头”跟前面我们说的“头节点”是两个概念,实际前面的在单链表阶段称呼不严谨,但是为了更好的理解就直接称为单链表的头节点。
typedef int DLDataType;
typedef struct DListNode
{
struct DListNode* prev;
struct DListNode* next;
DLDataType data;
}DLNode;
DLNode* BuyDLNode(DLDataType x)
{
DLNode* newnode = (DLNode*)malloc(sizeof(DLNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->prev = NULL;
newnode->next = NULL;
return newnode;
}
DLNode* DLInit()
{
DLNode* phead = BuyDLNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
void DLPrint(DLNode* phead)
{
assert(phead);
DLNode* cur = phead;
printf("Guard");
while (cur->next != phead)
{
cur = cur->next;
printf("<==>%d", cur->data);
}
printf("\n");
}
assert
断言,因为phead指向哨兵位,一定不为空NULL
,所有停止条件就看哨兵位,当cur指针的下一个为哨兵位时,就代表走到尾部void DLPushBack(DLNode* phead, DLDataType x)
{
assert(phead);
DLNode* newnode = BuyDLNode(x);
DLNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
newnode->next = phead;
}
如果是单链表,是不是还要讨论空链表的特殊情况啊?但是,在双向循环链表中,上述代码就可包含所有情况。因为哨兵位的存在,使得链表一定不为空。
同时,因为哨兵位的存在,我们不用传二级指针了,只用对结构体进行操作。怎么样,有没有发现双向链表的巨大优势?
void DLPushFront(DLNode* phead, DLDataType x)
{
assert(phead);
DLNode* newnode = BuyDLNode(x);
DLNode* first = phead->next;
newnode->next = first;
first->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
ps:先链接后一个节点,再链接前一个节点
删除时,不能删除哨兵位,要不然后续都无法对链表进行操作,所以专门写个函数来判断除了哨兵位链表是否为空。
bool DLEmpty(DLNode* phead)
{
assert(phead);
return phead == phead->next;
}
void DLPopBack(DLNode* phead)
{
assert(phead);
assert(!DLEmpty(phead));
DLNode* tail = phead->prev;
DLNode* tailPrev = tail->prev;
free(tail);
tailPrev->next = phead;
phead->prev = tailPrev;
}
void DLPopFront(DLNode* phead)
{
assert(phead);
assert(!DLEmpty(phead));
DLNode* first = phead->next;
DLNode* second = first->next;
second->prev = phead;
phead->next = second;
free(first);
}
DLNode* DLFind(DLNode* phead, DLDataType x)
{
assert(phead);
DLNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
NULL
在pos前插入
void DLInsert(DLNode* pos, DLDataType x)
{
assert(pos);
DLNode* newnode = BuyDLNode(x);
DLNode* prev = pos->prev;
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
在这里可以复用指定插入函数,快速实现头插和尾插。
头插
void DLPushFront(DLNode* phead, DLDataType x)
{
assert(phead);
DLInsert(phead->next, x);
}
尾插
void DLPushBack(DLNode* phead, DLDataType x)
{
assert(phead);
DLInsert(phead, x);
}
在pos删除
void DLErase(DLNode* pos)
{
assert(pos);
DLNode* posPrev = pos->prev;
DLNode* posNext = pos->next;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
}
在这里也可以复用指定删除函数,快速实现头删和尾删。
头删
void DLPopFront(DLNode* phead)
{
assert(phead);
assert(!DLEmpty(phead));
DLErase(phead->next);
}
尾删
void DLPopBack(DLNode* phead)
{
assert(phead);
assert(!DLEmpty(phead));
DLErase(phead->prev);
}
大家有没有发现,因为双向链表存在哨兵位,链表不为空,省去了很多关于空链表和空指针的讨论,一路下来浑身舒畅!
void DLDestroy(DLNode* phead)
{
assert(phead);
DLNode* cur = phead->next;
while (cur != phead)
{
DLNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
一级指针
的连贯性,所以我们选择最后手动在外部将链表指针置为NULL
,实现半自动(和free
函数的逻辑一致)不同点 | 顺序表 | 链表 |
---|---|---|
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持,O(1) | 不支持,O(N) |
任意位置插入或者删除元素 | 可能需要搬移元素,效率低,O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |