解题来源:人人都能看得懂的Leetcode力扣刷题教程合集(最后更新:695 .岛屿的最大面积)https://www.bilibili.com/video/BV1wA411b7qZ
语言:JavaScript
边刷题边记录【持续更新】
题目目录
- 3. 无重复字符的最长子串
- 4. 寻找两个正序数组的中位数
- 5. 最长回文子串
- 15. 三数之和
- 19. 删除链表的倒数第 N 个结点
- 20. 有效的括号
- 21. 合并两个有序链表
- 24. 两两交换链表中的节点
- 49. 字母异位词分组
- 53. 最大子序和
- 54. 螺旋矩阵
- 55. 跳跃游戏
- 56. 合并区间
- 62. 不同路径
- 66. 加一
- 70. 爬楼梯
- 73. 矩阵置零
- 83. 删除排序链表中的重复元素
- 92. 反转链表Ⅱ
- 121. 买卖股票的最佳时机
- 122. 买卖股票的最佳时机Ⅱ
- 123. 买卖股票的最佳时机Ⅲ
- 125. 验证回文串
- 134. 加油站
- 141. 环形链表
- 141. 环形链表Ⅱ
- 152. 乘积最大子数组
- 153. 寻找旋转排序数组中的最小值
- 160. 相交链表
- 187. 重复的DNA序列
- 198. 打家劫舍
- 200. 岛屿数量
- 217. 存在重复元素
- 219. 存在重复元素II
- 242. 有效的字母异位词
- 283. 移动零
3. 无重复字符的最长子串
题目
思路
滑动窗口 Sliding Window -> 双指针
- 创建一个set
- 两个指针:第一个指针指向字符串的开头,第二个随着for循环遍历字符串
- 如果set里面没有s[i],说明目前为止没有重复的字符,把s[i]添加到set里,然后更新最大不重复字符的数量
- 如果set里面有s[i],则从set里开始删除s[j],并且递增,再检查set里是否有s[i],如此反复直到set里没有s[i]为止
- 重复步骤3和4,直到遍历完整个字符串
代码
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
const set = new Set();
let i = 0, j = 0, maxLength = 0;
if(s.length === 0){
return 0;
}
for(i; i<s.length; i++){
if(!set.has(s[i])){
set.add(s[i]);
maxLength = Math.max(maxLength, set.size);
} else {
while(set.has(s[i])){
set.delete(s[j]);
j++;
}
set.add(s[i]);
}
}
return maxLength;
};
4. 寻找两个正序数组的中位数
题目
思路
5. 最长回文子串
题目
思路
中心扩散思想(从中间往两边扩散,去判断两边的字符是否相等):
- 如果字符串长度小于2,直接返回原字符
- 定义两个变量,一个start存储当前找到的最大回文字符串的起始位置,另一个maxLength记录字符串的长度(终止位置就是start+maxLength)
- 创建一个helper functtion,判断左边和右边是否越界,同时最左边的字符是否等于最右边的字符。如果以上3个条件都满足,则判断是否需要更新回文字符串最大长度及最大字符串的起始位置,然后left–,right++,继续判断,直到不满足3个条件之一。
- 遍历字符串,每个位置调用helper function两遍,第一遍检查i-1,i+1,第二遍检查i,i+1。(检查两遍是因为会有abba这种回文字符串,没有中心点)
代码
/**
* @param {string} s
* @return {string}
*/
var longestPalindrome = function(s) {
if(s.length < 2){
return s;
}
let start = 0;
let maxLength = 1;
function expendAroundCenter(left, right){
while(left >= 0 && right < s.length && s[left]===s[right]){
if(right - left + 1 > maxLength){
maxLength = right - left + 1;
start = left;
}
left--;
right++;
}
}
for(let i = 0; i < s.length; i++){
expendAroundCenter(i-1, i+1);
expendAroundCenter(i, i+1);
}
return s.substring(start, start+maxLength);
};
15. 三数之和
题目
思路
- 给数组排序
- 遍历数组,从0遍历到length-2
- 如果当前的数字等于前一个数字,则要跳过这个数
- 如果数字不同,则设置start=i+1.end=length-1,查看i,start和end三个数的和比零大还是比小,如果比零小,start++,如果比0大,end–,如果等于0,把这三个数加入到结果里。
- 返回结果。
代码
var threeSum = function(nums) {
const array = [];
nums.sort((a,b) => a-b);
for(let i = 0; i < nums.length - 2; i++){
if(i!==0 && nums[i]===nums[i-1]){
continue;
}
let start = i+1, end = nums.length - 1;
while(start < end){
if(nums[start]+nums[i]+nums[end] === 0 ){
array.push([nums[start],nums[i],nums[end]]);
start++;
end--;
//去重判断,在重新开始寻找的时候记得去重
while(start<end && nums[start] === nums[start-1]){
start++;
}
while(start<end && nums[end] === nums[end+1]){
end--;
}
}else if(nums[start]+nums[i]+nums[end] < 0){
start++;
}else{
end--;
}
}
}
return array;
};
19. 删除链表的倒数第 N 个结点
题目
思路
双指针法:
两个指针n1,n2,让n1不动,n2往前走n个位置,然后n1、n2同时移动,当n2到底部的时候,n1刚好指向需要删除的节点。(需要创建一个dumy头部结点应对特殊情况)
代码
var removeNthFromEnd = function(head, n) {
let dummy = new ListNode();
dummy.next = head;
let n1 = dummy, n2 = dummy;
for(let i = 0; i <= n; i++){
n2 = n2.next;
}
while(n2 !== null){
n1 = n1.next;
n2 = n2.next;
}
n1.next = n1.next.next;
return dummy.next;
};
20. 有效的括号
题目
思路
栈stack:
- 创建一个HashMap,把括号配对放进去
- 创建一个stack(object),for循环遍历字符串,对于每一个字符,如果map里有这个key,那说明它是个左括号,从map里取得相应的右括号,把它push进stack里,否则的话,它就是右括号,需要pop出stack里的第一个字符然后看它是否等于当前的字符,如果不相符,则返回false。
- 循环结束后,如果stack不为空,说明还剩一些左括号没有被闭合,返回false,否则返回true。
代码
var isValid = function(s) {
const mappings = new Map();
mappings.set("(",")");
mappings.set("[","]");
mappings.set("{","}");
const stack = [];
for(let i = 0; i<s.length; i++){
if(mappings.has(s[i])){
stack.push(mappings.get(s[i]));
} else {
if(stack.pop() !== s[i]){
return false;
}
}
}
if(stack.length !== 0){
return false;
}
return true;
};
21. 合并两个有序链表
题目
思路
依次比较大小,将小的放进新的链表里
*链表题中经常使用dumy结点,去存储链表的头部,相当于头部的占位符。
代码
var mergeTwoLists = function(l1, l2) {
let curr = new ListNode();
let dumy = curr;
while(l1 !== null && l2 !== null){
let newNode = new ListNode();
if(l1.val < l2.val){
curr.next = l1;
l1 = l1.next;
} else {
curr.next = l2;
l2 = l2.next;
}
curr = curr.next;
}
//直接把剩余结点全部添加进curr
if(l2 !== null) {
curr.next = l2;
}
if(l1 !== null) {
curr.next = l1;
}
return dumy.next;
};
24. 两两交换链表中的节点
题目
思路
- n1 = p.next
- n2 = p.next.next
- p.next = n2
- n1.next = n2.next
- n2.next = n1
- p = n1
加dumy结点
代码
var swapPairs = function(head) {
let dummy = new ListNode();
dummy.next = head;
let current = dumy;
while(current.next !== null && current.next.next !== null) {
let n1 = current.next;
let n2 = current.next.next;
current.next = n2;
n1.next = n2.next;
n2.next = n1;
current = n1;
}
return dumy.next;
};
49. 字母异位词分组
题目
思路
解法一:通过字母排序,去判断是否相同,但是时间复杂度是nlogn
解法二:用到了统计每个字母出现频率的技巧
- 检查是否为空数组
- 建立一个长度为26的数组,初始值都为0
- 遍历所有字符串,按字母的出现频率放到数组的对应位置里(利用asci码)
- 遍历数组,按照相同字母出现频率进行分组归类(使用HashMap)
- 遍历map,将结果返回
代码
// 解法二:用到了统计每个字母出现频率的技巧
var groupAnagrams = function(strs) {
if(strs.length === 0){
return [];
}
const map = new Map();
for(const str of strs){
const characters = Array(26).fill(0); //ES6中的新方法,给定长度都填充为0
for(let i=0; i<str.length; i++){
// 常用方法
const ascii = str.charCodeAt(i) - 97;
characters[ascii]++;
}
const key = characters.join(",");
if(map.has(key)){
// map.set(key, map.get(key).push(str));
map.set(key, [...map.get(key), str]); // ES6中的解构写法
} else {
map.set(key, [str]);
}
}
const result = [];
for(const arr of map) {
result.push(arr[1]); //0对应key,1对应value
}
return result;
};
// 更新解法一:通过字母排序,去判断是否相同
var groupAnagrams = function(strs) {
let [map,len] = [new Map(),strs.length]
for(let i = 0; i < len; i++){
let newStr = strs[i].split('').sort().join('')
if(map.has(newStr)){
map.get(newStr).push(strs[i])
}else{
map.set(newStr,[strs[i]])
}
}
return Array.from(map.values())
};
53. 最大子序和
题目
思路
动态规划经典题
对于数组中的每一个数字遍历到的时候都要做一个抉择:是继续加,还是从新的开始。
判断标准:是加入新的数字后原来数组的合比较大,还是这个数字它本身作为一个新数组比较大。
在过程中每次新开需要记录最大值
[-2,1,-3,4,-1,2,1,-5,4]
i = 0 -> [-2]
i = 1 -> [-2, 1] -1 < 1 -> [1]
i = 2 -> [1,-3] > -3 -> [1,-3]
i = 3 -> [1, -3, 4] < 4 -> [4]
代码
//教程写法:创建了一个动态规划数组去放每个位置上的最大值
var maxSubArray = function(nums) {
const memo = [];
memo[0] = nums[0];
let max = nums[0];
for(let i = 1; i < nums.length; i++) {
memo[i] = Math.max(nums[i] + memo[i-1], nums[i]);
max = Math.max(max, memo[i]);
}
return max;
};
//自己的写法
var maxSubArray = function(nums) {
if(nums.length === 1){
return nums[0];
}
let maxSum = nums[0];
let curSum = 0;
for(let i = 0; i<nums.length; i++) {
let newSum = curSum + nums[i];
curSum = newSum >= nums[i] ? newSum : nums[i];
maxSum = curSum > maxSum ? curSum : maxSum;
}
return maxSum;
};
54. 螺旋矩阵
题目
思路
- 如果数组为空,返回空数组
- 定义4个边界以及当前方向(顺序为右下左上)
- 当左边界小于等于右边界,且上边界小于等于下边界时,执行while循环;按照右下左上顺序,依次将路径上的字符添加到结果里
- while循环结束后,返回结果
代码
/**
* @param {number[][]} matrix
* @return {number[]}
*/
var spiralOrder = function(matrix) {
if(matrix.length == 0) {
return [];
}
let result = [];
let left = 0;
let right = matrix[0].length - 1;
let top = 0;
let bottom = matrix.length - 1;
let direction = "right";
while(left <= right && top <= bottom) {
if (direction === "right") {
for(let i = left; i <= right; i++) {
result.push(matrix[top][i]);
}
top++;
direction = "down";
} else if (direction === "down") {
for (let i = top; i <= bottom; i++) {
result.push(matrix[i][right]);
}
right--;
direction = "left";
} else if (direction === "left") {
for (let i = right; i >= left; i--) {
result.push(matrix[bottom][i]);
}
bottom--;
direction = "up";
} else if (direction === "up") {
for (let i = bottom; i >= top; i--) {
result.push(matrix[i][left]);
}
left++;
direction = "right";
}
}
return result;
};
// 自创解法(?)
/**
* @param {number[][]} matrix
* @return {number[]}
*/
var spiralOrder = function(matrix) {
if (matrix.length === 0) {
return [];
}
let turn = 0;
const m = matrix.length;
const n = matrix[0].length;
const arr = [];
let ind = 0;
let i = 0;
let j = 0;
while (ind < m * n) {
while (j < n - turn) {
arr[ind] = matrix[i][j];
j++;
ind++;
}
j--;
i++;
while (ind < m * n && i < m - turn) {
arr[ind] = matrix[i][j];
i++;
ind++;
}
i--;
j--;
while (ind < m * n && j >= turn) {
arr[ind] = matrix[i][j];
j--;
ind++;
}
j++;
i--;
while (ind < m * n && i > turn) {
arr[ind] = matrix[i][j];
i--;
ind++;
}
i++;
j++;
turn ++;
}
return arr;
};
55. 跳跃游戏
题目
思路
动态规划或者贪心算法
- 动态规划深度优先遍历 top-down
一旦某个点为死点后,那么到达那条点的路都是不通的
将所有可行的路一个个扩展开,然后去检查是否走得通
-
动态规划 bottom-up
从最后一个点开始,去找能到内容为1的点
缺点:动态规划比贪心算法速度慢 -
贪心算法
和bottom-up算法类似,但是只用一个maxJump去存index,计算前面点到这个点的最大跳跃长度是不是大于等于maxJump
代码
/**
* @param {number[]} nums
* @return {boolean}
*/
//第一种 动态规划 深度优先遍历
var canJump = function(nums) {
const totalLength = nums.length;
const memo = Array(totalLength).fill(0);
memo[totalLength-1] = 1; //最后一个点标记为可以走的路
function jump(position) {
if(memo[position] === 1){
return true;
} else if (memo[position] === -1){
return false;
}
//防止数组越界
const maxJump = Math.min(position + nums[position], totalLength-1);
for(let i = position+1; i <= maxJump; i++) {
if(jump(i)===true) {
memo[position] = 1;
return true;
}
}
memo[position] = -1;
return false;
}
return jump(0);
};
//视频老师写的bottom-up
var canJump = function(nums) {
const totalLength = nums.length;
const memo = Array(totalLength).fill(0);
//dp
memo[totalLength-1] = 1;
for(let i = totalLength-2; i >= 0; i--) {
const maxJump = Math.min(i+nums[i], totalLength-1);
for(let j = i+1; j <= maxJump; j++) {
if(memo[j]===1) {
memo[i] = 1;
break;
}
}
}
if(memo[0]===1) {
return true;
} else {
return false;
}
};
//贪心算法
var canJump = function(nums) {
let maxJump = nums.length - 1;
for(let i = nums.length-1; i >= 0; i--) {
if(i+nums[i] >= maxJump) {
maxJump = i;
}
}
return maxJump === 0;
};
//自己写的贪心算法?用了递归
var canJump = function(nums) {
function canJumpHelper(nums, lastIndex) {
if(lastIndex === 0) {
return true;
}
for (let i = 0; i < lastIndex; i++) {
if (nums[i] >= lastIndex - i) {
return canJumpHelper(nums, i);
}
}
return false;
}
return canJumpHelper(nums, nums.length - 1);
};
56. 合并区间
题目
思路
- 将数组中的区间按照起始位置排序
- 用curr数组记录当前合并的最大区间,遍历数组中的每一个区间,如果当前区间的起始位置小于curr的终点位置,则可以继续合并,并更新curr的起始和终止位置,如果当前区间的起始位置大于curr的终止位置,则无法合并,所以将curr加入到result里,并用当前的区间替换curr的值。
代码
/**
* @param {number[][]} intervals
* @return {number[][]}
*/
var merge = function(intervals) {
if(intervals.length < 2) {
return intervals;
}
intervals.sort((a, b) => a[0] - b[0]);
let result = [];
let curr = intervals[0];
for(let interval of intervals) {
if(curr[1] >= interval[0]) {
curr[1] = Math.max(curr[1], interval[1]);
} else {
result.push(curr);
curr = interval;
}
}
result.push(curr);
return result;
};
62. 不同路径
题目
思路
经典动态规划题
到达一个格子的路径条数 = 到达左边格子的路径条数 + 到达上边格子的路径条数
二维动态数组手动填充第一行和第一列,然后用第一行和第一列的值计算其他的(背包问题)
代码
/**
* @param {number} m
* @param {number} n
* @return {number}
*/
var uniquePaths = function(m, n) {
const memo = [];
//javascript中声明二维数组的方法
for(let i = 0; i < n; i++) {
memo.push([]);
}
for(let row = 0; row < n; row++) {
memo[row][0] = 1;
}
for(let column = 0; column < m; column++) {
memo[0][column] = 1;
}
for(let row = 1; row < n; row++) {
for(let col = 1; col < m; col++) {
memo[row][col] = memo[row-1][col] + memo[row][col-1];
}
}
return memo[n-1][m-1];
};
66. 加一
题目
思路
- 如果这一位不是9的话,就直接加一然后返回
- 如果是9的话,则去上一位再重复1、2步骤
代码
/**
* @param {number[]} digits
* @return {number[]}
*/
var plusOne = function(digits) {
for(let i = digits.length-1; i>=0; i--) {
if(digits[i] !== 9) {
digits[i]++;
return digits;
} else {
digits[i] = 0;
}
}
//只要代码执行到这里,就意味着所有位数都是9
const result = [1, ...digits];
return result;
};
//自己写的(结合了大数相加,但是把问题复杂了,因为是进行加一操作,只需要判断是不是9即可)
var plusOne = function(digits) {
let sum = digits[digits.length-1] + 1;
let flag = sum/10;
digits[digits.length-1] = sum%10;
let i = digits.length - 1;
while(flag === 1) {
if(i === 0) {
if(flag===1) {
digits.unshift(1);
}
break;
}
i--;
sum = digits[i] + flag;
flag = sum/10;
digits[i] = sum%10;
}
return digits;
};
70. 爬楼梯
题目
思路
动态规划,需要一个memo数组去存
到n级台阶有的爬法,等于n-1级台阶的爬法加上n-2级台阶的爬法
代码
/**
* @param {number} n
* @return {number}
*/
var climbStairs = function(n) {
const memo = [];
memo[1] = 1;
memo[2] = 2;
for(let i = 3; i <= n; i++) {
memo[i] = memo[i-2] + memo[i-1];
}
return memo[n];
};
73. 矩阵置零
题目
思路
- 检查并标记第一行和第一列是否有0(firstColHasZero和firstRowHasZero)
- 使用第一行和第一列,来标记其余行列是否含有0
- 接下来,利用第一行和第一列的标0情况,将matrix中的数字标0
- 最后,处理第一行和第一列,如果firstColHasZero为true,则将第一行全设为0,如果firstRowHasZero为true,则将第一列全设为0
特殊情况:第一行或第一列本身已经包含0
1.检查并标记第一行和第一列是否有0:
firstColHasZero = false
firstRowHasZero = true
2.使用第一行和第一列,来标记其余行列是否含有0
3.利用第一行和第一列的标0情况,将matrix中的数字标0
4.处理第一行和第一列的情况
代码
/**
* @param {number[][]} matrix
* @return {void} Do not return anything, modify matrix in-place instead.
*/
var setZeroes = function(matrix) {
let firstColHasZero = false;
let firstRowHasZero = false;
//检查第一行第一列是否有0
for(let i = 0; i < matrix.length; i++) {
if(matrix[i][0] === 0) {
firstColHasZero = true;
}
}
for(let i = 0; i < matrix[0].length; i++) {
if(matrix[0][i] === 0) {
firstRowHasZero = true;
}
}
//使用第一行和第一列,来标记其余行列是否含有0
for(let row = 1; row<matrix.length; row++) {
for(let col = 1; col<matrix[0].length; col++) {
if(matrix[row][col] === 0) {
matrix[row][0] = 0;
matrix[0][col] = 0;
}
}
}
//利用第一行和第一列,将其余数字标0
for(let row = 1; row<matrix.length; row++) {
for(let col = 1; col<matrix[0].length; col++) {
if(matrix[row][0] === 0 || matrix[0][col] === 0) {
matrix[row][col] = 0;
}
}
}
if(firstColHasZero) {
for(let i=0; i<matrix.length; i++) {
matrix[i][0] = 0;
}
}
if(firstRowHasZero) {
for(let i=0; i<matrix[0].length; i++) {
matrix[0][i] = 0;
}
}
return matrix;
};
//自己的方法,用到了ES6中的Set数据类型,将每一个为0的元素的行列数存在set中,最后根据set中的值将矩阵相应位置置零
var setZeroes = function(matrix) {
let rowSet = new Set();
let colSet = new Set();
for(let i = 0; i < matrix.length; i++) {
for(let j = 0; j < matrix[i].length; j++) {
if(matrix[i][j]===0) {
rowSet.add(i);
colSet.add(j);
}
}
}
for(let row of rowSet.keys()) {
for(let i = 0; i < matrix[row].length; i++) {
matrix[row][i] = 0;
}
}
for(let col of colSet.keys()) {
for(let i = 0; i < matrix.length; i++) {
matrix[i][col] = 0;
}
}
return matrix;
};
//set思路改进,改进第二次循环。第二次循环,只要位置横坐标或纵坐标在集合中,该位置为0
// 作者:mantoufan
// 链接:https://leetcode-cn.com/problems/set-matrix-zeroes/solution/shuang-xun-huan-6xing-dai-ma-chao-98-by-mantoufan/
// 来源:力扣(LeetCode)
var setZeroes = function(matrix, h = [new Set(), new Set()]) {
for(var i = 0; i < matrix.length; i++)
for (var j = 0; j < matrix[0].length; j++)
if (matrix[i][j] === 0) h[0].add(i), h[1].add(j)
for(var i = 0; i < matrix.length; i++)
for (var j = 0; j < matrix[0].length; j++)
if (h[0].has(i) || h[1].has(j)) matrix[i][j] = 0
};
83. 删除排序链表中的重复元素
题目
思路
比较相邻元素,然后操作指针删除掉重复元素
代码
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var deleteDuplicates = function(head) {
let current = head;
while(current && current.next) {
let next = current.next;
if(current.val === next.val) {
current.next = next.next;
next.next = null;
} else {
current = current.next;
}
}
return head;
};
92. 反转链表Ⅱ
题目
思路
- 反转m至n之间的链表
- 将反转后的链表与原链表拼接
用三个指针prev, curr, next
开始反转
将next往后挪一位,curr.next指向prev
prev往后挪一位,curr变为next的位置
将2指向反转后的新结点5,将3指向接下来的结点6
所以需要两个额外的指针占位
prev2.next = prev
curr2.next = curr
代码
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} left
* @param {number} right
* @return {ListNode}
*/
var reverseBetween = function(head, left, right) {
let dumy = new ListNode();
let prev = null;
let curr = head;
let next = head;
for(let i = 1; i < left; i++) {
prev = curr;
curr = curr.next;
}
let prev2 = prev;
let curr2 = curr;
for(let i = left; i <= right; i++) {
next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
if(prev2 !== null) {
prev2.next = prev;
}else{
head = prev;
}
curr2.next = curr;
return head;
};
121. 买卖股票的最佳时机
题目
思路
每个买入点应该是该点的左半边区域的最低点(对于每个点作为卖出点来说,想找到其最大利益的买入点,那一定是它左边区域的最低点)
代码
/**
* @param {number[]} prices
* @return {number}
*/
var maxProfit = function(prices) {
if(prices.length === 0) {
return 0;
}
let minPrice = prices[0];
let maxProfit = 0;
for (let i = 0; i < prices.length; i++) {
if(prices[i] < minPrice) {
minPrice = prices[i];
} else {
maxProfit = Math.max(maxProfit, prices[i] - minPrice);
}
}
return maxProfit;
};
122. 买卖股票的最佳时机Ⅱ
题目
思路
在每一段股票向上的趋势中,底端买入,顶端卖出
代码
/**
* @param {number[]} prices
* @return {number}
*/
var maxProfit = function(prices) {
if(prices.length === 0) {
return 0;
}
let profit = 0;
let valley = prices[0];
let peak = prices[0];
let i = 0;
while(i<prices.length-1) {
//下跌的情况,大于等于是因为可能会有停在某一个点几天(平台期)后继续下跌的情况
while(i<prices.length-1 && prices[i] >= prices[i+1]){
i++;
}
//上涨的情况
valley = prices[i];
while(i<prices.length-1 && prices[i] <= prices[i+1]){
i++;
}
peak = prices[i];
profit += peak - valley;
}
return profit;
};
//代码优化,用贪心算法:
//一天一天的比较,把每一天与下一天进行比较,如果下一天比这一天股票价格高的话,就用i+1的价格减去i天的价格,然后加到股票利润中
var maxProfit = function(prices) {
if(prices.length === 0) {
return 0;
}
let profit = 0;
for(let i = 0; i < prices.length-1; i++) {
if(prices[i+1] > prices[i]) {
profit += prices[i+1] - prices[i];
}
}
return profit;
};
123. 买卖股票的最佳时机Ⅲ
题目
思路
动态规划
横着的表示天数,纵的表示进行几次交易,格子中表示获得的最大利润
对于二维的动态规划表格:先填第一行和第一列,再根据第一行和第一列填之后的格子
对于每一天我们有两种选择:
- 当天什么都不做,那么当天的利润就延续的是前一天的利润
- 当天把股票卖掉,那么当天的价格减去前一天的价格,加上上一次交易获得的利润
对于算法进行优化:
用一个maxProfit变量存储前一天计算出来的最大利润
代码
/**
* @param {number[]} prices
* @return {number}
*/
var maxProfit = function(prices) {
if(prices.length === 0) {
return 0;
}
//定义一个三行,列数为prices.length列的二维数组
const dp = Array.from(Array(3), () => new Array(prices.length));
for(let i = 0; i<prices.length; i++) {
dp[0][i] = 0;
}
for(let i = 0; i<3; i++) {
dp[i][0] = 0;
}
for(let i = 1; i < 3; i++) {
let maxProfit = -prices[0];
for(let j = 1; j < prices.length; j++) {
dp[i][j] = Math.max(dp[i][j-1], prices[j]+maxProfit);
maxProfit = Math.max(maxProfit, dp[i-1][j] - prices[j]);
}
}
return dp[2][prices.length-1];
};
125. 验证回文串
题目
思路
- 用正则表达式去掉非数字和字母(因为js没有封装好的判断非字母和数字的方法)
- 如果字符串长度小于2,返回true
- 定义两个指针,一个在字符串开头,一个在字符串结尾
- 建立一个while循环,当left<right时执行循环,如果在任意地点s[left]!==s[right],返回false,都则left++,right–,继续执行循环
- 当循环完成后,return true
代码
/**
* @param {string} s
* @return {boolean}
*/
var isPalindrome = function(s) {
s = s.toLowerCase().replace(/[\W_]/g, "");
if(s.length < 2) {
return true;
}
let left = 0;
let right = s.length - 1;
while(left < right) {
if(s[left] !== s[right]) {
return false;
}
left++;
right--;
}
return true;
};
134. 加油站
题目
思路
- 把每个加油站油的数量加起来,为总共油的数量;把每段路程消耗的油加起来,为总共消耗油的数量;如果总油量>=总消耗量则进行下面步骤。
- 从第一个加油站开始,对每个加油站加上其中的油减去路程消耗看是否大于等于零,如果任何一个小于零,则从该点的下一个点为起点开始继续检验。
为什么中间的点不用再试:
一定是最后一个点行不通,因为到最后一个点的时候一定是大于等于零的油量去减去消耗量仍旧走不通,所以如果以0油量为起点,那当然也走不通。所以如果从中间的点再试结果是一样的。
=>所有加油站只需要试一遍,算法为O(n)
代码
/**
* @param {number[]} gas
* @param {number[]} cost
* @return {number}
*/
var canCompleteCircuit = function(gas, cost) {
let totalGas = 0;
let totalCost = 0;
for(let i = 0; i < gas.length; i++) {
totalGas += gas[i];
totalCost += cost[i];
}
if(totalGas < totalCost) {
return -1;
}
let currentGas = 0;
let start = 0;
for(let i = 0; i < gas.length; i++) {
currentGas = currentGas - cost[i] + gas[i];
if(currentGas < 0) {
currentGas = 0;
start = i + 1;
}
}
return start;
};
//自己的暴力解法,时间复杂度O(n^2)
var canCompleteCircuit = function(gas, cost) {
let gasVolumn = 0;
let start = 0;
for (let i = 0; i < gas.length; i++) {
if(gas[i] >= cost[i]) {
start = i;
gasVolumn = 0 + gas[i] - cost[i];
for(let j = 1; j <= gas.length; j++) {
let index = (i+j)%gas.length;
if(index === start){
return start;
}
gasVolumn = gasVolumn + gas[index];
if(gasVolumn - cost[index] < 0) {
break;
} else {
gasVolumn -= cost[index];
}
}
}
}
return -1;
};
141. 环形链表
题目
思路
定义两个指针,一个快指针和一个慢指针,快指针每次向前走一步, 慢指针每次向前走两步,如果快慢指针能够相遇,则表示有环形链表,否则则没有。
代码
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function(head) {
if(head === null) {
return false;
}
let slow = head;
let fast = head;
while(fast.next && fast.next.next) {
slow = slow.next;
fast = fast.next.next;
if(slow === fast) {
return true;
}
}
return false;
};
141. 环形链表Ⅱ
题目
思路
定义两个指针,一个快指针和一个慢指针,快指针每次向前走一步, 慢指针每次向前走两步,如果快慢指针能够相遇,则表示有环形链表,否则则没有。(环形链表Ⅰ中步骤)
如果相遇了,那么把快指针放回到链表的头部,快指针和慢指针都向前走一步,则两个指针相遇的点则为环的起始位置。
=>弗洛伊德算法
为什么有效:
快指针的路程是慢指针的两倍,所以入环的时候,快指针和慢指针相差一倍的路程距离(即为慢指针走的步数),此时慢指针走的步数则为起点到入环的那个点的距离。
在7-3步之后快慢指针可以相遇。
快指针和慢指针离入环点的距离都为3,所以将快指针置为链表起点后,快慢指针相遇的地方为入环点。
d-d-l = l (此时圈的周长比前面的l大)
l = nd + r
这种情况下,快指针到入环点后不一定会和慢指针相遇,它们俩会继续绕圈,绕r+nd圈
代码
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var detectCycle = function(head) {
if(head === null) {
return null;
}
let slow = head;
let fast = head;
let isCycle = false;
while(fast.next && fast.next.next) {
slow = slow.next;
fast = fast.next.next;
if(slow === fast) {
isCycle = true;
break;
}
}
if(!isCycle) {
return null;
}
fast = head;
while(slow !== fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
};
152. 乘积最大子数组
题目
思路
关键点:负数
与最大子序列和不同之处:不仅要保留最大乘积的子数组,还要保留最小乘积的子数组,比如-1,2,5,-1000,其中-1000可能会和负数相乘成为最大的乘积。
代码
/**
* @param {number[]} nums
* @return {number}
*/
var maxProduct = function(nums) {
if(nums.length < 2) {
return nums.length === 0 ? 0 : nums[0];
}
const maxProductMemo = [];
const minProductMemo = [];
maxProductMemo[0] = nums[0];
minProductMemo[0] = nums[0];
let max = nums[0];
for(let i = 1; i < nums.length; i++) {
maxProductMemo[i] = Math.max(nums[i], nums[i]*maxProductMemo[i-1], nums[i]*minProductMemo[i-1]);
minProductMemo[i] = Math.min(nums[i], nums[i]*maxProductMemo[i-1], nums[i]*minProductMemo[i-1]);
max = Math.max(max, maxProductMemo[i]);
}
return max;
};
153. 寻找旋转排序数组中的最小值
题目
思路
此时旋转数组后,左半边完全大于右半边
- 如果数组长度为1,返回唯一的一个数
- 定义两个指针,第一个left指向数组开头,第二个right指向数组结尾
- 检查数组是否被翻转,如果没有,则返回数组里的第一个数
- 当left小于right时,取中间作为mid进行二分搜索,如果mid的左边一个数大于mid,则返回mid,如果mid的右边一个数小于mid,则返回mid+1
- 否则的话,如果left所在的数小于mid,则将left右移至mid+1位置(砍掉左半边)
- 否则的话,将right左移到mid-1位置(砍掉右半边)
代码
/**
* @param {number[]} nums
* @return {number}
*/
var findMin = function(nums) {
if(nums.length === 1) {
return nums[0];
}
let left = 0;
let right = nums.length-1;
if(nums[right] > nums[left]) {
return nums[0];
}
while(left < right) {
let mid = Math.floor(left + (right-left) / 2);
if(nums[mid] > nums[mid+1]) {
return nums[mid+1];
}
if (nums[mid] < nums[mid-1]) {
return nums[mid];
}
if (nums[left] < nums[mid]) {
left = mid + 1;
} else {
right = mid -1;
}
}
};
//自己的暴力解法
var findMin = function(nums) {
if(nums.length === 1) {
return nums[0];
}
let min = nums[0];
for(let i = 1; i < nums.length; i++) {
if(nums[i] < nums[i-1]) {
min = nums[i];
break;
}
}
return min;
};
160. 相交链表
题目
思路
在两个链表的头部放置两个指针,边往后挪动边比较两个结点是否相等
当一个链表走完之后,把它的指针挪到另一个链表的开头
最终两个指针会指向同一个结点,如果两个都为空还没有相交的话,则代表两个链表不相交
如果有交点,两个指针一定会相交,因为它们走的总路程是相等的
代码
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} headA
* @param {ListNode} headB
* @return {ListNode}
*/
var getIntersectionNode = function(headA, headB) {
let n1 = headA;
let n2 = headB;
//这里n1!==n2的判断包含了不相交时,走到终点两者同时为null的情况
while(n1 !== n2) {
if(n1 === null) {
n1 = headB;
} else {
n1 = n1.next;
}
if(n2 === null) {
n2 = headA;
} else {
n2 = n2.next;
}
}
return n1;
};
187. 重复的DNA序列
题目
思路
滑动窗口题
用一个map存下每次的固定长度的字符串,value为该字符串的重复次数,每次取的时候查看map里面有没有该key,没有的话就把字符串放到map里,有的话就value加一,最后输出map中value大于一的key值。
代码
/**
* @param {string} s
* @return {string[]}
*/
var findRepeatedDnaSequences = function(s) {
const map = new Map();
const result = [];
let i = 0;
while(i+10<=s.length) {
const dna = s.substr(i, 10);
if (map.get(dna) == undefined) {
map.set(dna, 1);
} else if (map.get(dna) === 1) {
map.set(dna, 2);
result.push(dna);
} else {
map.set(dna, map.get(dna) + 1);
}
i++;
}
return result;
};
198. 打家劫舍
题目
思路
动态规划题
到每一个房子都有两种选择,第一种是偷当前的房子,同时放弃掉下一个它相邻的房子,第二种是不偷当前的房子,而去偷下一间房子,用动态规划去记录当前的金额数
memo[i] = max(memo[i-2]+i, memo[i-1])
代码
/**
* @param {number[]} nums
* @return {number}
*/
var rob = function(nums) {
// 边界条件
if(nums.length === 0) {
return 0;
}
if(nums.length === 1) {
return nums[0];
}
const memo = [];
memo[0] = nums[0];
memo[1] = Math.max(nums[0], nums[1]);
for(let i = 2; i <= nums.length; i++) {
memo[i] = Math.max(nums[i] + memo[i-2], memo[i-1]);
}
return memo[nums.length - 1];
};
//空间复杂度优化
var rob = function(nums) {
// 边界条件
if(nums.length === 0) {
return 0;
}
if(nums.length === 1) {
return nums[0];
}
let prev2 = nums[0];
let prev1 = Math.max(nums[0], nums[1]);
for(let i = 2; i < nums.length; i++) {
const temp = Math.max(nums[i] + prev2, prev1);
prev2 = prev1;
prev1 = temp;
}
return prev1;
};
200. 岛屿数量
题目
思路
dfs+沉没
遍历二维数组,遇到1的时候记录island + 1,同时把第一个岛全都沉没掉,即将1都变成0,以后每次遇到一个岛都加一,并且沉没该岛。
沉没思路:首先把当前点沉没,由1改为0,再去找它四周的点去沉没(dfs算法)
代码
/**
* @param {character[][]} grid
* @return {number}
*/
var numIslands = function(grid) {
let count = 0;
function dfs(row, col) {
if(row < 0 || row >= grid.length || col < 0 || col >= grid[0].length || grid[row][col] === "0") {
return;
}
grid[row][col] = "0";
dfs(row-1, col);
dfs(row+1, col);
dfs(row, col-1);
dfs(row, col+1);
}
for(let row = 0; row < grid.length; row++) {
for(let col = 0; col < grid[0].length; col++) {
if(grid[row][col] === "1") {
count++;
dfs(row, col);
}
}
}
return count;
};
217. 存在重复元素
题目
思路
这道题比较简单。。
代码
var containsDuplicate = function(nums) {
const map = new Map();
for(let i = 0; i < nums.length; i++) {
if(map.has(nums[i])) {
return true;
} else {
map.set(nums[i], 1);
}
}
return false;
};
219. 存在重复元素II
题目
思路
用map去存下索引,遍历的时候根据map中的值判断
代码
/**
* @param {number[]} nums
* @param {number} k
* @return {boolean}
*/
var containsNearbyDuplicate = function(nums, k) {
const map = new Map();
for(let i = 0; i < nums.length; i++) {
if(map.has(nums[i])) {
let diff = Math.abs(map.get(nums[i]) - i);
if(diff > k) {
map.set(nums[i], i);
} else {
return true;
}
} else {
map.set(nums[i], i);
}
}
return false;
};
242. 有效的字母异位词
题目
思路
自己的思路:用两个map分别存两个字符串中字符出现的个数,之后比较两个map的value是不是相同
老师的思路:
- 如果两个数组长度不一致,则返回false
- 创建一个map,用来存储每个字符出现的次数
- 对于第一个单词的每个字母,在map中将出现次数+1,对于第二个单词的字母,在map中将出现次数-1
- 遍历完后,检查map里的每一个字母出现的次数是不是0
代码
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
//自己的思路
var isAnagram = function(s, t) {
if(s.length !== t.length) {
return false;
}
const map1 = new Map();
const map2 = new Map();
for(let i = 0; i < s.length; i++) {
let char = s[i];
let char2 = t[i];
if(map1.has(char)) {
map1.set(char, map1.get(char) + 1);
} else {
map1.set(char, 1);
}
if(map2.has(char2)) {
map2.set(char2, map2.get(char2) + 1);
} else {
map2.set(char2, 1);
}
}
for(let char of map1.keys()) {
if(map2.get(char) !== map1.get(char)) {
return false;
}
}
return true;
};
// 老师的思路
var isAnagram = function(s, t) {
if(s.length !== t.length) {
return false;
}
const map = new Map();
for(let i = 0; i < s.length; i++) {
let char1 = s[i];
let char2 = t[i];
if(map.has(char1)) {
map.set(char1, map.get(char1) + 1);
} else {
map.set(char1, 1);
}
if(map.has(char2)) {
map.set(char2, map.get(char2) - 1);
} else {
map.set(char2, -1);
}
}
for(let times of map.values()) {
if(times !== 0) {
return false;
}
}
return true;
};
283. 移动零
题目
思路
去年面试阿里前端碰到的题目,挺怀念的,当时面试官给了一个很棒的解法,利用sort排序函数来解,有0的就往后排
老师的思路:
把所有非零的数移动到数组的最前面,然后在末端全部填上0,第一个指针正常遍历,第二个指针记录在哪个位置放非零数
代码
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
// 利用排序
var moveZeroes = function(nums) {
return nums.sort((a, b) => {
if(a === 0) return 1;
if(b === 0) return -1;
return 0;
})
};
// 老师的思路
var moveZeroes = function(nums) {
let j = 0;
for(let i = 0; i < nums.length; i++) {
if(nums[i] !== 0) {
nums[j] = nums[i];
j++;
}
}
for(i = j; i < nums.length; i++) {
nums[i] = 0;
}
return nums;
};