算法入门学习笔记(一)

前言

本笔记用于记录刷题过程中遇到的中等及以上难度的题和一些特殊算法思想,语言大部分会用 JavaScript 来刷题,也有的会用C++Java

目前先看了一下算法基础,可以参考以下文章的介绍,基本上js扫盲没问题,然后开始力扣刷题
https://juejin.cn/post/7087134135193436197

力扣目前是先把LeetCode75刷完,同时也做每日一题,进行算法扫盲

唉,没想到上一次刷力扣都是一年前了,这一次争取刷题量大一点,坚持的久一点,会持续更新的

算法笔记(一)已经记录了二十道题,后续每二十道题会新开笔记,刷完后会对题型类别进行再整理

算法,启动!

题库LeetCode75

本题库涉及到的知识点一览

  • 数组 / 字符串 334 151 1071 605 2789
  • 双指针
  • 滑动窗口 1493 1004 1793
  • 前缀和
  • 哈希表 / 哈希集合
  • 栈 735
  • 队列
  • 链表
  • 二叉树 - 深度优先搜索
  • 二叉树 - 广度优先搜索
  • 二叉搜索树
  • 图 - 深度优先搜索 1261 841
  • 图 - 广度优先搜索 2684 310
  • 堆 / 优先队列 215
  • 二分查找 875
  • 回溯
  • 动态规划 - 一维 1137
  • 动态规划 - 多维 72 2312 62
  • 位运算
  • 前缀树
  • 区间集合
  • 单调栈 739 901

334.递增的三元子序列

题目:

给你一个整数数组 nums ,判断这个数组中是否存在长度为 3 的递增子序列。
如果存在这样的三元组下标 (i, j, k) 且满足 i < j < k ,使得 nums[i] < nums[j] < nums[k] ,返回 true ;否则,返回 false 。


解析:

  1. 双向遍历
    其实本质上就是找到数组中的一个元素,左边有值比它小,右边有值比它大。那么新创建两个长度为n的MIN和MAX数组,即MIN[i]表示nums[0]到nums[i]中的最小值,MAX[i]表示nums[0]到nums[i]中的最大值,然后遍历nums数组找到MIN[i-1]<nums[i]<MAX[i+1]的返回true

    整个逻辑需要遍历数组三遍,额外创建两个数组

  2. 贪心算法
    简单说就是寻找局部最优解,再把每次迭代后的最优解叠加

    本题目怎么联想到贪心呢,因为看完题目后三元组中的第一个肯定是越小越容易满足递增的情况,所以就是要找到最小的做first,第二小的做second,这样遍历只用找到比second大就返回true

    那么迭代思路就是先令nums[0]为first,second为正无穷,开始遍历。遇到nums[i]>second的返回true;遇到first<nums[i]<second,second换成更小的nums[i];遇到first>=nums[i],first换成更小的nums[i]。

    注意第三种情况second是没变的,那么新的first位置是在second后面的,只要后续遍历找到一个nums[i]大于second,曾经的first肯定排在second前面,那么就找到了;如果nums[i]小于second大于新的first,second就直接更新了,那么又回到了first位置在second前面的情况。

/**
 * @param {number[]} nums
 * @return {boolean}
 */
var increasingTriplet = function (nums) {
    const len = nums.length;
    if (len < 3) {
        return false
    }
    let first = nums[0]
    let second = Number.MAX_VALUE;
    for (let i = 1; i < len; i++) {
        const num = nums[i]
        if (num > second) {
            return true
        } else if (num > first) {
            second = num
        } else {
            first = num
        }
    }
    return false
};

215.数组中的第K个最大元素

题目:

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。


解析:

  1. 快排
    之前看快排的时候只记得二分递归,其实忽略了每一次查找中的中间值的最终位置会被确定下来,可以利用这一点在查找的过程中就 确定某些顺序位置上的值 。简单来说就是某次二分时中间值被放到了了nums[k]的位置(降序),那么这个中间值就是第k大的元素

    用Java和js写了一下快排,注意这里我用的是倒序,所以k传参的时候记得要-1

