标题
力扣算法
文章目录
1、贩卖柠檬水,能否正确找零
题目描述在柠檬水摊上,每一杯柠檬水的售价为 5 美元。
顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
示例 1:
输入:[5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。
错误
本来想用出入栈来实现,奈何想的太过复杂,花费大量时间,还是得不到正确答案,后来使用官方的解答改成js形式实现了
思路一:
如果为5美元,则five+1;
如果为10美元,判断five是否大于0是的话ten+1;five-1否则return false;
如果为20美元,判断是否有five和ten是否大于0,是的话,ten-1;five-1;否则判断five是否大于2;是的话five-3;否则return false;
循环结束,return true
解法一
var lemonadeChange = function (bills) {
let five=0,
ten=0;
for (let i = 0; i < bills.length; i++) {
if(bills[i]==5){
five++;
}else if(bills[i]==10){
if(five==0){
return false
}
ten++;
five--;
}else{
if(ten>0&&five>0){
ten--;
five--;
}else if(five>=3){
five-=3;
}else{
return false;
}
}
}
return true;
};
缺点:耗时太长
解法二(大佬解法)
方案1:先找零,再比较。不使用逗号,运算符。n5增加时continue不比较n5 < 0
var lemonadeChange = function(bills) {
var i = -1
var n5 = 0
var n10 = 0
while(++i < bills.length) {
if (bills[i] === 5) {
n5++
continue
} else if (bills[i] === 10) {
n5--
n10++
} else if (n10) {
n5--
n10--
} else {
n5 -= 3
}
if (n5 < 0) {
return false
}
}
return true
};
2、返回nums中和为target的两个数的下标
题目描述:给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
解法
var twoSum = function(nums, target) {
let arr=[];
for(let i=0;i<nums.length;i++){
for(let j=i+1;j<nums.length;j++){
if((nums[i]+nums[j])===target){
// arr.push(i);
// arr.push(j);
return arr=[i,j];
// break
}
}
}
// return arr;
};
3、两数相加
题目描述:给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。
如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例:
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
思路与算法
由于输入的两个链表都是逆序存储数字的位数的,因此两个链表中同一位置的数字可以直接相加。
我们同时遍历两个链表,逐位计算它们的和,并与当前位置的进位值相加。具体而言,如果当前两个链表处相应位置的数字为 n1,n2,进位值为carry,则它们的和为 n1+n2+carry;其中,答案链表处相应位置的数字为
(
n
1
+
n
2
+
carry
)
%
10
(n1+n2+\textit{carry}) \% 10
(n1+n2+carry)%10
而新的进位值为
∣
n
1
+
n
2
+
carry
10
∣
|\frac{n1+n2+\textit{carry}}{10}|
∣10n1+n2+carry∣
如果两个链表的长度不同,则可以认为长度短的链表的后面有若干个 0。
此外,如果链表遍历结束后,有 carry>0,还需要在答案链表的后面附加一个节点,节点的值为 carry。
解法
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
var addTwoNumbers = function(l1, l2) {
let head = null, tail = null;
let carry = 0;
while (l1 || l2) {
const n1 = l1 ? l1.val : 0;
const n2 = l2 ? l2.val : 0;
const sum = n1 + n2 + carry;
if (!head) {
head = tail = new ListNode(sum % 10);
} else {
tail.next = new ListNode(sum % 10);
tail = tail.next;
}
carry = Math.floor(sum / 10);
if (l1) {
l1 = l1.next;
}
if (l2) {
l2 = l2.next;
}
}
if (carry > 0) {
tail.next = new ListNode(carry);
}
return head;
};
4、无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
找到数组中最大值的方法
1.es6拓展运算符...
Math.max(...arr)
2.es5 apply(与方法1原理相同)
Math.max.apply(null,arr)
3.for循环
let max = arr[0];
for (let i = 0; i < arr.length - 1; i++) {
max = max < arr[i+1] ? arr[i+1] : max
}
4.数组sort()
let arr1=arr.sort((num1, num2) => {
return num1 - num2 < 0
})[0]
5.数组reduce
arr.reduce((num1, num2) => {
return num1 > num2 ? num1 : num2}
)
自己的解法:对js方法还不是很属性,很多想不到的地方
修改多次,很多因素没有考虑到,现在也存在问题,当开头时空格时,是undefined
var lengthOfLongestSubstring = function (s) {
let arr = s.split('');
let k = 0;
let arr1 = [];
if (s == '') {//当为空时
return 0;
} else if (s == ' ') {//当为空格时
return 1;
} else {
arr1.push(1);//当只有一个字符时
for (let i = 1; i < arr.length; i++) {
let long = 1;
for (let j = k; j < i; j++) {
if (arr[i] != arr[j]) {
long++;
} else {
k = j + 1;
break;
}
}
arr1.push(long);
}
}
// return Math.max(...arr1);//最省时间
// return Math.max.apply(null, arr1);
// let max=arr1[0];
// arr1.forEach(item=>{//占内存最小
// max=max<item?item:max;
// })
// return max;
arr1.sort((a, b) => {
return b - a;
})
return arr1[0];
};
大佬解法
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
// 滑动窗口初始化为一个空数组
let arr = [];
// 要返回的字符串的长度
let max = 0;
for (let i = 0; i < s.length; i++) {
// 使用 indexOf 判断是否在数组中出现过
let index = arr.indexOf(s[i])
// 如果出现过
if (index !== -1) {
// 从数组开头到当前字符串全部截取掉
arr.splice(0, index + 1);
}
// 在窗口右边放进新的字符
arr.push(s.charAt(i));//charAt(i)返回指定位置的字符
// 更新下最大值
max = Math.max(arr.length, max);//两个数比较
}
// 返回
return max;
};
大佬解法二(哈希)未看懂
var lengthOfLongestSubstring = function (s) {
const map = {};
let l = 0,
r = 0,
max = 0;
while (r < s.length) {
const pos = map[s[r]];
// 如果 s[r] 曾在 [l, r] 滑动窗口中出现
// 就收缩滑动窗口左侧,把 l 指针移动到 s[r] 上次出现的位置 + 1
if (pos >= l && pos <= r) l = pos + 1;
// 更新 s[r] 出现的位置
map[s[r]] = r;
// 计算滑动窗口大小
max = Math.max(max, r - l + 1);
// 滑动窗口继续右移扩张
r++;
}
return max;
};
5、寻找两个正序数组的中位数
暴力解法
var findMedianSortedArrays = function(nums1, nums2) {
let num = nums1.concat(nums2);
num = num.sort((a,b) => a - b);
let length = num.length;
if (length > 0 && length % 2 === 0) {
return (num[length / 2 - 1] + num[length / 2]) / 2;
} else {
return num[(length - 1) / 2];
}
};
二分查找
大佬思维 作者:6xiaodi
优化:二分查找解决
解题思路
合并后的数组可以分为两部分,即中位数的左边和右边
合并后的数组左边(不包括中位数)的长度很容易求出,就是(Merged+1) / 2,再取整
假设Nums1数组partlen1和Nums2数组partlen2的和为合并后的左边部分,再假设Nums1左分段为l1,右分段为r1,Nums2左分段为l2,右分段为r2。
如果Merged是偶数,则两个中位数为Max(l1,l2),Min(r1,r2)。
如果Merged是奇数,则中位数为Max(l1,l2)或 Min(r1,r2)。
如何找到合适的partlen1呢?
l1 < r1
l2 < r2
l1 < r2
l2 < r1
对Nums1进行二分查找,找到合适的partlen1。
解题步骤
对长度短的数组进行二分查找
求出两个数组的总长度
进行二分查找
partlen1为二分查找的后中间的数
partlen2为总长度len 加1除2后减去 partlen1
求出l1、l2、r1、r2
注意一定考虑极端情况,假如 partlen1为0,l1就不存在,设置为负无穷大,否则为Nums1[partlen1 - 1],同样的partlen2也同理
另外一种极端的情况,partlen1完全等于Nums1,则合并后的右分区完全来自Nums2,r1就不存在设置为无穷大,否则为Nums1[partLen1],同样的partlen2也同理
判断结果:先判断不符合条件的 1、 l1 > r2 说明Num1给大了,end指针往前移动 2、l2 > r1 说明Num1给小了,start指针往后移动 3、其余均是符合条件,直接根据奇偶性返回结果即可。
解法
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number}
*/
var findMedianSortedArrays = function(nums1, nums2) {
let len1 = nums1.length;
let len2 = nums2.length;
// 对长度短的数组进行二分查找
if (len1 > len2) {
return findMedianSortedArrays(nums2, nums1);
}
let len = len1 + len2;//两个字符串总长度
let start = 0;//开始位置
let end = len1;//结束位置
// 两个数组左分段的长度
let partLen1 = 0;
let partLen2 = 0;
while (start <= end) {
// >>右移一位:有效位整体右移,再使用符号位的值填充空位。移动过程中超出的值将被丢弃。
//在这里的意思相当于Math.floor((start + end)/2)
partLen1 = (start + end) >> 1;
partLen2 = ((len + 1) >> 1) - partLen1;
let l1 = partLen1 === 0? -Infinity : nums1[partLen1 - 1];
let l2 = partLen2 === 0? -Infinity : nums2[partLen2 - 1];
let r1 = partLen1 === len1 ? Infinity : nums1[partLen1];
let r2 = partLen2 === len2 ? Infinity : nums2[partLen2];
if (l1 > r2) {
end = partLen1 - 1;
} else if (l2 > r1) {
start = partLen1 + 1;
} else { // 满足条件的情况: l1 <= r2 && l2 <= r1
return len % 2 === 0 ?
(Math.max(l1, l2) + Math.min(r1, r2) ) / 2 :
Math.max(l1, l2)
}
}
};
6、给定一个字符串 s
,找到 s
中最长的回文子串。
自己有思路,但是不知道怎么写,看了大佬的,原来很简单
思路:
先找出s的所有字串,判断这些字串是不是回文,如果是且长度更长则记录下来,最后return s.substr(begin,maxlen);
暴力解法
/**
* @param {string} s
* @return {string}
*/
var longestPalindrome = function (s) {
let charArray=s.split('');
let maxlen=1;
let begin=0;
for(let i=0;i<s.length;i++){
for(let j=i+1;j<s.length;j++){
if(j-i+1>maxlen&&isPlalindrome(charArray,i,j)){
maxlen=j-i+1;
begin=i
}
}
}
return s.substr(begin,maxlen);
};
var isPlalindrome = function (charArray, left, right) {
while (left < right) {
if (charArray[left] != charArray[right]) {
return false;
}
left++;
right--;
}
return true;
}
大佬解法(动态规划)
动态规划
特别说明:
以下「动态规划」的解释只帮助大家了解「动态规划」问题的基本思想;
「动态规划」问题可以难到非常难,在学习的时候建议不要钻到特别难的问题中去;
掌握经典的动态规划问题的解法,理解状态的定义的由来、会列出状态转移方程;
然后再配合适当难度的问题的练习;
有时间和感兴趣的话可以做一些不太常见的类型的问题,拓宽视野;
「动态规划」讲得比较好的经典书籍是《算法导论》。
提示:右键「在新便签页打开图片」可查看大图。
1、思考状态(重点)
状态的定义,先尝试「题目问什么,就把什么设置为状态」;
然后思考「状态如何转移」,如果「状态转移方程」不容易得到,尝试修改定义,目的依然是为了方便得到「状态转移方程」。
「状态转移方程」是原始问题的不同规模的子问题的联系。即大问题的最优解如何由小问题的最优解得到。
2、思考状态转移方程(核心、难点)
状态转移方程是非常重要的,是动态规划的核心,也是难点;
常见的推导技巧是:分类讨论。即:对状态空间进行分类;
归纳「状态转移方程」是一个很灵活的事情,通常是具体问题具体分析;
除了掌握经典的动态规划问题以外,还需要多做题;
如果是针对面试,请自行把握难度。掌握常见问题的动态规划解法,理解动态规划解决问题,是从一个小规模问题出发,逐步得到大问题的解,并记录中间过程;
「动态规划」方法依然是「空间换时间」思想的体现,常见的解决问题的过程很像在「填表」。
3、思考初始化
初始化是非常重要的,一步错,步步错。初始化状态一定要设置对,才可能得到正确的结果。
角度 1:直接从状态的语义出发;
角度 2:如果状态的语义不好思考,就考虑「状态转移方程」的边界需要什么样初始化的条件;
角度 3:从「状态转移方程」方程的下标看是否需要多设置一行、一列表示「哨兵」(sentinel),这样可以避免一些特殊情况的讨论。
4、思考输出
有些时候是最后一个状态,有些时候可能会综合之前所有计算过的状态。
5、思考优化空间(也可以叫做表格复用)
「优化空间」会使得代码难于理解,且是的「状态」丢失原来的语义,初学的时候可以不一步到位。先把代码写正确是更重要;
「优化空间」在有一种情况下是很有必要的,那就是状态空间非常庞大的时候(处理海量数据),此时空间不够用,就必须「优化空间」;
非常经典的「优化空间」的典型问题是「0-1 背包」问题和「完全背包」问题。
(下面是这道问题「动态规划」方法的分析)
这道题比较烦人的是判断回文子串。因此需要一种能够快速判断原字符串的所有子串是否是回文子串的方法,于是想到了「动态规划」。
「动态规划」的一个关键的步骤是想清楚「状态如何转移」。事实上,「回文」天然具有「状态转移」性质。
一个回文去掉两头以后,剩下的部分依然是回文(这里暂不讨论边界情况);
依然从回文串的定义展开讨论:
-
如果一个字符串的头尾两个字符都不相等,那么这个字符串一定不是回文串;
-
如果一个字符串的头尾两个字符相等,才有必要继续判断下去。
- 如果里面的子串是回文,整体就是回文串;
- 如果里面的子串不是回文串,整体就不是回文串。
即:在头尾字符相等的情况下,里面子串的回文性质据定了整个子串的回文性质,这就是状态转移。因此可以把「状态」定义为原字符串的一个子串是否为回文子串。
第 1 步:定义状态
dp[i][j] 表示子串 s[i…j] 是否为回文子串,这里子串 s[i…j] 定义为左闭右闭区间,可以取到 s[i] 和 s[j]。
第 2 步:思考状态转移方程
在这一步分类讨论(根据头尾字符是否相等),根据上面的分析得到:
dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
说明:
「动态规划」事实上是在填一张二维表格,由于构成子串,因此 i 和 j 的关系是 i <= j ,因此,只需要填这张表格对角线以上的部分。
看到 dp[i + 1][j - 1] 就得考虑边界情况。
边界条件是:表达式 [i + 1, j - 1] 不构成区间,即长度严格小于 2,即 j - 1 - (i + 1) + 1 < 2 ,整理得 j - i < 3。
这个结论很显然:j - i < 3 等价于 j - i + 1 < 4,即当子串 s[i…j] 的长度等于 2 或者等于 3 的时候,其实只需要判断一下头尾两个字符是否相等就可以直接下结论了。
如果子串 s[i + 1…j - 1] 只有 1 个字符,即去掉两头,剩下中间部分只有 11 个字符,显然是回文;
如果子串 s[i + 1…j - 1] 为空串,那么子串 s[i, j] 一定是回文子串。
因此,在 s[i] == s[j] 成立和 j - i < 3 的前提下,直接可以下结论,dp[i][j] = true,否则才执行状态转移。
第 3 步:考虑初始化
初始化的时候,单个字符一定是回文串,因此把对角线先初始化为 true,即 dp[i][i] = true 。
事实上,初始化的部分都可以省去。因为只有一个字符的时候一定是回文,dp[i][i] 根本不会被其它状态值所参考。
第 4 步:考虑输出
只要一得到 dp[i][j] = true,就记录子串的长度和起始位置,没有必要截取,这是因为截取字符串也要消耗性能,记录此时的回文子串的「起始位置」和「回文长度」即可。
第 5 步:考虑优化空间
因为在填表的过程中,只参考了左下方的数值。事实上可以优化,但是增加了代码编写和理解的难度,丢失可读和可解释性。在这里不优化空间。
注意事项:总是先得到小子串的回文判定,然后大子串才能参考小子串的判断结果,即填表顺序很重要。
大家能够可以自己动手,画一下表格,相信会对「动态规划」作为一种「表格法」有一个更好的理解。
参考代码 :未明白
public class Solution {
public String longestPalindrome(String s) {
// 特判
int len = s.length();
if (len < 2) {
return s;
}
int maxLen = 1;
int begin = 0;
// dp[i][j] 表示 s[i, j] 是否是回文串
boolean[][] dp = new boolean[len][len];
char[] charArray = s.toCharArray();
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
for (int j = 1; j < len; j++) {
for (int i = 0; i < j; i++) {
if (charArray[i] != charArray[j]) {
dp[i][j] = false;
} else {
if (j - i < 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}
// 只要 dp[i][j] == true 成立,就表示子串 s[i..j] 是回文,此时记录回文长度和起始位置
if (dp[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1;
begin = i;
}
}
}
return s.substring(begin, begin + maxLen);
}
}
7、Z字型变换
将一个给定字符串根据给定的行数,以从上往下、从左到右进行 Z 字形排列。
比如输入字符串为 “LEETCODEISHIRING” 行数为 3 时,排列如下:
L C I R
E T O E S I I G
E D H N
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:“LCIRETOESIIGEDHN”。
思路:
建立一个二维数组,把对应的字符插入到二维数组中,再拼接起来
步骤:
- 先建立一个一维数组,把规律放进去
- 创建一个长度为numRows的二维数组
- 遍历字符串,把对应的字符插入到二维数组中
- 拼接数组并将其转为字符串返回
难点:
不知道怎么把对应规律和二维数组联系起来
缺点:
时间、空间占用大,思路复杂
我的代码
/**
* @param {string} s
* @param {number} numRows
* @return {string}
*/
var convert = function (s, numRows) {
let arr = [],
num = 0,
s1 = '',
flag = 0,
left = 0,
right = 2 * numRows - 2;
while (left <= right) {
arr[left] = arr[right] = num;
num++;
left++;
right--;
}
let arr1 = new Array();
for (let j = 0; j < numRows; j++) {
arr1[j] = new Array();
}
for (let i = 0; i < s.length; i++) {
arr1[arr[flag]].push(s.charAt(i));
if (flag < 2 * numRows - 3) {
flag++;
} else {
flag = 0;
}
}
let arr2 = [];
for (let i = 0; i < arr1.length; i++) {
arr2 = arr2.concat(arr1[i]);
}
s1 = arr2.join('');
return s1;
};
大佬解法:(我的思路和大佬一样,但是,我想不到这么好的实现方法)
/**
* @param {string} s
* @param {number} numRows
* @return {string}
*/
var convert = function (s, numRows) {
if (numRows == 1)
return s;
const len = Math.min(s.length, numRows);
const rows = [];
for (let i = 0; i < len; i++) rows[i] = "";
let loc = 0;
let down = false;
for (const c of s) {
rows[loc] += c;
if (loc == 0 || loc == numRows - 1)
down = !down;
loc += down ? 1 : -1;
}
let ans = "";
for (const row of rows) {
ans += row;
}
return ans;
};
8、给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。
大佬解法:
/**
* @param {number} x
* @return {number}
*/
var reverse = function(x) {
let result = 0;
while(x !== 0) {
result = result * 10 + x % 10;
x = (x / 10) | 0;
}
return (result | 0) === result ? result : 0;//如果有溢出则与原来不相等(|位或运算符)
};
9、字符串转换整数 (atoi)
正则解法
/**
* @param {string} s
* @return {number}
*/
var myAtoi = function(s) {
const re = new RegExp(/^(-|\+)?\d+/);
let str = s.trim().match(re);
let res = str ? Number(str[0]) : 0;
return res >= 0 ? Math.min(res, 2**31 - 1) : Math.max(res, -(2**31))
};
本人解法
/**
* @param {string} s
* @return {number}
*/
var myAtoi = function (s) {
let num = '';
let reg = /\d+/;
let flag = 0;
if (!reg.test(s)||s.length == 0) {
return 0;
}
for (let i = 0; i < s.length; i++) {
if (s[i] !== ' ') {
if (s[i] == '+' || s[i] == '-') {
num += s[i];
if (!reg.test(s[i + 1])) {
return 0
}
} else if (reg.test(s[i])) {
num += s[i];
if (!reg.test(s[i + 1])) {
break;
}
} else {
return 0
}
}
}
num = parseInt(num);
num = num >= Math.pow(2, 31) ? Math.pow(2, 31) - 1 : num;
num = num <= -Math.pow(2, 31) ? -Math.pow(2, 31) : num;
return num;
};
10、判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
本人解法(转为字符后判断)
/**
* @param {number} x
* @return {boolean}
*/
var isPalindrome = function(x) {
if(x<0){
return false
}
let s=x+'',
left=0,
right=s.length-1;
while(left<right){
if(s[left]!==s[right]){
return false
}
left++;
right--;
}
return true;
};
本人解法(数字翻转后判断与原来是否相等)
/**
* @param {number} x
* @return {boolean}
*/
var isPalindrome = function(x) {
if(x<0){
return false
}
let num=0;
let s=x;
while(s){
num=num*10+s%10;
s=s/10|0;
}
return num===x;
};
官方解法(类似数字翻转,不过只翻转一半翻转)
/**
* @param {number} x
* @return {boolean}
*/
var isPalindrome = function (x) {
if (x < 0||x%10==0&&x!=0) {
return false
}
let num = 0;
while (x>num) {
num = num * 10 + x % 10;
x = Math.floor(x / 10);
}
return num===x||Math.floor(num/10)===x;
};
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lemonade-change
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。