算法学习记录(六十六)

117 阅读3分钟

问:

  1. 剑指 Offer 56 - I. 数组中数字出现的次数
  2. 剑指 Offer 57. 和为s的两个数字
  3. 剑指 Offer 57 - II. 和为s的连续正数序列
  4. 剑指 Offer 58 - I. 翻转单词顺序
  5. 剑指 Offer 59 - I. 滑动窗口的最大值
  6. 剑指 Offer 59 - II. 队列的最大值
  7. 剑指 Offer 60. n个骰子的点数 解:
  8. 整个数组异或一遍,可知最后剩下的是 eor = a ^ b, a,b不相等,所以eor不为0,由此可推断出eor的二进制中,必定至少存在一个1,我们假设它在X位上。也就是说 a 和 b的二进制在X位上必定不相等。那么,只要我们以X位的数作为分界,把数组分为两份,可知道a,b必定在不同区域。那么我们再设置一个变量eor2去异或其中的一份数组,可以得到eor2就是a或者b。最后用eor ^ eor2,其结果就是另外一个数
const singleNumbers = function(nums) {
    let eor = 0
    nums.forEach(i => eor ^= i)
    const one = eor & (~eor + 1)
    let eor2 = 0
    nums.forEach((i) => {
        if ((i & one) === 0) eor2 ^= i
    })
    return [eor2, eor ^ eor2]
};
  1. 双指针遍历
const twoSum = function(nums, target) {
    let left = 0
    let right = nums.length - 1
    while (left < right) {
        if (nums[left] + nums[right] < target) {
            left++
        } else if (nums[left] + nums[right] === target) {
            return [nums[left], nums[right]]
        } else {
            right--
        }
    }
    return []
};
  1. 滑动窗口,窗口中值小于target就继续扩张,大于就缩小,等于就结算一次然后继续扩张
const findContinuousSequence = function(target) {
    let left = 1
    let right = 1
    const res = []
    const sumArr = []
    let sum = 0
    while (right < target) {
        if (sum === target) {
            res.push([...sumArr])
        }
        if (sum <= target) {
            sum += right
            sumArr.push(right)
            right++
        } else {
            sum -= left
            sumArr.shift()
            left++
        }
    }
    return res
};
  1. 用join也可
const reverseWords = function(s) {
    const strArr = s.trim().split(' ')
    let res = ''
    for (let i = strArr.length - 1; i >= 0; i--) {
        if (strArr[i] === '') continue
        res += i !== 0 ? strArr[i] + ' ' : strArr[i]
    }
    return res
};
  1. 双端队列,维持一个不递增的队列,前大后小。如果窗口左指针压中的值等于队列的头部,意味着左指针就是当前最大值,所以窗口右移时要移除队列的头部。如果窗口右移时左指针不是队列头部,意味着窗口内最大值与此时的左指针无关。
const maxSlidingWindow = function(arr, size) {
    const queue = []
    let leftIdx = 0
    const res = []
    for (let i = 0; i < arr.length; i++) {
        while (queue.length && queue[queue.length -1] < arr[i]) {
            queue.pop()
        }
        queue.push(arr[i])
        // 若窗口尚未形成,跳过
        if (i < size - 1) continue
        // 窗口已经形成,放入结果数组
        res.push(queue[0])
        // 若窗口最左的值是最大的,那么移除这个值(因为滑动之后最左值不再有效)
        if (arr[leftIdx] === queue[0]) queue.shift()
        leftIdx++
    }
    return res
};
  1. 与上题思路差不多。一个队列正常存取值。另一个队列存值时维持单调不递增,pop时判断pop出来的值是否是队列头部,如果是的话那pop出来的是最大值,把单调队列的头部也移除。否则意味着pop出来的不是最大值,不用对单调队列进行操作。
class MaxQueue {
    private queue: number[]
    private maxQueue: number[]
    constructor() {
        this.queue = []
        this.maxQueue = []
    }

    max_value(): number {
        return this.maxQueue[0] ?? -1
    }

    push_back(value: number): void {
        this.queue.push(value)
        while (this.maxQueue.length && this.maxQueue[this.maxQueue.length - 1] < value) {
            this.maxQueue.pop()
        }
        this.maxQueue.push(value)
    }

    pop_front(): number {
        const res = this.queue.shift() ?? -1
        if (this.maxQueue[0] === res) {
            this.maxQueue.shift()
        }
        return res
    }
}
  1. dp表含义为投i个骰子的时候,投出j的概率是多少。
const dicesProbability = function(n) {
    const dp = []
    for (let i = 1; i <= n; i++) {
        dp[i] = []
    }
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= 6 * i; j++) {
            if (i === 1) {
                dp[i][j] = j <= 6 ? 1 / 6 : 0
                continue
            }
            // 斜率优化
            dp[i][j] = (dp[i][j - 1] ?? 0) + ((dp[i - 1][j - 1] ?? 0) - (dp[i - 1][j - 7] ?? 0)) * (1 / 6)
        }
    }
    return dp[n].slice(n)
};