class Solution {
    int quickselect(int[] nums, int l, int r, int k) {
        if (l == r) return nums[k];
        int x = nums[l], i = l - 1, j = r + 1;
        while (i < j) {
            do i++; while (nums[i] > x);
            do j--; while (nums[j] < x);
            if (i < j){
                int tmp = nums[i];
                nums[i] = nums[j];
                nums[j] = tmp;
            }
        }
        if (k <= j) return quickselect(nums, l, j, k);
        else return quickselect(nums, j + 1, r, k);
    }
    public int findKthLargest(int[] _nums, int k) {
        int n = _nums.length;
        return quickselect(_nums, 0, n - 1, k-1);
    }
}

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function (nums, k) {
    let quickselect = (nums, l, r, k) => {
        if (l == r) return nums[k];
        let x = nums[l], i = l - 1, j = r + 1;
        while (i < j) {
            do i++; while (nums[i] > x);
            do j--; while (nums[j] < x);
            if (i < j) {
                let tmp = nums[i];
                nums[i] = nums[j];
                nums[j] = tmp;
            }
        }
        if (k <= j) return quickselect(nums, l, j, k);
        else return quickselect(nums, j + 1, r, k);
    }

    return quickselect(nums, 0, nums.length - 1, k - 1);

};

  1. 堆排序
    最大值或者最小值堆和优先队列很适合查找这些有优先级顺序的问题,本题可以建立一个最大堆,做 k−1次删除操作后堆顶元素就是答案。

    不过理论上时间复杂度是o(nlog(n))=o(n)+o(klog(n)),即建堆和删除操作,这个方法纯粹是复习下堆相关的算法,Java方法基本就是JavaScript改一下声明,就不贴代码了

    (突然发现Java和JavaScript语法还蛮像的,只不过JavaScript没有class和函数类型的概念,其他数据结构以及思路是一样的)

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function (nums, k) {
    let heapSize = nums.length
    function buildMaxHeap(nums, heapSize) {
        //这里的i是最后一个非叶子节点
        for (let i = Math.floor(heapSize / 2) - 1; i >= 0; i--) {
            maxHeapify(nums, i, heapSize)
        }
    }
    // 从左向右,自上而下的冒泡
    function maxHeapify(nums, i, heapSize) {
        //左子节点
        let l = i * 2 + 1
        //右子节点
        let r = i * 2 + 2
        let largest = i
        if (l < heapSize && nums[l] > nums[largest]) {
            largest = l
        }
        if (r < heapSize && nums[r] > nums[largest]) {
            largest = r
        }
        if (largest !== i) {
            swap(nums, i, largest) // 进行节点调整
            // 继续调整下面的非叶子节点,直到叶子节点
            maxHeapify(nums, largest, heapSize)
        }
    }
    function swap(a, i, j) {
        let temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
    buildMaxHeap(nums, heapSize) // 构建好了一个最大堆
    // 最大堆是最大元素和最后一个元素交换,再删去最后一个元素
    for (let i = nums.length - 1; i >= nums.length - k + 1; i--) {
        swap(nums, 0, i)
        --heapSize
        // 重新调整最大堆
        maxHeapify(nums, 0, heapSize);
    }
    return nums[0]
};

72.编辑距离

题目:

给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数。
你可以对单词字符进行单个插入/删除/替换操作


解析:

标着中等,一看官方题解居然是hard难度,现在都卷成这样了吗

这道题的关键在于抽象出状态变化转移,这样才能套dp

这里dp[i][j]表示 word1 中前 i 个字符,变换到 word2 中前 j 个字符,最短需要操作的次数。同时考虑空的情况,那就是全部字符增加或者删除了

状态变化如下所示:

  • 增,dp[i][j] = dp[i][j - 1] + 1
  • 删,dp[i][j] = dp[i - 1][j] + 1
  • 改,dp[i][j] = dp[i - 1][j - 1] + 1

其中改这一步如果word1[i-1]刚好等于word2[j-1],那么就不用改,所以

  • 改,dp[i][j] = (word1[i - 1] != word2[j - 1]) ? dp[i - 1][j - 1] + 1 : dp[i - 1][j - 1]

然后遍历或者递归得到完整的dp表,返回dp[word1.length][word2.length]

/**
 * @param {string} word1
 * @param {string} word2
 * @return {number}
 */
var minDistance = function (word1, word2) {
    let n = word1.length;
    let m = word2.length;
    let dp = Array.from(Array(n + 1), () => Array(m + 1));
    // 有一个字符串为空串
    if (n * m == 0) {
        return n + m;
    }
    //边界初始化
    for (let i = 0; i < n + 1; i++) {
        dp[i][0] = i;
    }
    for (let j = 0; j < m + 1; j++) {
        dp[0][j] = j;
    }
    for (let i = 1; i < n + 1; i++) {
        for (let j = 1; j < m + 1; j++) {
            dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1], (word1[i - 1] != word2[j - 1]) ? dp[i - 1][j - 1] : (dp[i - 1][j - 1] - 1)) + 1;
        }
    }
    return dp[n][m]
};

1137.第N个泰波那契数

题目:

泰波那契序列 Tn 定义如下:
T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2
给你整数 n,请返回第 n 个泰波那契数 Tn 的值。


解析:

题目不难,可以递归,但是一旦数据过多会超时;也可以用动态规划,把状态方程换成递推关系式

官方题解用到的矩阵快速幂方法很有意思,记录一下,这样做时间复杂度O(logn)
本题是同样的思路,这里直接贴官方题解了
picture
快速幂指路,我觉得下方文章讲得很好
https://www.cnblogs.com/bigsai/p/15169985.html

var tribonacci = function(n) {
    if (n === 0) {
        return 0;
    }
    if (n <= 2) {
        return 1;
    }
    const q = [[1, 1, 1], [1, 0, 0], [0, 1, 0]];
    const res = pow(q, n);
    return res[0][2];
};

const pow = (a, n) => {
    //单位矩阵
    let ret = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
    while (n > 0) {
        //判断奇偶用
        if ((n & 1) === 1) {
            ret = multiply(ret, a);
        }
        //二进制移位本质上就是除以2或乘以2
        n >>= 1;
        a = multiply(a, a);
    }
    return ret;
}

const multiply = (a, b) => {
    const c = new Array(3).fill(0).map(() => new Array(3).fill(0));
    for (let i = 0; i < 3; i++) {
        for (let j = 0; j < 3; j++) {
            c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
        }
    }
    return c;
}

1071.字符串的最大公因子

题目:

对于字符串 s 和 t,只有在 s = t + t + t + … + t + t(t 自身连接 1 次或多次)时,我们才认定 “t 能除尽 s”。
给定两个字符串 str1 和 str2 。返回 最长字符串 x,要求满足 x 能除尽 str1 且 x 能除尽 str2


解析:

这题虽然是简单题,但是背后的思想还蛮重要的

辗转相除法 GCD(被除数,除数)= GCD(除数,余数)

理解辗转相除的关键在于被除数和除数有相同的因数,那么被除数和除数的余数跟它们也有一样的因数,反之亦然

辗转相除指路,我觉得这篇文章讲的很清楚
https://zhuanlan.zhihu.com/p/324578532

把文章中证明过程贴一下

本题目的逻辑在于如果它们有公因子abc,那么str1就是m个 abc 的重复,str2是n个abc的重复,连起来就是m+n个abc,那么理论上先后顺序不重要,m+n个abc跟n+m个abc是一样的。即如果 str1 + str2 === str2 + str1 就意味着有解,str1 + str2 !== str2 + str1 也是无解的充要条件。当确定有解的情况下,最优解是长度为 gcd(str1.length, str2.length) 的字符串。

