系列文章目录
文章目录
- 系列文章目录
- 前言
- 一、哨兵位的头节点
- 二、双向链表的结点
- 三、接口函数的实现
- 1、创建结点
- 2、初始化
- 3、尾插与尾删
- 4、头插与头删
- 5、打印
- 6、查找
- 7、随机插入与随机删除
- 8、判空、长度与销毁
- 四、顺序表和链表的对比
- 总结
前言
一般题目给的单链表是无头单向非循环链表,但是我们可以升级成双向带头循环链表,这个链表比起单链表更有优势。
一、哨兵位的头节点
上面带有head头结点的链表就是带头的链表,题目中的链表一般没有头节点,phead指针直接指向第一个结点,而带头的链表phead指针指向头结点,头节点指向第一个结点,一般称为 哨兵位的头节点。
二、双向链表的结点
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
比起单链表的结点,多了指向前一个结点的指针——prev。
三、接口函数的实现
1、创建结点
LTNode* BuyListNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
assert(newnode);
newnode->next = NULL;
newnode->prev = NULL;
newnode->data = x;
return newnode;
}
2、初始化
LTNode* InitList()
{
LTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
初始化即开辟一个头节点,然后让这个头节点的前后指针域都指向自己。
3、尾插与尾删
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
newnode->prev = phead->prev;
phead->prev->next = newnode;
newnode->next = phead;
phead->prev = newnode;
}
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);//空
phead->prev = phead->prev->prev;
free(phead->prev->next);
phead->prev->next = phead;
}
双向带头循环链表并没有单独讨论空链表的情况,这就是头节点的好处,之所以不用讨论就是因为节点的个数不可能为0,最少也包括一个头节点。
4、头插与头删
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->next;
newnode->next = phead->next;
newnode->prev = phead;
tail->prev = newnode;
phead->next = newnode;
}
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* tail = phead->next->next;
LTNode* tailPrev = phead->next;
phead->next = tail;
tail->prev = phead;
free(tailPrev);
}
5、打印
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
printf("[%p | %d | %p]", cur->prev, cur->data, cur->next);
if(cur->next != phead)printf("<->");
cur = cur->next;
}
printf("\n");
}
就是从头遍历一遍即可,但是需要注意的是,这是一个循环链表,如果我们不加限制条件的话,他会一直循环下去。所以,我们这里需要加上判断条件。
6、查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
assert(phead->next != phead);
LTNode* cur = phead->next;
while (cur->data != x && cur != phead)cur = cur->next;
if (cur == phead)return NULL;
else return cur;
}
也要加限制条件。
7、随机插入与随机删除
void* LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = BuyListNode(x);
LTNode* tail = pos->prev;
tail->next = newnode;
newnode->next = pos;
newnode->prev = tail;
pos->prev = newnode;
}
void* LTErase(LTNode* pos)
{
assert(pos);
LTNode* tail = pos->prev;
tail->next = pos->next;
tail->next->prev = tail;
free(pos);
}
实现方式之前的头尾操作一样,也可以复用到头尾操作中。
8、判空、长度与销毁
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next != phead;
}
size_t LTSize(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
size_t size = 0;
while (cur != phead)
{
size++;
cur = cur->next;
}
return size;
}
void LTDestory(LTNode* phead)
{
LTNode* cur = phead->next;
while (cur != phead)LTErase(cur);
free(phead);
}
逐个释放就行。
四、顺序表和链表的对比
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持O(1) | 不支持:O(N) |
任意位置插入或者删除元素 | 可能需要搬移元素,效率低O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
缓存利用率 | 高 | 低 |
总结
链表和顺序表各有优势,带头双向链表比起单链表更加方便操作。
深窥自己的心,而后发觉一切的奇迹在你自己。——培根