前言
今天刷的是20.有效的括号、146.LRU缓存机制、41.缺失的第一个正数。(数模刚结束,这段时间不想再打数模了…今天的题是随便挑的,146是每日打卡题目,说实话有点小小的硬核)
20.有效的括号(容易):
题目描述:
给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/valid-parentheses
题解:
之前写了个暴力一点的解法,虽然也用了栈,但没那么快。后面加上哈希表结构(其实是个JSON对象),就快多了。这道题中,借助栈可以帮助我们完成括号对的递归消除。同时,适当使用哈希表之类的数据结构,也可以在O(1)时间内找到当前字符匹配的括号。
Code
// 优化解法
var isValid = function(s) {
if(s.length&1) return false; //奇数个符号,可以直接判断为不闭合
let map={'{':'}','[':']','(':')','?':'?'};
let stack=['?'];
for(const c of s){
if(map.hasOwnProperty(c)) stack.push(c);
else if(map[stack.pop()]!=c) return false;
}
return stack.length==1;
};
// 低效解法
var isValid = function(s) {
let stack=[];
for(let i=0;i<s.length;i++){
if(stack.length==0)
stack.push(s[i])
else{
if(s[i]==")"){
if(stack[stack.length-1]=="(")
stack.pop();
else if(stack[stack.length-1]=="["||"{")
return false;
}else if(s[i]=="}"){
if(stack[stack.length-1]=="{")
stack.pop();
else if(stack[stack.length-1]=="["||"(")
return false;
}else if(s[i]=="]"){
if(stack[stack.length-1]=="[")
stack.pop();
else if(stack[stack.length-1]=="{"||"(")
return false;
}else{
stack.push(s[i])
}
}
}
if(stack.length==0) return true;
else return false;
};
146.LRU缓存机制(中等):
题目描述:
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥已经存在,则变更其数据值;如果密钥不存在,则插入该组「密钥/数据值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
示例:
LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得密钥 2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得密钥 1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lru-cache
题解:
这道题琢磨了挺久,搞明白的时候已经天黑了哈哈,这种硬核的题目咋难度只有中等。
LRU是调度算法中比较常见的,首先要了解它是怎么运作的:
一、读取数据:
数据被读取时,意味着被使用,要移到数据结构的首部,其他数据的相对位置不变。
二、写入数据:
1、之前有的,就更新它的内容,然后放在首部;
2、之前没有的,按容量分两种情况:
①有容量,写入,并且放在首部;
②没容量,先把原先最末端的(过期的)删了,然后再写入,放在首部。
具体可以通过手机后台应用的显示顺序来理解(我觉得这方法挺好理解,好活儿)。
上述操作都要达到O(1)的时间复杂度,意味着数据结构能够使得查询、添加、删除都达到O(1)的时间复杂度。
光靠哈希表是做不到的,因为哈希表无序,且不能给键值对增加时间相关的属性,来得知密钥距离上次使用过去了多久。
结合数组的话?不行,插入和移动元素都是O(n),更别说删除元素了,也是O(n);单链表呢,要删一个节点需要访问它的前驱结点,我们在达到容量上限时删的是最后一个结点,这意味着删除的时间复杂度会是O(n)。
如果结合双向链表,似乎就可行了,因为双向链表可以访问前驱结点和后继结点,删除和移动只要改变节点指针的指向,不管是添加还是删除,时间复杂度都是O(1)。
在做题之前,我才发现一个冷知识:JS 的 Map 中的键是有序存储的(Map - JavaScript | MDN)。迭代它时,会以插入的顺序返回出键值,在这道题里使用Map作为哈希表就会变得很轻松。比如说可以让Map的首端表示最久没被使用过的密钥,尾部表示最近被使用过的密钥。那么在获取的时候,可以直接delete掉对应的键值对,并重新插入,回到Map尾部。在插入的时候同理,可以直接插入Map尾部。假如需要删除,则只要删除掉Map中处于首端的数据,可以通过迭代器+next().value来实现。
当然,我觉得这道题考察的是对LRU底层数据结构哈希链表的理解与实现,还是老实造轮子比较好。
Code
// 解法一:使用Map的解法(40% 100%)
/**
* @param {number} capacity
*/
var LRUCache = function(capacity) {
this.capacity = capacity;
this.cache = new Map();
};
/**
* @param {number} key
* @return {number}
*/
LRUCache.prototype.get = function(key) {
if(this.cache.has(key)) {
var val = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, val);
return val;
}
else
return -1;
};
/**
* @param {number} key
* @param {number} value
* @return {void}
*/
LRUCache.prototype.put = function(key, value) {
if(this.cache.size < this.capacity && !this.cache.has(key)) {
this.cache.set(key, value);
} else if(this.cache.has(key)) {
this.cache.delete(key);
this.cache.set(key, value);
} else if (this.cache.size = this.capacity) {
// Map.prototype.keys() 返回一个迭代对象
// 迭代对象 Iterator.next() 是迭代对象的第一个对象,而不是值,需要 .value获取值
this.cache.delete(this.cache.keys().next().value);
this.cache.set(key, value);
}
};
// 解法二:造轮子(参考榜一大佬的代码,90%,100%)
class LinkedNode {
constructor(key, value) {
this.key = key // 存key
this.value = value // 存value
this.next = null
this.prev = null
}
}
//哈希链表
function LRUCache(capacity) {
this.capacity = capacity // 缓存的容量
this.cache = {} // 哈希表用对象实现
this.size = 0 // 缓存数目,初始为0
this.head = new LinkedNode() // 虚拟头结点
this.tail = new LinkedNode() // 虚拟尾节点
this.head.next = this.tail
this.tail.prev = this.head // 相联系
};
//删除结点
LRUCache.prototype._removeNode=function(node){
node.prev.next=node.next;
node.next.prev=node.prev;
}
//增加新结点(到首部)
LRUCache.prototype._addNode=function(node){
node.prev=this.head;//指向虚拟头结点
node.next=this.head.next;//指向第一个结点
this.head.next.prev=node;//反过来相连
this.head.next=node;
}
//密钥被访问,把结点移动到首部
LRUCache.prototype._moveToHead=function(node){
//和Map解法差不多的思路,先删掉,再插入首部,时间复杂度为O(1)
this._removeNode(node);
this._addNode(node);
}
//当容量满的时候,把末端的最久没被访问的密钥删除(弹出)
LRUCache.prototype._popTail=function(){
//通过虚拟尾结点,得到尾部结点,然后把它删了。
let res=this.tail.prev;
this._removeNode(res);
return res;
}
//原型方法get
LRUCache.prototype.get = function(key) {
//不存在
if (!this.cache.hasOwnProperty(key)) {
return -1
}
//存在,获取并移动到首部
let node = this.cache[key];
this._moveToHead(node);
return node.value;
};
//原型方法put
LRUCache.prototype.put = function(key, value) {
//不存在,创建新结点并添加到首部
if (!this.cache.hasOwnProperty(key)) {
let newNode = new LinkedNode();
newNode.key = key;
newNode.value = value;
this.cache[key] = newNode;
this._addNode(newNode);
//改变容量,别忘记
this.size += 1;
//溢出,删掉末端结点(这里是先污染后判断,会更简洁一些)
if (this.size > this.capacity) {
let tail = this._popTail();
delete this.cache[tail.key];
this.size--;
}
} else {
//存在,更新value并送到首部
let node = this.cache[key];
node.value = value;
this._moveToHead(node);
}
};
41. 缺失的第一个正数(困难):
题目描述:
给你一个未排序的整数数组,请你找出其中没有出现的最小的正整数。
你的算法的时间复杂度应为O(n),并且只能使用常数级别的额外空间。
示例:
输入: [1,2,0]
输出: 3
输入: [3,4,-1,1]
输出: 2
输入: [7,8,9,11,12]
输出: 1
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/first-missing-positive
题解:
读题,对时间复杂度有要求,O(n),而且常数级别的额外空间,O(1)。再看一下输入数据,可以是任意正数(要考虑负数、0)。我们先假设输入里面存在正数,如果我们想要寻找第一个缺失正数,似乎就得遍历从1开始的所有数字,看一下数组中是否存在,假如存在就continue,不存在就返回缺失数字作为结果。这样的时间复杂度是多少呢,要取决于这个数组最大的正数是多少,以及从数组中查找元素是否存在要花多少时间。综合来说时间复杂度肯定要O(n²),使用到的空间是O(1)。
要缩小时间复杂度,关键就在于遍历小于等于最大值所有正数这个操作①,和从数组中查找元素是否存在这个操作②,能不能将其中一个的时间降到常数级别。操作①肯定降不下来,所以要从操作②入手。注意,骚操作来了,我们知道数组是这么存的:a[0]=4,a[1]=5,a[2]=7…,我能不能把索引作为元素,把元素作为索引呢?上面的数组变成a[4]=0,a[5]=1,a[7]=2…其实就是变成哈希表,可以在操作①遍历的时候,将元素作为键值,加入哈希表,之后再次遍历,就可以通过值是否存在,得知是否缺失对应的正数。不过这道题不能另外创建空间,我们得在原数组上想想办法。
上面的思路还是比较模糊,可以整理一下:
1、遍历一遍,检查数组里面有没有1。如果没有,那就结束返回1。
2、看一下nums等不等于[1],等于的话答案就是2。
3、再次遍历,把负数,零和大于n的数替换成1。(这是因为缺失的正数一定小于等于n+1,所以可以把大于n的都排除了)。
4、第三次遍历,在j位置读到数字i(大于0)时,将第i个元素变成负的(表示出现过),但要避免重复置负。
5、再次遍历,返回出现的第一个正数元素的下标。假如遍历完没发现正数元素,那就返回n+1。
Code
var firstMissingPositive = function(nums) {
let n=nums.length;
let contain1=false;
for(let i=0;i<n;i++){
if(nums[i]==1)
contain1=true;
}
if(!contain1) return 1;
if(n==1) return 2;
for(let i=0;i<n;i++){
if(nums[i]<=0||nums[i]>n)
nums[i]=1;
}
for(let i=0;i<n;i++){
let a=Math.abs(nums[i]);
if(a==n)
//把第0位当成第n位使用,置负表示n出现了
nums[0]=-Math.abs(nums[0]);
else
nums[a]=-Math.abs(nums[a]);
}
//判断2到n-1是否出现
for(let i=2;i<n;i++){
if(nums[i]>0)
return i;
}
//判断n是否出现
if(nums[0]>0)
return n;
//1到n都出现了,结果是n+1
return n+1;
};