/**
 * @param {string} str1
 * @param {string} str2
 * @return {string}
 */
var gcdOfStrings = function(str1, str2) {
  if (str1 + str2 !== str2 + str1) return ''
  //余数为0的时候除数就是最大约数
  const gcd = (a, b) => (0 === b ? a : gcd(b, a % b))
  return str1.substring(0, gcd(str1.length, str2.length))
};

739.每日温度

题目:

给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。


解析:

单调栈 即栈中放的数据单调有序

所有找下一个最大/小值距离当前值的距离都可以用单调栈。 本方法用空间换时间,空间复杂度o(n)额外维护一个数组,时间复杂度o(n)只用正向遍历一遍

单调递减栈步骤如下:维护一个数组下标的单调栈,正向遍历数组,当栈为空的时候直接当前下标入栈;不为空的时候比较栈顶元素对应的数组数值temperatures[stack[stack.length-1]]和当前遍历数组数值temperatures[i],temperatures[i]>temperatures[stack[stack.length-1]]的话,栈顶元素pop出来,该元素对应的距离为i-stack[stack.length-1],然后循环直至栈为空或者不满足大于的条件,最后i进栈

pop的时候更新距离是因为遍历数组是按正序遍历的,那么满足大小条件的一定是右边第一个

/**
 * @param {number[]} temperatures
 * @return {number[]}
 */
var dailyTemperatures = function(temperatures) {
    const len=temperatures.length
    let ans=new Array(len).fill(0)
    let stack=[]
    for(let i=0;i<len;i++){
        while(stack.length&&temperatures[i]>temperatures[stack[stack.length-1]]){
            const index=stack.pop()
            ans[index]=i-index
        }
        stack.push(i)
    }
    return ans
};

735.小行星碰撞

题目:

给定一个整数数组 asteroids,表示在同一行的小行星。
对于数组中的每一个元素,其绝对值表示小行星的大小,正负表示小行星的移动方向(正表示向右移动,负表示向左移动)。每一颗小行星以相同的速度移动。
找出碰撞后剩下的所有小行星。碰撞规则:两个小行星相互碰撞,较小的小行星会爆炸。如果两颗小行星大小相同,则两颗小行星都会爆炸。两颗移动方向相同的小行星,永远不会发生碰撞。


解析:
题目规则简化是数组相邻元素左正右负就只留下绝对值更大的,不断碰撞直到只剩下左正右负的相邻元素

代码逻辑是用栈来模拟行星碰撞,逐一遍历数组,将元素依次压入栈中,当出现左正右负时,先比较其和栈顶元素绝对值大小,同时修改alive状态,直到左正右负消失,alive的值决定是否push元素

/**
 * @param {number[]} asteroids
 * @return {number[]}
 */
var asteroidCollision = function (asteroids) {
    const stack = []
    for (const aster of asteroids) {
        //代表每次遍历要压入栈的那个行星是否爆炸
        let alive = true;
        while (alive && stack.length > 0 && stack[stack.length - 1] > 0 && aster < 0) {
            //入栈元素大于栈顶元素绝对值才设置alive为true
            alive = stack[stack.length - 1] < -aster;
            if (stack[stack.length - 1] <= -aster) {
                stack.pop();
            }
        }
        if (alive) {
            stack.push(aster);
        }
    }
    const size = stack.length;
    const ans = new Array(size).fill(0);
    for (let i = size - 1; i >= 0; i--) {
        ans[i] = stack.pop();
    }
    return ans;

};

605.种花问题

题目:

假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false 。


解析:

虽然是简单题,但是我官方贪心的解答没看懂(尴尬)。我自己做就是两个思路,跳格子和连续三个0,不解释了。

class Solution {
    public boolean canPlaceFlowers(int[] flowerbed, int n) {
        int len = flowerbed.length;
        int i = 0;
        while (i < len && n > 0) {
            //当前i为1,i+1必然为0,那么只有i+2才可能为0
            if (flowerbed[i] == 1) {
                i += 2;
            } else if (i == flowerbed.length - 1||flowerbed[i + 1] == 0 ) {
                //当前i为0且i+1为0,那么当前i可以种花
                //注意末尾为0也是可以种花的
                //这里有一个编译上的小错误注
                //开始我把i+1==0放在第一个判断,会出现out of bounds的错误,要把判断i==末尾长度先判断
                i += 2;
                n--;
            } else {
                i += 3;
            }
        }
        return n <= 0;
    }
}
var canPlaceFlowers = function (flowerbed, n) {
    const len = flowerbed.length;
    for (let i = 0; i < len; i++) {
        if ((i === 0 || flowerbed[i - 1] === 0) && flowerbed[i] === 0 && (i === len - 1 || flowerbed[i + 1] === 0)) {
            n--;
            i++; 
        }
    }
    return n <= 0;
};

1261.在受污染的二叉树中查找元素

题目:

给出一个满足下述规则的二叉树:

  1. root.val == 0
  2. 如果 treeNode.val == x 且 treeNode.left != null,那么 treeNode.left.val == 2 * x + 1
  3. 如果 treeNode.val == x 且 treeNode.right != null,那么 treeNode.right.val == 2 * x + 2
    现在这个二叉树受到「污染」,所有的 treeNode.val 都变成了 -1。
    请你先还原二叉树,然后实现 FindElements 类:
  • FindElements(TreeNode* root) 用受污染的二叉树初始化对象,你需要先把它还原。
  • bool find(int target) 判断目标值 target 是否存在于还原后的二叉树中并返回结果。

解析:

深度优先,从根节点开始左右赋值,递归到节点为空止,然后哈希表查找即可

