双向链表的组成
双向链表主要分为三部分
-
前项指针
-
后项指针
-
数据域
-
一般情况下,创建单一个体,前项指针和后项指针都赋值为空
-
一般采用记录头节点和记录尾节点,再封装的方式写双向链表
双向链表的结构体描述(节点)
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef struct Node
{
int data; //数据域
struct Node* front; //前项指针
struct Node* tail; //后项指针
}NODE,*LPNODE;
再定义一个结构体(链表)
-
通过记录头节点和尾节点的方式去描述链表
typedef struct DList
{
LPNODE frontNode; //头节点
LPNODE tailNode; //尾节点
int curSize; //当前节点个数
}DLIST,*LPDLIST;
创建节点
把用户的数据变成一个结构体变量
LPNODE createNode(int data)
{
//申请内存
LPNODE newNode = (LPNODE)malloc(sizeof(NODE));
//断言处理
assert(newNode);
//前项指针、后项指针置为空
newNode->front = NULL;
newNode->tail = NULL;
//给数据做初始化
newNode->data = data;
return newNode;
}
创建链表
-
描述链表最初的状态
LPDLIST createDList()
{
LPDLIST list = (LPDLIST)malloc(sizeof(DLIST)); //用结构体变量描述最初状态 做动态内存申请
assert(list);
list->frontNode = NULL; //链表刚开始是空的 头尾节点指向空
list->tailNode = NULL;
list->curSize = 0; //当前元素个数为0
return list;
}
头插法
①新节点的后项指针指向原来链表的头节点
②原来链表的头节点的前项指针指向新节点
③把原来的头节点移到新节点的位置
//要插入的链表 插入的数据
void push_front(LPDLIST list,int data)
{
//创建新节点
LPNODE newNode = createNode(data);
//list为空 没办法做插入
if (list == NULL)
return;
//只有一个节点 插入的节点既是头节点也是尾节点
if (list->curSize == 0)
{
//头节点和尾节点都是指向这个节点
list->frontNode = newNode;
list->tailNode = newNode;
list->curSize++;
}
else
{
//新节点的后项指针指向原来链表的头节点
newNode->tail = list->frontNode;
//原来链表的头节点的前项指针指向新节点
list->frontNode->front = newNode;
//把原来的头节点移到新节点的位置
list->frontNode = newNode;
list->curSize++;
}
}
//测试代码
LPDLIST list = createDList();
push_front(list, 1); //1
push_front(list, 2); //2 1
printByFront(list); //1 2 通过前项指针做打印 从后往前
printByTail(list); //2 1 通过后项指针做打印 从前往后
打印链表
可以通过前项指针去做打印,也可以通过后项指针去做打印
通过前项指针做打印
从最后一个节点往前走,由于记录头节点和尾节点,所以就非常简单啦🙄
//要打印的链表
void printByFront(LPDLIST list)
{
//定义一个移动的指针指向最后一个节点
LPNODE pmove = list->tailNode;
//不为空
while (pmove != NULL)
{
//打印数据
printf("%d\t", pmove->data);
//用前项指针去做遍历
pmove = pmove->front;
}
printf("\n");
}
通过后项指针做打印
类似单链表的 next 指针的方式做遍历
//要打印的链表
void printByTail(LPDLIST list)
{
//定义一个移动的指针指向第一个节点
LPNODE pmove = list->frontNode;
//不为空
while (pmove)
{
//打印数据
printf("%d\t", pmove->data);
//用后项指针去做遍历
pmove = pmove->tail;
}
printf("\n");
}
尾插法
由于记录了尾节点的位置,所以就非常简单啦🙄
①原来链表的尾节点的后项指针指向新节点
②新节点的前项指针指向原来链表的尾节点
③把原来的尾节点移到新节点的位置
void push_back(LPDLIST list, int data)
{
LPNODE newNode = createNode(data);
//list为空 没办法做插入
if (list == NULL)
return;
//只有一个节点 插入的节点既是头节点也是尾节点
if (list->curSize == 0)
{
//头节点和尾节点都是指向这个节点
list->frontNode = newNode;
list->tailNode = newNode;
list->curSize++;
}
else
{
//原来链表的尾节点的后项指针指向新节点
list->tailNode->tail = newNode;
//新节点的前项指针指向原来链表的尾节点
newNode->front = list->tailNode;
//把原来的尾节点移到新节点的位置
list->tailNode = newNode;
list->curSize++;
}
}
//测试代码
push_back(list, 999);
printByTail(list); //2 1 999
printByFront(list); //999 1 2
在指定位置前做插入
①首先找到相邻的两个节点
②创建新节点做插入四步操作
单链表只需要两步操作,双向链表需要四步,因为有二个指针
//要插入的链表 用数据充当指定位置 要插入的数据
void push_appoin(LPDLIST list, int posData, int data)
{
//有可能会修改表头 如果list为空 没办法做插入
if (list == NULL||list->curSize==0)
return;
//第一个节点里面的数据等于指定位置的数据
if (list->frontNode->data == posData)
{
//头插法
push_front(list, data);
}
else
{
//其他情况 定义两个移动的指针做查找 单向还是双向是无关紧要的 只需要遍历一遍就可以了 往哪边遍历都可以
LPNODE preNode = list->frontNode;
LPNODE curNode = list->frontNode;
//当前节点不等于空 并且当前节点里面的数据不等于指定数据
while (curNode != NULL && curNode->data != posData)
{
//并排往下走 前面那个节点保存一下这个位置
preNode = curNode;
//当前节点走到原来位置的下一个
curNode = preNode->tail;
}
//分析结果做插入 当前节点等于空表示没有找到指定位置 无法做插入
if (curNode == NULL)
{
printf("未找到指定位置,无法插入!\n");
}
else
{
//找到了 创建一个新节点做插入
LPNODE newNode = createNode(data);
//tail指向
preNode->tail = newNode;
newNode->tail = curNode;
//front指向
curNode->front = newNode;
newNode->front = preNode;
list->curSize++;
}
}
}
//测试代码
push_appoin(list, 999, 888); //在999前面插入888
printByTail(list); //2 1 888 999
printByFront(list); //999 888 1 2
头删法
保存表头的下一个然后删除表头
需要处理表头下一个节点的前项指针,如果前项指针没有做处理,通过前项做遍历会陷入死循环,要让前项指针等于空作为结束条件
void pop_front(LPDLIST list)
{
//只有0个节点 没办法做删除
if (list == NULL || list->curSize == 0)
{
printf("无法删除!链表为空\n");
return;
}
//保存表头的下一个节点
LPNODE nextNode = list->frontNode->tail; //nextNode=NULL
//删除表头
free(list->frontNode);
//把头节点移到 nextNode 的位置,只有1个节点删除的节点是表头,把表头置为空 NULL
list->frontNode = nextNode;
//如果只有1个节点,删除的节点是表头,nextNode=NULL,没办法把nextNode->front置为空,所以要判断nextNode!=NULL
if (nextNode != NULL)
{
//把nextNode的前项指针置为空
nextNode->front = NULL;
}
else
{
//如果nextNode=NULL,删除的节点也是表尾,尾节点内存被释放了,需要把表尾置为空
list->tailNode = NULL;
}
list->curSize--;
}
//测试代码
pop_front(list);
尾删法
由于记录了尾部的位置,因此双向链表的尾删法不需要找尾部的上一个,单向链表用记录尾部的方式,依然需要去找尾节点的上一个
删掉最后一个节点一定会影响它上一个节点,尾节点的上一个节点通过尾节点的前项指针就可以找到,只需要处理尾节点上一个节点的后项指针就可以了,把它置为空,然后把尾节点释放,最后把表尾移到上一个位置
void pop_back(LPDLIST list)
{
//只有0个节点 没办法做删除
if (list == NULL || list->curSize == 0)
{
printf("链表为空无法删除!\n");
return;
}
//保存表尾的上一个节点
LPNODE preNode = list->tailNode->front;
//删除表尾
free(list->tailNode);
//把尾节点移到 preNode 的位置
list->tailNode = preNode;
//如果只有一个节点preNode=NULL
if (preNode != NULL)
{
preNode->tail = NULL;
}
else
{
//如果preNode=NULL,删除的节点也是表头,需要把表头置为空
list->frontNode = NULL;
}
list->curSize--;
}
//测试代码
pop_back(list);
指定位置删除
①preNode 的后项指针指向 curNode->tail
②curNode->tail 的前项指针指向 preNode
//要删除的链表 指定数据
void pop_appoin(LPDLIST list, int posData)
{
//只有0个节点 没办法做删除
if (list == NULL || list->curSize == 0)
{
printf("链表为空无法删除!");
return;
}
//头节点的数据等于指定数据
if (list->frontNode->data == posData)
{
//头删法
pop_front(list);
return;
}
//其他情况 定义两个移动的指针做查找
LPNODE preNode = list->frontNode;
LPNODE curNode = list->frontNode;
//当前节点不等于空 并且当前节点里面的数据不等于指定数据
while (curNode != NULL&&curNode->data!=posData)
{
//并排往下走 前面那个节点保存一下这个位置
preNode = curNode;
//当前节点走到原来位置的下一个
curNode = preNode->tail;
}
//分析结果做插入 当前节点等于空表示没有找到指定位置 无法做删除
if (curNode == NULL)
{
printf("未找到指定位置无法删除!\n");
}
else
{
//尾节点的数据等于指定数据
if (list->tailNode == curNode)
{
//尾删法
pop_back(list);
}
else
{
//释放之前做连接
preNode->tail = curNode->tail;
//判断curNode->tail是不是不空 当删除表尾的时候,curNode->tail等于空
curNode->tail->front = preNode;
free(curNode);
curNode = NULL;
list->curSize--;
}
}
}
//测试代码
pop_appoin(list, 1314);
printByTail(list);
printByFront(list);