滑动窗口、双指针

  1. 需要移动左右两头的问题要想到双指针
  2. 在一些数组类、字符串类题目中,我们可以用滑动窗口来观察可能的候选结果。当滑动窗口从数组的左边滑到了右边,我们就可以从所有的候选结果中找到最优的结果。
  3. 对于【给一个字符串组成的句子(带空格或标点),然后对句中单个字符串进行一系列处理】的题目,可以使用以下模板:
    核心思想:先把句子中所有字符串取出放入字符串数组,再对数组中的字符串进行操作后重新连接即可,具体问题具体细节还需要按题目要求分析 。而遍历句子取字符串的思路,就是遇到字符把它放入临时字符串,遇到空格或者标点(如果有标点),就把临时字符串输出,并且清空
    (1)如果有前后置空格,那么必须判断临时字符串非空才能输出,否则会输出空串
    模板如下:
	s += " "; // 这里在句子最后一个字符位置加上空格,这样最后一个字符串就不会遗漏
    let temp = "";  // 临时字符串
    let res = []; // 存放字符串的数组
    let ch;
    for (let i = 0; i < s.length; i++) {// 遍历字符句子
        ch = s.charAt(i);
        if (ch === ' ') {// 遇到空格
            if (temp) {// 临时字符串非空
                res.push(temp);
                temp = "";  // 清空临时字符串
            }
        }
        else temp += ch;
    }

(2)没有前后置的空格不需要判断空串

	s += " "; // 这里在最后一个字符位置加上空格,这样最后一个字符串就不会遗漏
    let temp = "";  //临时字符串
    let res = []; //存放字符串的数组
    let ch;
    for (let i = 0; i < s.length; i++) {//遍历字符句子
        ch = s.charAt(i);
        if (temp) {//临时字符串非空
        	res.push(temp);
            temp = "";  //清空临时字符串
        }
        else temp += ch;
    }

3. 无重复字符的最长子串

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
示例 4:
输入: s = “”
输出: 0

解:滑动窗口
一旦涉及出现次数 用HashMap 字符串HashMap题型都是用字符作为键 出现的次数作为值 这里 我们需要构造子串 所以我们需要下标的信息 涉及子串的信息 往往可以通过滑动串口来动态的扩张/收缩 子串
1.使用两个指针i j i指向子串的第一个字符 j用来遍历字符串。遍历字符串 遍历到的字符和map中的键比较 若该字符不存在 则将该字符加入map 其中该字符是键 该字符所在字符串的索引是值 且 max = Math.max(max,i - j)记录字串的长度 括号中的max是上一次计算出来的子串的长度 若该字符存在map 移动i指向下一个元素 j遍历查看下一个元素是否在map中……如此循环 直到遍历完字符串
2.注:本题中(i,j] i指向的字符串取不到 j指向的字符串能取到
3.若字符存在map中 关于i应该指向哪个元素的问题:
(1)当前字符包含在当前有效的子串中,如:abca,当我们遍历到第二个a,当前有效最长子串是 abc,我们又遍历到a,那么此时更新j为 map.get(a) = 0,当前有效子段更新为 bca;
(2)当前字符不包含在当前最长有效子串中,如:abba,我们先添加a,b进map,此时i=-1,我们再添加b,发现map中包含b,而且b包含在最长有效子串中,就是(1)的情况,我们更新 i=map.get(b) = 1,此时子串更新为 b,而且map中仍然包含a,map.get(a)=0;随后,我们遍历到a,发现a包含在map中,且map.get(a)=0,如果我们像(1)一样处理,就会发现 i=map.get(a)=0,实际上,i此时应该不变,i始终为1,子串变成 ba才对。所以在写代码时 我们让i = Math.max(i, map.get(s.charAt(j)));
代码如下:
JS

var lengthOfLongestSubstring = function(s) {
    if (s === "") return 0;
    if (s.trim().length === 0) return 1;
    let map = new Map();
    let i = -1;
    let max = 1;
    for (let j = 0; j < s.length; j++) {
        if (map.has(s.charAt(j))) {
            i = Math.max(i, map.get(s.charAt(j)));
        }
        map.set(s.charAt(j), j);
        max = Math.max(max, j - i);
    }

    return max;
};

