LeetCode--数组

基础知识

数组是存放在连续内存空间上的相同类型数据的集合。数组可以方便的通过下标索引的方式获取到下标下对应的数据。需要两点注意的是:

  • 数组下标都是从0开始的。
  • 数组内存空间的地址是连续的。
    因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。数组的元素是不能删的,只能覆盖。

技巧

1.在处理数组和链表相关问题时,双指针技巧是经常用到的,双指针技巧主要分为两类:左右指针快慢指针。所谓左右指针,就是两个指针相向而行或者相背而行;而所谓快慢指针,就是两个指针同向而行,一快一慢。
2.快慢指针。数组问题中比较常见的快慢指针技巧,是让你原地修改数组。比如有序数组/链表中去重,对数组中的某些元素进行**「原地删除」**。

题目

一、快慢指针(修改数组、滑动窗口)

Ⅰ修改数组

1. 删除有序数组中的重复项
简单
在这里插入图片描述
思路:
由于数组已经排序,所以重复的元素一定连在一起。让慢指针 slow 走在后面,快指针 fast 走在前面探路,找到一个不重复的元素就赋值给 slow 并让 slow 前进一步。这样,就保证了 nums[0…slow] 都是无重复的元素,当 fast 指针遍历完整个数组 nums 后,nums[0…slow] 就是整个数组去重之后的结果。
删除排序链表中的重复元素也是同理。

/**
 * @param {number[]} nums
 * @return {number}
 */
var removeDuplicates = function(nums) {
    let slow = fast = 0;
    while(fast<nums.length){
        if(nums[fast]!=nums[slow]){
            slow++;
            nums[slow] = nums[fast];
        }
        fast++;
    }
    return slow+1;
};

2. 移除元素
简单
在这里插入图片描述
思路:
如果 fast 遇到值为 val 的元素,则直接跳过,否则就赋值给 slow 指针,并让 slow 前进一步。这里是先给 nums[slow] 赋值然后再给 slow++,这样可以保证 nums[0…slow-1] 是不包含值为 val 的元素的,最后的结果数组长度就是 slow。

/**
 * @param {number[]} nums
 * @param {number} val
 * @return {number}
 */
var removeElement = function(nums, val) {
    let k = 0;
    for(let i = 0; i < nums.length; i++){
        if(nums[i]!=val){
            nums[k++] = nums[i]
        }
    }
    return k
};
var removeElement = function(nums, val) {
    let slow = fast = 0;
    while(fast<nums.length){
        if(nums[fast]!=val){
            nums[slow] = nums[fast];
            slow++;
        }
        fast++;
    }
    return slow;
}

3. 移动零
简单
在这里插入图片描述
思路:
相当于移除 nums 中的所有 0,然后再把后面的元素都赋值为 0 即可。

/**
 * @param {number[]} nums
 * @return {void} Do not return anything, modify nums in-place instead.
 */
var moveZeroes = function(nums) {
    let slow = fast = 0;
    while(fast<nums.length){
        if(nums[fast]!=0){
            nums[slow] = nums[fast];
            slow++;
        }
        fast++;
    }
    for(;slow<nums.length;slow++){
        nums[slow] = 0;
    }
};
Ⅱ滑动窗口
A.模板
/* 滑动窗口算法框架 */
var slidingWindow = function(s) {
    let window = {};
    let left = 0, right = 0;
    while (right < s.length) {
        // c 是将移入窗口的字符
        let c = s[right];
        // 增大窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...

        /*** debug 输出的位置 ***/
        // 注意在最终的解法代码中不要 print
        // 因为 IO 操作很耗时,可能导致超时
        alert("window: [%d, %d)\n", left, right);
        /********************/
        
        // 判断左侧窗口是否要收缩
        while (window needs shrink) {
            // d 是将移出窗口的字符
            let d = s[left];
            // 缩小窗口
            left++;
            // 进行窗口内数据的一系列更新
            ...
        }
    }
}

1.left 指针在后,right 指针在前,两个指针中间的部分就是「窗口」,算法通过扩大和缩小「窗口」来解决某些问题。
2.两处 … 表示的更新窗口数据的地方, 这两个 … 处的操作分别是扩大和缩小窗口的更新操作,它们操作是完全对称的。(以下题解都将窗口定义为左开右闭区间,需要搞清楚到底是先更新结果还是先缩小窗口。)
3.算法的时间复杂度依然是 O(N),其中 N 是输入字符串/数组的长度。字符串/数组中的每个元素都只会进入窗口一次,然后被移出窗口一次,不会说有某些元素多次进入和离开窗口,所以算法的时间复杂度就和字符串/数组的长度成正比。
4.需要思考:
什么时候应该移动 right 扩大窗口?窗口加入字符时,应该更新哪些数据?
什么时候窗口应该暂停扩大,开始移动 left 缩小窗口?从窗口移出字符时,应该更新哪些数据?
我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?

