数组和链表
一、数组
1.1 什么是数组?
数组是一种线性表数据结构,它用一组连续的内存空间来存储一组具有相同类型的数据。
线性表(Linear List) 顾名思义就是数据排列像一条线一样的结构,每个线性表上的数据最多只有前和后两个方向。其实除了数组,还有队列、链表、栈等也是线性表。
与线性表对立的是 非线性表,比如二叉树、堆、图等。之所以叫非线性,是因为在非线性表中数据的关系并不是简单的前后关系。
1.2 数组的特性
连续的内存空间 和 相同类型的数据。正是这两个限制,才有了数组的一大特性—随机访问(即可以根据数据下标直接访问任意元素) 。但有利也有弊,因为要保证数组的连续性,这两个限制让数组的插入、删除工作变得更加复杂。注意:数组的查找复杂度并不是 O(1),只有使用下标直接访问的复杂度才是O(1)。
为什么可以实现随机访问呢? 拿一个长度为 10 的 int 类型的数组 int [] a = new int [10] 来举例。计算机给数组 a[10] 声明了一块连续的内存空间,因为数组的每个元素的类型都是相同的,所以每个元素内存大小都一样,所以我们就可以拿 数组在内存块的首地址base_address + 要访问的下标*单个类型的size
如 : a[i]_address = base_address + i * data_type_size
1.3 数组下标为什么是从 0 开始的?
为什么大多数编程语言中,数组都要从 0 开始编号,而不是从1开始?
- 从数组存储模型来看,“下标” 最确切的定义 应该是 “偏移(offset)”
- 最开始 C 语言采用了 下标从 0 开始,后来的 Java 和 JavaScript 都抄袭了它。
二、链表
2.1 什么是链表
和数组一样,链表也是一种 线性表,不同的是从内存上来看,数组是一串连续的内存,而链表是把不连续的内存通过指针给串联起来。
链表的每个元素都称为 节点(Node),每个节点除了存储数据外还存放了下个节点在内存中的地址(如果是双向链表则还包含前一个节点的地址)。
常见的链表有单链表、双链表、循环链表等等
2.2 单链表实现 LRU 缓存淘汰策略
什么是缓存?
缓存是一种提高数据读取性能的技术,在硬件设计、软件开发中都有着非常广泛的应用,比如常见的 CPU 缓存、数据库缓存、浏览器缓存等。
什么是缓存淘汰策略?
缓存淘汰策略就是缓存被用满时清理数据的优先顺序。因为缓存的大小是有限的,当缓存被用满时,哪些数据需要被清理出去,哪些数据应该被保留?就需要用到缓存淘汰策略。
都有哪些缓存淘汰策略?
常见的3种包括先进先出策略FIFO(First In First Out)、最少使用策略 LFU(Least Frequently Used)、最近最少使用策略 LRU (Least Recently Used)
使用单链表怎样实现 LRU 缓存淘汰策略?
实现思路:
数据结构: 一个有序存储的单链表,链表越往后为越早使用过的数据。
缓存淘汰策略:
当处理一个新数据时,首先判断它在缓存中是否存在
-
已缓存:这时遍历链表找到之前缓存的数据并删除此结点,然后把新数据加入到头节点。
-
未缓存:
- 还有空闲缓存:把数据插入到头节点。
- 缓存空间不足:删除尾节点,并把数据插入到头节点。
2.3 用单链表存储的字符串怎么判断是不是回文字符串?
回文字符串 : 例:level 、moon 、php …
1、单链表反转然后跟原链表比较每个节点的值,完全一样则为回文字符串。
2.4 利用哨兵简化实现难度
首先我们先思考一下什么是哨兵?为什么要使用哨兵?
以单链表的插入操作为例:
往单链表 的 p 节点后插入一个节点的代码通常是这样的:
new_node.next = p.next;
p.next = new_node;
但是,如果是要向空链表中插入节点时,就需要另作处理:
if(head == null){
head = new_node;
}
与插入类似,链表的删除在当链表只有一个节点时也需要特殊处理:
if(head.next == null){
head = null;
}
从上面的分析来看,链表的插入、删除需要对插入第一个节点、和删除最后一个节点的情况做特殊处理。这样代码实现起来就会很繁琐,不简洁,而且很可能因为考虑不全而出错。想要解决这个问题,就需要哨兵出场了。需要注意的是哨兵要解决的是边界问题,不直接参与业务逻辑。
还记得如何表示一个空节点么? head = null 表示链表中没有节点了。其中 head 表示链表中的头节点,指向链表中的第一个节点。
如果我们引入哨兵节点,在任何时候,不管链表是不是为空,head 指针都会一直指向这个哨兵节点。我们也把这种有哨兵节点的链表叫带头链表。相反,没有哨兵节点的链表叫不带头链表。
观察带头链表可以发现,哨兵节点并不存储数据。而因为哨兵节点一直存在,所以插入第一个节点和删除最后一个节点都可以统一为相同的代码实现逻辑了。