var FindElements = function(root) {
    this.valSet = new Set();
    const dfs = (node, val) => {
        if (node == null) {
            return;
        }
        node.val = val;
        this.valSet.add(val);
        dfs(node.left, val * 2 + 1)
        dfs(node.right, val * 2 + 2)
    }
    dfs(root, 0);
};

FindElements.prototype.find = function(target) {
    return this.valSet.has(target);
};

151.反转字符串中的单词

题目:

给你一个字符串 s ,请你反转字符串中单词的顺序。
单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格


解析:

没啥特别的需要解释,主要是熟练使用js里面string的内置函数

trim():去除字符串的头尾空格
split(/\s/)和split(/\s+/):以单空格和连续多个空格划分成数组
reverse():数组元素反转
join(’ '):数组拼接字符串

var reverseWords = function(s) {
    return s.trim().split(/\s+/).reverse().join(' ');
};

这里记录一下我自己对于一些js内置函数的实现,可能有漏洞

function  trim(str){
    for(var  i  =  0  ;  i<str.length  &&  str[i]=="  "  ;  i++  )  ;
    for(var  j  =str.length;  j>0  &&  str.[j-1]=="  "  ;  j--)  ;
    if(i>j)  return  "";  
    return  str.substring(i,j);  
}

function substring(string,start,end){
    let newString
    for(let i=start;i<=end;i++){
        newstring+=string[i]
    }
    return newString
}

function slice(arr,start,end){
    let newArr=new Array(end-start+1)
    for(let i=start,n=0;i<=end;i++,n++){
        newstring[n]=arr[i]
    }
    return newArr
}

1004.最大连续1的个数

题目:

给定一个二进制数组 nums 和一个整数 k,如果可以翻转最多 k 个 0 ,则返回 数组中连续 1 的最大个数 。


解析:

  1. 二分查找

    题目本质上是求找出一个最长的子数组,该子数组内最多允许有k个0

    对于数组A的区间[left,right] 而言,只要它包含不超过k个0,就可以得到满足要求并且长度为right−left+1 的区间

    遍历所有的节点作为right,每个都找到合适的left(二分法查找),然后比较返回区间最大的

    /**
     * @param {number[]} nums
     * @param {number} k
     * @return {number}
     */
    var longestOnes = function (nums, k) {
        const n = nums.length
        const pre = new Array(n + 1).fill(0)
        for (let i = 1; i <= n; i++) {
            pre[i] = pre[i - 1] + (1 - nums[i - 1])
        }
        let ans = 0;
        for (let right = 0; right < n; ++right) {
            const left = binarySearch(pre, pre[right + 1] - k)
            ans = Math.max(ans, right - left + 1)
        }
        return ans
    };
    
    const binarySearch = (pre, target) => {
        let i = 0, j = pre.length - 1
        while (i < j) {
            const mid = Math.floor((j - i) / 2) + i
            if (pre[mid] < target) {
                i = mid + 1
            } else {
                j = mid
            }
        }
        return j
    }
    
  2. 滑动窗口

    滑动窗口的套路就是左右边界指针,右指针右移,判断区间条件是否满足,不满足条件后开始移动左指针,然后返回最大区间

    class Solution {
        public int longestOnes(int[] nums, int k) {
            int len = nums.length, right = 0, left = 0, zeros = 0, res = 0;
            while (right < len) {
                if (nums[right] == 0) {
                    zeros++;
                }
                while (zeros > k) {
                    if (nums[left] == 0) {
                        zeros--;
                    }
                    left++;
                }
                res = Math.max(res, right - left + 1);
                right++;
            }
            return res;
        }
    }
    

1493.删掉一个元素以后全为1的最长子数组

题目:

给你一个二进制数组 nums ,你需要从中删掉一个元素。
请你在删掉元素的结果数组中,返回最长的且只包含 1 的非空子数组的长度。
如果不存在这样的子数组,请返回 0 。


解析:
滑动窗口类的题目

  1. 递推
    官方的思路跟滑动窗口没啥关系,主要是递推的思路

    假设当前的下标是i,知道 「以第i-1位结尾的最长连续全1子数组」 和 **「以第i+1位开头的最长连续全1子数组」**的长度分别是多少,这两个量的和就是删除第i位之后最长的且只包含1的非空子数组的长度

    所以需要额外维护两个数组,遍历三遍

    class Solution {
        public int longestSubarray(int[] nums) {
            int n = nums.length;
            //pre(i) 为「以第i位结尾的最长连续全1子数组」
            int[] pre = new int[n];
            //suf(i)为「以第i位开头的最长连续全1子数组」
            int[] suf = new int[n];
    
            pre[0] = nums[0];
            for (int i = 1; i < n; ++i) {
                //当前值为0的话重新计数,为1的话+1
                pre[i] = nums[i] != 0 ? pre[i - 1] + 1 : 0; 
            }
    
            suf[n - 1] = nums[n - 1];
            for (int i = n - 2; i >= 0; --i) {
                suf[i] = nums[i] != 0 ? suf[i + 1] + 1 : 0;
            }
    
            int ans = 0;
            for (int i = 0; i < n; ++i) {
                int preSum = i == 0 ? 0 : pre[i - 1];
                int sufSum = i == n - 1 ? 0 : suf[i + 1];
                ans = Math.max(ans, preSum + sufSum);
            }
    
            return ans;
        }
    }
    
    

    官方思路还可以优化,只用遍历一次,额外维护两个数组p0(i) 为「以第i位结尾的最长连续全1子数组」和p1(i) 为「以第i位结尾,并可以在某处删除一个0的最长连续全1子数组」。这两个数组的递推式在遇到1时是一样的,区别在遇到0时,p0重新计数,但是p1可以通过删除当前0达到连续全1

    class Solution {
        public int longestSubarray(int[] nums) {
            int ans = 0;
            int p0 = 0, p1 = 0;
            for (int num : nums) {
                if (num == 0) {
                    p1 = p0;
                    p0 = 0;
                } else {
                    ++p0;
                    ++p1;
                }
                ans = Math.max(ans, p1);
            }
            if (ans == nums.length) {
                --ans;
            }
            return ans;
        }
    }
    
    
  2. 滑动窗口

    类似上一题的思路,这道题就是滑动区间最多只能有一个0

    class Solution {
        public int longestSubarray(int[] nums) {
            int len = nums.length, right = 0, left = 0, zeros = 0, res = 0;
            while (right < len) {
                if (nums[right] == 0) {
                    zeros++;
                }
                while (zeros > 1) {
                    if (nums[left] == 0) {
                        zeros--;
                    }
                    left++;
                }
                res = Math.max(res, right - left );
                right++;
            }
            return res;
        }
    }
    
    

2789.合并后数组的最大元素

题目:

给你一个下标从 0 开始、由正整数组成的数组 nums 。
你可以在数组上执行下述操作 任意 次:
选中一个同时满足 0 <= i < nums.length - 1 和 nums[i] <= nums[i + 1] 的整数 i 。将元素 nums[i + 1] 替换为 nums[i] + nums[i + 1] ,并从数组中删除元素 nums[i] 。
返回你可以从最终数组中获得的 最大 元素的值。


解析:

贪心+倒序遍历数组
题目的意思抽象出来就是用两个有序的数字的和替换原来的两个数字。那么为了和最大,应该尽可能多合并,最大的数合并。

按照题目要求,应该是一个递增的数组,那么后面的数更大一点,所以从后往前遍历,尽可能多的合并

//官方解法
var maxArrayValue = function(nums) {
    let sum=nums[nums.length-1];
    for(let i=nums.length-2;i>=0;i--){
        sum=nums[i]<=sum?nums[i]+sum:nums[i];
    }
    return sum;
};
//其实可以一行代码搞定,因为最近在看reduce的函数实现,reduce做这种回调求和可以秒
var maxArrayValue = function (nums) {
    return nums.reverse().reduce(
        (accumulator, currentValue) => currentValue <= accumulator ? accumulator + currentValue : currentValue,
    )
};

875.爱吃香蕉的珂珂

题目:

珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。
珂珂可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 h 小时内吃掉所有香蕉的最小速度 k(k 为整数)。


解析:

经典的 二分法,当一个题目有 范围单调性 的时候考虑二分法

本题的速度是有范围的整数,单调性体现在吃香蕉的速度越快,耗时越少;速度越慢,耗时越多

当二分查找猜测的速度恰好使得珂珂在规定的时间内吃完香蕉的时候,还应该去尝试更小的速度是不是还可以保证在规定的时间内吃完香蕉。

最慢速度为每小时吃一根香蕉,最快为每小时吃max(piles)根香蕉,然后尝试中点速度mid。如果以这个速度能够在h小时内吃完香蕉,那么这个速度应该是速度上界;如果吃不完那么这个速度应该是速度下界。

具体一点是,如果在速度speed下可以在h小时内吃掉所有香蕉,则最小速度一定小于或等于speed,因此将上界调整为speed;否则,最小速度一定大于 speed,因此将下界调整为speed+1

空间复杂度是o(1),时间复杂度是o(nlogm),m是piles数组的最大值,二分查找要进行logm轮,每一轮要把数组遍历一遍

class Solution {
    public int minEatingSpeed(int[] piles, int h) {
        int low = 1, high = 0;
        for (int pile : piles) {
            high = Math.max(high, pile);
        }
        while (low < high) {
            //这里其实(high+low)/2也可以,但是防止high和low都很大,相加会溢出
            int mid = (high - low) / 2 + low;
            int time = getTime(piles, mid);
            if (time <= h) {
                //当所花时间更少的时候,还要继续二分,因为要找到最慢的速度
                high = mid;
            } else {
                low = mid + 1;
            }
        }
        return high;
    }

    public int getTime(int[] piles, int speed) {
        int time = 0;
        for (int pile : piles) {
            //最精妙的地方,不用ceil()就实现了向上取整,执行效率大幅提高
            //这里如果有余数最少是1,加上speed-1会大于等于speed,相当于向上取整
            //不是speed是怕pile和speed可以整除
            int curTime = (pile + speed - 1) / speed;
            time += curTime;
        }
        return time;
    }
}

841.钥匙和房间

题目:

有 n 个房间,房间按从 0 到 n - 1 编号。最初,除 0 号房间外的其余所有房间都被锁住。你的目标是进入所有的房间。然而,你不能在没有获得钥匙的时候进入锁住的房间。
当你进入一个房间,你可能会在里面找到一套不同的钥匙,每把钥匙上都有对应的房间号,即表示钥匙可以打开的房间。你可以拿上所有钥匙去解锁其他房间。
给你一个数组 rooms 其中 rooms[i] 是你进入 i 号房间可以获得的钥匙集合。如果能进入 所有 房间返回 true,否则返回 false。


解析:

x房间有y的钥匙,那么可以记作x->y,把房间都看做节点,本质上就是一个有向图,询问从0号节点出发是否能够到达所有的节点,直接上深度优先搜索和广度优先搜索

//深度优先搜索dfs
class Solution {
    boolean[] vis;
    int num;
    public boolean canVisitAllRooms(List<List<Integer>> rooms) {
        int n=rooms.size();
        num=0;
        vis = new boolean[n];
        dfs(rooms,0);
        return num==n;
    }
    public void dfs(List<List<Integer>> rooms,int x){
        vis[x]=true;
        num++;
        for(int item:rooms.get(x)){
            if(!vis[item]){
                dfs(rooms,item);
            }
        }
    }
}
/**
 * @param {number[][]} rooms
 * @return {boolean}
 */
//深度优先搜索dfs
var canVisitAllRooms = function(rooms) {
    const vis=new Set();
    const dfs=(room)=>{
        vis.add(room);
        for( const item of rooms[room]){
            if(!vis.has(item)){
                dfs(item);
            }
        }
    }
    dfs(0);
    return vis.size==rooms.length
};
//广度优先搜索bfs
class Solution {
    public boolean canVisitAllRooms(List<List<Integer>> rooms) {
        int n = rooms.size(), num = 0;
        boolean[] vis = new boolean[n];
        Queue<Integer> que = new LinkedList<Integer>();
        vis[0] = true;
        que.offer(0);
        while (!que.isEmpty()) {
            int x = que.poll();
            num++;
            for (int item : rooms.get(x)) {
                if (!vis[item]) {
                    vis[item] = true;
                    que.offer(item);
                }
            }
        }
        return num == n;
    }
}
/**
 * @param {number[][]} rooms
 * @return {boolean}
 */
//广度优先搜索bfs
var canVisitAllRooms = function(rooms) {
    const vis=new Set();
    const que=[];
    que.push(0);
    //不要忘了0是默认能进去的
    vis.add(0);
    while(que.length){
        const room=que.shift()
        for(const item of rooms[room]){
            if(!vis.has(item)){
                vis.add(item);
                que.push(item);
            }
        }
    }
    return vis.size==rooms.length
};

62.不同路径

题目:

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?


解析:

  1. 数学解法
    就是解 C m + n − 2 m − 1 C^{m−1}_{m+n−2} Cm+n2m1,写个公式可以秒

    class Solution {
    public int uniquePaths(int m, int n) {
        long ans = 1;
        for (int x = n, y = 1; y < m; ++x, ++y) {
            ans = x * ans / y;
        }
        return (int) ans;
        }
    }
    
  2. 动态规划

    本题的状态方程为f(i,j)=f(i−1,j)+f(i,j−1),即(i,j)格子只能由格子(i−1,j)或者(i,j−1)走一步得到,而题目需要求的是从格子(0,0)走到格子(m-1,n-1)有多少种走法

    class Solution {
    public int uniquePaths(int m, int n) {
        //这个f表示走到某一格需要的步数
        int[][] f = new int[m][n];
        for (int i = 0; i < m; ++i) {
            f[i][0] = 1;
        }
        for (int j = 0; j < n; ++j) {
            f[0][j] = 1;
        }
        for (int i = 1; i < m; ++i) {
            for (int j = 1; j < n; ++j) {
                f[i][j] = f[i - 1][j] + f[i][j - 1];
            }
        }
        return f[m - 1][n - 1];
    }
    }
    

2312.卖木头

题目:

给你两个整数 m 和 n ,分别表示一块矩形木块的高和宽。同时给你一个二维整数数组 prices ,其中 prices[i] = [hi, wi, pricei] 表示你可以以 pricei 元的价格卖一块高为 hi 宽为 wi 的矩形木块。
每一次操作中,你必须按下述方式之一执行切割操作,以得到两块更小的矩形木块:
沿垂直方向按高度 完全 切割木块,或
沿水平方向按宽度 完全 切割木块
在将一块木块切成若干小木块后,你可以根据 prices 卖木块。你可以卖多块同样尺寸的木块。你不需要将所有小木块都卖出去。你 不能 旋转切好后木块的高和宽。
请你返回切割一块大小为 m x n 的木块后,能得到的 最多 钱数。
注意你可以切割木块任意次。


解析:

每日一题来了道困难的动态规划,就着这道题学一下动态规划相关的内容

首先把题目给的三元组(x,y,price)使用哈希映射为键为(h,w),值为price,状态转移方程为f(x,y)=price,表示当木块的高和宽分别是x和y时,可以得到的最多钱数

如果x>1,那么可以沿水平方向将木块切成两部分,它们的高分别是 i(1≤i<x)和x−i,宽均为y,状态转移方程:
f ( x , y ) = max ⁡ 1 ≤ i < x { f ( i , y ) + f ( x − i , y ) } f(x,y)=\max_{1 \leq i < x} \big\{ f(i, y) + f(x-i, y) \big\} f(x,y)=max1i<x{f(i,y)+f(xi,y)}

如果y>1,那么我们可以沿垂直方向将木块切成两部分,它们的宽分别是 j (1≤j<y)和y−j,高均为x,可以得到状态转移方程:
f ( x , y ) = max ⁡ 1 ≤ j < y { f ( x , j ) + f ( x , y − j ) } f(x,y)=\max_{1 \leq j < y} \big\{ f(x, j) + f(x, y-j) \big\} f(x,y)=max1j<y{f(x,j)+f(x,yj)}

当有多种情况满足时,选择它们中的较大值,得到最优解。

优化的话注意切割的对称性,例如对于高为3宽为3的木块,横着可以有两种切法,但是本质上是一样的,都是切割成(1,3)和(2,3)

/**
 * @param {number} m
 * @param {number} n
 * @param {number[][]} prices
 * @return {number}
 */
var sellingWood = function(m, n, prices) {
    //f[x][y]是表示在(x,y)大小矩形中切割方式的最优解
    const f = Array.from({length: m + 1}, () => Array(n + 1).fill(0));
    for (const [w, h, p] of prices) {
        //把宽和高拼接到一起
        f[w][h] = p;
    }
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            //j/2是表示对称性只用比较一遍
            for (let k = 1; k <= j/2; k++) f[i][j] = Math.max(f[i][j], f[i][k] + f[i][j - k]); // 垂直切割
            for (let k = 1; k <= i/2; k++) f[i][j] = Math.max(f[i][j], f[k][j] + f[i - k][j]); // 水平切割
        }
    }
    return f[m][n];
};