B.题目

1. 最小覆盖子串
困难
在这里插入图片描述
思路:
滑动窗口算法的思路是这样:
1、在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。
PS:理论上设计两端都开或者两端都闭的区间,但设计为左闭右开区间是最方便处理的。因为这样初始化 left = right = 0 时区间 [0, 0) 中没有元素,但只要让 right 向右移动(扩大)一位,区间 [0, 1) 就包含一个元素 0 了。如果你设置为两端都开的区间,那么让 right 向右移动一位后开区间 (0, 1) 仍然没有元素;如果你设置为两端都闭的区间,那么初始区间 [0, 0] 就包含了一个元素。这两种情况都会给边界处理带来不必要的麻烦。
2、不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
3、此时,停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。
4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。
在这里,创建need 和 window 两个计数器,分别记录 T 中字符出现次数和「窗口」中的相应字符的出现次数。 创建valid 变量表示窗口中满足 need 条件的字符个数,如果 valid 和 need.size 的大小相同,则说明窗口已满足条件,已经完全覆盖了串 T。如果一个字符进入窗口,应该增加 window 计数器;如果一个字符将移出窗口的时候,应该减少 window 计数器;当 valid 满足 need 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。
当我们发现某个字符在 window 的数量满足了 need 的需要,就要更新 valid,表示有一个字符已经满足要求。而且,你能发现,两次对窗口内数据的更新操作是完全对称的。当 valid == need.size() 时,说明 T 中所有字符已经被覆盖,已经得到一个可行的覆盖子串,现在应该开始收缩窗口了,以便得到「最小覆盖子串」。移动 left 收缩窗口时,窗口内的字符都是可行解,所以应该在收缩窗口的阶段进行最小覆盖子串的更新,以便从可行解中找到长度最短的最终结果。

/**
 * @param {string} s
 * @param {string} t
 * @return {string}
 */
var minWindow = function(s, t) {
    //左右窗口 左闭右开区间
    let left = right = 0;
    //记录最小子串的开始索引和长度  Infinity(无穷大)在 JS 中是一个特殊的数字,它的特性是:它比任何有限的数字都大
    let start = 0, len = Infinity;
    //记录满足字符个数的数量
    let valid= 0;
    //使用两个字典分别记录窗口和t中的字符个数
    let window = {};
    let need = {};
    //记录目标字符串中各元素个数
    // for(let i = 0; i<t.length; i++){
    //     if(need[t[i]]!=undefined)need[t[i]]++;
    //     else need[t[i]] = 1;
    //     window[t[i]] = 0;
    // }
    for(const c of t){
        if(need[c] == undefined)need[c] = 0;
        need[c]++;
        window[c] = 0;
    }
    //Object.keys(obj) 返回值:一个表示给定对象的所有可枚举属性的字符串数组
    const needSize = Object.keys(need).length;
    //左闭右开区间
    while(right<s.length){
        // addChar 是将移入窗口的字符
        let addChar = s[right];
        // 增大窗口
        right++;
        // 更新窗口
        //如果是目标字符串中的元素 则更新窗口中记录的元素个数 并判断是否个数满足
        if(need[addChar]!=undefined){
            window[addChar]++;
            if(window[addChar]==need[addChar]){
                valid++;
            }
        }
        //判断左侧窗口是否要收缩
        //目标串中各元素的数量在窗口中都得到满足
        //救命 map的大小是size不是length!!!!
        while(valid==needSize){
            //更新最小覆盖子串
            //左闭右开区间长度为right-left
            if(right-left<len){
                start = left;
                len = right-left;
            }
            // delChar 是将移出窗口的字符
            let delChar = s[left];
            // 减小窗口
            left++;
            // 更新窗口
            //如果是目标字符串中的元素 则更新窗口中记录的元素个数 并判断是否有效条件数是否减少
            if(need[delChar]!=undefined){
                //注意这里也是== 与上面相同
                if(window[delChar]==need[delChar]){
                    valid--;
                }
                window[delChar]--;//救命 应该先比较在做减法!!!
                // 因为 right 是开区间,就比方说 right = 0 的初始情况,此时 s[right] 还没进入窗口对吧,所以你扩大窗口时要先 window[s[right]]++,把它移入窗口,然后才能更新 valid 变量。反过来,left 是闭区间,比方说 left = 0 的情况,你要缩小窗口的话,先得判断移出 s[left] 是否会引起 valid 的更新,所以要先判断 valid 的更新,再执行 window[s[left]]-- 将 s[left] 移出窗口。
                //时刻记住窗口区间是左闭右开区间,然后搞清楚到底是先更新结果还是先缩小窗口。
            }
        }
    }
    //string.substr(start,length) 在字符串中抽取从 开始 下标开始的指定数目的字符。
    return len === Infinity?'':s.substr(start, len);
};
/**
 * @param {string} s
 * @param {string} t
 * @return {string}
 */
