每天一道算法题(第二期)

前言

算法这个活动很严,每天必须打卡,而且不限制语言,群内已有PHP、Python、Java、Javascript等语言,欢迎大家参加,并坚持。能坚持的公众号回复算法。本公众号以JS为主,解题思路均以js来举例。

上周回顾:
1、两个数组的交集 II
2、1比特与2比特字符
3、二进制求和
4、两数之和
5、加一

爬楼梯

假设你正在爬楼梯。需要 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 阶
    

题解:

  • 斐波那契数列公式法

    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];
    };
    
  • 数学公式法

    image

    执行用时: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的值;
    执行用时: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
    };
    
  • 有序互斥法
    先排序,然后利用相邻不想等来找出答案;
    执行用时: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];
      }
    };
    
  • 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);  
     }
    
  • 左右索引法
    从左边检索和右边检索,如果相等,就找到结果;
    执行用时: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];
        }
      }
    
  • 循环减法
    从拷贝一个数组,定义一个变量,在循环中,动态修改数组,如果拷贝数组中存在当前值,就相减,否则相加;(这是最笨的方法)
    执行用时: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
    }
    
  • 字符串分割法(正则思想)
    将数组转换成字符串,然后遍历使用每一项的值去分割字符串,若分割后数组长度为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.     111221

1 被读作 "one 1" ("一个一") , 即 11

11 被读作 "two 1s" ("两个一"), 即 21

21 被读作 "one 2", "one 1"("一个二" , "一个一") , 即 1211

给定一个正整数n(1 ≤ n ≤ 30),输出报数序列的第 n 项。
注意:整数顺序将表示为一个字符串。

  • 示例 1:
    输入: 1
    输出: "1"
    
  • 示例 2:
    输入: 4
    输出: "1211"
    

题解:

  • 对象法
    用一个对象来存储报数字符串,分两种情况处理,当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]
    }
    
  • 正则法
    先观察规律,显然每一阶数中任意部分的元素一次最多连续不会超过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;
    };
    
  • 递归法
    先从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
    

题解:

  • 字符串分割法
    分割成数组,然后取最后一个;
    执行用时: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。
    
题解:
  • 递归
    分割成字符串数组,然后递归,当小于10,返回结果,大于等于则递归;
    执行用时:104ms;内存消耗:36MB;

    ar 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
    }
    
  • 条件法
    有循环就有条件,看见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
    }
    
  • 模除法
    有如下关系: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
    };
    

第二期结束,下期见。如果有一起学习的朋友,可以加微信群。


image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容