1.Hash基础
1.1Hash的概念和基本特征
哈希(Hash)也称为散列,就是把任意长度的输入,通过散列算法,变换成固定长度的输出,这个输出值
就是散列值。
很多人可能想不明白,这里的映射到底是啥意思,为啥访问的时间复杂度为O(1)?我们只要看存的时候和
读的时候分别怎么映射的就知道了。
我们现在假设数组array存放的是1到15这些数,现在要存在一个大小是7的Hash表中,该如何存呢?我们
存储的位置计算公式是:
index=number&7
通过取模运算。然后继续存7-13,结果如下
最后再存 14和15
这时候我们会发现有些数据被存到同一个位置了,我们后面再讨论。接下来,我们看看如何取。
假如我要测试13在不在这里结构里,则同样使用上面的公式来进行,很明显13模7=6,我们直接访问
array[6]这个位置,很明显是在的,所以返回true。
假如我要测试20在不在这里结构里,则同样使用上面的公式来进行,很明显20模7=6,我们直接访问
array[6]这个位置,但是只有6和13,所以返回false。.
理解这个例子我们就理解了H©s是如何进行最基本的映射的,还有就是为什么访问的时间复杂度为O(1)。
1.2Hash碰撞的处理方法
在上面的例子中,我们发现有些在Hsh中很多位置可能要存两个甚至多个元素,很明显单纯的数组是不行
的,这种两个不同的输入值,根据同一散列函数计算出的散列值相同的现象叫故碰撞。
那该怎么解决呢?常见的方法有:开放定址法(Uava里的Threadlocal、链地址法(Uava里的
ConcurrentHashMap)、再哈希法(布隆过滤器)、建立公共溢出区。后两种用的比较少,我们重点看前两
个。
开放地址法:(也就是扩大数组大小)
开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能
找到,并将记录存入。
链地址法(也就是数组存放的元素改为链式结构)
在HashMap(JDK8之前)底层就是这样实现的
将哈希表的每个单元作为链表的头结点,所有哈希地址为的元素构成一个同义词链表。即发生冲突时就把
该关键字链在以该单元为头结点的链表的尾部。例如:
队列的基础知识
2.1队列的概念和基本特征
基本特征,就是存数据是一个端口,取数据是一个端口,也就是一个存取受限的线性表结构
即FIFO先进先出原则
队列的实现也有两种方法:基于数据/基于链表
下面我们采用链表来实现队列
2.2实现队列
采用双向链表实现
public class Node {
public int val;
public Node next;
public Node pre;
public Node() {
}
public Node(int val) {
this.val = val;
}
}
public class LinkQueue {
public static void main(String[] args) {
LinkQueue linkQueue=new LinkQueue();
linkQueue.push(1);
linkQueue.push(2);
linkQueue.push(3);
System.out.println(linkQueue.pull());
linkQueue.traverse();
}
private Node front;
private Node rear;
private int size;
public LinkQueue(){
front=null;
rear=null;
size=0;
}
//入队
public void push(int val){
Node node=new Node(val);
if(front==null&&rear==null){
front=node;
rear=node;
}else {
node.next=rear;
rear.pre=node;
rear=node;
}
}
//出队
public int pull(){
if(front==null&&rear==null){
throw new RuntimeException("队列为空");
}
if(front==rear){
int res=front.val;
front=null;
rear=null;
return res;
}
int res=front.val;
Node node=front.pre;
node.next=null;
front=node;
return res;
}
//遍历队列
public void traverse(){
Node cur=front;
while (cur!=null){
System.out.println(cur.val);
cur=cur.pre;
}
}
}
1. 用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
- void push(int x) 将元素 x 推到队列的末尾
- int pop() 从队列的开头移除并返回元素
- int peek() 返回队列开头的元素
- boolean empty() 如果队列为空,返回 true ;否则,返回 false
说明:
- 你 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
- 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
解题思路:
队列的特性是 FIFOFIFOFIFO(先入先出),而栈的特性是 FILOFILOFILO(先入后出)。
知道两者特性之后,我们需要用两个栈来模拟队列的特性,一个栈为入队栈,一个栈为出对栈。
当出队栈存在内容时,出队栈的栈顶,即为第一个出队的元素。
若出队栈无元素,我们的需求又是出队的话,我们就需要将入队栈的内容反序导入出队栈,然后弹出栈顶即可。
注意:根据栈的的特性,我们仅能使用 pushpushpush 和 poppoppop 操作。
class MyQueue {
private static Stack<Integer> inStack;
private static Stack<Integer> outStack;
public MyQueue() {
inStack=new Stack<Integer>();
outStack=new Stack<Integer>();
}
public void push(int x) {
inStack.push(x);
}
public int pop() {
if(outStack.isEmpty()){
in2out();
}
return outStack.pop();
}
private void in2out() {
while (!inStack.isEmpty()){
Integer pop = inStack.pop();
outStack.push(pop);
}
}
public int peek() {
if(outStack.isEmpty()){
in2out();
}
return outStack.peek();
}
public boolean empty() {
if(inStack.isEmpty()&&outStack.isEmpty()){
return true;
}else {
return false;
}
}
}
2. 用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
- void push(int x) 将元素 x 压入栈顶。
- int pop() 移除并返回栈顶元素。
- int top() 返回栈顶元素。
- boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。
解题思路:两个队列
为了满足栈的特性,即最后入栈的元素最先出栈,在使用队列实现栈时,应满足队列前端的元素是最
后入栈的元素。可以使用两个队列实现栈的操作,其中queue1用于存储栈内的元素,queve2作为入
栈操作的辅助队列。
入栈操作时,首先将元素入队到queue2,然后将queue1的全部元素依次出队并入队到queue2,,此
时queue2的前端的元素即为新入栈的元素,再将queue1和queue2互换,则queue1的元素即为栈
内的元素,queue1的前端和后端分别对应栈顶和栈底。
由于每次入栈操作都确保guue1的前瑞元素为栈顶元素,因此出栈操作和获得栈顶元素操作都可以
简单实现。出栈操作只需要移除quue1的前端元素并返回即可,获得栈顶元素操作只需要获得
queue1的前端元素并返回即可(不移除元素)。
由于queue1用于存储栈内的元素,判断栈是否为空时,只需要判断queue1是否为空即可。
class MyStack {
private Queue<Integer> queue1;
private Queue<Integer> queue2;
public MyStack() {
queue1=new LinkedList<>();
queue2=new LinkedList<>();
}
public void push(int x) {
queue2.offer(x);
while (!queue1.isEmpty()){
queue2.offer(queue1.poll());
}
Queue<Integer> temp=queue1;
queue1=queue2;
queue2=temp;
}
public int pop() {
return queue1.poll();
}
public int top() {
return queue1.peek();
}
public boolean empty() {
return queue1.isEmpty();
}
}
一个队列:
入栈操作时,首先获得入栈前的元素个数 nnn,然后将元素入队到队列,再将队列中的前 nnn 个元素(即除了新入栈的元素之外的全部元素)依次出队并入队到队列,此时队列的前端的元素即为新入栈的元素,且队列的前端和后端分别对应栈顶和栈底。
由于每次入栈操作都确保队列的前端元素为栈顶元素,因此出栈操作和获得栈顶元素操作都可以简单实现。出栈操作只需要移除队列的前端元素并返回即可,获得栈顶元素操作只需要获得队列的前端元素并返回即可(不移除元素)。
class MyStack{
private Queue<Integer> queue;
public MyStack() {
this.queue=new LinkedList<Integer>();
}
public void push(int x) {
int size = queue.size();
queue.offer(x);
while (size>0){
queue.offer(queue.poll());
size--;
}
}
public int pop() {
return queue.poll();
}
public int top() {
return queue.peek();
}
public boolean empty() {
return queue.isEmpty();
}
}
3. n数之和专题
3.1. 两数之和
解题思路:
Hashmap:每次存入 nums[i] - i
如果遍历的时候 map.contains(target-nums[i],就将 value 和i返回
class Solution {
public int[] twoSum(int[] nums, int target) {
HashMap<Integer,Integer> map=new HashMap<>();
int[] res=new int[2];
for (int i = 0; i < nums.length; i++) {
if(map.containsKey(target-nums[i])){
res[0]=map.get(target-nums[i]);
res[1]=i;
break;
}else {
map.put(nums[i],i);
}
}
return res;
}
}
3.2. 三数之和
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请
你返回所有和为 0 且不重复的三元组。
注意: 答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]] 解释: nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意,输出的顺序和三元组的顺序并不重要。
解题思路:
排序+双指针+去重
class Solution {
public static void main(String[] args) {
int[] nums={-1,0,1,2,-1,-4};
threeSum(nums);
}
public static List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res=new ArrayList<>();
for (int i = 0; i < nums.length-2; i++) {
int x=nums[i];
int l=1+i;
int r=nums.length-1;
while (l<r){
if(nums[l]+nums[r]+x==0){
List<Integer> list=Arrays.asList(x,nums[l],nums[r]);
res.add(list);
l++;
r--;
}else if(nums[l]+nums[r]+x>0){
r--;
}else {
l++;
}
}
}
return res.stream().distinct().collect(Collectors.toList());
}
}
LRU也是非常经典的问题,而且常年是算法的热门,其实现也有技巧,现在我们就来一起看看。
缓存是应用软件的必备功能之一,在操作系统,Java里的Spring、mybatis、redis、mysql等软件中都有
自己的内部缓存模块,而缓存是如何实现的呢?在操作系统教科书里我们知道常用的有FFO、LRU和LFU
三种基本的方式。FIFO也就是队列方式不能很好利用程序局部性特征,缓存效果比较差,一般使用LRU
(最近最少使用)和LFU(最不经常使用淘汰算法)比较多一些。LRU是淘汰最长时间没有被使用的页面,
而LU是淘汰一段时间内,使用次数最少的页面。
从实现上LRU是相对容易的,而LFU比较复杂,我们本章重点研究一下LRU的问题,这也是一道高频题
目。LeetCode146:设计一个LRU缓存,这个题也经常见到,在牛客也是长期排名前三
1. LRU含义
先看看leetcode146,设计和实现一个LRU缓存机制
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
- LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
- int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
- void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
一个不太好的思路:
一个map存数据
一个map存(key,存入的时间)
class LRUCache {
private HashMap<Integer,Integer> map;
private HashMap<Integer,Integer> mapLRU;
int size;
int time;
public LRUCache(int capacity) {
map=new HashMap<>();
mapLRU=new HashMap<>();
size=capacity;
time=0;
}
public int get(int key) {
Integer res = map.get(key);
if(res==null){
return -1;
}
mapLRU.put(key, time++);
return res;
}
public void put(int key, int value) {
if(map.size()==size&&!map.containsKey(key)){
Set<Integer> set = mapLRU.keySet();
int min=Integer.MAX_VALUE;
int myKey=-1;
for (Integer i : set) {
Integer num = mapLRU.get(i);
if(num<min){
min=num;
myKey=i;
}
}
mapLRU.remove(myKey);
map.remove(myKey);
}
map.put(key,value);
mapLRU.put(key,time++);
}
}
哈希表+双向链表
LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。
双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。
这样以来,我们首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在 O(1)O(1)O(1) 的时间内完成 get 或者 put 操作。具体的方法如下:
对于 get 操作,首先判断 key 是否存在:
如果 key 不存在,则返回 −1;
如果 key 存在,则 key 对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。
对于 put 操作,首先判断 key 是否存在:
如果 key 不存在,使用 key 和 value 创建一个新的节点,在双向链表的头部添加该节点,并将 key 和该节点添加进哈希表中。然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;
如果 key 存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。
public class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; 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;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode newNode = new DLinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}