var minWindow = function(s, t) {
    //左右窗口 左闭右开区间
    let left = right = 0;
    //记录最小子串的开始索引和长度 
    let start = 0, len = 100000;
    //记录满足字符个数的数量
    let valid= 0;
    //使用两个map分别记录窗口和t中的字符个数
    let window = new Map();
    let need = new Map();
    //记录目标字符串中各元素个数
    for(let i = 0; i<t.length; i++){
        need.set(t[i],(need.get(t[i])||0)+1);
    }
    //左闭右开区间
    while(right<s.length){
        // addChar 是将移入窗口的字符
        let addChar = s[right];
        // 增大窗口
        right++;
        // 更新窗口
        //如果是目标字符串中的元素 则更新窗口中记录的元素个数 并判断是否个数满足
        if(need.has(addChar)){
            window.set(addChar,(window.get(addChar)||0)+1);
            if(window.get(addChar)==need.get(addChar)){
                valid++;
            }
        }
        //判断左侧窗口是否要收缩
        //目标串中各元素的数量在窗口中都得到满足
        //注意:map大小为size属性
        while(valid==need.size){
            //更新最小覆盖子串
            //左闭右开区间长度为right-left
            if(right-left<len){
                start = left;
                len = right-left;
            }
            // delChar 是将移出窗口的字符
            let delChar = s[left];
            // 减小窗口
            left++;
            // 更新窗口
            //如果是目标字符串中的元素 则更新窗口中记录的元素个数 并判断是否有效条件数是否减少
            if(need.has(delChar)){
                //注意:先-1则为<
                window.set(delChar,window.get(delChar)-1);
                if(window.get(delChar)<need.get(delChar)){
                    valid--;
                }
            }
        }
    }
    let ret = [];
    if(len<100000){
        while(len--){
            ret.push(s[start]);
            start++;
        } 
        return ret.join('');
    }
    else return '';
};

2. 字符串的排列
中等
在这里插入图片描述
思路:
跟上一题比,相当于窗口大小是固定的,就是s1的长度。那么窗口长度大于固定长度时就要缩小窗口。当发现 valid == need.size() 时,就说明窗口中就是一个合法的排列,所以立即返回 true。

/**
 * @param {string} s1
 * @param {string} s2
 * @return {boolean}
 */
var checkInclusion = function(s1, s2) {
    let left = 0,
    right = 0;
    //let need =window= {}; 定义的是同一个{} 不能这样写!!!
    let need ={},window= {};
    let valid = 0;
    for(const x of s1){
        if(need[x]==undefined)need[x] = 0;
        need[x]++;
        window[x]=0;
    }
    const needSize = Object.keys(need).length;
    while(right<s2.length){
        let addChar = s2[right];
        right++;
        if(need[addChar]!=undefined){
            window[addChar]++;
            if(window[addChar]==need[addChar]){
                valid++;
            }
        }
        while(right-left==s1.length){
            if(valid == needSize){
                return true;
            }
            let delChar = s2[left];
            left++;
            if(need[delChar]!=undefined){
                if(window[delChar]==need[delChar]){
                    valid--;
                }
                window[delChar]--;
                // 因为 right 是开区间,就比方说 right = 0 的初始情况,此时 s[right] 还没进入窗口对吧,所以你扩大窗口时要先 window[s[right]]++,把它移入窗口,然后才能更新 valid 变量。反过来,left 是闭区间,比方说 left = 0 的情况,你要缩小窗口的话,先得判断移出 s[left] 是否会引起 valid 的更新,所以要先判断 valid 的更新,再执行 window[s[left]]-- 将 s[left] 移出窗口。
            }
        }
    }
    return false;
};

3. 查找字符串中的所有字母异位词
中等
在这里插入图片描述
思路:
同上,将所有索引输出即可。
4. 无重复字符的最长子串
中等
在这里插入图片描述
思路:
要在收缩窗口完成后更新 res,因为窗口收缩的 while 条件是存在重复元素,换句话说收缩完成后一定保证窗口中没有重复

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function(s) {
    let left = 0, right = 0;
    let window = {};
    let res = 0;
    while(right<s.length){
        let addChar = s[right];
        right++;
        if(window[addChar]==undefined)window[addChar] = 0;
        window[addChar]++;
        while(window[addChar]>1){
            let delChar = s[left];
            left++;
            window[delChar]--;
        }
        //左闭右开区间
        res = Math.max(right-left,res);
    }
    return res;
};

二、左右指针(二分查找、nSum、反转数组、回文串判断)

Ⅰ二分查找

