LeetCode 剑指 Offer 专项练习 (第一阶段)

本文介绍了在不允许使用传统算术运算符的情况下,如何通过位运算进行整数除法,以及利用前缀和与哈希表解决和为k的子数组问题。同时,探讨了使用双指针和哈希表在最短时间内找到包含所有目标字符的最短字符串的方法,并讨论了LRU缓存的实现策略,确保所有操作的时间复杂度为O(1)。
摘要由CSDN通过智能技术生成

1、整数除法(二进制)
在这里插入图片描述

不得使用 *、/、% ,那只能使用加减以及位运算模拟乘除。

首先容易想到的是 通过减法, a减去n个b后,当差值小于b时,n即为a与b的商。但该算法计算n次,时间复杂度为O(n)。由题目可知,当a为 -2^31, b为-1时,循环次数达到20亿次,一定会超时。所以需要比逐个减更快速的“除法”。

提高运算效率,先看式子:a/b = r …… c 即 b * r + c = a,即整数(a - c)是可以由若干个b表示的。这可以联想到二进制,任意整数可以用若干个二进制数相加得到,同理,a-c可以由若干的二进制数 乘 b 构成。由于二进制数可以使用位运算计算所得,所以计算一个二进制数的时间复杂度为O(lgN),通过循环的减法计算a-b需要的次数也为O(lgN),得到总的时间复杂度为O(lg2N)。N的取值范围为0到31,所以改解法最大循环次数不会超过千次,符合时间要求。
在这里插入图片描述
需要注意题目中有负数,需要先对数据取绝对值。

class Solution {
    func divide(_ a: Int, _ b: Int) -> Int {
        var count:Int = 0
        var tmpA:Int = abs(a)
        var tmpB:Int = abs(b)
        var del:Int = tmpA
        while (tmpA >= tmpB)
        {
            del = tmpA - tmpB
            count += 1
            var k:Int = 1
            while (del >= tmpB)
            {
                tmpB = tmpB << 1
                if (del - tmpB) < 0
                {
                    break
                }
                k = k << 1
                count += k
                del = del - tmpB
            }
            tmpB = abs(b)
            tmpA = del
        }
        
        if a < 0
        {
            count = 0 - count
        }
        
        if b < 0
        {
            count = 0 - count
        }
        
        if count >= 2147483648
        {
            count -= 1
        }
        
        return count
    }
}

2、和为k的子数组(前缀和、哈希表)
在这里插入图片描述

数列的求和问题,如果从第一个数开始,逐个相加再比较是否相等,需要比较次n2,时间复杂度O(n2),显然不符合要求的。
现用an表示数列的第n项,Sn表示数列前n项的求和。则序列和等于k有以下情况:(1)Sn 等于 k;(2)Sn - Sm(0< m < n) 等于 k;第一种情况,只需遍历求和即可判断得到;而第二种情况则需要使用哈希表存放序列求和对应的个数,才可以得到。
举例:数组为[1, 2, -1, -2, 3]
在这里插入图片描述
遍历数组,将符合上述两种情况的数量相加即可得到最终的答案,时间复杂度为O(n)。

class Solution {
   func subarraySum(_ nums: [Int], _ k: Int) -> Int {
       var count:Int = 0
        var sumNums:Int = 0
        var numsMap:Dictionary<Int, Int> = Dictionary<Int, Int>()
        
        var index:Int = 0
        while (index < nums.count)
        {
            sumNums += nums[index]
            let del = sumNums - k
            if del == 0
            {
                count += 1
            }
            
            count += (numsMap[del] ?? 0)
            
            numsMap[sumNums] = (numsMap[sumNums] ?? 0) + 1
            
            index += 1
        }
        return count
    }
}

3、含有所有字符的最短字符串(双指针、哈希表)

在这里插入图片描述
该题主要为两个步骤:(1)在s中找到含有t所有字符的子字符串;(2)记录子字符串的长度。步骤二并没有循环,所以主要考虑的问题是如何快速完成步骤一。如果从字符串的开端开始,逐个拼接寻找符合的要求的子串,需要遍历n2次,时间复杂度O(n2)显然超出时间的限制。所以可以考虑双指针配合哈希表使用。
先设置两个指针:start 指针指向子串的开始,end 指针指向子串的结尾。如何判断start到end的区间内的字符串包含了t的所有字符?可以先遍历字符串t,获取t不重复的字符个数(countOfT) 以及t每个字符对应的数量的哈希表。遍历字符串s时,记录s当前字符的个数,当个数与t中对应字符的对应个数相等时,记录s的计数(countOfS)。当countOfS 与 countOfT 相等时,说明此时start 到 end区间内到字符串已经含有t的所有字符,此时需要将start右移,以缩短子字符串的长度,直到该子字符串不能包含t中所有字符为止, 时间复杂度为O(n)。
举例:s = “ADOBECBANC”, t = “ABC” 则countOfT = 3, HashOfT:
在这里插入图片描述
在这里插入图片描述

