目录
一、双向链表
1、链表的分类
链表的结构非常多样,以下情况组合起来就有8种(2 x 2 x 2)链表结构:
链表说明:
1.单向或者双向
2.带头或者不带头
注意:
博主在单链表中(不带头)表述的“头结点”只是为了表示这是链表的第一个结点,并不是哨兵位结点。这种表述是不规范的,只是为了让大家好理解而已。
3.循环或者不循环
重点:循环类型的尾结点的next指针不为空(NULL)
虽然有这么多的链表的结构,但是我们实际中最常⽤还是两种结构:
第一,单链表:不带头单向不循环链表
结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。
第二,双向链表:带头双向循环链表
2、概念与结构
3、实现双向链表
1.申请新的结点空间
//申请新的空间结点
LTNode* LTBuyNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
newnode->data = x;
//prev next
newnode->next = newnode->prev = newnode;
return newnode;
}
思路分析:
通过malloc函数申请一块大小为LTNode的结构体大小的结点空间给指针变量newnode,再判断newnode是否为空(NULL),若为空则申请失败和退出程序,若不为空则将数据存在结点的数据位置,再让newnode的next和newnode的prev都分别指向它自己本身,形成循环。
2.初始化
//test.c
LTNode* plist = NULL;
LTInit(&plist);
//List.h
//初始化
void LTInit(LTNode** pphead)
{
//创建一个头结点(哨兵位)
*pphead = LTBuyNode(-1);
}
思路分析:
首先将plist指针变量初始化置为空(NULL),再使用通过LTBuynode函数申请新的结点空间作为头结点,即哨兵位,它不存储数据,所以传实参-1来表示无数据。
调试监视窗口显示初始化如下:
扩展:只传一级指针初始化的测试运行代码:
//只传一级指针的初始化
void LTInit(LTNode* phead)
{
LTNode* phead = LTBuyNode(-1);
return phead;
}
3.尾插(哨兵位之前或最后一个有效结点之后)
//尾插(哨兵位之前或最后一个有效结点之后)
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
//phead phead->prev newnode
newnode->next = phead;
newnode->prev = phead->prev;
phead->prev->next = newnode;
phead->prev = newnode;
}
思路分析:
(1)首先通过assert断言判断phead头结点(哨兵位)的地址是否为空;
(2)再通过LTBuyNode函数来申请新的空间结点赋值给newnode指针变量;
(3)再改变指针的指向,即将newnode的next指针指向phead,newnode的prev指针指向phead的prev指针(即旧的尾结点);
(4)再让旧的尾结点的next指针指向新结点,最后让phead的prev指向新结点newnode。
注意:
双向链表为空的情况:只有一个哨兵位。
我们需要明白两个概念:第一,第一个结点:第一个有效的结点;第二,哨兵位:头结点 。
基本的尾插的方法跟上面的差不多,流程图如下:
重点:新申请的结点插到头结点的前面相当于尾插,两者关系是等价的。
测试用例运行结果:
测试时尾插空指针时,会出现:
4.头插(只能在哨兵位之后和第一个有效结点之前)
//头插(只能在哨兵位之后和第一个有效结点之前)
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
//phead newnode phead->next(d1)
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
思路分析:
(1)首先assert断言判断phead头结点的地址是否为空(NULL);
(2)再通过LTBuyNode函数来申请新的空间结点赋值给newnode指针变量;
(3)改变指针方向,即将newnode的next指针指向phead的next(第一个有效结点处);
(4)再让newnode的prev指针指向phead头结点;
(5)然后将第一个有效结点的prev指针指向newnode新结点;
(6)最后将头结点的next指针指向newnode。
测试用例运行结果:
5.bool类型判断
//bool类型判断
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
思路分析:
bool类型判断的头文件是#include<stdbool.h>,断言判断phead是否为空,然后通过返回判断phead的next指针是否等于phead的值,即判断是否为空的链表(NULL)。
6.尾删 (哨兵位之前的一个结点,即最后一个有效结点)
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
//phead prev(del->prev) del(phead->prev)
LTNode* del = phead->prev;
LTNode* prev = del->prev;
prev->next = phead;
phead->prev = prev;
free(del);
del = NULL;
}
思路分析:
(1)首先assert断言判断phead头结点地址是否为空(NULL)和!LETmpty(phead)是否为真;
(2)定义尾结点(phead的prev指针指向的结点)为del,定义del的prev指针指向的结点为prev;
(3)改变结点指针的指向,即使prev的next指针指向phead头结点;使phead的prev指针指向prev,使prev成为新的尾结点;
(4)最后再释放del结点,置为空(NULL)。
测试用例运行结果:
7.头删(只能在哨兵位之后的第一个有效结点)
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
//phead del(phead->next) del->next
LTNode* del = phead->next;
del->next->prev = phead;
phead->next = del->next;
free(del);
del = NULL;
}
思路分析:
(1)首先assert断言判断phead头结点地址是否为空(NULL)和!LETmpty(phead)是否为真;
(2)定义第一个有效数据结点(phead的next指针指向的结点)为del;
(3)改变指针的指向,使del的下一个结点(del的next指针指向的结点)的prev指针指向phead;再使phead的next指针指向del的下一个结点;最后再释放del结点并置为空(NULL)。
测试用例运行结果:
测试时头删到空指针时,会出现:
8.查找数据
//查找数据
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;
}
思路分析:
(1)首先assert断言判断phead结点地址是否为空(NULL);
(2)定义第一个有效数据的结点为pcur,进入while循环判断条件,判断pcur是否不等于phead,若不等于phead,则进入循环判断pcur的data是否为所想查找的数据,若是则返回pcur,若不是则让pcur往后移一个结点;
(3)最后若不等于phead,则跳出循环返回NULL,即查找不到所想查找的数据。
测试代码运行结果:
9.在pos位置之后插入结点
//在pos位置之后插⼊结点
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
//pos newnode pos->next
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
思路分析:
(1) 首先assert断言报错指定位置pos结点的地址是否为空(NULL);
(2)通过LTBuyNode函数申请新的空间结点给定义的结构体指针变量newnode;
(3)改变指针的指向,即将newnode结点的next指针指向pos的下一个位置(即pos的next指针指向结点位置);将newnode的prev指针指向pos结点;
(4)最后将pos的下一个结点(即pos的next指针指向结点位置)的prev指针指向newnode结点处;将pos的next指针指向newndoe。
测试代码运行结果:
10.删除指定位置结点
//删除指定位置结点
void LTErase(LTNode* pos)
{
assert(pos);
//pos->prev pos pos->next
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
思路分析:
(1) 首先assert断言判断pos结点处的地址是否为空;
(2)改变指针的指向,使pos的前驱结点(pos的prev指针指向的位置)的next指针指向pos的下一个结点位置处(pos的next指针指向的结点位置);
(3)使pos的下一个结点位置(pos的next指针指向的结点位置)的prev指针指向pos的前一个结点(pos的prev指针指向的位置);
(4) 最后再释放pos结点,并且置为空(NULL)。
测试代码运行结果:
11.销毁链表
//销毁
void LTDesTroy(LTNode** pphead)
{
assert(pphead && *pphead);
LTNode* pcur = (*pphead)->next;
while (pcur != *pphead)
{
LTNode* Next = pcur->next;
free(pcur);
pcur = Next;
}
//销毁哨兵位结点
free(*pphead);
*pphead = NULL;
pcur = NULL;
}
思路分析:
(1) 首先assert断言判断pphead(头结点地址的地址)和*pphead(头结点的地址)是否为空(NULL);
(2)使头结点的下一个结点(即(*pphead)->next)定义为pcur结构体指针变量;
(3)进入循环判断条件判断pcur是否不等于*pphead(头结点的地址),若是不等于*pphead则进入循环,将pcur的next指针指向的结点定义为Next结构体指针变量,再释放pcur,让pcur指针变量往后走一个结点位置;
(4)若条件不符合,则跳出循环,直接销毁哨兵位结点,释放头结点并置为空(NULL),并也将pcur指针变量置为空(NULL)。
测试代码运行结果:
扩展:只传一级指针的销毁测试运行代码:
//只传一级指针的销毁
//传一级指针,需要手动将plist置为NULL
void LTDesTroy2(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* Next = pcur->next;
free(pcur);
pcur = Next;
}
//销毁哨兵位结点
free(phead);
phead = pcur = NULL;
}
测试代码运行结果如下,可以成功销毁链表,但是需要手动将plist置为NULL:
12.打印函数
//打印函数
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
思路分析:
(1) 定义pcur结构体指针变量为头结点的下一个结点(即phead的next指针指向的结点位置);
(2)然后进入循环判断条件判断pcur是否不为头结点,若是则进入循环直接打印pcur的data数据,再将pcur往后走一个结点单位;
(3)直到pcur为头结点,跳出循环。
二、完整实现双链表的三个文件
List.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//定义双向链表结点的结构
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* next; //指针保存下⼀个结点的地址
struct ListNode* prev; //指针保存前⼀个结点的地址
}LTNode;
//为了保持接口的一致性,可以把接口优化都为一级指针,如下:
//初始化
void LTInit(LTNode** pphead);//二级
//只传一级指针的初始化
/*void LTInit(LTNode* phead);*///一级
//销毁
void LTDesTroy(LTNode** pphead);//二级
//只传一级指针的销毁
/*void LTDesTroy2(LTNode* phead);*///一级
//传一级指针,需要手动将plist置为NULL
//打印函数
void LTPrint(LTNode* phead);
//插入:
//第一个参传一级还是二级,要看pphead指向的结点会不会发生改变
//如果发生改变,那么pphead的改变要影响实参,传二级
//如果不发生改变,pphead不会影响实参,传一级
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//删除:
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);
//bool类型判断
bool LTEmpty(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 1
#include<List.h>
//申请新的空间结点
LTNode* LTBuyNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
newnode->data = x;
//prev next
newnode->next = newnode->prev = newnode;
return newnode;
}
//初始化
void LTInit(LTNode** pphead)
{
//创建一个头结点(哨兵位)
*pphead = LTBuyNode(-1);
}
//只传一级指针的初始化
//void LTInit(LTNode* phead)
//{
// LTNode* phead = LTBuyNode(-1);
// return phead;
//}
//销毁
void LTDesTroy(LTNode** pphead)
{
assert(pphead && *pphead);
LTNode* pcur = (*pphead)->next;
while (pcur != *pphead)
{
LTNode* Next = pcur->next;
free(pcur);
pcur = Next;
}
//销毁哨兵位结点
free(*pphead);
*pphead = NULL;
pcur = NULL;
}
//只传一级指针的销毁
//传一级指针,需要手动将plist置为NULL
//void LTDesTroy2(LTNode* phead)
//{
// assert(phead);
// LTNode* pcur = phead->next;
// while (pcur != phead)
// {
// LTNode* Next = pcur->next;
// free(pcur);
// pcur = Next;
// }
// //销毁哨兵位结点
// free(phead);
// phead = pcur = NULL;
//}
//打印函数
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
//bool类型判断
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
//尾插(哨兵位之前或最后一个有效结点之后)
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
//phead phead->prev newnode
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);
//phead newnode phead->next(d1)
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
//phead prev(del->prev) del(phead->prev)
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(!LTEmpty(phead));
//phead del(phead->next) del->next
LTNode* del = phead->next;
del->next->prev = phead;
phead->next = del->next;
free(del);
del = 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;
}
//在pos位置之后插入结点
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
//pos newnode pos->next
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
//删除指定位置结点
void LTErase(LTNode* pos)
{
assert(pos);
//pos->prev pos pos->next
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include<List.h>
void ListTest01()
{
//创建双向链表变量
LTNode* plist = NULL;
LTInit(&plist);
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
/*LTPushBack(NULL,1);*/
/*LTPrint(plist);*/
/*LTPushFront(plist, 1);
LTPrint(plist);
LTPushFront(plist, 2);
LTPrint(plist);
LTPushFront(plist, 3);
LTPrint(plist);
LTPushFront(plist, 4);
LTPrint(plist);*/
尾删
//LTPopBack(plist);
//LTPrint(plist);
头删
//LTPopFront(plist);
//LTPrint(plist);
//LTPopFront(plist);
//LTPrint(plist);
//LTPopFront(plist);
//LTPrint(plist);
//LTPopFront(plist);
//LTPrint(plist);
//LTPopFront(plist);
//LTPrint(plist);
//LTNode* pos = LTFind(plist, 1);
//if (pos == NULL)
//{
// printf("没有找到!\n");
//}
//else
//{
// printf("找到了!\n");
//}
/*LTInsert(pos, 11);*/
//LTErase(pos);
//LTPrint(plist);
LTDesTroy(&plist);
}
int main()
{
ListTest01();
return 0;
}
三、顺序表与链表的分析
不同点
|
顺序表
|
链表(单链表)
|
存储空间上
|
物理上⼀定连续
|
逻辑上连续,但物理上不⼀定连续
|
随机访问
|
支持:O(1)
|
不⽀持:O(N)
|
任意位置插入或者删除元素
|
可能需要搬移元素,效率低O(N)
|
只需修改指针指向O(1)
|
插入
|
动态顺序表,空间不够时需要扩容和空间浪费
|
没有容量的概念,按需申请释放,不存在空间浪费
|
应用场景
|
元素高效存储+频繁访问
|
任意位置高效插入和删除
|
总的来说,顺序表和链表没有哪一个绝对的更好,存在即合理。