目录

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

前面实现了单向链表 -- 但是在使用的过程中会发现,在尾部的操作在时间复杂度是O(N)级别,而且在扩容方面存在着较多的缺陷,导致了效率还是较低,为此我们引入了双向链表,这时才可以完美解决顺序表的所以缺陷
1. 无头单向非循环链表: 结构简单 ,一般不会单独用来存数据。实际中更多是作为 其他数据结 构的子结构 ,如哈希桶、图的邻接表等等。另外这种结构在 笔试面试 中出现很多。2. 带头双向循环链表: 结构最复杂 ,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面代码实现了就知道了。
- #pragma once
-
- #include
- #include
- #include
- #include
-
- 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);
- #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; -- 这里置空没有用
- }
- #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执行指令,不会直接访问内存
1、先看数据在不在三级缓存,在(命中)。直接访问
2、不在(不命中),先加载到缓存,再访问
加载的时候,由于顺序表开辟的空间是连续的空间,找到一个就相当于找到了后面的数据,而链表开辟的空间并不是连续的,所以访问到的命中率的概率就会较低
与程序员相关的CPU缓存知识 | 酷 壳 - CoolShell
上穷碧落下黄泉,两处茫茫皆不见。
唐·白居易 《长恨歌》