一、什么是链表
链表是一种常见的数据结构,主要用于存储和管理数据。
1.1 链表的类型
根据链表的实现方式和特点,可以分为以下几种类型:
- 单向链表:每个节点只有一个指针,指向下一个节点。
- 双向链表:每个节点有两个指针,一个指向前一个节点,一个指向后一个节点。
- 循环链表:最后一个节点的指针指向第一个节点,形成一个环形结构。
- 双向循环链表:既有双向链表的特点,也有循环链表的特点,即最后一个节点的指针指向第一个节点,同时第一个节点的指针指向最后一个节点。
此外,链表还可以分为带哨兵位和不带哨兵位的链表。
1.2 单链表和双向链表的区别
- 双向循环链表可以在单向链表的基础上实现双向遍历,而单向链表只能单向遍历;
- 在插入和删除节点时,双向循环链表比单向链表更灵活;
- 双向链表需要更多的空间,实现复杂度更高,但代码写起来更为简单。
二、实现带头双向循环链表的准备工作
2.1 实现的功能
- 创建返回链表的头结点
- 双向链表销毁
- 双向链表打印
- 双向链表尾插
- 双向链表尾删
- 双向链表头插
- 双向链表头删
- 双向链表查找
- 双向链表在pos的前面进行插入
- 双向链表删除pos位置的节点
为此,我们定义以下这些函数来实现这些功能:
- ListNode* ListCreate()
- void ListDestory(ListNode* pHead)
- void ListPrint(ListNode* pHead)
- void ListPushBack(ListNode* pHead, LTDataType x)
- void ListPopBack(ListNode* pHead);
- void ListPushFront(ListNode* pHead, LTDataType x)
- void ListPopFront(ListNode* pHead)
- ListNode* ListFind(ListNode* pHead, LTDataType x)
- void ListInsert(ListNode* pos, LTDataType x)
- void ListErase(ListNode* pos)
2.2 准备文件
创建DList.h,DList.c,Test.c一个头文件,两个源文件。
DList.h文件实现功能函数的声明、头文件与宏定义
DList.c文件写链表功能的具体代码
Test.c文件用于测试
这样在DList.c和Test.c文件中加上#include "DList.h"
即可。
三、具体实现
3.1 链表结构
由于我们不知道链表中存储数据的类型,所以我们能将数据类型重命名,方便修改。
双向链表中,每个节点都有指向前一个节点和后一个节点的指针。
我们便可以在DList.h
中定义链表:
typedef int LTDataType;
typedef struct ListNode
{
LTDataType _data;//存储的数据
struct ListNode* _next;//指向前一个节点的指针
struct ListNode* _prev;//指向后一个节点的指针
}ListNode;
对于每个数据,用宏定义它的打印格式:
#define FORMAT "%d"
3.2 创建返回链表的头结点
3.2.1 创建一个新节点
不少功能中都需要创建一个新的节点,为了方便,避免代码重复,我们可以专门定义一个函数BuyLTNode(LTDataType x)
实现创建新节点的功能,返回值为ListNode*
。
ListNode* BuyLTNode(LTDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL)//申请空间失败
{
perror("malloc fail");
return NULL;
}
newnode->_data = x;
newnode->_prev = NULL;
newnode->_next = NULL;
return newnode;
}
3.2.2 创建返回链表的头结点
头结点即哨兵位,它不存储任何数据,只用于标记链表的开始或结束位置。我们可以传值-1来利用BuyLTNode()
函数。
因为是循环链表,它的前节点和后节点都指向自己。
ListNode* ListCreate()
{
ListNode* head = BuyLTNode(-1);
head->_prev = head;
head->_next = head;
return head;
}
3.3 双向链表销毁
从第一个节点(头结点的下一个)遍历到头结点,一个一个释放。
void ListDestory(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->_next;
while (cur != pHead)
{
ListNode* next = cur->_next;//先保存下一个节点
free(cur);
cur = next;
}
free(pHead);//头结点也要释放
}
3.4 双向链表打印
“guard"是头结点,”<==>"表示双向。
void ListPrint(ListNode* pHead)
{
assert(pHead);
printf("guard<==>");//打印头结点
ListNode* cur = pHead->_next;
while (cur!=pHead)
{
printf(FORMAT"<==>", cur->_data);
cur = cur->_next;
}
printf("guard\n");//循环
}
3.5 双向链表尾插
相比于单链表只能从头遍历到尾才能尾插而言,双链表的好处就体现出来了,头结点的前节点就是尾。需要注意的是newnode->_prev = pHead->_prev;
一定要在 pHead->_prev = newnode;
的前面。
void ListPushBack(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* newnode = BuyLTNode(x);
newnode->_next = pHead;
newnode->_prev = pHead->_prev;
pHead->_prev->_next = newnode;
pHead->_prev = newnode;
}
我们可以在Test.c中测试代码是否正确:
3.6 双向链表尾删
让尾的next指向头,头的prev指向尾的prev,free尾。
特殊情况:当双链表为空时就不用尾删,我们可以写一个判空函数bool LTEmpty(ListNode* pHead)
(C99提供了Bool型,头文件<stdbool.h>
)。
3.6.1 判空函数
空为真(ture),非空为假(false)。
bool LTEmpty(ListNode* pHead)
{
assert(pHead);
return pHead->_next == pHead;
}
3.6.2 尾删函数
void ListPopBack(ListNode* pHead)
{
assert(pHead);
assert(!LTEmpty(pHead));
ListNode* del = pHead->_prev;//先保存
del->_prev->_next = pHead;
pHead->_prev = del->_prev;
free(del);
}
测试:
3.7 双向链表头插
思路就是先连newnode的next与prev,再断头结点的next与原先第一个节点的prev,需要注意的是pHead->_next->_prev = newnode;
要放在
pHead->_next = newnode;
的前面(或者先保存第一个节点)。
void ListPushFront(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* newnode = BuyLTNode(x);
newnode->_prev = pHead;
newnode->_next = pHead->_next;
pHead->_next->_prev = newnode;
pHead->_next = newnode;
}
测试:
3.8 双向链表头删
让头结点的next指向第二个节点,第二个节点的prev指向头节点。
void ListPopFront(ListNode* pHead)
{
assert(pHead);
assert(!LTEmpty(pHead));
ListNode* del = pHead->_next;
pHead->_next = del->_next;
del->_next->_prev = pHead;
}
测试:
3.9 双向链表查找
找到了就返回这个节点的地址,找不到就返回NULL.
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;
}
测试:
3.10 双向链表在pos的前面进行插入
和上面一样,先连newnode的prev与next,再断原本的链表。
pos->_prev->_next = newnode;
在
pos->_prev = newnode;
之前。
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* newnode = BuyLTNode(x);
newnode->_prev = pos->_prev;
newnode->_next = pos;
pos->_prev->_next = newnode;
pos->_prev = newnode;
}
有了这个函数后,
头插可以换成ListInsert(pHead->next,LTDataType x);
尾插可以换成ListInsert(pHead, LTDataType x);
测试:
3.11 双向链表删除pos位置的节点
这个就很简单了,把前后连起来就行了。
void ListErase(ListNode* pos)
{
assert(pos);
pos->_prev->_next = pos->_next;
pos->_next->_prev = pos->_prev;
free(pos);
}
测试:
四、代码
4.1 DList.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
#define FORMAT "%d"
// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
LTDataType _data;
struct ListNode* _next;
struct ListNode* _prev;
}ListNode;
// 创建返回链表的头结点.
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
4.2 DList.c
#include "DList.h"
ListNode* BuyLTNode(LTDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL)//申请空间失败
{
perror("malloc fail");
return NULL;
}
newnode->_data = x;
newnode->_prev = NULL;
newnode->_next = NULL;
return newnode;
}
ListNode* ListCreate()
{
ListNode* head = BuyLTNode(-1);
head->_prev = head;
head->_next = head;
return head;
}
void ListDestory(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->_next;
while (cur!=pHead)
{
ListNode* next = cur->_next;
free(cur);
cur = next;
}
free(pHead);
}
void ListPrint(ListNode* pHead)
{
assert(pHead);
printf("guard<==>");//打印头结点
ListNode* cur = pHead->_next;
while (cur!=pHead)
{
printf(FORMAT"<==>", cur->_data);
cur = cur->_next;
}
printf("guard\n");
}
void ListPushBack(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* newnode = BuyLTNode(x);
newnode->_next = pHead;
newnode->_prev = pHead->_prev;
pHead->_prev->_next = newnode;
pHead->_prev = newnode;
}
bool LTEmpty(ListNode* pHead)
{
assert(pHead);
return pHead->_next == pHead;
}
void ListPopBack(ListNode* pHead)
{
assert(pHead);
assert(!LTEmpty(pHead));
ListNode* del = pHead->_prev;
del->_prev->_next = pHead;
pHead->_prev = del->_prev;
free(del);
}
void ListPushFront(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* newnode = BuyLTNode(x);
newnode->_prev = pHead;
newnode->_next = pHead->_next;
pHead->_next->_prev = newnode;
pHead->_next = newnode;
}
void ListPopFront(ListNode* pHead)
{
assert(pHead);
assert(!LTEmpty(pHead));
ListNode* del = pHead->_next;
pHead->_next = del->_next;
del->_next->_prev = pHead;
}
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;
}
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* newnode = BuyLTNode(x);
newnode->_prev = pos->_prev;
newnode->_next = pos;
pos->_prev->_next = newnode;
pos->_prev = newnode;
}
void ListErase(ListNode* pos)
{
assert(pos);
pos->_prev->_next = pos->_next;
pos->_next->_prev = pos->_prev;
free(pos);
}
ps:大家一定要养成free的习惯,博主前面就忘了…引以为戒。