1.前提是数组是有序的
2.二分思维的精髓就是:通过已知信息尽可能多地收缩(折半)搜索空间,从而增加穷举效率,快速找到目标。
3.在解决这个问题时采用的是左闭右闭区间,[left, right]。这个区间其实就是每次进行搜索的区间。初始化 right 的赋值是 nums.length - 1,即最后一个元素的索引,而不是 nums.length。while 循环终止当搜索区间为空的时候应该终止。while中循环条件为为left<=right, 当left==rigth+1时终止。
4.分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节。
5.计算 mid 时需要防止溢出。left + (right - left) / 2就和 (left + right) / 2 的结果相同,但是有效防止了 left 和 right 太大,直接相加导致整数溢出的情况。mid = left + ((right-left)>>1)和left + Math.floor((right-left)/2)也可。

A.模板(有序数组)

1.寻找一个数(基本的二分搜索)

var binarySearch = function(nums, target) {
    let left = 0; 
    let right = nums.length - 1; // 左闭右闭区间 right是可以取到的
	//终止条件是left == right+1
    while(left <= right) {
        let mid = left + ((right-left)>>1);//防止溢出
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)//左闭右闭区间 mid已经被搜索比较 跳过mid
            left = mid + 1; // 注意
        else if (nums[mid] > target)
            right = mid - 1; // 注意
    }
    return -1;
}

注意:
1.[left, right]这个区间其实就是每次进行搜索的区间。循环条件是left<=right
2.当我们发现索引 mid 不是要找的 target 时,去搜索区间 [left, mid-1] 或者区间 [mid+1, right]继续寻找。因为 mid 已经搜索过,应该从搜索区间中去除。
3.但是这个算法只能查找到这个数,无法寻找其边界。比如说给你有序数组 nums = [1,2,2,2,3],target 为 2,此算法返回的索引是 2,没错。但是如果我想得到 target 的左侧边界,即索引 1,或者我想得到 target 的右侧边界,即索引 3,这样的话此算法是无法处理的。

因为我们初始化 right = nums.length - 1
所以决定了我们的「搜索区间」是 [left, right]
所以决定了 while (left <= right)
同时也决定了 left = mid+1 和 right = mid-1
因为我们只需找到一个 target 的索引即可
所以当 nums[mid] == target 时可以立即返回

2.寻找左侧边界的二分搜索

var left_bound = function(nums, target) {
    let left = 0, right = nums.length - 1;
    // 搜索区间为 [left, right]
    while (left <= right) {
        let mid = left + ((right-left)>>1);
        if (nums[mid] < target) {
            // 搜索区间变为 [mid+1, right]
            left = mid + 1;
        } else if (nums[mid] > target) {
            // 搜索区间变为 [left, mid-1]
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 收缩右侧边界
            // 找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid-1] 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
            right = mid - 1;
        }
    }
    // 判断 target 是否存在于 nums 中
    // 此时 target 比所有数都大,返回 -1
    if (left == nums.length) return -1;
    // 判断一下 nums[left] 是不是 target
    return nums[left] == target ? left : -1;
}

注意:
1.left 的取值范围是 [0, nums.length-1]。假如输入的 target 非常大,那么就会一直触发 nums[mid] < target 的 if 条件,left 会一直向右侧移动,直到等于nums.length, 索引越界。需要进行判断。
2.找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid-1] 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
3.假设target存在,while终止前 left==right, 计算得出left, right, mid三者相等。right由上一次循环收缩右侧边界而来,则target == right+1。则这次循环left=mid+1,即right+1,那么left>right跳出循环。此时left等于target。

3.寻找右侧边界的二分查找

var right_bound = function(nums, target) {
    let left = 0, right = nums.length - 1;
    while (left <= right) {
        let mid = left + ((right-left)>>1);
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 这里改成收缩左侧边界即可
            left = mid + 1;
        }
    }
    // 最后改成返回 left - 1
    if (left - 1 < 0) return -1;
    return nums[left - 1] == target ? (left - 1) : -1;
}

注意:
1.当 nums[mid] == target 时,不要立即返回,而是增大「搜索区间」的左边界 left,使得区间不断向右靠拢,达到锁定右侧边界的目的。
2. 对 left 的更新必须是 left = mid + 1(mid等于target),就是说 while 循环结束时,nums[left] 一定不等于 target 了,而 nums[left-1] 可能是 target。假设target存在,循环在left等于right时,计算得出left,right,mid三者相等,而这个left由上次循环得出可能等于target+1,那么这次循环中right=mid-1则right等于target,left等于target+1.
3. left 的取值范围是 [0, nums.length-1]。left取0时left-1会越界,需要进行判断。
4. 由于 while 的结束条件为 right 等于 left - 1,所以你把上述代码中的 left - 1 都改成 right 也没有问题。

B.题目

