如果每天做一道算法题,那是不是每天都在进步?
前言
这个活动是从2019年7月中旬开始的,人数不算多,也就几个好友和好友的好友,过程中也会有人因为工作的缘故或其他原因放弃,或许未来还会有人离开。
活动的主要形式就是在leetcode刷题,每个工作日一道题,每周做总结,目前已经是第十三期,接下来我会把每期的题做一个总结,由于我是侧重javascript,所以活动中的每道题都是以js语言来实现解题方法。
活动的规则比较严谨,群里每天早上10点之前发题,晚上10点审核,审核有管理员专门审核,称为打卡,没有打卡的人,需要发红包给管理员作为每天统计费用。
活动的目的就是培养算法思维,了解常见的算法,比如分治算法、贪心算法、动态优化等等。
微信公众号惊天码盗同步。
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶题解:
思路1:斐波那契数列公式法
dp[n] = dp[n-1] + dp[n-2]
执行用时:64ms;内存消耗:34.2MB;
var climbStairs = function(n) {
const dp = [];
dp[0] = 1;
dp[1] = 1;
for(let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
};
思路2:数学公式法
执行用时:72ms;内存消耗:33.7MB;
var climbStairs = function(n) {
const sqrt_5 = Math.sqrt(5);
const fib_n = Math.pow((1 + sqrt_5) / 2, n + 1) -Math.pow((1 - sqrt_5) / 2,n + 1);
return Math.round(fib_n / sqrt_5);
};
只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。你可以不使用额外空间来实现吗?
示例 1:
输入: [2,2,1]
输出: 1
示例 2:
输入: [4,1,2,1,2]
输出: 4
题解:
思路1:对象计数法
用一个对象来存储出现的次数,然后取出次数为1的值;
执行用时:116ms;内存消耗:37.5MB;
var singleNumber = function(nums) {
let obj={};
let result=null;
nums.forEach(item=>{
if(!obj[item]){
obj[item]=1
}else{
obj[item]++
}
})
Object.keys(obj).forEach(item=>{
if(obj[item]===1){
result=item-0
}
})
return result
};
思路2:有序互斥法
先排序,然后利用相邻不想等来找出答案;
执行用时:156ms;内存消耗:36.9MB;
var singleNumber = function(nums) {
nums = nums.sort();
for (let i = 0; i < nums.length; i++) {
if (nums[i] !== nums[i - 1] && nums[i] !== nums[i + 1])
return nums[i];
}
};
思路3:XOR法(异或法)
第一次看到这种解法,也从侧面体现了对js位运算的不熟悉,XOR位运算,如果你了解它的含义,就不怀疑为什么可以解决此题了,XOR位运算会先转化为二进制,如果两位只有一位为 1 则设置每位为 1,最后转化回来;
执行用时:72ms;内存消耗:35.8MB;
var singleNumber = function(nums) {
let gg = function(total,num){
return total ^ num;
}
return nums.reduce(gg);
}
思路4:左右索引法
从左边检索和右边检索,如果相等,就找到结果;
执行用时:536ms;内存消耗:35.2MB;
var singleNumber = function(nums) {
for (let i = 0; i < nums.length; i++) {
if (nums.lastIndexOf(nums[i]) === nums.indexOf(nums[i]))
return nums[i];
}
}
思路5:循环减法
从拷贝一个数组,定义一个变量,在循环中,动态修改数组,如果拷贝数组中存在当前值,就相减,否则相加;(这是最笨的方法)
执行用时:1228ms;内存消耗:37.6MB;
var singleNumber = function(nums) {
let num=0;
let list=[...nums]
nums.forEach((item,i)=>{
list.splice(i,1,null);
if(list.includes(item)){
num-=item
}else{
num+=item
}
})
return num
}
思路6:字符串分割法(正则思想)
将数组转换成字符串,然后遍历使用每一项的值去分割字符串,若分割后数组长度为2则该项只出现一次。
执行用时:9460ms;内存消耗:41.9MB;
var singleNumber = function(nums) {
const numsStr = `/${nums.join('//')}/`;
for (let i = 0; i < nums.length; i++) {
if (numsStr.split(`/${nums[i]}/`).length === 2) return nums[i];
}
}
报数
报数序列是一个整数序列,按照其中的整数的顺序进行报数,得到下一个数。其前五项如下:
1. 1
2. 11
3. 21
4. 1211
5. 1112211 被读作 "one 1" ("一个一") , 即 11。
11 被读作 "two 1s" ("两个一"), 即 21。
21 被读作 "one 2", "one 1" ("一个二" , "一个一") , 即 1211。
给定一个正整数 n(1 ≤ n ≤ 30),输出报数序列的第 n 项。
注意:整数顺序将表示为一个字符串。
示例 1:
输入: 1
输出: "1"示例 2:
输入: 4
输出: "1211"题解:
思路1:对象法
用一个对象来存储报数字符串,分两种情况处理,当key为1和当key>1;当key>1的时候,获取到key-1的字符串,转化为数组,在利用对象来存储此数组中数字出现的次数,但此时会遇到一个问题,像111221这样前面的1和后面的1不同,所以我们存储时要区分开;这里用num做了区分,当值不相同的时候num再赋值。
ob中存的是每个字符串解读的对象,比如4的报数“1211”,5的报数是解读1211的字符串,所以ob中存的是
{
0:{count:1,value:1},
1:{count:1,value:2},
2:{count:2,value:1}
}//拼接之后就是111221
obj的每一个key值就是n,value就是报数字符串;
执行用时:136ms;内存消耗:36.9MB;
var countAndSay = function(n) {
let obj={}
for(let i=1;i<=n;i++){
obj[i]='';
if(i==1){
obj[i]+=1+''
}
if(i>1){
let num=null;
let ob={};
let list=obj[i-1].split('');
let flag=false;
list.forEach((item,index)=>{
if(index==0){
num=0;
ob[index]={
value:item,
count:1
}
}
if(index>0){
if(ob[num]&&ob[num].value===item){
ob[num].count&&ob[num].count++
}else{
num=index
ob[index]={
value:item,
count:1
}
}
}
})
for(let k in ob){
obj[i]+=ob[k].count+''+ob[k].value
}
}
}
return obj[n]
}
思路2:正则法
先观察规律,显然每一阶数中任意部分的元素一次最多连续不会超过3次,所以任一阶数的组成元素最多只有1、2、3。所以我直接使用正则/(1+)|(2+)|(3+)/g来匹配字符串即可。
执行用时:80ms;内存消耗:34.8MB;
var countAndSay = function(n) {
let str = '1';
for (let i = 0; i < n - 1; i++) {
str = str.match(/(1+)|(2+)|(3+)/g).reduce(
(pre, cur) => pre + cur.length + cur[0]
, '');
}
return str;
};
思路3:递归法
先从1到n这个过程中,通过数组来存储当前结果,所以我们的结构可以是这样fn(index,['...'],n),然后当index==n的时候设立递归终止条件;
执行用时:124ms;内存消耗:35.1MB;
var countAndSay = function(n) {
const createStr = function (index, str, n) {
//终止条件:查询到了第n个数了,立即返回,否则index+1
if(index == n) return str.join('') ;
index++
let newChar = []
let k = 1//保存该数存在次数:当查询到不等的时候,在下方重置k
for(let j = 0; j < str.length; j++) {
let char = str[j]
if(char == str[j+1] && j != str.length - 1) {
//不等,且遍历没到底,那就继续寻找
k++
}else {
newChar.push(k)
newChar.push(str[j])
k=1
}
}
return createStr(index, newChar, n)
}
return createStr(1, ['1'], n)
};
最后一个单词的长度
给定一个仅包含大小写字母和空格 ' ' 的字符串,返回其最后一个单词的长度。
如果不存在最后一个单词,请返回 0 。
说明:一个单词是指由字母组成,但不包含任何空格的字符串。
示例:
输入: "Hello World"
输出: 5
题解:
思路1:字符串分割法
分割成数组,然后取最后一个;
执行用时:68ms;内存消耗:33.7MB;
var lengthOfLastWord = function(s) {
let list=s.trim().split(' ')
return list[list.length-1].length
}
当然你也可以用正则;
执行用时:76ms;内存消耗:33.5MB;
var lengthOfLastWord = function(s) {
s = s.replace(/(\s*$)/g, "")
let arr = s.split(' ')
return arr[arr.length -1].length
}
各位相加
给定一个非负整数 num,反复将各个位上的数字相加,直到结果为一位数。
示例:
输入: 38
输出: 2
解释: 各位相加的过程为:3 + 8 = 11, 1 + 1 = 2。由于 2 是一位数,所以返回 2。
题解:
思路1:递归法
分割成字符串数组,然后递归,当小于10,返回结果,大于等于则递归;
执行用时:104ms;内存消耗:36MB;
var addDigits = function(num) {
let val=0;
let getGe=(n)=>{
const list=(n+'').split('');
let res=0;
list.forEach(item=>{
res+=item-0
})
if(res>=10){
getGe(res)
}
if(res<10){
val= res
}
}
getGe(num)
return val
}
思路2:条件法
有循环就有条件,看见for就想到while;
执行用时:100ms;内存消耗:36.2MB;
var addDigits = function(num) {
let Digit=num;
while(Digit>=10){
Digit=Digit+'';
let list=[...Digit];
Digit=list.reduce((a,b)=>(a-0)+(b-0))
}
return Digit
}
思路3:模除法
有如下关系:num = a * 10000 + b * 1000 + c * 100 + d * 10 + e
即:num = (a + b + c + d + e) + (a * 9999 + b * 999 + c * 99 + d * 9)
这道题最后的目标,就是不断将各位相加,相加到最后,当结果小于10时返回。因为最后结果在1-9之间,得到9之后将不会再对各位进行相加,因此不会出现结果为0的情况。因为 (x + y) % z = (x % z + y % z) % z,又因为 x % z % z = x % z,因此结果为 (num - 1) % 9 + 1,只模除9一次,并将模除后的结果加一返回。北风其凉 --OSCHINA
但是有1个关键的问题,如果num是9的倍数,那么就不适用上述逻辑。原本我是想得到n被打包成10个1份的份数+打不进10个1份的散落个数的和。通过与9取模,去获得那个不能整除的1,作为计算份数的方式,但是如果可以被9整除,我就无法得到那个1,也得不到个位上的数。
可以这么做的原因:原本可以被完美分成9个为一份的n样物品,我故意去掉一个,那么就又可以回到上述逻辑中去得到我要的n被打包成10个一份的份数+打不进10个一份的散落个数的和。而这个减去的1就相当于从,在10个1份打包的时候散落的个数中借走的,本来就不影响原来10个1份打包的份数,先拿走再放回来,都只影响散落的个数,所以没有关系。liveforexperience --力扣(LeetCode)
这是一种数学思维,也是算法中常见的思维方式。
执行用时:100ms;内存消耗:35.4MB;
var addDigits = function(num) {
return (num-1)%9+1
};第二期结束,下期见。如果有一起学习的朋友,可以加微信群。
关注公众号「惊天码盗」,回复算法,拉你进群。