当前这个专题是最后一个版块(模拟),这个专题结束后还会出现剑指offer专题,希望能够对小伙伴们笔试有所帮助,早日拿到offer!
前言
所谓的模拟题,运用的“模拟算法”,其实并没有什么完全准确的定义。模拟算法,用一句老话说,就是“照着葫芦画瓢”;官方化的诠释则是:根据题目表述进行筛选提取关键要素,按需求书写代码解决实际问题。
模拟这个算法其实并不难,主要是逻辑上的麻烦,但正常刷题时我们都不把模拟的逻辑思维理清就直接做,如果这题没有太水的话,是非常容易错的。
特点
●码量大
●操作多
●思路繁复杂
●较为复杂的模拟题,出错后难以定位错误
技巧
●在动手写代码之前,在草纸上尽可能地写好要实现的流程.
●在代码中,尽量把每个部分模块化,写成函数、结构体或类.
●对于一些可能重复用到的概念,可以统-转化,方便处理:如,某题给你"YY-MM-DD时:分"把它抽取到一
个函数,处理成秒,会减少概念混淆
●调试时分块调试。模块化的好处就是可以方便的单独调某一部分。
●写代码的时候一定要思路清晰, 不要想到什么写什么,要按照落在纸上的步骤写。
具体详细内容可见博客:https://blog.csdn.net/qq_61386381/article/details/123264225
旋转数组
题目描述:一个数组A中存有 n 个整数,在不允许使用另外数组的前提下,将每个整数循环向右移 M( M >=0)个位置,即将A中的数据由(A0 A1 ……AN-1 )变换为(AN-M …… AN-1 A0 A1 ……AN-M-1 )(最后 M 个数循环移至最前面的 M 个位置)。如果需要考虑程序移动数据的次数尽量少,要如何设计移动的方法?
示例:
输入:6,2,[1,2,3,4,5,6]
返回值:[5,6,1,2,3,4]
思路:循环右移相当于从第m个位置开始,左右两部分视作整体翻转。即abcdefg右移3位efgabcd可以看成AB翻转成BA(这里小写字母看成数组元素,大写字母看成整体)。既然是翻转我们就可以用到reverse函数。
具体步骤:
step 1:因为mmm可能大于nnn,因此需要对nnn取余,因为每次长度为nnn的旋转数组相当于没有变化。
step 2:第一次将整个数组翻转,得到数组的逆序,它已经满足了右移的整体出现在了左边。
step 3:第二次就将左边的mmm个元素单独翻转,因为它虽然移到了左边,但是逆序了。
step 4:第三次就将右边的n−mn-mn−m个元素单独翻转,因此这部分也逆序了。
import java.util.*;
public class Solution {
/**
* 旋转数组
* @param n int整型 数组长度
* @param m int整型 右移距离
* @param a int整型一维数组 给定数组
* @return int整型一维数组
*/
public int[] solve (int n, int m, int[] a) {
// write code here
m = m % n ;
// 逆转全部数组元素
reverse(a, 0, n-1);
// 逆转开头几个元素
reverse(a, 0, m-1);
// 逆转结尾的几个元素
reverse(a, m, n-1);
return a;
}
public void reverse(int [] nums, int start, int end){
while(start < end){
swap(nums, start++, end--);
}
}
// swap 函数
public void swap(int[] nums, int a, int b){
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}
螺旋矩阵
题目描述:给定一个m x n大小的矩阵(m行,n列),按螺旋的顺序返回矩阵中的所有元素。
示例:
输入: [[1,2,3],[4,5,6],[7,8,9]]
返回值: [1,2,3,6,9,8,7,4,5]
思路:简单模拟,我们想象有一个矩阵,从第一个元素开始,往右到底后再往下到底后再往左到底后再往上,结束这一圈,进入下一圈螺旋。
具体步骤:
step 1:首先排除特殊情况,即矩阵为空的情况。
step 2:设置矩阵的四个边界值,开始准备螺旋遍历矩阵,遍历的截止点是左右边界或者上下边界重合。
step 3:首先对最上面一排从左到右进行遍历输出,到达最右边后第一排就输出完了,上边界相应就往下一行,要判断上下边界是否相遇相交。
step 4:然后输出到了右边,正好就对最右边一列从上到下输出,到底后最右边一列已经输出完了,右边界就相应往左一列,要判断左右边界是否相遇相交。
step 5:然后对最下面一排从右到左进行遍历输出,到达最左边后最下一排就输出完了,下边界相应就往上一行,要判断上下边界是否相遇相交。
step 6:然后输出到了左边,正好就对最左边一列从下到上输出,到顶后最左边一列已经输出完了,左边界就相应往右一列,要判断左右边界是否相遇相交。
step 7:重复上述3-6步骤直到循环结束。
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> spiralOrder(int[][] matrix) {
ArrayList<Integer> res = new ArrayList<>();
// 排除特殊情况
if(matrix.length == 0){
return res;
}
// 左边
int left = 0;
// 右边
int right = matrix[0].length -1;
// 上边
int up = 0;
// 下边
int down = matrix.length -1;
// 直到边界重合
while(left <= right && up <= down){
//上边界从左到右
for(int i = left; i<= right; i++){
res.add(matrix[up][i]);
}
//继续向下
up ++;
if(up > down)
break;
// 右边界从上到下
for(int i = up; i<=down; i++)
res.add(matrix[i][right]);
// 右边界向左
right--;
if(left > right)
break;
// 下边界从右往左
for(int i = right ; i>=left; i--){
res.add(matrix[down][i]);
}
//下边界向上
down--;
if(up > down){
break;
}
// 左边界从下到上
for(int i=down; i>=up;i--)
res.add(matrix[i][left]);
// 左边界向右
left++;
if(left > right)
break;
}
return res;
}
}
顺时针旋转矩阵
题目描述:有一个nxn整数矩阵,请编写一个算法,将矩阵顺时针旋转90度。
给定一个nxn的矩阵,和矩阵的阶数n,请返回旋转后的nxn矩阵。
示例:
输入:[[1,2,3],[4,5,6],[7,8,9]],3
返回值:[[7,4,1],[8,5,2],[9,6,3]]
这个题目没有技巧,需要将原矩阵和转换后的矩阵画出来
这就是互为转置的两个矩阵。因为转置的可逆性,只要过程逆转,就可以得到顺时针旋转90度后的矩阵了。
具体步骤:
step 1:遍历矩阵的下三角矩阵,将其与上三角矩阵对应的位置互换,其实就是数组下标交换后的互换。
step 2:遍历矩阵每一行,将每一行看成一个数组使用reverse函数翻转。
import java.util.*;
public class Solution {
public int[][] rotateMatrix(int[][] mat, int n) {
// write code here
int length = mat.length;
//矩阵转置
for(int i=0; i< length; i++){
for(int j=0; j<i ; j++){
// 交换
int temp =mat[i][j];
mat[i][j] = mat[j][i];
mat[j][i] = temp;
}
}
// 进行每行翻转
for(int i =0 ; i < length; i++){
for(int j=0; j< length/2; j++){
int temp = mat[i][j];
mat[i][j] = mat[i][length -j-1];
mat[i][length-j-1] = temp;
}
}
return mat;
}
}
设计LRU缓存结构
题目描述:
设计LRU(最近最少使用)缓存结构,该结构在构造时确定大小,假设大小为 capacity ,操作次数是 n ,并有如下功能:
- Solution(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
- get(key):如果关键字 key 存在于缓存中,则返回key对应的value值,否则返回 -1 。
- set(key, value):将记录(key, value)插入该结构,如果关键字 key 已经存在,则变更其数据值 value,如果不存在,则向缓存中插入该组 key-value ,如果key-value的数量超过capacity,弹出最久未使用的key-value
输入:
[“set”,“set”,“get”,“set”,“get”,“set”,“get”,“get”,“get”],[[1,1],[2,2],[1],[3,3],[2],[4,4],[1],[3],[4]],2
返回值:
[“null”,“null”,“1”,“null”,“-1”,“null”,“-1”,“3”,“4”]
说明:
我们将缓存看成一个队列,最后一个参数为2代表capacity,所以
Solution s = new Solution(2);
s.set(1,1); //将(1,1)插入缓存,缓存是{“1”=1},set操作返回"null"
s.set(2,2); //将(2,2)插入缓存,缓存是{“2”=2,“1”=1},set操作返回"null"
output=s.get(1);// 因为get(1)操作,缓存更新,缓存是{“1”=1,“2”=2},get操作返回"1"
s.set(3,3); //将(3,3)插入缓存,缓存容量是2,故去掉某尾的key-value,缓存是{“3”=3,“1”=1},set操作返回"null"
output=s.get(2);// 因为get(2)操作,不存在对应的key,故get操作返回"-1"
s.set(4,4); //将(4,4)插入缓存,缓存容量是2,故去掉某尾的key-value,缓存是{“4”=4,“3”=3},set操作返回"null"
output=s.get(1);// 因为get(1)操作,不存在对应的key,故get操作返回"-1"
output=s.get(3);//因为get(3)操作,缓存更新,缓存是{“3”=3,“4”=4},get操作返回"3"
output=s.get(4);//因为get(4)操作,缓存更新,缓存是{“4”=4,“3”=3},get操作返回"4"
思路:哈希表+双向链表
知识点1:哈希表
哈希表是一种根据关键码(key)直接访问值(value)的一种数据结构。而这种直接访问意味着只要知道key就能在O(1)O(1)O(1)时间内得到value,因此哈希表常用来统计频率、快速检验某个元素是否出现过等。
知识点2:双向链表
双向链表是一种特殊的链表,它除了链表具有的每个节点指向后一个节点的指针外,还拥有一个每个节点指向前一个节点的指针,因此它可以任意向前或者向后访问,每次更改节点连接状态的时候,需要变动两个指针。
插入与访问值都是O(1),没有任何一种数据结构可以直接做到。
于是我们可以想到数据结构的组合:访问O(1)很容易想到了哈希表;插入O(1)的数据结构有很多,但是如果访问到了这个地方再选择插入,且超出长度要在O(1)之内删除,我们可以想到用链表,可以用哈希表的key值对应链表的节点,完成直接访问。但是我们还需要把每次访问的key值节点加入链表头,同时删掉链表尾,所以选择双向链表,便于删除与移动。
具体步骤:
step 1:构建一个双向链表的类,记录key值与val值,同时一前一后两个指针。用哈希表存储key值和链表节点,这样我们可以根据key值在哈希表中直接锁定链表节点,从而实现在链表中直接访问,能够做到O(1)时间访问链表任意节点。
//设置双向链表结构
class Node{
int key;
int val;
Node pre;
Node next;
//初始化
public Node(int key, int val) {
this.key = key;
this.val = val;
this.pre = null;
this.next = null;
}
}
step 2:设置全局变量,记录双向链表的头、尾及LRU剩余的大小,并全部初始化,首尾相互连接好。
//构建初始化连接
//链表剩余大小
this.size = k;
this.head.next = this.tail;
this.tail.pre = this.head;
step 3:遍历函数的操作数组,检查第一个元素判断是属于set操作还是get操作。
step 4:如果是set操作,即将key值与val值加入链表,我们先检查链表中是否有这个key值,可以通过哈希表检查出,如果有直接通过哈希表访问链表的相应节点,修改val值,并将访问过的节点移到表头;如果没有则需要新建节点加到表头,同时哈希表中增加相应key值(当然,还需要检查链表长度还有无剩余,若是没有剩余则需要删去链表尾)。
//没有见过这个key,新值加入
if(!mp.containsKey(key)){
Node node = new Node(key, val);
mp.put(key, node);
//超出大小,移除最后一个
if(size <= 0)
removeLast();
//大小还有剩余
else
//大小减1
size--;
//加到链表头
insertFirst(node);
}
//哈希表中已经有了,即链表里也已经有了
else{
mp.get(key).val = val;
//访问过后,移到表头
moveToHead(mp.get(key));
}
step 5:不管是新节点,还是访问过的节点都需要加到表头,若是访问过的,需要断开原来的连接,再插入表头head的后面。
//移到表头函数
void moveToHead(Node node){
//已经到了表头
if(node.pre == head)
return;
//将节点断开,取出来
node.pre.next = node.next;
node.next.pre = node.pre;
//插入第一个前面
insertFirst(node);
}
step 6:删除链表尾需要断掉尾节点前的连接,同时哈希表中去掉这个key值。
void removeLast(){
//哈希表去掉key
mp.remove(tail.pre.key);
//断连该节点
tail.pre.pre.next = tail;
tail.pre = tail.pre.pre;
}
step 7:如果是get操作,检验哈希表中有无这个key值,如果没有说明链表中也没有,返回-1,否则可以根据哈希表直接锁定链表中的位置进行访问,然后重复step 5,访问过的节点加入表头。
if(mp.containsKey(key)){
Node node = mp.get(key);
res = node.val;
moveToHead(node);
}
import java.util.*;
public class Solution {
//设置双向链表结构
static class Node{
int key;
int val;
Node pre;
Node next;
//初始化
public Node(int key, int val) {
this.key = key;
this.val = val;
this.pre = null;
this.next = null;
}
}
//哈希表
private Map<Integer, Node> mp = new HashMap<>();
//设置一个头
private Node head = new Node(-1, -1);
//设置一个尾
private Node tail = new Node(-1, -1);
private int size = 0;
public int[] LRU (int[][] operators, int k) {
//构建初始化连接
//链表剩余大小
this.size = k;
this.head.next = this.tail;
this.tail.pre = this.head;
//获取操作数
int len = (int)Arrays.stream(operators).filter(x -> x[0] == 2).count();
int[] res = new int[len];
//遍历所有操作
for(int i = 0, j = 0; i < operators.length; i++){
if(operators[i][0] == 1)
//set操作
set(operators[i][1], operators[i][2]);
else
//get操作
res[j++] = get(operators[i][1]);
}
return res;
}
//插入函数
private void set(int key, int val){
//没有见过这个key,新值加入
if(!mp.containsKey(key)){
Node node = new Node(key, val);
mp.put(key, node);
//超出大小,移除最后一个
if(size <= 0)
removeLast();
//大小还有剩余
else
//大小减1
size--;
//加到链表头
insertFirst(node);
}
//哈希表中已经有了,即链表里也已经有了
else{
mp.get(key).val = val;
//访问过后,移到表头
moveToHead(mp.get(key));
}
}
//获取数据函数
private int get(int key){
int res = -1;
if(mp.containsKey(key)){
Node node = mp.get(key);
res = node.val;
moveToHead(node);
}
return res;
}
//移到表头函数
private void moveToHead(Node node){
//已经到了表头
if(node.pre == head)
return;
//将节点断开,取出来
node.pre.next = node.next;
node.next.pre = node.pre;
//插入第一个前面
insertFirst(node);
}
//将节点插入表头函数
private void insertFirst(Node node){
node.pre = head;
node.next = head.next;
head.next.pre = node;
head.next = node;
}
//删去表尾函数,最近最少使用
private void removeLast(){
//哈希表去掉key
mp.remove(tail.pre.key);
//断连该节点
tail.pre.pre.next = tail;
tail.pre = tail.pre.pre;
}
}
=====================================================================
import java.util.*;
class Node {
public int key;
public int value;
public Node front;
public Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
this.front = null;
this.next = null;
}
}
public class Solution {
private int size;
private int capacity;
private Node start;
private Node end;
private Map<Integer, Node> map;
public Solution(int capacity) {
// write code here
this.capacity = capacity;
this.size = 0;
this.map = new HashMap<>(capacity + 1);
this.start = new Node(0, 0);
this.end = new Node(0, 0);
this.start.front = this.end;
this.start.next = this.end;
this.end.front = this.start;
this.end.next = this.start;
}
public int get(int key) {
// write code here
if (!map.containsKey(key)) {
return -1;
}
Node node = this.removeFromLink(map.get(key));
this.insertIntoStart(node);
return node.value;
}
public void set(int key, int value) {
// write code here
if (this.map.containsKey(key)) {
Node node = this.removeFromLink(map.get(key));
node.value = value;
this.insertIntoStart(node);
return;
}
Node newNode = new Node(key, value);
this.insertIntoStart(newNode);
map.put(newNode.key, newNode);
this.size++;
if (this.size > this.capacity) {
Node deleteNode = this.removeFromLink(this.end.front);
map.remove(deleteNode.key);
this.size--;
}
}
private Node removeFromLink(Node node) {
Node before = node.front, after = node.next;
before.next = after;
after.front = before;
node.front = null;
node.next = null;
return node;
}
private void insertIntoStart(Node node) {
Node temp = this.start.next;
this.start.next = node;
node.front = this.start;
temp.front = node;
node.next = temp;
}
}