1. 二分查找
简单
在这里插入图片描述

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var search = function(nums, target) {
    var left = 0;
    var right = nums.length - 1;
    var mid =0;
    while(left<=right){
        mid = left + ((right-left)>>1);
        if(nums[mid]>target){
            right = mid-1
        }
        else if(nums[mid]<target){
            left = mid+1
        }
        else{
            return mid
        }
    }
    return -1
};

2. 在排序数组中查找元素的第一个和最后一个位置
中等
在这里插入图片描述

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var searchRange = function(nums, target) {
    var left_bound = function(nums,target){
        let left = 0, right = nums.length-1;
        while(left<=right){
            let mid = left + ((right-left)>>1);
            if(nums[mid]==target){
                right = mid-1;
            }
            else if(nums[mid]>target){
                right = mid-1;
            }
            else if(nums[mid]<target){
                left = mid+1;
            }
        }
        if(left==nums.length)return -1;
        return nums[left]==target?left:-1;
    }
    var right_bound = function(nums, target){
        let left=0, right = nums.length - 1;
        while(left<=right){
            let mid = left + Math.floor((right-left)/2);
            if(nums[mid]==target){
                left = mid+1;
            }
            else if(nums[mid]>target){
                right = mid-1;
            }
            else if(nums[mid]<target){
                left = mid+1;
            }
        }
        if(right<0)return -1;
        return nums[right]==target?right:-1;
    }
    let res = [];
    let left = left_bound(nums,target);
    let right = right_bound(nums,target);
    res.push(left);
    res.push(right);
    return res;
};
C.实际应用题目

二分搜索的原型就是在有序数组中搜索一个元素 target,返回该元素对应的索引。如果该元素不存在,那可以返回一个什么特殊值,这种细节问题只要微调算法实现就可实现。还有一个重要的问题,如果「有序数组」中存在多个 target 元素,那么这些元素肯定挨在一起,这里就涉及到算法应该返回最左侧的那个 target 元素的索引还是最右侧的那个 target 元素的索引,也就是所谓的「搜索左侧边界」和「搜索右侧边界」,这个也可以通过微调算法的代码来实现。
在具体的算法问题中,常用到的是搜索左侧边界搜索右侧边界这两种场景。
思路:
首先从题目中抽象出一个自变量x(一般是题目所求的变量),一个关于x的函数f(x),以及一个目标值target。
x,f(x),target需要满足以下条件:
①.f(x)必须是x上的单调函数(单调增或单调减都可以)。
②.题目是计算满足约束条件f(x)==target时的x值。要根据题目确定是求左边界还是右边界。同时还要注意f(x)与x的是单调递增还是递减的关系,注意是移动左边界还是右边界。
③.**根据题目确定自变量x的取值范围。**初始化left和right。

框架:

var f = function(x){
	...
}
var solution = function(nums, target) {
	//具体问题具体分析自变量x的最小值
    let left = ...; 
    //具体问题具体分析自变量x的最大值
    let right = ...+1;
    // 搜索区间为 [left, right]
    while (left <= right) {
        let mid = left + ((right-left)>>1);
        if (f[mid] < target) {
            // 移动左边界还是右边界让f(x)大一点?
            ...
        } else if (f[mid] > target) {
            // 移动左边界还是右边界让f(x)小一点?
            ...
        } else if (f[mid] == target) {
        	//看求左边界还是右边界
            ...
        }
    }
    return left;
}

题目:
1. 珂珂吃香蕉
中等
在这里插入图片描述
思路:
自变量x是珂珂吃香蕉的速度。吃香蕉的速度越快,吃完所有香蕉所需时间就越少,速度和时间是一个单调递减关系。f(x)定义为:若吃香蕉的速度为x根/小时,则需要f(x)小时吃完所有香蕉。target是吃香蕉的时间限制H。
吃香蕉的最小速度为1,最大应该是piles数组中的最大值,因为每小时最多吃一堆香蕉,吃再多也白搭。也可以根据题目中的piles[i]的取值范围1<=plies[i]<=10^9,将right初始化为取值范围之外的值。
在这里插入图片描述
题目要求计算最小速度,也就是x要尽可能小, 求左边界。

/**
 * @param {number[]} piles
 * @param {number} h
 * @return {number}
 */
var minEatingSpeed = function(piles, h) {
    var f = function(piles, x){
        let hour = 0;
        for(let i = 0; i< piles.length; i++){
            hour+=Math.floor(piles[i]/x);
            if(piles[i]%x>0)hour++;
        }
        return hour;
    }
    let left = 1;
    let right = 1000000000;
    while(left<=right){
        let mid = left + Math.floor((right-left)/2);
        let hour = f(piles, mid);
        if(hour==h){
            right = mid-1;
        }
        else if(hour>h){
            left = mid+1;
        }
        else if(hour<h){
            right = mid-1;
        }
    }
    return left;
};

