字符串(String)篇——part1

野望

part1:翻转整数

image.png

心路历程:

1.理解题意:整数、32位、有符号、反转每位数字
2.看懂示例:以上三个示例分别描述了反转、保留符号、去零(如果最高位为0则只保留其他字符)。
3.注意:描述了取值范围,超出范围则返回0

尝试:

1.第一步:判断

是否为整数?是否超出长度范围?是否包含符号位?

2.第二步:翻转
3.第三步:处理

是否最高位为0?是否需要拼接符号位?

代码如下:

/**
 * @description: 翻转整数
 * @params {number} str - 传入的整数
 * @return {number} 翻转后的整数
 */
function reverse(str) {
     let symbol = ""
     if (typeof str === "number" && Number.isInteger(str)) {
        if (str.toString().length > 32 !== true) {
            if (str.toString()[0] === "-") {
                symbol = str.toString()[0]
            } 
            // 翻转和处理
            let numArr, newStr
            if (symbol) {
                numArr = [].slice.call(str.toString().slice(1)).reverse()
            } else {
                numArr = [].slice.call(str.toString()).reverse()
            } 
            newStr = numArr.join("")
            for (let i = 0; i < newStr.length; i++) {
                if (newStr[i] === "0") {
                    newStr = newStr.slice(i + 1)
                } 
                i++
            }
            return parseInt(symbol + newStr)
        } else {
            return new Error("长度超出限制")
        } 
    } else {
        return new Error("非整数")
    }
}
console.log(reverse(123)) // 321
console.log(reverse(-123)) // -321
console.log(reverse(120)) // 21

可以看到,按照我分析的思路,以及我现在能够熟练使用的一些方法,已然是自以为是的将它实现了。接下来看看最终答案是什么吧。

终极解决方案————精华来袭:

image.png
代码如下:

/**
 * @param {number} x
 * @return {number}
 */
const reverse = (x) => {
  // 非空判断
  if (typeof x !== 'number') {
    return;
  }
  // 极值
  const MAX = 2147483647;
  const MIN = -2147483648;

  // 识别数字剩余部分并翻转
  const rest =
    x > 0
      ? String(x)
        .split('')
        .reverse()
        .join('')
      : String(x)
        .slice(1)
        .split('')
        .reverse()
        .join('');

  // 转换为正常值,区分正负数
  const result = x > 0 ? parseInt(rest, 10) : 0 - parseInt(rest, 10);

  // 边界情况
  if (result >= MIN && result <= MAX) {
    return result;
  }
  return 0;
};

分析:
1.思路上:与我的思路基本一致
2.代码上:比我的简洁无数倍,使用的方法也更加准确
3.存在的问题:(1)我的代码首先做了判断整数的处理,但没有判断是否为空的情况;(2)极值的判别片面又草率,题中给出的是数值的取值范围,而我的判断是字符长度,也是明显的答非所问。

代码拆解————深入分析:

1.判别类型及是否为空:

const reverse = (x) => {
     // 非空判断
     if (typeof x !== 'number') {
        console.log(x)
        return;
     }
}

试问有没有和我一样在判别类型和判别是否为空时使用了两个条件,如:

if (!x) {
    return
}
if (typeof x !== "number") {
    return
}
// 或
if (!x && typeof x !== "number") {
    return
}

在以前我确实没有深入的考虑到typeof x !== "number"这个条件能够进行判空。

现在仔细思考如果传入的值为空的话其实它是undefined,如果是""、null、[]、{}的话,类型又不是number。

所以这样的判空处理确实很聪明。在这种场景下既能确定类型又能确定参数是否为空。

2.极值:

const MAX = 2147483647;
const MIN = -2147483648;

最上面我的代码,在确定边界时简单的判断了参数的长度。很明显是不符合条件的,相信很少有人会犯和我同样的错误。

但是贴这段代码的目的是,我在定义常量的时候总是 const min或const max。这样写当然也可以,但是不够显而易见。通常都是使用全大写的方式。

3.识别数字剩余部分并翻转:

const rest =
      x > 0
         ? String(x)
         .split('')
         .reverse()
         .join('')
         : String(x)
         .slice(1)
         .split('')
         .reverse()
         .join('');

这段代码太值得学习了,首先是书写风格。我在写三目运算的时候都是一行,很多时候都长到没法看。

其次是使用的方法,准确,准确,准确,我只能这样来形容了。

第一是String(x),使用了一个包装类(其实就是调用了toString()方法,但包装类更简洁清晰)。

第二是split(),这个方法用于字符串分割,我通常是用于处理时间格式等;但精髓在于如果传入的是"",那么它会把字符串的每个字符都分割开来,而且这个方法返回的是一个数组!再对比我自己的方法,还要先使用[].slice.call(x)把它变成数组。惭愧啊,学艺不精啊。