11. 盛最多水的容器

给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
在这里插入图片描述
解:
因为要求容积 肯定要距离越远越好 所以最初我们要求一个指针i位于数组头部 一个指针j位于数组尾部
每次记录面积 面积=(j-i)*两个指针指向的数字中较小值 然后移动指向的数组中较小值的那个指针 因为面积受限于较短边 所以我们只有改变较小边才能增加获得更大的面积的可能性

15. 三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
示例 2:
输入:nums = []
输出:[]
示例 3:
输入:nums = [0]
输出:[]

解:
1.首先对数组升序排序:nums.sort((a, b) => a - b);
2.排序后固定一个数nums[i],再使用左右指针指向nums[i]后面的两端nums[L] nums[R],计算三个数的和sum 若sum = 0 则添加进结果集 数组中添加数组:arrs.push([nums[i],nums[l],nums[r]]);
3.(1)若nums[i] > 0,因为nums[i]后面的数肯定比nums[i]大,所以三数之和必然无法等于0,结束循环
(2)如果sum < 0 则L++
(2)若sum > 0 则R−−
4.在遍历的过程中:
(1)如果nums[i] = nums[i−1],则说明该数字重复,会导致结果重复,所以应该跳过
(2)当sum = 0时,nums[L] = nums[L+1],则会导致结果重复,应该跳过,L++
(3)当sum= 0时,nums[R] = nums[R−1],则会导致结果重复,应该跳过,R−−
代码如下:
JS:

var threeSum2 = function(nums) {
    if (nums.length < 3) return [];
    nums.sort((a, b) => a - b);
    let arrs = [];
    let l;
    let r;
    let sum;
    for (let i = 0; i < nums.length - 2; i++) {
        if (nums[i] > 0) break;
        if (i > 0 && nums[i] === nums[i - 1]) continue;
        l = i + 1;
        r = nums.length - 1;
        while (l < r) {
            sum = nums[l] + nums[i] + nums[r];
            if (sum > 0) {
                r--;
                while (nums[r] === nums[r + 1]) r--;
            }else if (sum < 0) {
                l++;
                while (nums[l] === nums[l - 1]) l++;
            }else {
                arrs.push([nums[i],nums[l],nums[r]]);
                r--;
                while (nums[r] === nums[r + 1]) r--;
                l++;
                while (nums[l] === nums[l - 1]) l++;
            }
        }
    }

    return arrs;
}

JAVA:

	static List<List<Integer>> threeSum2(int[] nums) {
        List<List<Integer>> arrayLists = new ArrayList<>();
        Arrays.sort(nums);
        int num1;
        for (int i = 0; i < nums.length; i++) {
            if (i > 0 && nums[i - 1] == nums[i]) continue;

            num1 = 0 - nums[i];
            int j = i + 1;
            int k = nums.length - 1;
            while (j < k){
                if (j > i + 1 && nums[j] == nums[j - 1]){
                    j++;
                    continue;
                }
                if (k < nums.length - 1 && nums[k] == nums[k + 1]){
                    k--;
                    continue;
                }
                List<Integer> arrayList = new ArrayList<>();
                if (nums[j] + nums[k] == num1) {
                    arrayList.add(nums[j]);
                    arrayList.add(nums[k]);
                    arrayList.add(nums[i]);
                    arrayLists.add(arrayList);
                    j++;
                    k--;
                }else if (nums[j] + nums[k] < num1){
                    j++;
                }else {
                    k--;
                }
            }
        }
        return arrayLists;
    }

42. 接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

例1:
在这里插入图片描述
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