2684.矩阵中移动的最大次数

题目:

给你一个下标从 0 开始、大小为 m x n 的矩阵 grid ,矩阵由若干 正 整数组成。
你可以从矩阵第一列中的 任一 单元格出发,按以下方式遍历 grid :
从单元格 (row, col) 可以移动到 (row - 1, col + 1)、(row, col + 1) 和 (row + 1, col + 1) 三个单元格中任一满足值 严格 大于当前单元格的单元格。
返回你在矩阵中能够 移动 的 最大 次数。


解析:

  1. BFS
    首先把所有行坐标加入到集合中,作为出发点。然后对其依次遍历,对每一个单元格,找到下一个列的相邻单元格,并判断是否严格大于当前单元格。

    如果是,说明可以移动到达。把所有可到达的单元格行坐标加到集合中,并用于下一轮的搜索。

    当到达最后一列或者集合为空,搜索结束,返回矩阵中移动的最大次数。

    class Solution {
    public int maxMoves(int[][] grid) {
        int row = grid.length, col = grid[0].length;
        Set<Integer> q = new HashSet<>();
        for (int i = 0; i < row; i++) {
            q.add(i);
        }
        for (int j = 1; j < col; j++) {
            Set<Integer> q2 = new HashSet<>();
            for (int i : q) {
                //下一列的三行与本列进行比较
                for (int i2 = i - 1; i2 <= i + 1; i2++) {
                    if (0 <= i2 && i2 < row && grid[i][j - 1] < grid[i2][j]) {
                        q2.add(i2);
                    }
                }
            }
            q = q2;
            if (q.isEmpty()) {
                return j - 1;
            }
        }
        return col - 1;
    }
    }
  1. DFS
    从第一列任意一行开始递归,只要没出界就继续向前,最后返回访问到的最大列数,期间访问过的格子可以置为0证明已访问过
    class Solution {
    private int ans;

    public int maxMoves(int[][] grid) {
        for (int i = 0; i < grid.length; i++) {
            dfs(i, 0, grid); // 从第一列的任一单元格出发
        }
        return ans;
    }

    private void dfs(int i, int j, int[][] grid) {
        ans = Math.max(ans, j);
        if (ans == grid[0].length - 1) { // ans 已达到最大值
            return;
        }
        // 向右上/右/右下走一步
        for (int k = Math.max(i - 1, 0); k < Math.min(i + 2, grid.length); k++) {
            if (grid[k][j + 1] > grid[i][j]) {
                dfs(k, j + 1, grid);
            }
        }
        grid[i][j] = 0;
    }

    }