class Solution {
    func minWindow(_ s: String, _ t: String) -> String {
        let sArr:Array<Character> = Array<Character>(s)
        let tArr:Array<Character> = Array<Character>(t)
        
        var sMap:Array<Int> = Array<Int>.init(repeating: 0, count: 60)
        var tMap:Array<Int> = Array<Int>.init(repeating: 0, count: 60)
        
        let A:Character = "A"
        var countOfT:Int = 0
        
        for item in tArr
        {
            let t2Num:Int = Int(item.asciiValue! - A.asciiValue!)
            tMap[t2Num] += 1
            if tMap[t2Num] == 1
            {
                countOfT += 1
            }
        }
        
        var start:Int = 0
        var end:Int = 0
        
        var lastStart:Int = -1
        var lastEnd:Int = -1
        var countOfS:Int = 0
        
        while (end < sArr.count)
        {
            let s2Num:Int = Int(sArr[end].asciiValue! - A.asciiValue!)
            sMap[s2Num] += 1
            if sMap[s2Num] == tMap[s2Num]
            {
                countOfS += 1
            }
            
            while (countOfS == countOfT)
            {
                // 记录
                if lastEnd == -1 || ((lastEnd - lastStart) > (end - start))
                {
                    lastEnd = end
                    lastStart = start
                }
                let s2NumStart:Int = Int(sArr[start].asciiValue! - A.asciiValue!)
                sMap[s2NumStart] -= 1
                if sMap[s2NumStart] < tMap[s2NumStart]
                {
                    countOfS -= 1
                }
                start += 1
            }
            end += 1
        }
        
        if lastEnd == -1
        {
            return ""
        }
        let result:String = String(s.dropFirst(lastStart).prefix((lastEnd - lastStart + 1)))
        return result
    }
}

4、LRU (最近最少使用置换算法)
在这里插入图片描述
要求对容器的访问、加入新元素、修改元素的时间复杂度都为O(1)。需要使用哈希配合双链表实现。假设缓存块为页面,哈希表存放关键字key对应的页面节点。使用head指针指向链表开头、tail指针指向结尾,双链表可以让链表刷新时找到前后页面。当访问或修改某个关键字对应页面时,将该页面放置到链表末尾,然后更新tail指针。当加入新关键字时,若当前链表长度达到容量上限,移除head指针指向元素,然后更新head指针即可。所以无论访问、修改或添加元素,时间复杂的都为O(1)。
在实现上,可以在链表的头尾分别加上 虚拟的头尾指针,避免移除head节点或添加tail节点时需要单独考虑。空间复杂度方面,双向链表最大节点量为capacity + 2,哈希表最大节点量为capacity,所以空间复杂度为O(capacity)。

// 内存节点
public class cacheNode
{
    public var index:Int
    public var value:Int
    public var next:cacheNode?
    public var pre:cacheNode?
    
    init(_ id_:Int, _ value_: Int)
    {
        index = id_
        value = value_
        next = nil
        pre = nil
    }
}
// LRU算法
class LRUCache {
    private var head:cacheNode?
    private var map:Dictionary<Int, cacheNode>
    private var capacity_:Int
    private var count:Int
    private var tail:cacheNode?
    
    init(_ capacity: Int) {
        capacity_ = capacity
        head = cacheNode(-1, -1)
        head?.next =  cacheNode(-2, -1)
        tail = head?.next
        tail?.pre = head
        map = Dictionary<Int, cacheNode>()
        count = 0
    }

    private func delete()
    {
        map.removeValue(forKey: head!.next!.index)
        head!.next!.next?.pre = head
        head!.next = head!.next!.next
        count -= 1
    }
    
    private func refresh(_ key:Int)
    {
        let cur = map[key]
        
        if cur?.next === tail
        {
            return
        }
        
        cur?.pre?.next = cur?.next
        cur?.next?.pre = cur?.pre
        cur?.pre = tail?.pre
        tail?.pre?.next = cur
        tail?.pre = cur
        cur?.next = tail
        map[key] = tail?.pre
    }

    func get(_ key: Int) -> Int {
        if map.keys.contains(key) == true
        {
            refresh(key)
        }
        return map[key]?.value ?? -1
    }

    func put(_ key: Int, _ value: Int) {
        if map.keys.contains(key) == true
        {
            map[key]?.value = value
            refresh(key)
        }
        else
        {
            count += 1
            if count > capacity_
            {
                delete()
            }
            
            let tmp:cacheNode? = tail?.pre
            tmp?.next = cacheNode(key, value)
            tmp?.next?.pre = tmp
            tail?.pre = tmp?.next
            tmp?.next?.next = tail
            map[key] = tail?.pre
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值