解:
方法一(暴力法):先思考积水的多少是由什么决定的:当前位置左边的最高的柱子和当前位置右边的最高的柱子中最短的那根柱子决定的 并且当前位置的积水量是那个柱子与当前位置的差值 用代码解释:每一列上的面积/当前位置i的积水量=Math.min(i左边的最高的柱子,i右边的最高的柱子)-height[i];那么使用暴力法就是遍历height 对于每一个height[i] 向左遍历找到左边最高的柱子 向右遍历找到右边最高的柱子 这两个柱子中最短的那根柱子减去height[i]即当前位置的积水量 累加起来即可 代码如下

var trap = function (height) {
  let res = 0;
  for (let i = 0; i < height.length; i++) {
    let max_letf = 0;
    let max_right = 0;
    // 以当前curr 这个位置为基准,找出左边的边界的最大值 找出右边边界的最大值
    for (let j = i; j >= 0; j--) {
      max_letf = Math.max(max_letf, height[j])
    }
    for (let j = i; j < height.length; j++) {
      max_right = Math.max(max_right, height[j])
    }

    res += Math.min(max_letf, max_right) - height[i]
  }
  return res
};

方法二(双指针):

  1. 定义两个指针分别指向数组的首尾left = 0;right = height.length - 1;再定义两个变量left_max right_max分别记录左边最长的柱子 右边最长的柱子 初始化为0
  2. 由于积水量是由短的那边决定的 所以我要先确定数组两头哪一头短 比较height[left]与height[right] 若左边短 说明积水量取决于左边的大小 则从左边开始遍历 否则从右边开始遍历
  3. 从左边开始遍历:比较height[left]与left_max 若height[left]<=left_max 说明会积水 积水量等于二者的差值 累加积水量并将大的那个存储在left_max中 若height[left]>left_max 更新左边最高的柱子left_max left_max = height[left] 此时height[left]不会有积水 如下图 第二个箭头指的那根柱子上不会有积水。比较完了后 指针右移继续比较height[left]与height[right]
    在这里插入图片描述
  4. 从右边开始遍历:比较height[right]与right_max 若height[right]<=right_max 说明会积水 积水量等于二者的差值 累加积水量并将大的那个存储在right_max中 若height[right]>right_max 更新左边最高的柱子right_max right_max = height[right] 此时height[right]不会有积水 比较完了后 指针左移继续比较height[left]与height[right]
    在这里插入图片描述代码如下:
var trap = function (height) {
  let left = 0;
  let right = height.length - 1;
  let res = 0;
  let leftMax = 0;
  let rightMax = 0;
  while (left < right) {
    if (height[left] < height[right]) {
      leftMax = Math.max(height[left], leftMax);
      res += leftMax - height[left];
      left++;
    } else {
      rightMax = Math.max(height[right], rightMax);
      res += rightMax - height[right];
      right--;
    }
  }
  return res;
};

415. 字符串相加

给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和。
解:两个指针n1 n2 分别指向两个字符串的最后一个字符 设立标志位flag用来表示是否进位 n1 n2指向的两个字符及flag相加 如果结果大于9 flag设为1 表示有进位 如果小于10 表示没进位 flag设为0 最后遍历完了检查flag有没有进位 有的话再加上去
代码如下:

var addStrings = function(num1, num2) {
    let ans = [];
    let arr1 = num1.split("");
    let arr2 = num2.split("");
    let n1 = arr1.length - 1;
    let n2 = arr2.length - 1;
    let num = 0;
    let flag = 0;
    while(n1 >= 0 && n2 >= 0) {
        num = parseInt(arr1[n1]) + parseInt(arr2[n2]) + flag;
        if (num > 9) {
            num = num % 10;
            flag = 1;
        }else {// num <= 9
            flag = 0;
        }
        ans.push(num);
        n1--;
        n2--;
    }
    if (n1 === -1 && n2 !== -1) {
        while (n2 >= 0) {
            num = parseInt(arr2[n2]) + flag;
            if (num > 9) {
                num = num % 10;
                flag = 1;
            }else {// num <= 9
                flag = 0;
            }
            ans.push(num);
            n2--;
        }
    }
    if (n2 === -1 && n1 !== -1){
        while (n1 >= 0) {
            num = parseInt(arr1[n1]) + flag;
            if (num > 9) {
                num = num % 10;
                flag = 1;
            }else {// num <= 9
                flag = 0;
            }
            ans.push(num);
            n1--;
        }
    }
    if (flag !== 0) ans.push(1);
    return ans.reverse().join("");
};

