双向带头链表
带头双向链表是一种数据结构,它是双向链表的一个变体。在双向链表中,每个节点都有两个指针,一个指向前一个节点,另一个指向后一个节点。这种结构允许在两个方向上遍历链表
带头双向链表的特点是它有一个额外的“头节点”(或称为“哨兵节点”),这个头节点不包含实际的数据,而是作为链表的起始点。头节点的存在简化了链表操作,因为不需要特别处理链表为空的情况,或是对链表的第一个和最后一个节点进行特殊操作
带头双向链表具有以下特点:
-
头节点:这是一个特殊的节点,通常不存储任何数据(或者存储一些代表链表状态的元信息),它的存在是为了简化链表操作。
-
双向链接:链表中的每个节点都有两个链接:一个指向前一个节点,另一个指向后一个节点。头节点也遵循这个规则,它的前一个节点链接到链表的最后一个节点,后一个节点链接到链表的第一个节点
-
插入和删除操作:由于链表是双向的,所以可以从两个方向插入和删除节点。头节点的存在使得在链表头部插入和删除节点变得更加方便
-
遍历:可以从头节点开始,向前或向后遍历整个链表
-
灵活性和效率:相比于单向链表,双向链表(尤其是带头节点的)在进行某些操作时更加灵活和高效,尤其是在需要频繁地插入和删除节点时
双向带头链表实现的功能介绍
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int ListDataType;
typedef struct ListNode
{
ListDataType _data;
struct ListNode* _prev;
struct ListNode* _next;
}ListNode;
ListNode* ListCreate(ListDataType x);
//双向链表的初始化
ListNode* ListInit();
//双向链表的删除
void ListDestory(ListNode* pHead);
//双向链表的打印
void ListPrint(ListNode* pHead);
//双向链表的尾插
void ListPushBack(ListNode* pHead, ListDataType x);
//双向链表的头插
void ListPushFront (ListNode* pHead, ListDataType x);
//双向链表的尾删
void ListPopBack(ListNode* pHead);
//双向链表的头删
void ListPopFront(ListNode* pHead);
//双向链表的查找
ListNode* ListNodeFinde(ListNode* pHead, ListDataType x);
//双向链表的在pos位置前删除
void ListInsert(ListNode* pos, ListDataType x);
//双向链表删除pos点
void ListErase(ListNode* pos);
下面我们将一步一步的介绍每个功能:
链表的初始化和新结点的创建
ListNode* ListCreate(ListDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL)
{
perror("malloc failed");
exit(-1);
}
newnode->_next = NULL;
newnode->_prev = NULL;
newnode->_data = x;
return newnode;
}
这个是一个结点的简单样子
使用Malloc创建一个ListNode
的结构体。ListNode
是一个代表双向链表节点的结构体,包含至少三个成员:_next
(指向下一个节点的指针),_prev
(指向前一个节点的指针),和 _data
(存储数据的变量)
ListNode* ListInit()
{
ListNode* phead = ListCreate(-1);
phead->_next = phead;
phead->_prev = phead;
return phead;
}
这是一个哨兵位,在初始化的时候它的头尾都指向自己
首先调用 ListCreate(-1)
来创建一个新的链表节点。这里的 -1
是用于初始化头节点的数据值。在许多实现中,头节点的数据部分通常不用于存储有效数据,而是作为链表操作的辅助
接着,将新创建的节点(phead
)的 _next
和 _prev
指针都指向它自己。这种做法是典型的空双向链表(带头节点)的初始化方式,其中头节点的前一个和后一个节点都是它自己,表示链表为空
链表的打印
void ListPrint(ListNode* phead)
{
assert(phead);
ListNode* cur = phead->_next;
printf("head <-> ");
while (cur != phead)
{
printf("%d <-> ", cur->_data);
cur = cur->_next;
}
printf("\n");
}
-
初始化遍历指针:定义一个
ListNode*
类型的指针cur
,并将其初始化为指向头节点的下一个节点(phead->_next
)。这是遍历链表的起点 -
打印头节点标记:首先打印出
"head <-> "
,表示链表的起始 -
遍历链表:使用一个
while
循环遍历链表。只要cur
不等于phead
(即没有回到头节点,这意味着链表尚未遍历完),循环就继续。在每次循环中,打印当前节点的数据(cur->_data
),然后将cur
更新为下一个节点(cur->_next
)
链表的头插和头删
头插
void ListPushFront(ListNode* phead, ListDataType x)
{
assert(phead);
ListNode* newnode = ListCreate(x);
ListNode* tmp = phead->_next;
phead->_next = newnode;
newnode->_prev = phead;
newnode->_next = tmp;
//ListInsert(phead->_next, x);
}
头插的例子
-
创建新节点:调用
ListCreate(x)
创建一个新的节点,将传入的数据x
存储在该节点中 -
保存当前第一个节点:将链表的当前第一个节点(即头节点的下一个节点,
phead->_next
)保存到一个临时变量tmp
中 -
将新节点插入到链表前端:
- 将头节点的
_next
指针指向新节点,即phead->_next = newnode
- 将新节点的
_prev
指针指向头节点,即newnode->_prev = phead
- 将新节点的
_next
指针指向原先的第一个节点(即tmp
),即newnode->_next = tmp
- 将头节点的
头删
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);
}
-
定位第一个和第二个节点:
first
指向链表的第一个节点,即头节点的下一个节点(phead->_next
)second
指向链表的第二个节点,即first
节点的下一个节点(first->_next
)
-
移除第一个节点:
- 将头节点的
_next
指针指向second
,即phead->_next = second
- 将
second
节点的_prev
指针指向头节点,即second->_prev = phead
- 释放(
free
)first
节点所占用的内存 - 将
first
设置为NULL
以避免悬挂指针
- 将头节点的
链表的尾插和尾删
尾插
void ListPushBack(ListNode* phead, ListDataType x)
{
assert(phead);
ListNode* tail = phead->_prev;
ListNode* newnode = ListCreate(x);
tail->_next = newnode;
newnode->_prev = tail;
phead->_prev = newnode;
newnode->_next = phead;
//ListInsert(phead, x);
}
头插和尾插类似
-
找到尾节点:通过
phead->_prev
获取链表的尾节点,存储在tail
变量中。在带头节点的双向链表中,头节点的_prev
成员通常指向链表的最后一个节点 -
创建新节点:调用
ListCreate(x)
创建一个新的节点,其数据字段被设置为x
-
将新节点链接到链表:
- 将当前尾节点的
_next
指针指向新节点:tail->_next = newnode
- 将新节点的
_prev
指针指向当前尾节点:newnode->_prev = tail
- 将当前尾节点的
-
更新头节点和新节点的指针:
- 将头节点的
_prev
指针指向新节点,使新节点成为链表的新尾节点:phead->_prev = newnode
- 将新节点的
_next
指针指向头节点,维持链表的循环结构:newnode->_next = phead
- 将头节点的
尾删
void ListPopBack(ListNode* phead)
{
assert(phead);
assert(phead->_next != phead);
ListNode* tail = phead->_prev;
ListNode* cur = tail->_prev;
cur->_next = phead;
phead->_prev = cur;
free(tail);
tail = NULL;
//ListErase(phead->_prev);
}
-
定位尾节点和倒数第二个节点:
tail
指向链表的尾节点,即头节点的_prev
成员cur
指向倒数第二个节点,即tail
的_prev
成员
-
移除尾节点:
- 将倒数第二个节点的
_next
指针指向头节点,即cur->_next = phead
- 将头节点的
_prev
指针指向倒数第二个节点,即phead->_prev = cur
- 释放(
free
)tail
节点所占用的内存 - 将
tail
设置为NULL
以避免悬挂指针
- 将倒数第二个节点的
链表的查找
//双向链表的查找
ListNode* ListNodeFinde(ListNode* phead, ListDataType x)
{
assert(phead);
ListNode* cur = phead->_next;
while (cur != phead)
{
if (cur->_data == x)
{
return cur;
}
cur = cur->_next;
}
return NULL;
}
-
初始化遍历指针:定义一个
ListNode*
类型的指针cur
,并将其初始化为指向头节点的下一个节点(phead->_next
)。这是遍历链表的起点 -
遍历链表:使用一个
while
循环遍历链表。只要cur
不等于phead
(即没有回到头节点,这意味着链表尚未遍历完),循环就继续 -
查找匹配节点:
- 在每次循环中,检查当前节点的数据(
cur->_data
)是否等于查找的值x
- 如果找到匹配的节点,则返回该节点的指针
- 如果没有找到,将
cur
更新为下一个节点(cur->_next
)
- 在每次循环中,检查当前节点的数据(
-
未找到匹配节点:如果遍历完整个链表都没有找到匹配的节点,最后函数返回
NULL
链表的删除pos点内容和pos点前插入
pos点前插入
//双向链表的在pos位置前插入
void ListInsert(ListNode* pos, ListDataType x)
{
assert(pos);
ListNode* newnode = ListCreate(x);
ListNode* prev = pos->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
pos点插入
-
创建新节点:调用
ListCreate(x)
创建一个新的节点,其数据字段被设置为x
-
定位
pos
的前一个节点:通过pos->_prev
获取pos
节点的前一个节点,存储在prev
变量中 -
将新节点插入到链表中:
- 将
prev
节点的_next
指针指向新节点:prev->_next = newnode
- 将新节点的
_prev
指针指向prev
:newnode->_prev = prev
- 将新节点的
_next
指针指向pos
:newnode->_next = pos
- 将
pos
节点的_prev
指针指向新节点:pos->_prev = newnode
- 将
pos点删除
//双向链表删除pos点
void ListErase(ListNode* pos)
{
assert(pos);
ListNode* prev = pos->_prev;
ListNode* next = pos->_next;
prev->_next = next;
next->_prev = prev;
free(pos);
pos = NULL;
}
-
定位前后节点:
prev
指向pos
节点的前一个节点,即pos->_prev
next
指向pos
节点的下一个节点,即pos->_next
-
从链表中移除
pos
节点:- 将
prev
节点的_next
指针指向next
,即prev->_next = next
- 将
next
节点的_prev
指针指向prev
,即next->_prev = prev
- 将
-
释放内存并清除指针:
- 使用
free(pos)
释放pos
节点所占用的内存 - 将
pos
设置为NULL
以避免悬挂指针
- 使用
链表和顺序表的优势和缺陷
链表(双向)优势:
- 任意位置插入删除都是O(1)
- 按需申请释放,合理利用空间、不存在浪费
问题:
- 下标随机访问不方便,O(N)
顺序表问题:
- 头部或者中间插入删除效率低,要挪动数据。O(N)
- 空间不够需要扩容,扩容一定的消耗,且可能存在一定的空间浪费
- 只适合尾插尾删
优势(物理内存延续)
- 支持下标的随机访问O(N)