310.最小高度树

题目:

树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。
给你一棵包含 n 个节点的树,标记为 0 到 n - 1 。给定数字 n 和一个有 n - 1 条无向边的 edges 列表(每一个边都是一对标签),其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间存在一条无向边。
可选择树中任何一个节点作为根。当选择节点 x 作为根节点时,设结果树的高度为 h 。在所有可能的树中,具有最小高度的树(即,min(h))被称为 最小高度树 。
请你找到所有的 最小高度树 并按 任意顺序 返回它们的根节点标签列表。
树的 高度 是指根节点和叶子节点之间最长向下路径上边的数量。


解析:

官方题解写得真的看不太懂(尴尬),还是按照我自己的思路来吧

先对树的知识进行回顾,含有 n个节点的树;
任意两个节点之间有且仅有一条路径;
树中的共有 n−1条不同的边;
叶子节点的度为 1,非叶子节点的度至少为 2;
树的高度由根节点到叶子节点的最大距离决定。

设 dist[x][y]表示从节点x到节点y的距离,假设树中距离最长的两个节点为 (x,y),它们之间的距离为 maxdist=dist[x][y],则可以推出以任意节点构成的树最小高度一定为 minheight = ⌈ maxdist 2 ⌉ \textit{minheight} = \Big \lceil \dfrac{\textit{maxdist}}{2} \Big \rceil minheight=2maxdist,且最小高度的树根节点一定在节点x到节点y的路径上