冗余代码太多 优化一下:

var addStrings = function(num1, num2) {
    let i = num1.length - 1, j = num2.length - 1, add = 0;
    const ans = [];
    while (i >= 0 || j >= 0 || add != 0) {
        const x = i >= 0 ? num1.charAt(i) - '0' : 0;
        const y = j >= 0 ? num2.charAt(j) - '0' : 0;
        const result = x + y + add;
        ans.push(result % 10);
        add = Math.floor(result / 10);
        i -= 1;
        j -= 1;
    }
    return ans.reverse().join('');
};

剑指 Offer 05. 替换空格

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
解:
1.双指针
(1)统计字符串的空格数量。
(2)根据空格数量和原有字符串有效字符长度,计算出刚好存放替换后的字符长度的数组。
(3)创建两个指针,一个指数组末尾,一个指字符串有效位的末尾,实现原地修改。
(4)值得注意的是:数组遍历,一定要从后往前遍历,避免从前往后,造成字符被修改,导致错误!
代码如下:
JS:

var replaceSpace = function(s) {
    if (s.length === 0) return '';
    let count = 0;
    for (let i = 0; i < s.length; i++) {
        if (s.charAt(i) === ' ') {
            count++;
        }
    }
    let arr = new Array(s.length + count * 3);
    let i = arr.length - 1;
    let j = s.length - 1;
    while (i >= 0 && j >= 0) {
        if (s.charAt(j) === " ") {
            arr[i--] = "0";
            arr[i--] = "2";
            arr[i--] = "%";
        }else {
            arr[i--] = s.charAt(j);
        }
        j--;
    }
    return arr.join('');
};
  1. split() 方法用于把一个字符串分割成字符串数组。
  • 语法:stringObject.split(separator,howmany)
  • separator:必需。字符串或正则表达式,从该参数指定的地方分割stringObject。若传入空字符串 ("") 那么stringObject中的每个字符之间都会被分割。
  • howmany:可选。该参数可指定返回的数组的最大长度。如果设置了该参数,返回的子串不会多于这个参数指定的数组。如果没有设置该参数,整个字符串都会被分割,不考虑它的长度。
  1. join() 方法用于把数组中的所有元素放入一个字符串。
  • 语法:arrayObject.join(separator)
  • separator:可选。指定要使用的分隔符。如果省略该参数,则使用逗号作为分隔符。

2.正则替换

var replaceSpace = function(s) {
    // 1. 调用库函数懒蛋法 正则匹配 s.replace
    return s.replace(/\s/g, '%20')
};

去掉字符串前后的空格

function trim(str){  
    str = str.replace(/^(\s|\u00A0)+/,'');  
    for(var i=str.length-1; i>=0; i--){  
        if(/\S/.test(str.charAt(i))){  
            str = str.substring(0, i+1);  
            break;  
        }  
    }  
    return str;  
}

剑指 Offer 21. 调整数组顺序使奇数位于偶数前面

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。

示例1
输入:[1,2,3,4]
返回值:[1,3,2,4]
示例2
输入:[2,4,6,5,7]
返回值:[5,7,2,4,6]

解:
1.定义两个指针k l k从头遍历 找到第一个偶数 l从后遍历找到第一个奇数 交换两个数
2.从头遍历数组 找到偶数后开始从后往前找奇数 为了避免重复遍历 我们定义j指针指向上一次遍历找到的奇数 j初始化为nums.length - 1
3.判断数的奇偶性:奇数&1===1
4.交换两个数nums[l] nums[k]:^异或:相同为0,不同为1

nums[l]=nums[l]^nums[k];
nums[k]=nums[l]^nums[k];
nums[l]=nums[l]^nums[k];