第三是reverse(),这个没什么好说的,数组上的方法用于翻转数组。

第四是join(),把数组的每个元素按照规则拼接成字符串,也很简单。

第五是slice(),按照索引提取字符串的某一个部分,并返回一个新字符串。

4.转换为正常值,区分正负数:

const result = x > 0 ? parseInt(rest, 10) : 0 -parseInt(rest, 10);

怎么说呢,我只能说不怪别人太聪明,只怪自己太愚蠢。我的代码在处理正负整数的时候,首先保留了"-"负号...那正变负不就是0减整数就完事了吗?对不起 x n,代码没写好,数学也没整明白。

parseInt(rest, 10),这里的10是进制,默认就是10进制。所以写作这样也可以:

const result = x > 0 ? parseInt(rest) : 0 - parseInt(rest);

5.边界情况:

if (result >= MIN && result <= MAX) {
    return result;
}
return 0;

这个就没什么好说的了。也可以这么写:

return result >= MIN && result <= MAX ? result : 0

复杂度分析:
image.png

巨人的肩膀上好成功,大树底下好乘凉。复盘这段代码确实给我带来了不少的收获。但别急,还有!

image.png

在看到这个方法名的时候我已经被吓到了,这是个嘛?查理 · 芒格曾大致说过,遇到困难的东西要敢于迈步。确实,方法总比困难多,美妙的方法也总是被少数人掌握。

代码如下:

/**
 * @param {number} x
 * @return {number}
 */
const reverse = (x) => {
      // 获取相应数的绝对值
      let int = Math.abs(x);
      
      // 极值
      const MAX = 2147483647;
      const MIN = -2147483648;
      let num = 0;

      // 遍历循环生成每一位数字
      while (int !== 0) {
            // 借鉴欧几里得算法,从 num 的最后一位开始取值拼成新的数
            num = (int % 10) + (num * 10);
            // 剔除掉被消费的部分
            int = Math.floor(int / 10);
      }
      
      // 异常值
      if (num >= MAX || num <= MIN) {
            return 0;
      }
      if (x < 0) {
            return num * -1;
      }
      return num;
};

分析:
1.思路上:欧几里得求最大公约数,看到这句话起初我把重心全放在欧几里得了...但实际上它就是在求最大公约数,这就简单了。然后是翻转,模10取最低位再乘10取最高位,也好理解。
2.代码上:很明显的看到,区别于第一种方法(主要使用String上的方法),这种方法主要使用的是Math上的方法。

代码拆解————深入分析:

1.获取相应数的绝对值

const reverse = (x) => {
    let int = Math.abs(x);
}

利用Math.abs将参数处理成正整数。但是这里有一个问题,还是需要判空判类型的,如果不做处理的话Math.abs(x)会返回0或NaN,而且不会抛错,0就不说了,NaN和任何值进行运算都是NaN,所以判空和判类型的处理是必不可少的。改进如下:

const reverse = (x) => {
    if (typeof x !== "number") {
        return;
    }
    let int = Math.abs(x)
}

2.极值:

const MAX = 2147483647;
const MIN = -2147483648;

3.遍历循环生成每一位数字:

let num = 0;
while (int !== 0) {
     // 借鉴欧几里得算法,从 num 的最后一位开始取值拼成新的数
     num = (int % 10) + (num * 10);
     // 剔除掉被消费的部分
     int = Math.floor(int / 10);
}

while循环确实用的很少,但是不要慌。我们直观的看到循环的入口是int !== 0,而出口是个啥呢?什么时候结束循环呢?

Math.floor()返回小于或等于一个给定数字的最大整数,也就是说当int被消费的很小的时候,int = 0,而此时就会跳出循环了。

回到代码,我们可以代入一个值,比如123。如图:
image.png

看到这里我发现,似乎跟最大公约数没什么关系,反而跟欧几里得算法有点关系...但实际上也很简单。

就按照上面的代入法来一个深入剖析:首先这种方法最精髓的地方就在于while循环内的两行代码。

num这个变量实际上是用来接收int处理结果的变量,而int这个变量是逐渐被“消费”的变量,就好像把一杯水倒进一个空杯子一样。

而在代码上来说,将参数模10,取到的是参数的最低位;而 num * 10 就相当于把num的位数向前推了1位,然后再加上低位数字。然后就相当于做了一次翻转。

image.png

不得不说这个方法真的很巧妙!!!

4.边界情况:

if (num >= MAX || num <= MIN) {
    return 0;
}

5.符号:

if (x < 0) {
    return num * -1;
}

复杂度分析:
image.png

阅读 98

为了不加班
一个为了写出漂亮代码而努力的前端人

一个为了写出漂亮代码而努力的前端人

15 声望
2 粉丝
0 条评论

一个为了写出漂亮代码而努力的前端人

15 声望
2 粉丝
宣传栏