5月25日 每日leetcode三题(20/146/41)

前言

今天刷的是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;
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值