代码如下:
JS:

var exchange = function(nums) {
    if (nums.length < 2) return nums;
    let j = nums.length - 1;
    for (let k = 0; k < nums.length; k++) {// 从头遍历数组 指向第一个偶数
        if ((nums[k] & 1) !== 1) {// 偶数
            for (let l = j; l > k; l--) {// 从后遍历数组 找到第一个奇数
                if ((nums[l] & 1) === 1) {// 奇数
                    j = l - 1;
                    nums[l]=nums[l]^nums[k];
                    nums[k]=nums[l]^nums[k];
                    nums[l]=nums[l]^nums[k];
                    break;
                }
            }
        }
    }
    return nums;
};

如果要保证交换后奇数和奇数,偶数和偶数之间的相对位置不变 首先对数组中的元素进行遍历,每遇到一个奇数就将它加入到 奇数辅助数组中,每遇到一个偶数,就将它将入到偶数辅助数组中。最后再将两个数组合并。
代码如下:
JS:

function reOrderArray( array ) {
    // write code here
    let arr1 = [],arr2 = []
    for(let item of array){
        if(item % 2 == 0) arr2.push(item)
        else arr1.push(item)
    }
    return arr1.concat(arr2)
}

还有一个抖机灵的解法:

var exchange = function(nums) {
	return nums.sort((a,b)=>b%2-a%2)
};

剑指 Offer 57. 和为s的两个数字

输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。

示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]
示例 2:
输入:nums = [10,26,30,31,47,60], target = 40
输出:[10,30] 或者 [30,10]

解:双指针i j分别指向数组的两端 当两个数的和大于target时 j-- 小于时i++ 等于时输出

var twoSum = function(nums, target) {
    let i = 0;
    let j = nums.length - 1;
    let left, right, sum;
    while (i < j) {
        left = nums[i];
        right = nums[j];
        sum = left + right;
        if (sum < target) {
            i++;
        }else if (sum > target) {
            j--;
        }else {
            return [left, right];
        }
    }
    return [];
};

剑指 Offer 57 - II. 和为s的连续正数序列

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

示例 1:
输入:target = 9
输出:[[2,3,4],[4,5]]
示例 2:
输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]

解:设连续正整数序列的左边界i和右边界j ,则可构建滑动窗口从左向右滑动。循环中,每轮判断滑动窗口内元素和与目标值target的大小关系,若相等则记录结果,若大于target则移动左边界i(以减小窗口内的元素和),若小于target则移动右边界j(以增大窗口内的元素和)。

var findContinuousSequence = function(target) {
    let mid = Math.ceil(target / 2);
    let ans = [];
    let i = 1;
    let arr = [];
    let sum = 1;
    let j = 2;
    while (i < mid && j <= mid && i < j) {
        if (sum + j < target) {
            sum += j;
            j++;
        }else {// sum + j >= target
            if (sum + j === target){
                for (let k = i; k <= j; k++) {
                    arr.push(k);
                }
                ans.push(arr.slice());
                arr = [];
            }
            sum -= i;
            i++;
        }
    }
    return ans;
};

剑指 Offer 58 - I. 翻转单词顺序

输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。

示例 1:
输入: “the sky is blue”
输出: “blue is sky the”
示例 2:
输入: " hello world! "
输出: “world! hello”
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
示例 3:
输入: “a good example”
输出: “example good a”
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

解:
方法一:去除字符串前后的空格 将字符串以空格分隔为数组 反转数组 将数组以空格拼接成字符串

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

方法二:若不能使用内部函数 则遍历

var reverseWords = function(s) {
    if (!s || s.trim().length === 0) return "";
    let ans = "";
    let begin = -1;
    for (let i = 0; i <= s.length; i++) {
        if (s.charAt(i) === " " || i === s.length) {
            if (begin !== -1) {
                ans = " " + s.substring(begin, i) + ans;
                begin = -1;
            }
        }else {
            if (begin === -1) {
                begin = i;
            }
        }
    }
    return ans.substring(1,ans.length);
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值