Giter Site home page Giter Site logo

chenjiah / blog Goto Github PK

View Code? Open in Web Editor NEW
9.0 2.0 7.0 1.37 MB

📝个人博客 - 欢迎关注 👀 和点赞 ⭐️

Home Page: https://chenjiahao.xyz/blog

License: MIT License

JavaScript 15.03% Shell 0.38% HTML 1.71% Vue 73.45% CSS 3.35% SCSS 6.08%
blog f2e javascript css html web-development nodejs electron

blog's Introduction

Welcome to Blog 👋

Version Documentation License: MIT Twitter: F2E_McChen

Issues are forbid!

Official Account

Wechat - 前端成长记

Author

👤 McChen

Show your support

Give a ⭐️ if this project helped you!

📝 License

Copyright © 2019 McChen.
This project is MIT licensed.

blog's People

Contributors

chenjiah avatar dependabot[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

blog's Issues

【Leetcode 做题学算法周刊】第五期

首发于微信公众号《前端成长记》,写于 2019.12.06

背景

本文记录刷题过程中的整个思考过程,以供参考。主要内容涵盖:

  • 题目分析设想
  • 编写代码验证
  • 查阅他人解法
  • 思考总结

目录

Easy

100.相同的树

题目地址

题目描述

给定两个二叉树,编写一个函数来检验它们是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

示例:

输入:       1         1
          / \       / \
         2   3     2   3

        [1,2,3],   [1,2,3]

输出: true

输入:      1          1
          /           \
         2             2

        [1,2],     [1,null,2]

输出: false

输入:       1         1
          / \       / \
         2   1     1   2

        [1,2,1],   [1,1,2]

输出: false

题目分析设想

题目直接说了是二叉树,而二叉树的遍历方式有两种:深度优先和广度优先,我就从这两个思路来作答。

编写代码验证

Ⅰ.深度优先

代码:

/**
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {boolean}
 */
var isSameTree = function(p, q) {
    if (p === null && q === null) return true
    if (p === null || q === null) return false

    if (p.val !== q.val) return false

    return isSameTree(p.left, q.left) && isSameTree(p.right, q.right)
};

结果:

  • 57/57 cases passed (52 ms)
  • Your runtime beats 98.81 % of javascript submissions
  • Your memory usage beats 16.66 % of javascript submissions (33.8 MB)
  • 时间复杂度 O(n)n 为节点个数

Ⅱ.广度优先

代码:

/**
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {boolean}
 */
var isSameTree = function(p, q) {
    if (p === null && q === null) return true
    if (p === null || q === null) return false

    let pQ =[p] // 左侧比较队列
    let qQ =[q] // 右侧比较队列

    let res = true

    while(true) {
        if (!pQ.length || !qQ.length) {
            res = pQ.length === qQ.length
            break
        }
        // 当前比较节点
        let curP = pQ.shift()
        let curQ = qQ.shift()
        if ((curP && !curQ) || (!curP && curQ) || (curP && curQ && curP.val !== curQ.val)) {
            res = false
            break
        } else {
            let pL = curP ? curP.left : null
            let pR = curP ? curP.right : null
            if (pL || pR) { // 至少一个存在才有意义
                pQ.push(pL, pR) // 依次推入比较数组,实际上就是广度优先
            }
            let qL = curQ ? curQ.left : null
            let qR = curQ ? curQ.right : null
            if (qL || qR) { // 至少一个存在才有意义
                qQ.push(qL, qR) // 依次推入比较数组,实际上就是广度优先
            }
        }
    }

    return res
};

结果:

  • 57/57 cases passed (64 ms)
  • Your runtime beats 73.27 % of javascript submissions
  • Your memory usage beats 15.53 % of javascript submissions (33.8 MB)
  • 时间复杂度 O(n)n 为节点个数

查阅他人解法

思路基本上都是这两种,未发现方向不同的解法。

思考总结

一般碰到二叉树的题,要么就深度遍历,要么就广度遍历。深度优先,也叫先序遍历。

101.对称二叉树

题目地址

题目描述

给定一个二叉树,检查它是否是镜像对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。

示例:

    1
   / \
  2   2
 / \ / \
3  4 4  3

但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:

   1
   / \
  2   2
   \   \
   3    3

说明:

如果你可以运用递归和迭代两种方法解决这个问题,会很加分。

题目分析设想

还是一道二叉树的题,所以常规思路就是遍历操作,深度优先或广度优先都可。镜像对称可以观察到很明显的特点是有相同的根节点值,且每个树的右子树与另一个树的左字数对称相等。深度优先的方式,其实就是递归的思路,符合题目的说明。

编写代码验证

Ⅰ.深度优先

代码:

/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isSymmetric = function(root) {
    function isMirror (l, r) {
        if (l === null && r === null) return true
        if (l === null || r === null) return false

        return l.val === r.val && isMirror(l.left, r.right) && isMirror(l.right, r.left)
    }
    return isMirror(root, root)
};

结果:

  • 195/195 cases passed (68 ms)
  • Your runtime beats 87.74 % of javascript submissions
  • Your memory usage beats 41.48 % of javascript submissions (35.5 MB)
  • 时间复杂度 O(n)n 为节点个数

Ⅱ.广度优先

代码:

/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isSymmetric = function(root) {
    if (root === null) return true
    // 初始队列
    let q = [root.left, root.right]
    // 依次将同级push进队列,每次取两个对称节点进行判断
    while(q.length) {
        let l = q.shift()
        let r = q.shift()
        if (l === null && r === null) continue
        if (l === null || r === null) return false
        if (l.val !== r.val) return false

        q.push(l.left, r.right, l.right, r.left)
    }
    return true
};

结果:

  • 195/195 cases passed (64 ms)
  • Your runtime beats 94.88 % of javascript submissions
  • Your memory usage beats 28.3 % of javascript submissions (35.6 MB)
  • 时间复杂度 O(n)n 为节点个数

查阅他人解法

看到一个有意思的思路,将树按照左中右的顺序输入到数组,加上层数,该数组也是对称的。

Ⅰ.左中右顺序输出数组

代码:

/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isSymmetric = function(root) {
    if (root === null) return true
    // 输出数组
    let arr = []
    search(arr, root, 1);
    // 入参分别为输出,节点和层级
    function search(output, n, k) {
        if (n.left !== null) {
            search(output, n.left, k+1)
        }

        if (n.right !== null) {
            search(output, n.right, k + 1);
        }
    }
     //判断是否对称
     let i = 0, j = arr.length - 1
     while (i < j) {
         if (arr[i] != arr[j]) {
             return false
         }
         i++
         j--
     }
     return true
};

结果:

  • 195/195 cases passed (72 ms)
  • Your runtime beats 76.3 % of javascript submissions
  • Your memory usage beats 6.11 % of javascript submissions (36.3 MB)
  • 时间复杂度 O(n)n 为节点个数

思考总结

这道题的大致解法都是遍历节点或者利用队列,只是在递归的细节上会有些差异。左中右输出数组的思路很清奇,虽然效率明显会更低下,但是不失为一种思路。

104.二叉树的最大深度

题目地址

题目描述

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:

给定二叉树 [3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7

返回它的最大深度 3 。

题目分析设想

这道题最基本的思路就是计算出每条子节点的深度,再进行比较。为了提升效率,可以增加同级比对,去除不可能是最长节点的叶节点计算。

所以这里我就用以下几种思路来实现深度优先算法。

  • 递归,直接获取子树最大高度加 1
  • 利用队列,求深度转化为求有多少层

编写代码验证

Ⅰ.递归

代码:

/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function(root) {
    if (root === null) return 0
    // 左侧子树的最大高度
    let l = maxDepth(root.left)
    // 右侧子树的最大高度
    let r = maxDepth(root.right)
    return Math.max(l, r) + 1
};

结果:

  • 39/39 cases passed (60 ms)
  • Your runtime beats 99 % of javascript submissions
  • Your memory usage beats 45.77 % of javascript submissions (37.1 MB)
  • 时间复杂度 O(n)n 为节点个数

Ⅱ.利用队列

代码:

/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function(root) {
    if (root === null) return 0
    // 队列
    let q = [root]
    let dep = 0
    while(q.length) {
        let size = q.length
        dep++
        while(size > 0) {
            let node = q.shift()
            if (node.left !== null) q.push(node.left)
            if (node.right !== null) q.push(node.right)
            size--
        }
    }
    return dep
};

结果:

  • 39/39 cases passed (68 ms)
  • Your runtime beats 91.33 % of javascript submissions
  • Your memory usage beats 30.1 % of javascript submissions (37.2 MB)
  • 时间复杂度 O(n)n 为节点个数

查阅他人解法

这里看到一个用栈的角度来实现的,取栈高度的最大值,其他的基本都是循环的细节差异,大体思路一致。

Ⅰ.利用栈

代码:

/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function(root) {
    if (root === null) return 0
    // 栈
    let s = [{
        node: root,
        dep: 1
    }]
    let dep = 0

    while(s.length) {
        // 先进后出
        var cur = s.pop()
        if (cur.node !== null) {
            let curDep = cur.dep
            dep = Math.max(dep, curDep)
            if (cur.node.left !== null) s.push({node: cur.node.left, dep: curDep + 1})
            if (cur.node.right !== null) s.push({node: cur.node.right, dep: curDep + 1})
        }
    }
    return dep
};

结果:

  • 39/39 cases passed (72 ms)
  • Your runtime beats 81.41 % of javascript submissions
  • Your memory usage beats 66.6 % of javascript submissions (37 MB)
  • 时间复杂度 O(n)n 为节点个数

思考总结

二叉树的操作,一般就是深度优先和广度优先,所以基本上就朝这两个方向上去解,然后进行优化就可以了。

107.二叉树的层次遍历II

题目地址

题目描述

给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)

例如:

给定二叉树 [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回其自底向上的层次遍历为:

[
  [15,7],
  [9,20],
  [3]
]

题目分析设想

这道题在我看来还是两种方式,深度优先和广度优先。

  • 深度优先,记录下每个节点对应的层数后,按层数反向输出即可
  • 广度优先,记录下每层的节点后反向输出

编写代码验证

Ⅰ.深度优先

代码:

/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrderBottom = function(root) {
    // 当前层级标识
    let idx = 0
    let res = []

    function levelOrder(node, floor, arr) {
        if (node === null) return arr
        if(arr[floor]) {
            arr[floor].push(node.val)
        } else {
            arr[floor] = [node.val]
        }
        levelOrder(node.left, floor + 1, arr)
        levelOrder(node.right, floor + 1, arr)
        return arr
    }

    return levelOrder(root, idx, res).reverse()
};

结果:

  • 34/34 cases passed (68 ms)
  • Your runtime beats 77.01 % of javascript submissions
  • Your memory usage beats 34.78 % of javascript submissions (34.7 MB)
  • 时间复杂度 O(n)n 为节点个数

Ⅱ.广度优先

代码:

/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrderBottom = function(root) {
    if (root === null) return []
    // 初始队列
    let q = [root]
    let res = []

    while(q.length) {
        // 当前层节点数量
        const count = q.length
        let curArr = []
        for(let i = 0; i < count;i++) {
            const node = q.shift()
            curArr.push(node.val)
            // 将子节点依次推入队列
            if (node.left) q.push(node.left)
            if (node.right ) q.push(node.right )
        }
        res.push(curArr)
    }
    return res.reverse()
};

结果:

  • 34/34 cases passed (64 ms)
  • Your runtime beats 89.2 % of javascript submissions
  • Your memory usage beats 32.3 % of javascript submissions (34.7 MB)
  • 时间复杂度 O(n)n 为节点个数

查阅他人解法

没有看到什么特别的解法,主要都是按 BFS 和 DFS 来处理,要么迭代,要么递归等等。

这里就介绍下别的吧,在第一种解法中我们使用的是前序优先,当然用中序优先或后序优先也可以,下面代码可以说明区别:

// 先序,顺序为 根 -> 左 -> 右
function levelOrder(node, floor, arr) {
    if(arr[floor]) {
        arr[floor].push(node.val)
    } else {
        arr[floor] = [node.val]
    }

    levelOrder(node.left, floor + 1, arr)
    levelOrder(node.right, floor + 1, arr)
    return arr
}
// 中序,顺序为 左 -> 根 -> 右
function levelOrder(node, floor, arr) {
    levelOrder(node.left, floor + 1, arr)

   if(arr[floor]) {
       arr[floor].push(node.val)
   } else {
       arr[floor] = [node.val]
   }

    levelOrder(node.right, floor + 1, arr)
    return arr
}
// 后序,顺序为 左 -> 右 -> 根
function levelOrder(node, floor, arr) {
    levelOrder(node.left, floor + 1, arr)
    levelOrder(node.right, floor + 1, arr)

    if(arr[floor]) {
        arr[floor].push(node.val)
    } else {
        arr[floor] = [node.val]
    }
    return arr
}

思考总结

二叉树的题目就根据情况在深度优先和广度优先中择优选择即可,基本不会有太大的问题。

108.将有序数组转换为二叉搜索树

题目地址

题目描述

将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。

本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。

示例:

给定有序数组: [-10,-3,0,5,9],

一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树:

      0
     / \
   -3   9
   /   /
 -10  5

题目分析设想

这里有两点要注意的:高度平衡二叉树要求每个节点的左右两个子树的高度差的绝对值不超过 1;而二叉搜索树要求左子树上所有节点值小于根节点,右子树上所有节点值大于根节点。

而题目给出的是一个有序的数组,所以可以直接考虑二分后进行处理,我这就直接递归作答:找到根节点,递归生成左右子树。

编写代码验证

Ⅰ.递归

代码:

/**
 * @param {number[]} nums
 * @return {TreeNode}
 */
var sortedArrayToBST = function(nums) {
    if (!nums.length) return null
    // 中位数,用偏移避免溢出
    const mid = nums.length >>> 1
    const root = new TreeNode(nums[mid])
    root.left = sortedArrayToBST(nums.slice(0, mid))
    root.right = sortedArrayToBST(nums.slice(mid + 1))
    return root
};

结果:

  • 32/32 cases passed (80 ms)
  • Your runtime beats 70.72 % of javascript submissions
  • Your memory usage beats 29.79 % of javascript submissions (37.8 MB)
  • 时间复杂度 O(n)

查阅他人解法

这里看到另外一种解法,先创建一个平衡二叉树,然后中序遍历树同时遍历数组即可,因为中序遍历出来的刚好是有序数组。

Ⅰ.创建树后中序遍历数组赋值

代码:

/**
 * @param {number[]} nums
 * @return {TreeNode}
 */
var sortedArrayToBST = function(nums) {
    if (!nums.length) return null

    // 节点总数
    let len = nums.length
    let root = new TreeNode(-1);
    let q = [root]
    // 已经创建了根节点
    len--
    while(len) {
        const node = q.shift()
        // 左子树
        const l = new TreeNode(-1)
        q.push(l)
        node.left = l
        len--
        if (len) {
            // 右子树
            const r = new TreeNode(-1)
            q.push(r)
            node.right = r
            len--
        }
    }

    let i = 0
    inorder(root)
    function inorder(node) {
        if (node === null) return
        inorder(node.left)
        node.val = nums[i++]
        inorder(node.right)
    }

    return root
};

结果:

  • 32/32 cases passed (72 ms)
  • Your runtime beats 93.4 % of javascript submissions
  • Your memory usage beats 24.12 % of javascript submissions (37.8 MB)
  • 时间复杂度 O(n)

思考总结

这里其实是个逆向思维,之前是二叉树输出数组,现在变成数组转成二叉树。刚好可以翻一下前序中序和后序的区别,这里中序就可以了。不过这道题我还是更推荐递归二分求解。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

移动端前端开发入门总结

首发于微信公众号《前端成长记》,写于 2015.04.10

前言

做了快半个月的移动端开发了,就移动端前端开发来做一个小小的总结,希望能帮助新人更快入门。

一些实用技巧

meta标签

<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="format-detection"content="telephone=no, email=no" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" /><!-- 删除苹果默认的工具栏和菜单栏 -->
<meta name="apple-mobile-web-app-status-bar-style" content="black" /><!-- 设置苹果工具栏颜色 -->
<meta name="format-detection" content="telphone=no, email=no" /><!-- 忽略页面中的数字识别为电话,忽略email识别 -->
<!-- 启用360浏览器的极速模式(webkit) -->
<meta name="renderer" content="webkit">
<!-- 避免IE使用兼容模式 -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- 针对手持设备优化,主要是针对一些老的不识别viewport的浏览器,比如黑莓 -->
<meta name="HandheldFriendly" content="true">
<!-- 微软的老式浏览器 -->
<meta name="MobileOptimized" content="320">
<!-- uc强制竖屏 -->
<meta name="screen-orientation" content="portrait">
<!-- QQ强制竖屏 -->
<meta name="x5-orientation" content="portrait">
<!-- UC强制全屏 -->
<meta name="full-screen" content="yes">
<!-- QQ强制全屏 -->
<meta name="x5-fullscreen" content="true">
<!-- UC应用模式 -->
<meta name="browsermode" content="application">
<!-- QQ应用模式 -->
<meta name="x5-page-mode" content="app">
<!-- windows phone 点击无高光 -->
<meta name="msapplication-tap-highlight" content="no">
<!-- 适应移动端end -->

Html5+Css3的使用

因为手机大多数都是高级浏览器,HTML5可以实现一些HTML4中无法实现的丰富的WEB应用程序的体验,可以减少开发者很多的工作量,当然了你决定使用HTML5前,一定要对此非常熟悉,要知道HTML5的新标签的作用。比如定义一块内容或文章区域可使用section标签,定义导航条或选项卡可以直接使用nav标签等等。

按钮带有圆角效果,有内发光效果还有高光效果,这样的按钮使用CSS3写是无法写出来的,当然圆角可以使用CSS3来写,但高光和内发光却无法使用CSS3编写,这个时候你不妨使用-webkit-border-image来定义这个按钮的样式。-webkit-border-image就个很复杂的样式属性。

还有渐变,动画等你能够想象出的各种酷炫狂拽吊炸天的效果。

块级化a标签

因为大多数都是触屏手机,要让用户很方便的能点击到标签,建议用42X42。操作对象的大小符合手指的操作,按键的大小设置规范:食指点击的间距 约为77 mm, 1mm间距,拇指点击88 mm,2mm间距。当前推荐的值为9mm 大小,最小应不小于7mm。

自适应布局

在编写CSS时,我不建议前端工程师把容器(不管是外层容器还是内层)的宽度定死。为达到适配各种手持设备,我建议前端工程师使用自适应布局模式(支付宝采用了自适应布局模式),因为这样做可以让你的页面在ipad、itouch、ipod、iphone、android、web safarik、chrome都能够正常的显示,你无需再次考虑设备的分辨率。页面必须自适应屏幕大小,可以采用流体布局。其它一些小问题可以采用media query,比如对于图片的处理,只要设置宽度,让图片自适应,防止图片变形,当然要兼容的设备分辨率差距很大的时候,需要利用media queries根据分辨率的不同加载不同的图片(需要同一张设置为几种不同的规格),一是防止小分辨率设备加载大图片浪费流量,二是防止大分辨率设备加载小图片导致的图片模糊问题。

适当优雅降级

要做好优雅降级(平稳退化),少用JS,图片,要用户禁止下载JS和图片的时候页面也能体现价值(因为很多APP默认设置为3G下是不自动下载图片等资源的)。

字体大小与行间距

不建议使用px/em,而使用rem,虽然它是相对单位,但使用rem单位可以避开很多层级的关系而行间距则直接是采用字体的倍数。为了方便计算,在html元素中常将font-size设为62.5%。注意在chorme是会强制等于12的~

IOS在第三方输入法下不支持onkeyup事件

// 使用oninput进行代替onkeyup事件
$("#user-name")[0].oninput = function() {
    that.checkusername();
};

部分android系统中元素被点击时产生的边框怎么去掉

android用户点击一个链接,会出现一个边框或者半透明灰色遮罩, 不同生产商定义出来额效果不一样,可设置-webkit-tap-highlight-color的alpha值为0去除部分机器自带的效果

a,button,input,textarea{
    -webkit-tap-highlight-color: rgba(0,0,0,0;)
    -webkit-user-modify:read-write-plaintext-only;
}

-webkit-user-modify有个副作用,就是输入法不再能够输入多个字符

另外,有些机型去除不了,如小米2

对于按钮类还有个办法,不使用a或者input标签,直接用div标签

winphone系统a、input标签被点击时产生的半透明灰色背景怎么去掉

<meta name="msapplication-tap-highlight" content="no">

webkit表单元素的默认外观怎么重置

.css{-webkit-appearance:none;}

移动端如何清除输入框内阴影

在iOS上,输入框默认有内部阴影,但无法使用 box-shadow 来清除,如果不需要阴影,可以这样关闭:

input,
textarea {
  border: 0; /* 方法1 /
  -webkit-appearance: none; / 方法2 */
}

移动端禁止选中内容

.user-select-none {
    -webkit-user-select: none; /* Chrome all / Safari all /
    -moz-user-select: none; / Firefox all (移动端不需要) /
    -ms-user-select: none; / IE 10+ */
}

移动端取消touch高亮效果

在做移动端页面时,会发现所有a标签在触发点击时或者所有设置了伪类 :active 的元素,默认都会在激活状态时,显示高亮框,如果不想要这个高亮,那么你可以通过css以下方法来进行全局的禁止:

html {-webkit-tap-highlight-color: rgba(0, 0, 0, 0);}

但这个方法在三星的机子上无效,有一种妥协的方法是把页面非真实跳转链接的a标签换成其它标签,可以解决这个问题。

如何禁止保存或拷贝图像(IOS)

通常当你在手机或者pad上长按图像 img ,会弹出选项 存储图像 或者 拷贝图像,如果你不想让用户这么操作,那么你可以通过以下方法来禁止:

img { -webkit-touch-callout: none; }

屏幕旋转的事件

window.orientation,取值:正负90表示横屏模式、0和180表现为竖屏模式;

window.onorientationchange = function(){
    switch(window.orientation){
        case -90:
        case 90:
            alert("横屏:" + window.orientation);
        case 0:
        case 180:
            alert("竖屏:" + window.orientation);
        break;
    }
}

audio元素和video元素在ios和andriod中无法自动播放

应对方案:触屏即播

$('html').one('touchstart',function(){
    audio.play()
})

手机拍照和上传图片

<input type=file accept="image/*">
<input type=file accept="video/*">

使用总结:

  • ios 有拍照、录像、选取本地图片功能
  • 部分android只有选取本地图片功能
  • winphone不支持
  • input控件默认外观丑陋

消除transition闪屏

.css{
/设置内嵌的元素在 3D 空间如何呈现:保留 3D/
-webkit-transform-style: preserve-3d;
/(设置进行转换的元素的背面在面对用户时是否可见:隐藏)/
-webkit-backface-visibility: hidden;
}

开启硬件加速,解决页面闪白,保证动画流畅

.css {
-webkit-transform: translate3d(0, 0, 0);
-moz-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}

设计高性能CSS3动画的几个要素

  • 尽可能地使用合成属性transform和opacity来设计CSS3动画
  • 不使用position的left和top来定位
  • 利用translate3D开启GPU加速

移动端border-radius的几个BUG

Android 2.3 自带浏览器不支持 %

通常我们实现一个正圆只需要border-radius: 50%即可,大致代码如下

.foo {
    width: 100px;
    height: 100px;
    border-radius: 50%;
    border: 1px solid blue;
}

然而 Android 2.3 是不支持百分比的,要兼容我们只能使用一个较大值,比如border-radius: 999px;

Android 及 Safari 低版本 img 圆角问题

当 img 元素有border 时设置border-radius 会导致圆角变形,需要在img 外面嵌套一个元素并设置border 和border-radius。

Android 4.2.x 背景色溢出

测试发现,在 Android 4.2.x 系统自带浏览器中,同时设置border-radius,border和背景色的时候,背景色会溢出到圆角以外部分,可以使用背景剪裁background-clip: padding-box;来修复,但是如果border-color为半透明时,背景直角部分依然会露出来

关于背景剪裁background-clip

background-clip: border-box|padding-box|content-box;

描述
border-box 背景被裁剪到边框盒。
padding-box 背景被裁剪到内边距框。
content-box 背景被裁剪到内容框。

Android 4.2.x 不支持border-radius缩写

这个 BUG在小米上测试并未发现,国外有人反映三星 Galaxy S4 中自带浏览器不支持。

解决方案就是使用border-radius的四个扩写属性,缩写属性放到最后。

以上两个问题影响到 Android 4.2.x 内核的系统以及在其基础上定制的系统的自带浏览器,比如:红米,小米3,阿里云OS 等,安卓版 Chrome 不受影响。

完整代码应该是这样的:

.foo {
    width: 100px;
    height: 100px;
    border: 5px solid blue;
    border-top-left-radius: 999px; /* 左上角 */
    border-top-right-radius: 999px; /* 右上角 */
    border-bottom-right-radius: 999px; /* 右下角 */
    border-bottom-left-radius: 999px; /* 左下角 */
    border-radius: 999px;
    background-color: #ccc;
    background-clip: padding-box;
}

用box-shadow模拟边框

背景色溢出另一个解决办法就是使用box-shadow模拟border;差不多可以达到效果

border: 1px solid #333333;
/*替换为*/
box-shadow: 0 0 1px 1px #333333;

其他问题

  • IE9 中fieldset元素不支持border-radius。
  • IE9 中带有背景渐变(gradient)的时候背景溢出。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

Nodejs创建多级目录文件夹

首发于微信公众号《前端成长记》,写于 2016.07.12

背景

由于Nodejs的API只提供创建一级目录,所以如果需要创建多级目录的话,那需要一级一级去创建,为了代码的简便,采用递归的写法。

创建方式

异步创建

var fs = require('fs');
var path = require('path');
function async (dirname, callback){
    fs.exists(dirname, function (exists){
        if(exists){
            callback();
        }else{
            mkdirs.async(path.dirname(dirname), function (){
                fs.mkdir(dirname, callback);
            });
        }
    });
}

缺点是无法保证执行回调的顺序

同步创建

var fs = require('fs');
var path = require('path');
function sync (dirname) {
    if(fs.existsSync(dirname)){
        return true;
    }else{
        if(mkdirs.sync(path.dirname(dirname))){
            fs.mkdirSync(dirname);
            return true;
        }
    }
}

可以精准的保证执行完成之后再执行后续操作

jm-mkdirs

根据上述的原理,自己编写一个node环境的npm包,专门用于创建多级目录文件夹。

Github地址及使用文档:jm-mkdirs

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

修复IScroll点击无效问题,增加scrollTo数值容错处理

首发于微信公众号《前端成长记》,写于 2016.03.04

修复IScroll点击BUG

由于做移动端开发,使用IScroll的的时候,在IScroll区域有需要点击时部分安卓机点击无效。若是设置click为true,则iOS需双击才能触发点击事件。

版本号

  • IScroll: v5.1.3

针对不同机型进行IScroll配置参数click的修改

function IScroll (el, options) {
// INSERT POINT: DEFAULTS
    // fix click in bad Android
    this.options.click = this.fixIScrollClickInAndroid();
}
IScroll.prototype = {
    version: '5.1.3',
    fixIScrollClickInAndriod: function (){
        if (/iPhone|iPad|iPod|Macintosh/i.test(navigator.userAgent)) return false;
        if (/Chrome/i.test(navigator.userAgent)) return (/Android/i.test(navigator.userAgent));
        if (/Silk/i.test(navigator.userAgent)) return false;
        if (/Android/i.test(navigator.userAgent)) {
        var s = navigator.userAgent.substr(navigator.userAgent.indexOf('Android')+8,5);
        //return parseFloat(s[0]+s[3]) < 44 ? false : true}
        return parseFloat(s[0]+s[2]+s[4]) <= 442 && parseFloat(s[0]+s[2]+s[4]) > 430 ? true : false}
    }
}

增加scrollTo数值容错处理

在返回页面的时候需要定位,所以把第一次进入的位置存入sessionStorage了,取出来时调用scrollTo(0,pos)无效。

出现原因

IScroll对于数值设置为Number,当存入sessionStorage会转为字符串,若取出来不进行parseFloat处理,将返NaN导致无法定位

解决方案

  • 对从sessionStorage中取出来的数值,进行parseFloat处理
  • 修改IScroll关于scrollTo位置的源码,增加 || 0 操作
var myScroll = new Scroll("#wrapper");
window.sessionStorage.setItem("pos",myScroll.y);
myScroll.scrollTo(parseFloat(window.sessionStorage.getItem("pos"))) // 也可修改scrollTo方法做 || 0 转换为数值处理

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

移动端rem布局万能适配mixin

首发于微信公众号《前端成长记》,写于 2016.05.12

基于 640 宽的设计稿

/================================================================
以下为基于ip5 宽度320做的适配,标准html{font-size:10px},即1rem = 10px
=================================================================/
@mixin queryWidth($min, $max) {
    @if $min == -1 {
      @media screen and (max-width: $max+px) {
        html {
          font-size: ( ($max+1) / 320 ) * 10px;
        }
      }
    } @else if $max == -1 {
      @media screen and (min-width: \$min+px) {
        html {
          font-size: ( $min / 320 ) * 10px;
        }
      }
    } @else {
      @media screen and (min-width: $min+px) and (max-width: $max+px) {
        html {
          font-size: ( $min / 320 ) * 10px;
        }
      }
    }
}
@media only screen and (orientation: landscape) {
  html {
    font-size: 10px;
  }
}
@include queryWidth(-1, 319); // for iphone 4
@include queryWidth(320, 359); // for iphone 5
@include queryWidth(360, 374);
@include queryWidth(375, 383); // for iphone 6
@include queryWidth(384, 399);
@include queryWidth(400, 413);
@include queryWidth(414, -1); // for iphone 6 plus

基于 750 宽的设计稿

只需将mixin中320改为375即可。若要100px=1rem,只需将mixin中10改成100即可。

当然,这只能实现大部分常规机型的适配,有些特殊机型还是需要自己再加上对应的适配。接下来罗列一下我用到过的适配,希望得到各位帮助逐步完善。

@media screen and (min-width: 320px) {} //for iphone 5
@media screen and (min-width: 375px) {} //for iphone 6
@media screen and (min-width: 414px) {} //for iphone 6 plus
@media screen and (min-aspect-ratio: 69/100) {} //for huawei荣耀6
@media screen and (min-aspect-ratio: 7/10) {} //for mx3
@media screen and (max-height: 480px) {} //for iphone 4
@media screen and (max-height: 420px) {} //for iphone 4 微信
@media only screen and (orientation: landscape) {} // for 横屏

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

深入浅出Node.js读书笔记

首发于微信公众号《前端成长记》,写于 2019.05.06

Node简介

Node应用场景

  • I/O密集型,因为可以利用事件循环的处理能力,并行I/O,所以擅长I/O密集型
  • CPU密集型 建议适当分解大型运算任务,不阻塞I/O,如果计算耗时超过普通阻塞I/O的好使,那么就需要重新评估

模块机制

Node模块实现

  1. 路径分析
  2. 文件定位
  3. 编译执行

Node缓存编辑和执行后的对象

文件定位

文件拓展名分析,会调用fs模块同步阻塞式判断文件是否存在,依次.js、.node、.json。

如果是.node和.json,在传递给require时带上拓展名,会加快一点速度。同步配合缓存,可以大幅度缓解阻塞式调用的缺陷

编译过程

javascript核心模块通过 process.binding('natives') 取出,编译成功的模块缓存到NativeModule._cache对象,文件模块缓存到Module._cache

兼容多种模块规范

;(function (name, definition) {
// 检测上下文环境是否为AMD或CMD
var hasDefine = typeof define === 'function'
// 检测上下文环境是否为Node
var hasExports = typeof module !== 'undefined' && module.exports
if (hasDefine) {
// 如果是AMD或CMD环境
define(definition)
} else if (hasExports) {
// 如果是Node环境,定义为Node模块
module.exports = definition()
} else {
// 执行结果挂载window上
this[name] = definition()
}
})('hello', function () {
var hello = function () {}
return hello
})

异步I/O

Node异步I/O模型基本要素

事件循环,观察者,请求对象,I/O线程池构成了Node异步I/O模型的基本要素

观察者模式

idle观察者先于I/O观察者,I/O观察者先于check观察者。
idle观察者

  • process.nextTick 保存在数组中,每轮循环中会把全部回调执行完

check观察者

  • setInmediate 保存在链表中

Node高性能原因之一

Node通过事件驱动的方式处理请求,无须为每一个请求创建额外的对应线程,可以省掉创建线程和销毁线程的开销,同时操作系统在调度任务时因为线程较少,上下文切换的代价很低。这使得服务器能够有条不紊地处理请求,即使在大量连接的情况下,也不受线程上下文切换开销的影响。

异步编程

EventEmitter

多异步之间协作方案

使用事件发布/订阅

var emitter = new events.Emitter()
emitter.on('data', function (msg) {
console.log(msg)
})
emitter.emit('data', 'I'm a message')

也可再次封装,可参考 EventProxy 模块

异步并发控制

通过队列去判断,是否达到阀值,达到就从队列中去取出执行

目前ES6 ES7 已经支持 Promise Async/Await 来实现,不需要再自行编写

内存控制

V8内存分类

  • rss 常驻内存
  • heapTotal 申请的堆内存
  • heapUsed 当前使用的内存

V8的垃圾回收算法

分为新生代(存活时间较短)和老生代。
新生代采用 Scavenge 算法,内存一分为二,From - To。通过复制存活对象再进行交换,空间换时间。
老生代采用 Mark-Sweep & Mark-Compact ,Sweep 标记清除,Compact 对象移动(无内存碎片)

Buffer 内存不通过V8分配,而是从Node自行分配(突破大小限制)。所以V8回收的主要适合V8的堆内存。

内存泄漏常见原因

  • 缓存,将内存当缓存,常见的hashObj,建议使用Redis
  • 队列消费不及时,队列消费速度小于生产速度,比如说日志写入数据库
  • 作用域未释放

内存泄漏排查

  • node-heapdump
  • node-memwatch

大文件读取使用 createReadStream/createWriteStream 代替 readFile/writeFile ,pipe来通过文件流进行操作。

Buffer

Buffer内存分配

采用slab动态内存管理机制,固定大小的内存区域。

  • 小Buffer对象,主要使用一个局部变量pool作为中间处理对象,处于分配状态的slab单元都指向它。

正确拼接buffer

+= 内隐藏了 toString() 操作,在宽字节中文可能会出现问题。

var fs = require('fs')
var iconv = require('iconv-lite')
var res = fs.createReadStream('test.md')
var chunks = []
var size = 0
res.on('data', function (chunk) {
  chunks.push(chunk)
  size += chunk.length
})
res.on('end', function () {
  var buf = Buffer.concat(chunks, size)
  var str = iconv.decode(buf, 'utf8')
})

设置 highWaterMark 值可以提高二进制文件的读取速度

网络编程

TCP 传输控制协议

在OSI模型上属于传输层协议,由物理层(网络物理硬件)、数据链结层(网络特有的链路接口)、网络层(IP)、传输层(TCP/UDP)、会话层(通信连接/维持会话)、表示层(加密/解密等)、应用层(HTTP/SMTP/IMAP等)组成。

TCP是面向连接的,典型的3次握手行程会话, 客户端请求连接 -> 服务端响应 -> 客户端开始传输,通过套接字连接。

UDP 用户数据包协议

TCP中会话基于连接完成,如果客户端需要与另一个TCP服务通信需要另创建一个套接字(socket)。UDP一个套接字可以与多个UDP服务通信。目前广泛应用,DNS服务基于此实现。

socket.send(buf, offset, length, port, address, [callback])

HTTP 超文本传输协议

HTTP请求报文和响应报文都包括报文头和报问题。HTTP服务继承自TCP服务器,可以与多个客户端保持连接,由于采用事件驱动,不会为每个连接创建额外的线程或进程,所以可以保持很低的内存占用,进而满足高并发。

WebSocket

WebSocket客户端基于事件的编程模型与Node中自定义事件相差无几;WebSocket实现了客户端与服务器端之间的长连接,而Node事件驱动的方式十分擅长与大量的客户端保持高并发连接。

优势:

  • 客户端与服务器端只建立一个TCP链接,可以使用更少链接。
  • WebSocket服务器端可以推送数据到客户端,这远比HTTP请求响应模式更灵活、更高效。
  • 有更轻量级的协议头,减少数据传送量。

进程

Node中父进程在实际创建子进程之前,会创建IPC通道并监听它,然后才真正创建出子进程,并通过环境变量(NODE_CHANNEL_FD)告诉子进程这个IPC通道的文件描述符。子进程在启动的过程中,根据文件描述符去连接这个已存在的IPC通道,从而完成父子进程之间的链接。

只有启动的子进程是Node进程时,子进程才会根据环境变量去连接IPC通道,对于其他类型的子进程则无法实现进程间通信,除非其他进程也按约定去连接这个已经创建好的IPC通道

集群稳定,利用上CPU多核,多工作进程存活状态管理,平滑重启,重启限量阀值

// 主进程代码
var fork = require('child_process').fork
var cpus = require('os').cpus()

var server = require('net').createServer()
server.listen(1337)

var limit = 10
var during = 60000
var restart = []
var isTooFrequently = function(){
    var time = Date.now()
    var length = restart.push(time)
    if (length > limit) {
        restart = restart.slice(limit * -1)
    }
    return restart.length >= limit && restart[restartl.length - 1] - restart[0] < during
}

var workers = {}

var createWorker = function () {
    if (isTooFrequently()) {
        process.emit('giveup', length, during)
        return;
    }
    var worker = fork(__dirname + '/worker.js')
    worker.on('message', function (message) {
        if (message.act === 'suicide') {
            createWorker()
        }
    })
    worker.on('exit', function () {
        console.log('Worker ' + worker.pid + ' exited.')
        delete workers[worker.pid]
    })
    worker.send('server', server)
    workers[worker.pid] = worker
    console.log('Create Worker. pid: ' + worker.pid)
}

for(var i = 0; i < cpus.length; i++) {
    createWorker()
}

process.on('exit', function (){
    console.log('process exiting...')
    for(var pid in workers) {
        workers[pid].kill()
    }
})
// 业务逻辑 worker.js
var http = require('http')
var server = http.createServer(function(req, res){
    res.writeHead(200, {'Content-Type': 'text/plain'})
    res.end('handled by child, pid is ' + process.pid + '\n')
    throw new Error('throw exception')
})

var worker
process.on('message', function (m, tcp) {
    if (m === 'server') {
        worker = tcp
        worker.on('connection', function (socket) {
            server.emit('connection', socket)
        })
    }
})
process.on('uncaughtException', function () {
    process.send({act: 'suicide'})
    worker.close(function () {
        process.exit(1)
    })
    setTimeout(() => {
        process.exit(1)
    }, 5000);
})

测试

单元测试

  • 单一职责
  • 接口抽象
  • 层次分类

通过 assert 模块做断言

测试用例、测试报告、测试覆盖率、mock、工程化和自动化(travis-ci)

性能测试

  • 基准测试
  • 压力测试

RT:响应时间
TPS:吞吐量,在单位时间内处理请求的数量
QPS:每秒查询率
并发数:并发用户数是指系统可以同时承载的正常使用系统功能的用户的数量

产品化

项目工程化

  1. 目录结构

|—— History.md // 项目改动历史
|—— INSTALL.md // 安装说明
|—— Makefile // Makefile文件
|—— benchmark // 基准测试
|—— controllers // 控制器
|—— lib // 没有模块化的文件牡蛎
|—— middlewares // 中间件
|—— package.json // 包描述文件,项目依赖配置等
|—— proxy // 数据代理目录,类似MVC中的M
|—— test // 测试目录
|—— tools // 工具目录
|—— views // 视图目录
|—— routes.js // 路由注册表
|—— dispatch.js // 多进程管理
|—— README.md // 项目说明文件
|—— assets // 静态文件目录
|—— assets.json // 静态文件与CDN路径的映射文件
|—— bin // 可执行脚本
|—— config // 配置目录
|—— logs // 日志目录
|—— app.js // 工作进程

  1. 构建工具
  • makefile
  • grunt
  1. 编码规范
  • jslint
  • jshint
  1. 代码审查

部署流程

  1. 部署环境
  • stage环境,普通测试环境
  • pre-release环境,预发布环境
  • product环境,生产环境
  1. 部署操作

可以在主进程启动时将进程ID写到pid文件中,在脚本停止或者重启应用时通过kill给进程发SIGTERM信号,收到信号删除该pid文件并退出进程。

# 完整应用启动、停止和重启脚本
#!/bin/sh
DIR=`PWD`
NODE=`which node`
# get action
ACTION = $1

# help
usage() {
echo "Usage: ./appct1.sh {start|stop|restart}"
exit 1;
}

get_pid() {
if [ -f ./run/app.pid]; then
echo `cat ./run/app.pid`
fi
}

# start app
start() {
pid = `get_pid`

if [ l -z $pid]; then
echo 'server is already running'
else
$NODE $DIR/app.js 2>&1 &
echo 'server is running'
fi
}

# stop app
stop() {
pid=`get_pid`
if [ -z $pid ]; then
echo 'server not running'
else
echo 'server is stopping...'
kill -1$ $pid
echo "server stopped !"
fi
}
# restart app
restart() {
stop
sleep 0.5
echo ====
start
}

case "$ACTION" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
*(
usage
;;
esac

性能

拆分原则:做专一的事、让擅长的工具做擅长的事情、将模型简化、将风险分离、缓存

  1. 动静分离

可以将静态资源和请求让nginx或者cdn处理,node只处理动态内容

  1. 启动缓存

redis或memcached

  1. 多进程架构

cluster/pm/forever

  1. 读写分离

因为读取数据库速度远远高于写入的速度,通常会进行数据库的读写分离,将数据库进行主从设计,这样读数据操作不再受到写入的影响

日志

  1. 访问日志

  2. 异常日志

日志格式化记录

日志写入可以在线写,但建议进行离线数据分析,分析完成后同步到数据库。因为数据库写入会有一系列处理,消费速度低于写入速度,导致内存泄漏

  1. 日志分割

监控报警

  1. 监控
  • 日志监控
  • 响应时间,异常或性能瓶颈
  • 进程监控,多进程需要监控进程数量
  • 磁盘监控,监控日志频繁写导致磁盘空间被用光带来的问题
  • 内存监控,监控服务器内存使用情况,如果只升不降,那一定存在内存泄漏问题
  • CPU占用监控,用户态较高说明需要较高的CPU开销(一般低于70%),内核态较高说明花时间进行进程调度或系统调用(小于35%)
  • CPU load监控,CPU平均负载,过高说明进程数量过多,可能是重复启动新进程导致
  • I/O负载,反应磁盘读写情况
  • 网络监控,流入流量和流出流量
  • 应用状态监控,监控应用状态,最简单直接响应时间戳校验
  • DNS监控,DNSPod。

报警实现

  1. 邮件报警

nodemailer可实现

  1. 短信或电话报警

通过短信服务平台

稳定性

  • 多机器部署,利用更多的硬件资源,需要考虑负载均衡,状态共享和数据一致性。
  • 多机房部署,解决地理位置带来的延迟,并且可以互为灾备
  • 容灾备份,备份进行灾备

目前尽量部署在虚拟机上,避免实体机上多服务器同时停止服务

异构共存

Node能够通过协议与已有的系统很好地异构共存。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

【Leetcode 做题学算法周刊】第七期

首发于微信公众号《前端成长记》,写于 2020.01.15

背景

本文记录刷题过程中的整个思考过程,以供参考。主要内容涵盖:

  • 题目分析设想
  • 编写代码验证
  • 查阅他人解法
  • 思考总结

目录

Easy

121.买卖股票的最佳时机

题目地址

题目描述

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。

注意你不能在买入股票前卖出股票。

示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

示例 2:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0

题目分析设想

这道题,我的第一反应有点像求最大子序和,只不过这里不是求连续,是求单个,转换为增益的**来处理。当然也可以使用两次遍历的笨办法来求解。我们分别来验证一下。

编写代码验证

Ⅰ.两次遍历

代码:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (prices.length < 2) return 0
    // 因为是利润,所以不考虑负数
    let profit = 0
    for(let i = 0; i < prices.length; i++) {
        for(let j = i + 1; j < prices.length; j++) {
            profit = Math.max(prices[j] - prices[i], profit)
        }
    }
    return profit
};

结果:

  • 200/200 cases passed (384 ms)
  • Your runtime beats 25.89 % of javascript submissions
  • Your memory usage beats 19.85 % of javascript submissions (35.9 MB)
  • 时间复杂度 O(n^2)

Ⅱ.增益**

代码:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (prices.length < 2) return 0
    // 因为是利润,所以不考虑负数
    let profit = 0
    let last = 0
    for(let i = 0; i < prices.length - 1; i++) {
        // 这里其实可以转换为每两项价格相减后,再求最大子序和
        // prices[i + 1] - prices[i] 就是增益,和0比较是因为求利润,不是求连续和
        last = Math.max(0, last + prices[i + 1] - prices[i])
        profit = Math.max(profit, last)
    }
    return profit
};

结果:

  • 200/200 cases passed (64 ms)
  • Your runtime beats 94.53 % of javascript submissions
  • Your memory usage beats 19.85 % of javascript submissions (35.9 MB)
  • 时间复杂度 O(n)

查阅他人解法

这里看到两种不同的思考,一种是理解为波峰和波谷,找到波谷后的下一个波峰,判断每个波峰与波谷差值的大小。另外一种是基于状态机的动态规划,也就是说把可能性都前置运算后,再进行比较。

Ⅰ.波峰波谷

代码:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (prices.length < 2) return 0
    // 波谷
    let min = Infinity
    // 因为是利润,所以不考虑负数
    let profit = 0
    for(let i = 0; i < prices.length; i++) {
        if (prices[i] < min) {
            min = prices[i]
        } else if (prices[i] - min > profit) {
            // 这里是当前这个波峰和波谷的差值与历史的进行比较
            profit = prices[i] - min
        }
    }
    return profit
};

结果:

  • 200/200 cases passed (68 ms)
  • Your runtime beats 86.75 % of javascript submissions
  • Your memory usage beats 21.34 % of javascript submissions (35.8 MB)
  • 时间复杂度 O(n)

Ⅱ.动态规划

代码:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (prices.length < 2) return 0
    // 动态初始数组
    let dp = new Array(prices.length).fill([])
    // 0:用户手上不持股所能获得的最大利润,特指卖出股票以后的不持股,非指没有进行过任何交易的不持股
    // 1:用户手上持股所能获得的最大利润
    // 状态 dp[i][0] 表示:在索引为 i 的这一天,用户手上不持股所能获得的最大利润
    // 状态 dp[i][1] 表示:在索引为 i 的这一天,用户手上持股所能获得的最大利润
    // -prices[i] 就表示,在索引为 i 的这一天,执行买入操作得到的收益
    dp[0][0] = 0
    dp[0][1] = -prices[0]

    for(let i = 1; i < prices.length; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])
        dp[i][1] = Math.max(dp[i - 1][1], -prices[i])
    }
    return dp[prices.length - 1][0]
};

结果:

  • 200/200 cases passed (72 ms)
  • Your runtime beats 75.01 % of javascript submissions
  • Your memory usage beats 12.43 % of javascript submissions (36.7 MB)
  • 时间复杂度 O(n)

这个思路还有一系列的优化过程,可以点击这里查看

思考总结

很多问题都可以转换成动态规划的**来解决,但是我这里还是更推荐使用增益**,也可以理解为差分数组。但是如果题目允许多次买入卖出,我会更推荐使用动态规划来解决问题。

122.买卖股票的最佳时机Ⅱ

题目地址

题目描述

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 

示例 2:

输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5  (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
```javascript

示例 3:

```javascript
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0

题目分析设想

上面刚刚做了算最大收益的,这题明显是算累计收益的,所以可以按以下几个方向:

  • 一次遍历,直接遍历,不断比较前后两天价格,如果后一天收益高,则差值加到利润,可以理解为贪心算法。
  • 波峰波谷,找到所有波峰波谷,差值相加即可
  • 动态规划

编写代码验证

Ⅰ.一次遍历

代码:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    let profit = 0
    for(let i = 1; i < prices.length; i++) {
        if (prices[i] > prices[i - 1]) {
            profit += prices[i] - prices[i - 1]
        }
    }
    return profit
};

结果:

  • 201/201 cases passed (68 ms)
  • Your runtime beats 77.02 % of javascript submissions
  • Your memory usage beats 13.55 % of javascript submissions (35.7 MB)
  • 时间复杂度 O(n)

Ⅱ.波峰波谷

代码:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (!prices.length) return 0
    let profit = 0
    // 波峰波谷
    let min = max = prices[0]
    let i = 0
    while (i < prices.length - 1) {
        while(prices[i] >= prices[i + 1]) {
            i++
        }
        min = prices[i]
        while(prices[i] <= prices[i + 1]) {
            i++
        }
        max = prices[i]
        profit += max - min
    }
    return profit
};

结果:

  • 201/201 cases passed (68 ms)
  • Your runtime beats 77.02 % of javascript submissions
  • Your memory usage beats 14.4 % of javascript submissions (35.7 MB)
  • 时间复杂度 O(n)

Ⅲ.动态规划

代码:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (prices.length < 2) return 0
    // 动态初始数组
    let dp = new Array(prices.length).fill([])
    // 0:用户手上不持股所能获得的最大利润,特指卖出股票以后的不持股,非指没有进行过任何交易的不持股
    // 1:用户手上持股所能获得的最大利润
    // 状态 dp[i][0] 表示:在索引为 i 的这一天,用户手上不持股所能获得的最大利润
    // 状态 dp[i][1] 表示:在索引为 i 的这一天,用户手上持股所能获得的最大利润
    // -prices[i] 就表示,在索引为 i 的这一天,执行买入操作得到的收益
    dp[0][0] = 0
    dp[0][1] = -prices[0]

    for(let i = 1; i < prices.length; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])
        dp[i][1] = Math.max(dp[i - 1][1], dp[i][0] - prices[i])
    }
    return dp[prices.length - 1][0]
};

结果:

  • 201/201 cases passed (76 ms)
  • Your runtime beats 37.68 % of javascript submissions
  • Your memory usage beats 5.13 % of javascript submissions (36.7 MB)
  • 时间复杂度 O(n)

查阅他人解法

这里看到了动态规划的优化版,主要是降低空间复杂度。其他的思路都区别不大。

Ⅰ.动态规划优化版

代码:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (prices.length < 2) return 0
    // cash 表示持有现金
    // hold 表示持有股票
    let cash = new Array(prices.length).fill(null)
    let hold = new Array(prices.length).fill(null)

    cash[0] = 0
    hold[0] = -prices[0]

    for(let i = 1; i < prices.length; i++) {
        cash[i] = Math.max(cash[i - 1], hold[i - 1] + prices[i])
        hold[i] = Math.max(hold[i - 1], cash[i - 1] - prices[i])
    }
    return cash[prices.length - 1]
};

结果:

  • 201/201 cases passed (68 ms)
  • Your runtime beats 77.02 % of javascript submissions
  • Your memory usage beats 9.7 % of javascript submissions (36 MB)
  • 时间复杂度 O(n)

还可以进一步进行状态压缩

代码:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (prices.length < 2) return 0
    // cash 表示持有现金
    // hold 表示持有股票
    // 加了两个变量来存储上一次的值
    let cash = tempCash = 0
    let hold = tempHold = -prices[0]

    for(let i = 1; i < prices.length; i++) {
        cash = Math.max(tempCash, tempHold + prices[i])
        hold = Math.max(tempHold, tempCash - prices[i])

        tempCash = cash
        tempHold = hold
    }
    return tempCash
};

结果:

  • 201/201 cases passed (72 ms)
  • Your runtime beats 58.45 % of javascript submissions
  • Your memory usage beats 10.55 % of javascript submissions (35.8 MB)
  • 时间复杂度 O(n)

思考总结

就这道题而言,我会推荐使用一次遍历的方式,也就是贪心算法,理解起来会十分清晰。当然,动态规划的解决范围更广,基本上可以解决这类型的所有题目。增益也是一个比较常见的手段。总体而言,这两道股票题还比较简单。

125.验证回文串

题目地址

题目描述

给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。

说明:本题中,我们将空字符串定义为有效的回文串。

示例:

输入: "A man, a plan, a canal: Panama"
输出: true

输入: "race a car"
输出: false

题目分析设想

这道题我有两个方向,一是改变原输入串,二是不改变原输入串。

  • 改变原输入串,可以去掉非字母和数字的字符后,反转判断或者双指针判断或者单指针
  • 不改变原输入串,直接双指针判断

主要作答方法就是反转判断,双指针法以及二分法。

编写代码验证

Ⅰ.反转判断

代码:

/**
 * @param {string} s
 * @return {boolean}
 */
var isPalindrome = function(s) {
    // 正则去除不满足条件的字符
    let str = s.toLowerCase().replace(/[^0-9a-z]/g, '')
    return str === str.split('').reverse().join('')
};

结果:

  • 476/476 cases passed (72 ms)
  • Your runtime beats 95.33 % of javascript submissions
  • Your memory usage beats 47.7 % of javascript submissions (38.1 MB)
  • 时间复杂度: O(1)

Ⅱ.双指针法(预处理字符)

代码:

/**
 * @param {string} s
 * @return {boolean}
 */
var isPalindrome = function(s) {
    // 正则去除不满足条件的字符
    let str = s.toLowerCase().replace(/[^0-9a-z]/g, '')
    let len = str.length
    let l = 0
    let r = len - 1
    while(l < r) {
        if (str.charAt(l) !== str.charAt(r)) {
            return false
        }
        l++
        r--
    }
    return true
};

结果:

  • 476/476 cases passed (76 ms)
  • Your runtime beats 89.25 % of javascript submissions
  • Your memory usage beats 70.96 % of javascript submissions (37.4 MB)
  • 时间复杂度: O(n)

Ⅲ.单指针法(预处理字符)

代码:

/**
 * @param {string} s
 * @return {boolean}
 */
var isPalindrome = function(s) {
    // 正则去除不满足条件的字符
    let str = s.toLowerCase().replace(/[^0-9a-z]/g, '')
    let len = str.length
    // 最多需要判断的次数
    let max = len >>> 1
    let i = 0
    while(i < max) {
        if (len % 2) { // 奇数
            if (str.charAt(max - i - 1) !== str.charAt(max + i + 1)) {
                return false
            }
        } else { // 偶数
            if (str.charAt(max - i - 1) !== str.charAt(max + i)) {
                return false
            }
        }
        i++
    }
    return true
};

结果:

  • 476/476 cases passed (72 ms)
  • Your runtime beats 95.33 % of javascript submissions
  • Your memory usage beats 56.02 % of javascript submissions (38 MB)
  • 时间复杂度: O(n)

Ⅳ.双指针法

代码:

/**
 * @param {string} s
 * @return {boolean}
 */
var isPalindrome = function(s) {
    let len = s.length
    let l = 0
    let r = len - 1
    while (l < r) {
        if (!/[0-9a-zA-Z]/.test(s.charAt(l))) {
            l++
        } else if (!/[0-9a-zA-Z]/.test(s.charAt(r))) {
            r--
        } else {
            if(s.charAt(l).toLowerCase() !== s.charAt(r).toLowerCase()) {
                return false
            }
            l++
            r--
        }

    }
    return true
};

结果:

  • 476/476 cases passed (76 ms)
  • Your runtime beats 89.25 % of javascript submissions
  • Your memory usage beats 13.06 % of javascript submissions (42 MB)
  • 时间复杂度: O(n)

查阅他人解法

这里看到一种利用栈的思路,先进后出,推一半入栈然后进行比较。

Ⅰ.利用栈

代码:

/**
 * @param {string} s
 * @return {boolean}
 */
var isPalindrome = function(s) {
    // 正则去除不满足条件的字符
    let str = s.toLowerCase().replace(/[^0-9a-z]/g, '')
    let mid = str.length >>> 1
    let stack = str.substr(0, mid).split('')
    // 起始位置如果字符个数为奇数则跳过中间位
    for(let i = str.length % 2 ? mid + 1 : mid; i < str.length; i++) {
        const last = stack.pop()
        if (last !== str.charAt(i)) {
            return false
        }
    }
    return true
};

结果:

  • 476/476 cases passed (84 ms)
  • Your runtime beats 65.67 % of javascript submissions
  • Your memory usage beats 71.81 % of javascript submissions (37.4 MB)
  • 时间复杂度: O(n)

思考总结

总体而言,判断回文字符或者相关的题目,我更推荐采用双指针法,思路非常清晰。这里头尾递归比较也可以作答,就不在这里列举了。

136.只出现一次的数字

题目地址

题目描述

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明:

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

输入: [2,2,1]
输出: 1

输入: [4,1,2,1,2]
输出: 4

题目分析设想

这题说明了线性时间复杂度,所以最多一次遍历。很容易想到用 Hash 表或者其他方式对各数字出现次数做个统计来求解,但是需要考虑如何不适用额外空间。这里很明显就指向了离散数学中的异或运算。

  • Hash 法,需要额外 O(n) 的空间
  • 异或运算

编写代码验证

Ⅰ.Hash 法

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var singleNumber = function(nums) {
    let hash = {}
    for(let i = 0; i < nums.length; i++) {
        if (hash[nums[i]]) {
            hash[nums[i]] = false
        } else if (hash[nums[i]] === undefined) {
            hash[nums[i]] = true
        }
    }
    for(let i in hash) {
        if(hash[i]) {
            return parseInt(i)
        }
    }
};

结果:

  • 16/16 cases passed (72 ms)
  • Your runtime beats 68.39 % of javascript submissions
  • Your memory usage beats 5.49 % of javascript submissions (38.6 MB)
  • 时间复杂度: O(n)

Ⅱ.异或运算

简单列一下几条运算规则,利用这规则,发现很容易作答这道题。

  • 交换律: a^b^c = a^c^b
  • 任何数和 0 异或为本身:a^0 = a
  • 相同的数异或为 0:a^a = 0

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var singleNumber = function(nums) {
    let n = 0
    for(let i = 0; i < nums.length; i++) {
        n ^= nums[i]
    }
    return n
};

结果:

  • 16/16 cases passed (60 ms)
  • Your runtime beats 95.77 % of javascript submissions
  • Your memory usage beats 74.07 % of javascript submissions (35.3 MB)
  • 时间复杂度: O(n)

查阅他人解法

没有发现其他不同方向的解法。

思考总结

这里的话第一想法大多都是借助哈希表来实现,但是由于有补充说明,所以更推荐使用异或算法。纯粹是数学公式的应用场景之一,没有什么太多好总结的地方。

141.环形链表

题目地址

题目描述

给定一个链表,判断链表中是否有环。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

示例 1
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

进阶:

你能用 O(1)(即,常量)内存解决此问题吗?

题目分析设想

这道题的本质其实就是对象的比较,而对应的相等应当是引用同样的内存,可以想象成数组中找到同样的元素。所以第一个想法就是哈希表,当然也可以使用快慢指针来做处理。由于哈希表需要额外的内存,所以可以做优化,比如直接改变原对象,做特殊标识或者其他方式。

  • 哈希表,直接利用哈希表存储,也可以使用 Map/Set 等等,直接判断对象相等即可
  • 特殊标识,哈希表需要额外空间,可以直接在原对象上打标识,或者置为空等等特殊标识均可
  • 双指针法,一快一慢,如果是环,那必然会存在相等的时候,如果不是环,那快的先走完

编写代码验证

Ⅰ.哈希表

代码:

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    let hashArr = []
    // val 可能为 0 ,所以不能直接 !head
    while (head !== null) {
        if (hashArr.includes(head)) {
            return true
        } else {
            hashArr.push(head)
            head = head.next
        }
    }
    return false
};

结果:

  • 17/17 cases passed (116 ms)
  • Your runtime beats 12.03 % of javascript submissions
  • Your memory usage beats 5.05 % of javascript submissions (38.5 MB)
  • 时间复杂度: O(n)

Ⅱ.特殊标识法

代码:

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    while (head && head.next) {
        if (head.FLAG) {
            return true
        } else {
            head.FLAG = true
            head = head.next
        }
    }
    return false
};

结果:

  • 17/17 cases passed (76 ms)
  • Your runtime beats 78.6 % of javascript submissions
  • Your memory usage beats 16.32 % of javascript submissions (37.5 MB)
  • 时间复杂度: O(n)

Ⅲ.双指针法

代码:

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    if (head && head.next) {
        let slow = head
        let fast = head.next
        while(slow !== fast) {
            if (fast && fast.next) {
                // 快指针需要比慢指针移动速度快,才能追上,所以是 .next.next
                fast = fast.next.next
                slow = slow.next
            } else {
                // 快指针走到头了,所以必然不是环
                return false
            }
        }
        return true
    } else {
        return false
    }
};

结果:

  • 17/17 cases passed (76 ms)
  • Your runtime beats 78.6 % of javascript submissions
  • Your memory usage beats 56.97 % of javascript submissions (36.6 MB)
  • 时间复杂度: O(n)

查阅他人解法

这里发现一个有意思的思路,通过链路导致。如果是环,那么倒置后的尾节点等于倒置前的头节点。如果不是环,那么就是正常的倒置不相等。

Ⅰ.倒置法

代码:

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    if (head === null || head.next === null) return false
    if (head === head.next) return true
    let p = head.next
    let q = p.next
    let x = head
    head.next = null
    // 相当于每遍历一个链表,就把后面的指向前面一项,这样当循环的时候,会反方向走出环形
    while(q !== null) {
        p.next = x
        x = p
        p = q
        q = q.next
    }
    return p === head
};

结果:

  • 17/17 cases passed (72 ms)
  • Your runtime beats 90.05 % of javascript submissions
  • Your memory usage beats 35.91 % of javascript submissions (36.8 MB)
  • 时间复杂度: O(n)

思考总结

一般去重或者找到重复项用哈希的方式都能解决,但是在这题里,题目期望空间复杂度是 O(1),要么是改变原数据本身,要么是使用双指针法。这里我比较推荐双指针法,当然倒置法也比较巧妙。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

干货!从0开始,0成本搭建个人动态博客

首发于微信公众号《前端成长记》,写于 2019.10.12

导读

有句老话说的好,好记性不如烂笔头。人生中,总有那么些东西你愿去执笔写下。

本文旨在把整个搭建的过程和遇到的问题及解决方案记录下来,希望能够给你带来些许帮助。

本文涉及的主要技术:

在线查看

我的博客

背景

我的博客的折腾史分成下面三个阶段:

  • 基于 hexo 搭建静态博客,结合 Github Pages 提供域名和服务器资源

  • 自行采购服务器和域名,进行页面和接口的开发及部署,搭建动态博客

  • 基于 Github PagesGithub Api 搭建动态博客

第1种方式,文章内容采用 Markdown 编写,静态页面通过 hexo 生成,部署到 Github Pages 上。缺点很明显,每次有新内容,都需要重新编译部署。

第2种方式,灵活度极高,可以按需开发。缺点也很明显,开发和维护工作量大,同时还需要服务器和域名成本。

第3种方式,采用 ISSUE 来记录文章,天然支持 Markdown ,接口调用 Github Api,部署到 Github Pages 上。除了一次性开发外没有任何额外成本。

显而易见,本博客这次改版就是基于第3种方式来实现的,接下来我们从0开始一步步做。

技术选型

由于是个人博客,技术选型可以大胆尝试。

笔者选择了 vue-cli 进行项目结构的初始化,同时采用 vue3.x 的语法 Composition-Api 进行页面开发。采用 Github API v4 ,也就是 GraphQL 语法进行 API 调用。

上手开发

环境准备

node

前往 Node.js官网 下载,这里推荐下载 LTS 稳定版。下载后按照步骤进行安装操作即可。

Window 下记得选上 Add To Path ,保证全局命令可用

vue-cli

执行以下代码全局安装即可。

npm install -g @vue/cli

项目初始化

通过 vue-cli 来初始化项目,按照下面内容选择或自行按需选择。

vue create my-blog

init

完成初始化并安装依赖后,查看到的项目目录如下:

dir

其他依赖安装

  1. @vue/composition-api

使用 Vue 3.0 语法必要依赖

npm install @vue/composition-api --save
  1. graphql-request

简单轻巧的的 graphQL 客户端。同样还有 Apollo, Relay 等可以进行选择。选择它的理由是:简单轻巧,以及基于 Promise

npm install graphql-request --save
  1. github-markdown-css

使用 Github 的风格渲染 Markdown,选择它的理由是原汁原味。

npm install github-markdown-css --save

项目开发

我的博客之前是使用的 fexo 风格主题,所以本次也是以此为UI依据进行开发。

项目整体分成几个页面:

  • /archives 文章列表
  • /archives/:id 文章详情
  • /labels 标签列表
  • /links 友链
  • /about 关于
  • /board 留言板
  • /search 搜索

Ⅰ.请求封装

查看源码

import { GraphQLClient } from 'graphql-request';

import config from '../../config/config';
import Loading from '../components/loading/loading';

const endpoint = 'https://api.github.com/graphql';

const graphQLClient = new GraphQLClient(endpoint, {
  headers: {
    authorization: `bearer ${config.tokenA}${config.tokenB}`,
    'X-Requested-With': 'XMLHttpRequest',
    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  },
});

const Http = (query = {}, variables = {}, alive = false) => new Promise((resolve, reject) => {
  graphQLClient.request(query, variables).then((res) => {
    if (!alive) {
      Loading.hide();
    }
    resolve(res);
  }).catch((error) => {
    Loading.hide();
    reject(error);
  });
});

export default Http;

我们可以看到配置了 headers ,这里是 Github Api 要求的鉴权。

这里有两个坑,只有在打包提交代码后才发现:

  1. token 不能直接提交到 Github,否则再使用时会发现失效。这里我猜测是安全扫描机制,所以我上面将 token 分成两部分拼接绕过这个。

  2. Content-Type 需要设置成 x-www-form-urlencoded ,否则会跨域请求失败。

接下来我们将修改 main.js 文件,将请求方法挂载到 Vue实例 上。

...
import Vue from 'vue';
import Http from './api/api';

Vue.prototype.$http = Http;
...

Ⅱ.文章列表开发

查看源码

主要将介绍 composition-apigraphqh 相关,其余部分请查阅 Vue文档

我们首先需要引入 composition-api ,修改 main.js 文件

...
import Vue from 'vue';
import VueCompositionApi from '@vue/composition-api';

Vue.use(VueCompositionApi);
...

然后新建一个 Archives.vue 去承接页面内容。

首先的变动是生命周期的变动,使用 setup 函数代替了之前的 beforeCreatecreated 钩子。值得注意的有两点:

  1. 该函数和 Templates 一起使用时返回一个给 template 使用的数据对象。
  2. 该函数内没有 this 对象,需使用 context.root 获取到根实例对象。
...
export default {
  setup (props, context) {
    // 通过context.root获取到根实例,找到之前挂载在Vue实例上的请求方法
    context.root.$http(xxx)
  }
}
...

数据查询的语法参考 Github Api

我这里是以 blog 仓库 的 issue 来作为文章的,所以我这里的查询语法大致意思:

按照 ownername 去查仓库, ownerGithub 账号,name 是仓库名称。
查询 issues 文章列表,按照创建时间倒序返回,first 表示每次返回多少条。after 表示从哪开始查。所以结合这个就很容易实现分页,代码如下:

...
// 引入,使用 reactive 创建响应式对象
import {
  reactive,
} from '@vue/composition-api';
export default {
  setup (props, context) {
    const archives = reactive({
      cursor: null
    });
    const query = `query {
      repository(owner: "ChenJiaH", name: "blog") {
        issues(orderBy: {field: CREATED_AT, direction: DESC}, labels: null, first: 10, after:${archives.cursor}) {
          nodes {
            title
            createdAt
            number
            comments(first: null) {
              totalCount
            }
          }
          pageInfo {
            endCursor
            hasNextPage
          }
        }
      }
    }`;
    // 通过context.root获取到根实例,找到之前挂载在Vue实例上的请求方法
    context.root.$http(query).then(res => {
      const { nodes, pageInfo } = res.repository.issues
      archives.cursor = `"${pageInfo.endCursor}"`  // 最后一条的标识,需要用 "" 传入
    })
  }
}
...

Ⅲ.标签列表开发

查看源码

这里我没有找到 issues 中返回全部的 labels 数据,所以只能先查全部 label ,再默认查询第一项 label ,语法如下:

...
const getData = () => {
    const query = `query {
        repository(owner: "ChenJiaH", name: "blog") {
          issues(filterBy: {labels: "${archives.label}"}, orderBy: {field: CREATED_AT, direction: DESC}, labels: null, first: 10, after: ${archives.cursor}) {
            nodes {
              title
              createdAt
              number
              comments(first: null) {
                totalCount
              }
            }
            pageInfo {
              endCursor
              hasNextPage
            }
            totalCount
          }
        }
      }`;
    context.root.$http(query).then((res) => {
      ...
    });
  };
  const getLabels = () => {
    context.root.$loading.show('努力为您查询');
    const query = `query {
      repository(owner: "ChenJiaH", name: "blog") {
        labels(first: 100) {
          nodes {
            name
          }
        }
      }
    }`;
    context.root.$http(query).then((res) => {
      archives.loading = false;
      archives.labels = res.repository.labels.nodes;

      if (archives.labels.length) {
        archives.label = archives.labels[0].name;

        getData();
      }
    });
  };
...

Ⅳ.文章详情开发

查看源码

文章详情分成两部分:文章详情查询和文章评论。

  1. 文章详情查询

这里首先引入 github-markdown-css 的样式文件,然后给 markdown 的容器加上 markdown-body 的样式名,内部将会自动渲染成 github 风格的样式。

...
<template>
...
    <div class="markdown-body">
      <p class="cont" v-html="issue.bodyHTML"></p>
    </div>
...
</template>
<script>
import {
  reactive,
  onMounted,
} from '@vue/composition-api';
import { isLightColor, formatTime } from '../utils/utils';

export default {
    const { id } = context.root.$route.params;  // 获取到issue id
    const getData = () => {
      context.root.$loading.show('努力为您查询');
      const query = `query {
          repository(owner: "ChenJiaH", name: "blog") {
            issue(number: ${id}) {
              title
              bodyHTML
              labels (first: 10) {
                nodes {
                  name
                  color
                }
              }
            }
          }
        }`;
      context.root.$http(query).then((res) => {
        const { title, bodyHTML, labels } = res.repository.issue;
        issue.title = title;
        issue.bodyHTML = bodyHTML;
        issue.labels = labels.nodes;
      });
    };
};
</script>
<style lang="scss" scoped>
  @import "~github-markdown-css";
</style>
...

注意这里有个label颜色的获取

众所周知,Github Label 的字体颜色是根据背景色自动调节的,所以我这里封装了一个方法判断是否为亮色,来设置文字颜色。

// isLightColor
const isLightColor = (hex) => {
  const rgb = [parseInt(`0x${hex.substr(0, 2)}`, 16), parseInt(`0x${hex.substr(2, 2)}`, 16), parseInt(`0x${hex.substr(4, 2)}`, 16)];
  const darkness = 1 - (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;
  return darkness < 0.5;
};
  1. 文章评论部分

这里我采用的是 utterances ,请按照步骤初始化项目,Blog Post 请选择 Specific issue number ,这样评论才会是基于该 issue 的,也就是当前文章的。然后在页面中按下面方式配置你的相关信息引入:

...
import {
  reactive,
  onMounted,
} from '@vue/composition-api';
export default {
  setup(props, context) {
    const { id } = context.root.$route.params;  // issue id
    const initComment = () => {
      const utterances = document.createElement('script');
      utterances.type = 'text/javascript';
      utterances.async = true;
      utterances.setAttribute('issue-number', id);
      utterances.setAttribute('theme', 'github-light');
      utterances.setAttribute('repo', 'ChenJiaH/blog');
      utterances.crossorigin = 'anonymous';
      utterances.src = 'https://utteranc.es/client.js';

      // 找到对应容器插入,我这里用的是 comment
      document.getElementById('comment').appendChild(utterances);
    };

    onMounted(() => {
      initComment();
    });    
  }
}
...

这个方案的好处是:数据完全来自 Github Issue ,并且自带登录体系,非常方便。

Ⅴ.留言板开发

查看源码

刚好上面部分提到了 utterances ,顺势基于这个开发留言板,只需要把 Blog Post 更换成其他方式即可,我这里选择的是 issue-term ,自定义标题的单条 Issue 下留言。为了避免跟文章那里区分,所以我使用另外一个仓库来管理留言。实现代码如下:

...
import {
  onMounted,
} from '@vue/composition-api';

export default {
  setup(props, context) {
    context.root.$loading.show('努力为您查询');

    const initBoard = () => {
      const utterances = document.createElement('script');
      utterances.type = 'text/javascript';
      utterances.async = true;
      utterances.setAttribute('issue-term', '【留言板】');
      utterances.setAttribute('label', ':speech_balloon:');
      utterances.setAttribute('theme', 'github-light');
      utterances.setAttribute('repo', 'ChenJiaH/chenjiah.github.io');
      utterances.crossorigin = 'anonymous';
      utterances.src = 'https://utteranc.es/client.js';

      document.getElementById('board').appendChild(utterances);

      utterances.onload = () => {
        context.root.$loading.hide();
      };
    };

    onMounted(() => {
      initBoard();
    });
  },
};
...

Ⅵ.搜索页开发

查看源码

这里碰到一个坑,找了很久没有找到模糊搜索对应的查询语法。

这里感谢一下 simbawus ,解决了查询语法的问题。具体查询如下:

...
      const query = `query {
        search(query: "${search.value} repo:ChenJiaH/blog", type: ISSUE, first: 10, after: ${archives.cursor}) {
          issueCount
          pageInfo {
            endCursor
            hasNextPage
          }
          nodes {
            ... on Issue {
              title
              bodyText
              number
            }
          }
        }
      }`;
...

还好有 ... 拓展运算符,要不然 nodes 这里面的解析格式又不知道该怎么写了。

Ⅶ.其他页面开发

其他页面多数为静态页面,所以按照相关的语法文档开发即可,没有什么特别的难点。

另外我这也未使用 composition-api 的全部语法,只是根据项目需要进行了一个基本的尝试。

项目发布和部署

项目的提交

项目的提交采用 commitizen ,采用的理由是:提交格式规范化,可以快速生成变更日志等,后期可做成自动化。参考对应使用使用步骤使用即可。

项目的版本管理

项目的版本管理采用 Semantic Versioning 2.0.0

项目的部署

编写了一个 deploy.sh 脚本,并配置到 package.json 中。执行 npm run deploy 将自动打包并推送到 gh-pages 分支进行页面的更新。

// package.json
{
  ...
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "inspect": "vue-cli-service inspect",
    "deploy": "sh build/deploy.sh",
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md"
  },
 ...
}
#!/usr/bin/env sh

set -e

npm run build

cd dist

git init
git config user.name 'McChen'
git config user.email '[email protected]'
git add -A
git commit -m 'deploy'

git push -f [email protected]:ChenJiaH/blog.git master:gh-pages

cd -

gh-pages 的使用需要先创建 用户名.github.io 的仓库

结尾

至此,一个0成本的动态博客已经完全搭建好了。开发过程中还遇到了一些 eslint 相关的提示和报错,直接搜索基本可解决。

如有疑问或不对之处,欢迎留言。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

有意思的前端面试题收录分析解答

首发于微信公众号《前端成长记》,写于 2017.01.20

介绍

近期看见一些有意思的前端面试题,激起了我心中的火,故收录并尝试解答分析

面试题

  1. 写一个mul函数调用时生成如下输出:
console.log(mul(2)(3)(4)); // output : 24 
console.log(mul(4)(3)(4)); // output : 48 

即乘法。

** 解答 **

function mul(a){
    var result = function(b){
        return mul(a * b)
    };
    result.valueOf = function(){
        return a;
    }
    return result
}

** 分析 **

初一看这道题,被三个()看的有的晕乎,心想两个的我见过,三个的还真是头一回。

仔细琢磨下,发现这其中需要实现的就是需要保留每次执行函数的结果,用于跟下一次的入参做乘法;同时函数需要自执行,因为最终需要输出结果而不是Function

得出这几点:

  • 该mul函数返回结果肯定是一个Function Object,而且该函数的返回值应该是两次入参的乘积
  • 在return之前肯定进行取值操作了

然后我们通过解答去分析,mul(2)的执行结果是 function(b){ return mul(2 * y)},但是,该函数进行了valueOf操作,所以第一次执行的结果是 mul(2 * y),接下来,将3传入,所以此时的结果是mul(6 * y),注意这时候的6是刚才的结果,y是接下来传入的参数值,同理传入4的时候没有y值了,所以最终的返回值为 2 * 3 * 4=24。

** 出处 **

https://www.zhihu.com/question/54822257

  1. Concatenating functions

Functional programming thrives from the reuse of functions. One core feature to extend the reuse is the concatenation of functions.

You probably know this feature from your favorite shell: ls -la | sort | head lists the top lines of the sorted result of ls -la

Build a function pipe to achieve this with JS. An example use could be:

var addOne = function(e) {
    return e + 1;
};
var square = function(e) {
    return e * e;
};
var result = [1,2,3,4,5].map(addOne.pipe(square)) //-> [4,9,16,25,36]

Since a function only can return one value it is absolutely sufficient to only support functions that consume only one parameter. Build your pipe function in a way, that one can pipe an arbitrary number of functions.

** 解答 **

// just a small amount of possible functions to start testing with.
var addOne = function(e) {return e + 1;};
var square = function(e) {return e * e;};
// Extend the Function prototype with a method pipe
Function.prototype.pipe = function(input){
  var func = this;
  return function(num){
    return input(func(num));
  };
};

** 分析 **

这道题在我看来是将函数作为入参,另外考察数组map的使用

我们从 addOne.pipe(square) 看,这里说明pipe接收一个Fn作为参数,同时很显示,输出是原数组的每项加一的平方。即可以得出:

Function.prototype.pipe = function (square){
    // 里面一定有
    // addOne(number);    // 这里number是map方法的第一个参数
    // 接着pipe(square)这里必须是需要返回number+1的平方,细看square方法,如果我们把addOne(number)+1作为入参传入square,是不是就OK了呢,于是:
    return function (number){
        return square(addOne(number))
    }
    // 然后再优化一下写法
    return function (number) {
        return square(this(number))
    }.bind(this);

如果用ES6的写法就更简单了,直接 ...args

Function.prototype.pipe = function (square) {
  return (...args) => square(this(...args))
}

** 出处 **

https://www.codewars.com/kata/concatenating-functions/javascript

长期收集补充,欢迎大家提供有趣的前端面试题

此收录长期有效。

如果您也遇见或者接触到有意思的前端题目,欢迎留言给我,我们一起做这个有意义有价值的事情。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

50个实用jquery代码段整理

首发于微信公众号《前端成长记》,写于 2015.08.11

介绍

JQuery有一些使用频率比较高的方法,下面我做了一份大概的整理。

实用方法

1. 创建嵌套的过滤器

//允许你减少集合中的匹配元素的过滤器,
//只剩下那些与给定的选择器匹配的部分。在这种情况下,
//查询删除了任何没(:not)有(:has)
//包含class为“selected”(.selected)的子节点。
.filter(":not(:has(.selected))")

2. 如何重用元素搜索

var allItems = $("div.item");
var keepList = $("div#container1 div.item");
//现在你可以继续使用这些jQuery对象来工作了。例如,
//基于复选框裁剪“keep list”,复选框的名称
//符合
$(formToLookAt + " input:checked").each(function() {
    keepList = keepList.filter("." + $(this).attr("name"));
});

3. 任何使用has()来检查某个元素是否包含某个类或是元素

//jQuery 1.4.*包含了对这一has方法的支持。该方法找出
//某个元素是否包含了其他另一个元素类或是其他任何的
//你正在查找并要在其之上进行操作的东东。
$("input").has(".email").addClass("email_icon");

4. 如何使用jQuery来切换样式表

//找出你希望切换的媒体类型(media-type),然后把href设置成新的样式表。
$('link[media='screen']').attr('href', 'Alternative.css');

5. 如何限制选择范围(基于优化目的)

//尽可能使用标签名来作为类名的前缀,
//这样jQuery就不需要花费更多的时间来搜索
//你想要的元素。还要记住的一点是,
//针对于你的页面上的元素的操作越具体化,
//就越能降低执行和搜索的时间。
var in_stock = $('#shopping_cart_items input.is_in_stock');

6. 如何正确地使用ToggleClass

//切换(toggle)类允许你根据某个类的
//是否存在来添加或是删除该类。
//这种情况下有些开发者使用:
a.hasClass('blueButton') ? a.removeClass('blueButton') : a.addClass('blueButton');
//toggleClass允许你使用下面的语句来很容易地做到这一点
a.toggleClass('blueButton');

7. 如何设置IE特有的功能

if ($.browser.msie) {
// Internet Explorer就是个虐待狂
}

8. 如何使用jQuery来代替一个元素

$('#thatdiv').replaceWith('fnuh');

9. 如何验证某个元素是否为空

if ($('#keks').html()) {
//什么都没有找到;
}

10. 如何从一个未排序的集合中找出某个元素的索引号

$("ul > li").click(function () {
    var index = $(this).prevAll().length;
});

11. 如何把函数绑定到事件上

$('#foo').bind('click', function() {
 	alert('User clicked on "foo."');
});

12. 如何追加或是添加html到元素中

$('#lal').append('sometext');

13. 在创建元素时,如何使用对象字面量(literal)来定义属性

var e = $("", { href: "#", class: "a-class another-class", title: "..." });

14. 如何使用多个属性来进行过滤

//在使用许多相类似的有着不同类型的input元素时,
//这种基于精确度的方法很有用
var elements = $('#someid input[type=sometype][value=somevalue]').get();

15. 如何使用jQuery来预加载图像:

jQuery.preloadImages = function() {
    for(var i = 0; i < arguments.length; i++) {
        $("<img />").attr('src', arguments[i]);
    }
};
//用法
$.preloadImages('image1.gif', '/path/to/image2.png', 'some/image3.jpg');

16. 如何为任何与选择器相匹配的元素设置事件处理程序

$('button.someClass').live('click', someFunction);
//注意,在jQuery 1.4.2中,delegate和undelegate选项
//被引入代替live,因为它们提供了更好的上下文支持
//例如,就table来说,以前你会用
//.live()
$("table").each(function(){
    $("td", this).live("hover", function(){
        $(this).toggleClass("hover");
    });
});
//现在用
$("table").delegate("td", "hover", function(){
    $(this).toggleClass("hover");
});

17. 如何找到一个已经被选中的option元素

$('#someElement').find('option:selected');

18. 如何隐藏一个包含了某个值文本的元素

$("p.value:contains('thetextvalue')").hide();

19. 如果自动滚动到页面中的某区域

jQuery.fn.autoscroll = function(selector) {
    $('html,body').animate(
        {scrollTop: $(selector).offset().top},
        500
    };
}
//然后像这样来滚动到你希望去到的class/area上。
$('.area_name').autoscroll();

20. 如何检测各种浏览器

检测Safari (if($.browser.safari)),
检测IE6及之后版本 (if ($.browser.msie && $.browser.msie && $.browser.version > 6 )),
检测IE6及之前版本 (if ($.browser.msie && $.browser.msie && $.browser.version <= 6 )),
检测FireFox 2及之后版本 (if ($.browser.mozilla && $.browser.mozilla && $.browser.version >= ‘1.8 ))

21. 如何替换串中的词

var el = $('#id');
el.html(el.html().replace(/word/ig, ''));

22. 如何禁用右键单击上下文菜单

$(document).bind('contextmenu',function(e){
    return false;
});

23. 如何定义一个定制的选择器

$.expr[':'].mycustomselector = function(element, index, meta, stack){
// element- 一个DOM元素
// index – 栈中的当前循环索引
// meta – 有关选择器的元数据
// stack – 要循环的所有元素的栈
// 如果包含了当前元素就返回true
// 如果不包含当前元素就返回false };
// 定制选择器的用法:
$('.someClasses:test').doSomething();

24. 如何检查某个元素是否存在

if ($('#someDiv').length) {
//**!!!它存在……
}

25. 如何使用jQuery来检测右键和左键的鼠标单击两种情况

$("#someelement").live('click', function(e) {
    if( (!.browser.msie && e.button == 0) || (.browser.msie && e.button == 0) || (.browser.msie && e.button == 1) ) {
    alert("Left Mouse Button Clicked");
    } else if(e.button == 2) {
        alert("Right Mouse Button Clicked");
    }
});

26. 如何显示或是删除input域中的默认值

//这段代码展示了在用户未输入值时,
//如何在文本类型的input域中保留
//一个默认值
wap_val = [];
$(".swap").each(function(i){
    wap_val[i] = $(this).val();
    $(this).focusin(function(){
    if ($(this).val() == swap_val[i]) {
        $(this).val("");
    }
}).focusout(function(){
    if (.trim(.trim((this).val()) == "") {
        $(this).val(swap_val[i]);
    }});
});
<input type="text" value="Enter Username here.." />

27. 如何在一段时间之后自动隐藏或关闭元素(支持1.4版本)

//这是1.3.2中我们使用setTimeout来实现的方式
setTimeout(function() {
    $('.mydiv').hide('blind', {}, 500)
}, 5000);
//而这是在1.4中可以使用delay()这一功能来实现的方式(这很像是休眠)
$(".mydiv").delay(5000).hide('blind', {}, 500);

28. 如何把已创建的元素动态地添加到DOM中

var newDiv = $('');
newDiv.attr('id','myNewDiv').appendTo('body');

29. 如何限制“Text-Area”域中的字符的个数

jQuery.fn.maxLength = function(max){
    this.each(function(){
        var type = this.tagName.toLowerCase();
        var inputType = this.type? this.type.toLowerCase() : null;
        if(type == "input" && inputType == "text" || inputType == "password"){
        //Apply the standard maxLength
        this.maxLength = max;
        }
        else if(type == "textarea"){
            this.onkeypress = function(e){
                var ob = e || event;
                var keyCode = ob.keyCode;
                var hasSelection = document.selection? document.selection.createRange().text.length > 0 : this.selectionStart != this.selectionEnd;
                return !(this.value.length >= max && (keyCode > 50 || keyCode == 32 || keyCode == 0 || keyCode == 13) && !ob.ctrlKey && !ob.altKey && !hasSelection);
            };
            this.onkeyup = function(){
                if(this.value.length > max){
                    this.value = this.value.substring(0,max);
                }
            };
        }
    });
};
//用法
$('#mytextarea').maxLength(500);

30. 如何为函数创建一个基本的测试

//把测试单独放在模块中
module("Module B");
test("some other test", function() {
//指明测试内部预期有多少要运行的断言
    expect(2);
//一个比较断言,相当于JUnit的assertEquals
    equals( true, false, "failing test" );
    equals( true, true, "passing test" );
});

31. 如何在jQuery中克隆一个元素

var cloned = $('#somediv').clone();

32. 在jQuery中如何测试某个元素是否可见

if($(element).is(':visible') == 'true') {
//该元素是可见的
}

33. 如何把一个元素放在屏幕的中心位置

jQuery.fn.center = function () {
    this.css('position','absolute');
    this.css('top', ( (window).height()−this.height())/+(window).height()−this.height())/+(window).scrollTop() + 'px');
    this.css('left', ( (window).width()−this.width())/2+(window).width()−this.width())/2+(window).scrollLeft() + 'px');
    return this;
}
//这样来使用上面的函数:
$(element).center();

34. 如何把有着某个特定名称的所有元素的值都放到一个数组中

var arrInputValues = new Array();
$("input[name='table[]']").each(function(){
    arrInputValues.push($(this).val());
});

35. 如何从元素中除去html

(function($) {
    $.fn.stripHtml = function() {
    var regexp = /<("[^"]*"|'[^']*'|[^'">])*>/gi;
    this.each(function() {
        (this).html((this).html((this).html().replace(regexp,") );});
        return $(this);
    }
})(jQuery);
//用法:
$('p').stripHtml();

36. 如何使用closest来取得父元素

$('#searchBox').closest('div');

37. 如何使用Firebug和Firefox来记录jQuery事件日志

// 允许链式日志记录
// 用法:
$('#someDiv').hide().log('div hidden').addClass('someClass');
jQuery.log = jQuery.fn.log = function (msg) {
    if (console){
      console.log("%s: %o", msg, this);
    }
    return this;
};

38. 如何强制在弹出窗口中打开链接

jQuery('a.popup').live('click', function(){
    newwindow=window.open($(this).attr('href'),'','height=200,width=150');
    if (window.focus) {
        newwindow.focus();
    }
    return false;
});

39. 如何强制在新的选项卡中打开链接

jQuery('a.newTab').live('click', function(){
    newwindow=window.open($(this).href);
    jQuery(this).target = "_blank";
    return false;
});

40. 在jQuery中如何使用.siblings()来选择同辈元素

// 不这样做
$('#nav li').click(function(){
    $('#nav li').removeClass('active');
    $(this).addClass('active');
});
//替代做法是
$('#nav li').click(function(){
    $(this).addClass('active').siblings().removeClass('active');
});

41. 如何切换页面上的所有复选框

var tog = false;
// 或者为true,如果它们在加载时为被选中状态的话
$('a').click(function() {
    $("input[type=checkbox]").attr("checked",!tog);
    tog = !tog;
});

42. 如何基于一些输入文本来过滤一个元素列表

//如果元素的值和输入的文本相匹配的话
//该元素将被返回
$('.someClass').filter(function() {
    return (this).attr(′value′)==(this).attr(′value′)==('input#someId').val();
})

43. 如何获得鼠标垫光标位置x和y

$(document).ready(function() {
    $(document).mousemove(function(e){
        $(#XY’).html(”X Axis :  + e.pageX +  | Y Axis  + e.pageY);
    });
});

44. 如何把整个的列表元素(List Element,LI)变成可点击的

$("ul li").click(function(){
    window.location=$(this).find("a").attr("href");
    return false;
});
<ul>
<li><a href="#">Link 1</a></li>
<li><a href="#">Link 2</a></li>
<li><a href="#">Link 3</a></li>
<li><a href="#">Link 4</a></li>
</ul>

45. 如何使用jQuery来解析XML(基本的例子)

function parseXml(xml) {
//找到每个Tutorial并打印出author
    $(xml).find("Tutorial").each(function() {
        ("#output").append(("#output").append((this).attr("author") + "");
    });
}

46. 如何检查图像是否已经被完全加载进来

$('#theImage').attr('src', 'image.jpg').load(function() {
    alert('This Image Has Been Loaded');
});

47. 如何使用jQuery来为事件指定命名空间

//事件可以这样绑定命名空间
$('input').bind('blur.validation', function(e){
// ...
});
//data方法也接受命名空间
$('input').data('validation.isValid', true);

48. 如何检查cookie是否启用

var dt = new Date();
dt.setSeconds(dt.getSeconds() + 60);
document.cookie = "cookietest=1; expires=" + dt.toGMTString();
var cookiesEnabled = document.cookie.indexOf("cookietest=") != -1;
if(!cookiesEnabled) {
//没有启用cookie
}

49. 如何让cookie过期

var date = new Date();
date.setTime(date.getTime() + (x * 60 * 1000));
$.cookie('example', 'foo', { expires: date });

50. 如何使用一个可点击的链接来替换页面中任何的URL

$.fn.replaceUrl = function() {
    var regexp = /((ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/gi;
    this.each(function() {
        $(this).html(
            (this).html().replace(regexp,(this).html().replace(regexp,′1‘)
        );
    });
    return $(this);
}
//用法 
$('p').replaceUrl();

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

现代模块加载机制解析

首发于微信公众号《前端成长记》,写于 2016.05.31

前言

随着近几年模块化被越来越多的前端人士所念叨,我也义无反顾的涌入了这股大潮,本文将浅析个人的模块加载理解。

分析

本文不对具体的库进行分析,只从宏观上介绍一些核心的概念。

模拟module函数

var MyModules = (function Manager() {
    var modules = {};
    function define(name, deps, impl) {
        for (var i = 0; i < deps.length; i++) {
            deps[i] = modules[deps[i]];
        }
        modules[name] = impl.apply(impl, deps);
    }
     function get(name) {
        return modules[name];
    }
    return {
        define: define,
        get: get
    }
})();

这里核心是 modules[name] = impl.apply(impl, deps),为了模块的定义引入包装函数,并可传入依赖,返回值,也就是模块的API了,根据key存在modules列表中。

定义模块

MyModules.define("moduleA", [], function () {
    function hello(who){
        return "Let me introduce:" + who;
    }
    return {
        hello: hello
    }
});
MyModules.define("moduleB", ["moduleA"], function (module) {
    var hungry = "hippo";
    function awesome(){
        console.log(module.hello(hungry).toUpperCase());
    }
    return {
        awesome: awesome
    }
});
var moduleA = MyModules.get("moduleA");
var moduleB = MyModules.get("moduleB");
console.log(moduleA.hello("hippo"));
moduleB.awesome();

moduleA和moduleB都是通过一个返回公共API的函数来定义的。模块间也可以相互依赖并使用,这便是模块化加载的核心所在。

未来的模块机制(ES6)

ES6的模块没有“行内”格式,必须被定义在独立的文件中才行,浏览器或引擎有一个默认的“模块加载器”

// moduleA.js
function hello(who){
    return "Let me introduce:" + who;
}
export hello;
// moduleB.js
import hello form "moduleA";
var hungry = "hippo";
function awesome(){
    console.log(hello(hungry).toUpperCase());
}
export awesome;
// main.js
import moduleA form "moduleA";
import moduleB form "moduleB";
console.log(bar.hello("rhino"));
foo.awesome();

需要把代码片段提取出来分单独的js文件。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

jm-base

首发于微信公众号《前端成长记》,写于 2016.09.12

介绍

任何环境下通用的工具函数(封装了一些常用方法)

安装

npm install jm-base

属性

version

版本号

timestamp

更新时间戳

API

animate(obj, target, time, type)

字段 类型 默认值 说明
obj Object jquery对象
target String "无" transform属性值
time String "无" 动画时间,单位/s
type String ease-out 动画过渡方式

ajax(config)

config为参数配置对象

属性值 类型 默认值 说明
url String "无" request url
data Object {} request params
type String "get" 请求类型,可选"get"、"post"
dataType String "json" 数据类型,可选"json"、"jsonp"
traditional Boolean false  
callback Function callback(data) 成功回调函数
failFn Function failFn() 失败回调函数

createDiv(className, innerHtml)

属性值 类型 默认值 说明
className String "无" class名
innerHtml String {} inner内容

返回一个原生dom对象

format(str, data)

属性值 类型 默认值 说明
str String "无" html模板
data JSON {} 数据

返回一个填充数据的html片段

formatTime(time, format)

属性值 类型 默认值 说明
time Number 时间戳
format String "无" 输出时间格式 “yyyy-MM-dd HH:mm:ss” 年月日 时分秒 格式自定义

返回一个自定义格式化好的时间字符串

getBrowserInfo(time, format)

返回一个结果对象,属性值有 systemTypeappType

systemType属性值 | 类型 | 默认值 | 说明
-- | -- | --
iOS | Boolean | 无 | 是否iOS系统
Android | Boolean | 无 | 是否Android系统

appType属性值 | 类型 | 默认值 | 说明
-- | -- | --
mqq | Boolean | 无 | 是否处于手机qq环境
wx | Boolean | 无 | 是否处于微信环境
jdApp | Boolean | 无 | 是否处于京东APP环境
jrApp | Boolean | 无 | 是否处于金融APP环境
wyApp | Boolean | 无 | 是否处于京东钱包APP环境
jdStock | Boolean | 无 | 是否处于京东股票APP环境

getCookie(name)

属性值 类型 默认值 说明
name String cookie-name

返回cookie值

getUrlString(name)

属性值 类型 默认值 说明
name String url参数

返回url参数值

lazyLoad($images)

属性值 类型 默认值 说明
$images Object $("img[data-src]") 需要懒加载的图片对象

返回一个是否完成的标志

pageLock()

移动端页面移动锁定

pageUnlock()

移动端页面移动解锁

toast(obj)

传入一个toast配置对象

参数值 类型 默认值 说明
text String "无" toast信息
pos String/Object "top" toast位置字符串或者css配置对象
autoClose Boolean true 是否自动消失
maxWidth String "无" 最大宽度

适用环境

  • 兼容AMD/CMD
  • 支持Node.js
// import jm from 'jm'; // ES6
var jm = require('jm');
jm.{API}

License

MIT

Github

Github地址及使用文档:jm-base

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

vue入门实例-基于vue&vue-router重写Cnode社区

首发于微信公众号《前端成长记》,写于 2016.12.23

介绍

涉及到的技术包括但不限于:

  • vue
  • vue-router
  • webpack
  • zepto
  • fastclick
  • scss

Tips: vue&vue-router 使用的为 2.x 版

线上地址

https://chenjiahao.xyz/vue-cnode

开发步骤

最初的设想是完完全全由自己构建,这样能够熟悉全部的过程。

  • 创建项目目录层级(按照脚手架)
  • 创建package.json,然后安装什么babel,各种loader等等一大堆乱七八糟
  • 创建webpack.config.js,配置入口js,生成文件路径,预加载和加载资源等等
  • 引入webpack plugins,通过这个webpack-hot-middleware实现重载

配置了一大堆之后,发现还有问题,然后还需要考虑build的配置,遂,猝,享年1小时。然后果断选择vue官网脚手架入手。

通过vue官网提供的命令行工具快速搭建一个应用

# 全局安装 vue-cli
$ npm install --global vue-cli
# 创建一个基于 webpack 模板的新项目
$ vue init webpack my-project
# 安装依赖,走你
$ cd my-project
$ npm install
$ npm run dev

这个时候你会发现 my-project 项目已经自动创建,文件路径大致如下

image

文章编写时 cli 版本为 2.x

取Cnode社区最常用的主题页相关入手

大概可以分为主题页,主题详情页,评论点赞等涉及到登录的这里暂时跳过。

我这里全部采用单文件组件方式开发,即 *.vue

什么是单文件组件?

与vue相关的资源全部放在src文件夹下,无需vue渲染的资源放到static文件夹下,附上开发文件目录

image

  1. 我们先来看看main.js
// 引入vue
import Vue from 'vue';
// 引入zepto
import $ from 'webpack-zepto';
// 引入vue-router
import VueRouter from 'vue-router';
// 引入路由配置文件
import routes from './routes.js';
//  引入fastclick
import FastClick from 'fastclick';
// 引入页面入口文件,其实这个可以不要,我这里引入的唯一目的就是在App.vue下加载全局通用css
import App from './App.vue'
// ajax设置跨域
$.ajaxSettings.crossDomain = true;
/* eslint-disable no-new */
// 安装VueRouter插件
Vue.use(VueRouter);
// 创建一个router的实例对象
var router = new VueRouter({
  routes
});
// 移动端修复点透和延时
FastClick.attach(document.body);
// 创建一个Vue实例对象
var vm = new Vue({
  el: "#app",   // 挂载元素,这里挂载到index.html中id为app的元素上
  router,   // 引入路由,等于router: router
  template: '<App/>',   // 注意:这里如果使用了template,那么<App></App>将会替换你整个id为app的元素,除非里面含有slot标签,你想用render也是一个道理
  components: { // 组件引入,App对应的就是<App></App>
    App
  }
});
  1. 接下来看看路由配置routes.js
// 引入路由对应的组件
import preLoader from './assets/view/preLoader.vue';
import all from './assets/view/all.vue';
import topic from './assets/view/topic.vue';
import about from './assets/view/about.vue';
export default [
  {
    path: "/",  // 路由路径,当页面路径为.../#/的时候使用组件preLoader去渲染页面
    name: "home",   // 可以理解为key
    component: preLoader    // 组件
  },
  {
    path: "/all",
    name: "all",
    component: all
  },
  {
    path: "/topic/:id",
    name: "topic",
    component: topic
  },
  {
    path: "/about",
    name: "about",
    component: about
  }
];
  1. 如何实现页面跳转,我们来看看根页面的组件preLoader.vue
// template里面就是等待渲染html结构
<template>
  <div id="pageLoader" v-show="show"><img src="~assets/images/preLoad.gif" alt=""></div>
</template>
<script>
    // 不习惯export写法可以用 module.exports = {}
    export default {
      name: 'preLoader',
      data (){
        return {
          show: true
        }
      },
      mounted: function () {
        // 往this.$router里push一个name为all的对象,意味着路由将跳转到/#/ + all => /#/all,这里还有其他的模式,不论
        setTimeout (() => {
          this.$router.push({
            name: 'all'
          });
          this.show = false;
        },1314);
      }
    }
</script>
// 这里如果要使用scss的话必须加上lang="scss",为了让ide不报错,给style加上rel跟type如下就可以了
// 注意:vue-cli默认最后是编译处理css的,这里想使用scss必须安装style-loader和vue-style-loader,不需要改webpack配置
<style rel="stylesheet/scss" type="text/css"  lang="scss">
  .main-wrap { position: relative; max-width: 750px; min-width: 320px; margin: 0 auto; width: 100%; min-height: 100%;}
  #pageLoader { position: fixed; left: 50%; top: 50%; transform: translate(-50%,-50%) translateZ(0); width: 160px; height: 20px;}
</style>
  1. 页面跳转之后进入到主题的列表页all.vue,template跟style没什么好说的,正常写吧,来看看script部分
// 引入组件
import $ from 'webpack-zepto';
import backTop from '../components/backtop.vue';
export default {
  name: "all",
  components: {
    backTop,
  },
  filters: {
    getFreeTime: function (time) {    // 这里是自定义过滤器,由于vue2.0干掉了默认提供的filters,所以要使用的话需要自行编写
      // Code
      return time
    }
  },
  data (){
    return {
      SCROLL_LOCK: false, // 滚动锁,在ajax触发之后完成之前保证不会再次发出请求
      menuShow: false,    // menu菜单控制器
      topics: [], // 数据
      params: {   // 请求参数
        page: 1,
        limit: 20,
        tab: 'all',
        mdrender: true
      }
    }
  },
  mounted: function () {  // 当结构挂载之后
    // 这里就是我们要做的事情了...
    // 先判断是否存在tab
    if (this.$route.query && this.$route.query.tab) {
      this.params.tab = this.$route.query.tab; // 当前所在类别
      $(".menu-list a").eq(this.getTitle(this.$route.query.tab).idx).addClass("active").siblings("a").removeClass("active");
    } else {
      $(".menu-list a").eq(0).addClass("active").siblings("a").removeClass("active");
    }
    // 请求数据,这里提为一个methods了,因为存在多次调用
    this.getTopics();
    $("body").removeClass("os-mode");
    // 滚动
    var self = this;
    $(window).on("scroll", function () {
      self.getScrollData()
    })
  },
  methods: {
    getTitle: function (val) {
      // 获取动态的标题
      return obj;
    },
    getTopics: function (type) {  // 请求数据
      var self = this;
      $.get("https://cnodejs.org/api/v1/topics", self.params, function (res) {
        if (res && res.data) {  // 如果查到数据
          self.SCROLL_LOCK = true;
          self.topics = res.data;
        }
      })
    },
    // 滚动加载数据
    getScrollData() {
      if (this.SCROLL_LOCK) {
        var totalheight = $(window).height() + $(window).scrollTop();
        if ($(document).height() <= totalheight + 200) {
          this.SCROLL_LOCK = false;
          this.params.limit += 20;
          this.getTopics();
        }
      }
    },
    // 显示sidebar
    openMenu: function () {
      this.menuShow = true;
      $("body").addClass("os-mode");
    },
    // 收起sidebar
    closeMenu: function () {
      this.menuShow = false;
      $("body").removeClass("os-mode");
    },
  },
  computed: { // 计算属性,适用于频繁变动
    title: function () {
      return this.getTitle(this.params.tab).title;
    },
    getOpenClass: function () {
      if (this.menuShow) {
        return "open";
      } else {
        return "";
      }
    }
  },
  watch: {    // 监听器
    $route: function (to, from) {     // 这里是核心,监听$route(路由的配置对象),一旦这玩意变了,就肯定有情况
      // 如果是当前页面切换分类的情况
      if (to.query && to.query.tab) {
        this.params.tab = to.query.tab;
        $(".menu-list a").eq(this.getTitle(to.query.tab).idx).addClass("active").siblings("a").removeClass("active");
      } else {
        $(".menu-list a").eq(0).addClass("active").siblings("a").removeClass("active");
      }
      this.getTopics();
      // 隐藏导航栏
      this.menuShow = false;
      $("body").removeClass("os-mode");
    }
  }
}

** 一个完整的组件就大概是上述几个步骤了,按照源码安装编写试试吧 **

缺点与小尾巴

缺点

  • 其实里面组件分的并不细,比如header、sidebar都可以单独抽出来作为组件存在,可以减少代码量
  • 还可以拓展评论,登录,用户信息等一些列CNode API提供的功能页面
  • 如果没有脚手架,直接上手开发复杂度不低
  • 这里还没有引入vuex做状态管理,如果涉及到非父子组件通信建议使用

写在结尾的话

CNode项目还是比较建议作为一个vue&vue-router的入门级练手项目,因为里面实际上涉及到了单页路由的切换管理,同时也实现了日常开发的一些基本功能:包括tab页签切换,ajax请求数据,能够满足日常项目的基本要求。

特别简单的页面不建议使用,正常html+css+js的效率远高于使用vue.

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

Nodejs通过base64实现复制文件

首发于微信公众号《前端成长记》,写于 2016.07.13

背景

在服务器环境上,基本没有可视化界面,都是通过命令行操作,想写一个可大量自动复制文件的脚本。

实现过程

将文件转为Buffer

var fs = require('fs');
function encode (filePath){
    // read binary data
    var bitmap = fs.readFileSync(filePath);
    // convert binary data to base64 encoded string
    return new Buffer(bitmap).toString('base64');
}

返回的是一个base64格式的Buffer,便于后续生成文件

Buffer解码成文件生成到指定路径

var fs = require('fs');
function decode (filePath, saveFilePath) {
    var base64str = this.encode(filePath);
    // create buffer object from base64 encoded string, it is important to tell the constructor that the string is base64 encoded
    var bitmap = new Buffer(base64str, 'base64');
    // write buffer to file
    fs.writeFileSync(saveFilePath, bitmap);
    console.log('******** File created from base64 encoded string ********');
}

这里的操作都是同步的,完全可以执行完后检测新文件是否存在

jm-copy-base64

根据上述的原理,自己编写一个node环境的npm包,专门用于自动复制文件。

Github地址及使用文档:jm-copy-base64

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

实现未知高度父容器及其左右排列子容器的高度自适应

首发于微信公众号《前端成长记》,写于 2015.04.10

需求

需要实现两个子容器中一个依据另外一个的高度而自动等高,同时父容器也是同样自动等高,宽度可以自适应或者按需分配。

<div class="container">
  <div class="content-1">未知高度和宽度的内容</div>
  <div class="content-2">未知高度和宽度内容</div>
</div>

方案

目前我学习到的两种解决办法一种是利用flexbox,另外一种是利用table和table-cell,下文均有解释。

利用Flexbox

Flexbox通常能让我们更好的操作他的子元素布局,例如:

  • 如果元素容器没有足够的空间,我们无需计算每个元素的宽度,就可以设置他们在同一行
  • 可以快速让他们布局在一列
  • 可以方便让他们对齐容器的左、右、中间等
  • 无需修改结构就可以改变他们的显示顺序
  • 如果元素容器设置百分比和视窗大小改变,不用提心未指定元素的确切宽度而破坏布局,因为容器中的每个子元素都可以自动分配容器的宽度或高度的比例

听起来相当有用,不是吗?接下来让我们更详细的探索它。

目前支持的浏览器

Opera Mobile12.1+、Opera12.5+、Firefox18+(partial)和chrome。Chrome需要添加浏览器前缀“-webkit-”,Opera支持标准语法,不用添加任何前缀。Firefox有部分支持,也需要添加前缀“-moz-”,同时需要设置一个标志(到firefox浏览器地址栏中输入:about:config,搜索“flexbox”,找到之后双击“layout.css.flexbox.enabled”,设置他的“value”值为“true”)。注意,其他浏览器除了opera自2009年支持flexbox以来,都使用旧的语法规则,真的不应该使用这些过时的语法。

具体支持可查看 caniuse

flexbox的术语

  • 伸缩容器: 一个设有“display:flex”或“display:inline-flex”的元素
  • 伸缩项目: 伸缩容器的子元素
  • 主轴、主轴方向: 用户代理沿着一个伸缩容器的主轴配置伸缩项目,主轴是主轴方向的延伸。
  • 主轴起点、主轴终点: 伸缩项目的配置从容器的主轴起点边开始,往主轴终点边结束。
  • 主轴长度、主轴长度属性: 伸缩项目的在主轴方向的宽度或高度就是项目的主轴长度,伸缩项目的主轴长度属性是width或height属性,由哪一个对着主轴方向决定。
  • 侧轴、侧轴方向: 与主轴垂直的轴称作侧轴,是侧轴方向的延伸。
  • 侧轴起点、侧轴终点: 填满项目的伸缩行的配置从容器的侧轴起点边开始,往侧轴终点边结束。
  • 侧轴长度、侧轴长度属性: 伸缩项目的在侧轴方向的宽度或高度就是项目的侧轴长度,伸缩项目的侧轴长度属性是「width」或「height」属性,由哪一个对着侧轴方向决定。

关于更多flexbox的信息参考这篇:Flexbox——快速布局神器

就我们这个问题的话,只需要向下面这样设置样式即可:

.container { display: flex}
.content-1, .content-2 { flex: 1; width: 0} // 不需要自适应可以一个写宽度,另外一个写flex,或者flex按比例分配

使用 table和table-cell

从前在页面布局的时候,table被大量的使用,其中一个好处便是元素可以轻松的定位,不会出现什么窜行的问题。你要是用div的话,一会inline一会float很是蛮烦。怎么样才能在使用div的时候也能享受的table定位的好处呢?以开头的问题为例:

.container{ width: 90%; margin: 50px auto; border: #333333 solid 1px; padding: 10px; display: table;}
.content-1{ height: 50px; background: #f30; width: 35%; display: table-cell;}
.content-2{ height: 50px; background: #03f; display: table-cell; }

将父容器的display指定为table,这样浏览器便会把parent当作一个table对待,然后向table中添加元素,元素具有的效果就会和直接使用td标签一样。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

【包教包会】Chrome拓展开发实践

首发于微信公众号《前端成长记》,写于 2019.10.18

导读

有句老话说的好,好记性不如烂笔头。人生中,总有那么些东西你愿去执笔写下。

本文旨在把整个开发的过程和遇到的问题及解决方案记录下来,希望能够给你带来些许帮助。

安装和源码

安装和源码

背景

《干货!从0开始,0成本搭建个人动态博客》 中,已经完成了动态博客的搭建。接下来,将围绕该博客,开发对应的 Chrome拓展,方便使用。

上手开发

本文不需要前期准备,直接跟我做就好了

功能拆分

这里主要分为几个大的功能点:

  • 内容菜单导航,方便快速进入到博客的指定菜单页
  • 地址栏搜索,根据内容可直接在地址栏出现匹配结果的文章
  • 新文章推送,如果有文章更新则自动推送

Ⅰ.必要知识介绍

Chrome 拓展插件 实际上是由 HTML/CSS/JS/图片 等资源组成的一个 .crx 的拓展包,解压出来即可得到真正内容。

Chrome 拓展插件 对项目结构没有要求,只需要在开发根目录下有一个 mainfest.json 即可。

进入 Chrome 拓展程序 页面,打开 开发者模式 开始我们的开发之路。

Ⅱ.基础配置开发

首先,新建一个 src 目录作为插件的文件目录,然后新建一个 mainfest.json 文件,文件内容如下:

// mainfest.json
{
  // 插件名称
  "name": "McChen",
  // 插件版本号
  "version": "0.0.1",
  // 插件描述
  "description": "Chrome Extension for McChen.",
 // 插件主页
  "homepage_url": "https://chenjiahao.xyz",
  // 版本必须指定为2
  "manifest_version": 2
}

然后打开 Chrome 拓展程序页面,点击 加载已解压的拓展程序 按钮,选择上面新建的 src 文件,将会看到如下两处变化:

code-img1

code-img2

你会发现你的拓展插件已经添加到右上角了,点击右键时出现的第一行为 name ,点击跳转链接为 homepage_url

接下来我们为我们的拓展插件添加图标,在 src 中新建一个名为 icon.png 的图标,然后修改 mainfest.json 文件:

// mainfest.json
{
...
   "icons": {
    "16": "icon.png",
    "32": "icon.png",
    "48": "icon.png",
    "128": "icon.png"
  }
...
}

点击插件开发的更新图标,我们可以看到图标已经加上了:

code-img3

code-img4

这里会发现,右上角的图标为什么是置灰的呢?这里就需要聊到 browser_actionpage_action[参考文档]

  • browser_action :如果你想让图标一直可见,那么配置该项
  • page_action :如果你不想让图标一直可见,那么配置该项

为了让图标一直可见,我们来修改下 mainfest.json

{
...
  "browser_action": {
    "default_icon": "icon.png",
    "default_title": "McChen"
  },
...
}

此时再次更新查看效果:

code-img5

到这里,基础的配置开发已经完成了,接下来就是功能部分。

Ⅲ.内容菜单导航开发

[参考文档]

内容导航菜单我用在两个地方:鼠标点击右上角图标的 Popup 和网页中按鼠标右键出现的菜单。

先看看鼠标点击右上角图标 Popup 的,给 mainfest.json 增加 default_popup 就是 popup 展示的页面内容了。

{
...
  "browser_action": {
    "default_icon": "icon.png",
    "default_title": "McChen",
    "default_popup": "popup.html"
  },
...
}

新建一个 popup.html 文件,内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <title>McChen</title>
  <meta charset="utf-8"/>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <style type="text/css">
    #McChen-container { padding: 4px 0; margin: 0; width: 80px; user-select: none; overflow: hidden; text-align: center; background-color: #f6f8fc;}
    .McChen-item_a { position: relative; display: block; font-size: 14px; color: #283039; transition: all 0.2s; line-height: 28px; text-decoration: none; white-space: nowrap; text-indent: 16px;}
    .McChen-item_a:before { position: absolute; top: 50%; margin-top: -14px; font-size: 16px; line-height: 28px;}
    .McChen-item_a:after { position: absolute; top: 50%; margin-top: -14px; font-size: 16px; line-height: 28px;}
    .McChen-item_a + .McChen-item_a { border-top: 1px solid #f0f2f5;}
    .McChen-item_a:hover { color: #0074ff;}
    .McChen-item_a:nth-child(1):before { content: '·'; left: 4px;}
    .McChen-item_a:nth-child(2):before { content: '··'; left: 2px;}
    .McChen-item_a:nth-child(3):before { content: '···'; left: 0;}
    .McChen-item_a:nth-child(4):before { content: '····'; left: -2px;}
    .McChen-item_a:nth-child(5):before { content: '····'; margin-top: -16px; left: -2px;}
    .McChen-item_a:nth-child(5):after { content: '·'; margin-top: -12px; left: -2px;}
    .McChen-item_a:nth-child(6):before { content: '····'; margin-top: -16px; left: -2px;}
    .McChen-item_a:nth-child(6):after { content: '··'; margin-top: -12px; left: -2px;}
    .McChen-item_a:nth-child(7):before { content: '····'; margin-top: -16px; left: -2px;}
    .McChen-item_a:nth-child(7):after { content: '···'; margin-top: -12px; left: -2px;}
  </style>
</head>
<body id="McChen-container">
  <a class="McChen-item_a" href="https://chenjiahao.xyz" target="_blank">主页</a>
  <a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/archives" target="_blank">博客</a>
  <a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/labels" target="_blank">标签</a>
  <a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/links" target="_blank">友链</a>
  <a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/about" target="_blank">关于</a>
  <a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/board" target="_blank">留言</a>
  <a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/search" target="_blank">搜索</a>
</body>
</html>

我们更新后来看看效果,点击右上角图标将会看到如下的内容弹窗:

code-img6

下一步,我们来实现在网页中按鼠标右键出现的菜单。

首先,你必须要配置对应的权限才能使用这个 API ,还需要配置修改 mainfest.json 内容:

[权限参考文档]

...
  "permissions": [
    "contextMenus"
  ]
...

接下来,需要通过 API 调用去创建对应的菜单,这里需要用到常驻在后台运行的 js 才行,所以还需要修改 mainfest.json 文件:

...
  "background": {
    "scripts": [
      "background.js"
    ]
  },
...

然后我们新建一个 backgroud.js 文件,文件内容如下:

[参考文档]

chrome.contextMenus.create({
	id: 'McChen',
	title: 'McChen',
	contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
});
chrome.contextMenus.create({
	id: 'home',
	title: '主页',
	parentId: 'McChen', // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单
	contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
});
chrome.contextMenus.create({
	id: 'archives',
	title: '博客',
	parentId: 'McChen', // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单
	contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
});
chrome.contextMenus.create({
	id: 'labels',
	title: '标签',
	parentId: 'McChen', // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单
	contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
});
chrome.contextMenus.create({
	id: 'links',
	title: '友链',
	parentId: 'McChen', // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单
	contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
});
chrome.contextMenus.create({
	id: 'about',
	title: '关于',
	parentId: 'McChen', // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单
	contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
});
chrome.contextMenus.create({
	id: 'board',
	title: '留言',
	parentId: 'McChen', // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单
	contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
});
chrome.contextMenus.create({
	id: 'search',
	title: '搜索',
	parentId: 'McChen', // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单
	contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
});
// 监听菜单点击事件
chrome.contextMenus.onClicked.addListener(function (info, tab) {
	if (info.menuItemId === 'home') {
		chrome.tabs.create({url: 'https://chenjiahao.xyz'});
	} else {
		chrome.tabs.create({url: 'https://chenjiahao.xyz/blog/#/' + info.menuItemId});
	}
})

更新后,点击鼠标右键将查看到如下内容:

code-img7

至此,内容菜单导航功能已全部完成。

Ⅳ.地址栏搜索开发

[参考文档]

地址栏搜索主要是通过 Omnibox 来实现的,我们首先需要设置关键字,在这里我设置成 'mc' ,修改 mainfest.json 文件:

...
{
  "omnibox": { "keyword" : "mc" }
}
...

更新后,我们在地址栏输入 mcTab 或者 Space 键可看到如下内容:

code-img8

接下来我们进行接口开发,由于需要进行接口调用,所以需要配置允许请求的地址,修改 mainfest.json 文件:

...
{
  "permissions": [
    "contextMenus",
    // 允许请求全部https
    "https://*/"
  ],
}
...

然后修改 background.js 文件内容:

...
let timer = '';
chrome.omnibox.onInputChanged.addListener((text, suggest) => {
	if (timer) {
		clearTimeout(timer)
		timer = ''
	} else {
		timer = setTimeout(() => {
			if (text.length > 1) {
				const xhr = new XMLHttpRequest();
				xhr.open("POST", "https://api.artfe.club/transfer/github", true);
				xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
				xhr.onreadystatechange = function () {
					if (xhr.readyState === 4) {
						const list = JSON.parse(xhr.responseText).data.search.nodes;
						if (list.length) {
							suggest(list.map(_ => ({content: 'ISSUE_NUMBER:' + _.number, description: '文章 - ' + _.title})))
						} else {
							suggest([
								{content: 'none', description: '无相关结果'}
							])
						}
					}
				};
				xhr.send('query=' + query);
			} else {
				suggest([
					{content: 'none', description: '查询中,请稍后...'}
				])
			}
		}, 300)
	}
});

// 当选中建议内容时触发
chrome.omnibox.onInputEntered.addListener((text) => {
	if (text.startsWith('ISSUE_NUMBER:')) {
		const number = text.substr(13)
		chrome.tabs.query({active: true, currentWindow: true}, function (tabs) {
			if (tabs.length) {
				const tabId = tabs[0].id;
				const url = 'https://chenjiahao.xyz/blog/#/archives/' + number;
				chrome.tabs.update(tabId, {url: url});
			}
		});
	}
});
...

这里有几个地方需要注意一下:

  1. onInputChanged 这方法触发频率高,和正常开发一样,需要做一次函数防抖,要不然请求频率会特别高。
  2. 这里面不允许写 Promise ,所以我使用的 XMLHttpRequest
  3. suggestcontentdescription 字段都不允许为空,但是在事件回调里需要识别,所以我这里特意增加了一个前缀 ISSUE_NUMBER:

更新后,在地址栏输入 mcTab 后,输入 干货 ,将会看到如下内容:

code-img9

至此,地址栏搜索功能已全部完成。

Ⅴ.新文章推送开发

[存储参考文档]

[推送参考文档]

新文章推送功能,首先我们需要知道之前的最新文章是哪篇,才能做到精准推送,所以这里需要用到 Storage ,也就是存储功能。存下最新文章的 ID ,轮询最新文章,如果有更新,则存下最新文章的 ID 并且调用推送的 API 。所以,我们需要先增加权限配置,修改 mainfest.json 文件:

...
  "permissions": [
    "storage",
    "contextMenus",
    "notifications",
    "https://*/"
  ],
...

然后修改 'background.js' 文件内容:

...
getLatestNumber();
chrome.storage.sync.get({LATEST_TIMER: 0}, function (items) {
	if (items.LATEST_TIMER) {
		clearInterval(items.LATEST_TIMER)
	}
	const LATEST_TIMER = setInterval(() => {
		getLatestNumber()
	}, 1000 * 60 * 60 *24)
	chrome.storage.sync.set({LATEST_TIMER: LATEST_TIMER})
});
function getLatestNumber () {
	const query = `query {
		repository(owner: "ChenJiaH", name: "blog") {
			issues(orderBy: {field: CREATED_AT, direction: DESC}, labels: null, first: 1, after: null) {
				nodes {
					title
					number
				}
			}
		}
	}`;
	const xhr = new XMLHttpRequest();
	xhr.open("POST", "https://api.artfe.club/transfer/github", true);
	xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
	xhr.onreadystatechange = function () {
		if (xhr.readyState === 4) {
			const list = JSON.parse(xhr.responseText).data.repository.issues.nodes;
			if (list.length) {
				const title = list[0].title;
				const ISSUE_NUMBER = list[0].number;
				chrome.storage.sync.get({ISSUE_NUMBER: 0}, function(items) {
					if (items.ISSUE_NUMBER !== ISSUE_NUMBER) {
						chrome.storage.sync.set({ISSUE_NUMBER: ISSUE_NUMBER}, function() {
							chrome.notifications.create('McChen', {
								type: 'basic',
								iconUrl: 'icon.png',
								title: '新文章发布通知',
								message: title
							});
							chrome.notifications.onClicked.addListener(function (notificationId) {
								if (notificationId === 'McChen') {
									chrome.tabs.create({url: 'https://chenjiahao.xyz/blog/#/archives/' + ISSUE_NUMBER});
								}
							})
						});
					}
				});
			}
		}
	};
	xhr.send('query=' + query);
}
...

注意:由于是后台常驻,所以需要增加轮询来判断是否有更新,我这里设置的是一天一次

更新后,第一次我们会看到浏览器右下角会有推送消息如下:

code-img10

至此,新文章推送功能也已经开发完成了。

打包发布

在拓展程序页面点击打包扩展程序,选择 src 作为根目录打包即可。

将会生成 src.crxsrc.pem 两个文件, .crx 文件就是你提交到拓展商店的资源, .pem 文件是私钥,下次进行打包更新时需要使用。

由于打包需要 5$ ,所以我这里就不做演示了,需要的可以自行尝试,[发布地址]

结尾

一个基于动态博客的 Chrome 拓展插件 就开发完了,欢迎下载使用。

如有疑问或不对之处,欢迎留言。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

Nodejs+Express+Mongodb搭建简单的应用

首发于微信公众号《前端成长记》,写于 2016.04.27

Nodejs 介绍

Node.js应用场景

高并发、高性能服务器

Node.js最重要特性

通过单线程实现异步处理环境

实现高性能的两种机制

一. 非阻塞型I/O

在执行访问数据库的代码之后可以立即转而执行后面的代码,把数据库返回结果的处理代码放在回调函数中执行。(传统阻塞型I/O表现为在执行访问数据库的代码后,线程将暂停下来,等待数据库返回结果再继续执行)

二. 事件环

在一个时刻只能执行一个事件回调函数,但是在执行一个事件回调的中途可以转而处理其他事件(包括触发新的事件、声明该事件的回调函数等),然后返回继续执行原事件回调函数

Node.js v0.10中的核心模块

模块名 模块功能 stability(稳定度)
assert 为应用程序的单元测试添加断言处理 5-Locked(今后不会被修改)
buffer 用于实现二进制数据的存储与转换 3-stable(稳定)
child_process 用于实现子进程的创建与管理 3-stable(稳定)
cluster 用于实现多进程 1-Experimental(实验性阶段)
console 用于为控制台输出信息 4-API Frozen(今后API部分不会被修改)
crypto 用于实现数据的加密解密处理 2-Unstable(不稳定)
debugger 用于实现一个内置调试器来帮助开发者调试应用程序 3-stable(稳定)
dns 用于实现与DNS相关的处理 3-stable(稳定)
domain 用于实现多个I/O之间的协作处理 2-Unstable(不稳定)
events 用于为事件处理提供一个基础类 4-API Frozen(今后API部分不会被修改)
fs 用于操作文件及文件系统 3-stable(稳定)
http 用于实现HTTP服务器端及客户端 3-stable(稳定)
https 用于实现HTTPS服务器端及客户端 3-stable(稳定)
net 用于创建TCP服务器与客户端 3-stable(稳定)
os 用于获取操作系统信息 4-API Frozen(今后API部分不会被修改)
path 用于处理文件路径 3-stable(稳定)
punycode 用于实现Punycode字符串的编码及解码 2-Unstable(不稳定)
querystring 用于处理HTTP请求中使用的查询字符串 3-stable(稳定)
readline 用于读取一行标准输入流 2-Unstable(不稳定)
repl 用于实现REPL(Read-Eval-Print-Loop)交互式运行环境 (无)
stream 用于为流的输入/输出处理提供一个基础类 2-Unstable(不稳定)
string_decoder 用于实现从二进制数据到字符串数据之间的转换 3-stable(稳定)
tls 用于使用OpenSSL来实现TLS/SSL通信处理 3-stable(稳定)
tty 用于获取来自于TTY终端的信息 2-Unstable(不稳定)
url 用于实现URL字符串的解析与格式化 3-stable(稳定)
util 用于实现各种实用函数 5-Locked(今后不会被修改)
vm 用于为Javascript脚本代码提供一个独立的运行环境 2-Unstable(不稳定)
zlib 内部使用zlib类库来实现数据的压缩及解压处理 3-stable(稳定)

Node.js v0.10中追加的类、函数与对象

类、函数及对象名 描述
Buffer类 用于为二进制数据的存储提供一个缓存区
setTimeout函数 用于在制定时间到达时执行一个指定函数,指定方法为从当前时刻之后多少毫秒
clearTimeout函数 用于取消在setTimeout函数内指定的函数的执行
setInterval函数 用于指定每隔多少时间执行一个指定函数
clearInterval函数 用于取消在setInterval函数内指定的函数的执行
require函数 用于加载模块
module对象 用于访问信息模块
process对象 用于访问进程信息

一个简单的栗子(demo)

先在node项目中建立一个demo.js,代码如下:

var http = require('http');
    http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.write('<head><meta charset="utf-8" /></head>');
    res.end("<div style='font-size: 50px;'>I'm McChen</div>");
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

然后在命令行中执行node demo.js

node .\demo.js
//Tips: 命令行中将显示 Server running at http://127.0.0.1:1337/

在任意浏览器打开 http://127.0.0.1:1337/ ,将发现页面中会显示 I'm McChen

开发前准备

  • 安装 Nodejs
  • 安装 Express (用 Webstorm 的小伙伴们可省略此步,直接创建 Nodejs 项目即可)
  • 安装 Mongodb
  • 安装 Mongovue (中文数据库可视化工具,习惯用命令行的可不需要)

非 Webstorm 开发

  1. 创建一个 express 项目
express --sessions newProject
  1. package.json 进行编辑,主要是依赖 dependencies
{
    "name": "newProject",
    "version": "0.0.1",
    "private": true,
    "scripts": {
        "start": "node app.js"
    },
    `"dependencies": {
        "express": "3.4.8",
        "jade": "*"
    }
}
  1. 安装依赖,完成之后执行 node app.js
npm install // or cnpm install

浏览器打开localhost:3000即可发现一个简单express页面建立完成了

  1. 配置入口文件及express参数

在app.js最后面加上下面代码,实例化Express并赋值给app变量

var app = express()

接着对express的参数进行配置,这里设置了端口,寻找views的目录,用什么模板引擎来处理这些views,和一些其它的东西。还要注意最后一行,它告诉Express把public/目录下的静态文件作为顶层目录的文件来托管。

// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));

继续增加插件检查和过期提醒

// 替换前
app.use(express.bodyParser());
// 替换后
app.use(express.urlencoded());

创建一个http server并且启动它

app.get('/', routes.index);
app.get('/users', user.list);
http.createServer(app).listen(app.get('port'), function(){
    console.log('Express server listening on port ' + app.get('port'));
});

webstorm 开发

直接new一个新Node.js Express App 项目,直接搞定上面全部流程

Mongodb安装连接

安装 Mongodb 后在 bin 文件目录下执行:

mongod --dbpath D:\database

表示把数据保存在D盘database文件夹下。然后新开一个cmd进入bin文件夹后,执行:

mongo

这时候表示已经连接上了数据库,接下来就可以进行操作了。
首先新建一个属于自己的数据库,并写入数据:

use myDataBase
db.myCollection.insert({ “username” : “XXX″, “email” : “XXX” })

db表示刚才创建的myDataBase的数据库,myCollection表示在里面创建了一张表,后面是插入的字段

把mongo连接到node

在app.js中插入以下代码

app.get('/myDataBase', routes.myDataBase(db));  // 当用户访问myDataBase时,把db传递到路由
router.myDataBase= function(db) {   // 创建对应的myDataBase路由
   return function(req, res) {
      var collection = db.get('myCollection');
       collection.find({},{},function(e,docs){
           res.render('myPage', {
                "data" : docs
           });
       });
   };
};

创建对应的模板,我这里采用的是jade,即myPage.jade(由于不习惯,后期我切换为artTemplate了)

doctype html
html
  head
    title= "McChen"
    link(rel='icon', href='http://www.jd.com/favicon.ico')
    link(rel='stylesheet', href='http://static.360buyimg.com/finance/mobile/base/cm/1.0.0/reset.css')
    link(rel='stylesheet', href='/stylesheets/style.css')
    script(src="/javascripts/test.js")
  body
	h1 McChen
	p.name.title Learning Node
    p.name: #{data[0].username}

保存文件,重启node服务器,你会发现data[0]=XXX,已经从数据库中成功取出来了。往数据库中写入数据,可以通过ajax,然后render数据库的数据。

接下来让我们做一个简单的登录页吧。

搭建简单的登录页

最终效果

image

项目文件结构

先把文件结构贴出来利于后续流程说明

image

步骤

  1. 首先新建一个login.jade文件,代码如下(注意缩进,间隔为2字间距,md不缩进-_-!)
extends layout
block link
    link(rel='stylesheet', href='/stylesheets/1.0.0/login.css')
block content
    canvas#jm-funnyBackground
    div.loginBox.abs-mt
        div.title
           h3
        div.u-name
            h4 用户名:
            input.username#Name(tabindex='1', placeholder='请输入用户名',name='Name', type='text')
        div.u-pwd
            h4 密码:
            input.password#Password(tabindex='2', placeholder='请输入密码',name='Password', type='password')
            a.logon#Logon(href='javascript:;') 登录
block script
    script(src='/javascripts/modules/funnyBackground/funnyBackground.js')
    script(src='/javascripts/modules/login/login.js')

extend是jade模板的一大特色,继承,另外一大特色是mixin,以后再叙(实际项目我已经更换为artTemplate了)。

  1. 编写对应scss和js并引入

  2. 根据上一篇文章,开两个cmd,一个mongod –dbpath D:\database,另外一个mongo。

  • use database,建立database数据库
  • 接下来为数据库里添加用户表,存放登录用户名和密码,db.usercollection.insert({“username”:”admin”, “password”:”123456”})
  • 这时候可以通过mongoVUE或者cmd find 可以看到在database这数据库中已经增加了一张usercollection的表,里面有三个字段,_id,username, password,如下图:

image

  1. 交互

login.js 截取部分交互代码

self.getAjaxData("/login",{username: username,password:password},"post","json",self.loginCallback)  // 发ajax请求到localhost:3000/login
   loginCallback: function (data) {  // ajax成功回调
       if(data.code == 200){
           window.location = '/index';
       } else if(data.code == 201) {
           alert("您输入的用户名或密码不正确");
       }
   },
   getAjaxData: function (url, data, type, dataType, callback, failFn) {
       $.ajax({
           url: url,
           data: data,
           type: type,
           dataType: dataType,
           success: function (data) {
               callback && callback(data);
           },
           error: function () {
               failFn && failFn();
           }
       });
   }
  • 首先通过ajax给localhost:3000/login发出请求
  • 然后在node中处理请求,req.body.*就是ajax传入的data
router.post('/login', function (req, res, next) {
    // 请求带过来的data在req.body里
    var username = req.body.username;
    var password = req.body.password;
    var collection = db.get('usercollection'); //获得数据库中的集合(类似关系数据库中的表)
    collection.find({"username": username, "password": password}, {}, function (e, docs) { //取得所有的集合数据, 渲染到页面上,关键字是data,第一个{}表示where,第二个{}表示显示隐藏,第三个为回调函数
        if (docs != "") {
            req.session.username = docs[0].username;
            req.session.password = docs[0].password;
            res.send({"code": 200});    // 200 表示登录成功
        } else {
            res.send({"code": 201});    // 201 表示登录失败
        }
    });
});
  • node通过res.send给ajax返回了一个json,里面只有一个状态参数code
  • login.js去判断,如果登录成功,则进行页面跳转,window.location = “/index”
  • node拦截localhost:3000/index,并进行数据渲染,这里利用了session进行参数存储,判断登录状态亦可使用
*GET index page */
router.get('/index', function (req, res, next) {
    var docs = res.session;
    res.render('index', {
        "data": docs
    });
});

因为express4.X以上已经把session模块分离出来了,所以必须要多引入依赖模块express-session

在app.js中增加

var session = require('express-session');
app.use(session({
    secret: "McChen",  // 必填参数,可随意取名
    name: "loginMes", // 存入cookies的参数名
    cookie: {maxAge: 60000 * 60 * 24 * 15},  // cookie的生命周期
    resave: false,  // 必须,否则session会报错
    saveUninitialized: true  必须,否则session会报错
}));

之后便可以愉快的利用res.session进行存储了

结束

至此,一个与数据库交互的登录页面已经基本完成

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

基于Vue实现标题/背景等任意内容的动态切换

首发于微信公众号《前端成长记》,写于 2018.01.26

背景

目前项目中,使用 Vue 开发的一般都是SPA项目,而非多页面项目,所以页面标题就需要自己根据每个不同的路由进行对应的设置了。

阅读此文前,您需要对以下内容有所了解:

解决方案

  • 每个页面都手动设置 document.title = XXX
  • 采用 Vue 指令 的方式实现

这里推荐使用 Vue 指令 的方式。

核心代码

指令一般有以下几个钩子函数

export default {
    // 绑定钩子函数
    bind (el, binding, vnode) {},
    // 插入时
    inserted (el, binding, vnode) {
        // el 是绑定的DOM对象,可以选择自己设置dataset,也可以选择直接绑定在binding
        // 通过取dataset
        document.title = el.dataset.title;
        // 这里也可以同时进行其他操作,比如换背景,换其他任意你想更换的东西
        el.dataset.bg && (document.body.className = el.dataset.bg);
    },
    // 组件VNode更新时
    update (el, binding, vnode, oldVnode) {
        // el 是绑定的DOM对象,可以选择自己设置dataset,也可以选择直接绑定在binding
        // 通过绑定在binding
        document.title = binding.expression;  // 字符串形式用 expression;变量形式用 value
        // 这里也可以同时进行其他操作,比如换背景,换其他任意你想更换的东西
        el.dataset.bg && (document.body.className = el.dataset.bg);
    },
    // 组件更新时
    componentUpdated (el, binding, vnode, oldVnode) {},
    // 指定与元素解绑时
    unbind (el, binding, vnode) {}
}
<template>
    // 通过dataset设置
    <div v-title data-title="我是标题"></div>
    // 通过binding设置
    <div v-title="我是标题"></div>
</template>

结尾

Vue 指令这种形式,希望不要仅用于此,因为能够拓展延伸到什么程度,完全取决于您的想象力。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

Nodejs实现发送邮件

首发于微信公众号《前端成长记》,写于 2017.01.04

背景

在日常工作中,一些监控系统都支持预警邮件,即当页面的某些指标达到阀值的时候,自动发送邮件给相关负责人。

团队站项目想实现一个定制化邮件发送的功能,在不同的时期给不同的人发送邮件。

核心代码

由于本项目开发环境为Node环境,故去github上找了一个封装好的邮件发送模块 nodemailer

安装

npm install nodemailer

使用

var nodemailer = require('nodemailer');
 
var config = {
    // service: 'jd',
    // pool: true,
    host: 'smtp.jd.com',
    ignoreTLS: true,
    port: 25,
    // secure: true, // use SSL
    // debug: true,
    auth: {
        user: '******',
        pass: '******'
    },
    logger: true
};
 
// create reusable transporter object using the default SMTP transport
var transporter = nodemailer.createTransport(config);
 
// verify connection configuration
transporter.verify(function(error, success) {
    if (error) {
        console.log(error);
    } else {
        console.log('Server is ready to take our messages');
    }
});
// setup e-mail data with unicode symbols
var mailOptions = {
    from: '[email protected]', // sender address
    to: '[email protected], [email protected]', // list of receivers
    subject: '测试邮件标题20170104', // Subject line
     // text: 'Hello world11', // plaintext body
    html: '<b>测试邮件内容20170104</b>' // html body
};
// send mail with defined transport object
transporter.sendMail(mailOptions, function(error, info){
    if(error){
        return console.log(error);
    }
    console.log('Message sent: ' + info.response);
});

常见错误

  1. ECONNECTION
{ [Error: 101057795:error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol:openssl\ssl\s23_clnt.c:794:
] code: 'ECONNECTION', command: 'CONN' }

** 解决方案 ** jd邮箱不需要设置 secure: true

  1. ECONNREFUSED
{ [Error: connect ECONNREFUSED 127.0.0.1:25]
  code: 'ECONNECTION',
  errno: 'ECONNREFUSED',
  syscall: 'connect',
  address: '127.0.0.1',
  port: 25,
  command: 'CONN' }

** 解决方案 ** jd邮箱不在service范围内,需要使用 host+auth+port 配置,而非service

  1. UNABLE_TO_VERIFY_LEAF_SIGNATURE
{ [Error: unable to verify the first certificate] code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' }

** 解决方案 ** jd邮箱配置时需要带上 ignoreTLS:true

  1. EAUTH
{ [Error: Invalid login: 535 5.7.3 Authentication unsuccessful]
  code: 'EAUTH',
  response: '535 5.7.3 Authentication unsuccessful',
  responseCode: 535,
  command: 'AUTH LOGIN' }

** 解决方案 ** jd邮箱配置auth时user不能带@jd.com

其他

如果您的开发过程中遇到不在列表中的坑,可以添加到列表中,感谢!

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

node应用连接数据库服务器实战

首发于微信公众号《前端成长记》,写于 2017.05.12

背景

最近开发一个公司项目,需要支持外网访问,而公司要求:应用服务器跟数据服务器必须分开,所以才有了应用服务器连接数据服务器这么一说。

本文章适合对mongodb有所涉猎者阅读

Let's go

提前申明

  • 本项目使用的是mongodb服务器,为了简化api,使用的是monk进行连接,当然也可以直接使用原生api进行连接,下面都将做介绍
  • 应用服务器已安装node:v6.10.3,mongodb:v2.2.26,monk:v4.0.0
  • 数据服务器已安装mongodb
  • 除开启等必要操作在命令行执行外,全部在node脚本中执行

如何给mongodb设置权限

为了安全起见,我们一般都会给数据库设置不同权限的用户。

  1. 首先,需要在不开启auth条件下启动数据库,进行mongodb/bin目录
./mongo --dbpath database
  1. 连接上之后,我们开始增加一个账户并配置相应权限
var MongoClient = require('mongodb').MongoClient
MongoClient.connect('mongodb://localhost:27017/database', function(err, db){
  // db为当前连接上的数据库
 
  // Add the new user to the database
  db.addUser('username', 'password', function(err, result) {
    if(result == 1) {
        // 添加成功
        var col = db.get("testcollection");
        col.insert({"test":"name"})
        // 这个时候你会发现数据表testcollection中有了一条数据
    }
  });
})
  1. mongodb使用认证模式连接
./mongo --dbpath database --auth
  1. 查询之前添加的数据
var monk = require("monk");
var db = monk('localhost:27017/database');
var col = db.get("testcollection");
col.find({},{},function(err, docs){
    if(err) {
        console.error(err)
    } else {
        console.log(docs)
    }
})

你会发现报错如下:

uncaught exception: error: {     
    "$err" : "unauthorized db:database lock type:-1 client:127.0.0.1",     
    "code" : 10057
}

不要紧张,这意思就是你没有认证数据库admin,连接时加上密码认证即可。

连接带认证的mongodb数据库

  1. mongodb原生方法
var MongoClient = require('mongodb').MongoClient;
MongoClient.connect('mongodb://localhost:27017/database', function(err, db) {
    // Authenticate using the newly added username
    db.authenticate('username', 'password').then(function(result) {
      if(result == 1) {
        console.log("认证成功")
        // TODO:查询检验
        var col = db.collection("testcollection");
        col.find({},function(err, docs){
            console.log(docs)
            // 你会发现此时可以成功获取到数据了        
        })
      }
    });
  });
});
  1. monk方法

这里遇到个问题,正常代码逻辑如下:

var monk = require("monk");
var db = monk("mongodb://localhost:27017/database");
 
db.authenticate('username', 'password');

结果发现报错:

db.authenticate is not a function

查monk源码manager.js发现连接这一段代码如下:

Manager.prototype.open = function (uri, opts, fn) {
  MongoClient.connect(uri, opts, function (err, db) {
    if (err || !db) {
      this._state = STATE.CLOSED
      this.emit('error-opening', err)
    } else {
      this._state = STATE.OPEN
      this._db = db
      this.emit('open', db)
    }
    if (fn) {
      fn(err, this)
    }
  }.bind(this))
}

是的,你没看错,它是直接连接,没有提供 authenticatecreateUser 等其他方法,心里一片绝望,此时,想解决问题有两种方案:

  • 使用mongodb原生语法,代码量大增且修改量巨大
  • 修改monk源码,增加认证等过程

两种方法感觉都不太合适,正值绝望之时,看见下面的代码

MongoClient.connect('mongodb://username:password@localhost:27017/database');

既然原生支持这种连接,那么monk应该也可以,如果可以,那么便解决了上面的问题了,说做就做。

var monk = require("monk");
var db = monk("mongodb://username:password@localhost:27017/database");
var col = db.collection("testcollection");
col.find({},function(err, docs){
    console.log(docs)
    // 你会发现此时可以成功获取到数据了        
})

Perfect!

进入应用服务器连接数据服务器

  1. 通过之前申请到的权限,进入应用服务器的堡垒机,连接进入应用服务器

  2. 在应用服务器上进行数据库的连接测试

var monk = require("monk");
var db = monk("mongodb://username:password@server:27017/database");

只需要把 usernamepasswordserver 填入上面的字段,即可成功连接数据库服务器。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

基于markdown快速生成易维护的Vue项目文档

首发于微信公众号《前端成长记》,写于 2018.02.23

背景

Markdown是一种可以使用普通文本编辑器编写的标记语言,通过简单的标记语法,它可以使普通文本内容具有一定的格式。通常我们都用来写文档。

解决过程

** 最开始的思路是,文档只通过.md进行维护,找到一个md解析器,将md解析成html对应填入到.vue文件中,于是便有了下面的操作: **

  1. 通过npm中找到 marked 这个node包,首先进行安装
npm install marked --save
  1. 在.vue文件中引入md文件并解析成html输出到页面上
var marked = require('marked');
var md = require('./test.md')
export default {
  data () {
    return {
      mdHtml: "",
    }
  },
  mounted () {
    this.mdHtml = marked(md);
  }
}

这时候发现,因为解析过程是在marked中做的,如果md中想使用自定义组件,比如jrv-alert,也会被原封不动的转成。这并不是我们期望的效果,所以,** 这种方式不可行 **。

** 接下来,考虑的就是在编译过程中应当使用vue预发先做前置的解析。所以便有了下面的代码: **

  1. 通过npm中找到 markdown-loader 这个node包,首先进行安装。
npm install markdown-loader
  1. 在webpack中配置loader
{    module: {
        rules: [{
                test: /\.md$/,
                use: [
                    {
                        loader: "html-loader"
                    },
                    {
                        loader: "markdown-loader",
                        options: {
                            /* your options here */
                        }
                    }
                ]
            }]
    }
}

在路由配置中,通过不同的.md文件去渲染对应的路径,验证发现,诸如jrv-alert这种自定义组件的html也被正确编译,没有问题。但是,我这边的需求仍然有变动,还有一些参数需要动态配置传入,这时候光使用loader已经无法满足要求了。** 这种方式也不可行 **。

** 于是,思路变成了,如何能够自定义处理编译过程中的md文件。接下来就是实践的步骤: **

  1. 通过npm中找到 vue-markdown-loader 这个node包,首先进行安装。
npm install vue-markdown-loader
  1. 在webpack中配置loader
{  test: /\.md$/,
  loader: 'vue-markdown-loader',
  options: options
}

** 这时候,我们可以拿到整个编译的过程了,在options里可以做一些自定义配置。但是,我们想让md变得更易于维护,想编写一个自定义的md语法,这时候仍然需要以下处理。 **

  1. 通过npm中找到 markdown-it-container 这个node包,首先进行安装。
npm install markdown-it-container --save
  1. 在webpack中配置loader的options
const MarkdownItContainer = require('markdown-it-container')
const striptags = require('./strip-tags')
const vueMarkdown = {
   preprocess: (MarkdownIt, source) => {
      MarkdownIt.renderer.rules.table_open = function () {
         return '<table class="table">'
      }
      MarkdownIt.renderer.rules.fence = utils.wrapCustomClass(MarkdownIt.renderer.rules.fence)
      return source
   },
   use: [
      [MarkdownItContainer, 'demo', {
         validate: params => params.trim().match(/^demo\s*(.*)$/),
         render: (tokens, idx) => {
            if (tokens[idx].nesting === 1) {
               const html = utils.convertHtml(striptags(tokens[idx + 1].content, 'script'));
 
               const m = tokens[idx].info.trim().match(/^demo\s+(.*)$/);
               if(m && m[1]) { // demo 有配置info
                  const infoArr = m[1].split("|");
                  let str = "";
                  for(let i = 1 ; i < infoArr.length; i++) {
                     str += `<p>${infoArr[i].trim()}</p>`
                  }
                  return `<jrv-demo>`
                     + `<div slot="demo">${html}</div>`
                     + `<div slot="info"><h4>${infoArr[0].trim()}</h4>${str}</div>`
                     + `<div slot="source-code">`
               } else {
                  return `<jrv-demo>`
                     + `<div slot="demo">${html}</div>`
                     + `<div slot="source-code">`
               }
            } else {
               // closing tag
               return '</div></jrv-demo>'
            }
         }
      }]
   ]
}
{  test: /\.md$/,
  loader: 'vue-markdown-loader',
  options: options
}

在上述代码中,我们定义了一个名叫 demo的自定义语法,并且制定了使用 jrv-demo 这个组件去生成该预发对应的html。完整配置代码

** 到这一步为止,我们已经能够实现,使用纯md维护对应的vue文档了。接下来要实现的就是自动化的过程。 **

后续

自动化的过程的思路大概如下:通过抓取组件列表,获取组件的slot项和data项,自动生成对应md中的slot部分和data部分。或者说半自动化就可以手动维护一个json表去对应这些。设置scripts自动生成更新对应md文件。

实例参见:art-vue文档

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

【Leetcode 做题学算法周刊】第三期

首发于微信公众号《前端成长记》,写于 2019.11.13

背景

本文记录刷题过程中的整个思考过程,以供参考。主要内容涵盖:

  • 题目分析设想
  • 编写代码验证
  • 查阅他人解法
  • 思考总结

目录

Easy

35.搜索插入位置

题目地址

题目描述

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

你可以假设数组中无重复元素。

示例:

输入: [1,3,5,6], 5
输出: 2

输入: [1,3,5,6], 2
输出: 1

输入: [1,3,5,6], 7
输出: 4

输入: [1,3,5,6], 0
输出: 0

题目分析设想

这道题目有点明显,题干说明了是排序数组,重点是排序数组,所以很明显的第一反应会使用二分法来解题。同时可以注意一下,数组中无重复元素。所以这道题我就按两个方案来作答:

  • 暴力法,直接遍历
  • 二分法,可以理解成不断折半排除不可能

编写代码验证

Ⅰ.暴力法

代码:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var searchInsert = function(nums, target) {
    if (nums.length === 0 || nums[0] > target) return 0;
    if(nums[nums.length - 1] < target) return nums.length;

    for(let i = 0; i < nums.length; i++) {
        if(nums[i] >= target) return i
    }
};

结果:

  • 62/62 cases passed (60 ms)
  • Your runtime beats 92.48 % of javascript submissions
  • Your memory usage beats 63.22 % of javascript submissions (33.8 MB)
  • 时间复杂度 O(n)

Ⅱ.二分法

代码:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var searchInsert = function(nums, target) {
    if (nums.length === 0 || nums[0] > target) return 0;
    if(nums[nums.length - 1] < target) return nums.length;

    let left = 0; // 起点
    let right = nums.length - 1; // 终点
    while(left < right) {
         // 零填充右位移,使用位运算避免溢出,大部分情况下等于 (left + right / 2)
        let i = parseInt((left + right) >>> 1) // 这里选择取左
        if (nums[i] < target) { // 中位数小于目标值
            left = i + 1 // 排除中位数左侧
        } else {
            right = i // 排除中位数右侧
        }
    }
    return left
};

结果:

  • 62/62 cases passed (52 ms)
  • Your runtime beats 99.31 % of javascript submissions
  • Your memory usage beats 61.31 % of javascript submissions (33.8 MB)
  • 时间复杂度 O(log2(n))

查阅他人解法

基本上这道题就是针对二分法进行考察的,所以没有看到其他特别的解法。

思考总结

看见排序数组,查找下标,那么就可以果断选择二分法啦。

38.报数

题目地址

题目描述

报数序列是一个整数序列,按照其中的整数的顺序进行报数,得到下一个数。其前五项如下:

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"

输入: 4
输出: "1211"

题目分析设想

这道题目有点绕,其实我们只看右侧项就可以,每次都是读上一次项。报数的规则实际上就是相同连续数字合并后进行每位的报数。最简单的想法是直接使用正则替换就可以了。当然也可以从递归和遍历的方式来作答,我们分别来看看。

  • 正则法,替换相同连续数字为 长度 + 数字本身
  • 递归法,不断转换成 n-1 求解
  • 遍历法,不断转换成 n+1 求解

编写代码验证

Ⅰ.正则法

代码:

/**
 * @param {number} n
 * @return {string}
 */
var countAndSay = function(n) {
    let str = '1'
    for(let i = 1; i < n; i++) {
        // 匹配项长度+第一位即为读数
        str = str.replace(/(\d)\1*/g, (match) => (`${match.length}${match.charAt(0)}`))
    }
    return str
};

结果:

  • 18/18 cases passed (56 ms)
  • Your runtime beats 98.81 % of javascript submissions
  • Your memory usage beats 32.53 % of javascript submissions (35.6 MB)
  • 时间复杂度 O(n)

Ⅱ.递归法

代码:

/**
 * @param {number} n
 * @return {string}
 */
var countAndSay = function(n) {
    if (n === 1) return '1'
    debugger
    let str = countAndSay(n - 1)
    let item = str.charAt(0)

    let count = 0 // 计数器
    let res = ''
    for(let i = 0; i < str.length; i++) {
        if(str.charAt(i) === item) {
            count++
        } else {
            res += `${count}${item}`
            item = str.charAt(i)
            count = 1
        }

        if (i === str.length - 1) { // 最后一位,需要取数
            res += `${count}${item}`
        }
    }
    return res
};

结果:

  • 18/18 cases passed (64 ms)
  • Your runtime beats 92.23 % of javascript submissions
  • Your memory usage beats 28.23 % of javascript submissions (35.7 MB)
  • 时间复杂度 O(n^2)

Ⅲ.遍历法

代码:

/**
 * @param {number} n
 * @return {string}
 */
var countAndSay = function(n) {
    let str = '1'
    for(let i = 1; i < n; i++) {
        str = countEach(str)
    }
    function countEach(str) {
        let count = 0
        let res = ''
        for(let i = 0; i < str.length; i++) {
            if(i === 0 || str.charAt(i) === str.charAt(i - 1)) {
                count++
            } else {
                res += `${count}${str.charAt(i - 1)}`
                count = 1
            }
            if (i === str.length - 1) {
                res += `${count}${str.charAt(i)}`
            }
        }

        return res
    }

    return str
};

结果:

  • 18/18 cases passed (60 ms)
  • Your runtime beats 96.98 % of javascript submissions
  • Your memory usage beats 41.69 % of javascript submissions (35.5 MB)
  • 时间复杂度 O(n^2)

查阅他人解法

这里看到一个开怀一笑的解法,直接字典法,缺点很明显,但是当前情况下确实是最快的。

Ⅰ.正则法

代码:

/**
 * @param {number} n
 * @return {string}
 */
var countAndSay = function(n) {
    const map = {
        1:"1",
		2:"11",
		3:"21",
		4:"1211",
		5:"111221",
		6:"312211",
		7:"13112221",
		8:"1113213211",
		9:"31131211131221",
		10:"13211311123113112211",
		11:"11131221133112132113212221",
		12:"3113112221232112111312211312113211",
		13:"1321132132111213122112311311222113111221131221",
		14:"11131221131211131231121113112221121321132132211331222113112211",
		15:"311311222113111231131112132112311321322112111312211312111322212311322113212221",
		16:"132113213221133112132113311211131221121321131211132221123113112221131112311332111213211322211312113211",
		17:"11131221131211132221232112111312212321123113112221121113122113111231133221121321132132211331121321231231121113122113322113111221131221",
		18:"31131122211311123113321112131221123113112211121312211213211321322112311311222113311213212322211211131221131211132221232112111312111213111213211231131122212322211331222113112211",
		19:"1321132132211331121321231231121113112221121321132122311211131122211211131221131211132221121321132132212321121113121112133221123113112221131112311332111213122112311311123112111331121113122112132113213211121332212311322113212221",
		20:"11131221131211132221232112111312111213111213211231132132211211131221131211221321123113213221123113112221131112311332211211131221131211132211121312211231131112311211232221121321132132211331121321231231121113112221121321133112132112312321123113112221121113122113121113123112112322111213211322211312113211",
		21:"311311222113111231133211121312211231131112311211133112111312211213211312111322211231131122211311122122111312211213211312111322211213211321322113311213212322211231131122211311123113223112111311222112132113311213211221121332211211131221131211132221232112111312111213111213211231132132211211131221232112111312211213111213122112132113213221123113112221131112311311121321122112132231121113122113322113111221131221",
		22:"132113213221133112132123123112111311222112132113311213211231232112311311222112111312211311123113322112132113213221133122112231131122211211131221131112311332211211131221131211132221232112111312111213322112132113213221133112132113221321123113213221121113122123211211131221222112112322211231131122211311123113321112131221123113111231121113311211131221121321131211132221123113112211121312211231131122211211133112111311222112111312211312111322211213211321322113311213211331121113122122211211132213211231131122212322211331222113112211",
		23:"111312211312111322212321121113121112131112132112311321322112111312212321121113122112131112131221121321132132211231131122211331121321232221121113122113121113222123112221221321132132211231131122211331121321232221123113112221131112311332111213122112311311123112112322211211131221131211132221232112111312211322111312211213211312111322211231131122111213122112311311221132211221121332211213211321322113311213212312311211131122211213211331121321123123211231131122211211131221131112311332211213211321223112111311222112132113213221123123211231132132211231131122211311123113322112111312211312111322212321121113122123211231131122113221123113221113122112132113213211121332212311322113212221",
		24:"3113112221131112311332111213122112311311123112111331121113122112132113121113222112311311221112131221123113112221121113311211131122211211131221131211132221121321132132212321121113121112133221123113112221131112311332111213213211221113122113121113222112132113213221232112111312111213322112132113213221133112132123123112111311222112132113311213211221121332211231131122211311123113321112131221123113112221132231131122211211131221131112311332211213211321223112111311222112132113212221132221222112112322211211131221131211132221232112111312111213111213211231132132211211131221232112111312211213111213122112132113213221123113112221133112132123222112111312211312112213211231132132211211131221131211132221121311121312211213211312111322211213211321322113311213212322211231131122211311123113321112131221123113112211121312211213211321222113222112132113223113112221121113122113121113123112112322111213211322211312113211",
		25:"132113213221133112132123123112111311222112132113311213211231232112311311222112111312211311123113322112132113212231121113112221121321132132211231232112311321322112311311222113111231133221121113122113121113221112131221123113111231121123222112132113213221133112132123123112111312111312212231131122211311123113322112111312211312111322111213122112311311123112112322211211131221131211132221232112111312111213111213211231132132211211131221232112111312212221121123222112132113213221133112132123123112111311222112132113213221132213211321322112311311222113311213212322211211131221131211221321123113213221121113122113121132211332113221122112133221123113112221131112311332111213122112311311123112111331121113122112132113121113222112311311221112131221123113112221121113311211131122211211131221131211132221121321132132212321121113121112133221123113112221131112212211131221121321131211132221123113112221131112311332211211133112111311222112111312211311123113322112111312211312111322212321121113121112133221121321132132211331121321231231121113112221121321132122311211131122211211131221131211322113322112111312211322132113213221123113112221131112311311121321122112132231121113122113322113111221131221",
		26:"1113122113121113222123211211131211121311121321123113213221121113122123211211131221121311121312211213211321322112311311222113311213212322211211131221131211221321123113213221121113122113121113222112131112131221121321131211132221121321132132211331121321232221123113112221131112311322311211131122211213211331121321122112133221121113122113121113222123211211131211121311121321123113111231131122112213211321322113311213212322211231131122211311123113223112111311222112132113311213211221121332211231131122211311123113321112131221123113111231121113311211131221121321131211132221123113112211121312211231131122113221122112133221121113122113121113222123211211131211121311121321123113213221121113122113121113222113221113122113121113222112132113213221232112111312111213322112311311222113111221221113122112132113121113222112311311222113111221132221231221132221222112112322211213211321322113311213212312311211131122211213211331121321123123211231131122211211131221131112311332211213211321223112111311222112132113213221123123211231132132211231131122211311123113322112111312211312111322111213122112311311123112112322211213211321322113312211223113112221121113122113111231133221121321132132211331121321232221123123211231132132211231131122211331121321232221123113112221131112311332111213122112311311123112112322211211131221131211132221232112111312111213111213211231132132211211131221131211221321123113213221123113112221131112211322212322211231131122211322111312211312111322211213211321322113311213211331121113122122211211132213211231131122212322211331222113112211",
		27:"31131122211311123113321112131221123113111231121113311211131221121321131211132221123113112211121312211231131122211211133112111311222112111312211312111322211213211321322123211211131211121332211231131122211311122122111312211213211312111322211231131122211311123113322112111331121113112221121113122113111231133221121113122113121113222123211211131211121332211213211321322113311213211322132112311321322112111312212321121113122122211211232221123113112221131112311332111213122112311311123112111331121113122112132113311213211321222122111312211312111322212321121113121112133221121321132132211331121321132213211231132132211211131221232112111312212221121123222112132113213221133112132123123112111311222112132113311213211231232112311311222112111312211311123113322112132113212231121113112221121321132122211322212221121123222112311311222113111231133211121312211231131112311211133112111312211213211312111322211231131122211311123113322113223113112221131112311332211211131221131211132211121312211231131112311211232221121321132132211331221122311311222112111312211311123113322112132113213221133122211332111213112221133211322112211213322112111312211312111322212321121113121112131112132112311321322112111312212321121113122112131112131221121321132132211231131122211331121321232221121113122113121122132112311321322112111312211312111322211213111213122112132113121113222112132113213221133112132123222112311311222113111231132231121113112221121321133112132112211213322112111312211312111322212311222122132113213221123113112221133112132123222112111312211312111322212321121113121112133221121311121312211213211312111322211213211321322123211211131211121332211213211321322113311213212312311211131122211213211331121321122112133221123113112221131112311332111213122112311311123112111331121113122112132113121113222112311311222113111221221113122112132113121113222112132113213221133122211332111213322112132113213221132231131122211311123113322112111312211312111322212321121113122123211231131122113221123113221113122112132113213211121332212311322113212221",
		28:"13211321322113311213212312311211131122211213211331121321123123211231131122211211131221131112311332211213211321223112111311222112132113213221123123211231132132211231131122211311123113322112111312211312111322111213122112311311123112112322211213211321322113312211223113112221121113122113111231133221121321132132211331121321232221123123211231132132211231131122211331121321232221123113112221131112311332111213122112311311123112112322211211131221131211132221232112111312211322111312211213211312111322211231131122111213122112311311221132211221121332211213211321322113311213212312311211131122211213211331121321123123211231131122211211131221232112111312211312113211223113112221131112311332111213122112311311123112112322211211131221131211132221232112111312211322111312211213211312111322211231131122111213122112311311221132211221121332211211131221131211132221232112111312111213111213211231132132211211131221232112111312211213111213122112132113213221123113112221133112132123222112111312211312112213211231132132211211131221131211322113321132211221121332211213211321322113311213212312311211131122211213211331121321123123211231131122211211131221131112311332211213211321322113311213212322211322132113213221133112132123222112311311222113111231132231121113112221121321133112132112211213322112111312211312111322212311222122132113213221123113112221133112132123222112111312211312111322212311322123123112111321322123122113222122211211232221123113112221131112311332111213122112311311123112111331121113122112132113121113222112311311221112131221123113112221121113311211131122211211131221131211132221121321132132212321121113121112133221123113112221131112212211131221121321131211132221123113112221131112311332211211133112111311222112111312211311123113322112111312211312111322212321121113121112133221121321132132211331121321132213211231132132211211131221232112111312212221121123222112311311222113111231133211121321321122111312211312111322211213211321322123211211131211121332211231131122211311123113321112131221123113111231121123222112111331121113112221121113122113111231133221121113122113121113221112131221123113111231121123222112111312211312111322212321121113121112131112132112311321322112111312212321121113122122211211232221121321132132211331121321231231121113112221121321133112132112312321123113112221121113122113111231133221121321132132211331221122311311222112111312211311123113322112111312211312111322212311322123123112112322211211131221131211132221132213211321322113311213212322211231131122211311123113321112131221123113112211121312211213211321222113222112132113223113112221121113122113121113123112112322111213211322211312113211",
		29:"11131221131211132221232112111312111213111213211231132132211211131221232112111312211213111213122112132113213221123113112221133112132123222112111312211312112213211231132132211211131221131211132221121311121312211213211312111322211213211321322113311213212322211231131122211311123113223112111311222112132113311213211221121332211211131221131211132221231122212213211321322112311311222113311213212322211211131221131211132221232112111312111213322112131112131221121321131211132221121321132132212321121113121112133221121321132132211331121321231231121113112221121321133112132112211213322112311311222113111231133211121312211231131122211322311311222112111312211311123113322112132113212231121113112221121321132122211322212221121123222112111312211312111322212321121113121112131112132112311321322112111312212321121113122112131112131221121321132132211231131122111213122112311311222113111221131221221321132132211331121321231231121113112221121321133112132112211213322112311311222113111231133211121312211231131122211322311311222112111312211311123113322112132113212231121113112221121321132122211322212221121123222112311311222113111231133211121312211231131112311211133112111312211213211312111322211231131122111213122112311311222112111331121113112221121113122113121113222112132113213221232112111312111213322112311311222113111221221113122112132113121113222112311311222113111221132221231221132221222112112322211211131221131211132221232112111312111213111213211231132132211211131221232112111312211213111213122112132113213221123113112221133112132123222112111312211312111322212321121113121112133221132211131221131211132221232112111312111213322112132113213221133112132113221321123113213221121113122123211211131221222112112322211231131122211311123113321112132132112211131221131211132221121321132132212321121113121112133221123113112221131112311332111213211322111213111213211231131211132211121311222113321132211221121332211213211321322113311213212312311211131122211213211331121321123123211231131122211211131221131112311332211213211321223112111311222112132113213221123123211231132132211231131122211311123113322112111312211312111322111213122112311311123112112322211213211321322113312211223113112221121113122113111231133221121321132132211331121321232221123123211231132132211231131122211331121321232221123113112221131112311332111213122112311311123112112322211211131221131211132221232112111312211322111312211213211312111322211231131122111213122112311311221132211221121332211213211321322113311213212312311211131211131221223113112221131112311332211211131221131211132211121312211231131112311211232221121321132132211331121321231231121113112221121321133112132112211213322112312321123113213221123113112221133112132123222112311311222113111231132231121113112221121321133112132112211213322112311311222113111231133211121312211231131112311211133112111312211213211312111322211231131122111213122112311311221132211221121332211211131221131211132221232112111312111213111213211231132132211211131221232112111312211213111213122112132113213221123113112221133112132123222112111312211312111322212311222122132113213221123113112221133112132123222112311311222113111231133211121321132211121311121321122112133221123113112221131112311332211322111312211312111322212321121113121112133221121321132132211331121321231231121113112221121321132122311211131122211211131221131211322113322112111312211322132113213221123113112221131112311311121321122112132231121113122113322113111221131221",
		30:"3113112221131112311332111213122112311311123112111331121113122112132113121113222112311311221112131221123113112221121113311211131122211211131221131211132221121321132132212321121113121112133221123113112221131112212211131221121321131211132221123113112221131112311332211211133112111311222112111312211311123113322112111312211312111322212321121113121112133221121321132132211331121321132213211231132132211211131221232112111312212221121123222112311311222113111231133211121321321122111312211312111322211213211321322123211211131211121332211231131122211311123113321112131221123113111231121123222112111331121113112221121113122113111231133221121113122113121113221112131221123113111231121123222112111312211312111322212321121113121112131112132112311321322112111312212321121113122122211211232221121321132132211331121321231231121113112221121321132132211322132113213221123113112221133112132123222112111312211312112213211231132132211211131221131211322113321132211221121332211231131122211311123113321112131221123113111231121113311211131221121321131211132221123113112211121312211231131122211211133112111311222112111312211312111322211213211321223112111311222112132113213221133122211311221122111312211312111322212321121113121112131112132112311321322112111312212321121113122122211211232221121321132132211331121321231231121113112221121321132132211322132113213221123113112221133112132123222112111312211312112213211231132132211211131221131211322113321132211221121332211213211321322113311213212312311211131122211213211331121321123123211231131122211211131221131112311332211213211321223112111311222112132113213221123123211231132132211231131122211311123113322112111312211312111322111213122112311311123112112322211213211321322113312211223113112221121113122113111231133221121321132132211331222113321112131122211332113221122112133221123113112221131112311332111213122112311311123112111331121113122112132113121113222112311311221112131221123113112221121113311211131122211211131221131211132221121321132132212321121113121112133221123113112221131112311332111213122112311311123112112322211322311311222113111231133211121312211231131112311211232221121113122113121113222123211211131221132211131221121321131211132221123113112211121312211231131122113221122112133221121321132132211331121321231231121113121113122122311311222113111231133221121113122113121113221112131221123113111231121123222112132113213221133112132123123112111312211322311211133112111312211213211311123113223112111321322123122113222122211211232221121113122113121113222123211211131211121311121321123113213221121113122123211211131221121311121312211213211321322112311311222113311213212322211211131221131211221321123113213221121113122113121113222112131112131221121321131211132221121321132132211331121321232221123113112221131112311322311211131122211213211331121321122112133221121113122113121113222123112221221321132132211231131122211331121321232221121113122113121113222123211211131211121332211213111213122112132113121113222112132113213221232112111312111213322112132113213221133112132123123112111311222112132113311213211221121332211231131122211311123113321112131221123113112221132231131122211211131221131112311332211213211321223112111311222112132113212221132221222112112322211211131221131211132221232112111312111213111213211231131112311311221122132113213221133112132123222112311311222113111231132231121113112221121321133112132112211213322112111312211312111322212321121113121112131112132112311321322112111312212321121113122122211211232221121311121312211213211312111322211213211321322123211211131211121332211213211321322113311213211322132112311321322112111312212321121113122122211211232221121321132132211331121321231231121113112221121321133112132112312321123113112221121113122113111231133221121321132122311211131122211213211321222113222122211211232221123113112221131112311332111213122112311311123112111331121113122112132113121113222112311311221112131221123113112221121113311211131122211211131221131211132221121321132132212321121113121112133221123113112221131112311332111213213211221113122113121113222112132113213221232112111312111213322112132113213221133112132123123112111312211322311211133112111312212221121123222112132113213221133112132123222113223113112221131112311332111213122112311311123112112322211211131221131211132221232112111312111213111213211231132132211211131221131211221321123113213221123113112221131112211322212322211231131122211322111312211312111322211213211321322113311213211331121113122122211211132213211231131122212322211331222113112211"
    }

    return map[n]
};

结果:

  • 18/18 cases passed (56 ms)
  • Your runtime beats 98.81 % of javascript submissions
  • Your memory usage beats 98.5 % of javascript submissions (33.5 MB)
  • 时间复杂度 O(1)

思考总结

总体而言,这道题用正则去解答十分简单,考验的点在正则的匹配这块;当然递归或者遍历也是常规思路;字典法纯属一乐。推荐使用正则来解答此题。

53.最大子序和

题目地址

题目描述

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6

进阶:

如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。

题目分析设想

这道题首先基本解法肯定是暴力的遍历求解,直接遍历找出最大区间。当然这里我们也可以使用动态规划来思考问题,列出动态和转移方程式,等于求解 Max(d[0, i])。另外进阶里面提示分治法,分治法在之前有用过,我们也可以做为一个方向。所以大概有三种:

  • 遍历求解,直接遍历算出各区间值
  • 动态规划问题,求解动态问题,找到每个动态区间的最大值
  • 分治,不断二分找区间内的最大子序和

注意一下,只要是寻找最大值最小值的,初始值需要定义为理论上的最大最小值。

编写代码验证

Ⅰ.遍历求解

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function(nums) {
    let res = Number.MIN_SAFE_INTEGER
    for(let i = 0; i < nums.length; i++) {
        let sum = 0
        // 分别算出 i 开始的最大子序和
        for(let j = i; j < nums.length; j++) {
            sum += nums[j];
            res = Math.max(res, sum)
        }
    }
    return res
};

结果:

  • 202/202 cases passed (272 ms)
  • Your runtime beats 7.61 % of javascript submissions
  • Your memory usage beats 41.88 % of javascript submissions (35.1 MB)
  • 时间复杂度 O(n^2)

Ⅱ.动态规划

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function(nums) {
    let res = dp = nums[0] // 初始值
    for(let i = 1; i < nums.length; i++) {
        dp = Math.max(dp + nums[i], nums[i]) // 动态取得最大值
        res = Math.max(res, dp)
    }
    return res
};

结果:

  • 202/202 cases passed (68 ms)
  • Your runtime beats 90.14 % of javascript submissions
  • Your memory usage beats 45 % of javascript submissions (35.1 MB)
  • 时间复杂度 O(n)

Ⅲ.分治

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function(nums) {
    // 不断分治
    function countByDichotomy (start, end) {
        // 存储左侧结果,右侧结果,两者更大值,以及两者相加的值
        // 解释一下:最大子序列在左右两区间内要么过界要么不过界。
        // 如果不过界,则最大值为 Max(left, right)
        // 如果过界,则最大为左区间到中间的最大值加中间到右区间的最大值
        if (end === start) { // 数组就一项
            return {
                lmax: nums[start], // 左半区间包含其右端点的最大子序和
                rmax: nums[start], // 右半区间包含其左端点的最大子序和
                sum: nums[start], // 总和
                result: nums[start] // 区域内部的最大子序和
            }
        } else {
            const mid = (start + end) >>> 1 // 这个取中位数写法之前解释过,避免溢出
            const left = countByDichotomy(start, mid) // 左区间中计算结果
            const right = countByDichotomy(mid + 1, end) // 右区间中计算结果
            return {
                lmax: Math.max(left.lmax, left.sum + right.lmax),
                rmax: Math.max(right.rmax, left.rmax + right.sum),
                sum: left.sum + right.sum,
                result: Math.max(left.rmax + right.lmax, Math.max(left.result, right.result))
            }
        }
    }
    return countByDichotomy(0, nums.length - 1).result;
};

结果:

  • 202/202 cases passed (60 ms)
  • Your runtime beats 97.89 % of javascript submissions
  • Your memory usage beats 5.01 % of javascript submissions (36.7 MB)
  • 时间复杂度 O(n)

查阅他人解法

查阅题解的过程中发现了以下几种有意思的思路:

  • 动态规划,使用增益的思路。其实上面我们写的动态规划是一样的
  • 贪心法,尝试多加一位,取较大值
  • 分治中使用贪心法求区间

Ⅰ.动态规划

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function(nums) {
    let res = nums[0]
    let sum = nums[0] // 增益
    for(let i = 1; i < nums.length; i++) {
        if (sum > 0) { // 正向增益, sum 保留并加上当前遍历数字
            sum += nums[i]
        } else { // sum 更新为当前遍历数字
            sum = nums[i]
        }
        res = Math.max(res, sum)
    }
    return res
};

结果:

  • 202/202 cases passed (60 ms)
  • Your runtime beats 97.89 % of javascript submissions
  • Your memory usage beats 47.5 % of javascript submissions (35.1 MB)
  • 时间复杂度 O(n)

Ⅱ.贪心法

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function(nums) {
    let res = Number.MIN_SAFE_INTEGER // 初始值
    let sum = 0
    for(let i = 0; i < nums.length; i++) {
        sum += nums[i]
        res = Math.max(res, sum)
        if (sum < 0) { // 重新开始找子序串
            sum = 0;
        }
    }
    return res
};

结果:

  • 202/202 cases passed (68 ms)
  • Your runtime beats 90.14 % of javascript submissions
  • Your memory usage beats 45 % of javascript submissions (35.1 MB)
  • 时间复杂度 O(n)

Ⅲ.分治中使用贪心法求区间

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function(nums) {
    // 获取跨边界的和
    function getMaxCross (start, mid, end) {
        let leftRes = Number.MIN_SAFE_INTEGER
        let leftSum = 0
        for(let i = mid; i >= start; i--) {
            leftSum += nums[i]
            leftRes = Math.max(leftRes, leftSum)
        }

        let rightRes = Number.MIN_SAFE_INTEGER
        let rightSum = 0
        for(let i = mid + 1; i <= end; i++) {
            rightSum += nums[i]
            rightRes = Math.max(rightRes, rightSum)
        }

        return leftRes + rightRes
    }

    function countByDichotomy (start, end) {
        if (start === end) {
            return nums[start]
        } else {
            const mid = (start + end) >>> 1
            const leftSum = countByDichotomy(start, mid)
            const rightSum = countByDichotomy(mid + 1, end)
            const midSum = getMaxCross(start, mid, end)
            // 三者比较最大的就为最大子序和
            return Math.max(leftSum, rightSum, midSum)
        }
    }

    return countByDichotomy(0, nums.length - 1)
};

结果:

  • 202/202 cases passed (72 ms)
  • Your runtime beats 80.56 % of javascript submissions
  • Your memory usage beats 49.38 % of javascript submissions (35.1 MB)
  • 时间复杂度 O(nlog(n))

思考总结

个人认为动态规划在这套题里面解题思路清晰,贪心法也可以理解为基于遍历基础上做的延伸,而分治法需要画图加以理解。一般看到这种最大最长的题目,基本上就可以用动态规划问题来尝试作答了。

58.最后一个单词的长度

题目地址

题目描述

给定一个仅包含大小写字母和空格 ' ' 的字符串,返回其最后一个单词的长度。

如果不存在最后一个单词,请返回 0

说明:一个单词是指由字母组成,但不包含任何空格的字符串。

示例:

输入: "Hello World"
输出: 5

题目分析设想

这道题看上去像是一道字符串题,我们可以从以下几个方面来尝试作答:

  • 遍历,从末尾开始,效率高
  • lastIndexOf,直接找空格
  • 正则
  • split

编写代码验证

Ⅰ.遍历

代码:

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLastWord = function(s) {
    if(!s.length) return 0
    let i = s.length - 1
    while(i >= 0 && s.charAt(i) === ' ') {
        i--
    }
    if(i < 0) return 0 // 全是空格

    let j = i
    while(j >= 0 && s.charAt(j) != ' ') {
        j--
    }
    return i - j
};

结果:

  • 59/59 cases passed (64 ms)
  • Your runtime beats 81.14 % of javascript submissions
  • Your memory usage beats 29.72 % of javascript submissions (33.8 MB)
  • 时间复杂度 O(n)

Ⅱ.lastIndexOf

代码:

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLastWord = function(s) {
    if(!s.length) return 0
    s = s.trim()
    const idx = s.lastIndexOf(' ')
    return idx === -1 ? s.length : s.length - 1 - idx
};

结果:

  • 59/59 cases passed (48 ms)
  • Your runtime beats 99.48 % of javascript submissions
  • Your memory usage beats 36.52 % of javascript submissions (33.7 MB)
  • 时间复杂度 O(1)

Ⅲ.正则

代码:

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLastWord = function(s) {
    if(!s.length) return 0
    const match = s.match(/([a-zA-Z]+)\s*$/)
    let res = 0
    if (match) {
        res = match.pop()
        return res.length
    }
    return res
};

结果:

  • 59/59 cases passed (80 ms)
  • Your runtime beats 26.65 % of javascript submissions
  • Your memory usage beats 5.95 % of javascript submissions (34.2 MB)
  • 时间复杂度 O(1)

Ⅳ.split

代码:

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLastWord = function(s) {
    if(!s.length) return 0
    s = s.trim()
    const arr = s.split(' ')
    if (arr.length) {
        let str = arr.pop()
        return str.length
    } else {
        return 0
    }
};

结果:

  • 59/59 cases passed (60 ms)
  • Your runtime beats 90.57 % of javascript submissions
  • Your memory usage beats 13.8 % of javascript submissions (34 MB)
  • 时间复杂度 O(1)

查阅他人解法

没有在题解中看到什么特别的解法,大部分都是基于类库解的,比如 JavascriptStringArray 的方法。或者是遍历实现的。

思考总结

直到现在也没有弄明白这道题的考察点在哪里?不过我建议感兴趣的同学,可以自己拓展实现 lastIndexOf ,参考 上一期 28题的数十种解法,应该能有不小收获。

66.加一

题目地址

题目描述

给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。

最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。

你可以假设除了整数 0 之外,这个整数不会以零开头。

示例:

输入: [1,2,3]
输出: [1,2,4]
解释: 输入数组表示数字 123

输入: [4,3,2,1]
输出: [4,3,2,2]
解释: 输入数组表示数字 4321

题目分析设想

这道题我有两个大方向,一是数组遍历进行求解,另外一种是数组转数字再处理。但是转数字可能会溢出,所以就只想到从遍历的角度来作答。

  • 遍历,从后往前遍历找到不为9的项,后面填0就可以了

编写代码验证

Ⅰ.遍历

代码:

/**
 * @param {number[]} digits
 * @return {number[]}
 */
var plusOne = function(digits) {
    for(let i = digits.length - 1; i >= 0; i--) {
        // 找不到不为9的数,直接加1输出就可以了
        if(digits[i] !== 9) {
            digits[i]++
            return digits
        } else {
            digits[i] = 0
        }
    }
    digits.unshift(1)
    return digits
};

结果:

  • 109/109 cases passed (60 ms)
  • Your runtime beats 93.72 % of javascript submissions
  • Your memory usage beats 26.35 % of javascript submissions (33.8 MB)
  • 时间复杂度 O(n)

查阅他人解法

发现思路基本都是遍历,但是具体实现会有些差距,这里只列举一个最简单的。

Ⅰ.遍历

代码:

/**
 * @param {number[]} digits
 * @return {number[]}
 */
var plusOne = function(digits) {
    for(let i = digits.length - 1; i >= 0; i--) {
        digits[i]++
        // 取10的余数,做了赋值操作,为0就继续进位
        digits[i] %= 10
        if(digits[i] !== 0) {
            return digits
        }
    }
    digits.unshift(1)
    return digits
};

结果:

  • 109/109 cases passed (64 ms)
  • Your runtime beats 85.29 % of javascript submissions
  • Your memory usage beats 94.34 % of javascript submissions (33.4 MB)
  • 时间复杂度 O(n)

思考总结

这道题可能大众的想法会转成数字再处理,但是做数字运算的时候,千万要记住考虑溢出的问题。另外因为是加1,所以倒序遍历就可以了。至于是判断末位为9还是对10取余,我觉得都是一个很好理解的思路,也避免了代码的繁琐。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

canvas实现下雪效果

首发于微信公众号《前端成长记》,写于 2017.01.20

介绍

该Demo主要是requestAnimationFrame的一个简单应用,实现了粒子效果,网上也有一些其他的粒子效果的实现的根本原理也大致相同,只是运动路径算法差异。

在线示例

在线示例

实现

requestAnimationFrame

** 首先,你需要明白requestAnimationFrame是个什么东西 **

在浏览器动画程序中,我们通常使用一个定时器来循环每隔几毫秒移动目标物体一次,来让它动起来。如今有一个好消息,浏览器开发商们决定:“嗨,为什么我们不在浏览器里提供这样一个API呢,这样一来我们可以为用户优化他们的动画。”所以,这个requestAnimationFrame()函数就是针对动画效果的API,你可以把它用在DOM上的风格变化或画布动画或WebGL中。

** 那么使用它有什么优势呢? **

浏览器可以优化并行的动画动作,更合理的重新排列动作序列,并把能够合并的动作放在一个渲染周期内完成,从而呈现出更流畅的动画效果。如果在一个浏览器标签页里运行一个动画,当这个标签页不可见时,浏览器会暂停它,这会减少CPU,内存的压力,节省电池电量。

所以,我们第一步现在代码中增加一个rAF方法

// 使用最优频率requestAnimationFrame代替定时器,当然如果浏览器如果不支持的话,还是使用定时器完成
window.rAF = (function(){    
  return window.requestAnimationFrame ||
             window.webkitRequestAnimationFrame ||
             window.mozRequestAnimationFrame ||
            window.oRequestAnimationFrame ||
            window.msRequestAnimationFrame ||
            function (callback) {                
              setTimeout(callback, 1000 / 60);            
            };
})();

初始化基本配置

  1. 首先我们可以设置一些默认的值,便于后续做修改调整或者适配等。
// 默认配置
var defaultSetting = {
  width: 900, // canvas默认宽度,后面修改为频幕宽度
  height: 300, // canvas默认高度
  canvas: null, // canvas对象
  ctx: null, // 画布content
  snowArr: [], // 雪花数组
  total: 80  // 雪花数量
}
  1. 接下来往雪花数组中推入指定数量的雪花对象,当然,每一个雪花我们可以给它设定自己的属性,长宽,渐变等。
defaultSetting.width = window.innerWidth || document.documentElement.clientWidth ||  document.body.clientWidth;
defaultSetting.canvas = document.getElementById("canvas");
defaultSetting.canvas.setAttribute("width",defaultSetting.width);
defaultSetting.ctx = defaultSetting.canvas.getContext("2d");
for(var i = 0; i < defaultSetting.total;i++){
    defaultSetting.snowArr.push({
        "left":Tools.createRandom(0, defaultSetting.width), // 工具函数createRandom是创建一个m上下n的随机数
        "top":Tools.createRandom(0, defaultSetting.height),
        "deg":Tools.createRandom(-6, 6),
        "scale":Tools.createRandom(3, 6)
    });
}

这个时候我们就得到了开始时刻雪花的数组和相关属性值。

核心,不断渲染雪花位置的函数

function updateSnow(){
    // 清除画布,重新绘制下一帧雪花的位置
    defaultSetting.ctx.clearRect(0, 0, defaultSetting.width, defaultSetting.height);
    defaultSetting.ctx.save();
    // 循环雪花数组,分别绘制每一个雪花的位置
    for(var i=0; i<defaultSetting.snowArr.length; i++){
        var h = 0.5 * defaultSetting.snowArr[i].scale;
        defaultSetting.snowArr[i].left = defaultSetting.snowArr[i].left + Math.tan(Tools.radian(defaultSetting.snowArr[i].deg))*h/2;
        defaultSetting.snowArr[i].top = defaultSetting.snowArr[i].top + h;
        // 删除画面外的雪花
        if(defaultSetting.snowArr[i].left < 0 || defaultSetting.snowArr[i].left > defaultSetting.width || defaultSetting.snowArr[i].top > defaultSetting.height){
            defaultSetting.snowArr.splice(i--, 1);
            continue;
        }
        var width_i = defaultSetting.snowArr[i].scale;
        // 雪花边界投影
        var ra = defaultSetting.ctx.createRadialGradient(defaultSetting.snowArr[i].left, defaultSetting.snowArr[i].top, width_i/4, defaultSetting.snowArr[i].left, defaultSetting.snowArr[i].top, width_i);
        ra.addColorStop(0, "rgba(255,255,255,0.8)");
        ra.addColorStop(1, "rgba(255,255,255,0.1)");
        defaultSetting.ctx.fillStyle = ra;
        defaultSetting.ctx.beginPath();
        defaultSetting.ctx.arc(defaultSetting.snowArr[i].left, defaultSetting.snowArr[i].top, width_i, 0, 2*Math.PI);
        defaultSetting.ctx.fill();
    }
    defaultSetting.ctx.restore();
    // 持续刷新雪花的位置
    rAF(updateSnow);
}

这样我们就得到了一组80个不断运动的雪花,但是这样的话不会有源源不断的新雪花加入,所以,最后一步,我们还需要不断补充新鲜雪花。

补充新鲜雪花

这个时候就会发现开始把雪花数组给单独提出来是多么明智,只需要不断push到该数组就行了

// 增加新的雪花
function createNewSnow(){
    setTimeout(function(){
        if(defaultSetting.snowArr.length < 200){
            for(var i=0; i<20; i++){
                defaultSetting.snowArr.push({
                    "left":Tools.createRandom(0, defaultSetting.width),
                    "top":0,
                    "deg":Tools.createRandom(-6, 6),
                    "scale":Tools.createRandom(3, 6)
                });
            }
        }
        createNewSnow();
    }, Math.random()*200+500);
}
createNewSnow();

总结

一个简单的粒子下雪效果就是这么简单,进一步可以去试试alloy团队的粒子效果。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

【Leetcode 做题学算法周刊】第二期

首发于微信公众号《前端成长记》,写于 2019.11.05

背景

本文记录刷题过程中的整个思考过程,以供参考。主要内容涵盖:

  • 题目分析设想
  • 编写代码验证
  • 查阅他人解法
  • 思考总结

目录

Easy

20.有效的括号

题目地址

题目描述

给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。

注意空字符串可被认为是有效字符串。

示例:

输入: "()"
输出: true

输入: "()[]{}"
输出: true

输入: "(]"
输出: false

输入: "([)]"
输出: false

输入: "{[]}"
输出: true

题目分析设想

这道题从题面来看,仍然需要对字符串做遍历处理,找到相互匹配的括号,剔除后继续做处理即可。所以这道题我的解题想法是:

  • 使用栈来记录,匹配的一对就出栈,最后判断栈是否为空

有几点需要注意下,可以减少一些计算量:

  1. 题面说明了字符串只含有三种括号,所以长度为奇数,一定无效
  2. 只要有一对不符合,则可判定一定无效
  3. 堆栈长度超过字符串长度一半,则一定无效
  4. 先找到右括号则一定无效

编写代码验证

Ⅰ.记录栈

代码:

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
    if (s === '') return true;
    if (s.length % 2) return false;
    // hash 表做好索引
    const hash = {
        '(': ')',
        '[': ']',
        '{': '}'
    }
    let arr = []
    for (let i = 0; i < s.length; i++) {
        if (!hash[s.charAt(i)]) { // 推入的是右括号
            if (!arr.length || hash[arr[arr.length - 1]] !== s.charAt(i)) {
                return false
            } else {
                arr.pop()
            }
        } else {
            if (arr.length >= s / 2) {   // 长度超过一半
                return false
            }
            arr.push(s.charAt(i))
        }
    }
    return !arr.length
};

结果:

  • 76/76 cases passed (64 ms)
  • Your runtime beats 90.67 % of javascript submissions
  • Your memory usage beats 64.59 % of javascript submissions (33.8 MB)
  • 时间复杂度: O(n)

查阅他人解法

发现一个很暴力的解法,虽然效率不高,但是思路清奇。我们来看看实现:

Ⅰ.暴力正则

代码:

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
    if (s === '') return true;
    if (s.length % 2) return false;

    while(s.length) {
        const s_ = s
        s = s.replace('()','').replace('[]','').replace('{}','')
        if (s === s_) return false;
    }
    return true;
};

结果:

  • 76/76 cases passed (104 ms)
  • Your runtime beats 14.95 % of javascript submissions
  • Your memory usage beats 19.75 % of javascript submissions (35.7 MB)
  • 时间复杂度: O(n)

思考总结

就这题而言,我还是更倾向于增加一个辅助栈来做记录。因为一旦去掉只包含括号的限制,那么正则将无法解答。

21.合并两个有序链表

题目地址

题目描述

将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

题目分析设想

这道题从题面上就说明了这是一道链表相关问题,要进行链表合并,无非是修改链表指针指向,或者是链表拼接。所以,这道题我有两种思路的解法:

  • 修改指针,不断取出某一条链表中的数,插入到另外一条链表
  • 链表拼接,递归比较哪条链表的元素更小,就截取拼接到另一条

两种方式的区别很明显,修改指针的方式需要存储和不断修改指针指向,拼接的方式直接做链表拼接。

当然这里也有一些特殊值需要考虑进来。

编写代码验证

Ⅰ.修改指针

代码:

/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var mergeTwoLists = function(l1, l2) {
    if (l1 === null) return l2
    if (l2 === null) return l1
    // 结果链表
    let l = new ListNode(0)
    // 不断更新的当前结点指针,对象赋值为传址,所以下面改指针指向即可
    let cursor = l
    // 会有一个先遍历完,变成 null
    while(l1 !== null && l2 !== null) {
        if (l1.val <= l2.val) { // 哪个小,指针就指向哪
            cursor.next = l1
            l1 = l1.next
        } else {
            cursor.next = l2
            l2 = l2.next
        }
        // 可以理解为 l.next.next.next ...
        cursor = cursor.next
    }
    // 有一个为空则可以直接拼接
    cursor.next = l1 === null ? l2 : l1
    return l.next
};

结果:

  • 208/208 cases passed (60 ms)
  • Your runtime beats 99.51 % of javascript submissions
  • Your memory usage beats 51.04 % of javascript submissions (35.4 MB)
  • 时间复杂度 O(m + n) ,分别代表两个链表长度

Ⅱ.链表拼接

代码:

/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var mergeTwoLists = function(l1, l2) {
    if (l1 === null) return l2
    if (l2 === null) return l1
    if (l1.val < l2.val) {
        l1.next = mergeTwoLists(l1.next, l2)
        return l1   // 这个是合并后的了
    } else {
        l2.next = mergeTwoLists(l1, l2.next)
        return l2   // 这个是合并后的了
    }
};

结果:

  • 208/208 cases passed (68 ms)
  • Your runtime beats 96.41 % of javascript submissions
  • Your memory usage beats 51.04 % of javascript submissions (35.4 MB)
  • 时间复杂度 O(m + n) ,分别代表两个链表长度

查阅他人解法

思路基本上都是这两种,未发现方向不同的解法。

无非是有些解法额外开辟了新的链表来记录,或者一些细节上的差异。

思考总结

这里的链表拼接解法,有没有发现跟 上一期 14题中的分治思路是一样的?对,实际上这个也是分治思路的一个应用。

26.删除排序数组中的重复项

题目地址

题目描述

给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。

示例:

给定数组 nums = [1,1,2],

函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2

你不需要考虑数组中超出新长度后面的元素。

给定 nums = [0,0,1,1,1,2,2,3,3,4],

函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4

你不需要考虑数组中超出新长度后面的元素。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

题目分析设想

如果是单纯的数组去重,那有很多种方法可以做。所以题目也加了限制条件,总结一下比较重要的几点:

  • 不要使用额外的数组空间,空间复杂度为 O(1)
  • 原地删除重复元素
  • 不需要考虑超过新长度后面的元素

这意味着不允许使用新的数组来解题,也就是对原数组进行操作。最后一点注意点可以看出,数组项的拷贝复制是一个方向,第二点可以看出数组删除是一个方向。删除元素的话就不会超过,所以不需要考虑两者结合。所以这题我分两个方向来解:

  • 拷贝数组元素
  • 删除数组元素

编写代码验证

Ⅰ.拷贝数组元素

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var removeDuplicates = function(nums) {
    if (nums.length === 0) return 0;
    var len = 1
    for(let i = 1; i < nums.length; i++) {
        if(nums[i] !== nums[i - 1]) { // 后一项不等于前一项
            nums[len++] = nums[i] // 拷贝数组元素
        }
    }
    return len
};

结果:

  • 161/161 cases passed (68 ms)
  • Your runtime beats 99.81 % of javascript submissions
  • Your memory usage beats 77.54 % of javascript submissions (36.6 MB)
  • 时间复杂度 O(n)

Ⅱ.删除数组元素

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var removeDuplicates = function(nums) {
    if (nums.length === 0) return 0;
    for(let i = 1; i < nums.length;) {
        if(nums[i] === nums[i - 1]) { // 后一项等于前一项
            nums.splice(i, 1)
        } else {
            i++
        }
    }
    return nums.length
};

结果:

  • 161/161 cases passed (96 ms)
  • Your runtime beats 75.93 % of javascript submissions
  • Your memory usage beats 30.85 % of javascript submissions (37.3 MB)
  • 时间复杂度 O(n)

查阅他人解法

这里看见一种很巧妙的解法,双指针法。相当于一个用于计数,一个用于扫描。

Ⅰ.双指针法

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var removeDuplicates = function(nums) {
    if (nums.length === 0) return 0;

    let i = 0;
    for(let j = 1; j < nums.length; j++) {
        if (nums[j] !== nums[i]) {
            nums[++i] = nums[j]
        }
    }
    return i + 1  // 下标 +1 为数组长度
};

结果:

  • 161/161 cases passed (68 ms)
  • Your runtime beats 99.81 % of javascript submissions
  • Your memory usage beats 84.03 % of javascript submissions (36.5 MB)
  • 时间复杂度 O(n)

思考总结

就三种解法而言,删除数组元素会频繁修改数组,不建议使用。双指针法和拷贝数组元素代码逻辑相似,但是思路上是截然不同的。

27.移除元素

题目地址

题目描述

给定一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

示例:

给定 nums = [3,2,2,3], val = 3,

函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2

你不需要考虑数组中超出新长度后面的元素。

给定 nums = [0,1,2,2,3,0,4,2], val = 2,

函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4

注意这五个元素可为任意顺序。

你不需要考虑数组中超出新长度后面的元素。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

题目分析设想

这题跟上一题非常相似,所以我们可以沿用上题的方向来解这道题:

  • 删除数组元素
  • 双指针法

编写代码验证

Ⅰ.删除数组元素

代码:

/**
 * @param {number[]} nums
 * @param {number} val
 * @return {number}
 */
var removeElement = function(nums, val) {
    if (nums.length === 0) return 0;

    for(let i = 0; i < nums.length;) {
        if (nums[i] === val) {
            nums.splice(i, 1)
        } else {
            i++
        }
    }
};

结果:

  • 113/113 cases passed (64 ms)
  • Your runtime beats 89.43 % of javascript submissions
  • Your memory usage beats 47.42 % of javascript submissions (33.7 MB)
  • 时间复杂度 O(n)

Ⅱ.双指针法

代码:

/**
 * @param {number[]} nums
 * @param {number} val
 * @return {number}
 */
var removeElement = function(nums, val) {
    if (nums.length === 0) return 0;

    let i = 0
    for(let j = 0; j < nums.length; j++) {
        if (nums[j] !== val) {
            nums[i++] = nums[j]
        }
    }
    return i
};

结果:

  • 113/113 cases passed (60 ms)
  • Your runtime beats 95.11 % of javascript submissions
  • Your memory usage beats 98.18 % of javascript submissions (33.3 MB)
  • 时间复杂度 O(n)

查阅他人解法

看到两个略有差异的方法:

  • 单指针法,使用 const of 替换一次遍历,只是写法区别,没有本质提升
  • 交换移除,相同时候与最后一项交换,同时数组长度减1

Ⅰ.单指针法

代码:

/**
 * @param {number[]} nums
 * @param {number} val
 * @return {number}
 */
var removeElement = function(nums, val) {
    if (nums.length === 0) return 0;

    let i = 0;
    for(const num of nums) {
        if(num !== val) {
            nums[i++] = num;
        }
    }
    return i;
};

结果:

  • 113/113 cases passed (68 ms)
  • Your runtime beats 80.29 % of javascript submissions
  • Your memory usage beats 43.35 % of javascript submissions (33.7 MB)
  • 时间复杂度 O(n)

Ⅱ.交换移除

代码:

/**
 * @param {number[]} nums
 * @param {number} val
 * @return {number}
 */
var removeElement = function(nums, val) {
    if (nums.length === 0) return 0;

    let i = nums.length;
    for(let j = 0; j < i;) {
        if (nums[j] === val) {
            nums[j] = nums[--i]
        } else {
            j++
        }
    }

    return i;
};

结果:

  • 113/113 cases passed (68 ms)
  • Your runtime beats 80.29 % of javascript submissions
  • Your memory usage beats 44.53 % of javascript submissions (33.7 MB)
  • 时间复杂度 O(n)

思考总结

这里开拓下思路:如果要移除的是多项,那么还是使用指针法做处理合适;如果是移除单项,那么使用交互移除法其实遍历次数最少。

28.实现strStr

题目地址

题目描述

实现 strStr() 函数。

给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1

示例:

输入: haystack = "hello", needle = "ll"
输出: 2

输入: haystack = "aaaaa", needle = "bba"
输出: -1

说明:

needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。

对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与 C 语言的 strstr() 以及 JavaindexOf() 定义相符。

题目分析设想

这道题很明显是一道字符串搜索的题目,估计是在考察算法,但是受限知识面,所以我就先以现有方式实现作答,再来学习算法了。

  • IndexOf 这个是原生方法,考察这个就没有意义了,所以不做详细论述
  • 遍历匹配,相当于自己实现一个 IndexOf

编写代码验证

Ⅰ.遍历匹配

代码:

/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
var strStr = function(haystack, needle) {
    if (needle === '') return 0
    if (needle.length > haystack.length) return -1
    if (needle.length === haystack.length && needle !== haystack) return -1
    for(let i = 0; i < haystack.length; i++) {
        if (i + needle.length > haystack.length) {
            return -1
        } else {
            const str = haystack.substr(i, needle.length)
            if (str === needle) {
                return i
            }
        }
    }
    return -1
};

结果:

  • 74/74 cases passed (64 ms)
  • Your runtime beats 90.58 % of javascript submissions
  • Your memory usage beats 44.22 % of javascript submissions (33.9 MB)
  • 时间复杂度 O(n)

查阅他人解法

首先查阅《算法导论》,看到字符串匹配有以下四种:

  • 朴素字符串匹配算法
  • Rabin-Karp 算法
  • 利用有限自动机进行字符串匹配
  • KMP 算法

然后再看题解,大概还找到以下三种算法:

  • BM 算法
  • Horspool 算法
  • Sunday 算法

Ⅰ.朴素字符串匹配算法

算法说明:

通过一个循环找到所有有效偏移,该循环对 n-m+1 个可能的 s 值进行检测,看能否满足条件 P[1..m] = T[s+1...s+m]。其中 n 是字符串长度, 'm' 是匹配字符串长度。

代码:

/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
var strStr = function(haystack, needle) {
    if (needle === '') return 0
    if (needle.length > haystack.length) return -1
    if (needle.length === haystack.length && needle !== haystack) return -1

    let i = 0;
    let j = 0;
    while(j < needle.length && i < haystack.length) {
        if(haystack[i] === needle[j]) { // 同位相等,继续判断下一位
            i++;
            j++;
        } else {
            i = i - j + 1; // i 偏移
            j = 0; // j 重置

            if (i + needle.length > haystack.length) { // 我增加的优化点,减少一些运算
                return -1
            }
        }
    }
    if (j >= needle.length) { // 子串比完了,此时 j 应该等于 needle.length
        return i - needle.length;
    } else {
        return -1
    }
};

结果:

  • 74/74 cases passed (56 ms)
  • Your runtime beats 98.45 % of javascript submissions
  • Your memory usage beats 30.12 % of javascript submissions (34.8 MB)
  • 时间复杂度 O((n-m + 1) * m)

Ⅱ.Rabin-Karp 算法

算法说明:

进行哈希运算,将字符串转成对应的哈希值进行比对,类似16进制。这里题目是字符串,我就用 ASCII 值来表示每个字符的哈希值,那么就可以计算出模式串的哈希值,再进行滚动比较。

每次滚动只需要做固定的 -*+ 三个操作,即可得出滚动串的哈希值了。

比如计算 bbc ,哈希值为 hash = (b.charCodeAt() * 128 ^ 2 + b.charCodeAt() * 128 + c.charCodeAt()),如果要计算后新值 bca 则为 (hash - b.charCodeAt() * 128 ^ 2) * 128 + c.charCodeAt()

代码:

/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
var strStr = function(haystack, needle) {
    if (needle === '') return 0
    if (needle.length > haystack.length) return -1
    if (needle.length === haystack.length && needle !== haystack) return -1

    let searchHash = 0 // 搜索字符串的hash值
    let startHash = 0 // 字符串起始的hash值

    for(let i = 0; i < needle.length; i++) {
        searchHash += needle.charCodeAt(i) * Math.pow(2, needle.length - i - 1)
        startHash += haystack.charCodeAt(i) * Math.pow(2, needle.length - i - 1)
    }

    if (startHash === searchHash)  return 0

    for(let j = 1; j < haystack.length - needle.length + 1; j++) {
        startHash = (startHash - haystack.charCodeAt(j - 1) * Math.pow(2, needle.length - 1)) * 2 + haystack.charCodeAt(j + needle.length - 1)
        if (startHash === searchHash) {
            return j
        }
    }
    return -1
};

结果:

  • 74/74 cases passed (68 ms)
  • Your runtime beats 81.31 % of javascript submissions
  • Your memory usage beats 16.86 % of javascript submissions (35.4 MB)
  • 时间复杂度 O(m + n)

注意:这里可能会存在溢出的情况,所以不是所有情况都适用。

Ⅲ.利用有限自动机进行字符串匹配

算法说明:

通过对文本字符串 T 进行扫描,找出模式 P 的所有出现位置。它们只对每个文本字符检查一次,并且检查每个文本字符时所用的时间为常数。一句话概括:字符输入引起状态机状态变更,通过状态转换图得到预期结果。

这里主要的核心点是判断每次输入,找到最长的后缀匹配,如果最长时的长度等于查找字符串长度,那就一定包含该查找字符串。

代码:

/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
var strStr = function(haystack, needle) {
    if (needle === '') return 0
    if (needle.length > haystack.length) return -1
    if (needle.length === haystack.length && needle !== haystack) return -1

    // 查找最大匹配后缀长度
    function findSuffix (Pq) {
        let suffixLen = 0
        let k = 0
        while(k < Pq.length && k < needle.length) {
            let i = 0;
            for(i = 0; i <= k; i++) {
                // 找needle中的多少项为当前状态对应字符串的匹配项
                if (Pq.charAt(Pq.length - 1 - k + i) !== needle.charAt(i)) {
                    break;
                }
            }

            // 所有项都匹配,即找到了后缀
            if (i - 1 == k) {
                suffixLen = k+1;
             }
            k++
        }
        return suffixLen
    }

    // 获取所有输入的字符集,比如 'abbc' 和 'cd' 合集为 ['a','b','c','d']
    const setArr = Array.from(new Set(haystack + needle)) // 用户输入的可选项

    // 建立状态机
    const hash = {}
    for(let q = 0; q < haystack.length; q++) {
        for(let k = 0; k < setArr.length; k++) {
            const char = haystack.substring(0, q) + setArr[k] // 下个状态的字符
            const nextState = findSuffix(char)
            // 求例如 0.a 0.b 0.c 的值
            if (!hash[q]) {
                hash[q] = {}
            }
            hash[q][char] = nextState
        }
    }

    // 根据状态机求解
    let matchStr = ''
    for(let n = 0; n < haystack.length; n++) {
        const map = hash[n]
        matchStr += haystack[n]
        const nextState = map[matchStr]

        if (nextState === needle.length) {
            return n - nextState + 1
        }
    }
    return -1
};

结果:

  • 74/74 cases passed (84 ms)
  • Your runtime beats 35.05 % of javascript submissions
  • Your memory usage beats 5.05 % of javascript submissions (39.8 MB)
  • 时间复杂度 O(n)

Ⅳ.KMP 算法

算法说明:

可以理解为在状态机的基础上,使用了一个前缀函数来进行状态判断。本质上也是前缀后缀的**。

代码:

// @lc code=start
/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
var strStr = function(haystack, needle) {
    if (needle === '') return 0
    if (needle.length > haystack.length) return -1
    if (needle.length === haystack.length && needle !== haystack) return -1

    // 生成匹配串各个位置下下的最长公共前后缀长度哈希表
    function getHash () {
        let i = 0 // arr[i] 表示 i 前面的字符串的最长公共前后缀长度
        let j = 1
        let hash = {
            0: 0
        }
        while (j < needle.length) {
            if (needle.charAt(i) === needle.charAt(j)) { // 相等直接 i j 都后移
                hash[j++] = ++i
            } else if (i === 0) {   // i 为起点且两者不相等,那么一定为0
                hash[j] = 0
                j++
            } else {
                // 这里解释一下: 因为i前面的字符串与j前面的字符串拥有相同的最长公共前后缀,也就是说i前面字符串的最长公共后缀与j前面字符串的最长公共前缀相同,所以i只需回到i前面字符串最长公共前缀的后一位开始比较
                i = hash[i - 1]
            }
        }
        return hash
    }

    const hash = getHash()
    let i = 0 // 母串中的位置
    let j = 0 // 子串中的位置
    while(i < haystack.length && j < needle.length) {
        if (haystack.charAt(i) === needle.charAt(j)) {  // 两个匹配,同时后移
            i++
            j++
        } else if (j === 0) { // 两个不匹配,并且j在起点,则母串后移
            i++
        } else {
            j = hash[j - 1]
        }
    }
    if (j === needle.length) {  // 循环完了,说明匹配到了
        return i - j
    } else {
        return -1
    }
};

结果:

  • 74/74 cases passed (60 ms)
  • Your runtime beats 94.74 % of javascript submissions
  • Your memory usage beats 23.73 % of javascript submissions (35.1 MB)
  • 时间复杂度 O(n)

Ⅴ.BM 算法

算法说明:

基于后缀匹配,匹配从后开始,但移动还是从前开始,只是定义了两个规则:坏字符规则和好后缀规则。

通俗来讲就是先验证是否为坏字符,然后判断是否在搜索词中进行对应的偏移进行下一步验证。如果匹配的话就从后往前校验,如果仍然匹配,就为好后缀。核心**是每次位移都在坏字符和好后缀规则中取较大值,由于两个规则都只与匹配项相关,所以可以提前生成规则表。

代码:

/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
var strStr = function(haystack, needle) {
    if (needle === '') return 0
    if (needle.length > haystack.length) return -1
    if (needle.length === haystack.length && needle !== haystack) return -1

    function makeBadChar (needle) {
        let hash = {}
        for(let i = 0; i < 256; i++) { // ascii 字符长度
            hash[String.fromCharCode(i)] = -1 // 初始化为-1
        }
        for(let i = 0; i < needle.length; i++) {
            hash[needle.charAt(i)] = i  // 最后出现该字符的位置
        }
        return hash
    }

    function makeGoodSuffix (needle) {
        let hashSuffix = {}
        let hashPrefix = {}
        for(let i = 0; i < needle.length; i++) {
            hashSuffix[i] = -1
            hashPrefix[i] = false
        }
        for(let i = 0; i < needle.length - 1; i++) { // needle[0, i]
            let j = i
            k = 0 // 公共后缀子串长度,尾部取k个出来进行比较
            while(j >= 0 && needle.charAt(j) === needle.charAt(needle.length - 1 - k)) { // needle[0,needle.length - 1]
                --j
                ++k
                hashSuffix[k] = j + 1 // 起始下标
            }

            if (j === -1) { // 说明全部匹配,意味着此时公共后缀子串也是模式的前缀子串
                hashPrefix[k] = true
            }
        }
        return { hashSuffix, hashPrefix }
    }

    function moveGoodSuffix (j, needle) {
        let k = needle.length - 1 - j
        let suffixes = makeGoodSuffix(needle).hashSuffix
        let prefixes = makeGoodSuffix(needle).hashPrefix
        if (suffixes[k] !== -1) { // 找到了跟好后缀一样的子串,获取下标
            return j - suffixes[k] + 1
        }
        for(let r = j + 2; r < needle.length; ++r) {
            if (prefixes[needle.length - r]) { // needle.length 是好后缀子串长度
                return r // 对齐前缀到好后缀
            }
        }
        return needle.length // 全部匹配,直接移动字符串长度
    }

    let badchar = makeBadChar(needle)
    let i = 0;
    while(i < haystack.length - needle.length + 1) {
        let j
        for(j = needle.length - 1; j >= 0; --j) {
            if (haystack.charAt(i + j) != needle[j]) {
                break; // 坏字符,下标为j
            }
        }
        if (j < 0) { // 匹配成功
            return i // 第一个匹配字符的位置
        }
        let moveLen1 = j - badchar[haystack.charAt(i + j)]
        let moveLen2 = 0
        if (j < needle.length -1) { // 如果有好后缀
            moveLen2 = moveGoodSuffix(j, needle)
        }
        i = i + Math.max(moveLen1, moveLen2)
    }

    return -1
};

结果:

  • 74/74 cases passed (72 ms)
  • Your runtime beats 69.29 % of javascript submissions
  • Your memory usage beats 5.05 % of javascript submissions (37 MB)
  • 时间复杂度 O(n)

Ⅵ.Horspool 算法

算法说明:

将主串中匹配窗口的最后一个字符跟模式串中的最后一个字符比较。如果相等,继续从后向前对主串和模式串进行比较,直到完全相等或者在某个字符处不匹配为止。如果不匹配,则根据主串匹配窗口中的最后一个字符在模式串中的下一个出现位置将窗口向右移动。

代码:

/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
var strStr = function(haystack, needle) {
    if (needle === '') return 0
    if (needle.length > haystack.length) return -1
    if (needle.length === haystack.length && needle !== haystack) return -1

    let hash = {}
    for(let i = 0; i < needle.length - 1; i++) {
        hash[needle.charAt(i)] = needle.length - 1 - i // 每个字符距离右侧的距离
    }

    let pos = 0

    while(pos < (haystack.length - needle.length + 1)) {
        let j = needle.length - 1 // 从右往左
        while(j >= 0 && haystack.charAt(pos + j) === needle.charAt(j)) {
            j--
        }
        if (j < 0) { // 全部匹配
            return pos
        } else { // 不匹配
            pos += hash[haystack.charAt(pos + needle.length - 1)] || needle.length // 其它字符移动整串
        }
    }

    return -1
};

结果:

  • 74/74 cases passed (64 ms)
  • Your runtime beats 84.29 % of javascript submissions
  • Your memory usage beats 22.41 % of javascript submissions (35.1 MB)
  • 时间复杂度 O(n)

Ⅶ.Sunday 算法

算法说明:

它的**跟 BM 算法 相似,但是它是从前往后匹配,匹配失败时关注主串内参与匹配的后一位字符。如果该字符不存在匹配字符中,则多偏移一位;如果存在,则偏移匹配串长度减该字符最右出现的位置。

代码:

/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
var strStr = function(haystack, needle) {
    if (needle === '') return 0
    if (needle.length > haystack.length) return -1
    if (needle.length === haystack.length && needle !== haystack) return -1

    let hash = {}
    for(let i = 0; i < needle.length; i++) {
        hash[needle.charAt(i)] = needle.length - i // 偏移表
    }

    for(let i = 0; i < haystack.length;) {
        let j
        for(j = 0; j < needle.length; j++) {
            if (haystack.charAt(i + j) !== needle.charAt(j)) {
                break
            }
        }
        if(j === needle.length) { // 完全匹配
            return i
        }
        if (i + needle.length >= haystack.length) { // 未找到
            return -1
        } else {
            i += hash[haystack.charAt(i + needle.length)] || needle.length + 1
        }
    }

    return -1
};

结果:

  • 74/74 cases passed (56 ms)
  • Your runtime beats 98.3 % of javascript submissions
  • Your memory usage beats 74.1 % of javascript submissions (33.6 MB)
  • 时间复杂度 O(n)

思考总结

就理解的难易度来讲,我建议先看 Sunday 算法Horspool 算法,不过 RMP 算法 的匹配思路打开了眼界,利用后缀前缀来处理问题。这里把常见的字符串算法都做了一次尝试,整体下来收获颇丰。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

换个角度聊效率

首发于微信公众号《前端成长记》,写于 2020.01.22

换个角度聊效率

PPT地址

内容源自作者上个月部门内部的分享,本文将围绕着以下四个角色来聊:

  • 交互设计
  • 视觉设计
  • 前端开发
  • 后端开发

为什么要聊这个

京东数科CEO陈生强在杭州乌镇互联网大会上说道:产业数字化核心本就是去解决企业的效率问题。

效率的基本保障

所谓“工欲善其事,必先利其器”。有了一些开发工具的辅助,我们能更高效地进行工作。

  • 交互设计:Axure 、Sketch 等
  • 视觉设计:PhotoShop 、Sketch 等
  • 前端开发:WebStorm 、VSCode 等
  • 后端开发:IDE 、Eclipse 等

如何提升效率

交互&前端

交互和前端合作起来有个最大的痛点,就是原型更新同步需要人力沟通,出错率高,所以我们可以通过一下两种方式来解决。

Axure Interactive Redline Tool

可以类比 Sketch 中的 Measure 插件。主要优点如下:

  • 在某些场景下,根据原型也能获取到尺寸信息,这样可以直接用于开发
  • 可以在线分享,在线更新,另外做变动通知

基于 Nginx 搭建局域网一体化文档平台

交互同学在本机搭建 Nginx 服务,配置好目录后,每次生成文件导出到该指定目录即可完成更新。前端同学可以通过 IP 完成局域网访问。这样都可以避免更新传递过程导致的问题。

视觉&前端

视觉和前端合作的时候,有时候会利用率不高,需要重复设计或者重复开发,所以为了解决这个问题,通常会采用下面的方式。

设计元素库 + 协同修改

在某种程度上统一设计规范,提供多套色系模版,以便快速生成对应的设计元素库,再配合开发 Sketch 插件,即可做到实时协同。本质上也是解决的是协同的效率和准确性。

元素 -> 组件 -> 系统模版

有了元素库以后,元素组合或者调整就可以发布成新的组件。组件组合加页面约束就可以生成系统,能够高效复用,快速完成相似度高的中后台系统的搭建与开发。

这里的页面约束指的是边距等一些基本设计约束定义。

GUI 工具

使用现有模版或者自行拖拽组合现有的组件,快速初始化对应项目UI及基本交互。这里可以参考阿里的飞冰。

Sketch 插件进行发布维护

通过 Sketch 插件进行组件的发布维护,将组件的维护权交给设计端,解决设计稿的还原度问题,解放前端花在 UI 上的时间。

交互&视觉&前端

这里我们有遇到这么一个痛点:交互的初稿过程是带有逻辑性的,如果给产品看原型的话可能不够直观,并且说服力不足。这里我们有一个解决方案如下:

原型 -> 页面

通过原型导出成 Markdown 文件,然后针对该文件做解析,然后拿到结构自动生成带导航内容的预览页面。

前端

前端也总结了几种方式来提高效率。

功能抽象,反馈交互和视觉

针对功能性需求,尽可能将其进行抽象,反馈给交互和视觉拓展组件元素,提高复用性。

Git Hooks + ESlint

类似设计,约定一套代码规范。在多人协作过程中,通过 BeforeCommit 钩子,自动进行代码质量检查,保障合作效率。

JSON + 组件 + 页面约束

通过 JSON 配置来建立组件的引用关系,加上页面阅读即可快速高效地生成一些偏固化的流程页。比如:实名认证、修改密码、风险评估等。

协议平台

以前的协议需要设计排版和前端制作,费时费力。通过将协议编译成 HTML,加上基本的设计约束和设计样式即可自动生成协议页面,大大提高效率,节省了时间。

前端&后端

前端和后端最大的一个吐槽点就是接口文档,格式参差不齐,交付方式千奇百怪。

接口文档平台

前后端的接口沟通往往是最费时且容易出错的。我们通过代码注释,生成可维护可预览的接口文档,在线对比测试,降低了出错率和沟通成本,同时也可以接入 Mock 进行更为完善的测试,节约测试资源。

网关平台

让后端只需要关心服务提供,前端只需要关心接口调用。中间的差异抹平交由网关层,同时也支持多接口调用,也能提高开发效率。

后端

后端由于只是略有涉猎,在这大胆做两个设想。

GraphQl + 可行的数据库设计

之前可能会出现需求微调,导致前后端都需要做字段更新等操作。引入 GraphQl 后,取什么数据由前端来决定。接口服务与数据库的链接可以参考 Restful 风格设计,或者其他可行的设计方式。

结合 GUI 工具快速完成简单项目

可以利用之前提到的 GUI 工具,通过拖拽实现自动布局,快速生成无复杂交互的项目,如一些表单项目:EBS、保单填写等。

做个总结

TODOS

我们首先要做的事:

  • 交互和视觉共同约定一套或多套不同场景下的设计语言
  • 前端根据设计语言由小到大鲫鱼场景进行组合封装
  • 基于设计语言,针对性地拓展效率工具

NEEDS

需要我们长期做的事:

  • 每个角色或岗位发现并收集工作中的痛点
  • 沟通讨论寻找提高效率的解决方案

SUMMARY

在业务相对趋于平稳的时期,提升各方面效率依然可以持续地创造价值。

最后,一句话共勉:有你有我,未来可期。

以上是分享的全部内容,感谢!

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

基于Vue实现加载进度条插件

首发于微信公众号《前端成长记》,写于 2018.02.26

概述

随着越来越多的网站采用顶部加载进度条来表示加载进度及状态,效果如下:

image

查看在线示例

使用

配置文档

可以单独在请求中使用,也可以使用在路由切换过程中。比如结合 vue-router 使用:

import loadingBar from  '../src/components/loading-bar/loading-bar'
router.beforeEach((to, from, next) => {
   if(to.path === "/" && to.name === "Home") {
      router.replace({path: '/summary'});
      next();
   } else {
      loadingBar.start();
      next();
   }
});
router.afterEach((to, from) => {
   loadingBar.finish();
});

源码

Github

实现方式

基于 Vue 的调用方式一般有两种:** 组件方式 ** 和 ** 插件方式 **。

推荐使用 插件方式 调用,当然如果只有一处使用的话,也可以使用组件方式。

  1. 首先编写一个单文件组件

该文件中含有进度条的样式结构以及一些动态入参,示例:

<template>
    <transition name="loading-fade">
        <div class="jrv-loading-bar" :style="barStyle" v-show="show">
            <div class="jrv-loading-bar-inner" :style="{'width': innerWidth, 'background-color': [status == 'success' && successBg, status == 'fail' && failBg,],}"
                 :class="{'jrv-loading-bar-inner_success': status == 'success', 'jrv-loading-bar-inner_fail': status == 'fail'}"></div>
        </div>
    </transition>
</template>
<script>
   export default {
      name: "jrvLoadingBar",
      data() {
         return {
            percent: 0,
            status: 'success',
            show: false,
            barBg: 'transparent',
            successBg: null,
            failBg: null,
         }
      },
      computed: {
         barStyle() {
            return {
               backgroundColor: `${this.barBg}`,
            }
         },
         innerWidth() {
            return this.percent + '%';
         },
      },
   }
</script>
<style type="text/scss" lang="scss">
    @import '../../../styles/components/_loading-bar.scss';
</style>

在上述代码中,可以发现入参主要有: percent(进度) status(状态) show(显示与否) barBg(背景色) successBg(成功背景色) failBg(失败背景色),所以使用起来,只需要修改这几个主要动态参数即可。

  1. 接下来是插件方式的调用

创建一个 loading-bar.js 文件,代码如下:

/**
 * @Author: Created By McChen
 * @Date: 2017/10/27
 * @Mail: [email protected]
 * @Version: V1.0.0
 */
 
import Vue from 'vue'
 
// 引入上面定义的单文件组件
import loadingBar from './src/loading-bar'
 
// 实例对象
let instance;
// 定时器
let timer;
 
let LoadingBar = {
   create () {
      if(!instance) {
         // 组件构造器
         const LoadingBarConstructor = Vue.extend(loadingBar);
         instance = new LoadingBarConstructor({});
         // 挂载实例
         instance.$mount();
         document.body.appendChild(instance.$el);
      }
   },
   update (options) {
      // 修改动态入参值
      for(let key in options) {
         instance[key] = options[key];
      }
   },
   hide () {
      let timer1 = setTimeout(() => {
         this.update({
            show: false
         });
         clearTimeout(timer1);
 
         let timer2 = setTimeout(() => {
            this.update({
               percent: 0
            });
            clearTimeout(timer2)
         }, 200);
      }, 800);
   },
   destroy () {
      document.body.removeChild(document.getElementsByClassName('jrv-loading-bar')[0]);
   },
   clearTimer () {
      if(timer) {
         clearInterval(timer);
         timer = null;
      }
   }
};
// 默认先创建一个
LoadingBar.create();
 
export default {
   config (obj) {
      LoadingBar.update({
         barBg: obj.barBg,
         successBg: obj.successBg,
         failBg: obj.failBg,
      })
   },
   start () {
      LoadingBar.create();
 
      if(timer) {
         return false;
      } else {
         let percent = 0;
 
         LoadingBar.update({
            percent: percent,
            status: 'success',
            show: true
         });
 
         timer = setInterval(() => {
            percent += Math.floor(Math.random () * 3 + 5);
            if (percent > 90) {
               clearInterval(timer);
               timer = null;
            }
            LoadingBar.update({
               percent: percent,
               status: 'success',
               show: true
            });
         }, 200);
      }
   },
   update (percent) {
      LoadingBar.clearTimer();
      LoadingBar.update({
         percent: percent,
         status: 'success',
         show: true
      });
   },
   finish () {
      if(timer) {
         LoadingBar.clearTimer();
         LoadingBar.update({
            percent: 100,
            status: 'success',
         });
         LoadingBar.hide();
      } else {
         LoadingBar.hide();
      }
   },
   error () {
      LoadingBar.clearTimer();
      LoadingBar.update({
         percent: 100,
         status: 'fail',
      });
      LoadingBar.hide();
   },
   destroy () {
      LoadingBar.clearTimer();
      instance = null;
      LoadingBar.destroy();
   },
}

结尾

其实不单单是 props 属性,你可以使用继承过来的 vue 对象的任意东西,怎么使用全凭各位的创造力啦~

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

jQuery插件编写学习中遇见的问题--attr&prop

首发于微信公众号《前端成长记》,写于 2015.09.01

介绍

最近在学习JQuery的插件的编写,有两种方式:

  • .fn.extend 作用于对象原型上
  • .extend 作用于JQuery对象上

我接下来编写的是表格奇偶数不同色,checkbox选择则高亮显示,使用$.fn.extend

在线效果

Edit jquery-plugin

代码

<table id="table1">
    <thead>
        <tr>
            <th></th>
            <th>姓名</th>
            <th>性别</th>
            <th>所在地</th>
        </tr>
    </thead>
    <tbody>
    <tr>
        <td><input type="checkbox"/></td>
        <td>张三</td>
        <td></td>
        <td>浙江杭州</td>
    </tr>
    <tr>
        <td><input type="checkbox"/></td>
        <td>李四</td>
        <td></td>
        <td>湖南长沙</td>
    </tr>
    <tr>
        <td><input type="checkbox"/></td>
        <td>王二</td>
        <td></td>
        <td>浙江温州</td>
    </tr>
    <tr>
        <td><input type="checkbox"/></td>
        <td>McChen</td>
        <td></td>
        <td>北京</td>
    </tr>
    <tr>
        <td><input type="checkbox"/></td>
        <td>NVSHEN</td>
        <td></td>
        <td>浙江杭州</td>
    </tr>
    </tbody>
</table>

html无非就是一个简单的表格,接下来随意设置样式

.even { background:#FFF38F;} .odd { background: #FFFFEE;} .selected { background: #ff0000;}

简单说一下 even是偶数行样式,odd是奇数行样式,selected是选中的样式即高亮。

插件编写

接下来是重点,遇见的问题也在这。先附上使用attr时的代码。

;(function ($) {
$.fn.extend({
    "tableColor": function (options) {
        return this.each(function () {
            var aaa = $.extend({
                  odd : "odd",
                   even : "even",
                  selected : "selected"
               },options);
                $("tbody>tr:odd",this).addClass(aaa.odd);
                $("tbody>tr:even",this).addClass(aaa.even);
                $("tbody>tr",this).on("click",function () {
                    var hasSelected = $(this).hasClass(aaa.selected);  //判断是否选中
                 //判断是否选中然后增加或移除class
                    $(this)[hasSelected?"removeClass":"addClass"](aaa.selected)
                       .find(":checkbox").attr("checked",!hasSelected);
               });
               $("tbody>tr:has(:checked)",this).addClass(aaa.selected);
               return this;
            })
        }
    })
})(jQuery);
//------------------------以上是插件编写的代码

接下来是插件的使用方法

//------------------------开始测试插件的应用
$(function () {
    $("#table1").tableColor()
        .find("th").css("color","red");
})

个人看来jq插件需要具备最基本的两点:

  • 可以自定义参数
  • 返回的是对象可以进行链式操作

当我们使用attr设置checked属性时,发现只有初次生效,解决的办法是使用prop。

浅析原因

官方对于prop()的解释是获取在匹配的元素集中的第一个元素的属性值,它的返回值不同于attr,为true或false。(attr返回checked或””),因此总结一下适用范围:

  • 添加属性名称该属性就会生效应该使用prop();
  • 是有true,false两个属性使用prop();
  • 其他则使用attr();

但是,官方推荐使用的是attr,这我有点不知甚解。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

【Leetcode 做题学算法周刊】第八期

首发于微信公众号《前端成长记》,写于 2020.05.07

背景

本文记录刷题过程中的整个思考过程,以供参考。主要内容涵盖:

  • 题目分析设想
  • 编写代码验证
  • 查阅他人解法
  • 思考总结

目录

Easy

155.最小栈

题目地址

题目描述

设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。

  • push(x) -- 将元素 x 推入栈中。
  • pop() -- 删除栈顶的元素。
  • top() -- 获取栈顶元素。
  • getMin() -- 检索栈中的最小元素。

示例:

MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin();   --> 返回 -3.
minStack.pop();
minStack.top();      --> 返回 0.
minStack.getMin();   --> 返回 -2.

题目分析设想

这道题感觉就是构造函数,仅此而已,给函数的原型加一些方法。差异就在方法实现本身上,这个就写一个个人的思路吧,内部维护每次操作后的最小值数组。当然用差值数组和最小值也是可以的。

编写代码验证

Ⅰ.内部维护最小值数组

代码:

/**
 * initialize your data structure here.
 */
var MinStack = function() {
    // 栈内数据用数组存储
    this.stacks = []
    this.mins = []
};

/**
 * @param {number} x
 * @return {void}
 */
MinStack.prototype.push = function(x) {
    this.stacks.push(x)
    // 比当前最小值小的数都需要存住
    if (!this.mins.length || this.mins[this.mins.length - 1] >= x) {
        this.mins.push(x)
    }
};

/**
 * @return {void}
 */
MinStack.prototype.pop = function() {
    // 如果推出的是最小值,那最小值数组也同步更新即可
    if (this.stacks.pop() === this.mins[this.mins.length - 1]) {
        this.mins.pop()
    }
};

/**
 * @return {number}
 */
MinStack.prototype.top = function() {
    return this.stacks.length ? this.stacks[this.stacks.length - 1] : undefined
};

/**
 * @return {number}
 */
MinStack.prototype.getMin = function() {
    return this.mins[this.mins.length - 1]
};

结果:

  • 18/18 cases passed (116 ms)
  • Your runtime beats 89.63 % of javascript submissions
  • Your memory usage beats 7.49 % of javascript submissions (44.9 MB)
  • 时间复杂度: O(1)

Ⅱ.差值数组和最小值

代码:

/**
 * initialize your data structure here.
 */
var MinStack = function() {
    // 栈内数据用数组存储
    this.stacks = []
    this.min = Infinity
};

/**
 * @param {number} x
 * @return {void}
 */
MinStack.prototype.push = function(x) {
    if (!this.stacks.length) {
        this.min = x
        // x - this.min = 0
        this.stacks.push(0)
    } else {
        // 先存差值再更新最小值
        this.stacks.push(x - this.min)
        if (x < this.min) this.min = x
    }
};

/**
 * @return {void}
 */
MinStack.prototype.pop = function() {
    // 当前值比最小值小,则需要
    let val = this.stacks.pop()
    if (val < 0) {
        this.min -= val
    }
};

/**
 * @return {number}
 */
MinStack.prototype.top = function() {
    if (this.stacks[this.stacks.length - 1] < 0) {
        // 当前最小值就是推出项的值
        return this.min
    } else {
        return this.stacks[this.stacks.length - 1] + this.min
    }
};

/**
 * @return {number}
 */
MinStack.prototype.getMin = function() {
    return this.min
};

结果:

  • 18/18 cases passed (124 ms)
  • Your runtime beats 71.47 % of javascript submissions
  • Your memory usage beats 16.67 % of javascript submissions (44.5 MB)
  • 时间复杂度: O(1)

查阅他人解法

这里看到有用栈的,JS 里面我就不构建了。另外还看见一种把值和最小值混合存储的思路,挺有意思的。

Ⅰ.混合存储

代码:

/**
 * initialize your data structure here.
 */
var MinStack = function() {
    // 栈内数据用数组存储
    this.stacks = []
    this.min = Infinity
};

/**
 * @param {number} x
 * @return {void}
 */
MinStack.prototype.push = function(x) {
    // 先推上一步的最小值,再推原数据
    if (x <= this.min) {
        this.stacks.push(this.min)
        this.min = x
    }
    this.stacks.push(x)
};

/**
 * @return {void}
 */
MinStack.prototype.pop = function() {
    // 如果推出值等于当前最小值,则将最小值更新为上一个最小值
    if (this.stacks.pop() === this.min) {
        this.min = this.stacks.pop()
    }
};

/**
 * @return {number}
 */
MinStack.prototype.top = function() {
    return this.stacks[this.stacks.length - 1]
};

/**
 * @return {number}
 */
MinStack.prototype.getMin = function() {
    return this.min
};

结果:

  • 18/18 cases passed (124 ms)
  • Your runtime beats 71.47 % of javascript submissions
  • Your memory usage beats 7.15 % of javascript submissions (45 MB)
  • 时间复杂度: O(1)

思考总结

我个人是不推荐混合存储的,因为如果需要拓展查最大值,那整体逻辑实际上都需要调整。而前面两种方法,都只需要增加最大值数组或者最大值即可。

160.相交链表

题目地址

题目描述

编写一个程序,找到两个单链表相交的起始节点。

如下面的两个链表:

相交链表

在节点 c1 开始相交。

注意:

  • 如果两个链表没有交点,返回 null.
  • 在返回结果后,两个链表仍须保持原有的结构。
  • 可假定整个链表结构中没有循环。
  • 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。

题目分析设想

这道题如果不加限制的话,方法还是很多的,比如说打标识,或者哈表表都可以解决问题。如果要满足内存要求的话,必然就不能使用哈希表。要满足保持原有结构,那就不能用打标识的方法了。这里有个取巧的方案,因为不存在循环,两者相加的总长度相等,所以如果存在交点,那结尾必然相等。

编写代码验证

Ⅰ.打标识

代码:

/**
 * @param {ListNode} headA
 * @param {ListNode} headB
 * @return {ListNode}
 */
var getIntersectionNode = function(headA, headB) {
    // 因为是内存引用指向,取巧了
    while(headA) {
        headA.FLAG = true
        headA = headA.next
    }
    while(headB) {
        if (headB.FLAG) return headB
        headB = headB.next
    }
    return null
};

结果:

  • 45/45 cases passed (104 ms)
  • Your runtime beats 50.31 % of javascript submissions
  • Your memory usage beats 5.01 % of javascript submissions (45.3 MB)
  • 时间复杂度: O(m + n)

Ⅱ.哈希表法

代码:

/**
 * @param {ListNode} headA
 * @param {ListNode} headB
 * @return {ListNode}
 */
var getIntersectionNode = function(headA, headB) {
    let hash = new Set()
    while(headA) {
        hash.add(headA)
        headA = headA.next
    }
    while(headB) {
        if (hash.has(headB)) return headB
        headB = headB.next
    }
    return null
};

结果:

  • 45/45 cases passed (100 ms)
  • Your runtime beats 67.12 % of javascript submissions
  • Your memory usage beats 68.1 % of javascript submissions (43.8 MB)
  • 时间复杂度: O(m + n)

Ⅲ.双指针法

代码:

/**
 * @param {ListNode} headA
 * @param {ListNode} headB
 * @return {ListNode}
 */
var getIntersectionNode = function(headA, headB) {
    // 用两个指针同时右移,A完了之后指向B继续右移
    let pA = headA
    let pB = headB
    while(pA !== pB) {
        pA = pA ? pA.next : headB
        pB = pB ? pB.next : headA
    }
    return pA
};

结果:

  • 45/45 cases passed (100 ms)
  • Your runtime beats 67.12 % of javascript submissions
  • Your memory usage beats 72.7 % of javascript submissions (43.3 MB)
  • 时间复杂度: O(m + n)

查阅他人解法

这里一般的二重循环就不说了,跟两个数组中找同样的数一样,没有什么区别,效率也比较底下。另外看见一个有意思的思路是,把两个链表拼成环,转化成找环节点的问题。

Ⅰ.转换环

代码:

/**
 * @param {ListNode} headA
 * @param {ListNode} headB
 * @return {ListNode}
 */
var getIntersectionNode = function(headA, headB) {
    if (headA == null || headB == null)  return null;
    let curA = headA,curB = headB;
    while(curA.next != null){
        curA = curA.next;
    }
    curA.next = headA;
    let fast = headB,slow = headB;
    while(fast != null&&fast.next != null){
          slow = slow.next;
          fast = fast.next.next;
          if(slow == fast){
             slow = headB;
             while(slow != fast){
                 slow = slow.next;
                 fast = fast.next;
            }
            curA.next = null;
            return fast;
          }
    }
    curA.next = null;
    return null;
};

结果:

  • 45/45 cases passed (104 ms)
  • Your runtime beats 50.31 % of javascript submissions
  • Your memory usage beats 12.39 % of javascript submissions (44.6 MB)
  • 时间复杂度: O(m + n)

思考总结

这里要满足空间复杂度要求,那就不能用哈希表法;要满足不改变原始数据结构,那就不能用标识法。所以这种情况下我更建议使用双指针法来直接作答,而转换为环虽然是一个思路,但是其实由一个问题转换为另一个问题,心智负担没有降低。

167.两数之和II输入有序数组

题目地址

题目描述

给定一个已按照 升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。

函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。

说明:

  • 返回的下标值(index1 和 index2)不是从零开始的。
  • 你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。

示例:

输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2  7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 

题目分析设想

这道题有几个地方做了说明了,需要注意一下:有序的升序数组、下标不从零开始、不能重复使用相同元素。之前第一期的两数之和的方法就不做分析了(两次遍历和哈希表),这里可以利用有序数组来提高效率,利用首尾指针,来做判断,可以理解为夹逼原则。

编写代码验证

Ⅰ.双指针夹逼

代码:

/**
 * @param {number[]} numbers
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(numbers, target) {
    let start = 0
    let end = numbers.length - 1
    while(start < end) {
        const sum = numbers[start] + numbers[end]
        if (sum === target) {
            // 不从零开始,所以需要加一
            return [start + 1, end + 1]
        } else if (sum > target) {
            end--
        } else {
            start++
        }
    }
    // 由于有唯一输出,所以必然有结果,也不需要做特殊处理
    return [-1, -1]
};

结果:

  • 17/17 cases passed (64 ms)
  • Your runtime beats 87.01 % of javascript submissions
  • Your memory usage beats 40.91 % of javascript submissions (35.2 MB)
  • 时间复杂度: O(n)

查阅他人解法

看了一下解法,没有效率更高的方式了。一些基本的解法在第一期的第一题可以查阅。另外有一个是以一个数为基准,二分查找,但是这样的话其实只是在两次循环基础上做了优化,甚至还不如哈希表效率高。所以,这里就没有看见其他有不同思路的解法了。

思考总结

既然数组有序,我们就可以用夹逼原则来求解,在这道题我觉得是最合适,也是最高效的解法了。

168.Excel表列名称

题目地址

题目描述

给定一个正整数,返回它在 Excel 表中相对应的列名称。

例如,

    1 -> A
    2 -> B
    3 -> C
    ...
    26 -> Z
    27 -> AA
    28 -> AB
    ...

示例:

输入: 1
输出: "A"

输入: 28
输出: "AB"

输入: 701
输出: "ZY"

题目分析设想

这道题其实很清晰,就是将10进制转成26进制的问题。直接用取余运算就行,唯一的难点在于如何转成26进制。

编写代码验证

Ⅰ.取余

代码:

/**
 * @param {number} n
 * @return {string}
 */
var convertToTitle = function(n) {
    let str = ''
    while( n > 0) {
        // 因为A代表1,减一就能从0开始匹配进制转换
        n--
        str += String.fromCharCode(n % 26 + 'A'.charCodeAt())
        n = Math.floor(n / 26)
    }
    return str.split('').reverse().join('')
};

结果:

  • 18/18 cases passed (64 ms)
  • Your runtime beats 57.06 % of javascript submissions
  • Your memory usage beats 100 % of javascript submissions (33.7 MB)
  • 时间复杂度: O(n)

查阅他人解法

看了一下解法,思路没有什么区别,有一个有意思的就是强行转换成一行,这里也列一下。

Ⅰ.单行代码

代码:

/**
 * @param {number} n
 * @return {string}
 */
var convertToTitle = function(n, s = '') {
    return !n-- ? s : convertToTitle(~~(n / 26), String.fromCharCode('A'.charCodeAt() + n % 26) + s);
};

结果:

  • 18/18 cases passed (60 ms)
  • Your runtime beats 76.47 % of javascript submissions
  • Your memory usage beats 100 % of javascript submissions (33.7 MB)
  • 时间复杂度: O(n)

思考总结

这道题本质上就是一道进制转换的题目,没有什么太大的难度。

169.求众数

题目地址

题目描述

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例:

输入: [3,2,3]
输出: 3

输入: [2,2,1,1,1,2,2]
输出: 2

题目分析设想

这道题就是找出出现次数最多的元素,按照常规的思路的话有这么两种方式。

  • 哈希法,通过哈希表记录每个数出现的次数,找到出现次数最多的数
  • 分治法,将数组二分,分别求两边的多数元素,然后找出出现次数多的元素

由于这里是找多数元素,有个前提是保证出现次数大于一半,这里也可以取巧,先排序,排序后中间那个数一定是多数元素。

编写代码验证

Ⅰ.哈希法

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var majorityElement = function(nums) {
    let hash = {}
    for(let i = 0; i < nums.length; i++) {
        if (hash[nums[i]] === undefined) {
            hash[nums[i]] = 1
        } else {
            hash[nums[i]] += 1
            // 大于等于一半就可以直接认定为众数了
            if (hash[nums[i]] >= nums.length / 2) {
                return nums[i]
            }
        }
    }
    let count = 0
    let val = null
    for(let key in hash) {
        if (hash[key] > count) {
            count = hash[key]
            val = key
        }
    }
    return val
};

结果:

  • 46/46 cases passed (88 ms)
  • Your runtime beats 41.1 % of javascript submissions
  • Your memory usage beats 92.86 % of javascript submissions (37.6 MB)
  • 时间复杂度: O(n)

Ⅱ.分治法

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var majorityElement = function(nums) {
    function countEle(arr, num, l, h) {
        let count = 0
        for(let i = l; i <= h; i++) {
            if (arr[i] === num) {
                count += 1
            }
        }
        return count
    }
    function getEle(arr, l, h) {
        debugger
        if (l === h) { // 就一个元素,直接返回
            return arr[l]
        }
        // 取中位数,减法主要为了防止溢出
        let mid = parseInt((h - l) / 2 + l)
        // 左边的众数
        let left = getEle(arr, l, mid)
        // 右边的众数
        let right = getEle(arr, mid + 1, h)
        // 如果左右两边数组为同一个众数,则直接返回
        if (left === right) {
            return left
        }
        // 比较众数出现次数,注意:要用父数组取出现次数做比较
        // 要不像这种将返回错误结果:[4,5,4,4,4,5]
        let leftCount = countEle(arr, left, l, h)
        let rightCount = countEle(arr, right, l, h)
        return leftCount > rightCount ? left : right
    }
    return getEle(nums, 0, nums.length - 1)
};

结果:

  • 46/46 cases passed (148 ms)
  • Your runtime beats 5.98 % of javascript submissions
  • Your memory usage beats 28.57 % of javascript submissions (38.2 MB)
  • 时间复杂度: O(nlog(n))

Ⅲ.排序法

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var majorityElement = function(nums) {
    return nums.sort()[nums.length >>> 1]
};

结果:

  • 46/46 cases passed (100 ms)
  • Your runtime beats 24.58 % of javascript submissions
  • Your memory usage beats 92.86 % of javascript submissions (37.3 MB)
  • 时间复杂度: O(nlog(n)),完全是数组排序的复杂度

查阅他人解法

发现了一种叫做 Boyer Moore 投票算法。这个算法通俗点解释起来还是很容易理解的,其实可以理解为互相抵消。每一个众数和一个其他数抵消,剩下的必然就是众数了。

Ⅰ.投票算法

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
/**
 * @param {number[]} nums
 * @return {number}
 */
var majorityElement = function(nums) {
    let count = 0
    let candidate = null
    for(let i = 0; i < nums.length; i++) {
        if (count === 0) {
            candidate = nums[i]
        }
        // 同样的数累加,不同的数相减,可以理解为同数量抵消,抵消完产生新的备选数
        count += (nums[i] === candidate) ? 1 : -1
    }
    return candidate
};

结果:

  • 46/46 cases passed (80 ms)
  • Your runtime beats 56.39 % of javascript submissions
  • Your memory usage beats 92.86 % of javascript submissions (37.3 MB)
  • 时间复杂度: O(n)

思考总结

比较显而易见的是,这道题使用投票算法只需遍历一次,也不需要额外的空间,为最优解。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

【Leetcode 做题学算法周刊】第一期

首发于微信公众号《前端成长记》,写于 2019.10.28

背景

本文记录刷题过程中的整个思考过程,以供参考。主要内容涵盖:

  • 题目分析设想
  • 编写代码验证
  • 查阅他人解法
  • 思考总结

目录

Easy

1.两数之和

题目地址

题目描述

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。

示例:

给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

题目分析设想

这道题首先说明了每种输入只会对应一个答案,并且不能利用数组中同样的元素,也就意味着一个数不能被使用两次,即 [0,0] 这种是不合理的。

看到这个题目,我有几个方向去尝试作答:

  • 暴力点,直接循环两次即可,预估性能最差
  • IndexOf ,循环次数最多,非常不推荐
  • 空间换时间,使用 HashMap ,减少一次循环

编写代码验证

Ⅰ.暴力法

代码:

// 暴力点
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    for(let i = 0; i < nums.length; i++) {
        // j 从 i+1 开始,去除一些无用运算
        for(let j = i + 1; j < nums.length; j++) {
            if (nums[i] + nums[j] === target) {
                return [i,j];
            }
        }
    }
};

结果:

  • 29/29 cases passed (124 ms)
  • Your runtime beats 60.13 % of javascript submissions
  • Your memory usage beats 66.05 % of javascript submissions (34.5 MB)
  • 时间复杂度:O(n^2)

Ⅱ.IndexOf

性能最差,每次判断都需要遍历剩余数组(极度不推荐,只是多展示一个实现方案)

代码:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    for(let i = 0; i < nums.length; i++) {
        const num = nums[i]
        const dif = target - num
        const remainArr = nums.slice(i + 1)
        if (remainArr.indexOf(dif) !== -1) {
            return [i, remainArr.indexOf(dif) + i + 1]
        }
    }
};

结果:

  • 29/29 cases passed (212 ms)
  • Your runtime beats 22.39 % of javascript submissions
  • Your memory usage beats 5 % of javascript submissions (49 MB)
  • 时间复杂度: O(n^2) ,阶乘的时间复杂度为 O(n)

Ⅲ.HashMap

代码:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    let hash = {}
    for(let i = 0; i < nums.length; i++) {
        const num = nums[i]
        const dif = target - num
        if (hash[dif] !== undefined) {
            return [hash[dif], i]
        } else {
            hash[num] = i
        }
    }
};

结果:

  • 29/29 cases passed (60 ms)
  • Your runtime beats 98.7 % of javascript submissions
  • Your memory usage beats 19.05 % of javascript submissions (35.3 MB)
  • 时间复杂度: O(n)

对比发现,HashMap 方案较暴力法在速度上有明显的提升。

查阅他人解法

这里看到还有两种方式,我们一一来尝试一下。

Ⅰ.使用数组替换 HashMap

代码:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    let arr = []
    for(let i = 0; i < nums.length; i++) {
        const num = nums[i]
        const dif = target - num
        if (arr[dif] !== undefined) {
            return [arr[dif], i]
        } else {
            arr[num] = i
        }
    }
};

结果:

  • 29/29 cases passed (60 ms)
  • Your runtime beats 98.7 % of javascript submissions
  • Your memory usage beats 17.89 % of javascript submissions (35.4 MB)
  • 时间复杂度: O(n)

跟使用 HashMap 性能差异不大。

Ⅱ.两次遍历 HashMap

代码:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    let res = new Map()
    for(let i = 0; i < nums.length; i++) {
        res.set(nums[i], i)
    }
    for(let i = 0; i < nums.length; i++) {
        const num = nums[i]
        const dif = target - num
        const idx = res.get(dif)
        if (idx !== undefined && idx !== i) {
            return [i, idx]
        }
    }
};

结果:

  • 29/29 cases passed (64 ms)
  • Your runtime beats 96.76 % of javascript submissions
  • Your memory usage beats 10.94 % of javascript submissions (35.9 MB)
  • 时间复杂度: O(n)

思考总结

这里我做个了简单的校验:输入 [2,2,2], 4 ,发现期望输出是 [0, 2] ,而不是 [0, 1] ,所以上面有几种解法实际上都过不了。如果是为了满足这种输出,我的推荐方案是 两次遍历 HashMap 。但是我个人是觉得 HashMap 一次遍历是更合理的。

7.整数反转

题目地址

题目描述

给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。

示例:

输入: 123
输出: 321

输入: -123
输出: -321

输入: 120
输出: 21

注意:

假设我们的环境只能存储得下 32 位的有符号整数,则其数值范围为 [−2^31, 2^31 − 1]。请根据这个假设,如果反转后整数溢出那么就返回 0。

题目分析设想

从题干上来看,有几个要注意的点:

  • 溢出返回 0
  • 0 为首位需要去掉取自然数

这里我有两种思路:

  • 利用数组反转 reverse 来反转再做自然数转换
  • 取余拿到每位上的数字再做加法和符号及溢出处理

编写代码验证

Ⅰ.数组反转

代码:

/**
 * @param {number} x
 * @return {number}
 */
var reverse = function(x) {
    const isNegative = x < 0
    const rev = Number(Math.abs(x).toString().split('').reverse().join(''))
    if (isNegative && -rev >= -Math.pow(2, 31)) {
        return -rev
    } else if (!isNegative && rev <= Math.pow(2,31) - 1) {
        return rev
    } else {
        return 0
    }
};

结果:

  • 1032/1032 cases passed (96 ms)
  • Your runtime beats 73.33 % of javascript submissions
  • Your memory usage beats 28.03 % of javascript submissions (35.9 MB)
  • 时间复杂度: O(1)

Ⅱ.取余

代码:

/**
 * @param {number} x
 * @return {number}
 */
var reverse = function(x) {
    const isNegative = x < 0
    let res = 0
    while(x !== 0) {
        res = res * 10 + x % 10
        x = parseInt(x / 10)
    }
    if ((isNegative && res >= -Math.pow(2, 31)) || (!isNegative && res <= Math.pow(2,31) - 1)) {
        return res
    } else {
        return 0
    }
};

结果:

  • 1032/1032 cases passed (80 ms)
  • Your runtime beats 96.71 % of javascript submissions
  • Your memory usage beats 56.8 % of javascript submissions (35.7 MB)
  • 时间复杂度: O(log10(n))

对比发现,使用取余的方式,性能上明显优于数组反转。

查阅他人解法

思路基本上都是这两种,未发现方向不同的解法。

思考总结

对比发现还有一些考虑不周的地方需要补全,比如说一些特殊值可直接返回,避免运算。这里我也做了一个简单的校验:输入 -0,发现期望输出是 0 而不是 -0。所以,我这里的代码做一些优化,如下:

/**
 * @param {number} x
 * @return {number}
 */
var reverse = function(x) {
    if (x === 0) return 0
    function isOverflow (num) {
        return num < -Math.pow(2, 31) || (num > Math.pow(2,31) - 1)
    }
    if (isOverflow(x)) return 0
    let res = 0
    while(x !== 0) {
        res = res * 10 + x % 10
        x = parseInt(x / 10)
    }
    return isOverflow(res) ? 0 : res
};

9.回文数

题目地址

题目描述

判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

示例:

输入: 121
输出: true

输入: -121
输出: false
解释: 从左向右读,  -121  从右向左读,  121- 。因此它不是一个回文数。

输入: 10
输出: false
解释: 从右向左读,  01 。因此它不是一个回文数。

进阶:

你能不将整数转为字符串来解决这个问题吗?

题目分析设想

这道题的第一感觉有点类似上一题整数反转的拓展,所以我们从两个方向入手:

  • 整数转字符串
  • 取余,前后逐位判断

在写的过程中需要考虑到去掉一些运算:把 <0-0 排除,因为负数和 -0 一定不为回文数;一位正整数一定是回文数;除了 0 以外,尾数为 0 的不是回文数。

编写代码验证

Ⅰ.转字符串

代码:

/**
 * @param {number} x
 * @return {boolean}
 */
var isPalindrome = function(x) {
    if (x < 0 || Object.is(x, -0) || (x % 10 === 0 && x !== 0)) return false;
    if (x < 10) return true;
    const rev = parseInt(x.toString().split('').reverse().join(''))
    return rev === x
};

结果:

  • 11509/11509 cases passed (252 ms)
  • Your runtime beats 79.41 % of javascript submissions
  • Your memory usage beats 52 % of javascript submissions (45.7 MB)
  • 时间复杂度: O(1)

这里有用到 ES6Object.is 来判断是否为 -0 ,当然 ES5 你也可以这么判断:

function (x) {
    return x === 0 && 1 / x < 0;    // -Infinity
}

可能有人会问不需要考虑数字溢出问题吗?

输入的数字不溢出,如果是回文数的话,那么输出的数字一定不溢出;如果不是回文数,不管溢出与否,都是返回 false

Ⅱ.取余

代码:

/**
 * @param {number} x
 * @return {boolean}
 */
var isPalindrome = function(x) {
    if (x < 0 || Object.is(x, -0) || (x % 10 === 0 && x !== 0)) return false;
    if (x < 10) return true;
    let div = 1
    while (x / div >= 10) { // 用来找出位数,比如121,那么就找到100,得到整数位
        div *= 10
    }
    while(x > 0) {
        let left = parseInt(x / div); // 左侧数起
        let right = x % 10; // 右侧数起
        if (left !== right) return false;

        x = parseInt((x % div) / 10);   // 去掉左右各一位数

        div /= 100; // 除数去两位
    }
    return true;
};

结果:

  • 11509/11509 cases passed (232 ms)
  • Your runtime beats 86.88 % of javascript submissions
  • Your memory usage beats 67.99 % of javascript submissions (45.5 MB)
  • 时间复杂度: O(log10(n))

查阅他人解法

这里看到一个更为巧妙的方式,只需要翻转一半即可。比如说 1221 ,只需要翻转后两位 21 即可。

Ⅰ.翻转一半

代码:

/**
 * @param {number} x
 * @return {boolean}
 */
var isPalindrome = function(x) {
    if (x < 0 || Object.is(x, -0) || (x % 10 === 0 && x !== 0)) return false;
    if (x < 10) return true;
    let rev = 0;    // 翻转的数字

    while(x > rev) {
        rev = rev * 10 + x % 10
        x = parseInt(x / 10)
    }

    return x === rev || x === parseInt(rev / 10);   // 奇数的话需要去掉中间数做比较
};

结果:

  • 11509/11509 cases passed (188 ms)
  • Your runtime beats 99.62 % of javascript submissions
  • Your memory usage beats 92.69 % of javascript submissions (44.8 MB)
  • 时间复杂度: O(log10(n))

思考总结

综上,最推荐翻转一半的解法。

13.罗马数字转整数

题目地址

题目描述

罗马数字包含以下七种字符: I, V, X, L,C,DM

字符 数值
I 1
V 5
X 10
L 50
C 100
D 500
M 1000

例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

  • I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
  • X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
  • C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。

给定一个罗马数字,将其转换成整数。输入确保在 1 到 3999 的范围内。

示例:

输入: "III"
输出: 3

输入: "IV"
输出: 4

输入: "IX"
输出: 9

输入: "LVIII"
输出: 58
解释: L = 50, V= 5, III = 3.

输入: "MCMXCIV"
输出: 1994
解释: M = 1000, CM = 900, XC = 90, IV = 4.

题目分析设想

这道题有个比较直观的想法,因为特殊情况有限可枚举,所以我这里有两个方向:

  • 枚举所有特殊组合,然后进行字符串遍历
  • 直接字符串遍历,判断当前位和后一位的大小

编写代码验证

Ⅰ.枚举特殊组合

代码:

/**
 * @param {string} s
 * @return {number}
 */
var romanToInt = function(s) {
    const hash = {
        'I': 1,
        'IV': 4,
        'V': 5,
        'IX': 9,
        'X': 10,
        'XL': 40,
        'L': 50,
        'XC': 90,
        'C': 100,
        'CD': 400,
        'D': 500,
        'CM': 900,
        'M': 1000
    }
    let res = 0
    for(let i = 0; i < s.length;) {
        if (i < s.length - 1 && hash[s.substring(i, i + 2)]) { // 在 hash 表中,说明是特殊组合
            res += hash[s.substring(i, i + 2)]
            i += 2
        } else {
            res += hash[s.charAt(i)]
            i += 1
        }
    }
    return res
};

结果:

  • 3999/3999 cases passed (176 ms)
  • Your runtime beats 77.06 % of javascript submissions
  • Your memory usage beats 80.86 % of javascript submissions (39.8 MB)
  • 时间复杂度: O(n)

Ⅱ.直接遍历

代码:

/**
 * @param {string} s
 * @return {number}
 */
var romanToInt = function(s) {
    const hash = {
        'I': 1,
        'V': 5,
        'X': 10,
        'L': 50,
        'C': 100,
        'D': 500,
        'M': 1000
    }
    let res = 0
    for(let i = 0; i < s.length; i++) {
        if (i === s.length - 1) {
            res += hash[s.charAt(i)]
        } else {
            if (hash[s.charAt(i)] >= hash[s.charAt(i + 1)]) {
                res += hash[s.charAt(i)]
            } else {
                res -= hash[s.charAt(i)]
            }
        }
    }
    return res
};

结果:

  • 3999/3999 cases passed (176 ms)
  • Your runtime beats 84.42 % of javascript submissions
  • Your memory usage beats 90.55 % of javascript submissions (39.6 MB)
  • 时间复杂度: O(n)
查阅他人解法

这里还看到一种方式,全部先按加法算,如果有前一位小于后一位的情况,直接减正负差值 2/20/200 。来看看代码:

Ⅰ.差值运算

代码:

/**
 * @param {string} s
 * @return {number}
 */
var romanToInt = function(s) {
    const hash = {
        'I': 1,
        'V': 5,
        'X': 10,
        'L': 50,
        'C': 100,
        'D': 500,
        'M': 1000
    }
    let res = 0
    for(let i = 0; i < s.length; i++) {
        res += hash[s.charAt(i)]
        if (i < s.length - 1 && hash[s.charAt(i)] < hash[s.charAt(i + 1)]) {
            res -= 2 * hash[s.charAt(i)]
        }
    }
    return res
};

结果:

  • 3999/3999 cases passed (232 ms)
  • Your runtime beats 53.57 % of javascript submissions
  • Your memory usage beats 80.05 % of javascript submissions (39.8 MB)
  • 时间复杂度: O(n)

换汤不换药,只是做了个加法运算而已,没有太大的本质区别。

思考总结

综上,暂时没有看到一些方向上不一致的解法。我这里推荐字符串直接遍历的解法,性能最佳。

14.最长公共前缀

题目地址

题目描述

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 ""。

示例:

输入: ["flower","flow","flight"]
输出: "fl"

输入: ["dog","racecar","car"]
输出: ""
解释: 输入不存在公共前缀。

说明:

所有输入只包含小写字母 a-z

题目分析设想

这道题一看觉得肯定是需要遍历的题,无非是算法上的优劣罢了。我有三个方向来尝试解题:

  • 遍历每列,取出数组第一项,逐个取字符串的每一位去遍历数组
  • 遍历每项,取出数组第一项,逐步从后截取,判断是否匹配数组中的每一项
  • 分治,将数组递归不断细成俩部分,分别求最大匹配后,再汇总求最大匹配

编写代码验证

Ⅰ.遍历每列

代码:

/**
 * @param {string[]} strs
 * @return {string}
 */
var longestCommonPrefix = function(strs) {
    if (strs.length === 0) return ''
    if (strs.length === 1) return strs[0] || ''
    const str = strs.shift()
    for(let i = 0; i < str.length; i++) {
        const char = str.charAt(i)
        for(let j = 0; j < strs.length; j++) {
            if (i === strs[j].length || strs[j].charAt(i) !== char) {
                return str.substring(0, i)
            }
        }
    }
    return str
};

结果:

  • 118/118 cases passed (68 ms)
  • Your runtime beats 89.17 % of javascript submissions
  • Your memory usage beats 57.83 % of javascript submissions (34.8 MB)
  • 时间复杂度: O(n)

Ⅱ.遍历每项

代码:

/**
 * @param {string[]} strs
 * @return {string}
 */
var longestCommonPrefix = function(strs) {
    if (strs.length === 0) return ''
    if (strs.length === 1) return strs[0] || ''
    let str = strs.shift()
    for(let i = 0; i < strs.length; i++) {
        while (strs[i].indexOf(str) !== 0) {
            str = str.substring(0, str.length - 1);
            if (!str) return ''
        }
    }
    return str
};

结果:

  • 118/118 cases passed (64 ms)
  • Your runtime beats 94.63 % of javascript submissions
  • Your memory usage beats 96.69 % of javascript submissions (33.5 MB)
  • 时间复杂度: O(n)

Ⅲ.分治

代码:

/**
 * @param {string[]} strs
 * @return {string}
 */
var longestCommonPrefix = function(strs) {
    if (strs.length === 0) return ''
    if (strs.length === 1) return strs[0] || ''
    function arrayToString (arr, start, end) {
        if (start === end) {    // 说明数组中只剩一项了
            return arr[start]
        } else {
            const mid = parseInt((start + end) / 2)
            const leftStr = arrayToString(arr, start, mid)
            const rightStr = arrayToString(arr, mid + 1, end)
            return getCommonPrefix(leftStr, rightStr)
        }
    }
    // 两个字符串取最长前缀
    function getCommonPrefix(left, right) {
        const min = Math.min(left.length, right.length)
        for(let i = 0; i < min; i++) {
            if (left.charAt(i) !== right.charAt(i)) {
                return left.substring(0, i)
            }
        }
        return left.substring(0, min)
    }
    return arrayToString(strs, 0, strs.length - 1)
};

结果:

  • 118/118 cases passed (60 ms)
  • Your runtime beats 98.09 % of javascript submissions
  • Your memory usage beats 34.54 % of javascript submissions (35.1 MB)
  • 时间复杂度: O(n)

查阅他人解法

这里还看见使用二分法,跟分治还是略有差异,是每次丢弃不包含答案的区间来减少运算量。

Ⅰ.二分法

代码:

/**
 * @param {string[]} strs
 * @return {string}
 */
var longestCommonPrefix = function(strs) {
    if (strs.length === 0) return ''
    if (strs.length === 1) return strs[0] || ''
    // 找到最短字符串长度
    let minLen = 0
    for(let i = 0; i < strs.length; i++) {
        minLen = minLen === 0 ? strs[i].length : Math.min(minLen, strs[i].length)
    }

    function isCommonPrefix (arr, pos) {
        const str = arr[0].substring(0, pos)    // 取第一项的前一半
        for(let i = 0 ; i < arr.length; i++) {
            if (arr[i].indexOf(str) !== 0) {
                return false
            }
        }
        return true
    }

    let low = 1
    let high = minLen   // 截取最大数量

    while (low <= high) {
        const mid = parseInt((low + high) / 2)
        if (isCommonPrefix(strs, mid)) {    // 如果前半段是
            low = mid + 1   // 继续判断后半段
        } else {
            high = mid - 1  // 前半段继续对半分继续判断
        }
    }

    return strs[0].substring(0, (low + high) / 2)
};

结果:

  • 118/118 cases passed (64 ms)
  • Your runtime beats 94.63 % of javascript submissions
  • Your memory usage beats 93.96 % of javascript submissions (33.5 MB)
  • 时间复杂度: O(log(n))

思考总结

具体情况具体分析,比如分治的算法也可以应用在快速排序中。个人比较推荐分治法和二分法求解这道题。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

桌面应用开发笔记Electron

首发于微信公众号《前端成长记》,写于 2016.05.11

编写步骤

  1. 安装nodejs以及npm

注意配置环境变量path,安装时勾选上可进行自动配置

  1. 使用淘宝npm镜像

因为现在换成https,故直接用npm安装报SSL配置问题,所以采用cnpm代替npm完成electron安装

npm install -g cnpm –registry=https://registry.npm.taobao.org
  1. 安装electron
cnpm install -g electron-prebuilt
  1. 安装git

官网下载安装,主要用于拉取electron在Github源码。自行下载electron的话可不安装。

  1. 初始化项目
git clone https://github.com/atom/electron-quick-start
npm install && npm start

接着会出现一个helloworld的桌面应用

打包步骤

  1. 下载electron框架

https://github.com/electron/electron/releases 选择对应系统的版本下载下来,得到一堆配置文件和locales以及resources文件夹,具体使用说明查看electron官网

  1. 安装asar,打包依赖组件
npm install -g asar
  1. 打包静态资源城app.asar
asar pack 文件夹名称 app.asar

文件夹里存放静态资源,最好不要放exe/dat等类型的文件;.asar的文件名必须为app.asar,必须!

  1. 将app.asar移至框架中的resources文件夹内

此时运行electron.exe,你会发现里面的内容已经换成你的资源了

  1. 通过winrar或者nsis等工具把刚才的资源制作成安装程序

至此,其他人通过下载该文件便安装你的桌面应用了。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

JPG和PNG特性分析及适用范围

首发于微信公众号《前端成长记》,写于 2015.04.09

背景

由于做移动端开发,必须要考虑到图片尺寸问题,所以就JPG和PNG做简单的分析。

特性分析

JPG的特性

有损压缩

  • 支持摄影图像或写实图像的高级压缩,并且可利用压缩比例控制图像文件大小
  • 有损压缩会使图像数据质量下降,并且在编辑和重新保存JPG格式图像时,这种下降损失会累积
  • JPG不适用于所含颜色很少、具有大块颜色相近的区域或亮度差异十分明显的较简单的图片

童鞋可以自行调整jpg质量或者采用工具压缩jpg进行对比,体积减小明显

PNG的特性

无损压缩

  • 能在保证最不失真的情况下尽可能压缩图像文件的大小
  • PNG用来存储灰度图像时,灰度图像的深度可多到16位,存储彩色图像时,彩色图像的深度可多到48位,并且还可存储多到16位的α通道数据
  • 对于需要高保真的较复杂的图像,PNG虽然能无损压缩,但图片文件较大,不适合应用在Web页面上

PNG8最多只能索引256种颜色,所以对于颜色较多的图像不能真实还原;PNG24则可以保存1600多万种颜色,基本能够真实还原我们人类肉眼所可以分别的所有颜色;PNG格式最多可以保存48位颜色通道。

总结

在存储图像时采用JPG还是PNG主要依据图像上的色彩层次和颜色数量进行选择。

  • 一般层次丰富颜色较多的图像采用JPG存储,而颜色简单对比强烈的则需要采用PNG。但也会有一些特殊情况,例如有些图像尽管色彩层次丰富,但由于图片尺寸较小,上面包含的颜色数量有限时,也可以尝试用PNG进行存储。
  • 有些矢量工具绘制的图像由于采用较多的滤镜特效也会形成丰富的色彩层次,这个时候就需要采用JPG进行存储了
  • 用于页面结构的基本视觉元素,如容器的背景、按钮、导航的背景等应该尽量用PNG格式进行存储,这样才能更好的保证设计品质
  • 一些内容元素,如广告Banner、商品图片等对质量要求不是特别苛刻的,则可以用JPG去进行存储从而降低文件大小

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

【Leetcode 做题学算法周刊】第六期

首发于微信公众号《前端成长记》,写于 2019.12.15

背景

本文记录刷题过程中的整个思考过程,以供参考。主要内容涵盖:

  • 题目分析设想
  • 编写代码验证
  • 查阅他人解法
  • 思考总结

目录

Easy

110.平衡二叉树

题目地址

题目描述

给定一个二叉树,判断它是否是高度平衡的二叉树。

本题中,一棵高度平衡二叉树定义为:

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。

示例 1:

给定二叉树 [3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7

返回 true

示例 2:

给定二叉树 [1,2,2,3,3,null,null,4,4]

       1
      / \
     2   2
    / \
   3   3
  / \
 4   4

返回 false

题目分析设想

我们上一期做过通过遍历求二叉树的最大深度的题目,这题最粗暴的一个方案就是计算出每个子树的最大深度做高度判断,很明显,这个效率低下。我们可以通过改成自底而上的方案,当中间过程不符合,则可以跳出计算。

编写代码验证

Ⅰ.计算子树最大深度做判断

代码:

/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isBalanced = function(root) {
    if (root === null) return true
    function maxDepth (node) {
        if (node === null) return 0
        const l = maxDepth(node.left)
        const r = maxDepth(node.right)
        return Math.max(l, r) + 1
    }

    return Math.abs(maxDepth(root.left) - maxDepth(root.right)) <= 1
    && isBalanced(root.left)
    && isBalanced(root.right)
};

结果:

  • 227/227 cases passed (80 ms)
  • Your runtime beats 77.66 % of javascript submissions
  • Your memory usage beats 26.73 % of javascript submissions (37.8 MB)
  • 时间复杂度 O(n^2)

Ⅱ.自底而上

代码:

/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isBalanced = function(root) {
    function maxDepth (node) {
        if (node === null) return 0
        const l = maxDepth(node.left)
        if (l === -1) return -1
        const r = maxDepth(node.right)
        if (r === -1) return -1
        return Math.abs(l - r) <= 1 ? Math.max(l, r) + 1 : -1
    }

    return maxDepth(root) !== -1
};

结果:

  • 227/227 cases passed (72 ms)
  • Your runtime beats 95.44 % of javascript submissions
  • Your memory usage beats 50.5 % of javascript submissions (37.5 MB)
  • 时间复杂度 O(n)

查阅他人解法

思路基本上都是这两种,未发现方向不同的解法。

思考总结

这里很明显,大家都是用深度遍历来解决问题,计算子树深度会发现,有很多重复运算,所以不妨试试自底而上的方式,直接在计算高度过程中就返回,也可以叫做“提前阻断”。所以,这道题建议是使用自底而上的方式来作答。

111.二叉树的最小深度

题目地址

题目描述

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

说明: 叶子节点是指没有子节点的节点。

示例:

给定二叉树 [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回它的最小深度 2.

题目分析设想

这道题很明显自顶而下就可以了,判断每个节点的子节点是否存在,不存在,则该路径为最短路径。如果存在,就按深度的方式比较最小值。总体上来说,也可以用之前求最大深度的几种方式来作答。

编写代码验证

Ⅰ.递归

代码:

/**
 * @param {TreeNode} root
 * @return {number}
 */
var minDepth = function(root) {
    if (root === null) return 0
    if (root.left === null && root.right === null) return 1
    let res = Infinity
    if(root.left !== null) {
        res = Math.min(minDepth(root.left), res)
    }
    if(root.right !== null) {
        res = Math.min(minDepth(root.right), res)
    }
    return res + 1
};

结果:

  • 41/41 cases passed (76 ms)
  • Your runtime beats 69.08 % of javascript submissions
  • Your memory usage beats 5.55 % of javascript submissions (37.9 MB)
  • 时间复杂度 O(n)

Ⅱ.利用栈迭代

代码:

/**
 * @param {TreeNode} root
 * @return {number}
 */
var minDepth = function(root) {
    if (root === null) return 0
    if (root.left === null && root.right === null) return 1
    // 栈
    let s = [{
        node: root,
        dep: 1
    }]
    let dep = Infinity
    while(s.length) {
        // 先进后出
        var cur = s.pop()
        if (cur.node !== null) {
            let curDep = cur.dep
            if (cur.node.left === null && cur.node.right === null) {
                dep = Math.min(dep, curDep)
            }
            if (cur.node.left !== null) s.push({node: cur.node.left, dep: curDep + 1})
            if (cur.node.right !== null) s.push({node: cur.node.right, dep: curDep + 1})
        }
    }
    return dep
};

结果:

  • 41/41 cases passed (68 ms)
  • Your runtime beats 93.82 % of javascript submissions
  • Your memory usage beats 75.31 % of javascript submissions (37 MB)
  • 时间复杂度 O(n)

Ⅲ.利用队列

代码:

/**
 * @param {TreeNode} root
 * @return {number}
 */
var minDepth = function(root) {
    if (root === null) return 0
    if (root.left === null && root.right === null) return 1
    // 队列
    let s = [{
        node: root,
        dep: 1
    }]
    let dep = 0
    while(s.length) {
        // 先进先出
        var cur = s.shift()
        var node = cur.node
        dep = cur.dep
        if (node.left === null && node.right === null) break;
        if (node.left !== null) s.push({node: node.left, dep: dep + 1})
        if (node.right !== null) s.push({node: node.right, dep: dep + 1})
    }
    return dep
};

结果:

  • 41/41 cases passed (76 ms)
  • Your runtime beats 69.08 % of javascript submissions
  • Your memory usage beats 6.79 % of javascript submissions (37.7 MB)
  • 时间复杂度 O(n)

查阅他人解法

总体上而言分成深度优先和广度优先,最基本的就是递归和迭代了。没有发现二叉树相关题目的一些新奇解法。

思考总结

很明显可以看出递归和利用栈迭代是深度优先,利用队列是广度优先。这里自顶而下比较合适,只要找到叶子节点,直接就是最小深度了,可以省去不少运算。

112.路径总和

题目地址

题目描述

给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。

说明: 叶子节点是指没有子节点的节点。

示例:

给定如下二叉树,以及目标和 sum = 22

              5
             / \
            4   8
           /   / \
          11  13  4
         /  \      \
        7    2      1

返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2

题目分析设想

这道题我的想法是因为要找到叶子节点,所以深度优先更为合适,这里就使用前文的两种方法:

  • 递归
  • 利用栈迭代

编写代码验证

Ⅰ.递归

代码:

/**
 * @param {TreeNode} root
 * @param {number} sum
 * @return {boolean}
 */
var hasPathSum = function(root, sum) {
    if (root === null) return false
    // 剩余需要的值
    sum -= root.val
    if (root.left === null && root.right === null) {
        return sum === 0
    } else {
        return hasPathSum(root.left, sum) || hasPathSum(root.right, sum)
    }
};

结果:

  • 114/114 cases passed (80 ms)
  • Your runtime beats 62.09 % of javascript submissions
  • Your memory usage beats 56.9 % of javascript submissions (37.1 MB)
  • 时间复杂度 O(n)

Ⅱ.迭代

代码:

/**
 * @param {TreeNode} root
 * @param {number} sum
 * @return {boolean}
 */
var hasPathSum = function(root, sum) {
    if (root === null) return false
    // 栈
    let stack = [{
        node: root,
        remain: sum - root.val
    }]
    while(stack.length) {
        // 先进后出
        var cur = stack.pop()
        var node = cur.node
        if (node.left === null && node.right === null && cur.remain === 0) return true
        if (node.left !== null) {
            stack.push({
                node: node.left,
                remain: cur.remain - node.left.val
            })
        }
        if (node.right !== null) {
            stack.push({
                node: node.right,
                remain: cur.remain - node.right.val
            })
        }
    }
    return false
};

结果:

  • 114/114 cases passed (72 ms)
  • Your runtime beats 88.51 % of javascript submissions
  • Your memory usage beats 33.33 % of javascript submissions (37.2 MB)
  • 时间复杂度 O(n)

查阅他人解法

这里看到一个方案是采用后序遍历,路径长度由之前的栈改成变量保存,但是这个在我看来没有中序遍历合适,感兴趣的可以 点此查阅 。另外还是有选择使用广度优先,利用队列来解的,这里也算一个不同思路,就当做补充吧。

Ⅰ.利用队列

代码:

/**
 * @param {TreeNode} root
 * @param {number} sum
 * @return {boolean}
 */
var hasPathSum = function(root, sum) {
    if (root === null) return false
    // 队列
    let q = [{
        node: root,
        sum: root.val
    }]
    while(q.length) {
        // 当前层元素的个数
        for(let i = 0; i < q.length; i++) {
            let cur = q.shift()
            let node = cur.node
            if (node.left === null && node.right === null && cur.sum === sum) return true

            if (node.left !== null) {
                q.push({ node: node.left, sum: cur.sum + node.left.val})
            }
            if (node.right !== null) {
                q.push({ node: node.right, sum: cur.sum + node.right.val})
            }
        }
    }
    return false
};

结果:

  • 114/114 cases passed (72 ms)
  • Your runtime beats 88.51 % of javascript submissions
  • Your memory usage beats 56.32 % of javascript submissions (37.1 MB)
  • 时间复杂度 O(n)

118.杨辉三角

题目地址

题目描述

给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。

杨辉三角

在杨辉三角中,每个数是它左上方和右上方的数的和。

示例:

输入: 5
输出:
[
     [1],
    [1,1],
   [1,2,1],
  [1,3,3,1],
 [1,4,6,4,1]
]

题目分析设想

这道题最笨的方案就是双重循环,首尾为1,其他位为 S(l)[n] = S(l-1)[n-1] + S(l-1)[n] 。当然这里很明显也可以当做一个动态规划问题来解答。

这里有个坑,给的是索引,不是第 n 行

编写代码验证

Ⅰ.动态规划

代码:

/**
 * @param {number} numRows
 * @return {number[][]}
 */
var generate = function(numRows) {
    let res = []
    for(let i = 0; i < numRows; i++) {
        // 所有默认都填了1,可以节省不少运算
        res.push(new Array(i+1).fill(1))
        // 第三行开始才需要修改
        for(j = 1; j < i; j++) {
            res[i][j] = res[i-1][j] + res[i-1][j-1]
        }
    }
    return res
};

结果:

  • 15/15 cases passed (60 ms)
  • Your runtime beats 85.2 % of javascript submissions
  • Your memory usage beats 55.52 % of javascript submissions (33.6 MB)
  • 时间复杂度 O(n^2)

查阅他人解法

这里看到两个不同方向的,一个是递归,因为这题在递归卡片中,一个是二项式定理。

Ⅰ.递归

代码:

/**
 * @param {number} numRows
 * @return {number[][]}
 */
var generate = function (numRows) {
    let res = []

    function sub(row, numRows, arr) {
        let temp = []
        if (row < numRows) {
            for (let i = 0; i <= row; i++) {
                if (row === 0) {
                    temp.push(1)
                } else {
                    let left = i - 1 >= 0 ? arr[row - 1][i - 1] : 0
                    let right = i < arr[row - 1].length ? arr[row - 1][i] : 0
                    temp.push(left + right)
                }
            }
            arr.push(temp)
            sub(++row, numRows, arr)
            return arr
        } else {
            return arr
        }
    }
    return sub(0, numRows, res)
};

结果:

  • 15/15 cases passed (64 ms)
  • Your runtime beats 68.27 % of javascript submissions
  • Your memory usage beats 56.86 % of javascript submissions (33.6 MB)
  • 时间复杂度 O(n^2)

Ⅱ.二项式定理

优势在于可以直接计算第n行,用二项式定理公式计算。 (a+b)^n 一共有n+1项,每一项的系数对应杨辉三角的第 n 行。第 r 项的系数等于 组合数 C(n,r)

代码:

/**
 * @param {number} numRows
 * @return {number[][]}
 */
var generate = function(numRows) {
    var res = [];

    /**
     * 组合数
     * @param n
     * @param r
     * @returns {number}
     * @constructor
     */
    function C(n, r) {
        if(n == 0) return 1;
        return F(n) / F(r) / F(n - r);
    }
    /**
     * 阶乘
     * @param n
     * @returns {number}
     * @constructor
     */
    function F(n) {
        var s = 1;
        for(var i = 1;i <= n;i++) {
            s *= i;
        }
        return s;
    }

    for (var i = 0;i < numRows;i++){
        res[i] = [];
        for (var j = 0;j < i + 1;j++){
            res[i].push(C(i, j));
        }
    }
    return res;
};

结果:

  • 15/15 cases passed (64 ms)
  • Your runtime beats 68.27 % of javascript submissions
  • Your memory usage beats 5.02 % of javascript submissions (34.3 MB)
  • 时间复杂度 O(n^2)

思考总结

对于数学敏感的开发者,很容易就想到使用二项式定理。但是在我看来,找到了一个计算规则,就很容易想到使用动态规划来解决问题,我也推荐使用动态规划来生成杨辉三角。

119.杨辉三角Ⅱ

题目地址

题目描述

给定一个非负索引 k,其中 k ≤ 33,返回杨辉三角的第 k 行。

在杨辉三角中,每个数是它左上方和右上方的数的和。

示例:

输入: 3
输出: [1,3,3,1]

进阶:

你可以优化你的算法到 O(k) 空间复杂度吗?

题目分析设想

上面从他人解法中发现了二项式定理可以直接求第 n 行。另外我们也可以发现个规律,第几行实际上就有几个数,且首尾为1。当然也可以使用动态规划来作答。

编写代码验证

Ⅰ.动态规划

代码:

/**
 * @param {number} rowIndex
 * @return {number[]}
 */
var getRow = function(rowIndex) {
    // rowIndex 是索引,0相当于第1行
    if (rowIndex === 0) return [1]
    let res = []
    for(let i = 0; i < rowIndex + 1; i++) {
        let temp = new Array(i+1).fill(1)
        // 第三行开始才需要修改
        for(let j = 1; j < i; j++) {
            temp[j] = res[j - 1] + res[j]
        }
        res = temp
    }
    return res
};

结果:

  • 34/34 cases passed (64 ms)
  • Your runtime beats 75.77 % of javascript submissions
  • Your memory usage beats 54.9 % of javascript submissions (33.8 MB)
  • 时间复杂度 O(n^2)

Ⅱ.二项式定理

代码:

/**
 * @param {number} rowIndex
 * @return {number[]}
 */
var getRow = function(rowIndex) {
    /**
     * 组合数
     * @param n
     * @param r
     * @returns {number}
     * @constructor
     */
    function C(n, r) {
        if(n == 0) return 1;
        return F(n) / F(r) / F(n - r);
    }
    /**
     * 阶乘
     * @param n
     * @returns {number}
     * @constructor
     */
    function F(n) {
        var s = 1;
        for(var i = 1;i <= n;i++) {
            s *= i;
        }
        return s;
    }
    let res = []
    // 因为是通过上一项计算,所以第1项的 n 为0
    for (var i = 0;i < rowIndex + 1;i++){
        res.push(C(rowIndex, i));
    }
    return res;
};

结果:

  • 34/34 cases passed (52 ms)
  • Your runtime beats 99.12 % of javascript submissions
  • Your memory usage beats 41.18 % of javascript submissions (34.5 MB)
  • 时间复杂度 O(n)

查阅他人解法

因为发现每行的对称性,所以也可以求一半后反转复制即可。

Ⅰ.反转复制

代码:

/**
 * @param {number} rowIndex
 * @return {number[]}
 */
var getRow = function(rowIndex) {
    // rowIndex 是索引,0相当于第1行
    if (rowIndex === 0) return [1]
    let res = []
    for(let i = 0; i < rowIndex + 1; i++) {
        let temp = new Array(i+1).fill(1)
        // 第三行开始才需要修改
        const mid = i >>> 1
        for(let j = 1; j < i; j++) {
            if (j > mid) {
                temp[j] = temp[i - j]
            } else {
                temp[j] = res[j - 1] + res[j]
            }
        }
        res = temp
    }
    return res
};

结果:

  • 34/34 cases passed (60 ms)
  • Your runtime beats 88.47 % of javascript submissions
  • Your memory usage beats 60.78 % of javascript submissions (33.7 MB)
  • 时间复杂度 O(n^2)

思考总结

其实更像一个数学问题,不断地找出规律来节省运算,真是“学好数理化,走遍天下都不怕”。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

纯css实现焦点轮播效果-功能可拓展

首发于微信公众号《前端成长记》,写于 2015.08.04

前言

相信对于很多刚接触前端的新手而言,轮播图是第一道需要去克服的坎,我这里提供一个纯css实现轮播的思路。

Demo

在线地址

代码

代码部分
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
    <meta name="format-detection" content="telephone=no"/>
    <meta name="apple-mobile-web-app-capable" content="yes"/>
    <meta charset="UTF-8">
    <title>纯CSS焦点轮播</title>
    <style type="text/css">
        body {  background-color: #f0f0f0;  }
        .focus_img { position: relative; height: 300px; width: 500px; overflow: hidden; margin: auto; border: solid 2px #000000;}
        ul { position: absolute; padding: 0; margin: 0; }
        .img_list { top: 0; left: 0; -webkit-animation: img_list 10s 2s infinite;}
        .img_list li { width: 500px; height: 300px; overflow: hidden;}
        .img_list img { height: 300px; width: 500px;}
        .img_index { bottom: 10px;right: 10px;}
        .img_index li {float: left; width: 16px; height: 16px; border: solid 1px #cccccc; background-color: #ffffff; color:#000000; text-align: center;line-height: 16px;overflow: hidden;cursor: pointer;margin-right: 2px;  }
        .one { background-color: #000000; color: #ffffff; -webkit-animation: one 10s 2s infinite;}
        .two { -webkit-animation: two 10s 2s infinite;}
        .three {-webkit-animation: three 10s 2s infinite;}
        .four {-webkit-animation: four 10s 2s infinite;}
        .five {-webkit-animation: five 10s 2s infinite;}
        @-webkit-keyframes img_list {
            0%{ -webkit-transform: translate(0,0);}
            10%,20% {-webkit-transform: translate(0,-300px);}
            30%,40% {-webkit-transform: translate(0,-600px);}
            50%,60% {-webkit-transform: translate(0,-900px);}
            70%,80% {-webkit-transform: translate(0,-1200px);}
            90%,100% {-webkit-transform: translate(0,0);}
        }
        @-webkit-keyframes one {
            10%,20%,30%,40%,50%,60%,70%,80%{ background-color: #ffffff; color: #000000;}
            0%,90%,100%{ background-color: #000000;color: #ffffff;}
        }
        @-webkit-keyframes two {
            0%,30%,40%,50%,60%,70%,80%,90%,100%{ background-color: #ffffff; color: #000000;}
            10%,20%{ background-color: #000000;color: #ffffff;}
        }
        @-webkit-keyframes three {
            0%,10%,20%,50%,60%,70%,80%,90%,100%{ background-color: #ffffff; color: #000000;}
            30%,40%{ background-color: #000000;color: #ffffff;}
        }
        @-webkit-keyframes four {
            0%,10%,20%,30%,40%,70%,80%,90%,100%{ background-color: #ffffff; color: #000000;}
            50%,60%{ background-color: #000000;color: #ffffff;}
        }
        @-webkit-keyframes five {
            0%,10%,20%,30%,40%,50%,60%,90%,100%{ background-color: #ffffff; color: #000000;}
            70%,80%{ background-color: #000000;color: #ffffff;}
        }
    </style>
</head>
<body>
<div class="focus_img">
    <ul class="img_list">
        <li><img src="https://ss0.bdstatic.com/-0U0bnSm1A5BphGlnYG/tam-ogel/1d68b8f42a077e5fc13dd53282b884ad_121_121.jpg" alt=""/></li>
        <li><img src="https://ss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=2677286959,1418084722&fm=58&s=05F07D3288656D01446505DD0100C022" alt=""/></li>
        <li><img src="https://ss0.baidu.com/6ONWsjip0QIZ8tyhnq/it/u=3509313783,3642023323&fm=58" alt=""/></li>
        <li><img src="https://ss2.bdstatic.com/8_V1bjqh_Q23odCf/pacific/833581989.png" alt=""/></li>
        <li><img src="https://ss1.bdstatic.com/9vo3dSag_xI4khGkpoWK1HF6hhy/wisegame/pic/item/c0cc7cd98d1001e9e09d1675b10e7bec54e79723.jpg" alt=""/></li>
    </ul>
    <ul class="img_index">
        <li class="one">1</li>
        <li class="two">2</li>
        <li class="three">3</li>
        <li class="four">4</li>
        <li class="five">5</li>
    </ul>
</div>
</body>
<script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
</html>

(完)

本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

【Leetcode 做题学算法周刊】第九期

首发于微信公众号《前端成长记》,写于 2021.01.11

背景

本文记录刷题过程中的整个思考过程,以供参考。主要内容涵盖:

  • 题目分析设想
  • 编写代码验证
  • 查阅他人解法
  • 思考总结

目录

Easy

171.Excel表列序号

题目地址

题目描述

给定一个Excel表格中的列名称,返回其相应的列序号。

例如,

    A -> 1
    B -> 2
    C -> 3
    ...
    Z -> 26
    AA -> 27
    AB -> 28
    ...

示例:

输入: "A"
输出: 1

输入: "AB"
输出: 28

输入: "ZY"
输出: 701

题目分析设想

这道题其实就是上面168题的逆推导,也是就26进制转10进制。所以解法也很直接,做转换即可。无非区别在于正序和倒序处理罢了。

编写代码验证

Ⅰ.正序转换

代码:

/**
 * @param {string} s
 * @return {number}
 */
var titleToNumber = function(s) {
    let res = 0
    for(let i = 0; i < s.length; i++) {
        res = res * 26 + (s.charCodeAt(i) - 'A'.charCodeAt() + 1)
    }
    return res
};

结果:

  • 1000/1000 cases passed (84 ms)
  • Your runtime beats 69.77 % of javascript submissions
  • Your memory usage beats 33.33 % of javascript submissions (36.1 MB)
  • 时间复杂度: O(n)

Ⅱ.倒序转换

代码:

/**
 * @param {string} s
 * @return {number}
 */
var titleToNumber = function(s) {
    let res = 0
    let mul = 1
    for(let i = s.length - 1; i >= 0; i--) {
        res += (s.charCodeAt(i) - 'A'.charCodeAt() + 1) * mul
        mul *= 26
    }
    return res
};

结果:

  • 1000/1000 cases passed (100 ms)
  • Your runtime beats 20.5 % of javascript submissions
  • Your memory usage beats 100 % of javascript submissions (34.7 MB)
  • 时间复杂度: O(n)

查阅他人解法

看了一下解法,大部分都是常规的倒序解法。正序的话其实就是秦九韶算法。

思考总结

这道题没有什么难度,就是一个进制转换的过程,比较容易想到的也就是倒序遍历,转换求解。

172.阶乘后的零

题目地址

题目描述

给定一个整数 n,返回 n! 结果尾数中零的数量。

示例:

输入: 3
输出: 0
解释: 3! = 6, 尾数中没有零。

输入: 5
输出: 1
解释: 5! = 120, 尾数中有 1 个零.

说明: 你算法的时间复杂度应为 O(log n) 。

题目分析设想

这道题明显不适合算出来阶乘的值再来算有多少个零,因为很容易溢出,且复杂度高。所以这里需要找到规律,这里规律还很明显,零的产生一定是由 2x5(4看作2x2),所以本质上还是看 5 的数量,当然 5^n 还需要再累加。

编写代码验证

Ⅰ.累加5的每个幂的个数

代码:

/**
 * @param {number} n
 * @return {number}
 */
var trailingZeroes = function(n) {
    let res = 0
    while (n) {
        n = n / 5 | 0
        res += n
    }
    return res
};

结果:

  • 502/502 cases passed (76 ms)
  • Your runtime beats 55.62 % of javascript submissions
  • Your memory usage beats 100 % of javascript submissions (34.5 MB)
  • 时间复杂度: O(log(n))

其实除5也可以等同于 xxx/5 + xxx/25 + xxx/125 + xxx/5^n,当5^n超过阶乘数的时候,必然取0。

代码:

/**
 * @param {number} n
 * @return {number}
 */
var trailingZeroes = function(n) {
    let res = 0
    let num = 5
    while (n >= num) {
        res += n / num | 0
        num *= 5
    }
    return res
};

结果:

  • 502/502 cases passed (76 ms)
  • Your runtime beats 55.62 % of javascript submissions
  • Your memory usage beats 100 % of javascript submissions (34.3 MB)
  • 时间复杂度: O(log(n))

查阅他人解法

没有看见更优的算法,但是看到一个偏硬核的,其实也就是上面的解法的提前枚举。因为我们之前数值运算有MAX边界,所以5^n是可枚举。

Ⅰ.枚举

代码:

/**
 * @param {number} n
 * @return {number}
 */
var trailingZeroes = function(n) {
    return (n/5 | 0)+(n/25 | 0)+(n/125 | 0)+(n/625 | 0)+(n/3125 | 0)+(n/15625 | 0)+(n/78125 | 0)+(n/390625 | 0)
    +(n/1953125 | 0)+(n/9765625 | 0)+(n/48828125 | 0)+(n/244140625 | 0)+(n/1220703125 | 0);
};

结果:

  • 502/502 cases passed (80 ms)
  • Your runtime beats 37.95 % of javascript submissions
  • Your memory usage beats 100 % of javascript submissions (34.4 MB)
  • 时间复杂度: O(log(n))

思考总结

枚举的方式纯属一乐,这道题的本质还是找到5的关键点,将问题进行转换求解即可。

190.颠倒二进制位

题目地址

题目描述

颠倒给定的 32 位无符号整数的二进制位。

示例:

输入: 00000010100101000001111010011100
输出: 00111001011110000010100101000000
解释: 输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596
     因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000

输入:11111111111111111111111111111101
输出:10111111111111111111111111111111
解释:输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293
     因此返回 3221225471 其二进制表示形式为 10111111111111111111111111111111 

提示:

  • 请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
  • 在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 2 中,输入表示有符号整数 -3,输出表示有符号整数 -1073741825。

进阶:
如果多次调用这个函数,你将如何优化你的算法?

题目分析设想

看到这道题,明显关键点就在各种位运算符的使用。但是也可以取巧通过字符串处理,所以下面就尝试从这两个方向去作答。

编写代码验证

Ⅰ.位运算

代码:

/**
 * @param {number} n - a positive integer
 * @return {number} - a positive integer
 */
var reverseBits = function(n) {
    let t = 32, r = 0;
    while(t--) {
        r = (r << 1) + (n & 1); // 输出结果左移并加上最后一位
        n >>= 1 // 待处理数字右移
    }
    return r >>> 0;
};

结果:

  • 600/600 cases passed (100 ms)
  • Your runtime beats 49.62 % of javascript submissions
  • Your memory usage beats 70.65 % of javascript submissions (39.4 MB)
  • 时间复杂度: O(1)

Ⅱ.字符串处理

代码:

/**
 * @param {number} n - a positive integer
 * @return {number} - a positive integer
 */
var reverseBits = function(n) {
    return parseInt(
        n.toString(2).split('').reverse().join('').padEnd(32, 0),
        2
    )
};

结果:

  • 600/600 cases passed (104 ms)
  • Your runtime beats 33.27 % of javascript submissions
  • Your memory usage beats 88.8 % of javascript submissions (39.1 MB)
  • 时间复杂度: O(1)

查阅他人解法

这里还看见另外一种位运算符的使用

Ⅰ.位移+换位

代码:

/**
 * @param {number} n - a positive integer
 * @return {number} - a positive integer
 */
var reverseBits = function(n) {
    let t = 32, r = 0;
    while(t--) {
        r = (r << 1) | (n & 1); // 输出结果左移并末位替换
        n >>= 1 // 待处理数字右移
    }
    return r >>> 0;
};

结果:

  • 600/600 cases passed (96 ms)
  • Your runtime beats 61.41 % of javascript submissions
  • Your memory usage beats 91.5 % of javascript submissions (39 MB)
  • 时间复杂度: O(1)

思考总结

这题的考点也就在位运算了,熟悉熟悉很容易就能处理。不过熟悉位运算符对于一些运算来讲还是非常有意义的。

191.位1的个数

题目地址

题目描述

编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 1 的个数(也被称为汉明重量)。

示例:

输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'

输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'

输入:11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'

提示:

  • 请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
  • 在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 2 中,输入表示有符号整数 -3,输出表示有符号整数 -1073741825。

进阶:
如果多次调用这个函数,你将如何优化你的算法?

题目分析设想

做了上道题之后,这道题,还可以使用位运算符来处理。同样,通过字符串获取也可以实现,但这显然不是这题的考点。

编写代码验证

Ⅰ.位运算

代码:

/**
 * @param {number} n - a positive integer
 * @return {number}
 */
var hammingWeight = function(n) {
    let t = 32, r = 0;
    while(t--) {
        if (n & 1 === 1) r++; // 判断最后一位
        n >>= 1 // 待处理数字右移
    }
    return r;
};

结果:

  • 601/601 cases passed (88 ms)
  • Your runtime beats 84.03 % of javascript submissions
  • Your memory usage beats 91.49 % of javascript submissions (38.8 MB)
  • 时间复杂度: O(1)

Ⅱ.字符串处理

代码:

/**
 * @param {number} n - a positive integer
 * @return {number}
 */
var hammingWeight = function(n) {
    return n.toString(2).replaceAll('0', '').length
};

结果:

  • 601/601 cases passed (92 ms)
  • Your runtime beats 68.73 % of javascript submissions
  • Your memory usage beats 88.54 % of javascript submissions (38.9 MB)
  • 时间复杂度: O(1)

查阅他人解法

实现方法就太多了,大同小异,既然考位运算,那就再换成左移作答吧。其他的思路也都大同小异,无非就是加减、几位运算的区别。

Ⅰ.位运算-1左移

代码:

/**
 * @param {number} n - a positive integer
 * @return {number} - a positive integer
 */
var reverseBits = function(n) {
    let t = 32, r = 0;
    while(t--) {
        if ((n & (1 << (32 - t))) != 0) {
            r++;
        }
    }
    return r;
};

结果:

  • 601/601 cases passed (104 ms)
  • Your runtime beats 21.48 % of javascript submissions
  • Your memory usage beats 82.75 % of javascript submissions (39 MB)
  • 时间复杂度: O(1)

思考总结

说白了如果做了上道题之后,这题就没有什么价值了,只是写法问题,解法可太多了。

198.打家劫舍

题目地址

题目描述

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)
     偷窃到的最高金额 = 1 + 3 = 4 

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)
     偷窃到的最高金额 = 2 + 9 + 1 = 12 

提示:

  • 0 <= nums.length <= 100
  • 0 <= nums[i] <= 400

题目分析设想

这道题可以转化一下,简化之后其实就是奇数项和偶数项的和的比较。最基础的思路就是分别求和,然后记录当前比较结果后再继续分别求和。如果每次利用上每次的结果的话,那就变成了一个动态规划求解的问题了,所以这道题我们循序渐进来作答。

编写代码验证

Ⅰ.分别求和

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var rob = function(nums) {
    let evenTotal = 0, oddTotal = 0;
    for(let i = 0; i < nums.length; i++) {
        if (i & 1) {
            evenTotal += nums[i]
            evenTotal = Math.max(evenTotal, oddTotal) // 取两种方式的更大值
        } else {
            oddTotal += nums[i]
            oddTotal = Math.max(evenTotal, oddTotal) // 取两种方式的更大值
        }
    }
    return Math.max(evenTotal, oddTotal)
};

结果:

  • 69/69 cases passed (84 ms)
  • Your runtime beats 53.82 % of javascript submissions
  • Your memory usage beats 61.74 % of javascript submissions (37.6 MB)
  • 时间复杂度: O(n)

Ⅱ.动态规划

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var rob = function(nums) {
    const len = nums.length;
    if (len === 0) return 0
    if (len === 1) return nums[0]
    if (len === 2) return Math.max(nums[0], nums[1])
    // dp[i] 表示偷i+1间的最大收益
    let dp = [nums[0], Math.max(nums[0], nums[1])]
    for(let i = 2; i < len; i++) {
        dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i])
    }
    return dp[nums.length - 1]
};

结果:

  • 69/69 cases passed (72 ms)
  • Your runtime beats 95.68 % of javascript submissions
  • Your memory usage beats 26.94 % of javascript submissions (37.9 MB)
  • 时间复杂度: O(n)

查阅他人解法

看了一下解法,基本都是动态规划求解。

思考总结

这道题如果看多了,基本能直接转化成一个动态规划的问题,有点类似于买卖股票。但是这题我更建议分别求和每次做比较的方式,这样的话其实空间复杂度是明显更低的。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

Nodejs创建缩略图

首发于微信公众号《前端成长记》,写于 2016.12.13

背景

团队站项目页因为采用瀑布流形式展示,同屏幕出现了大量大尺寸图,影响了页面的流畅度。

经过测试:

  1. 全部更换为一张大图,页面流畅
  2. 参考首页,多张正常大小图,页面流畅

基本可以确定:图大而且多,是导致页面卡顿的“真凶”

自己实现图片缩略图

Step1

缩略图嘛,首先要有原图,然后必然要对图片进行处理。Node里面,要处理图片,无非就是使用二进制流,然后通过Buffer去Do anything what you want…

Step2

对图片进行处理,我这里采用的是gm模块,人家有现成的轮子为啥要自己封装,484傻…gm模块必须依赖于一个本地图片处理软件。

这里有两个选择:GraphicsMagickImageMagick。我选择的是前者,你问我为什么?因为它前

安装的教程请google~

注意:需要添加到环境变量。

Step3

安装完成后,就是在nodejs下使用gm模块了

// import gm module
var gm = require("gm");
// use example
gm(filePath)
    .resize(100,200))
    .write("upload/" + outPath, function (e) {
        if(e) { logger.error("gm写图片失败了")}
        console.log("成功在本地生成缩略图 upload/"+ outPath);
        // Do something
    });

上述代码实现的是通过gm读取文件路径为filePath的文件,然后生成一张100X200的缩略图,接着写入生成到upload文件夹下的outPath下。运行一下,发现会抛出error,因为outPath的文件夹目录不存在。

** 为什么会不存在路径? **

因为我吃饱没事每个文件夹都打开然后找到之后自己创建吗,吃力不讨好。所以在这里,就是我前面文章介绍的mkdir模块大放异彩的时候啦~

var gm = require("gm");
var mkdirs = require('jm-mkdirs');
mkdirs.sync("upload/" + tempArr.join("/")); // 就是这句
gm(filesList[imageIdx].path)
    .resize(304,Math.ceil(304/filesList[imageIdx].radio))
    .write("upload/" + outPath, function (e) {
        if(e) { logger.error("gm写图片失败了")}
        console.log("成功在本地生成缩略图 upload/"+ outPath);
        logger.debug("成功在本地生成缩略图 upload/"+ outPath);
        // Do something
    });

这样会在一个新的本地路径下生成一个同结构的缩略图文件夹了。Perfect!

Step4

修改一下自己的需求任务,我这里是上传到云存储,同时上传原图和缩略图到云存储即可。

image

总结

  1. 自己开发的生成缩略图的功能目前也只是支持缩略图,要达到自定义宽高,质量等多方面辅助实现,还是离不开服务器的支持。
  2. 目前缩略图还可以增加根据图片大小生成SHA码,来作为标识,可以进行对比,用于文件的对比更新,做增量修改。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

jmui-一款简约的移动端组件库

首发于微信公众号《前端成长记》,写于 2015.12.07

介绍(已停止维护,不建议使用,仅供参考)

一款目标为代码简单、易用、齐全,处理部分移动端bug,涵盖机型广,大大减少开发交互型组件的工作量的移动端框架。

Demo

Demo

CORE&&API

jmui

js模块,jmui基础工具函数集合

语法:jmui.方法名

参数名称 参数类型 参数说明
vision String 版本号
timestamp Long 时间戳
方法名 使用说明
lazyLoad 图片懒加载,返回Booleantrue表示全部加载完成,false表示没有全部加载完成 Demo
requestAnimationFrame 对于现代浏览器使用requestAnimationFrame代替setTimeOutDemo Demo
tween 将缓动曲线集合添加至Math.TweenDemo
pageLock 锁定页面
pageUnlock 解锁页面
getUrlString 获取Url参数值 语法:jmui.getUrlString(name)参数类型:{String}
setCookie 将值存入cookie 语法:jmui.setCookie(c_name,value,expiredays) 参数说明:名称,键值,存在周期(可选)参数类型:{String}
getCookie 从cookie中取值语法:jmui.getCookie(c_name) 参数类型:{0String}
getBrowserInfo 获取浏览器UA 判断环境,返回object,{object}.in环境={boolean} 参数:inIos、inWx、inApp、inJdApp、inJrApp、inWyApp
addJrBanner 添加金融Banner,可进行金融APP下载语法:jmui.addJrBanner(imgSrc,id,saveDays) 参数说明:bannerUrl,下载id(默认70),存在周期(默认1天)参数类型:{String}、{Int}、{Int}
formatTime 时间格式化语法:jmui.formatTime(time,'yyyy-MM-dd HH:mm:ss') 参数说明:时间戳,输入格式参数类型:{Long}、{String}注:yyyy表示年,MM表示月,dd表示日,HH表示小时,mm表示分,ss表示秒(参数名不允许更改,格式可自定义)
createDiv 创建DIV,返回Object。语法:jmui.createDiv(className,innerHTML) 参数说明:标签class名,html内容参数类型:{String}
fixTips 固定提示,返回Object。语法:jmui.fixTips({text:"固定提示框",pos:"top",autoClose:false}) 参数说明:html内容,位置,自动隐藏参数类型:{String}、{top/mid}、{Boolean}
validate 验证器集合,返回Boolean。语法:jmui.validate.方法名(值)方法:checkTel(value) 验证手机号checkEmail(value) 验证邮箱地址checkPicture(value) 验证图片格式checkRar(value) 验证压缩格式checkIDCard(value) 验证身份证checkQQ(value) 验证QQ号checkPassWord(value) 验证密码 字母开头,长度在6~20之间,只能包含字母、数字和下划线checkCreditCard(value) 验证信用卡checkBankCard(value) 验证银联卡checkVisaCard(value) 验证Visa卡checkMasterCard(value) 验证万事达卡checkLoginName(value) 验证登录名checkTrueName(value) 验证真实姓名 考虑到外国人名 xx·XXXcheckChinese(value) 验证中文

jmuiForZepto

语法:$("选择器").方法名

方法名 使用说明
createAppDownload 将一个标签按钮转化为可检测安装与否的下载链接语法:$("选择器").addJrBanner(id,appSrc)参数说明:下载id(默认70),app链接(默认'jdmobild://')参数类型:{Int}、{String}
createBackTop 将元素变为回到顶部按钮语法:$("选择器").createBackTop(type)参数说明:scroll平滑滚动、static瞬间制定参数类型:{String}
useNineKeyboard input调用九宫键盘 兼容Android IOS语法:$("选择器").useNineKeyboard()
setOnlyRead input设置为已读语法:$("选择器").setOnlyRead()

部分文档

addrPicker

js模块,地址选择器

new AddrPicker({config});

参数名称 参数类型 参数说明
selecteBtn Object 必选,触发选择按钮
addrPickerBox Object 必选,地址选择的容器
moveBox Object 必选,可滑动区域容器
moveObj Object 必选,可滑动的对象
addrInput Object 必选,输入内容区域
lineHeight Int 必选,每个滚动项高度
btnSure Object 必选,确定按钮
btnCancel Object 必选,取消按钮
sureFn Function 可选,确定回调函数
cancelFn Function 可选,取消回调函数
ajaxUrl String 可选,数据Url
new addrPicker({
                selecteBtn: $("#btnChooseAddr"),
                addrPickerBox: $("#addrPicker"),
                moveBox: $(".picker-item"),
                moveObj: $(".picker-ul"),
                btnSure: $("#pickerSure"),
                btnCancel: $("#pickerCancel"),
                addrInput: $("#addrInput"),
                lineHeight: 40,
                ajaxUrl: {"province": "../data/provinces.json", "city": "../data/citys.json", "area": "../data/areas.json"},
                sureFn: function(id,text) {
                    $("#addrProvince").val(id[0] + "," + text[0])
                    $("#addrCity").val(id[1] + "," + text[1])
                    $("#addrArea").val(id[2] + "," + text[2])
                }
            });

Demo实例

countDown

countDown

countDown

参数名称 参数类型 参数说明
$day Object 必选,显示天的对象
$hour Object 必选,显示小时的对象
$min Object 必选,显示分的对象
$second Object 可选,显示秒的对象
totalSecond Int 必选,倒计时总时间
timeCent Int 可选,倒计时单位
endFn Function 可选,结束回调函数
new CountDown({
                $day: $(".day"),
                $hour: $(".hour"),
                $min: $(".min"),
                $second: $(".second"),
                totalSecond: 10,
                endFn: function () {
                    alert("终于等到你,还好我没放弃---------然并卵");
                }
            });

Demo实例

dropElement

js模块,元素飘落插件

语法:new DropElement({config});

参数名称 参数类型 参数说明
sizeArr Array 必选,掉落元素的尺寸数组(二维)
count Int 可选,同时出现的最大数量
during Int 可选,完成一次掉落的周期
splitTime Int 可选,每个元素出现的时间间隔
width Int 可选,可掉落的区域宽度
loop Boolean 可选,是否循环掉落
new DropElement({
                sizeArr: [[10, 24], [13, 15], [16, 19], [12, 14], [10, 15]],
                count: 15,
                during: 3000,
                splitTime: 300,
                loop: true
            });

Demo实例

iscroll

js模块,web最流畅的滚动插件

语法:new IScroll("#wrapper", {config});

查看参数

new IScroll("#wrapper", {mouseWheel: true, probeType: 3});

Demo实例

md5

js模块,MD5转换插件

语法:new MD5({config});

参数名称 参数类型 参数说明
$btn Object 必选,转换触发按钮
str String 必选,需要转换的字符串
endFn Function 可选,转换完成回调函数
new MD5({
                $btn:$(".jmui-md5-btnTransform"),
                str:$(".jmui-md5-str").text(),
                endFn: function (str) {
                    $(".jmui-md5-str").text(str);
                }
            })

Demo实例

preLoad

js模块,预加载插件

语法:new PreLoad({config});

参数名称 参数类型 参数说明
tasks Array 必选,存放预加载资源Url的数组
finishedFn Function 必选,完成加载的回调函数
prefix String 可选,输出的前缀
new PreLoad({
                tasks:[
                    "1.png",
                    "2.png",
                    "3.png"
                ],
                finishedFn:function(total){
                    console.log("已经加载完成了,共加载"+total+"个资源");
                }
            });

Demo实例

qrcode

js模块,二维码插件

语法:new QRCode(object,{config});

参数名称 参数类型 参数说明
object Object 必填,二维码的容器
text * 必选,二维码内容(支持 url 文本 电子邮件 短消息 电话 vCard 地理位置 二级制压缩文件)
width Int 可选,二维码宽度
height Int 可选,二维码高度
colorDark String 可选,二维码颜色
colorLight Int 可选,二维码底色
correctLevel 可选,二维码容错等级

查看参数

new QRCode(document.getElementById("qrcode"), {
                text: "http://jr.jd.com?" + Math.random(),
                width: 61,
                height: 61,
                colorDark: "#000000",
                colorLight: "#ffffff"
            })

Demo实例

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

[译文]如果你是一个前端开发者,那么2017年你该学习什么呢?

首发于微信公众号《前端成长记》,写于 2017.01.04

随着前端生态系统快节奏的发展,我们更倾向于把时间花在尝试一些最新的技术以及网上争论这些技术上。我不是说我们不应该这样做,但或许我们应该慢下来一点,把精力放到那些不变的事情上:它们可以很大程度上提高我们的工作的质量和价值,以及更好的理解那些新技术。

这篇文章是结合了我个人的经验以及个人的期望。分享我的同时,我也希望能够听听你们的想法。

  1. 学习如何编写具有可读性的代码

我们的大部分工作不是在编写新代码,而是修改现有代码。这意味着相比于写代码,你更多的是阅读代码,所以优化你的代码是为了下一个阅读的程序员,而不是为了注释。

我推荐阅读这三本令人叹为观止的书 - 由短到长排序:

  1. 深入学习JavaScript

当每周都有一个比任何旧框架都要优秀的新JavaScript框架时,我们很容易把大部分时间花在学习框架而不是JavaScript本身上。如果你正在使用是一个你并不明白它实现原理的框架,请暂时停下吧,深入学习JavaScript本身,直到你明白你使用的框架的实现原理。

  1. 学习函数式编程

多年来,我们一直都想要JavaScript中出现类。现在我们终于有了它们,但却不想使用它们:函数式才是我们想要的!我们甚至通过JSX编写HTML。

  1. 学习设计基础

作为前端开发人员,我们比团队中的任何人都更接近用户,甚至可能比设计师还要更接近用户。如果必须要设计师来验证你在屏幕上展示的每个像素,那么你可能做了一些错误的事情。

  1. 了解如何与人合作

我们有些人编程,是因为我们更喜欢与计算机交互而不是与人 - 不幸的是,这不是它的工作原理。

我们很少孤立工作:我们必须与其他开发人员,设计师,经理,有时甚至与用户交谈。虽然这很难,但对于你想真正了解你在做什么以及为什么要这么做来说,这一切是非常重要的。因为这是我们做什么的价值所在。

  1. 学习如何通过文本与人沟通

我们与同事和其他人交流的时候大部分都是使用文本:任务描述和评论,代码评论,Git提交,聊天消息,电子邮件,微博,博客帖子等。

想象一下,人们需要花多少时间去阅读和理解这些。如果你可以通过写得更清楚和简洁去减少这个时间,世界将变得更适宜工作。

  1. 学习经典的计算机科学知识

前端开发一点也不像是动画下拉菜单。它比以前更复杂,而那个臭名昭着的“JavaScript疲劳”的一部分源于我们必须解决的任务复杂性的不断提高。

然而,这意味着,是时候学习非前端开发几十年建立起来的全部知识理论。在这里,我想要让你多推荐一些。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

Nodejs做接口开发架构经验总结

首发于微信公众号《前端成长记》,写于 2018.09.05

  1. API设计风格

如果是以操作为主,选择 RPC,风格类似 /getUsers;如果是以资源指向为主,选择 Restful ,风格类似 /users。

  1. API版本设计

请务必带上版本进行设计,否则改动将造成不可预计的后果。

  1. 全局拦截器

比如我分成 stable 和 v1 和 v2 三个版本的接口, stable 是稳定对外输出接口,v1 和 v2 对应不同版本的业务接口。

假设业务接口需要增加登录拦截,那么在 express 中使用的话,像这样就可以:

let app = express()
// 全局控制,看需求也可以拆分成 /v1 和 /v2,进行针对性处理
app.use('/', (req, res, next) => {
    if (/^\/api\/stable\//g.test(req.url)) {    // stable api ,不需要登录拦截
        next()
    } else {
        // 校验登陆
        if (isLogin) {
            next()
        } else {
            // 未登录处理逻辑
            toLogin()
        }
    }
})
  1. 全局拦截后,需要保留登录状态

可以将登录信息写入 req。

如果使用了 multer 处理 fromData 类型上传,千万不要将信息写在 req.body 中,因为 multer 会处理 req ,非 req 自身 body 字段将被过滤。比如 req.body._userInfo = {name: 'McChen'} ,使用 multer 发现 req.body = {}.

  1. 处理 FormData 上传

使用 multer 中间件,可以自定义过滤,自定义存储等。

  1. 文件上传接口处理响应方式

分为两种情况:

  • 数据库数据插入成功就返回成功响应,后台继续进行上传操作,但有可能上传失败。优势是响应时间短,无法保证100%上传成功。
  • 全部上传完成后才返回成功响应。优势是可以保证100%上传成功,但是响应时间会很慢,尤其是多个大文件上传时。PC网线直连测试1.7M单文件耗时约2秒。

目前采用以成功率为唯一保障,多文件并行上传的方式。

  1. sort导致查询速度明显减慢

根据sort条件建立合理的索引值 ensureIndex

  1. aggrate聚合查询慢

目前没找到合适的解决方案,只在match和sort做了优化,该lookup的还得lookup

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

document.body.scrollTop等常见易混淆属性整理

首发于微信公众号《前端成长记》,写于 2015.09.01

介绍

Javascript中有一些常见易混淆的属性,下面我做了一份大概的整理。

混淆属性

document.body.clientWidth 网页可见区域宽

document.body.offsetWidth 网页可见区域宽(包括边线的宽)

document.body.offsetHieght 网页可见区域高(包括边线的高)

document.body.scrollWidth 网页正文全文宽

document.body.scrollHieght 网页正文全文高

document.body.scrollTop 网页被卷去的高

document.body.scrollLeft 网页被卷去的左

window.screenTop 网页正文部分上

window.screenLeft 网页正文部分左

window.screen.height 屏幕分辨率的高

window.screen.width 屏幕分辨率的宽

window.screen.availHeight 屏幕可用工作区高度

window.screen.availWidth 屏幕可用工作区宽度

scrollHeight 获取对象的滚动高度

scrollLeft 设置或获取位于对象左边界和窗口中目前可见内容的最左端之间的距离

scrollTop 设置或获取位于对象最顶端和窗口中可见内容的最顶端之间的距离

scrollWidth 获取对象的滚动宽度

offsetHeight 获取对象相对于版面或由父坐标 offsetParent 属性指定的父坐标的高度

offsetLeft 获取对象相对于版面或由 offsetParent 属性指定的父坐标的计算左侧位置

offsetTop 获取对象相对于版面或由 offsetTop 属性指定的父坐标的计算顶端位置

event.clientX 相对文档的水平坐标

event.clientY 相对文档的垂直坐标

event.offsetX 相对容器的水平坐标

event.offsetY 相对容器的垂直坐标

document.documentElement.scrollTop 垂直方向滚动的值

event.clientX+document.documentElement.scrollTop 相对文档的水平座标+垂直方向滚动的量

Tips
要获取当前页面的滚动条纵坐标位置,用:document.documentElement.scrollTop;
而不是:document.body.scrollTop;
documentElement 对应的是 html 标签,而 body 对应的是 body 标签

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

从0开始配置带权限的Mongodb

首发于微信公众号《前端成长记》,写于 2018.07.31

背景

由于数据安全问题,数据库加权限校验是必不可少的一部分。

跟传统数据库mySQL等不同的是,由于mongodb的用户名和密码是基于特定数据库的,而不是基于整个系统,所以需要对所有db进行密码设置。

步骤

前置准备

安装 mongodb 下载传送门

配置和启动数据库

打开CMD,执行下面操作

// 在安装目录的 bin 文件夹下执行// linux 需要 ./mongodb , MacOS 需要 sudo ./mongodbmongodb --dbpath 你的数据库文件夹路径

到这,能看到数据库已经成功连接。接下来,另外启动一个CMD,进行数据库操作。

// linux 需要 ./mongo , MacOS 需要 sudo ./mongo
mongo

此时可以看见 connecting to: test,表明成功。

进行用户和权限配置

  1. 首先进入 admin 数据库
use admin
  1. 创建管理员账户
db.createUser({user: "myuser", pwd: "mypwd", roles: [{role: "userAdminAnyDatabase", db: "admin"}]})

roles可选列表

  1. 验证是否成功
db.auth("myuser", "mypwd")

如果返回1,则为配置成功

重启数据库,开启授权模式

// 在安装目录的 bin 文件夹下执行
// linux 需要 ./mongodb , MacOS 需要 sudo ./mongodb
mongodb --dbpath 你的数据库文件夹路径 --auth

进行验证

show dbs

命令意思为查看数据库列表,但是因为没有经过校验,所以会报错如下:

image

auth校验

切换到admin,进行auth操作

use admin
db.auth("myuser", "mypwd")

如果返回1,则表明auth通过,此时再进行上一步的验证操作,将成功返回数据库列表。

给单独数据库配置不同权限账户

use yourdb
db.createUser({user: "youruser", pwd: "yourpwd", roles: [{role: "dbOwner", db: "yourdb"}]})

上述配置了该数据库的所有者,有该数据库的最高权限。

use yourdb
db.createUser({user: "youruser1", pwd: "yourpwd1", roles: [{role: "readWrite", db: "yourdb"}]})

上述配置了该用户对于数据库只有可读权限。

roles可选列表

结尾

至此,一个带密码校验的mongodb配置已经全部完成。Try it by yourself!

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

Vue 单元测试

首发于微信公众号《前端成长记》,写于 2018.05.31

背景

没有测试用例的代码库都是不完整的,使用者也会诚惶诚恐。所以本文简单介绍一下如何对 Vue 文件进行单元测试。

测试步骤

  1. 通过 vue-cli 脚手架构架项目,选中 karma && Mocha 后会自动初始化一个 Helloworld.spec.js 的测试用例并安装对应所需依赖(这步也可以手动安装依赖)

  2. 修改 HelloWorld.vue 文件

<template>
  <div>
    <h3>Helloworld</h3>
    <p class="num">{{ num }}</p>
    <button @click="add">增加</button>
  </div>
</template>
<script>
  export default {
    data () {
      return {
        num : 0
      }
    },
 
    methods: {
      add() {
        this.num++
      }
    }
  }
</script>
  1. 修改 HelloWorld.spec.js
import Vue from 'vue'
import HelloWorld from '@/components/HelloWorld'
describe('HelloWorld.vue', () => {
  it('h3的初始值应为HelloWorld', () => {
    const Constructor = Vue.extend(HelloWorld)
    const vm = new Constructor().$mount()
    expect(vm.$el.querySelector('h3').textContent).to.equal('HelloWorld')
  })
  it('点击按钮后,num应为1', () => {
    const Constructor = Vue.extend(HelloWorld)
    const vm = new Constructor().$mount()
    const button = vm.$el.querySelector('button')
    const clickEvent = new window.Event('click')
    button.dispatchEvent(clickEvent)
    vm._watcher.run()
    expect(vm.$el.querySelector('.num').textContent).to.equal(1)
  })
})

当然,你也可以选择使用 vue-test-utils 来缩减代码量

npm install vue-test-utils --save-dev

然后上面的代码可以修改为

import { mount } from 'vue-test-utils'
import { expect } from 'chai'
import HelloWorld from '@/components/HelloWorld'
describe('HelloWorld.vue', () => {
  it('h3的初始值应为HelloWorld', () => {
    const wrapper = mount(HelloWorld)
    expect(wrapper.find('h3').text()).to.equal('HelloWorld')
  })
  it('点击按钮后,num应为1', () => {
    const wrapper = mount(HelloWorld)
    const button = wrapper.find('button')
    // 点击按钮修改
    button.trigger(click)
    // 或者直接修改数据
    // wrapper,setData({num: 1})
    expect(wrapper.find('.num').text()).to.equal(1)
  })
})
  1. 执行测试命令
npm run test

结尾

至此,一个简单的 Vue 组件单元测试就完成了。

代码库有较为完整的测试用例也会更加的可靠,你们觉得呢?

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

【Leetcode 做题学算法周刊】第四期

首发于微信公众号《前端成长记》,写于 2019.11.21

背景

本文记录刷题过程中的整个思考过程,以供参考。主要内容涵盖:

  • 题目分析设想
  • 编写代码验证
  • 查阅他人解法
  • 思考总结

目录

Easy

67.二进制求和

题目地址

题目描述

给定两个二进制字符串,返回他们的和(用二进制表示)。

输入为非空字符串且只包含数字 10

示例:

输入: a = "11", b = "1"
输出: "100"

输入: a = "1010", b = "1011"
输出: "10101"

题目分析设想

这道题又是一道加法题,所以记住下,直接转数字进行加法可能会溢出,所以不可取。所以我们需要遍历每一位来做解答。我这有两个大方向:补0后遍历,和不补0遍历。但是基本的依据都是本位相加,逢2进1即可,类似手写10进制加法。

  • 补0后遍历,可以采用先算出的位数推入数组最后反转,也可以采用先算出的位数填到对应位置后直接输出
  • 不补0遍历,根据短数组的长度进行遍历,长数组剩下的数字与短数组生成的进位进行计算

查阅他人解法

Ⅰ.补0后遍历,先算先推

代码:

/**
 * @param {string} a
 * @param {string} b
 * @return {string}
 */
var addBinary = function(a, b) {
    let times = Math.max(a.length, b.length) // 需要遍历次数
    // 补 0
    while(a.length < times) {
        a = '0' + a
    }
    while(b.length < times) {
        b = '0' + b
    }
    let res = []
    let carry = 0 // 是否进位
    for(let i = times - 1; i >= 0; i--) {
        const num = carry + (a.charAt(i) | 0) + (b.charAt(i) | 0)
        carry = num >= 2 ? 1 : 0
        res.push(num % 2)
    }
    if (carry === 1) {
        res.push(1)
    }
    return res.reverse().join('')
};

结果:

  • 294/294 cases passed (68 ms)
  • Your runtime beats 95.13 % of javascript submissions
  • Your memory usage beats 72.58 % of javascript submissions (35.4 MB)
  • 时间复杂度 O(n)

Ⅱ.补0后遍历,按位运算

代码:

/**
 * @param {string} a
 * @param {string} b
 * @return {string}
 */
var addBinary = function(a, b) {
    let times = Math.max(a.length, b.length) // 需要遍历次数
    // 补 0
    while(a.length < times) {
        a = '0' + a
    }
    while(b.length < times) {
        b = '0' + b
    }
    let res = []
    let carry = 0 // 是否进位
    for(let i = times - 1; i >= 0; i--) {
        res[i] = carry + (a.charAt(i) | 0) + (b.charAt(i) | 0)
        carry = res[i] >= 2 ? 1 : 0
        res[i] %= 2
    }
    if (carry === 1) {
        res.unshift(1)
    }
    return res.join('')
};

结果:

  • 294/294 cases passed (60 ms)
  • Your runtime beats 99.65 % of javascript submissions
  • Your memory usage beats 65.82 % of javascript submissions (35.5 MB)
  • 时间复杂度 O(n)

Ⅲ.不补0遍历

当然处理方式还是可以选择上面两种,我这就采用先算先推来处理了。

代码:

/**
 * @param {string} a
 * @param {string} b
 * @return {string}
 */
var addBinary = function(a, b) {
    let max = Math.max(a.length, b.length) // 最大长度
    let min = Math.min(a.length, b.length) // 最大公共长度

    // 将长字符串拆成两部分
    let left = a.length > b.length ? a.substr(0, a.length - b.length) : b.substr(0, b.length - a.length)
    let right = a.length > b.length ? a.substr(a.length - b.length) : b.substr(b.length - a.length)

    // 公共长度部分遍历
    let rightRes = []
    let carry = 0
    for(let i = min - 1; i >= 0; i--) {
        const num = carry + (right.charAt(i) | 0) + (((a.length > b.length ? b : a)).charAt(i) | 0)
        carry = num >= 2 ? 1 : 0
        rightRes.push(num % 2)
    }

    let leftRes = []
    for(let j = max - min - 1; j >= 0; j--) {
        const num = carry + (left.charAt(j) | 0)
        carry = num >= 2 ? 1 : 0
        leftRes.push(num % 2)
    }

    if (carry === 1) {
        leftRes.push(1)
    }
    return leftRes.reverse().join('') + rightRes.reverse().join('')
};

结果:

  • 294/294 cases passed (76 ms)
  • Your runtime beats 80.74 % of javascript submissions
  • Your memory usage beats 24.48 % of javascript submissions (36.2 MB)
  • 时间复杂度 O(n)

查阅他人解法

看到一些细节上的区别,我这使用 '1' | 0 来转数字,有的使用 ''1' - '0''。另外还有就是初始化结果数组长度为最大长度加1后,最后判断首位是否为0需要剔除的,我这使用的是判断最后是否还要进位补1。

这里还看到用一个提案中的 BigInt 类型来解决的

Ⅰ.BigInt

代码:

/**
 * @param {string} a
 * @param {string} b
 * @return {string}
 */
var addBinary = function(a, b) {
    return (BigInt("0b"+a) + BigInt("0b"+b)).toString(2);
};

结果:

  • 294/294 cases passed (52 ms)
  • Your runtime beats 100 % of javascript submissions
  • Your memory usage beats 97.05 % of javascript submissions (34.1 MB)
  • 时间复杂度 O(1)

思考总结

通过 BigInt 的方案我们能看到,使用原生方法确实性能更优。简单说一下这个类型,目前还在提案阶段,看下面的等式基本就能知道实现原理自己写对应 Hack 来实现了:

BigInt(10) = '10n'
BigInt(20) = '20n'
BigInt(10) + BigInt(20) = '30n'

虽然这种方式很友好,但是还是希望看到加法题的时候,能考虑到遍历按位处理。

69.x的平方根

题目地址

题目描述

实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

示例:

输入: 4
输出: 2

输入: 8
输出: 2
说明: 8 的平方根是 2.82842...,
     由于返回类型是整数,小数部分将被舍去。

题目分析设想

同样,这里类库提供的方法 Math.sqrt(x) 就不说了,这也不是本题想考察的意义。所以这里有几种方式:

  • 暴力法,这里不用考虑溢出是因为x没溢出,所以即使加到平方根加1,也会终止循环
  • 二分法,直接取中位数运算,可以快速排除当前区域一半的区间

编写代码验证

Ⅰ.暴力法

代码:

/**
 * @param {number} x
 * @return {number}
 */
var mySqrt = function(x) {
    if (x === 0) return 0
    let i = 1
    while(i * i < x) {
        i++
    }
    return i * i === x ? i : i - 1
};

结果:

  • 1017/1017 cases passed (120 ms)
  • Your runtime beats 23 % of javascript submissions
  • Your memory usage beats 34.23 % of javascript submissions (35.7 MB)
  • 时间复杂度 O(n)

Ⅱ.二分法

代码:

/**
 * @param {number} x
 * @return {number}
 */
var mySqrt = function(x) {
    if (x === 0) return 0
    let l = 1
    let r = x >>> 1
    while(l < r) {
        // 这里要用大于判断,所以取右中位数
        const mid = (l + r + 1) >>> 1

        if (mid * mid > x) {
            r = mid - 1
        } else {
            l = mid
        }
    }
    return l
};

结果:

  • 1017/1017 cases passed (76 ms)
  • Your runtime beats 96.08 % of javascript submissions
  • Your memory usage beats 59.17 % of javascript submissions (35.5 MB)
  • 时间复杂度 O(log2(n))

查阅他人解法

这里看见了两个有意思的解法:

  • 2的幂次底层优化
  • 牛顿法

Ⅰ.幂次优化

稍微解释一下,二分法需要做乘法运算,他这里改用加减法

/**
 * @param {number} x
 * @return {number}
 */
var mySqrt = function(x) {
    let l = 0
    let r = 1 << 16 // 2的16次方,这里我猜是因为上限2^32所以取一半
    while (l < r - 1) {
        const mid = (l + r) >>> 1
        if (mid * mid <= x) {
            l = mid
        } else {
            r = mid
        }
    }
    return l
};

结果:

1017/1017 cases passed (72 ms)
Your runtime beats 98.46 % of javascript submissions
Your memory usage beats 70.66 % of javascript submissions (35.4 MB)

  • 时间复杂度 O(log2(n))

Ⅱ.牛顿法

算法说明:

在迭代过程中,以直线代替曲线,用一阶泰勒展式(即在当前点的切线)代替原曲线,求直线与 xx 轴的交点,重复这个过程直到收敛。

首先随便猜一个近似值 x,然后不断令 x 等于 xa/x 的平均数,迭代个六七次后 x 的值就已经相当精确了。

公式可以写为 X[n+1]=(X[n]+a/X[n])/2

代码:

/**
 * @param {number} x
 * @return {number}
 */
var mySqrt = function(x) {
    if (x === 0 || x === 1) return x

    let a = x >>> 1
    while(true) {
        let cur = a
        a = (a + x / a) / 2
        // 这里是为了消除浮点运算的误差,1e-5是我试出来的
        if (Math.abs(a - cur) < 1e-5) {
            return parseInt(cur)
        }
    }
};

结果:

  • 1017/1017 cases passed (68 ms)
  • Your runtime beats 99.23 % of javascript submissions
  • Your memory usage beats 9.05 % of javascript submissions (36.1 MB)
  • 时间复杂度 O(log2(n))

思考总结

这里就提一下新接触的牛顿法吧,实际上是牛顿迭代法,主要是迭代操作。由于在单根附近具有平方收敛,所以可以转换成线性问题去求平方根的近似值。主要应用场景有这两个方向:

  • 求方程的根
  • 求解最优化问题

70.爬楼梯

题目地址

题目描述

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例:

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1  + 1 
2.  2 

输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1.  1  + 1  + 1 
2.  1  + 2 
3.  2  + 1 

题目分析设想

这道题很明显可以用动态规划和斐波那契数列来求解。然后我们来看看其他正常思路,如果使用暴力法的话,那么复杂度将会是 2^n,很容易溢出,但是如果能够优化成 n 的话,其实还可以求解的。所以这道题我就从以下三个方向来作答:

  • 哈希递归,也就是暴力运算的改进版,通过存下算过的值降低复杂度
  • 动态规划
  • 斐波那契数列

编写代码验证

Ⅰ.哈希递归

代码:

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    let hash = {}
    return count(0)
    function count (i) {
        if (i > n) return 0
        if (i === n) return 1

        // 这步节省运算
        if(hash[i] > 0) {
            return hash[i]
        }

        hash[i] = count(i + 1) + count(i + 2)
        return hash[i]
    }
};

结果:

  • 45/45 cases passed (52 ms)
  • Your runtime beats 98.67 % of javascript submissions
  • Your memory usage beats 48.29 % of javascript submissions (33.7 MB)
  • 时间复杂度 O(n)

Ⅱ.动态规划

代码:

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    if (n === 1) return 1
    if (n === 2) return 2
    // dp[0] 多一位空间,省的后面做减法
    let dp = new Array(n + 1).fill(0)
    dp[1] = 1
    dp[2] = 2
    for(let i = 3; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2]
    }
    return dp[n]
};

结果:

  • 45/45 cases passed (48 ms)
  • Your runtime beats 99.48 % of javascript submissions
  • Your memory usage beats 21.49 % of javascript submissions (33.8 MB)
  • 时间复杂度 O(n)

Ⅲ.斐波那契数列

其实斐波那契数列就可以用动态规划来实现,所以下面的代码思路很相似。

代码:

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    if (n === 1) return 1
    if (n === 2) return 2
    let num1 = 1
    let num2 = 2
    for(let i = 3; i <= n; i++) {
        let count = num1 + num2
        num1 = num2
        num2 = count
    }
    // 相当于fib(n)
    return num2
};

结果:

  • 45/45 cases passed (56 ms)
  • Your runtime beats 95.49 % of javascript submissions
  • Your memory usage beats 46.1 % of javascript submissions (33.7 MB)
  • 时间复杂度 O(n)

查阅他人解法

查看题解发现这么几种解法:

  • 斐波那契公式(原来有计算公式可以直接用,尴尬)
  • Binets 方法
  • 排列组合

Ⅰ.斐波那契公式

代码:

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    const sqrt_5 = Math.sqrt(5)
    // 由于 F0 = 1,所以相当于需要求 n+1 的值
    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)
};

结果:

  • 45/45 cases passed (52 ms)
  • Your runtime beats 98.67 % of javascript submissions
  • Your memory usage beats 54.98 % of javascript submissions (33.6 MB)
  • 时间复杂度 O(log(n))

Ⅱ.Binets 方法

算法说明:

使用矩阵乘法来得到第 n 个斐波那契数。注意需要将初始项从 fib(2)=2,fib(1)=1 改成 fib(2)=1,fib(1)=0 ,来达到矩阵等式的左右相等。

解法参考官方题解

代码:

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {

    function pow(a, n) {
        let ret = [[1,0],[0,1]] // 矩阵
        while(n > 0) {
            if ((n & 1) === 1) {
                ret = multiply(ret, a)
            }
            n >> 1
            a = multiply(a, a)
        }
        return ret;
    }
    function multiply(a, b) {
        let c = [[0,0], [0,0]]
        for (let i = 0; i < 2; i++) {
            for(let j = 0; j < 2; j++) {
                c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j]
            }
        }
        return c
    }

    let q = [[1,1], [1, 0]]
    let res = pow(q, n)
    return res[0][0]
};

结果:

测试用例可以输出,提交发现超时。

这个笔者还没完全理解,所以很抱歉,暂时没有 js 相应代码分析,后续会补上。也欢迎您补充给我,感谢!

Ⅲ.排列组合

代码:

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    // n 个台阶走 i 次1阶和 j 次2阶走到,推导出 i + 2*j = n
    function combine(m, n) {
        if (m < n) [m, n] = [n, m];
        let count = 1;
        for (let i = m + n, j = 1; i > m; i--) {
            count *= i;
            if (j <= n) count /= j++;
        }
        return count;
    }
    let total = 0;
    // 取出所有满足条件的解
    for (let i = 0,j = n; j >= 0; j -= 2, i++) {
      total += combine(i, j);
    }
    return total;
};

结果:

  • 45/45 cases passed (60 ms)
  • Your runtime beats 87.94 % of javascript submissions
  • Your memory usage beats 20.72 % of javascript submissions (33.8 MB)
  • 时间复杂度 O(n^2)

思考总结

这种叠加的问题,首先就会想到动态规划的解法,刚好这里又满足斐波那契数列,所以我是推荐首选这两种解法。另外通过查看他人解法学到了斐波那契公式,以及站在排列组合的角度去解,开拓了思路。

83.删除排序链表中的重复元素

题目地址

题目描述

给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。

示例:

输入: 1->1->2
输出: 1->2

输入: 1->1->2->3->3
输出: 1->2->3

题目分析设想

注意一下,给定的是一个排序链表,所以只需要依次更改指针就可以直接得出结果。当然,也可以使用双指针来跳过重复项即可。所以这里有两个方向:

  • 直接运算,通过改变指针指向
  • 双指针,通过跳过重复项

如果是无序链表,我会建议先得到所有值然后去重后(比如通过Set)生成新链表作答。

编写代码验证

Ⅰ.直接运算

代码:

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {
    // 复制一个用做操作,由于对象是传址,所以改指针指向即可
    let cur = head
    while(cur !== null && cur.next !== null) {
        if (cur.val === cur.next.val) { // 值相等
            cur.next = cur.next.next
        } else {
            cur = cur.next
        }
    }
    return head
};

结果:

  • 165/165 cases passed (76 ms)
  • Your runtime beats 87.47 % of javascript submissions
  • Your memory usage beats 81.21 % of javascript submissions (35.5 MB)
  • 时间复杂度 O(n)

Ⅱ.双指针法

代码:

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {
    // 新建哨兵指针和当前遍历指针
    if (head === null || head.next === null) return head
    let pre = head
    let cur = head
    while(cur !== null) {
        debugger
        if (cur.val === pre.val) {
            // 当前指针移动
            cur = cur.next
        } else {
            pre.next = cur
            pre = cur
        }
    }
    // 最后一项如果重复需要把head.next指向null
    pre.next = null
    return head
};

结果:

  • 165/165 cases passed (80 ms)
  • Your runtime beats 77.31 % of javascript submissions
  • Your memory usage beats 65.1 % of javascript submissions (35.7 MB)
  • 时间复杂度 O(n)

查阅他人解法

忘记了,这里确实还可以使用递归来作答。

Ⅰ.递归法

代码:

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {
    if(head === null || head.next === null) return head
    if (head.val === head.next.val) { // 值相等
        return deleteDuplicates(head.next)
    } else {
        head.next = deleteDuplicates(head.next)
    }
    return head
};

结果:

  • 165/165 cases passed (80 ms)
  • Your runtime beats 77.31 % of javascript submissions
  • Your memory usage beats 81.21 % of javascript submissions (35.5 MB)
  • 时间复杂度 O(n)

思考总结

关于链表的题目一般都是通过修改指针指向来作答,区分单指针和双指针法。另外,遍历也是可以实现的。

88.合并两个有序数组

题目地址

题目描述

给定两个有序整数数组 nums1nums2,将 nums2 合并到 nums1 中,使得 num1 成为一个有序数组。

说明:

  • 初始化 nums1nums2 的元素数量分别为 mn
  • 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。

示例:

输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6],       n = 3

输出: [1,2,2,3,5,6]

题目分析设想

之前我们做过删除排序数组中的重复项,其实这里也类似。可以从这几个方向作答:

  • 数组合并后排序
  • 遍历数组并进行插入
  • 双指针法,轮流比较

但是由于题目有限定空间都在 nums1 ,并且不要写 return ,直接在 nums1 上修改,所以我这里主要的思路就是遍历,通过 splice 来修改数组。区别就在于遍历的方式方法。

  • 从前往后
  • 从后往前
  • 合并后排序再赋值

编写代码验证

Ⅰ.从前往后

代码:

/**
 * @param {number[]} nums1
 * @param {number} m
 * @param {number[]} nums2
 * @param {number} n
 * @return {void} Do not return anything, modify nums1 in-place instead.
 */
var merge = function(nums1, m, nums2, n) {
    // 两个数组对应指针
    let p1 = 0
    let p2 = 0
    // 这里需要提前把nums1的元素拷贝出来,要不然比较赋值后就丢失了
    let cpArr = nums1.splice(0, m)

    // 数组指针
    let p = 0
    while(p1 < m && p2 < n) {
        // 先赋值,再进行+1操作
        nums1[p++] = cpArr[p1] < nums2[p2] ? cpArr[p1++] : nums2[p2++]
    }
    // 已经有p个元素了,多余的元素要删除,剩余的要加上
    if (p1 < m) {
        // 剩余元素,p1 + m + n - p = m + n - (p - p1) = m + n - p2
        nums1.splice(p, m + n - p, ...cpArr.slice(p1, m + n - p2))
    }
    if (p2 < n) {
        // 剩余元素,p2 + m + n - p = m + n - (p - p2) = m + n - p1
        nums1.splice(p, m + n - p, ...nums2.slice(p2, m + n - p1))
    }
};

结果:

  • 59/59 cases passed (48 ms)
  • Your runtime beats 100 % of javascript submissions
  • Your memory usage beats 64.97 % of javascript submissions (33.8 MB)
  • 时间复杂度 O(m + n)

Ⅱ.从后往前

代码:

/**
 * @param {number[]} nums1
 * @param {number} m
 * @param {number[]} nums2
 * @param {number} n
 * @return {void} Do not return anything, modify nums1 in-place instead.
 */
var merge = function(nums1, m, nums2, n) {
    // 避免 nums1 = [0,0,0,0], nums2 = [1,2] 这种 nums1.length > nums2.length 并且 m = 0
    nums1.splice(m, nums1.length - m)
    // 两个数组对应指针
    let p1 = m - 1
    let p2 = n - 1
    // 数组指针
    let p = m + n - 1
    while(p1 >= 0 && p2 >= 0) {
        // 先赋值,再进行-1操作
        nums1[p--] = nums1[p1] < nums2[p2] ? nums2[p2--] : nums1[p1--]
    }
    // 可能nums2有剩余,由于指针是下标,所以截取数量需要加1
    nums1.splice(0, p2 + 1, ...nums2.slice(0, p2 + 1))
};

结果:

  • 59/59 cases passed (52 ms)
  • Your runtime beats 99.76 % of javascript submissions
  • Your memory usage beats 78.3 % of javascript submissions (33.6 MB)
  • 时间复杂度 O(m + n)

Ⅲ.合并后排序再赋值

代码:

/**
 * @param {number[]} nums1
 * @param {number} m
 * @param {number[]} nums2
 * @param {number} n
 * @return {void} Do not return anything, modify nums1 in-place instead.
 */
var merge = function(nums1, m, nums2, n) {
    arr = [].concat(nums1.splice(0, m), nums2.splice(0, n))
    arr.sort((a, b) => a - b)
    for(let i = 0; i < arr.length; i++) {
        nums1[i] = arr[i]
    }
};

结果:

  • 59/59 cases passed (64 ms)
  • Your runtime beats 90.11 % of javascript submissions
  • Your memory usage beats 31.21 % of javascript submissions (34.8 MB)
  • 时间复杂度 O(m + n)

查阅他人解法

这里看到一个直接用两次 while ,然后直接用 m/n 来计算下标的,没有额外空间,但是本质上也是从后往前遍历。

Ⅰ.两次while

代码:

/**
 * @param {number[]} nums1
 * @param {number} m
 * @param {number[]} nums2
 * @param {number} n
 * @return {void} Do not return anything, modify nums1 in-place instead.
 */
var merge = function(nums1, m, nums2, n) {
    // 避免 nums1 = [0,0,0,0], nums2 = [1,2] 这种 nums1.length > nums2.length 并且 m = 0
    // nums1.splice(m, nums1.length - m)
    // 从后开始赋值
    while(m !== 0 && n !== 0) {
        nums1[m + n - 1] = nums1[m - 1] > nums2[n - 1] ? nums1[--m] : nums2[--n]
    }
    // nums2 有剩余
    while(n !== 0) {
        nums1[m + n - 1] = nums2[--n]
    }
};

结果:

  • 59/59 cases passed (56 ms)
  • Your runtime beats 99.16 % of javascript submissions
  • Your memory usage beats 64.26 % of javascript submissions (33.8 MB)
  • 时间复杂度 O(m + n)

思考总结

碰到数组操作,会优先考虑双指针法,具体指针方向可以由题目逻辑来决定。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.