一、双向链表的结构
注意:双向链表又称带头双向循环链表这⾥的“带头”跟前⾯我们说的“头节点”是两个概念,实际前⾯的在单链表阶段称呼不严谨,但是为了同学们更好的理解就直接称为单链表的头节点。带头链表⾥的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这⾥“放哨 的”“哨兵位”存在的意义: 遍历循环链表避免死循环。双向链表每个节点储存一个有效数据+前驱指针+后继指针
二、. 双向链表的实现
2.1 创建&初始化
2.2.1 List.h
#pragma once
typedef struct ListNode
{
int val;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
//初始化
LTNode* LTInit();
2.2.2 List.c
#define _CRT_SECURE_NO_WARNINGS
#include"List.h"
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
LTNode* LTInit()//哨兵位初始化
{
LTNode* head = (LTNode*)malloc(sizeof(LTNode));
head->val = -1;
head->next = head->prev =head;
return head;
}
2.2.3 text.c
#define _CRT_SECURE_NO_WARNINGS
#include"List.h"
#include<stdio.h>
int main()
{
LTNode* head;
head=LTInit();
return 0;
}
代码运行测试:
2.2尾插&头插
分析:
尾插
1.往d3节点的后面插入数据叫做尾插
2.往哨兵位head之前插入数据也叫尾插
头插
在哨兵位和头节点之间插入
2.2.1 List.h
//尾插
//1.往d3节点的后面插入数据叫做尾插 2.往哨兵位head之前插入数据也叫尾插
void LTPushBack(LTNode* head, int x);
//打印
void LTPrint(LTNode* head);
//头插
void LTPushFront(LTNode* head, int x);
2.2.2 List.c
//创建新节点
LTNode* Listbuynode(int x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
node->val = x;
node->next = node->prev = NULL;
return node;
}
void LTPushBack(LTNode* head, int x)
{
LTNode* node = Listbuynode(x);
//对新节点进行操作
node->next = head;
node->prev = head->prev;
//对原来的尾节点和哨兵位进行操作
head->prev->next = node;
head->prev = node;
}
void LTPrint(LTNode* head)
{
assert(head);
LTNode* pcur = head->next;
while (pcur != head)
{
printf("%d->", pcur->val);
pcur = pcur->next;
}
printf("\n");
}
void LTPushFront(LTNode* head, int x)
{
LTNode* node = Listbuynode(x);
//对新节点进行操作
node->next = head->next;
node->prev = head;
//对哨兵位和头节点进行操作
head->next->prev = node;
head->next = node;
}
2.2.3 text.c
#define _CRT_SECURE_NO_WARNINGS
#include"List.h"
#include<stdio.h>
int main()
{
LTNode* head;
head=LTInit();
LTPushBack(head, 1);
LTPushBack(head, 2);
LTPushBack(head, 3);
LTPushFront(head,4);//4->1->2->3
LTPrint(head);
return 0;
}
2.3 头删&尾删
2.3.1 List.h
//尾删
void LTPopBack(LTNode* head);
//头删
void LTPopFront(LTNode* head);
2.3.2 List.c
void LTPopBack(LTNode* head)
{
//链表为空不能删除
assert(head);
assert(head->next != head);
//将尾节点进行保存
LTNode* del = head->prev;
//连接次尾节点和哨兵位
del->prev->next = head;
head->prev = del->prev;
free(del);
del = NULL;
}
void LTPopFront(LTNode* head)
{
//链表为空不能删除
assert(head);
assert(head->next != head);
//将头节点进行保存
LTNode* del = head->next;
//连接哨兵位和次头节点
head->next = del->next;
del->next->prev = head;
free(del);
del = NULL;
}
2.3.3 text.c
#define _CRT_SECURE_NO_WARNINGS
#include"List.h"
#include<stdio.h>
int main()
{
LTNode* head;
head=LTInit();
LTPushBack(head, 1);
LTPushBack(head, 2);
LTPushBack(head, 3);
LTPushFront(head, 4);
LTPrint(head);//4->1->2->3
LTPopFront(head);
LTPrint(head);//1->2->3
LTPopBack(head);
LTPrint(head);1->2
return 0;
}
2.4 查找数据&在pos节点后插入数据&删除pos节点
2.4.1 List.h
//在pos位置之后插入数据
void LTInsert(LTNode* pos, int x);
//删除pos节点
void LTErase(LTNode* pos);
//查找数据
LTNode* LTFind(LTNode* head, int x);
2.4.2 List.c
void LTInsert(LTNode* pos, int x)
{
assert(pos);
LTNode* node = Listbuynode(x);
//先处理新节点
node->prev = pos;
node->next = pos->next;
//在处理前后节点
pos->next = node;
node->next->prev = node;
}
LTNode* LTFind(LTNode* head, int x)
{
assert(head);
assert(head->next!=head);
LTNode* pcur = head->next;
while (pcur != head)
{
if (pcur->val == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
void LTErase(LTNode* pos)
{
assert(pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
2.4.3 text.c
#define _CRT_SECURE_NO_WARNINGS
#include"List.h"
#include<stdio.h>
int main()
{
LTNode* head;
head=LTInit();
LTPushBack(head, 1);
LTPushBack(head, 2);
LTPushBack(head, 3);
LTPushBack(head, 4);
//LTPopBack(head);
//LTPrint(head);
//LTPopBack(head);
LTNode* find = LTFind(head, 4);
LTInsert(find, 11);
LTPrint(head);//1->2->3->4->11
LTErase(find);//1->2->3->11
LTPrint(head);
return 0;
}
2.5 销毁
若销毁接口用二级指针接受,传哨兵位指针的地址,那么可以改变哨兵位(指针指向),使哨兵位指向NULL;
若销毁接口用一级指针接受,传一级指针(哨兵位指针),传过去的形参(是指针存储的地址),不能够改变指针的指向,在对形参操作,可以释放哨兵位指向的地址空间(形参的值为空间地址),但是不能改变实参指针的指向(实参依然指向原来被释放的地址空间),需要手动将实参置为NULL.
简而言之,若需要改变一级指针指向,需要传二级指针。
前面都是用一级指针传参,为了保持接口的一致性,我们用一级指针传参
2.5.1 List.h
//销毁
void LTDestroy(LTNode* phead);
2.5.2 List.c
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
LTNode* next = pcur->next;
while (pcur != phead)
{
free(pcur);
pcur = next;
next = next->next;
}
free(phead);
phead = NULL;
}
2.5.3 text.c
#define _CRT_SECURE_NO_WARNINGS
#include"List.h"
#include<stdio.h>
int main()
{
LTNode* head;
head=LTInit();
LTPushBack(head, 1);
LTPushBack(head, 2);
LTPushBack(head, 3);
LTPushFront(head, 4);
/*LTPrint(head);
LTPopFront(head);*/
LTPrint(head);
//LTPopBack(head);
//LTPrint(head);
//LTPopBack(head);
LTNode* find = LTFind(head, 4);
/*LTInsert(find, 11);*/
LTErase(find);
LTPrint(head);
LTDestroy(head);
head = NULL;
return 0;
}
2.6 完整代码
2.6.1 List.h
#pragma once
typedef struct ListNode
{
int val;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
//初始化
LTNode* LTInit();
//销毁
void LTDestroy(LTNode* phead);
//尾插
//1.往d3节点的后面插入数据叫做尾插 2.往哨兵位head之前插入数据也叫尾插
void LTPushBack(LTNode* head, int x);
//打印
void LTPrint(LTNode* head);
//头插
void LTPushFront(LTNode* head, int x);
//尾删
void LTPopBack(LTNode* head);
//头删
void LTPopFront(LTNode* head);
//在pos位置之后插入数据
void LTInsert(LTNode* pos, int x);
//删除pos节点
void LTErase(LTNode* pos);
//查找数据
LTNode* LTFind(LTNode* head, int x);
2.6.2 List.c
#define _CRT_SECURE_NO_WARNINGS
#include"List.h"
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
LTNode* LTInit()//哨兵位初始化
{
LTNode* head = (LTNode*)malloc(sizeof(LTNode));
head->val = -1;
head->next = head->prev =head;
return head;
}
//创建新节点
LTNode* Listbuynode(int x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
node->val = x;
node->next = node->prev = NULL;
return node;
}
void LTPushBack(LTNode* head, int x)
{
LTNode* node = Listbuynode(x);
//对新节点进行操作
node->next = head;
node->prev = head->prev;
//对原来的尾节点和哨兵位进行操作
head->prev->next = node;
head->prev = node;
}
void LTPrint(LTNode* head)
{
assert(head);
LTNode* pcur = head->next;
while (pcur != head)
{
printf("%d->", pcur->val);
pcur = pcur->next;
}
printf("\n");
}
void LTPushFront(LTNode* head, int x)
{
LTNode* node = Listbuynode(x);
//对新节点进行操作
node->next = head->next;
node->prev = head;
//对哨兵位和头节点进行操作
head->next->prev = node;
head->next = node;
}
void LTPopBack(LTNode* head)
{
//链表为空不能删除
assert(head);
assert(head->next != head);
//将尾节点进行保存
LTNode* del = head->prev;
//连接次尾节点和哨兵位
del->prev->next = head;
head->prev = del->prev;
free(del);
del = NULL;
}
void LTPopFront(LTNode* head)
{
//链表为空不能删除
assert(head);
assert(head->next != head);
//将头节点进行保存
LTNode* del = head->next;
//连接哨兵位和次头节点
head->next = del->next;
del->next->prev = head;
free(del);
del = NULL;
}
void LTInsert(LTNode* pos, int x)
{
assert(pos);
LTNode* node = Listbuynode(x);
//先处理新节点
node->prev = pos;
node->next = pos->next;
//在处理前后节点
pos->next = node;
node->next->prev = node;
}
LTNode* LTFind(LTNode* head, int x)
{
assert(head);
assert(head->next!=head);
LTNode* pcur = head->next;
while (pcur != head)
{
if (pcur->val == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
void LTErase(LTNode* pos)
{
assert(pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
LTNode* next = pcur->next;
while (pcur != phead)
{
free(pcur);
pcur = next;
next = next->next;
}
free(phead);
phead = NULL;
}
2.6.3 text.c
#define _CRT_SECURE_NO_WARNINGS
#include"List.h"
#include<stdio.h>
int main()
{
LTNode* head;
head=LTInit();
LTPushBack(head, 1);
LTPushBack(head, 2);
LTPushBack(head, 3);
LTPushFront(head, 4);
/*LTPrint(head);
LTPopFront(head);*/
LTPrint(head);
//LTPopBack(head);
//LTPrint(head);
//LTPopBack(head);
LTNode* find = LTFind(head, 4);
/*LTInsert(find, 11);*/
LTErase(find);
LTPrint(head);
LTDestroy(head);
head = NULL;
return 0;
}
三、顺序表和双向链表的优缺点分析
不同点
| 顺序表 |
链表(单链表)
|
存储空间上
| 物理上一定连续 |
逻辑上连续,但物理上不⼀定连续
|
随机访问
|
⽀持O(1)
| 不支持,O(n) |
任意位置插⼊或者删除元素
|
可能需要搬移元素,效率低O(N)
|
只需修改指针指向
|
插入 |
动态顺序表,空间不够时需要扩容
|
没有容量的概念
|
应⽤场景
|
元素⾼效存储+频繁访问
|
任意位置插⼊和删除频繁
|