2. 在D天内送达货物的能力
中等
在这里插入图片描述
思路:
船的运载能力是自变量x,f(x)是x运载能力下需要的运算天数,是单调递减的。target是days。船的最小运输能力应该是weight中元素的最大值,每次得装一件东西走,不能说装不了。船的最大运输能力是weights中左右元素之和,也就是一次把所有货物都运走。计算最小载重,也就是满足f(x)==target的条件下,x要尽可能小,求左边界。
在这里插入图片描述

/**
 * @param {number[]} weights
 * @param {number} days
 * @return {number}
 */
var shipWithinDays = function(weights, days) {
    var f = function(weights,x){
        let day = 1;
        let weightOneDay = 0;
        for(let i = 0; i<weights.length; i++){
            if((weightOneDay+weights[i]) <= x){
                weightOneDay+=weights[i];
            }
            else{
                day++;
                weightOneDay = weights[i];
            }
        }
        return day;
    }
    //注意最小值是最大货物的重量,最大值是所有货物加一起的重量
    let minx = 0, maxx= 0;
    for(let i = 0; i<weights.length; i++){
        minx = Math.max(minx,weights[i]);
        maxx += weights[i];
    }
    let left = minx, right = maxx;
    while(left<=right){
        let mid = left + Math.floor((right-left)/2);
        let day = f(weights, mid);
        if(day==days){
            right = mid - 1;
        }
        //注意f(x)与x成反比 当target<f(x)时 f(x)应该减小 mid应该增大 左边界增大
        else if(day>days){
            left = mid + 1;
        }
        else if(day<days){
            
            right = mid - 1;
        }
    }
    return left;
};

3. 分割数组的最大值
困难

在这里插入图片描述
思路:
这题和上面的题一样。货船每天运走的货物就是一个nums子数组,在m天内运完就是将nums划分为m个子数组。让货船的载重尽可能小,就是让所有子数组中最大的子数组之和尽可能小。

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
 var shipWithinDays = function(weights, days) {
    var f = function(weights,x){
        let day = 1;
        let weightOneDay = 0;
        for(let i = 0; i<weights.length; i++){
            if((weightOneDay+weights[i]) <= x){
                weightOneDay+=weights[i];
            }
            else{
                day++;
                weightOneDay = weights[i];
            }
        }
        return day;
    }
    //注意最小值是最大货物的重量,最大值是所有货物加一起的重量
    let minx = 0, maxx= 0;
    for(let i = 0; i<weights.length; i++){
        minx = Math.max(minx,weights[i]);
        maxx += weights[i];
    }
    let left = minx, right = maxx;
    while(left<=right){
        let mid = left + Math.floor((right-left)/2);
        let day = f(weights, mid);
        if(day==days){
            right = mid - 1;
        }
        //注意f(x)与x成反比 当target<f(x)时 f(x)应该减小 mid应该增大 左边界增大
        else if(day>days){
            left = mid + 1;
        }
        else if(day<days){
            
            right = mid - 1;
        }
    }
    return left;
};
var splitArray = function(nums, k) {
    return shipWithinDays(nums, k);
};
Ⅱ nSum问题
A.模板

1.两数之和
①假设输入一个数组 nums 和一个目标和 target,请你返回 nums 中能够凑出 target 的两个元素的值,比如输入 nums = [1,3,5,6], target = 9,那么算法返回两个元素 [3,6]。可以假设只有且仅有一对儿元素可以凑出 target。比如题目 两数之和 两数之和||-输入有序数组
先保证数组是有序数组,再使用双指针法即可。

/**
 * @param {number[]} numbers
 * @param {number} target
 * @return {number[]}
 */
 //两数之和||-输入有序数组
var twoSum = function(numbers, target) {
    let left = 0, right = numbers.length-1;
    while(left<right){
        let sum = numbers[left] + numbers[right];
        if(sum == target){
            return [left+1,right+1];
        }
        else if(sum>target){
            right--;
        }
        else if(sum<target){
            left++;
        }
    }
    return [];
};

②nums 中可能有多对儿元素之和都等于 target,请你的算法返回所有和为 target 的元素对儿,其中不能出现重复。
思路:排序加左右指针。sum == target 条件下,当给 res 加入一次结果后,left 和 right 应该跳过所有重复的元素。就可以保证一个答案只被添加一次,重复的结果都会被跳过,可以得到正确的答案。其他两个 if 分支也是可以做一点效率优化,跳过相同的元素
twoSum模板:

var twoSum = function(nums,  target){
		nums.sort((a,b)=>a-b);
        let left = 0, right = nums.length-1;
        let res = [];
        while(left<right){
            let sum = nums[left] + nums[right];
            let lbound = nums[left], rbound = nums[right];
            if(sum==target){
                res.push([nums[left], nums[right]]);
                while(left<right&&nums[left]==lbound){
                    left++;
                }
                while(left<right&&nums[right]==rbound){
                    right--;
                }
            }
            else if(sum>target){
                while(left<right&&nums[right]==rbound){
                    right--;
                }
            }
            else if(sum<target){
                while(left<right&&nums[left]==lbound){
                    left++;
                }
            }
        }
        return res;
    }

