数组 & 链表
数组
是一种线性表数据结构,它用一组连续的内存空间,来存储一组具有相同类型的数据。
使用了连续的内存空间和相同类型的数据,使得它可以“随机访问”,但同时也让数组的删除,插入等操作变得非常低效,
为了保证连续性,就需要做大量的数据搬移工作。 数组是从0开始编号的,目的是为了减少一次减法运算。
设计思想
空间换时间 & 时间换空间
空间换时间 :当内存空间充足的时候,为了追求代码更快的执行速度,
就可以舍弃对存储空间的要求,从而追求效率。
时间换空间 :内存空间比较紧缺时,为了让程序稳定运行,
就需要舍弃时间,极大满足对存储空间的要求。
缓存
实际上 就是利用了空间换时间的设计思想。如果我们把 数据存储在硬盘上,会比较节省内存,
但每次查找数据都要询问一次硬盘,会比较慢。
但如果我们通过缓存,提前将数据加载在内存中,虽然会比较消耗内存,但是查数据的速度就提高了。
对于执行较慢的程序,可以通过消耗更多内存(空 换 时)来优化
对于内存消耗过多的程序,可以通过消耗更多的时间(时 换 空)来降低内存的消耗
二维数组内存寻址公式
对于 m * n 的数组,a[i][j] (i<m,j<n)
的地址为:
address = base_address + (i*n+j)*type_size
链表的存储结构
数组需要一块连续的内存空间来存储,需要事先申请需要申请内存空间;
而链表通过“指针”将一组零散的内存块串联起来使用,不会占用还未使用的内存空间。
单链表
链表通过指针将一组零散的内存块串联在一起,内存块称为链表的“结点”。
每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址,叫作后继指针 next。
循环链表
循环链表跟单链表的区在尾结点指针是指向链表的头结点。
双向链表
双向链表支持两个方向,每个结点同时有后继指针 next 指向后面的结点,
还有一个前驱指针 prev 指向前面的结点。
LRU-缓存
缓存是一种提高数据读取性能的技术,在硬件设计、软件开发中都有着非常广泛的应用,
比如常见的CPU缓存、数据库缓存、浏览器缓存。
常见的缓存淘汰策略
先进先出策略 FIFO
最少使用策略 LFU
最近最少使用策略 LRU
思路:
维护一个有序单列表,越靠近链表尾部的节点越早之前访问的。
当有一个新的数据被访问时,从链表头开始顺序遍历链表:
1 如果此数据之前已经被缓存进链表中了,遍历得到这个数据对应的节点,
并将其从原来的位置删除,然后再插入到链表的头部
2 如果此数据没有在缓存链表中,则将此节点插入到链表的头部:
如果此时缓存超过容量,则链表尾节点删除。
回文字符串: 是一个正读和反读都一样的字符串
判断思路 :
1使用快慢两个指针找到链表中点,慢指针每次前进一步,快指针每次前进2步,这样当快指针指向末尾时,
慢指针指向了中点。
2 在慢指针前进的过程中,同时修改其next指针指向上一个元素prev,使得链表前半部分反序
3 最后比较重点两侧的链表是否相等。
链表 VS 数组 (对比)
数组简单易用,在是线上使用的是连续的内存空间,可以借助CPU的缓存机制,
预都数组中的数据,所以访问效率更高
链表在内存中并不是连续存储,所以对CPU缓存不友好,没办法有效预读
缺点
数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,
系统可能没有足够的连续内存空间分配给它,导致“内存不足”。
如果声明的数组过小,则可能出现不够用的情况。
这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。
链表本身没有大小的限制,天然地支持动态扩容
如果 对 内存的使用非常苛刻数组就更合适,因为链表中的每个结点都需要消耗额外的存储空间去存储一份指向下一个节点的指针
所以内存消耗会翻倍
对链表进行频繁的插入、删除操作,会导致频繁的内存申请和释放,容易造成内存碎片,对于Python编程语言,
就有可能频繁的GC(Garbage Collection,垃圾回收)。