C语言数据结构中已经提到链表的有关知识,链表根据以下三类特点可以分为八种:带头或不带头(也叫哨兵位、辅助表元等),单向或双向,循环或不循环。平常使用较多的,主要是不带头单向不循环链表(也就是最先接触的单链表)和带头双向循环链表。这篇文章将介绍双向链表的一些基本操作。
相较于普通的单链表,双向链表的节点结构体多了另一个方向的指针即前驱指针,以下是结构体内容:
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* prev;
struct ListNode* next;
}LTNode;
另外每个双向链表需要有作为哨兵位的表头,表头指向第一个表元(如果有)。最后尾表元的后继表元应该是表头,这样使得整个链表是循环的,同时每个节点的前驱指针指向上一个节点。那么以上双向链表的基本构成,下面是一张可以帮助了解的图片演示:
下面我们完成双向链表的基本操作函数,首先是创建双向链表和获取表元:
//创建一个节点
LTNode* LTBuyNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
newnode->data = x;
newnode->next = newnode->prev = newnode;
//对于一个新的节点,他的前驱指针和后继指针都指向他本身
return newnode;
}
LTNode* LTInit()
{
LTNode* phead = LTBuyNode(-1);
//要创建双向链表,只要建立一个头表元
//头表元的数值无意义,但最好不要与其他表元冲突或重合
return phead;
}
为了程序完整性,接下来先完成双向链表的销毁操作,这与单链表的类似,只要朝一个方向历边即可,但是不能忘记处理表头:
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
phead = NULL;
}
双向列表的打印,这与单链表一致:
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
然后就是较为关键的头插和尾差代码,这时将体现出不同。每增加一个节点,我们就要修改它上一个节点的后继节点,及其上一个节点原本的后继节点的前驱节点,同时这里的增加还包括将新增节点的前驱、后继节点“链接”在链表内。
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead;
newnode->prev = phead->prev;
phead->prev->next = newnode;
phead->prev = newnode;
}
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
相应的完成头删和尾删的函数,注意这里的头指的是第一个表元,头表元实质上不会被改变。删除时,除了释放空间,还要注意将删除表元的前驱和后继表元新的链接方式处理好:
void LTPopBack(LTNode* phead)
{
assert(phead);
//删除时链表不能无表元
assert(phead->next != phead);
LTNode* del = phead->prev;
//要删除的表元
LTNode* prev = del->prev;
//新的尾表元
prev->next = phead;
phead->prev = prev;
free(del);
del = NULL;
}
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* del = phead->next;
LTNode* next = del->next;
next->prev = phead;
phead->next = next;
free(del);
del=NULL;
}
最后就只有在指定位置的增加、删除操作,和查找操作了,代码的原理都是基本一样的了:
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
//删除pos位置的节点
void LTErase(LTNode* pos)
{
assert(pos);
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
//这一类操作只涉及局部,函数不需要传头表元
查找只需要按顺序历边,在完整代码中再体现。可以看的出来,只要注意好每个节点的四根“连线”的处理,双向链表并不复杂,而且还有很多便利之处。
以下给出完整代码和一个测试用的主函数。
List.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* prev;
struct ListNode* next;
}LTNode;
LTNode* LTInit();
void LTDestroy(LTNode* phead);
void LTPrint(LTNode* phead);
void LTPushBack(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);
void LTPushFront(LTNode* phead, LTDataType x);
void LTPopFront(LTNode* phead);
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
void LTErase(LTNode* pos);
LTNode* LTFind(LTNode* phead, LTDataType x);
List.c
#define _CRT_SECURE_NO_WARNINGS
#include"List.h"
//创建一个节点
LTNode* LTBuyNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
newnode->data = x;
newnode->next = newnode->prev = newnode;
//对于一个新的节点,他的前驱指针和后继指针都指向他本身
return newnode;
}
LTNode* LTInit()
{
LTNode* phead = LTBuyNode(-1);
//要创建双向链表,只要建立一个头表元
//头表元的数值无意义,但最好不要与其他表元冲突或重合
return phead;
}
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
phead = NULL;
}
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead;
newnode->prev = phead->prev;
phead->prev->next = newnode;
phead->prev = newnode;
}
void LTPopBack(LTNode* phead)
{
assert(phead);
//删除时链表不能无表元
assert(phead->next != phead);
LTNode* del = phead->prev;
//要删除的表元
LTNode* prev = del->prev;
//新的尾表元
prev->next = phead;
phead->prev = prev;
free(del);
del = NULL;
}
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* del = phead->next;
LTNode* next = del->next;
next->prev = phead;
phead->next = next;
free(del);
del=NULL;
}
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
//删除pos位置的节点
void LTErase(LTNode* pos)
{
assert(pos);
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
//这一类操作只涉及局部,函数不需要传头表元
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
test.c
#define _CRT_SECURE_NO_WARNINGS
#include"List.h"
int main()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushFront(plist, 4);
LTPushFront(plist, 5);
LTPushFront(plist, 6);
LTPrint(plist);
LTPopBack(plist);
LTPopFront(plist);
LTPrint(plist);
LTNode* ret = LTFind(plist, 1);
if (ret)
{
printf("找到了!\n");
LTInsert(ret, 666);
LTPrint(plist);
}
else {
printf("未找到!\n");
}
LTErase(ret);
LTPrint(plist);
LTDestroy(plist);
plist = NULL;
return 0;
}