目录
链表的分类
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
前面实现了单向链表 -- 但是在使用的过程中会发现,在尾部的操作在时间复杂度是O(N)级别,而且在扩容方面存在着较多的缺陷,导致了效率还是较低,为此我们引入了双向链表,这时才可以完美解决顺序表的所以缺陷
1. 无头单向非循环链表: 结构简单 ,一般不会单独用来存数据。实际中更多是作为 其他数据结 构的子结构 ,如哈希桶、图的邻接表等等。另外这种结构在 笔试面试 中出现很多。2. 带头双向循环链表: 结构最复杂 ,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面代码实现了就知道了。
带头双向循环链表
List.h
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int LTDataType; //重定义 方便改变数据类型
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prve;
LTDataType data;
}LTNode;
//初始话可以使用二级指针,也可以利用返回值来解决
//1.二级指针初始化
//void ListInit(LTNode** pphead);
//2.返回值初始化
LTNode* ListInit();
//销毁链表 -- 遍历节点一个一个释放
void ListDestory(LTNode* phead);
//打印
void ListPrint(LTNode* phead);
//尾插
void ListPushBack(LTNode* phead, LTDataType x);
//头插
void ListPushFront(LTNode* phead, LTDataType x);
//判断空链表(不包括哨兵位的头节点)
bool ListEmpty(LTNode* phead);
//尾删
void ListPopBack(LTNode* phead);
//头删
void ListPopFront(LTNode* phead);
//显示节点数量 -- 一般是单独把数量放在一个接口里面,尽量不要放在结构体里面
size_t ListSize(LTNode* phead);
//查找
LTNode* ListFind(LTNode* phead, LTDataType x);
//在pos之前插入
void ListInsert(LTNode* pos, LTDataType x);
//删除pos节点
void ListErase(LTNode* pos);
List.c
#include "List.h"
//初始化
LTNode* ListInit()
{
LTNode* guard = (LTNode*)malloc(sizeof(LTNode));
if (guard == NULL)
{
perror("malloc fail");
exit(-1);
}
guard->next = guard;
guard->prve = guard;
return guard;
}
//创立一个新的节点
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
exit(-1);
}
node->next = NULL; //新的节点置空
node->prve = NULL;
node->data = x;
return node;
}
//打印
void ListPrint(LTNode* phead)
{
assert(phead); //一定不会为空因为有哨兵位的头节点
printf("phead<=>");
LTNode* cur = phead->next; //当cur为哨兵位的头节点时就停下来结束打印
while (cur != phead)
{
printf("%d<=>", cur->data);
cur = cur->next;
}
printf("\n");
}
//尾插
void ListPushBack(LTNode* phead, LTDataType x)
{
assert(phead); //因为引入了哨兵位的头所以一定不会为空
//LTNode* newnode = BuyListNode(x);
//利用头直接找到尾
//LTNode* tail = phead->prve;
//tail->next = newnode;
//newnode->prve = tail;
//newnode->next = phead;
//phead->prve = newnode;
//当写好任意位置的插入时,就可以直接调用,不需要再写上面的了 -- 创建链表效率高
ListInsert(phead, x); //在phead的前面插入一个就相当于尾插了
}
//头插
void ListPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
//1.这样子写需要注意先后顺序 -- 防止覆盖找不到下一个节点
//LTNode* newnode = BuyListNode(x);
//phead->next->prve = newnode;
//newnode->next = phead->next;
//phead->next = newnode;
//newnode->prve = phead;
//2.也可以创立一个新的指针来保存下一个节点,这样就可以随便改了
//LTNode* newnode = BuyListNode(x);
//LTNode* first = phead->next;
//phead->next = newnode;
//newnode->prve = phead;
//newnode->next = first;
//first->prve = newnode;
//同尾插一样 -- 当写好任意位置的插入时,就可以直接调用,不需要再写上面的了
ListInsert(phead->next, x);
}
//判断空链表(不包括哨兵位的头节点)
bool ListEmpty(LTNode* phead)
{
assert(phead);
//1.可以这样写 -- 较易理解
//if (phead->next == NULL)
// return true;
//else
// return false;
//2.也可以直接返回
return phead->next == phead; //相等就是真(为空)否则就是假(不为空)
}
//尾删
void ListPopBack(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead)); //是空返回真,!致反就为假,就报错
//LTNode* tail = phead->prve;
//LTNode* prve = tail->prve;
//phead->prve = prve;
//prve->next = phead;
//free(tail);
//可以置空 -- 习惯,实际不起作用
//tail = NULL;
//一样一样
ListErase(phead->prve);
}
//头删
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
//LTNode* frist = phead->next;
//LTNode* second = frist->next;
//phead->next = second;
//second->prve = phead;
//free(frist);
//frist = NULL;
//一样一样
ListErase(phead->next);
}
//显示节点数量
size_t ListSize(LTNode* phead)
{
assert(phead);
size_t n = 0;
LTNode* cur = phead->next;
while (cur != phead)
{
++n;
cur = cur->next;
}
return n;
}
//查找
LTNode* ListFind(LTNode* phead,LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
//在pos之前插入
void ListInsert(LTNode* pos, LTDataType x)
{
assert(pos); //因为这里不需要传原链表,所以没有判断链表是否为空
LTNode* prve = pos->prve;
LTNode* newnode = BuyListNode(x);
// prve newnode pos 三个指针的链接
prve->next = newnode;
newnode->prve = prve;
newnode->next = pos;
pos->prve = newnode;
}
//删除pos节点
void ListErase(LTNode* pos) //也没有传哨兵位的头节点 -- 所以不检查了
{
assert(pos);
LTNode* prve = pos->prve;
LTNode* next = pos->next;
prve->next = next;
next->prve = prve;
free(pos);
//pos->next == NULL; //这里是习惯 -- 因为不是二级指针 这里改是没用的
}
//销毁链表 -- 遍历节点一个一个释放
//可以传二级指针,这样就内部置空 -- 但是也可以不置空,让调用的人自己置空(保持接口的一致性,都使用一级指针)
void ListDestory(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
//phead = NULL; -- 这里置空没有用
}
Test.c
#include "List.h"
//测试
void TestList1()
{
//二级指针初始化
//LTNode* plist = NULL;
//ListInit(&plist);
//返回值初始化
LTNode* plist = ListInit();
//尾插
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
//打印
ListPrint(plist);
//头插
ListPushFront(plist, 10);
ListPushFront(plist, 20);
ListPushFront(plist, 30);
ListPushFront(plist, 40);
//打印
ListPrint(plist);
//尾删
ListPopBack(plist);
ListPopBack(plist);
ListPopBack(plist);
ListPopBack(plist);
//打印
ListPrint(plist);
//头删
ListPopFront(plist);
//打印
ListPrint(plist);
//显示节点数 -- %zu专门打印size_t类型的打印格式
printf("%zu\n", ListSize(plist));
//查找并在其前面插入一个节点
ListInsert(ListFind(plist,10),200);
//打印
ListPrint(plist);
//查找并在其删除这个节点
ListErase(ListFind(plist, 200));
//打印
ListPrint(plist);
ListPopFront(plist);
//打印
ListPrint(plist);
//销毁链表 -- 因为内部不置空,所以外部要置空
ListDestory(plist);
plist = NULL;
}
int main()
{
TestList1();
return 0;
}
到了这里关于链表的问题也差不多入门了,这也是在实际中运用广泛的一种链表了,可以很好的解决单链表的缺点,它完美的解决了增删查改的单链表的各个方面的不便。但是关于链表与顺序表之间它们又有很多方面的不同,在实际的运用也要加以区分。
顺序表和链表的区别
不同点 顺序表 链表存储空间上 物理上一定连续 逻辑上连续,但物理上不一定连续随机访问 支持O(1) 不支持:O(N)任意位置插入或者删除元素 可能需要搬移元素,效率低O(N) 只需修改指针指向插入 动态顺序表,空间不够时需要扩容 没有容量的概念应用场景 元素高效存储+ 频繁访问 任意位置插入和删除频繁缓存利用率 高 低
备注:缓存利用率参考存储体系结构 以及 局部原理性
顺序表的优缺点
优点
1、尾插尾删效率很高
2、随机访问(用下标访问)
3、相比链表结构:cpu高速缓存命中率更高
缺点
1、头部和中部插入删除效率低 -- O(N)
2、扩容 -- 性能消耗 + 空间浪费
链表的优缺点
优点
1、任意位置插入删除效率很高 -- O(1)
2、按需申请释放
缺点
1、不支持随机访问
关于cpu告诉缓存命中率
在电脑的任务管理器中->性能->cpu中右下角可以看到
cpu执行指令,不会直接访问内存
1、先看数据在不在三级缓存,在(命中)。直接访问
2、不在(不命中),先加载到缓存,再访问
加载的时候,由于顺序表开辟的空间是连续的空间,找到一个就相当于找到了后面的数据,而链表开辟的空间并不是连续的,所以访问到的命中率的概率就会较低
与程序员相关的CPU缓存知识 | 酷 壳 - CoolShell
结束语
上穷碧落下黄泉,两处茫茫皆不见。
唐·白居易 《长恨歌》