假设最长的路径的m个节点依次为 p1→p2→⋯→pm,最长路径的长度为m−1.如果m为偶数,此时最小高度树的根节点为 p m 2 p_{\frac{m}{2}} p2m或者 p m 2 + 1 p_{\frac{m}{2} + 1} p2m+1,且此时最小的高度为 m 2 \dfrac{m}{2} 2m;如果m为奇数,此时最小高度树的根节点为 p m + 1 2 p_{\frac{m+1}{2}} p2m+1,且此时最小的高度为 m − 1 2 \dfrac{m-1}{2} 2m1

所以求最小高度树和根节点转化成求路径最长的两个叶子节点并求出其路径的最中间的节点。

首先找到距离节点0的最远节点x,然后找到距离节点x的最远节点y,然后找到节点x与节点y的路径,然后找到根节点

证明逻辑,反证法(官方题解给的公式过于抽象)

  • 假设a.b两点与树的直径相交,b点是离a点最远的点。那么c,d是树的直径。e是两条线的交点。因为b是离a最远的点所以eb一定比ec长。那么deb一定比树的直径cd长。这时就不符合定义了。

  • 假设a,b两点与cd不相交。那么在ab中有一条点e和cd中间有一条点f一定可以连在一起(这是一颗联通树),那么eb一定比efd长那么cfeb一定是一条更长的直径