双指针操作的部分虽然有那么多 while 循环,但是双指针部分的时间复杂度还是 O(N)。
2. nSum模板
想找和为 target 的n个数字,那么对于第一个数字,可能是什么?nums 中的每一个元素 nums[i] 都有可能!确定了第一个数字之后,剩下的n-1个数字可以是什么呢?其实就是和为 target - nums[i] 的n-1个数字。类似 twoSum,nSum 的结果也可能重复,比如3Sum,输入是 nums = [1,1,1,2,3], target = 6,结果就会重复。关键点在于,不能让第一个数重复,至于后面的两个数,我们复用的 twoSum 函数会保证它们不重复。所以代码中必须用一个 while 循环来保证 3Sum 中第一个元素不重复。
时间复杂度为o(N^n-1)。
该模板的前提是调用这个函数时
数组必须是排序的

n == 2 时是 twoSum 的双指针解法,n > 2 时就是穷举第一个数字,然后递归调用计算 (n-1)Sum,组装答案。需要注意的是,调用这个 nSum 函数之前一定要先给 nums 数组排序,因为 nSum 是一个递归函数,如果在 nSum 函数里调用排序函数,那么每次递归都会进行没有必要的排序,效率会非常低。

//调用该函数时nums必须是排过序的
var nSum = function(nums, n, start, target){
   let res = [];
   if(n<2||nums.length<n)return res;
   //一般情况 两数之和
   if(n==2){
       let left = start, right = nums.length-1;
       while(left<right){
           let sum = nums[left]+nums[right];
           let lbound = nums[left], rbound = nums[right];
           if(sum==target){
               res.push([nums[left], nums[right]]);
               while(left<right&&nums[left]==lbound){
                   left++;
               }
               while(left<right&&nums[right]==rbound){
                   right--;
               }
           }
           else if(sum<target){
               while(left<right&&nums[left]==lbound){
                   left++;
               }
           }
           else if(sum>target){
               while(left<right&&nums[right]==rbound){
                   right--;
               }
           }
       }
   }
   //n>2时 递归计算(n-1)sum的结果
   if(n>2){
       for(let i = start; i<nums.length-(n-1); i++){
           let sub = nSum(nums, n-1, i+1, target-nums[i]);
           if(sub.length){
               for(let j = 0; j<sub.length; j++){
                   sub[j].push(nums[i]);
                   res.push(sub[j]);
               }
           }
           //去重 保证第一个数字不重复
           while(nums[i]==nums[i+1]){
               i++;
           }
       }
   }
   return res;
}
B.题目

1. 三数之和
中等
在这里插入图片描述

//解法一
var threeSum = function(nums) {
  // 将数组升序排序
  nums.sort((a,b)=>a-b);
  let res = [];
  for(let i = 0;i<nums.length;i++){
      // 数组排过序,如果第一个数大于0直接返回res
      if(nums[i]>0)return res;
      let left = i+1, right = nums.length-1;
      //去重
      if(i>0&&nums[i]==nums[i-1]) continue;
      while(left<right){
          let sum = nums[i]+nums[left]+nums[right];
          if(sum<0)left++;
          else if (sum>0)right--;
          else{
              res.push([nums[i],nums[left],nums[right]]);
              //去重
              while(left<right&&nums[left]==nums[left+1]){
                  left++;
              }
              while(left<right&&nums[right]==nums[right-1]){
                  right--;
              }
              left++;
              right--;
          }
      }
  }
  return res;
};
//解法二
var threeSum = function(nums){
  var twoSum = function(nums, start, target){
      let left = start, right = nums.length-1;
      let res = [];
      while(left<right){
          let sum = nums[left] + nums[right];
          let lbound = nums[left], rbound = nums[right];
          if(sum==target){
              res.push([nums[left], nums[right]]);
              while(left<right&&nums[left]==lbound){
                  left++;
              }
              while(left<right&&nums[right]==rbound){
                  right--;
              }
          }
          else if(sum>target){
              while(left<right&&nums[right]==rbound){
                  right--;
              }
          }
          else if(sum<target){
              while(left<right&&nums[left]==lbound){
                  left++;
              }
          }
      }
      return res;
  }
  //升序排序
  nums.sort((a,b)=>a-b);
  let res = [];
  for(let i = 0; i<nums.length-2; i++){
      //寻找和为0-nums[i]的剩下两个数
      let twoNum = twoSum(nums,i+1,0-nums[i]);
      if(twoNum.length){
          for(let j = 0 ;j<twoNum.length; j++){
              twoNum[j].push(nums[i]);
              res.push(twoNum[j]);
          }
      }
      //去重
      while(nums[i]==nums[i+1]){
          i++;
      }
  }
  return res;
}

