文章目录
前言
一、顺序表与链表的定义
1、顺序表
2、链表
二、区别
1、顺序表(动态顺序表):
2、链表(带头双向循环链表):
3、将上述文字用图表形式展示:
4、CPU高速缓存命中率
总结
前言
路漫漫其修远兮,吾将上下而求索;
一、顺序表与链表的定义
1、顺序表
顺序表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素;即,在顺序表中其数据必须连续存放,数据之间的空间不能有空;
2、链表
不仅需要存储数据元素的信息,还要存储它后继元素的存储地址;
存储数据元素信息的域称为数据域,把存储其直接后继位置的域称为指针域。指针域中的存储信息称为指针或链。数据域 加上 指针域便构成了结点。
二、区别
顺序表与链表各有优缺,很难说明二者谁更优;准确来说这两个结构之间是相辅相承的关系;
1、顺序表(动态顺序表):
- 逻辑结构上是线性的,物理结构上也上一定是线性结构
- 支持随机访问(利用下标)
- 但是在任意位置上插入或者删除数据需要一个一个地挪动数据以保证顺序表结构的连续性,时间复杂度为O(N),效率不高;
- 并且插入数据,会面临扩容的问题,扩容本身就存在消耗,就地扩容消耗不大(开辟空间,返回该空间的地址),但倘若是异地扩容,便会找新空间,拷贝旧空间的数据到新空间中,释放旧空间中的数据,返回新空间的地址;并且扩容的空间不一定能使用完,存在空间浪费的可能性。
- 缓存利用率高;
- 顺序表更适合于用来存储数据并且频繁访问的地方;
2、链表(带头双向循环链表):
- 逻辑上是线性的,但是物理结构上不一定是线性的(取决于动态内存的分配策略);
- 不支持随机访问,倘若想要访问第i 个结点中的数据,只能利用循环进行访问,时间复杂度为O(N);
- 并且在任意位置插入、删除数据时不会有太大的消耗,只需改变结点之间的链接关系即可;
- 插入数据时其结点是一个一个空间开辟的,不存在空间的浪费和很大的消耗;
- 缓存利用率低;
- 链表更适合于要对数据进行频繁修改(任意位置插入、删除数据)的地方;
3、将上述文字用图表形式展示:
异 | 顺序表(动态顺序表) | 链表(带头双向循环链表) |
存储的空间 | 逻辑结构上为线性,物理结构上也为线性 | 逻辑结构上为线性,物理结构上不一定为线性(取决于动态空间的分配策略) |
随机访问 | 支持随机访问,利用下标 | 不支持随机访问,需要遍历 |
任意位置插入、删除数据 | 存在一个一个地挪动数据地可能性,效率低下,时间复杂度为O(N) | 均只需修改结点之间的链接关系即可 |
数据的插入 | 动态顺序表中,当空间不够时需要进行扩容处理(扩容具有空间、时间的消耗) | 按需申请空间 |
CPU高速缓存命中率 | 高 | 低 |
适合的应用场景 | 适用于存储数据并且频繁访问的地方 | 适用于对数据进行频繁修改(任意位置插入、删除数据)的地方 |
链表并不是优于顺序表,他们有着不同的适用场景;
- 例如,如果使用链表来对数据进行排序,其效率肯定比顺序表低;
- 排序在我们的日常生活中是非常常见的;例如在购物平台上,我们可以对商品按照销量、价格等进行排序以帮助我们进行筛选;当商品在页面进行排序的时候,其底层是应用了顺序表这样的结构进行排序(不会用链表,因为用链表来排序,效率低下);
包括在后面的文章中会讲述的TOP-K问题(从n个数中找出最大的前k个数)、堆排序问题(二叉树的排序)均会要求使用支持随机访问的数据结构,其实这两个个问题的底层用到的是数组;
针对顺序表与链表的结构特点,我们需要按照实际情况来选择;当此问题需要排序、二分查找等类似需要随机访问的算法,那么最好使用顺序表;当此问题无需排序,即无需对存储的数据进行随机访问的时候,而会对数据进行插入、删除,那么最好使用链表(注:此处仅仅只是举一个例子,还是需要根据具体问题具体分析来决定使用哪种结构);
接下来我们来了解一下,CPU高速缓存命中率;
4、CPU高速缓存命中率
无论你写什么样的代码均会交给CPU来执行,当前CPU具有多核技术;
从大的方面来说,对于单个计算机上的存储,分为带电存储和不带电存储;
带电存储
- 包括寄存器、高速缓存、主存(主存就是电脑的内存);带电存储即有电才能存储数据,其中这三个结构中,最重要的是主存;
不带电存储
- 包括本地二级存储(本地磁盘)、远程二级存储(分布式文件系统、Web服务器); 当你电脑关机的时候,你想要保存的数据存放在文件中,因为文件是存储在文件系统上即存储在磁盘或者硬盘上;
注:
1、磁盘(or 硬盘)是一种存储技术,当然还有一种技术叫做SSD(SSD 比磁盘会更快一些)
2、远程二级存储(分布式文件系统、Web服务器) 即网盘;例如,当你要将数据存入百度网盘之中,此数据便会通过网络传输以进行存储;
存储器的结构如下图所示:
注:电脑一般会有三级缓存(L1 cache、L2 cache、L3 cache)以及寄存器,三级缓存器与寄存器环绕在CPU周围;
为什么三级缓存与寄存器环绕在CPU周围?
- 因为CPU执行的速度非常快,那么数据在主存中的速度相较于CPU来说就很慢,三级缓存与寄存器环绕在CPU周围就能稍微弥补存储传输上的时间,当CPU计算内存中的数据的时候,首先会将这些数据放入寄存器或者高速缓存之中;
例如,当你想要计算a+b 的值时,变量a 与变量b 的数据是存储在主存上的;如果a 、b 所占用的内存比较小(可放入寄存器之中,寄存器一般只能存放 4byte 或者8byte 的数据)的时候,变量a、b 会被加载进寄存器之中,然后在寄存器之中完成对二者的计算,最后再将计算的结果放回内存之中;如果变量a、b 所占用的内存比较大,那么便会借助于高速缓存;
CPU是如何遍历顺序表和链表的呢?
首先我们先思考一下顺序表与链表中的数据存放在何处?显然是在堆上,因为顺序表与链表中存放数据的空间是动态开辟所得到的;
单单从物理结构上来说顺序表与链表的区别在于顺序表底层存储数据的空间是连续的,但是链表中底层存储数据的空间不一定是连续的,即多块不连续的空间利用指针链接在一起;
当你写下遍历顺序表或者遍历链表等代码的时候,程序的代码会经历编译、链接生成机器可读的二进制指令,CPU会执行这些二进制指令。当指令要遍历顺序表或者链表的时,CPU并不会直接去访问内存中的数据,CPU计算时使用的数据来自于三级缓存(L1 cache、L2 cache、L3 cache)以及寄存器;
这个过程是如何实现的?
(假设顺序表或者链表中第一个存放的数据为1,并且假设此数据很大,寄存器存放不下),若要访问存放数据1这块内存空间的位置,CPU会先看这块空间的地址是否在缓存中,如果在缓存中那么CPU便会直接访问,不在的话就CPU会先将此空间中的数据加载到缓存中,然后CPU再访问;在实际当中,当你访问了内存中的当前位置的数据马上可能会访问此位置相邻的数据("就近原则"),故而当将要访问空间中的数据加载到缓存时,会将此位置连续的一片空间加载到缓存之中(所要加载的“一片空间”的大小取决于内存);
假设顺序表和链表的数据均只有5个,并且CPU加载的一片空间的大小为假设为20 byte;
那么便意味着,当你第一次访问顺序表中存放数据1 的空间的地址时
CPU会先看这块空间的地址是否在缓存中,不在的话就会先将此空间中的数据以以及此空间开始向后连续的20byte 的空间的数据加载到缓存中;当CPU执行代码逻辑访问顺序表中下一空间的地址(存放数据2的空间),CPU发现此块空间的地址就在缓存中,在的话,即命中……故而在顺序表中的CPU高速缓存命中率高;
注:CPU缓存命中,即CPU执行指令所要访问的空间的地址在缓存或者寄存器中,那么便为命中;反之为未命中;
对于链表来说
由于其结点在空间中并不是连续存放的 (其结点的地址是否在物理结构连续取决于动态内存开辟的策略,有可能结点之间的距离隔得很近,但是大概率隔得很远(大于CPU加载地连续一片空间的大小)),即使CPU会将所要访问空间及其周围一片的空间中的数据加载到缓存中,大概率是需要一个结点一个结点地去加载,故而链表的CPU高速缓存命中率低;并且,此处的链表有5个结点,将此5个结点中的数据均加载进缓存的话,实际加载了100byte 的数据,而只会使用20byte 的数据,剩下80byte 的数据不会被CPU访问且还占用了缓存的空间,造成了缓存的污染(缓存污染简单来说就是将不会使用到的数据加载到缓存中);
注:缓存的大小是有限的;当你要将数据加载进缓存时,而缓存的空间不够用时,便会将缓存中最近未被访问的数据换出缓存;
总结
顺序表与链表各有优缺,很难说明二者谁更优;准确来说这两个结构之间是相辅相承的关系;要根据实际的使用情况来判断是使用顺序表更好,还是使用链表更好;