前言
上篇文章讲了如何用c语言实现单链表(无头单向非循环链表),这篇文章就来讲讲如何实现带头双向循环链表。
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都
是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带
来很多优势,实现反而简单了,下面跟着我把代码实现了就知道了。
在实现前,先讲讲哨兵位头节点,这个节点不存储有效的数据,只负责存储第一个有效节点的地址以找到第一个有效节点的位置。这个哨兵位就是这个链表的“头”。
一、代码实现讲解
实现的内容(头文件)
//List.h
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int LTDataType;
typedef struct SLTDListNode
{
struct SLTDListNode* next;
struct SLTDListNode* prev;
LTDataType val;
}LTNode;
//创建节点
LTNode* CreateNode(LTDataType x);
//初始化
LTNode* LTInit();
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//打印
void LTPrint(LTNode* phead);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//头删
void LTPopFront(LTNode* phead);
//找到值为x处的位置
LTNode* LTFind(LTNode* phead, LTDataType x);
//插入指定位置
void LTInsert(LTNode* pos, LTDataType x);
//删除指定位置
void LTErase(LTNode* pos);
//销毁链表
void LTDistroy(LTNode* phead);
1.1 哨兵位头节点的实现
LTNode* LTInit()
{
//创建哨兵位
LTNode* phead = CreateNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
一开始时无数据,先让哨兵位自己指向自自己。
1.2 插入数据
该链表有循环的特性之后,插入数据这块的实现会特别简单。
尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* tail = phead->prev;
LTNode* newnode = CreateNode(x);
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
在找尾节点这一块,单链表需要遍历一遍才能找到,时间复杂度较高,而这个带头双向循环链表就不必 那么麻烦,哨兵位的前一个就是尾节点。(先理解我是怎么实现的,第二部分我再说这个函数怎么用。)
如图:
头插
这里的图我就不画了,读者自己思考一下。
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* first = phead->next;
LTNode* newnode = CreateNode(x);
phead->next = newnode;
newnode->prev = phead;
first->prev = newnode;
newnode->next = first;
}
1.3删除数据
尾删
这里要先保存尾节点的前一个节点,以保证删除尾节点之后还能找到该节点,然后让这个节点指向哨兵位头节点,哨兵位头节点指向这个节点,以保证该链表的循环这一特性。
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
free(tail);
tailPrev->next = phead;
phead->prev = tailPrev;
}
图示:
头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != NULL);
LTNode* first = phead->next;
LTNode* second = first->next;
phead->next = second;
second->prev = phead;
free(first);
}
1.4指定位置插入删除数据
查找函数:找到节点,就返回节点的地址;找不到就返回NULL。
//下面的pos就是通过该函数查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->val == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
指定位置插入数据
//在pos的前面插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* posPrev = pos->prev;
LTNode* newnode = CreateNode(x);
posPrev->next = newnode;
newnode->prev = posPrev;
newnode->next = pos;
pos->prev = newnode;
}
若插入数据的位置在哨兵位头节点的前面,产生的效果就是尾插。
指定位置删除数据
先保存pos前后位置的节点,再让前后位置这两个节点链接以保证双向这一特性。再free掉pos的空间。(这个函数不是用来删除哨兵位的)
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* posPrev = pos->prev;
LTNode* posNext = pos->next;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
pos = NULL;
}
1.5销毁链表
先存下一个节点,再删除当前节点。
void LTDistroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
phead = NULL;
}
二、代码展示
头文件
//List.h
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int LTDataType;
typedef struct SLTDListNode
{
struct SLTDListNode* next;
struct SLTDListNode* prev;
LTDataType val;
}LTNode;
//创建节点
LTNode* CreateNode(LTDataType x);
//初始化
LTNode* LTInit();
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//打印
void LTPrint(LTNode* phead);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//头删
void LTPopFront(LTNode* phead);
//找到值为x处的位置
LTNode* LTFind(LTNode* phead, LTDataType x);
//插入
void LTInsert(LTNode* pos, LTDataType x);
//删除指定位置
void LTErase(LTNode* pos);
//销毁链表
void LTDistroy(LTNode** phead);
接口实现
#include"List.h"
//创建新节点
LTNode* CreateNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
assert(newnode);
newnode->val = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
//初始化
LTNode* LTInit()
{
//创建哨兵位
LTNode* phead = CreateNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->val == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void LTPrint(LTNode* phead)
{
assert(phead);
printf("哨兵位<=>");
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d<=>", cur->val);
cur = cur->next;
}
printf("\n");
}
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* tail = phead->prev;
LTNode* newnode = CreateNode(x);
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
free(tail);
tailPrev->next = phead;
phead->prev = tailPrev;
}
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* first = phead->next;
LTNode* newnode = CreateNode(x);
phead->next = newnode;
newnode->prev = phead;
first->prev = newnode;
newnode->next = first;
}
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != NULL);
LTNode* first = phead->next;
LTNode* second = first->next;
phead->next = second;
second->prev = phead;
free(first);
}
测试接口
//test.c
#include"List.h"
void test1()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPrint(plist);
LTPopBack(plist);
LTPrint(plist);
LTPushFront(plist, 5);
LTPrint(plist);
LTPopFront(plist);
LTPrint(plist);
LTDistroy(plist);
}
void test2()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPrint(plist);
LTNode* pos = LTFind(plist, 3);
LTErase(pos);
LTPrint(plist);
LTDistroy(plist);
}
int main()
{
test2();
return 0;
}
完整看下来的读者会发现带头双向循环链表的实现比单链表的实现要简单很多(甚至比顺序表的实现也更简单)。
三、顺序表和链表的区别
不同点 | 顺序表 | 链表 |
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定 连续 |
随机访问 | 支持O(1) | 不支持:O(N) |
任意位置插入或者删除 元素 | 可能需要搬移元素,效率低 O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要 扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
缓存利用率 | 高 | 低 |