2. 四数之和
中等
在这里插入图片描述

//解法一
var fourSum = function(nums, target) {
  let ret = [];
  if(nums.length<4)return ret;
  nums.sort((a,b)=>a-b);
  for(let i = 0;i<nums.length-3;i++){
      if(i>0&&nums[i]==nums[i-1])continue;
      for(let j = i+1;j<nums.length-2;j++){
          if(j>i+1&&nums[j]==nums[j-1])continue;
          let l = j+1, r = nums.length-1;
          while(l<r){
              let sum = nums[i]+nums[j]+nums[l]+nums[r];
              if(sum<target)l++;
              else if(sum>target)r--;
              else{
                  ret.push([nums[i],nums[j],nums[l],nums[r]]);
                  while(l<r&&nums[l]==nums[l+1])l++;
                  while(l<r&&nums[r]==nums[r-1])r--;
                  l++;
                  r--;
              }
          }
      }
  }
  return ret;
};
//解法二
//调用该函数时nums必须时排过序的
var nSum = function(nums, n, start, target){
  let res = [];
  if(n<2||nums.length<n)return res;
  //一般情况 两数之和
  if(n==2){
      let left = start, right = nums.length-1;
      while(left<right){
          let sum = nums[left]+nums[right];
          let lbound = nums[left], rbound = nums[right];
          if(sum==target){
              res.push([nums[left], nums[right]]);
              while(left<right&&nums[left]==lbound){
                  left++;
              }
              while(left<right&&nums[right]==rbound){
                  right--;
              }
          }
          else if(sum<target){
              while(left<right&&nums[left]==lbound){
                  left++;
              }
          }
          else if(sum>target){
              while(left<right&&nums[right]==rbound){
                  right--;
              }
          }
      }
  }
  //n>2时 递归计算(n-1)sum的结果
  if(n>2){
      for(let i = start; i<nums.length-(n-1); i++){
          let sub = nSum(nums, n-1, i+1, target-nums[i]);
          if(sub.length){
              for(let j = 0; j<sub.length; j++){
                  sub[j].push(nums[i]);
                  res.push(sub[j]);
              }
          }
          while(nums[i]==nums[i+1]){
              i++;
          }
      }
  }
  return res;
}
var fourSum = function(nums, target) {
  nums.sort((a,b)=>a-b);
  return nSum(nums, 4, 0, target);
}
Ⅲ 反转数组

1. 反转字符串
简单
在这里插入图片描述

/**
 * @param {character[]} s
 * @return {void} Do not return anything, modify s in-place instead.
 */
var reverseString = function(s) {
    let left = 0, right = s.length-1;
    while(left<right){
        [s[left],s[right]] = [s[right],s[left]];
        left++;
        right--;
    }
    return s;
};
Ⅳ 回文串判断

回文串就是正着读和反着读都一样的字符串。比如说字符串 aba 和 abba 都是回文串,因为它们对称,反过来还是和本身一样;反之,字符串 abac 就不是回文串。
可以使用左右指针相向而行判断是否为回文串。
回文子串问题则是让左右指针从中心向两端扩展
1. 最长回文子串
中等
在这里插入图片描述
思路:
找回文串的难点在于,回文串的的长度可能是奇数也可能是偶数,解决该问题的核心是从中心向两端扩散的双指针技巧。如果回文串的长度为奇数,则它有一个中心字符;如果回文串的长度为偶数,则可以认为它有两个中心字符。如果输入相同的 l 和 r,就相当于寻找长度为奇数的回文串,如果输入相邻的 l 和 r,则相当于寻找长度为偶数的回文串。

/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function(s) {
    var Palindrome = function(s, l ,r){
        while(l>=0&&r<s.length&&s[l]==s[r]){
             // 双指针,向两边展开
                l--;
                r++;  
        }
        //substring() 方法返回的子串包括 开始 处的字符,但不包括 结束 处的字符。
        //string.substr(start,length)方法可在字符串中抽取从 开始 下标开始的指定数目的字符。
        // 返回以 s[l+1] 和 s[r](不包括s[r]) 为中心的回文串
        return s.substring(l+1, r);
    }
    let res = '';
    for(let i = 0; i<s.length; i++){
        //如果输入相同的 l 和 r,就相当于寻找长度为奇数的回文串,如果输入相邻的 l 和 r,则相当于寻找长度为偶数的回文串。
        //奇数串
        let s1 = Palindrome(s, i, i);
        //偶数串
        let s2 = Palindrome(s, i, i+1);
        res = res.length<s1.length?s1:res;
        res = res.length<s2.length?s2:res;
    }
    return res;
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值