1.BFS

    /**
     * @param {number} n
     * @param {number[][]} edges
     * @return {number[]}
     */
    var findMinHeightTrees = function(n, edges) {
        const ans = [];
        if (n === 1) {
            ans.push(0);
            return ans;
        }
        //先构建图关系,创建n*n邻接矩阵,相邻节点push进矩阵里
        const adj = new Array(n).fill(0).map(() => new Array());
        for (const edge of edges) {
            adj[edge[0]].push(edge[1]);
            adj[edge[1]].push(edge[0]);
        }
        const parent = new Array(n).fill(-1);
        // 找到与节点 0 最远的节点 x 
        const x = findLongestNode(0, parent, adj);
        // 找到与节点 x 最远的节点 y 
        let y = findLongestNode(x, parent, adj);
        //求出节点 x 到节点 y 的路径 
        const path = [];
        parent[x] = -1;
        while (y !== -1) {
            path.push(y);
            y = parent[y];
        }
        const m = path.length;
        if (m % 2 === 0) {
            ans.push(path[Math.floor(m / 2) - 1]);
        }
        ans.push(path[Math.floor(m / 2)]);
        return ans;
    }
    const findLongestNode = (u, parent, adj) => {
        const n = adj.length;
        const queue = [];
        const visit = new Array(n).fill(false);
        queue.push(u);
        visit[u] = true;
        let node = -1;
        //bfs遍历
        while (queue.length) {
            const curr = queue.shift();
            node = curr;
            //遍历当前节点所有的相邻节点,未访问过的加入队
            //parent[]中记录了每个节点在遍历中的父节点
            //返回的的node可以根据parent[]一步一步逆推路径
            for (const v of adj[curr]) {
                if (!visit[v]) {
                    visit[v] = true;
                    parent[v] = curr;
                    queue.push(v);
                }
            }
        }
        return node;
    };

2.DFS

 var findMinHeightTrees = function(n, edges) {
     const ans = [];
     if (n === 1) {
         ans.push(0);
         return ans;
     }
     const adj = new Array(n).fill(0).map(() => new Array());
     for (const edge of edges) {
         adj[edge[0]].push(edge[1]);
         adj[edge[1]].push(edge[0]);
     }

     const parent = new Array(n).fill(-1);
     /* 找到与节点 0 最远的节点 x */
     let x = findLongestNode(0, parent, adj);
     /* 找到与节点 x 最远的节点 y */
     let y = findLongestNode(x, parent, adj);
     /* 求出节点 x 到节点 y 的路径 */
     const path = [];
     parent[x] = -1;
     while (y !== -1) {
         path.push(y);
         y = parent[y];
     }
     const m = path.length;
     if (m % 2 === 0) {
         ans.push(path[Math.floor(m / 2) - 1]);
     }
     ans.push(path[Math.floor(m / 2)]);
     return ans;
 }

 const findLongestNode = (u, parent, adj) => {
     const n = adj.length;
     const dist = new Array(n).fill(-1);
     dist[u] = 0;
     //dfs递归找到每个节点可以到的最远的距离,记录在dist[]
     //parent[]记录每个节点在路径里的父节点
     const dfs = (u, dist, parent, adj) => {
         for (const v of adj[u]) {
             if (dist[v] < 0) {
                 dist[v] = dist[u] + 1;
                 parent[v] = u;
                 dfs(v, dist, parent, adj); 
             }
         }
     }

     dfs(u, dist, parent, adj);
     let maxdist = 0;
     let node = -1;
     //找到最大距离,根据最后的node反推路径
     for (let i = 0; i < n; i++) {
         if (dist[i] > maxdist) {
             maxdist = dist[i];
             node = i;
         }
     }
     return node;
 }

  1. 拓扑排序

    类似剥皮,根据前面方法可知最小树的根节点一定为该路径中的中间节点,不停地删除最外层的度为1的节点,直到剩下根节点为止

    var findMinHeightTrees = function(n, edges) {
    const ans = [];
    if (n === 1) {
        ans.push(0);
        return ans;
    }
    //计算度数
    const degree = new Array(n).fill(0);
    const adj = new Array(n).fill(0).map(() => new Array());
    for (const edge of edges) {
        adj[edge[0]].push(edge[1]);
        adj[edge[1]].push(edge[0]);
        degree[edge[0]]++;
        degree[edge[1]]++;
    }
    const queue = [];
    for (let i = 0; i < n; i++) {
        //度数为1的入队
        if (degree[i] === 1) {
            queue.push(i);
        }
    }
    let remainNodes = n;
    while (remainNodes > 2) {
        const sz = queue.length;
        //每次都把外层度数为1的节点减去
        remainNodes -= sz;
        for (let i = 0; i < sz; i++) {
            const curr = queue.shift();
            for (const v of adj[curr]) {
                degree[v]--;
                if (degree[v] === 1) {
                    queue.push(v);
                }
            }
        }
    }
    while (queue.length) {
        ans.push(queue.shift());
    }
    return ans;
    };

1793.好子数组的最大分数

题目:

给你一个整数数组 nums (下标从 0 开始)和一个整数 k 。
一个子数组 (i, j) 的 分数 定义为 min(nums[i], nums[i+1], …, nums[j]) * (j - i + 1) 。一个 好 子数组的两个端点下标需要满足 i <= k <= j 。
请你返回 好 子数组的最大可能 分数 。


解析:

本题思路主要是使用双指针限定一个子数组范围,left初始下标为k - 1, right初始下标为k + 1,然后依次枚举比较数组中的最小值,移动子数组范围。

这里先以nums[k]为最小值进行比较,只要nums[left]和nums[right]比这个大,就可以继续扩大子数组范围;当nums[left]和nums[right]比这个小,那么就使用nums[left]和nums[right]中更大的那一个作为新的最小值继续比较

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var maximumScore = function (nums, k) {
    const len = nums.length;
    let left = k - 1, right = k + 1;
    let ans = 0;
    for (let min = nums[k]; ;) {
        while (left >= 0 && nums[left] >= min) {
            left--;
        }
        while (right < len && nums[right] >= min) {
            right++;
        }
        //注意这里(right - left - 1),此时的right和left都是取开区间,nums[left]和nums[right]比min小
        ans = Math.max(ans, (right - left - 1) * min);
        if (left == -1 && right == len) {
            break;
        }
        min = Math.max((left == -1 ? -1 : nums[left]), (right == len ? -1 : nums[right]));
        if (min == -1) {
            break;
        }
    }
    return ans
};

算法笔记(二)~

  • 17
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值