序: 马上要秋招了,作为一个数学系的学生想找一份研发类的工作确实需要做一些准备,现在开始重新梳理一下CS的知识。首先,从高中学NOIP常用的数据结构开始吧,从易到难慢慢来~
- 存储结构
- 数组
- 链表
- 常用方法
- 线性数据结构
- 栈
- 栈的概念
- 栈的应用
- 栈的扩容
- Deque实现栈的常用操作
- 队列
- 概念
- 单队列
- 循环队列
- 双端队列
- Stack 类的问题
- Deque的优势
- 优先队列(并非队列)
- 栈
- 非线性数据结构
- 二叉树(Binary tree)
- 二叉树的存储(具体实现)
- 完全二叉树 (基于节点结构要求命名)
- 满二叉树 (节点结构要求)
- 二叉搜索树 简称BST (节点数值要求)
- 平衡二叉树 也叫平衡二叉搜索树 (节点数组要求+结构要求)
- AVL树 (平衡二叉树严格实现)
- 红黑树 (平衡二叉树近似实现)
- 因此两种平衡二叉树的实现有不同的使用场景:
- 多路平衡查找树 (磁盘存储)
- 概念
- B树 数据和索引“住在一起”
- B+树 索引和数据“严格分居”
- B树和B+树对比
- 堆
- 堆的概念
- 堆的用途
- 图
- 图的遍历:深度优先 (DFS) 与广度优先 (BFS)
- 最小生成树 (Minimum Spanning Tree, MST)
- 最短路径 (Shortest Path)
- 拓扑排序 (Topological Sort)
- 哈希
- HashMap(最常用的哈希表实现)
- HashSet(用hash实现的集合类)
- 二叉树(Binary tree)
存储结构
有人将 数组 链表 队列 和 栈 都划分为线性数据结构,但我觉得将数组和链表成为存储结构更加合适,原因在于:
后面的 队列 栈 图 堆 树 这些数据结构都是为了实现某些特殊的功能(比如先进先出,维护最大值等等),但这些数据结构的底层实现还是数组和链表。
数组
数组(Array)比较简单易懂,就是由相同数据类型的的元素(element)组成,并且存储在连续的存储中的数据集合。
它的声明方式也很简单,c++中最常用的静态数组可以这样type arrayName[arraySize];
java中一般Object arrayName[arraySize] = new Object[arraySize];
来声明
特点就是由于元素类型相同且地址连续,因此可以提供随机访问 并且容量有限。
链表
链表(LinkedList)则比较灵活,它不要求内存连续,一般自己定义一个结构体(或者说类),在类中使用指针指向下一个元素。
声明方法如下:
定义方法
// 单向链表节点类
class ListNode {int val; // 节点存储的数据ListNode next; // 指向下一个节点的引用// 构造方法ListNode(int val) {this.val = val;this.next = null; // 初始化为 null,表示没有下一个节点}
}
对比
数组支持随机访问,而链表不支持。数组的大小固定,而链表则天然支持动态扩容。如果声明的数组过小,需要另外申请一个更大的内存空间存放数组元素,然后将原数组拷贝进去。
因此存储元素个数不确定的话更推荐使用链表,而非数组。
常用方法
- 添加元素:
boolean add(E e):将指定元素添加到列表的尾部。返回 true 表示添加成功
void add(int index, E element):在列表的指定位置插入指定元素。 - 获取元素:
E get(int index):返回列表中指定位置的元素。 - 删除元素:
E remove(int index):移除列表中指定位置的元素,并返回被移除的元素。
boolean remove(Object o):移除列表中首次出现的指定元素(如果存在)。 - 修改元素:
E set(int index, E element):用指定元素替换列表中指定位置的元素,并返回被替换的元素。 - 判断元素是否存在:
boolean contains(Object o):检查列表中是否包含指定元素。 - 查找元素位置:
int indexOf(Object o):返回列表中指定元素首次出现的位置,如果列表不包含该元素,则返回 -1。
int lastIndexOf(Object o):返回列表中指定元素最后出现的位置,如果列表不包含该元素,则返回 -1。 - 获取子列表:
ListsubList(int fromIndex, int toIndex):返回列表中指定的 fromIndex(包括)和 toIndex(不包括)之间的部分视图。 - 判断列表是否为空:
boolean isEmpty():检查列表是否为空。 - 获取列表大小:
int size():返回列表中的元素数量。 - 遍历循环
点击查看代码
List<String> list = new ArrayList<>();list.add("apple");list.add("banana");for (String element : list) {System.out.println(element);}
线性数据结构
接下来就是基于特定功能而设计的线性数据结构,这些数据结构既可以用数组来实现,也可以用链表来实现,这也是我将数组和链表分类为存储结构的原因。
根据元素进入和弹出的顺序,将线性数据结构分为队列(先进先出)和栈(先进后出)。
栈
栈的概念
栈 (Stack) 只允许在有序的线性数据集合的一端(称为栈顶 top)进行插入(push)和弹出(pop)数据的操作。
用数组实现的栈叫作 顺序栈 ,用链表实现的栈叫作 链式栈 。
栈的花样比较少,就讲讲应用吧。
栈的应用
- 浏览器或者DFS算法中 回溯(回到上一层)
- 算法题中 括号的匹配 ( ) {} []
- 内存中 维护函数调用:函数被调用时系统会在内存栈中为该函数创建一个栈帧Stack Frame,存储函数参数 局部变量 函数返回地址 函数调用前寄存器状态等
栈的扩容
当向栈中push压入新元素的时候,数组栈会先检查栈空间是否够用,不够则进行扩容。即使用java.util.Arrays 类的静态方法,Arrays.copyOf()扩容本质是 “创建新数组(反射获取类型) + 复制元素 + 填充默认值”,其高效性依赖于底层的 **System.arraycopy() **native 方法(C++实现)
Deque实现栈的常用操作
Deque 接口在 Java 集合框架中定义,常用的实现类有 ArrayDeque 和 LinkedList。
- addFirst(E e)
功能:将元素添加到双端队列的头部。 - addLast(E e)
功能:将元素添加到双端队列的尾部。 - offerFirst(E e)
功能:尝试将元素添加到双端队列的头部,如果队列已满返回 false,否则返回 true。 - offerLast(E e)
功能:尝试将元素添加到双端队列的尾部,如果队列已满返回 false,否则返回 true。 - removeFirst()
功能:移除并返回双端队列头部的元素,如果队列为空则抛出 NoSuchElementException。 - removeLast()
功能:移除并返回双端队列尾部的元素,如果队列为空则抛出 NoSuchElementException。 - pollFirst()
功能:移除并返回双端队列头部的元素,如果队列为空则返回 null。 - peekFirst()
功能:返回双端队列头部的元素,但不移除它,如果队列为空则返回 null。 - size()
功能:返回双端队列中元素的数量。
队列
概念
用数组实现的队列叫作 顺序队列 ,用链表实现的队列叫作 链式队列。
单队列
单队列是最简单的队列,但是它入队和出队的过程其实是一个滑动窗口指针L和R不断右移的过程,这样左边的空间就被浪费掉了。
因此数组实现的单队列(顺序队列)会出现假溢出的现象:明明数组左边还有空间,但是滑动窗口不断右移,移动到数组末端再移动就会“越界”。
那左侧的空间还空闲着,能否将左侧的空间利用起来?这就引出了循环队列的概念
循环队列
循环队列可以解决顺序队列的假溢出和越界问题:
原理很简单,将左侧空间利用起来,当rear 指针指向数组末端后再移动时移动到数组下标为0的位置。
双端队列
两端都可以进行插入和删除操作的队列。那么,如果我只在一端进行插入和删除 是不是就实现了栈的功能了?
没错,是这样的,在 Java 的官方文档中,推荐使用 Deque(双端队列)接口及其实现类(如 ArrayDeque)来替代 Stack 类。种替代的核心原因是 Stack 类存在设计缺陷,而 Deque 提供了更规范、高效的栈实现。
Stack 类的问题
- Stack 是 Java 早期(JDK 1.0)提供的栈实现,继承自 Vector 类(一个线程安全的动态数组)
- Stack 继承自 Vector,这意味着它不仅拥有栈的核心方法(push()、pop()、peek())还继承了 Vector 的所有公共方法(如 add(int index, E element)、remove(int index))。
- Stack 没有实现专门的栈接口,而是直接继承具体类 Vector,不符合面向对象设计中的 “面向接口编程” 原则,灵活性较差。
Deque的优势
Deque(java.util.Deque)是 Java 1.6 引入的双端队列接口。用它实现栈时,只需限制在一端进行操作,就能完美模拟栈的 “后进先出” 特性。
而且Deque 是接口,可根据场景选择不同实现:
- ArrayDeque:基于数组,适合大多数场景,性能最佳;
- LinkedList:基于链表,适合频繁插入 / 删除的场景(但性能略低于 ArrayDeque)。
而 Stack 是具体类,无法灵活替换实现。
优先队列(并非队列)
优先队列其实虽然名字叫队列,但其实它并不满足先进先出的要求,因此严格来讲并不算是“队列”。
它的特点是:每次从队列中取出的元素,都是当前队列中优先级最高的元素。
实现该这中特点一般用堆(Heap)实现,也有用平衡二叉搜索树实现的。
- 优先考虑堆:绝大多数情况下(如任务调度、算法实现),堆的效率和实现复杂度达到最佳平衡,是优先队列的标准实现。
- 特殊场景选平衡树:当需要频繁查找任意元素或按优先级遍历所有元素时,平衡树更合适。
非线性数据结构
存储在线性数据结构中的任意两两元素之间都是有明确的先后顺序的,但是当这种先后顺序并不明确时,就产生了非线性的数据结构。
二叉树(Binary tree)
二叉树树的结构可以认为是但单链结构的变体,每个节点中可以有两个指针指向后续节点。这里主要从大到小介绍一下树的分类和特点。
每个节点最多只有两个分支的树结构。
特点:
- 二叉树 的第 i 层至多拥有 $2^{i-1} 4个节点
- 深度为 k 的二叉树至多总共有 \(2^k-1\) 个节点(满二叉树的情况),至少有 2^(k-1) 个节点
(我这里认为只有一个根节点深度就是1)
二叉树的存储(具体实现)
- 链式存储 ,每个节点包括三个属性:
数据 data。data 不一定是单一的数据,根据不同情况,可以是多个具有不同类型的数据。
左节点指针 left
右节点指针 right
java具体实现
// 二叉树节点类
class TreeNode {int val;TreeNode left;TreeNode right;public TreeNode(int val) {this.val = val;this.left = null;this.right = null;}
}
- 顺序存储
组中的每一个位置仅存储节点的 data,不存储左右子节点的指针,子节点的索引通过数组下标完成。
完全二叉树 (基于节点结构要求命名)
除最后一层外,若其余层都是满的,并且最后一层是满的或者是在右边缺少连续若干节点
特点:
- 如果根节点编号为1,父结点(i),那么左子节点的序号就是 2i,右子节点的序号是 2i+1。
- 这样完全二叉树可以用数组储存节省存储空间
- 可以根据序号O(1)访问到某个节点,而不用从根节点慢慢找O(longn).
满二叉树 (节点结构要求)
满二叉树是指除了最后一层外,每一层的所有节点都有两个子节点,且最后一层的所有节点都是叶子节点(没有子节点) 的二叉树。
特点:
- 若满二叉树的深度k(根节点为第 1 层),则节点总数一定是$ 2^k-1$
- 所有叶子节点都集中在最后一层,且数量为 $2^ {k-1} $
二叉搜索树 简称BST (节点数值要求)
是一种满足 “左小右大” 规则的二叉树:
- 对于任意节点,其左子树中所有节点的值 < 该节点的值;
- 其右子树中所有节点的值 > 该节点的值;
- 左右子树也都是二叉搜索树。
有可能退化为链
平衡二叉树 也叫平衡二叉搜索树 (节点数组要求+结构要求)
平衡二叉树的设计初衷是解决普通二叉搜索树的 “倾斜问题”
它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树
平衡二叉树是一种概念,它的具体实现包括红黑树、AVL 树、替罪羊树、加权平衡树、伸展树 等
AVL树 (平衡二叉树严格实现)
AVL 树是一种经典的自平衡二叉搜索树(Self-Balancing Binary Search Tree),得名于其发明者 Adelson-Velsky 和 Landis。它的核心特点是通过严格控制节点的平衡状态:任意节点的左右子树高度差(平衡因子)的绝对值 ≤ 1。
维持平衡的操作:
- 右旋:适用于 “左子树过高” 的失衡
示意图
z(失衡节点) y/ \ / \y T4 ----→ x z/ \ / \ / \
x T3 T1 T2 T3 T4
/ \
T1 T2
步骤:将 y 作为新根,z 变为 y 的右子树,y 原来的右子树 T3 变为 z 的左子树。
- 左旋:与右旋相反
- 左右旋: 适用于“左子树的右子树过高”
示意图
失衡态:左子树的右子树过高Z(平衡因子=2) // 失衡节点:左子树高度 - 右子树高度 = 2/ \Y T4 // Y是Z的左子树(平衡因子=-1)/ \ // Y的右子树高度 > 左子树高度(右高左低)T1 X // X是Y的右子树/ \T2 T3首先:对失衡节点的左子树(Y)执行左旋Z(仍失衡)/ \X T4 // X成为Z的左子树/ \Y T3 // Y成为X的左子树/ \
T1 T2此时结构转化为 “左子树的左子树过高” 的标准场景按照第一个示例,只需要右旋即可:X(平衡)/ \Y Z/ \ / \
T1 T2 T3 T4
红黑树 (平衡二叉树近似实现)
上一小节我们提到了AVL树是如何保证树的自平衡的,但是在频繁地插入删除操作中AVL维持平衡的旋转操作会带来较大的开销(为了维持严格自平衡会进行多次的旋转:从插入位置到根节点都有可能进行旋转)。
红黑树对平衡的控制要宽松一些,只需要保证黑色节点的平衡即可。下面是红黑树的特点(来自Java Guide总结):
点击查看代码
- 每个节点非红即黑。黑色决定平衡,红色不决定平衡。这对应了 2-3 树中一个节点内可以存放 1~2 个节点。
- 根节点总是黑色的。
- 每个叶子节点都是黑色的空节点(NIL 节点)。这里指的是红黑树都会有一个空的叶子节点,是红黑树自己的规则。
- 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)。通常这条规则也叫不会有连续的红色节点。一个节点最多临时会有 3 个子节点,中间是黑色节点,左右是红色节点。
- 从任意节点到它的叶子节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。每一层都只是有一个节点贡献了树高决定平衡性,也就是对应红黑树中的黑色节点。
因此两种平衡二叉树的实现有不同的使用场景:
- AVL 树:适合查询密集型应用,如静态数据的索引(一旦构建后很少修改,频繁查询)。
- 红黑树:适合插入 / 删除密集型应用,是工业界更常用的自平衡树。
多路平衡查找树 (磁盘存储)
概念
多路平衡查找树是使用最多的多叉树,不约束每个节点最多两个子节点,而是允许每个节点有多个子节点。广泛应用于磁盘存储系统,如数据库索引、文件系统。
思考 :二叉树的结构不是挺好的嘛,为什么要引入多叉树呢?
为了减少I/O次数。
磁盘存储中,树的每个节点通常对应一个 “磁盘块”(如 4KB)。访问节点时,需先将磁盘块加载到内存(1 次 IO),再通过节点中的指针(存储子节点所在的磁盘块地址)访问下一层节点。因此:
树的高度 近似于 单次查询所需的 IO 次数。
下面我们用一个意图对比二叉树和 B 树在磁盘 IO 操作上的差异:
二叉搜索树IO过程
第1次IO:读取根节点(磁盘块1)
磁盘块1:[10] // 根节点值为10/ \/ \
左子树 右子树
[5] [20]
(磁盘块2) (磁盘块3)第2次IO:根据根节点判断15 > 10,读取右子树节点(磁盘块3)
磁盘块3:[20] // 节点值为20/ \/ \
左子树 右子树
[15] [25]
(磁盘块4) (磁盘块5)第3次IO:根据磁盘块3判断15 < 20,读取左子树节点(磁盘块4)
磁盘块4:[15] 找到目标关键字15二叉搜索树通过"左小右大" 的二路分支查找,从根到目标节点需经过 3 层,因此产生 3 次 IO。
多叉搜索树IO过程
第1次IO:读取根节点(磁盘块A)
磁盘块A:[10, 20] // 2个关键字,分隔为3个区间:(-∞,10)、(10,20)、(20,+∞)/ | \/ | \
子树1 子树2 子树3
[5,8] [15] [25]
(磁盘块B) (磁盘块C) (磁盘块D)第2次IO:根据根节点判断15在(10,20)区间,读取子树2(磁盘块C)
磁盘块C:[15] // 找到目标关键字15
通过多路分支(3 个分支)减少树高,从根到目标节点仅需经过 2 层,因此产生 2 次 IO。
B树 数据和索引“住在一起”
B树为什么叫B树,似乎是Rudolf Bayer的B来命名的。
B树的节点结构:一个B树的节点,无论是根节点、中间的“树枝”节点,还是最下方的“叶子”节点,它的内部都包含三样东西:
-
键 (Key):用来排序和查找的依据,比如用户ID。
-
数据 (Data):这条键对应的完整数据记录,或者是这条数据在硬盘上的存储地址。
-
子节点指针:指向下一层节点的“路标”。
B+树 索引和数据“严格分居”
B+树是B树的优化版,也是现在几乎所有关系型数据库(如MySQL)的默认选择。
B+树节点结构:B+树的节点分为两种类型:
-
内部节点 (Internal Node):包括根节点和所有“树枝”节点。它们里面只存储“键”和“子节点指针”。它们不存储任何真实数据,唯一的任务就是“指路”,告诉你下一步该往哪走。
-
叶子节点 (Leaf Node):位于树的最底层。它们才包含“键”和这条键对应的完整“数据”。
-
所有的叶子节点本身是通过一个双向链表串联起来的。这意味着所有的数据在底层都是有序排列的。
B树和B+树对比
对比项 | B树 | B+树 |
---|---|---|
数据存放 | 所有节点都可以存数据 | 只有叶子节点存数据 |
内部节点 | 存键+数据,功能复杂 | 只存键,纯粹的索引,更小更快 |
查找过程 | 路径不固定,可能提前结束 | 必须走到叶子节点,路径长度固定 |
范围查找 | 效率低(需反复遍历树) | 效率极高(只需在叶子链表上顺序移动) |
最后,为树结构放两个题单。
https://www.cnblogs.com/CLGYPYJ/tag/线段树/
https://www.cnblogs.com/CLGYPYJ/tag/最小生成树/
堆
堆的概念
堆中只关心父节点的值是否大于子节点的值,这点与二叉搜索树有所区别(我觉得因为只关心最值所以更简单一些了)
堆中的每一个节点值都大于等于(或小于等于)子树中所有节点的值。或者说,任意一个节点的值都大于等于(或小于等于)所有子节点的值。
堆的用途
当我们只关心所有数据中的最大值或者最小值,存在多次获取最大值或者最小值,多次插入或删除数据时,就可以使用堆。
为了方便存储和索引,我们通常用完全二叉树的形式来表示二叉堆。
图
图结构是概念最大,包含内容最多的结构。由非空有限个顶点和它们间的边构成的集合就是图。其实,树结构 和 链表 都是特殊的图。在拓扑学和离散数学中以及对概念非常熟悉了,这里就讲一讲常见的编程算法吧。
图的遍历:深度优先 (DFS) 与广度优先 (BFS)
这是图算法的“Hello, World!”,是所有其他复杂算法的基础。我们会理解它们的工作原理和适用场景。
- 岛屿数量 (Number of Islands) - 经典中的经典题!
问题描述:给你一个由 '1'(陆地)和 '0'(水)组成的二维网格,计算岛屿的数量。 - 以及马的遍历之列
java的板子
import java.util.*;/*** 这个类提供了图遍历算法DFS和BFS的通用Java模板。* 假设图使用邻接表来表示。* 邻接表是一个 Map,其中键是节点,值是该节点的邻居列表。*/
public class GraphTraversalTemplates {// 假设图的节点是整数类型// key: 节点, value: 该节点的邻居列表private Map<Integer, List<Integer>> adjList;public GraphTraversalTemplates() {this.adjList = new HashMap<>();}// 用于构建图的方法public void addEdge(int u, int v) {adjList.computeIfAbsent(u, k -> new ArrayList<>()).add(v);// 如果是无向图,需要添加反向边// adjList.computeIfAbsent(v, k -> new ArrayList<>()).add(u);}// --- 深度优先搜索 (DFS) - 递归模板 ---/*** DFS的启动方法。* 它处理了图可能包含多个不相连组件的情况。*/public void dfs_entry() {// visited集合用于记录已经访问过的节点,防止死循环Set<Integer> visited = new HashSet<>();// 遍历图中的所有节点for (int node : adjList.keySet()) {if (!visited.contains(node)) {// 如果节点未被访问,就从这个节点开始进行一次新的DFS遍历System.out.println("--- Starting new DFS from node: " + node + " ---");dfs_recursive_helper(node, visited);}}}/*** DFS的核心递归辅助函数。* @param currentNode 当前正在访问的节点* @param visited 记录已访问节点的集合*/private void dfs_recursive_helper(int currentNode, Set<Integer> visited) {// 1. 将当前节点标记为已访问visited.add(currentNode);// 2. 处理当前节点(例如,打印它)System.out.println("Visiting node: " + currentNode);// 3. 遍历当前节点的所有邻居// adjList.getOrDefault可以防止当前节点没有任何邻居时出现空指针异常for (int neighbor : adjList.getOrDefault(currentNode, new ArrayList<>())) {// 4. 如果邻居节点未被访问,则递归调用DFSif (!visited.contains(neighbor)) {dfs_recursive_helper(neighbor, visited);}}}// --- 广度优先搜索 (BFS) - 迭代模板 ---/*** BFS的启动方法。* 它同样处理了图可能不连通的情况。*/public void bfs_entry() {// visited集合用于记录已经访问过的节点Set<Integer> visited = new HashSet<>();// queue是BFS的核心数据结构Queue<Integer> queue = new LinkedList<>();// 遍历图中的所有节点for (int node : adjList.keySet()) {if (!visited.contains(node)) {// 如果节点未被访问,就从这个节点开始进行一次新的BFS遍历System.out.println("--- Starting new BFS from node: " + node + " ---");// 1. 将起始节点加入队列,并标记为已访问queue.offer(node);visited.add(node);// 2. 当队列不为空时,循环继续while (!queue.isEmpty()) {// 3. 从队列中取出一个节点int currentNode = queue.poll();// 4. 处理当前节点(例如,打印它)System.out.println("Visiting node: " + currentNode);// 5. 遍历当前节点的所有邻居for (int neighbor : adjList.getOrDefault(currentNode, new ArrayList<>())) {// 6. 如果邻居未被访问,则将其加入队列并标记为已访问if (!visited.contains(neighbor)) {visited.add(neighbor);queue.offer(neighbor);}}}}}}public static void main(String[] args) {GraphTraversalTemplates graph = new GraphTraversalTemplates();// 构建一个图(包含两个不连通的部分)// Component 1graph.addEdge(0, 1);graph.addEdge(0, 2);graph.addEdge(1, 2);graph.addEdge(2, 3);// Component 2graph.addEdge(4, 5);System.out.println("====== DFS Traversal ======");graph.dfs_entry();System.out.println("\n====== BFS Traversal ======");graph.bfs_entry();}
}
最小生成树 (Minimum Spanning Tree, MST)
主要解决“如何用最低成本连接所有点”的问题。我们会学习两种经典算法:Prim 和 Kruskal。
最短路径 (Shortest Path)
解决“从A点到B点的最快/最短路线是什么”的问题。核心算法是 Dijkstra 和 Floyd-Warshall。
拓扑排序 (Topological Sort)
主要用于有向无环图 (DAG),解决任务的依赖关系和执行顺序问题。
[========]
关于图的基本概念都比较简单,直接放题咬人吧(/狗头)。
https://www.cnblogs.com/CLGYPYJ/tag/搜索/
https://www.cnblogs.com/CLGYPYJ/tag/最短路/
https://www.cnblogs.com/CLGYPYJ/tag/并查集/
https://www.cnblogs.com/CLGYPYJ/tag/最大流/
哈希
HashMap(最常用的哈希表实现)
- put(K key, V value):将键值对存入哈希表(根据key的hashCode()计算存储位置)。
- get(Object key):根据key的哈希值查找对应值(平均时间复杂度 O (1))。
- remove(Object key):删除指定key对应的键值对。
- containsKey(Object key):判断哈希表中是否包含指定key(基于哈希值和equals())。
HashSet(用hash实现的集合类)
- boolean add(E e):将指定元素添加到此集合中(如果该元素尚未存在)。如果此集合尚未包含指定元素,则返回 true;如果此集合已经包含该元素,则返回 false。
- boolean remove(Object o):如果指定元素存在于此集合中,则将其移除。如果此集合包含指定元素,则返回 true;否则返回 false。
- boolean contains(Object o):如果此集合包含指定元素,则返回 true;否则返回 false。
- boolean isEmpty():如果此集合不包含任何元素,则返回 true;否则返回 false。
- void clear():从此集合中移除所有元素,使集合变为空。
LRU (哈希+双链表)
class LRUCache {class DLinkedNode{int key;int value;DLinkedNode prev;DLinkedNode next;DLinkedNode(){};DLinkedNode(int key,int value){this.key=key;this.value=value;}}private Map<Integer,DLinkedNode> cache = new HashMap<Integer,DLinkedNode>();private int size;private int capacity;private DLinkedNode head,tail;public LRUCache(int capacity) {//初始化?this.size=0;this.capacity = capacity;head = new DLinkedNode();tail = new DLinkedNode();head.next =tail;tail.prev = head;}public int get(int key) {DLinkedNode node = cache.get(key);if(node==null){return -1;}move2Head(node);return node.value;}public void put(int key, int value) {DLinkedNode node = cache.get(key);if(node==null){DLinkedNode nnode = new DLinkedNode(key,value);//节点入表cache.put(key,nnode);//插入到双链表头部add2Head(nnode);size++;if(size > capacity){//插入后如果队列慢了,则需要出队DLinkedNode tail = removeTail();//在hash中删除cache.remove(tail.key);size--;}}else{//如果节点存在,则修改并更新位置node.value= value;move2Head(node);}}//双链表 头部插入节点private void add2Head(DLinkedNode node){node.prev = head;node.next = head.next;head.next.prev = node;head.next=node;}private void move2Head(DLinkedNode node){removeNode(node);add2Head(node);}private void removeNode(DLinkedNode node){node.prev.next = node.next;node.next.prev = node.prev;}private DLinkedNode removeTail(){DLinkedNode res = tail.prev;removeNode(res);return res;}
}/*** Your LRUCache object will be instantiated and called as such:* LRUCache obj = new LRUCache(capacity);* int param_1 = obj.get(key);* obj.put(key,value);*/