此前一直未从0开始写过Redux的工程,近日想简单对比下ReduxMobx的各自特点,于是动手撸了TodoList,感受了它们的不同。对比上看

  • Redux可靠规整,有一整套最佳实践,写大型应用时能避免很多坑
  • MobX轻便锋利,概念不多上手容易,在中小型应用中开发效率更高

Redux

Redux吸收了Flux和Elm的设计特点,正如它在Three Principles中写到的那样,唯一可信数据源,状态数据只读,状态改变为纯函数,这三大特点最大可能提升了可预测性,减少了调试的难度。同时,在概念上也易于理解。不过,复杂的设定和较多的代码入侵使得个人项目使用时稍显笨重,团队项目使用在改动已有代码时又会有牵一发动全身的感觉。

在数据流上,Redux规定action描述state的改变情况,reducer根据action定义state如何更新。

  • action,由type和payload部分组成,描述发生了什么变化,如{ type: 'ADD_TODO', text: 'Eat pie.' }
  • reducer,接受state和action作为入参,返回一个全新的state,正如文档里所说Redux assumes you never mutate your data

在这种设计理念下,我们借助redux创建state,之后所有的状态更新,都通过state.dispatch提交action完成,再借由connect等工具同步更新组件的props实现数据绑定的效果。除了设计理念外,Redux一些工程实践上的设计也值得一提

和React的互动

Redux并不是和React绑定了,但确实经常和React同时出现。react-redux是用来和react绑定的库。在结合了Redux后,React会有一些最佳工程实践

  • 区分开绘制组件和容器组件,前者只负责将数据转化为标签,后者负责接入数据,完成逻辑,组织绘制组件,并向其传递用户交互触发dispatch的函数。这样的层级设计将会增加解耦,减少后期修改时的工作量
  • 在最外侧调用createStore生成store并传入<Provider>组件,内部的容器组件,通过connectmapStateToPropsmapDispatchToProps就能便利地和store进行沟通,就像下面这样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const mapStateToProps = (state, ownProps) => {
return {
active: state.filter === ownProps.filter
}
}

const mapDispatchToProps = (dispatch, ownProps) => {
return {
handleClick: () => {
dispatch(setFilter(ownProps.filter))
}
}
}

const FilterLink = connect(
mapStateToProps,
mapDispatchToProps
)(Link)

Action Creator

使用函数创建标准化的action,而不要把action直接写在dispatch内。就像下面这样

1
2
3
4
5
6
7
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
dispatch(addTodo(text))

这么做在action发生更改时,只需要修改定义函数的位置即可,简单方便。可以发现上面的action creator中,完成的工作只是简单的组装type和函数的入参在同一个action object里。当这样的函数很多时,还可以用action creator creator来帮我们一行生成这些类似的action creator。

1
2
3
4
5
6
7
8
9
10
11
12
13
const createActionCreator = (type, ...argNames) => {
return (...args) => {
let action = { type }
argNames.forEach((arg, index) => {
action[arg] = args[index]
})
return action
}
}

const addTodo = createActionCreator(ADD_TODO, 'text')
const setFilter = createActionCreator(SET_FILTER, 'filter')
const toggleTodo = createActionCreator(TOGGLE_TODO, 'id', 'to')

Split Reducer

除了上面讲到的Action Creator外,split reducer页很常见,它的应用场景出现在当state比较复杂时,可以针对state的每个field单独写reducer,然后通过Redux的combineReducers组合起来。

就像TodoList中,筛选条件filter和待办事项todos可以拆出两个reducer去更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const todos = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
id: action.id,
text: action.text,
status: 0
}
]
case TOGGLE_TODO:
return state.map(todo =>
(todo.id === action.id) ? {...todo, status: action.to || +!todo.status} : todo
)
default:
return state
}
}

const filter = (state = 'SHOW_ALL', action) => {
switch (action.type) {
case SET_FILTER:
return action.filter
default:
return state
}
}

const todoApp = combineReducers({
todos,
filter
})

Middleware和Async Action

首先,我们要明确一点,Redux中只有dispatch能改变store,然而默认情况下,dispatch只接受action。因此,当我们想异步修改store时,异步的逻辑只能写在组件里(Vuex里则可以通过action异步提交commit)。设想一下,假如一个fetchAPI的逻辑在多处都用到时,只能在这些地方重复书写这些代码。好在,Redux提供了middleware的概念,和Express中的中间件类似,不同的是Redux中中间件处理的是用户提交的dispatch请求而已。

文档里对中间件的介绍非常到位,鉴于我们对express以及co中类似概念的了解,我们直接从第4步看起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function logger(store) {
let next = store.dispatch

// Previously:
// store.dispatch = function dispatchAndLog(action) {

return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}

function applyMiddlewareByMonkeypatching(store, middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()

// Transform dispatch function with each middleware.
middlewares.forEach(middleware =>
store.dispatch = middleware(store)
)
}

可以看到,原理其实是类似的,关键点在于使用store的dispatch方法依次暂存上一个节点,这么做的好处是保证最终能抛出最后真正的dispatch,且能实现链式的效果。然而,使用dispatch显得还不自然,于是就有了下面的版本,每次将next主动传入,当让applyMiddleware里需要额外传入next。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}

const crashReporter = store => next => action => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}

// Warning: Naïve implementation!
// That's *not* Redux API.
function applyMiddleware(store, middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()
let dispatch = store.dispatch
middlewares.forEach(middleware =>
dispatch = middleware(store)(dispatch)
)
return Object.assign({}, store, { dispatch })
}

借助中间件的帮助,我们可以在dispatch前完成我们想要的操作:打log,catch错误,甚至提前终止流程。而异步dispatch正是借助了thunk中间件的帮助。它的实现很简单。

1
2
3
4
const thunk = store => next => action =>
typeof action === 'function'
? action(store.dispatch, store.getState)
: next(action)

在我们通过applyMiddleware注入到store中后,就可以在dispatch中写入函数了!就像下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function fetchPosts(subreddit) {
return dispatch => {
dispatch(requestPosts(subreddit))
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(subreddit, json)))
}
}

function fetchPostsIfNeeded(subreddit) {
return (dispatch, getState) => {
if (shouldFetchPosts(getState(), subreddit)) {
return dispatch(fetchPosts(subreddit))
}
}
}

在使用时正常dispatch即可。

1
dispatch(fetchPostsIfNeeded(selectedSubreddit))

其他常用的store API

  • getState() 获取当前的state状态
  • subscribe(listener) 在每次更改state时触发
  • createStore() 根据输入的reducer,initState,middleware等生成store

脚手架

官网在介绍时列举了许多依赖库,却并没有给出示例的脚手架。没有舒服的脚手架,效率和工作热情都会受影响。这里介绍一种使用create-react-app快速搭建Redux脚手架的过程,下面的Mobx类似。

项目依赖大概有下面这些

但其实create-react-app可以帮你搭好其中包括react、react-dom、eslint、babel、webpack、postcss等绝大多数依赖环境,且完成配置。剩下的react-redux和react-router手动安装即可。

create-react-app类似于vue-cli,创建的默认配置不满意时,还可以npm run eject将默认配置撤销成用户配置,交给用户自己配置。

MobX

对比Redux,诞生于2015年3月的MobX在概念上吸收Vue,Knockout等MVVM框架要更多一些1,号称是TFRP(Transparent Functional Reactive Programming)。Transparent在依赖项的更新是隐式完成的,Functional在Computed中的使用,Reactive就不说了。MobX原名Mobservable,而后改名为MobX,官方并未说明如何发音,姑且读作moʊ-bex。和Redux对比来看,巧合的是MobX的设计理念也可以分为三部分:

  • 将所有的状态抽出来,用observable修饰
  • 描述出状态到视图的映射关系,这个过程因框架而异,但是一般React多一些,使用observer修饰
  • 在要修改状态的位置使用action包裹(强烈建议你这么做)

不过还有很核心的一点,文档里并没有提到

  • 把所有state修改的副作用放在autorun/reaction/when体内,在必要时在体内继续使用action

哇,是不是简洁了很多。你对observable对象做的所有修改(不论有没有用action包裹)都会自动反映在视图中,在项目结构上,你完全可以根据自己需要组织。不过,没有Redux里transaction的概念,MobX中对状态的修改在时间上都是不可回溯的。同时,没有中间件的概念,意味着在状态比较复杂时,可维护性就会下降。

整个MobX的关键API主要是由下面几部分组成的

  • observable 创建被依赖项,在设计中即state
  • computed 被依赖项的计算值,和Vue中的computed属性一致
  • action 动作,用来修改state,显式的使用可以使逻辑更清楚,当然不在action里修改observable也是允许的
  • observer和autorun/reaction/when,前者是derivation即根据state衍生出的结果,后者是reaction即state变化会触发的副作用(如IO等)

与React的结合

MobX和React相结合的方式就自由了很多。大体上使用components存储组件,stores描述状态。

stores描述状态时,

  • 通过@observable描述需要响应变化的状态变量,同时尽量将所有改变状态变量的操作封装成整个class的方法(并不强制),便于管理。
  • 通过@computed声明能够直接根据当前状态变量得到的衍生值。有意思的是,MobX只在observable变化时更新这个值,而不是在用户需要时去计算,从而节省了许多时间
  • 通过autorun()reaction()when()声明式地定义状态改变时的side effect,它们的执行结果都会返回一个dispose函数,在它们的生命周期结束后方便显式垃圾回收。它们三个都是除了用户操作外几乎唯一能进行副作用操作的地方,其中
    • autorun()在定义时就会被执行一次
    • reaction()仅在变化时执行,且对函数内部改变状态不敏感
    • when()则是在变化时执行一次即失效
  • 通过@action描述对状态做修改的行为,推荐使用,修饰在方法名前,开启strict模式后,则是强制要求使用

上面这些关键字都有修饰词(如:@action.bound)还有对应的ES5语法。(如:@observable key = value等同于extendObservable(this, { key: value }))。MobX并不要求使用单一的状态树,可以用多个文字组织你的状态。其中的一个store文件可能像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class TodoList {
id = 0
@observable todos = []

@computed get activeTodoCount() {
return this.todos.filter(todo => todo.status === 0).length
}

saveToStorage() {
// 第一次不会触发
reaction(
() => this.toJS(),
todos => localStorage.setItem('todo-mobx', JSON.stringify({ todos }))
)
}

addTodo(title) {
this.id++;
this.todos.push(new TodoItem({id: this.id, title, status: 0}))
}

toggleAll(status) {
this.todos.forEach(todo => todo.status = +status)
}

delete(id) {
let todoId = this.todos.findIndex(todo => todo.id === id)
if (todoId !== -1) {
this.todos.splice(todoId, 1)
}
}

clearCompleted() {
this.todos = this.todos.filter(todo => todo.status === 0)
}

toJS() {
return this.todos.map(todo => todo.toJS())
}

static fromJS(arr) {
const todoStore = new TodoList();
todoStore.todos = arr.map(todo => TodoItem.from(todo))
return todoStore
}
}

components文件夹下做的事和使用其他框架其实差别不大。区别主要在引入observer后,用@observer装饰组件类。需要修改状态时,可以直接对props中传入的store进行
修改(不过还是建议使用store中定义好的方法修改),视图就会同步更新,副作用也会同步完成。一个使用了mobx-react的组件大概像下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@observer
class TodoList extends Component {
render() {
const { todoStore, filterStore } = this.props
if (!todoStore.todos.length) {
return null
}
return (
<section>
<input
type="checkbox"
onChange={this.toggleAll}
checked={todoStore.activeTodoCount === 0}
/>
<ul>
{this.getVisibleTodos().map(todo => (
<TodoItem
key={todo.id}
todo={todo}
filterStore={filterStore}
handleDestroy={this.handleDestroy}
/>
))}
</ul>
</section>
)
}

getVisibleTodos() {
const { todoStore, filterStore } = this.props
return todoStore.todos.filter(todo => {
switch (filterStore.filter) {
case ACTIVE:
return todo.status === 0
case COMPLETED:
return todo.status === 1
case REMOVED:
return todo.status === 2
default:
return true
}
})
}

toggleAll = (e) => {
let checked = e.target.checked;
this.props.todoStore.toggleAll(checked !== 0)
}

handleDestroy = (id) => {
this.props.todoStore.delete(id)
}
}

除了直接使用store外,mobx-react还提供了将observable对象通过<Provider>inject2传入组件的方式。其他的用法可以参看文档,还有中文版翻译

脚手架

除了Redux里面提到的,因为MobX中用到了最新的decorator特性,.babelrc配置文件大概是下面这样

1
2
3
4
5
6
7
8
{
"presets": [
"react",
"es2015",
"stage-1"
],
"plugins": ["transform-decorators-legacy", "react-hot-loader/babel"]
}

package.json中需要额外引入”mobx”和”mobx-react”两个库(至少)。官方还提供了mobx-react-boilerplate,这些环境都已帮你配置好,按照README.md操作即可。另外,官方提供的awesome list是一个非常好学习mobx的地方

参考

  1. https://github.com/mobxjs/mobx#credits
  2. https://github.com/mobxjs/mobx-react#provider-and-inject

问题全部来自《剑指Offer》,顺序一致。

单例模式

1.使用字面量声明对象

1
2
3
4
5
6
7
const Singleton = {
prop1: "prop1"
prop2: "prop2"
method1() {
console.log("I am a method.");
}
}

或者通过闭包封装自己的私有变量和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Singleton = function() {
let privateProp = "You can't see me";

function privateMethod() {
console.log("You can't see me either.");
}

return {
publicProp: `${privateProp}!`,
publicMethod() {
return privateMethod();
}
}
}

2.利用闭包

本质上和上面的方法类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const Singleton = function() {
let instance;

function init() {
return {
prop: "prop",
method() {
console.log("I am a method.");
}
};
}

return {
getInstance() {
// JS单线程,不考虑锁的问题
if (!instance) {
instance = init();
}
return instance;
}
}
}

还有一个酷炫的变种,不过是上面的构造函数写法

1
2
3
4
5
6
7
8
9
10
11
12
const Singleton = function() {
const instance = this;

this.prop = "prop";
this.method = function() {
console.log("I am a method.");
}

Singleton = function() {
return instance;
};
}

3.借助构造函数和JS中函数也是对象的特点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Singleton = function() {
// 这里不取名为instance也可以
// 显式返回this
if (typeof Singleton.instance === "object") {
return Singleton.instance;
}

// 成员和方法
this.prop = "prop";
this.method = function() {
console.log("I am a method.");
}

Singleton.instance = this;

//隐式返回this
}

参考

找出数组中重复的数字

Set,使用ES6内建的数据结构。

1
2
3
4
5
6
7
8
9
const mySet = new Set();
arr.forEach((val, index) => {
if (mySet.has(val)) {
console.log(val);
return;
} else {
mySet.add(val);
}
});

ObjectArray(空间复杂度O(n)),换汤不换药,略。

传统方法,时间复杂度O(n),空间复杂度O(1)。交换当前元素和当前索引对应的元素,直到两者相等。(请参考原书中的前提,数字都在0~n-1之间)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function findDuplicate(arr) {
// 边界检测
if (!arr || arr.length === 0) {
return false;
}
for (let i = 0, len = arr.length; i < len; i++) {
// 循环交换元素直到arr[i] == i
while (arr[i] != i) {
if (arr[i] === arr[arr[i]]) {
return arr[i];
}
// 交换
[arr[i], arr[arr[i]]] = [arr[arr[i]], arr[i]];
}
}
}

传统方法,且不修改数组,利用折半查找,递归寻找重复的元素。时间复杂度O(nlogn),空间复杂度O(1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function findDuplicate(arr) {
// 边界检测
if (!arr || arr.length === 0) {
return false;
}

function countRange (arr, start, end) {
if (start > end) {
return false;
} else if (start === end) {
return start;
}
const split = (start + end) / 2;
const count = arr.reduce((last, val) => {
return last + +(val >= start && val <= split);
}, 0);
if (count > split - start + 1) {
countRange(arr, start, split);
} else {
countRange(arr, split + 1, end);
}
}

return countRange(arr, 0, arr.length);
}

二维数组中的查找

前提:数组的每一行和每一列都递增

贪婪算法,从右上角开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function find(arr, val) {
if (!arr || !arr.length) {
return false;
}
let [rows, columns] = [arr.length, arr[0].length];

if (!rows || !columns) {
return false;
}
let [x, y] = [columns-1, 0]
while(x > 0 && y < rows) {
if (arr[y][x] === val) {
return true;
} else if (arr[y][x] > val) {
x--;
} else {
y++;
}
}
return false;
}

替换字符串中的空格

JavaScript中很好实现,可以直接使用库函数。

1
2
3
function replace(str) {
return str.replace(" ", "%20");
}

使用传统方法,思路是先从前到后找到空格数,扩展出足够空间。再使用两个指针,从后到前,P1指向旧字符串的末尾,P2指向新字符串的末尾。碰到空格时,P2插入”%20”。碰到其他字符时,挨个复制即可。

这种双指针从后向前的思维还可以用在两有序数组合并。从尾到头比较A1和A2数字。

链表设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// 单向链表
let LinkedList2 = (function() {
class Node {
constructor(element) {
this.element = element;
this.next = null;
}
}

class LinkedList {
constructor() {
this.length = 0;
this.head = null;
}

append(element) {
let node = new Node(element);
if (this.head === null) {
this.head = node;
} else {
let curr = this.head;
while(curr.next) {
curr = curr.next;
}
curr.next = node;
}
this.length++;
}

insert(pos, element) {
if (pos < 0 || pos > this.length) {
return false;
}
let curr = this.head,
node = new Node(element);
while(--pos >= 0) {
curr = curr.next;
}
node.next = curr.next;
curr.next = node;
this.length++;
}

remove(pos) {
if (pos < 0 || pos > this.length) {
return false;
}
let curr = this.head;
prev;
while(--pos >= 0) {
prev = curr;
curr = curr.next;
}
if(isUndefined(prev)) {
this.head = null;
} else {
prev.next = curr.next;
}
this.length--;
}

indexOf(element) {
let curr = this.head,
index = 0;
while (curr) {
if (element === curr.element) {
return index;
}
index++;
curr = curr.next;
}
return -1;
}
}

})();

// 双向链表
// 略

从尾到头打印链表

前提:单向链表。使用递归

1
2
3
4
5
6
7
function recursivePrint(node) {
if (node && node.next) {
console.log(recursivePrint(node.next));
} else {
console.log(node.element);
}
}

重建二叉树

前提:根据前序和中序遍历的结果。

1
2
3
4
5
6
7
class BinaryTreeNode {
constructor(element, left, right) {
this.element = element;
this.left = left;
this.right = right;
}
}

通过前序遍历找到根节点,结合中序遍历找到左右子树,之后递归构建左右子树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function constructTree(preorder, inorder) {
if (preorder.length === 0) {
return null;
}

function construct(preorder, inorder) {
// 找到根节点元素
let root = preorder[0];
// 确定左右子树
let index = inorder.indexOf(root),
left = (index === 0 ? null : construct(preorder.slice(0, index), inorder.slice(0, index)));
right = (index === inorder.length-1 ? null : construct(preorder.slice(index+1), inorder.slice(index+1)));

return new BinaryTreeNode(root, left, right);
}

return construct(preorder, inorder);
}

二叉树的下一个节点

前提:树的每个节点除了有两个子节点的指针,还有一个指向父节点的指针。寻找中序遍历的下一个节点。

1
2
3
4
5
6
7
8
class BinaryTreeNode {
constructor(element, left, right, parent) {
this.element = element;
this.left = left;
this.right = right;
this.parent = parent;
}
}

二叉树中的节点和周围节点可能有四种关系(画图出来更直观)

  1. 左子树的父节点
  2. 右子树的父节点
  3. 父节点的左子树
  4. 父节点的右子树

从而有下面的流程:

  1. 关系2存在时,返回右子树的最左节点,否则到步骤2
  2. 关系3存在时,返回父节点,否则到步骤3
  3. 关系4存在时,向上回溯,对父节点使用步骤2,否则到步骤4
  4. (节点是根节点)返回null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function findNextNode(node) {
let curr;
if (node.right) {
curr = node.right;
while(curr = curr.left) {
;
}
return curr;
} else if (node.parent) {
if (node.parent.left === node) {
return node.parent;
} else if (node.parent.right === node) {
curr = node.parent;
while (curr.parent && curr.parent.right === curr) {
curr = node.parent;
}
if (curr.parent) {
return curr.parent;
}
}
} else {
return null;
}
}

用两个栈实现队列

前提,实现append和delete功能,完成在队尾插入和队首删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Stack {
constructor() {
this.stack = []
}

get size() {
return this.stack.length;
}

push(element) {
return this.stack.push(element);
}

pop() {
return this.stack.pop();
}

}

思路,栈1只允许正序存放,栈2只允许倒序存放,由于栈2的内容始终较老,因此插入总发生在栈1。删除总发生在栈2(栈2为空时除外)。JS中没有泛型的概念,所以无需考虑栈中的数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Queue {
constructor() {
this.stack = new Stack();
this.inverseStack = new Stack();
}

append(element) {
this.stack.push(element);
return this.stack.size + this.inverseStack.size;
}

delete() {
if (!this.inverseStack.size) {
while (!this.stack.size) {
this.inverseStack.push(this.stack.pop());
}
}

// 判断是否为空
if (!this.inverseStack.size) {
return this.inverseStack.pop();
}

console.warn("Empty queue!");
return false;
}
}

用两个队列实现栈

前提同上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Queue {
constructor() {
this.queue = []
}

get size() {
return this.stack.length;
}

in(element) {
return this.stack.push(element);
}

out() {
return this.stack.shift();
}

}

思路和上面类似,但不大一样。每次元素出队时,所有元素一定会转移到另一个队列中。因此,插入和删除一定发生在有元素的队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Stack {
constructor() {
this.queue = new Queue();
this.backupQueue = new Queue();
}

append(element) {
if (queue.size) {
queue.in(element);
return queue.size;
} else {
backupQueue.in(element);
return backupQueue.size;
}
}

delete() {
if (queue.size) {
let element;
while(queue.size > 1) {
element = queue.out();
backupQueue.in(element);
}
return queue.out();
} else if(backQueue.size > 1) {
let element;
while(backupQueue.size) {
element = backupQueue.out();
queue.in(element);
}
return backupQueue.out();
}
console.warn("Empty stack!");
}
}

斐波那契数列

使用递归的代码好写,但是效率低;使用循环更好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function fibonacci(n) {
if (n < 2) {
return n;
}

let a = 0,
b = 1,
res = 1;
for (let i = 2; i <= n; i++) {
[res, a, b] = [a + b, b, res];
}

return res;
}

变种还有“青蛙跳台阶”等类最优子问题的问题。

旋转数组的最小数字

前提,将数组开头的部分元素搬运到数组的末尾,寻找最小的元素。

使用二分查找,右半边递增时,最小元素在左半边。左半边递增时,最小元素在右半边。注意下面两种情况:

  • 数组未做旋转
  • 旋转后,头尾元素相等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function findMin(arr) {
if (!arr || !.arr.length) {
return;
}

let lindex = 0,
rindex = arr.length - 1,
res = lindex;

while (arr[lindex] >= arr[rindex]) {
if (rindex - lindex === 1) {
res = rindex;
}

res = (lindex + rindex) / 2;

// 特殊情况
if (arr[res] == arr[lindex] && arr[res] == arr[rindex]) {
return arr.reduce((min, val) => {
return Math.min(min, val);
}, arr[0]);
}

if (arr[res] >= arr[lindex]) {
lindex = res;
} else if (arr[res] <= arr[rindex]) {
rindex = res;
}
}
return arr[res];
}

回溯法

通常适合用递归法实现,到达某个节点时,尝试所有可能,并在满足条件时递归到下一节点。

矩阵中的路径

前提,判断矩阵中有无一条不交叉路径能够返回指定字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function hasPath(arr, str) {

if (!arr || !arr.length || !str) {
return false;
}

// 使用数组记录历史数据,防止交叉
let occupied = arr.map(() => {
return arr[0].map(() => {
return false;
});
});

let pathMath = function(i, j, str) {
if (!str) {
return true;
}

if (i < 0 || i >= arr.length || j < 0 || i >= arr[0].length) {
return false;
}

if (str.substr(0, 1) == arr[i][j]) {
occupied[i][j] = true;
let newStr = str.substr(1),
res = pathMath(i-1, j, newStr) || pathMath(i+1, j, newStr) || pathMath(i, j-1, newStr) || pathMath(i, j+1, newStr);

// 回溯
if (!res) {
occupied[i][j] = false;
}
}

return res;
}

arr.foreach((val, i) => {
val.foreach((val, j) => {
if (pathMatch(i, j, str)) {
return true;
}
});
});

return false;
}

机器人的运动范围

前提,m行n列的矩阵,机器人从(0,0)出发不能进入数位和大于k的格子,求能到的格子个数。

思路同上甚至更简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function gridCount(rowCount, colCount, limit) {
if (limit < 0 || rowCount <= 0 || colCount <= 0) {
return 0;
}

// 使用数组记录历史数据,减少冗余查询
let walked = [];

let count = function(row, col) {
let cnt = 0;

if (check(row, col)) {
// 不必还原,减少重复查询
walked[row * rowCount + col] = true;
cnt = 1 + count(row-1, col) + count(row+1, col) + count(row, col-1) + count(row, col+1);
}
return cnt;
},
check = function () {
return row >= 0 && col >= 0 && !walked[row * rowCount + col] && digitSum(row) + digitSum(col) <= limit;
},
digitSum = function(num) {
let sum = 0;
while(num > 0) {
sum += num % 10;
num /= 10;
}
return sum;
};

return count(row, col)
}

剪绳子

前提,绳长为整数,切成若干段,使得绳长乘积最大。

类似于《算法导论》里的裁切钢管。首先可以用动态规划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function cutRope(len) {
if (len < 4) {
return len - 2;
}

let cut = [0, 1, 2];
for (let i = 4; i <= len; i++) {
let max = 0;
for (let j = 1; j <= i / 2; j++) {
let mul = cut[j-1] * cut[i-j-1];
if (max < mul) {
max = mul;
}
}
cut.push(max);
}
return cut[len-1];
}

经过数学推导,n>=5时,2(n-2)>n3(n-3)>n2(n-2)>=3(n-3),因此有最优子问题的解法。根据贪婪算法,应该尽量多切长度为3的绳子段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function cutRope(len) {
if (len < 4) {
return len - 2;
}

// 多切长度为3的绳子段
let cntOf3 = len / 3,
cntOf2 = 1,
remain = len % 3;
// 长度为4时,应该切成两个2
if (remain === 1) {
cntOf3--;
cntOf2 = 2;
}

return Math.pow(3, cntOf3) * cntOf2
}

位运算

经常用于奇技淫巧。在C风格的解法中常用。JS中应该不常出现。

二进制中1的数目

通过移位操作,需要注意的是,对入参移位容易出现死循环,可以对1进行移位并比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
function numOf1(n) {
let cnt = 0,
flag = 1;

while(flag) {
if (n & flag) {
cnt++;
}
flag <<= 1;
}

return cnt;
}

二进制整数减1相当于右侧0值变成1,最右侧1变成0,此时和原数做按位与,可以消掉最右侧0。利用这个特点可以进一步提高效率。这种思路可以用来解决很多二进制问题。

1
2
3
4
5
6
7
8
9
10
function numOf1(n) {
let cnt = 0;

while(n) {
++cnt;
n = (n-1) & n;
}

return cnt;
}

数值的整数次方

前提,不得使用库函数,需要考虑大数问题

用类似二分法的思想,由2到4,由4到8,由8到16。时间复杂度O(logn)。同时注意边界情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function pow(base, exp) {
let isNeg = exp > 0,
res = 1;

if (exp == 0) {
return 1;
}

if (base == 0) {
return 0;
}

function unsignedPow(base, exp) {
if (exp === 0) {
return 1;
}
if (exp === 1) {
return base;
}

let res = unsigned(base, exp >> 1);
res *= res;
if (exp & 0x1 === 1) {
res *= base;
}
return res;
}

res = unsignedPow(base, Math.abs(exp));

if (isNeg) {
res = 1 / res;
}

return res;
}

按顺序打印从0到最大的n位数字

大数问题。自定义数据结构实现整数+1操作,或者通过递归for循环依次打印每一位数字,像下面这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function printNums(n) {
function print(str, n) {
if (1 === n) {
for (let i = 0; i < 10; i++) {
console.log(str + i);
}
} else {
n--;
for (let i = 0; i < 10; i++) {
if (0 === i) {
print(str, n);
} else {
print(str + i, n);
}
}
}
}

if (n < 1) {
return;
}

print('', n);
}

删除链表节点

1
2
3
4
5
6
class LinkedListNode {
constructor(value, next) {
this.value = value;
this.next = next
}
}

O(1)时间删除单向链表中某节点

通常思路,需要从头结点循环找到节点的上一个节点,将它的next属性设置删除节点的next。但是这么做是O(n)的复杂度。更好的方法是,借尸还魂,覆盖当前节点的next节点,然后重设next节点跳过下一节点。

(因为JS中没有指针一说,这种模拟不能完全还原原题的问题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function removeNode(head, node) {
if (!head || !node) {
return;
}

if (!node.next) { // 不是尾节点
node.next = node.next.next;
node.value = node.value;
} else if (node == head) { // 链表只有一个节点
head = null;
} else { // 是尾节点,只能循环
let n = head;
while (n.next !== node) {
n = n.next;
}
n.next = null;
}
}

表示数值的字符串

考察思维的全面性。包括正数负数、小数、科学技术法都要考虑。格式遵循[+|-]?(\d+)(\.\d+)?([e|E][+|-]?\d+)?

1
2
3
4
5
6
// 正则表达式
/\d/.test(str);

// 类型转换
!isNaN(+str);

JS下解决问题并不如C++自然。下面是C++风格的解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
bool judgeUnsignedInt(const char ** str) {
const char * start = *str;
while(**str != \0 && **str >= '0' && **str <= '9') {
++(*str);
}

return *str > start;
}

bool judgeInt(const char ** str) {
if (**str === '+' || **str === '-') {
++(*str);
}
return judgeUnisignedInt(str);
}

bool isNumber(const char * str) {
if (str == null) {
return false;
}

// 先扫描整数部分
bool res = judgeInt(&str);

if (*str == '.') {
++str;

// 扫描小数部分
res = judgeUnsignedInt(&str) || res;
}

if (*str == 'e' || *str == 'E') {
++str;

// 扫描指数部分
res = judgeInt(&str) && res;
}

return res && *str == '\0';
}

调整整数数组中奇数和偶数的位置

前提,使得奇数都位于前边,偶数都位于后边。

考虑拓展性,用一个函数作为衡量指标,判断元素应该在前还是在后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function adjustArr(arr, standard) {
if (!arr || !arr.length || arr.length < 2) {
return arr;
}

let start = 0;
let end = arr.length - 1;
while (start < end) {
while (start < end && !standard(arr[start])) {
start++;
}

while (start < end && standard(arr[end])) {
end--;
}

if (start < end) {
[arr[start], arr[end]] = [arr[end], arr[start]];
}
}
}

// 在后面的标准
function standard (num) {
return num / 2 == 0;
}

鲁棒性

链表的倒数第K的节点

1
2
3
4
5
6
7
8
9
10
11
class LinkedList {
constructor(head) {
this.head = head;
}
}

class Node {
constructor(next) {
this.next = next;
}
}

考虑鲁棒性:

  • 空数组
  • k超过数组长度
  • k=0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function findLastK(head, k) {
if (!head || k === 0) {
return null;
}

let flag = head;
while (--k > 0) {
if (head.next) {
head = head.next;
} else {
return null
}
}

let res = head;
while(flag = flag.next) {
res = res.next;
}

return res;
}
当一个指针不足以解决链表的问题时,通常两个指针就可以解决问题

链表中环的入口节点

思路:

  1. 链表中有环存在时,走得快的指针将会追上走得慢的指针。
  2. 确定环的长度k后,让两个指针同速,一个领先k位出发,相遇的位置即是入口

考虑鲁棒性:

  • 无环时
  • 寻找相遇点的时候注意next属性是否存在
  • 头结点为环入口时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
function entryOfLoop(list) {
if (!list || !list.length || !list.length < 2) {
return null;
}

// 找到相遇位置
let slow, fast, meet;
slow = fast = list.head;

while (!!fast && !!slow) {
if (fast === slow) {
meet = fast;
}
slow = slow.next;
fast = fast.next;
if (!!fast) {
fast = fast.next;
}
}

// 无环存在时跳出
if (!!meet) {
return null;
}

// 确定环的长度
let length = 1;
let tmpNode = meet.next;
while (tmpNode !== meet) {
tmpNode = tmpNode.next;
length++;
}

// 先移动一个指针
let first = list.head;
while (length > 0) {
first = first.next;
length--;
}

// 再移动第二个指针
let next = list.head
while (next !== first) {
first = first.next;
next = next.next;
}

return first;
}

反转链表

前提:反转链表并输出反转后的头节点。

考虑鲁棒性:

  • 边界情况,如空链表或长度为1的链表
  • 反转过程中链表断裂
1
2
3
4
5
6
7
8
9
10
11
12
13
function reverseList(list) {
let node = list.head;
let prev = null;

while(!!node) {
if (!node.next) {
return node;
}
node.next = prev;
prev = node;
node = node.next;
}
}

合并两个排序的链表

类似归并排序。

考虑鲁棒性:

  • 空链表
  • 链表长度为1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function merge(list1, list2) {
if (!list1) {
return list2;
} else if (!list2) {
return list1;
}

let node = new Node();
let list = new List(node);
let h1 = list1.head;
let h2 = list2.head;

for (!!h1 || !!h2) {
let next;
if (!!h1 && !h2 || h1.value < h2.value) {
next = h1.value;
node = node.next;
h1 = h1.next;
} else {
next = h2.value;
h2 = h2.next;
}

node.next = new Node(next);
node = node.next;
}

list.head = list.head.next;
return list;
}

树的子结构

前提:判断树A中是否能找到和树B结构一样的子树。

使用递归思路更简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function hasSubTree(heap, needle) {
function match(root1, root2) {
if (!root2) {
return true;
}

if (!root1) {
return false;
}

return match(root1.left, root2.left) && match(root1.right, root2.right);
}

let root1 = heap.root;
let root2 = needle.root;

let res = false;
if (!root1 && !root2) {
if (root1.value === root2.value) {
res = match(root1, root2);
}

if (!res) {
res = hasSubTree(root1.left, root2);
}

if (!res) {
res = hasSubTree(root1.right, root2);
}
}

return res;
}

形象化

二叉树的镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function mirrorBinaryTree(root) {
if (!root) {
return;
}
// 跳过叶子节点
if (!root.left && !root.right) {
return;
}

[root.left, root.right] = [root.right, root.left];

if (!!root.left) {
mirrotBinaryTree(root.left);
}

if (!!root.right) {
mirrotBinaryTree(root.right);
}
}

对称的二叉树

即前序和对称遍历结果一致。和上面类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function isSymmetrical(root) {
function judge(root1, root2) {
if (root1 === null && root2 === null) {
return true;
}
if (root1 === null || root2 === null) {
return false;
}
if (root1.value !== root2.value2) {
return false;
}

return judge(root1.left, root2.right) && judge(root1.right, root2.left);
}

return judge(root, root);
}

顺时针打印矩阵

  • 打印一行“删去”一行,打印一列“删去”一列。
  • 最后一圈先向右再向下再向上再向左,根据顺序共有4种可能,通过if语句区分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function printMatrix(matrix) {
if (!matrix || !matrix.length || !matrix[0].length) {
return;
}
let top = 0;
let left = 0;
let bottom = matrix.length - 1;
let right = (matrix[0].length ? matrix[0].length - 1 : 0);

while (left <= right && top <= bottom) {
// 向右
for (let i = left; i <= right; i++) {
console.log(matrix[top][i] + ' ');
}
top++;

// 向下
if (top <= bottom) {
for (let i = top; i <= bottom; i++) {
console.log(matrix[i][right] + ' ');
}
right--;
} else {
return;
}

// 向左
if (left <= right) {
for (let i = right; i >= left; i--) {
console.log(matrix[bottom][i] + ' ');
}
bottom--;
} else {
return;
}

// 向上
if (top <= bottom) {
for (let i = bottom; i >= top; i--) {
console.log(matrix[i][left] + ' ');
}
left++;
} else {
return;
}
}
}

剑指offer上的思路是利用每圈的起始x和y坐标相等,判断循环终止条件,再根据条件决定一圈打印几行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function printMatrix(matrix) {
if (!matrix || !matrix.length || !matrix[0].length) {
return;
}

function print() {
let width = column - start - 1;
let height = rows - start - 1;

for (let i = start; i <= width; i++) {
console.log(matrix[start][i]);
}

if (start < height) {
for (let i = 0; i <= height; i++) {
console.log(matrix[i][width]);
}
}

if (start < width && start < height) {
for (let i = width-1; i >= start; i--) {
console.log(matrix[height][i]);
}
}

if (start < width && start < height - 1) {
for (let i = height-1; i < start+1; i--) {
console.log(matrix[i][start]);
}
}
}

let start = 0;
let rows = matrix.length;
let column = matrix[0].length;

while (column > 2 * start && rows > 2 * start) {
print();
++start;
}
}

包含min函数的栈

前提,设计栈的数据结构,使min、push和pop的时间复杂度都是O(1)。

注意:若使用变量保存最小元素,那么在最小元素出栈后会找不到次小元素。可见只使用一个变量是不够的,为配合原始栈的结构,需要有一个辅助栈记录最小元素的位置。(画图可以很容易理解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Stack {
constructor() {
this.stack = [];
this.minStack = [];
}

get min() {
let len = this.minStack.length;
return (len ? this.minStack[len-1] : Infinity);
}

push(element) {
this.stack.push(element);
this.minStack.push(Math.min(this.min, element));
}

pop() {
this.stack.pop();
this.minStack.pop();
}

top() {
return (this.stack.length ? this.stack[this.stack.length-1] : null);
}
}

栈的压入、弹出序列

前提:给定栈的压入顺序,判断某个出栈顺序是否可能

通过举例具象化思考。如入栈顺序为[1, 2, 3, 4, 5],[4, 3, 5, 2, 1]就是一个可能的例子。首先,4是第一个出栈元素,则[1,2,3]此时已经入栈,4出栈后,3位栈顶,出栈后,5不在栈内,将5入栈。剩下的5,2,1同理。因此,可以发现下面规律。

  1. 第一个出栈元素前的入栈序列此时正在栈内
  2. 之后所有的出栈元素,先在栈顶寻找,若有,则出栈;若没有,在栈外元素中寻找
  3. 若找到,将其入栈,若找不到,则返回false
  4. 如此循环直到栈空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function isPopOrder(push, pop) {
if (push.length !== pop.length) {
return false;
}
if (!push.length) {
return true;
}

// 寻找当前栈内元素
let first = pop[0];
let index = push.indexOf(first);
let stack = [];

if (index === -1) {
return false;
}

for (let i = 0; i <= index-1; i++) {
stack.push(push[i]);
}

let rest = push.slice(index);

for (let i = 2, len = pop.length; i < len; i++) {
let value = pop[i];
if (!push.length || push[push.length-1] !== value) {
let index = rest.indexOf(value);
if (index === -1) {
return false;
}
push.push(value);
rest.splice(index, 1);
} else {
push.pop();
}
}

return true;
}

从上到下打印二叉树

使用队列保证先后顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Queue {
constructor() {
this.queue = []
}

get length() {
return this.queue.length;
}

in(element) {
this.queue.push(element);
return this.queue.length;
}

out() {
return this.queue.shift();
}
}

function printTreeByTier(root) {
if (!root) {
return;
}

let q = new Queue();
q.in(root);

while(q.length) {
let node = q.out();
console.log(node.value);
if (node.left) {
q.in(node.left);
}
if (node.right) {
q.in(node.right);
}
}
}

进阶:分行从上到下打印二叉树

和上面类似,区别在入队时需要携带层数信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Queue {
constructor() {
this.queue = []
}

get length() {
return this.queue.length;
}

in(element) {
this.queue.push(element);
return this.queue.length;
}

out() {
return this.queue.shift();
}
}

function printTreeByTier(root) {
if (!root) {
return;
}

let tier = 0;
let q = new Queue();
q.in({
root,
tier,
});

while (q.length) {
let node = q.out();

if (node.tier !== tier) {
tier = node.tier;
console.log("换行");
}

if (q.root.left) {
q.in({
root: q.root.left,
tier: tier+1,
})
}

if (q.root.right) {
q.in({
root: q.root.right,
tier: tier+1,
});
}
}
}

进阶2:之字形打印二叉树

前提:第一行从左到右打印,第二行从右到左打印,以此类推。

因为每层的顺序都相反,很适合栈存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 栈的数据结构实现略
// ...

function printTreeByZ(root) {
if (!root) {
return;
}

let stack1 = new Stack();
let stack2 = new Stack();
// 当前在哪个栈内
let curr = 1;

stack1.push(root);

while (!stack1.empty() || !stack2.empty()) {
if (curr == 1) {
let node = stack1.pop();
console.log(node.value);

if (node.left) {
stack2.push(node.left);
}

if (node.right) {
stack2.push(node.right);
}

if (stack1.empty()) {
curr = 2;
console.log("换行");
}
} else {
let node = stack2.pop();
console.log(node.value);

if (node.right) {
stack1.push(node.right);
}

if (node.left) {
stack1.push(node.left);
}

if (stack2.emtpy()) {
curr = 1;
console.log("换行");
}
}
}
}

二叉搜索树的后序遍历序列

前提:判断整数序列是否是某个二叉搜索树的后序遍历序列,并假设所有数字都不相同

利用二叉搜索树的特点,结合后序遍历时尾元素为根节点,递归判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function verifySeqOfBST(seq) {
if (!seq || seq.length <= 0) {
return false;
}

let len = seq.length;
let root = seq[len-1];
// 找到右子树的起点
let rightIndex = seq.findIndex((element) => {
return element > root;
});

// 右子树的节点需要都大于左节点
if (rightIndex == -1 || seq.slice(rightIndex).every((element)=>{ return element > root; })) {
return false;
}

// 递归判断左右子树
let left = rightIndex <= 0 || verifySeqOfBST(seq.slice(0, rightIndex));
let right = rightIndex == -1 || verifySeqOfBST(seq.slice(rightIndex));

return left && right;
}

二叉树中和为某值的路径

前提:找到所有满足要求的路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function findPaths(root, target) {
let path = [];
let sum = 0;
let node = root;

// 利用闭包共享变量
let find = () => {
path.push(node);
sum += node.value;

if (sum === target) {
console.log(path)
}

if (node.left) {
node = node.left;
find();
}

if (node.right) {
node = node.right;
find();
}

// 不要忘了弹出栈顶元素
path.pop();
}

find();
}

分解

复杂链表的复制

前提:复杂链表中每个节点除了一个指向下个节点的指针外,还有一个指向任意节点的指针

1
2
3
4
5
6
7
class ComplexLinkedListNode {
constructor({value, next, extra}) {
this.value = value;
this.next = next;
this.extra = extra;
}
}

困难在extra属性的复制,在O(n)的时间复杂度,O(1)的空间复杂度的要求下,问题可以分解成3步

  1. 在每个原始节点后接上复制节点
  2. 原样复制extra属性
  3. 将复制的链表拆出来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function copyLinkedList(head) {
if (!head) {
return null;
}

let node = head;

// Step1: 在每个节点后添加一个复制节点
while (node) {
node.next = new ComplexLinkedListNode()({
value: node.value,
next: node.next
});
node = node.next;
}

// Step2: 复制每个节点的extra属性
node = head;
while (node) {
// 这一步大大减少了时间复杂度
node.next.extra = node.extra.next;
node = node.next.next;
}

// Step3: 拆开两条链
node = head
let newNode = new ComplexLinkedListNode();
while (node) {
newNode.next = node.next;
node.next = node.next.next;
node = node.next;
newNode = newNode.next;
}

return newNode.next;
}

二叉搜索树和双向链表

前提:将一个二叉搜索树转换成一个排序的双向链表,不允许新建节点。

一般涉及到树结构的问题,使用递归都要更直观方便。本题中,可以把问题拆解成,将根节点和已组装好的左右双向链表组装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function transBSTToBilink(root) {
if (!root) {
return;
}

let split = (root, type) => {
let node = new Node(root.value);
let left = node;
let right = node;

if (root.left) {
left = split(root.left, 'left');
left.next = node;
node.next = left;
}
if (root.right) {
right = split(root.right, 'right')
node.next = right;
right.next = node;
}

// 通过type区分当前是左子树还是右子树,从而返回最大点或最小点
return (type === 'left' ? right : left);

}

split(root);
}

序列化二叉树

要求:实习序列化和反序列化函数

通常情况需要两种遍历序列才能还原二叉树,这是因为二叉树非完全。对空缺的节点使用特殊字符,即可消除二义性,使用单一遍历序列还原二叉树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class BinaryTree {
// ...
}

BinaryTree.serialize = (root) => {
let str = [];
let serialize = (root) => {
if (!root) {
str.push('$');
return;
}
str.push(root.value);
serialize(root.left);
serialize(root.right);
});
serialize(root);
return str.join('');
}

BinaryTree.parse = (str) => {
if (!str) {
return null;
}
let i = 0;
let len = str.length;
let parse = (str) => {
if (str[i] === '$') {
return null;
}
let root = new Node(+str[i]);
// 最后一个节点必是叶节点
++i, root.left = parse(str);
++i, root.right = parse(str);
return root;
}
return parse(str);
}

字符串的排列

前提:不考虑重复字符的问题
要求:输入一个字符串,输出字符串中字符的所有排列

此题可以用递归,《剑指offer》上的解法简单易行。JS中对字符串的限制(只读)使得代码和C风格有别。这里我们考虑输入是一个字符数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function combinations(str) {
if (!str.length) {
return;
}

let len = str.length
function combine(comb, strs, start) {
if (start === len - 1) {
console.log(comb + strs[start]);
}

for (let i = start; i < len; i++) {
[strs[start], strs[i]] = [strs[i], strs[start]];
// 需要通过slice方法传递复制
combine(comb + strs[start], strs.slice(), start+1);
}
}
combine('', str, 0);
}

求字符串的组合(也可以叫求子集)

总共的解数目为$2^n$。从最低位开始,向上寻找时,有两种可能:新字符不出现,新字符出现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function subsets(str) {
if (!str.length) {
return;
}

function reduce(res, i) {
if (i === str.length-1) {
console.log(res + str[i]);
console.log(res);
return;
}

reduce(res+str[i], i+1);
reduce(res, i+1);
}

reduce('', 0)
}

输入8个数字的数组放在正方体的8个顶点,有无可能每对面对应的4个顶点和相等

相等于寻找全排列,使得a1+a2+a3+a4=a5+a6+a7+a8,且a1+a3+a5+a7=a2+a4+a6+a8,且a1+a2+a5+a6=a3+a4+a7+a8。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function findComb(nums) {
let len = nums.length;

function combine(nums, start) {
if (start === len-1) {
if(test(nums)) {
console.log(nums);
}
}

for (let i = start; i < len; i++) {
[nums[start], nums[i]] = [nums[i], nums[start]];
combine(nums.slice(), start + 1);
}
}

function test(nums) {
return nums[0] + nums[1] + nums[2] + nums[3] == nums[4] + nums[5] + nums[6] + nums[7] &&
nums[0] + nums[2] + nums[4] + nums[5] == nums[1] + nums[3] + nums[5] + nums[7] &&
nums[0] + nums[1] + nums[6] + nums[7] == nums[2] + nums[3] + nums[4] + nums[5];
}

combine(nums, 0);
}

8皇后问题

8皇后的限制条件在,不得同行同列,不得位于同一对角线。因此,为长度为8的数组初始化填入1~8,代表行数。填入的数代表列数,因为每行每列都只有一个皇后。所以全排列中,只需要删除满足下面条件的排列即可:

  • i-j != columns[i] - columns[j]j-i != columns[i] - columns[j],即不在一个对角线上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function eightQueens() {
let count = 0;

function combine(queens, start) {
if (start === 7) {
console.log(queens);
count++;
}

for (let i = start; i < 8; i++) {
[queens[start], queens[i]] = [queens[i], queens[start]];
if (test(queens, start)) {
combine(queens.slice(), start+1);
} else {
[queens[start], queens[i]] = [queens[i], queens[start]];
}
}
}

function test(queens, end) {
for (let i = 0; i < end; i++) {
let diff = queens[end] - queens[i];
if (diff == end - i || -diff == end - i) {
return false;
}
}
return true;
}

combine([0,1,2,3,4,5,6,7], 0);
console.log(count);
}

这种思路会带来重复的解,按照Matrix67的思路,可以通过位运算求解。使用row(列占用情况),ld(左下右上对角线占用),rd(右下左上对角线占用)三个变量存储当前列的禁用情况。使用“1”表示禁用,“0”表示可用。解决n皇后的问题代码仅在10行左右。详细解释见链接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function nQueens(n) {
if (!n || n <= 0) return;
let limit = (1 << n) - 1, sum = 0;
function queen(row, ld, rd) {
if (row !== limit) {
let pos = limit & ~(row | ld | rd);
while (pos) {
let p = pos & -pos; // 得到最右侧可放子的位置
pos -= p; // 除去这个位置
queen(row|p, (ld|p) << 1, (rd|p) >> 1)
}
} else sum++;
}
queen(0, 0, 0);
return sum;
}

时间效率

数组中出现次数超过一半的数字

思路1:超过一半的数组一定出现在中位数的位置,结合快排的思想,找到中位数
思路2:遍历所有数字,遇到与major相同的元素时,计数器+1,反之-1。计数器为0时,更换major。返回最后的major

注意:最后需要检查数组是否有满足条件的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function findMajor(nums) {
if (!nums.length) {
return;
}

let major;
times = 0;
nums.forEach((num) => {
if (times === 0) {
major = num;
times = 1
} else if (num === major) {
times++;
} else {
times--;
}
});

if (nums.reduce((sum, num) => { return sum + (num === major); }, 0) * 2 > nums.length) {
return major;
}
}

最小的k个数

同样的两种思路:

  1. 利用快排的中的partition,找到前k个数,时间复杂度O(n),但是会修改数组
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    function partition(arr, start, end) {
    if (start >= end) {
    return;
    }
    let index = start -1;
    let split = arr[end];
    for (let i = start; i < end; i++) {
    if (arr[i] < split) {
    index++;
    if (i != index) {
    [arr[i], arr[index]] = [arr[index], arr[i]];
    }
    }
    }
    index++;
    arr[end] = arr[index];
    arr[index] = split;
    return index;
    }

    function minKNums(nums, k) {
    if (!nums || k <= 0) {
    return;
    }

    if (k >= nums.length) {
    return nums;
    }

    let start = 0,
    end = nums.length - 1,
    index = partition(nums, start, end);

    while(index != k-1) {
    if (index > k-1) {
    end = index - 1;
    } else {
    start = index + 1;
    }
    index = partition(nums, start, end);
    }

    console.log(nums.slice(0, k));
    }
  2. 维护一个排序的长度为k的数据容器,当容器满时,根据容器最大值更新容器,时间复杂度O(nlogk),空间复杂度O(k),但是在k较小时,性能很接近
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class MinSet {
constructor() {
//...
}

get max() {
//...
}

get len() {
//...
}

input(element) {
//...
}

remove(element) {
//...
}

toString() {

}
}

function minKNums(nums, k) {
if (!nums || k <= 0) {
return;
}

if (k >= nums.length) {
return nums;
}

let set = new MinSet();
nums.forEach(num => {
if (set.len === k) {
if (set.max.value > num) {
set.remove(set.max);
set.input(num);
}
} else {
set.input(num);
}
});

set.toString();
}

数据流的中位数

使用二叉树、最大堆最小堆完成。保证最大堆和最小堆的数目差不超过1即可。当然,首先要实现最大和最小堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
function inherit(son, father) {
let prototype = Object.create(father.prototype);
prototype.constructor = son;
son.constructor = prototype;
}

function Heap() {
this.heap = [];
}

Heap.prototype.parent = function(i) {
return (i+1) / 2 - 1;
}

Heap.prototype.left = function(i) {
return 2 * i + 1;
}

Heap.prototype.right = function(i) {
return 2 * i + 2;
}

Heap.prototype.size = function() {
return this.heap.length;
}

function MinHeap() {
Heap.call(this);

function minify(index) {
let l = this.left(index),
r = this.right(index),
size = this.length;

if (l < size && this.heap[l] < this.heap[index]) {
i = l;
} else {
i = index;
}

if (r < size && this.heap[r] < this.heap[i]) {
i = r;
}
if (i != index) {
[this.heap[index], this.heap[i]] = [this.heap[i], this.heap[index]];
minify(i);
}
return;
}

this.add = function(num) {
this.heap.push(num);
minify(this.heap.length / 2 - 1);
}

this.shift = function() {
let num = this.heap.shift();
minify(this.heap.length / 2 - 1);
return num;
}

this.top = function() {
return this.heap[0];
}

}
inherit(MinHeap, Heap);

function MaxHeap() {
Heap.call(this);

function maxify(index) {
//... 同上
}

this.add = function(num) {
//... 同上
}

this.shift = function() {
let num = this.heap.shift();
maxify(this.heap.length / 2 - 1);
return num;
}

this.top = function() {
//... 同上
}
}

/* 借助上面的MinHeap和MaxHeap实现 */
function DynamicArray() {
this.min = new MinHeap();
this.max = new MaxHeap();
}

DynamicArray.prototype.insert = function(num) {
let minSize = this.min.size(),
maxSize = this.max.size();

if (minSize === maxSize) {
this.min.add(num);
// 插入在最小堆,但是数字在最大堆的范围
if (maxSize > 0 && num < this.max.top()) {
this.max.add(this.min.shift());
}
} else {
this.max.add(num);
// 同理,插入在最大堆,但是数字在最小堆的范围
if (minSize > 0 && num < this.min.top()) {
this.min.add(this.max.shift());
}
}
}

DynamicArray.prototype.getMedian = function() {
let size = this.min.size() + this.max.size();

if (!size) {
console.warn('Empty array.');
}

if (size & 0x01) {
return this.min.top();
} else {
return (this.min.top() + this.max.top()) / 2;
}
}

连续子数组的最大和

前提:数组由正负数组成。

经典的动态规划问题。用双指针的方法,采用贪婪算法,用f[i]表示以第i位结尾的最大和,当f[i-1]<=0时,可以抛弃之前的和,以当前元素为f[i]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function maxSum(nums) {
if (!nums.length) {
return;
}

let len = nums.length;
let res = nums[0];

for (let i = 1; i < len; i++) {
if (res <= 0) {
res = nums[i];
} else {
res += nums[i];
}
}

return res;
}

1~n整数中1出现的次数

这种数学题,多半会有找规律的简单方法。普通的方法是,逐个寻找每个数字中1出现的次数,时间复杂度O(nlogn)。逐位寻找1出现的次数,再累加,时间复杂度可以降至O(logn)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function numOf1(num) {

function count(str) {
let first = str[0];
let len = str.length;

if (str.length === 1) {
return first > 0;
}
// 第一位中1出现的次数
let firstCount = 0;
if (first > 1) {
firstCount = Math.pow(10, len-1);
} else if (first == 1) {
firstCount = +str.substr(1) + 1;
}

// 剩下的位数可以通过排列组合计算
let otherCount = first * (len - 1) * Math.pow(10, len-2);
return firstCount + otherCount + count(str.substr(1));
}

if (num < 1) {
return 0;
}

let numStr = `${num}`;
count(numStr);
}

数字序列中某一位的数字

前提:数字以0123456789101112131415...的格式序列化

需要找到规律。1位数占用10位,2位数占用180位,3位数占用2700位,……,n位数占用9 * (10 ^ n)位。通过循环确定位数后,对n取余,能知道是n位数第几个数的第几位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function findKthDigit(k) {
function countInDigits(n) {
return (n === 1 ? 10 : 9 * Math.pow(10, n-1));
}
// 第m个n位数的第k位
function find(n, m, k) {
// 找到n位数的起始
let start = Math.pow(10, n - 1);
let num = start + m;
return num / Math.pow(10, n - k - 1);
}

if (k < 0) {
return -1;
}

let digits = 1;
while (k >= countInDigits(digits)) {
k -= countInDigits(digits++);
}

return find(digits, k / digits, k % digits);
}

把数组排成最小的数

要求:拼接数组中的数,使之最小

此题的思路在拓展排序的规则。当组合mn < nm时,说明m应该在前,即元素间的比较准则是mn和nm的大小关系。注意大数问题,应该比较字符串。

1
2
3
4
5
function minComb(nums) {
nums.sort((a, b) => {
return ('' + a + b > '' + b + a ? 1 : -1);
});
}

把数字翻译成字符串

前提:0翻译成‘a’,1翻译成‘b’,逐位进行翻译,有时会有多种结果
要求:求可能的结果数目

动态规划,避免重复解决子问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function translateCount(numStr) {
if (!numStr || +numStr < 0) {
return 0;
}

let count;
let counts = [];
for (let i = numStr.length - 1; i >= 0; i--) {
if (i === numStr.length) {
count[i] = 1;
continue;
}

count = count[i+1];
let comb = +(count[i] + count[i+1]);

if (comb <= 25 && comb >= 10) {
count += (i < numStr.length - 2 ? counts[i+2] : 1);
}
count[i] = count;
}

return count[0];
}

礼物的最大价值

前提:从m*n的格子中拿礼物,从左上角开始,每次向下或向右取一格,直到右下角
要求:礼物价值总和最大

和上面的思路很类似,通过循环替代递归。比带有备忘录的递归更省时间和空间。坐标是(i, j)的格子最大总和只取决于(i-1, j)和(i, j-1)。因此可以用一维数组代替二维数组,减少空间使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getMaxValue(nums) {
if (!nums.length || !nums[0].length) {
return 0;
}

let row = nums[0];
let rows = nums.length;
let columns = nums[0].length;
for (let i = 0; i < rows; i++) {
for (let j = 0; j < columns; j++) {
let up = (i > 0 ? row[j] : 0)
let left = (j > 0 ? row[j-1] : 0);
row[j] = Math.max(left, up) + nums[i][j];
}
}
return row[columns - 1];
}

最长的不包括重复字符的子字符串

要求:返回字符串的长度

用类似动态规划的思路,借助之前的结果递推。同时,使用Object存储对应字符上次出现的位置,可以保证O(n)的时间复杂度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function longestSubStrWithDup(str) {
if (!str.length || str.length < 2) {
return str.length;
}

let pos = {};
let strArr = Array.from(str);
let maxLen = 0;
let curLen = 0;

strArr.forEach((char, num) => {
// 此前未出现过或出现在当前最长子字符串之外
let position = pos[char];
if (typeof position !== 'undefined' || position + curLen < i) {
curLen++;
} else {
// 出现在此前的最长子字符串中时,需要更新长度值
if (curLen > maxLen) {
maxLen = curLen;
}
curLen = i - position;
}
// 更新位置
pos[char] = i;
});

if (curLen > maxLen) {
maxLen = curLen;
}

return maxLen;
}

丑数

前提:丑数是指只包含因子2、3、5的数。求给定位置的丑数

普通的思路是通过循环依次判断数字是否为丑数直到找到。在允许空间消耗时,我们用数组去存储所有的丑数,并不断更新数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function uglyNum(k) {
if (k <= 0) {
return 0;
}

let count = 1;
let ugly = [1];
let ugly2 = 0;
let ugly3 = 0;
let ugly5 = 0;

while(count < k) {
uglynums.push(Math.min(ugly[ugly2]*2, ugly[ugly3]*3, ugly[ugly5]*5));
while (ugly[ugly2]*2 <= ugly[count]) {
ugly2++;
}
while (ugly[ugly3]*3 <= ugly[count]) {
ugly3++;
}
while (ugly[ugly5]*5 <= ugly[count]) {
ugly5++;
}
count++;
}

return ugly.pop();
}

第一个只出现一次的字符

哈希表,JavaScript中使用Object即可。第一次扫描时去除所有重复元素,第二次扫描时,打印第一个字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function firstUniqueChar(str) {
let strArr = Array.from(str);
let chars = {};
strArr.forEach(char => {
if (!chars[char]) {
chars[char] = 1;
} else {
chars[char]++;
}
});
strArr.forEach(char => {
if (chars[char] === 1) {
return char;
}
});
return false;
}

类似问题都可以用哈希表解决。

数组中的逆序对

逆序对的存在是数组未排序的结果。因此使用排序算法得到交换元素的次数即可。最直观的使用冒泡排序,时间复杂度为O(n^2)。使用归并和快排可以达到O(nlogn)的时间复杂度。

略。

两个链表的第一个公共节点

因为链表从交汇节点后拥有相同的节点,所以,从后向前回溯,直到节点不同为止即可。时间复杂度O(m+n),空间复杂度O(m+n)。若不使用辅助栈,空间复杂度还可降至O(1)。

1
2
3
4
5
6
7
8
9
10
11
function findFirstCommonNode(list1, list2) {
if (!list1 || !list2) {
return false;
}

// 遍历list1和list2到尾节点
// ...

// 向前回溯直到节点不同
// ...
}

排序数组中查找数字(二分查找)

排序数组中数字出现的次数

思路是,查找到数字第一次出现的地方和最后一次出现的地方。通过二分法查找,数字较小时,在右半边;较大时,在左半边;相等时判断左/右是否还有该元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
function numOfK(arr, k) {
if (!arr.length) {
return 0;
}

function findFirst(start, end) {
if (start > end) {
return -1;
}

let middle = (start + end) / 2;
let median = arr[middle];

if (median < k) {
start = middle + 1;
} else if (median > k) {
end = middle - 1;
} else {
if (middle > 0 && arr[middle-1] === median) {
end = middle - 1;
} else {
return middle;
}
}

return findFirst(start, end);
}

function findLast(start, end) {
if (start > end) {
return -1;
}

let middle = (start + end) / 2;
let median = arr[middle];

if (median < k) {
start = middle + 1;
} else if (median > k) {
end = middle - 1;
} else {
if (middle < arr.length-1 && arr[middle+1] === median) {
start = middle + 1;
} else {
return middle;
}
}

return findLast(start, end);
}

let first = findFirst(0, arr.length-1);
let last = findLast(0, arr.length-1);

if (findFirst() === -1) {
return 0;
}
return last - first + 1;
}

寻找从0~n-1中缺少的数字

数组事先排序时,借助二分法查找,时间复杂度O(logn);未排序时,通过求和和n(n-1)/2求差得出(不考虑大数问题的话)。中间元素和下标相等时,在右半边寻找;不相等时,若左侧元素相等,则返回该元素,左侧元素不等时,在左半边寻找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function findLeftNum(arr) {
let len = arr.length;
if (!len || len < 2) {
return;
}

let left = 0;
let right = len - 1;

while (left <= right) {
let middle = (left + right) >> 1;
if (arr[middle] === middle) {
left = middle + 1;
} else {
if (middle > 0 && arr[middle-1] !== middle-1) {
right = middle - 1;
} else {
return middle;
}
}
}

if (left > right) {
return len;
}
}

数组中和下标相等的元素

前提:数组元素都为整数,且单调递增。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function numsEqualsToIndex(nums) {
if (!nums.length) {
return -1;
}

let left = 0,
right = nums.length-1;

while (left <= right) {
let mid = (left + right) >> 1;

if (nums[mid] === mid) {
return mid;
}

if (nums[mid] > mid) {
right = middle - 1;
} else {
left = middle + 1;
}
}

return -1;
}

二叉搜索树的第k大的节点(遍历)

利用二叉搜索树的中序遍历的特点,找到第k大的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function findKthNode(root, k) {
if (k < 1 || !root) {
return null;
}

let res = null;

if (root.left) {
res = findKthNode(root.left, k);
}

if (res === null) {
if (k === 1) {
res = root;
}
k--;
}

if (root.right) {
res = findKthNode(root.right, k);
}

return res;
}

二叉树的深度

使用递归,比较左右子树的深度,取较大值。

1
2
3
4
5
6
7
8
9
10
function treeDepth(root) {
if (!root) {
return 0;
}

let left = treeDepth(root.left);
let right = treeDepth(root.right);

return Math.max(left, right) + 1;
}

判断一棵树是否是平衡二叉树

可以使用上面的思路,递归判断除叶节点外每个节点的左右子树深度。但是这样会重复遍历节点。但是如果使用后序遍历,就可以先遍历左右子树,再回到根节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function isBalanceTree(root) {
function tree(root) {
if (!root) {
return {
depth: 0,
flag: true
};
}

let left = tree(root.left);
let right = tree(root.right);

if (left.flag && right.flag) {
let diff = left.depth - right.depth;
if (diff <= 1 && diff >= -1) {
return {
depth: 1 + Math.max(left.depth, right.depth),
flag: true
};
}
}

return {
flag: false
};
}

tree(root);
}

数组中数字出现的次数(位运算)

在数组中找到唯二两个只出现一次的元素

前提:数组中其他元素都出现两次。

还是使用异或,不过这次异或的结果是两个数组的异或结果。找到结果中第一位1,说明这一位两个数不同,据此将数组分成两部分,分别异或,得到最终结果。时间复杂度O(n),空间复杂度O(1)。

1
2
3
4
5
6
7
8
9
10
function find2UniqueNums(nums) {
let sum = nums.reduce((sum, cur) => sum ^ cur);
let index = sum & -sum;
let nums1 = nums.filter(num => num & index);
let nums2 = nums.filter(num => !num & index);
return {
unique1: nums1.reduce((sum, cur) => sum ^ cur);
unique2: nums2.reduce((sum, cur) => sum ^ cur);
},
}

在数组中找到唯一一个出现一次的数字

前提:其余数字都出现3次。

使用和上面类似的思路,使用二进制表示每一个数,用数组存储各位1出现的和,累加后,将每一位数字对3取余,得到的数字就是结果的二进制表示。

和为s的若干数字(双指针)

2个数

前提:数组是单调递增的。因此可以在头尾放两个指针,向中间靠拢,逼近最终结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function sumS(nums, s) {
let len = nums.length;

if (!len || len < 3) {
return [];
}

let left = 0;
let right = len - 1;

while (left < right) {
let sum = nums[left] + nums[right];

if (sum === s) {
return [nums[left], nums[right]];
} else if (sum < s) {
left++;
} else {
right--;
}
}
return [];
}

正数序列

前提:打印出和为s的连续正数序列,长度最少为1。

若使用双指针,需要先确定上下界。这里的序列起始的上界为n/2,下界是1。然后使用类似的方法,确定起始后,后移后界求序列。时间复杂度O(n^2)。然而,实际上,确定下界后,连续正数序列和与序列长度满足函数关系f(m, n) = (m + n)(n - m + 1) / 2(其中m为起始值,n为截止值)。问题变成了检验后界是否满足一元二次方程组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function sumSSeq(s) {
if (s < 3) {
return;
}

function print(i, j) {
for (let index = i; index <= j; index++) {
console.log(index);
}
}

for (let i = 1, limit = Math.floor(s/2); i <= limit; i++) {
let j = i+1;
while (j <= limit+1) {
let sum = (i + j) * (j - i + 1) / 2;
if (sum >= s) {
if (sum === s) {
print(i, j);
}
break;
}
j++;
}
}
}

翻转字符串(多次翻转)

翻转单词的顺序

要求:翻转一句话中所有单词的顺序。

此题可以先翻转所有字符,再翻转每个单词内的字符顺序。

1
2
3
function reverseByWord(sentence) {
return sentence.split(' ').reverse().join(' ');
}

左旋转字符串

要求:将字符串头部的若干字符转移到字符尾部。

使用类似上面的思路,可以不用substring方法,用翻转也可实现,不过在JS中无法对字符串自由写入,法2反而不如法1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 法1,使用substring
function leftReverse(str, n) {
if (!str || n >= str.length) {
return str;
}

return str.substring(n) + str.substring(0, n);
}

// 法2,使用reverse
function leftReverse(str, n) {
if (!str || n >= str.length) {
return str;
}

let strArr = Array.from(str);
return strArr.slice(0, n).reverse().concat(strArr.slice(n).reverse()).reverse().join('');
}

队列的最大值(队列)

滑动窗口的最大值

前提:固定滑动窗口的宽度
要求:返回窗口内的最大值组成的数组

使用双端队列去维护滑动窗口内的最大值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function maxInWindow(nums, k) {
if (!nums.length || k > nums.length) {
return;
}

let queue = [];
let res = [];

for (let i = 0; i < k; i++) {
while(nums[queue[queue.length-1]] < nums[i]) {
queue.pop();
}
queue.push(i);
}

for (let i = k; i < nums.length; i++) {
res.push(nums[queue[0]]);

while(nums[i] >= nums[queue[queue.length-1]]) {
queue.pop();
}

// 出界的大值要移出
if (queue[0] <= i - k) {
queue.shift();
}

queue.push(i);
}
res.push(nums[queue[0]]);
return res;
}

队列的最大值

要求:设计队列,使max,shift和push的时间复杂度为1。

因为在push和shift操作时,队列就像上面的滑动窗口,可以用相同方法实现max。

略。


建模能力

n个骰子的点数(化递归为循环)

要求:输入n,返回n个骰子朝上一面的点数之和的概率分布。

使用循环的角度考虑,骰子递增时,点数和为k的可能性为k-6,k-5,k-4,……,k-1的可能性之和。因此我们可以得到下面的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function posibilities(n) {
if (n < 1) {
return [];
}

let res = [0, 1, 1, 1, 1, 1, 1];

for (let i = 2; i <= n; i++) {
for (let j = 6 * n; j > 0; j--) {
res[j] = res.slice(Math.max(0, j-6), j).reduce((sum, cur) => sum + (cur || 0), 0);
}
}

return res.slice(1).map(num => num / Math.pow(6, n));
}

扑克牌中的顺子

前提:从扑克牌中随机取5张,大小王可以当做任意数字,判断是否为顺子

这里我们假设传入的数组已经按照扑克牌中的大小对牌进行了排序。整体分3步,首先向数组的指定项插入数字,然后判断0的数目,最后看0的数目是否能填满undefined的个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function isSeq(nums) {
if (!nums.length || nums.length < 1) {
return false;
}

let numbers = [];
let zeros = 0;
let flag = false;
let index = -1;

nums.forEach(num => {
if (num === 0) {
zeros++;
continue;
}

if (!numbers[num]) {
numbers[num] = true;
} else {
// 有对子就不是顺子
return false;
}
});

numbers.forEach((num, idx) => {
if (flag) {
if (typeof num !== 'undefined') {
zeros--;
if (zeros-- < 0) {
index = idx;
return;
}
}
} else {
if (typeof num !== 'undefined') {
flag = true;
}
}
});

return numbers.slice(index).every(num => typeof num === 'undefined');

}

约瑟夫问题

字面意义的解法就不说了。根据《剑指offer》上的递推公式,可以得到f(n,m)=[f(n-1,m)+m]%n。因此,可以用循环实现。

1
2
3
4
5
6
7
8
9
10
11
function joseph(n, m) {
if (n < 1 || m < 1) {
return -1;
}

let remains = 0;
for (let i = 1; i < n; i++) {
remains = (remains + m) % i;
}
return remains;
}

股票的最大利润

也即起始、末尾数对最大差值区间。当遍历到f[i]时,寻找之前的最小值,即可得到利润,遍历整个数组即可得到最大利润,时间复杂度为O(n)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function maxBenifit(nums) {
const len = nums.length;
if (!len || len < 2) {
return;
}

let min = nums[0];
let res;

for (let i = 1; i < len; i++) {
let benifit = nums[i] - min;
if (typeof res === 'undefined' || res < benifit) {
res = benifit;
}
if (benifit < 0) {
min = nums[i];
}
}
return res;
}

不使用控制结构语句求解1+2+3+…+n

使用递归等等价形式即可。

1
2
3
function sum(n) {
return Array(n).reduce((sum, i) => sum + i, 0);
}

不用加减乘除做加法

使用异或可以得到没有进位的结果。可以通过位与运算得到都是1的位。和异或结果异或,直到位与结果都为0为止。

1
2
3
4
5
6
7
8
9
10
11
12
function add(a, b) {
let sum = a ^ b,
extra = a & b;

while (!extra) {
let tmp = sum & extra;
sum = sum ^ (extra << 1);
extra = tmp;
}

return sum;
}

构建乘积数组

要求:给定数组A,构建数组B,其中每个元素是A中缺少对应位元素的总乘积。不能使用除法。

使用两个辅助数组分别存储数组从前到后和从后到前两段元素的乘积。空间复杂度O(n)的情况下,可以在O(n)时间上得到结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function multiArr(nums) {
if (!nums.length || nums.length < 2) {
return [];
}

let m1 = 1,
m2 = 1,
mArr1 = [],
mArr2 = [],
res = [];

for (let i = 0; i < n; i++) {
m1 *= nums[i];
m2 *= nums[n - i - 1];
mArr1.push(m1);
mArr2.unshift(m2);
}

for (let i = 0; i < n; i++) {
res.push(mArr1[i] * mArr2[i]);
}

return res;
}

《黑客与画家》是Paul Graham的博文集。出版于2006年。它从一个“黑客”的角度阐述了编程,互联网程序等时兴的概念。深刻新鲜而发人深省。其对工作、语言的认识别具特色,而在学校教育和贫富分化等方面的认识却略显偏颇。

全书大致分为3部分

  • 1-4章解释黑客如何成长以及如何看待世界
  • 5-9章介绍创业、工作的方法论
  • 10-15章讨论编程语言的特点和使用方法

下面仅摘出些句子。个人评注已加粗。

为什么书呆子不受欢迎

  • 在人产生良知前,折磨是种娱乐。
  • 学校的真正目的是把儿童都关在一个地方,以便大人们白天可以腾出手来把事情做完(哈哈哈

黑客与画家

  • 编程语言是用来帮助思考程序的,而不是用来表达你已经想好的程序
  • 大多数黑客不是通过大学课程学会编程的,他们从实践中学习,13岁时就已经自己动手写程序了。
  • debug对我来说属于轻松的工作
  • 软件的部分功能就是解释自身,软件的使用方式最好能符合用户的直觉,源代码应该可以自己解释自己
  • 程序是写出来给人看的,附带能在机器上运行

不能说的话

  • (流行)第一批的接收者总是带有很强的抱负心,他们有自觉的精英意识,想把自己与普通人区分开来
  • 流行趋势确立以后,第二接收者就加入进来了,他们接受流行,不是因为想要与众不同,而是害怕与众不同
  • 与笨蛋辩论,你也会变成笨蛋

良好的坏习惯

  • 在我看来,一个人们拥有言论自由和行动自由的社会,往往最有可能采纳最优方案(真的么?

另一条路

  • (互联网程序)不需要为新版本付出额外的费用,或者做额外的准备,甚至可能你都不知道软件已经升级了
  • 互联网软件则完全不同,修改起来很方便。软件的发布过程可以分解成一系列的渐进式修改(快步小跑
  • 软件应该做到用户认为它可以做到的事情。但是,你不知道用户到底怎么想
  • 没有盗版是一种“优势”,但也是一个问题。一定数量的盗版对软件公司时有好处的。因为不管你的软件定价多少,有些用户永远不会购买
  • 要求用户做得越多,你担的风险就越大
  • 管理企业其实很简单,只要记住两点就可以了:做出用户喜欢的产品,保证开支小于收入
  • 从制造简洁的产品开始着手,首先要保证你自己愿意使用。然后,迅速作出1.0版,并不断加以改进
  • 无论如何,你都要使用自己的软件

如何创造财富

注意:标题不等于“如何致富”
  • 从经济学观点看,你可以把创业想象成一个压缩过程
  • 如果你想赚100万美元,就不得不忍受相当于100万美元的痛苦。创业公司将你所有的压力压缩到三四年
  • 通过创造有价值的东西而致富,优势不仅在于它合法,还在于它更简单,因为你只需要做出别人需要的东西就可以了
  • 金钱不是财富,而只是我们用来转移财富所有权的东西
  • 公司就是许多人聚在一起创造财富的地方,能够制造更多人们需要的东西
  • 人们需要的东西就是财富
  • 上班的日子不如上学的日子有趣,但是有人付钱给你,而不是你付钱给学校
  • 创造财富是大多数公司盈利的手段
  • 上班的日子为什么会差别这么大?不要搞糊涂了,你现在已经从顾客变成了仆人
  • 收入和风险是对称的,所以如果有巨大的获利可能,就必然存在巨大的失败可能。如果你有一个令你感到安全的工作,你是不会致富的
  • 创业的付出和回报虽然总体上是成比例的,但是在个体上是不成比例的
  • 只有在快速获得巨大利益的激励下,你才会去挑战那些困难的问题,否则你根本不愿意去碰它们

关注贫富分化

  • 进入社会以后,你不能总是靠父母养活,如果你需要什么东西,要么你自己做出来,要么做其他东西与需要的人交换金钱,再用金钱去买你想要的东西
  • 技术肯定加剧了有技术者与无技术者之间的生产效率差异
  • 技术在加大收入差距的同时,缩小了大部分的其他差距

防止垃圾邮件的一种方法

  • 我对贝叶斯方法寄予厚望,因为它的过滤能力可以随着垃圾邮件一起进化

设计者的品味

  • 他想要的学生不仅应该技术过硬,还应当能够使用技术做出优美的产品
  • 你需要的是咬牙向前冲刺的痛苦,而不是脚被钉子扎破的痛苦。解决难题的痛苦对设计师有好处,但是对付挑剔的客户的痛苦或者对付质量低劣的建材的痛苦就是另外一回事了
  • 在历史的任何时刻都有一些热点项目,一些团体在这些项目上做出伟大的成绩。如果你远离这些中心,几乎不可能单靠自己就取得伟大成果

编程语言解析

  • 程序员的时间要比计算机的时间昂贵得多
  • 长期使用某种语言,你就会慢慢按照这种语言的思维模式进行思考
  • 有些人认为编程语言应该防止程序员干蠢事,另一些人则认为程序员应该可以用编程语言干一切他们想干的事(即静态类型语言和动态类型语言
  • 事实上,有两种程度的面向对象编程:某些语言允许你以这种风格编程,另一些语言则强迫你一定要这样编程

一百年后的编程语言

  • 当我说Java不会成功时,我的意思是它和Cobol一样,进化之路已经走到了尽头(哦?
  • 一种语言的内核设计越小、越干净,它的生命力就越顽强
  • 用足够灵活的语言,聪明的程序员能写多好,笨的程序员就能写多烂
  • 一百年后的程序员最需要的编程语言就是可以让你毫不费力写出程序第一版的编程语言

拒绝平庸

  • Lisp很值得学习。你掌握它之后,会感动它给你带来的极大启发
  • 大概在1960年,Lisp语言引入了垃圾回收机制。……闭包是20世纪60年代Lisp语言引入的功能……宏也是60年代中期Lisp语言引入的,现在还是一片处女地(这里的“宏”为Lisp中的defmacro
  • 这里有一个评估竞争对手的妙招——关注他们的招聘职位

书呆子的复仇

  • 符号(Symbol)实际上是一种指针,指向存储在散列表中的字符串
  • 列表是Lisp的基本数据结构
  • 让客户感到满意才是你的设计方向。只要赢得用户,其他的事情就会接踵而至
  • JavaScript的写法比Lisp和Ruby稍微长一点,因为JavaScript依然区分语句和表达式
  • 所有这些语言都是图灵等价的
  • “任何C或Fortran程序复杂到一定程度之后,都会包含一个临时开发的、只有一半功能的、不完全符合规格的、到处都是bug的、运行速度很慢的Common Lisp实现。”
  • 想解决一个困难的问题,有三条路:1)使用一种强大的语言,2)为这个难题写一个事实上的解释器,3)你自己变成这个难题的人肉编译器(翻译过来就是语言本身/设计模式/问题抽象

梦寐以求的编程语言

  • 优秀的函数库的重要性将超过语言本身
  • 就算委员会只有两个人,还是会妨碍“再设计”,典型例子就是软件内部的各个接口有不同的人负责。这时除非两个人都同意改变接口,否则接口就无法改变

设计与研究

  • “用户需要的设计”≠“用户要求的设计”
  • 设计必须以人为本
  • 在软件领域,贴近用户的设计思想被归纳为“弱即是强”模式。
  • 一种功能有限但易于使用的软件可能对用户有更大吸引力
  • 先做出原型,在逐步加工做出成品,这种方式有利于鼓舞士气,因为它使得你随时都可以看到工作的成效

由于开发时间短,迭代速度快的特点,H5页面已大量用在各种移动端App和活动页中。响应时间作为和用户体验的重要部分,如果不能在这上面给用户可靠性和安全感,将会大大影响用户体验,进而影响用户使用产品的积极性。当页面并不复杂时,用户若能在1s内打开页面,看到信息的展示,会感觉速度还行,可以接受。当界面需要5s甚至以上才能显示时,对用户来说时不可接受的。

下文的标准和建议主要来自钉钉团队的H5性能优化方案

标准

通常来说首屏(onload)时间即资源加载完成时间建议在600ms内,首字时间时间在150ms内,根据网络类型做相应调整:

  • 2G,完全加载时间10s内,10s内占比80%
  • 3G,完全加载时间6s内,6s内占比60%
  • 4G和WiFi,完全加载时间3s内,3s内占比70%

(标准不是死的,上面只是举个范例)

这个时间可以在Chrome开发者工具里的Network中看到,也可以通过JS的performance API查询到

1
performance.timing.domContentLoadedEventEnd - performance.timing.connectStart

优化手段

资源加载

  • 按需加载,减少不必要资源的加载,不过这可能会增加页面的重绘
  • Lazyload,延迟加载,即不影响整体效果的图片展现给用户后再加载
  • 滚屏加载,根据字面意义,在下拉滚屏时加载额外数据,减少翻页时的重新加载和渲染
  • 响应式加载,即根据用户终端媒介加载不同大小的图片资源
  • 异步记载,异步加载第三方资源,防止影响本身的页面性能

此外,还有一些优化加载体验的方式:

  • 显示loading进度条,让用户能明确感知到页面的加载进度
  • 避免3xx、4xx、5xx的http状态码,因为3xx的跳转会影响加载时间,4xx和5xx通常是第三方资源的问题,可能会影响整个页面的展示。
  • favicon.ico,需要保证这个图标存在,且尽可能地小,并设置一个较长的缓存时间
  • 首次加载资源越小越好
  • 避免使用自定义中文字体(因为体积大)

图片使用

  • 格式,webp,jpg,png24/32较常用。jpg较为常用,避免使用png的大图片
  • 像素,一般来说不建议宽度超过640px
  • 合并,使用CSS Sprites,尤其是基本不变,大小类似的操作类型图标
  • 避免重设大小,即展示多大就加载多大的图片
  • 避免DataURL,DataURL是用Base64的方式,将图片变成一串文本编码放入代码的方式。尽管它能减少一次http交互的请求,但数据体积通常比二进制图片大1/3,且不会被缓存。
  • 使用替代方案,如CSS3,SVG或iconfont来完成简单的图片

服务端

  • Gzip,服务端要开启Gzip压缩
  • 设置合理的缓存时间,对静态资源,将缓存过期时间设置得长一些
  • 分域名部署,将动态资源和静态资源放置在不同的域名下,这样,静态资源请求时,不会带上动态域名中所设置的cookie头信息,从而减少http请求的大小
  • 减少Cookie,这部分数据使用的是上行流量,上行带宽更小,所以传输速度更慢,因此要尽量精简其大小。
  • CDN加速,部署CDN服务器,或使用第三方CDN服务,优化不同地域的接入速度。

代码

  • JS,CSS合并,除特殊情况外,所有JS压缩打包成一个文件,所有CSS压缩打包成一个文件,减少请求次数
  • 外联使用JS,CSS,有效利用缓存
  • 压缩HTML,JS,CSS,压缩代码可以减少大小到原来的1/3以下
  • 使用版本号标注JS、CSS等资源,让用户及时获取到最新的代码
  • 位置,通常CSS在前,JS在后。基础的JS文件,如日志,polyfill可以放在头部
  • 避免空src地址,在某些浏览器中可能会导致增加一个无效的http请求
  • 禁止CSS表达式,会让页面多次执行计算,造成卡顿等性能问题
  • 避免空css规则,降低渲染成本
  • 避免直接设置元素style,不利于复用和缓存,不过有些情况下确实是没有办法的办法

后台接口

  • 接口合并,尽量减少http请求次数
  • 缓存,在一些数据新旧敏感性不高的场景下,缓存接口数据

写个人项目是最爽的,再稀烂的代码都是自己的孩子,一把屎一把尿写出来的,总能认得。但是阅读本文的你,多半是给别人写码,面向老板编程的。你的码保不齐日后要别人维护。为了写出放心的码,提高搬砖效率,统一编码规范是必然的选择。

Boss Oriented Programming

有观众可能要问了,我们不是有lint工具吗?

就你话多

HTML

基本

doctype

每个页面开头,使用简单的<!DOCTYPE html>来启用标准模式。

1
2
3
4
5
<!-- bad -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<!-- good -->
<!DOCTYPE html>

charset

无特殊情况时,统一使用UTF-8

lang

推荐在html元素上使用lang属性

viewport

建议指定页面的viewport属性,保证移动端的友好展示

1
<meta name="viewport" content="width=device-width, maximum-scale=1.0, minimum-scale=1, user-scalable=no">

title

head必须要有title标签。

IE兼容模式

如不是特殊需要,通过edge mode通知IE使用最新的兼容模式。

1
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">

CSS和JavaScript引入

  • 引入CSS和JavaScript时不需要指明type,因为text/csstext/javascript分别是他们的默认值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!-- bad -->
    <!DOCTYPE html>
    <head>
    <link type="text/css" rel="stylesheet" href="index.css">
    <script type="text/javascript" src="index.js"></script>
    </head>

    <!-- good -->
    <!DOCTYPE html>
    <head>
    <link rel="stylesheet" href="index.css">
    <script src="index.js"></script>
    </head>
  • CSS在<head></head>中引入,基础JS脚本在<head></head>引入,其余在<body>结束标签前引入

缩进

必须使用两个空格表示一个缩进层级,禁止使用tab

1
2
3
4
5
6
7
8
9
<!-- bad -->
<div>
<p>just a example</p>
</div>

<!-- good -->
<div>
<p>just a example</p>
</div>

换行

单行不能超过120个字符

标签名称和标签属性统一使用小写

1
2
3
4
5
<!-- bad -->
<Div Id="foo"></Div>

<!-- good -->
<div id="foo"></div>

综上,下面是一个建议的html脚手架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, maximum-scale=1.0, minimum-scale=1, user-scalable=no">
<meta name="renderer" content="webkit">
<meta name="author" content="shenlvmeng">
<meta name="description" content="描述">
<meta name="keyword" content="关键词">
<title>Foo</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<script src="index.js"></script>
</body>
</html>

标签

  • 非自闭合标签必须有开始和结束标签,自动闭合标签不建议在结尾处使用斜线/
    1
    2
    3
    4
    5
    6
    <!-- bad -->
    <div class="foo" />
    <img src="foo.png" />
    <!-- good -->
    <div class="foo"></div>
    <img src="foo.png">
  • 建议使用语义化标签
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!-- good -->
    <header></header>
    <section>
    <nav>Navbar</nav>
    <article>
    <time>2017</time>
    <figure></figure>
    </article>
    <aside></aside>
    </section>
    <footer></footer>
  • 避免标签嵌套层级过深,尤其是无语义的嵌套
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- bad -->
    <div class="foo">
    <div class="bar">
    <div class="container">
    <div class="content">
    </div>
    </div>
    </div>
    </div>

命名

标签名

必须使用小写字母加连字符的命名方式

1
2
3
4
<!-- bad -->
<MyTag></MyTag>
<!-- good -->
<my-tag></my-tag>

class属性

使用小写字母加连字符,需要在Javascript中使用时,以J_开头,接大驼峰命名

1
2
3
4
<!-- bad -->
<div class="FooBar J_foo-bar"></div>
<!-- good -->
<div class="foo-bar J_FooBar"></div>

id属性

同上。建议使用class属性关联CSS。

属性

使用双引号包裹

1
2
3
4
<!-- bad -->
<div class='foo'></div>
<!-- good -->
<div class="foo"></div>

Boolean属性

不要为Boolean属性指定值。

一个元素中 Boolean 属性的存在表示取值true,不存在则表示取值false。

1
2
3
4
5
6
7
8
9
10
11
12
<!-- bad -->
<input type="text" name="nickname" disabled="disabled">
<input type="checkbox" name="hobbies" value="bicycle" checked="checked">
<select>
<option value="FrontEnd" selected="selected">FrontEnd</option>
</select>
<!-- good -->
<input type="text" name="nickname" disabled>
<input type="checkbox" name="hobbies" value="bicycle" checked>
<select>
<option value="FrontEnd" selected>FrontEnd</option>
</select>

自定义属性

必须data-为前缀,保证可读

1
2
3
4
<!-- bad -->
<div age="10"></div>
<!-- good -->
<div data-age="10"></div>

顺序

保证可读性,一致的属性顺序可以提高压缩率。按照出现频率,依次是:

  • class
  • id
  • data-*

少用style属性

建议使用class来控制样式,可以提高可维护性和可读性。

不在属性值中使用JavaScript语句

1
2
3
4
5
6
7
8
<!-- bad -->
<a id="hello" href="javascript:alert('hi');" onclick=";(function(){alert('hello');})()">
<!-- good -->
<script>
document.getElementById("hello").addEventListener(function() {
alert('hello');
});
</script>

多媒体

  • imgsrc属性禁止留空
  • img标签添加alt属性以声明替代文本
  • 在多媒体标签内部提供指示浏览器不支持该标签的说明,如objectaudiovideo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- bad -->
<img src="cat.png">
<audio controls>
<source src="foo.ogg" type="audio/ogg">
<source src="bar.mp3" type="audio/mpeg">
</audio>

<!-- good -->
<img src="cat.png" alt="cat">
<audio controls>
<source src="baz.ogg" type="audio/ogg">
<source src="qaz.mp3" type="audio/mpeg">
Your browser does not support the audio tag.
</audio>

CSS

基本

  • 推荐使用两个空格缩进
    1
    2
    3
    4
    5
    6
    7
    8
    /* bad */
    .example {
    color: #fff;
    }
    /* good */
    .example {
    color: #000;
    }
  • 单行不能超过120个字符,除非不可分割,如URL
  • 声明块左花括号前推荐添加空格
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* bad */
    .example{
    width: 100px;
    }

    /* good */
    .example {
    height: 15px;
    }
  • 声明块右花括号推荐单独成行
    1
    2
    3
    4
    5
    6
    7
    8
    /* bad */
    .example {
    width: 100px;}

    /* good */
    .example {
    height: 15px;
    }
  • 声明语句:需要有空格,前无空格
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* bad */
    .example {
    left:15px;
    }

    /* good */
    .example {
    top: 15px;
    }
  • 声明语句必须以分号结尾
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* bad */
    .example {
    left:15px
    }

    /* good */
    .example {
    top: 15px;
    }

注释

文档注释

声明在文件头部,表示文件作用

1
2
3
/**
* 这个文件的作用
*/

代码注释

传达代码上下文和目标,应该容易被人类理解。不要简单重复类名等冗余信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* bad */

/* header */
.header {
text-align: center;
...
}

/* good */

/* Wrapping contents of .title and .user-info */
.header {
text-align: center;
...
}

命名

同HTML规范,小写字母加短划线-。在JavaScript中出现的类名用J_开头,后接大驼峰命名,这类的class不能出现在CSS文件中。

  • 文本内容必须使用双引号包裹
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /* bad */
    html[lang|=zh] q:before {
    font-family: 'Microsoft YaHei', sans-serif;
    content: '“';
    }
    /* good */
    html[lang|="zh"] q:before {
    font-family: "Microsoft YaHei", sans-serif;
    content: "“";
    }
  • “0”值后不要使用单位。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* bad */
    .example {
    margin: 0px;
    }

    /* good */
    .example {
    margin: 0;
    }
  • 长度数值是0 - 1间的小数时,忽略整数部分的0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* bad */
    panel {
    opacity: 0.8
    }

    /* good */
    panel {
    opacity: .8
    }
  • 颜色值必须使用小写的十六进制表示,禁止使用颜色名和rgb(),带有透明度时可以使用rgba()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /* bad */
    .success {
    box-shadow: 0 0 2px rgba(0,128,0,.3);
    border-color: rgb(0, 128, 0);
    }

    /* good */
    .success {
    box-shadow: 0 0 2px rgba(0, 128, 0, .3);
    border-color: #008000;
    }
  • 可以缩写时,颜色值必须缩写
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* bad */
    .success {
    background-color: #aaccaa;
    }

    /* good */
    .success {
    background-color: #aca;
    }
  • url()函数中的路径禁止带引号,绝对路径时可以省去协议名
    1
    2
    3
    .logo {
    background: url(//assets/logo.png);
    }

选择器

  • 一个rule包含多个选择器时,每个选择器必须独占一行
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /* bad */
    .post, .page, .comment {
    line-height: 1.5;
    }

    /* good */
    .post,
    .page,
    .comment {
    line-height: 1.5;
    }
  • 属性选择器中的值必须用双引号包裹
    1
    2
    3
    p[lang|="zh"] {
    font-size: 12px;
    }
  • 选择器层级不要超过5级,靠后的选择器尽量精确
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* bad */
    .main .top .left .mod-a .content .detail {
    padding-left: 15px;
    }

    /* good */
    .content .detail {
    padding-left: 15px;
    }

属性

  • 建议相关的属性说明放在一组,并按下面顺序排列
    1. 定位(position、left、right、top、bottom、z-index)
    2. 盒子模型(display、float、width、height、margin、padding、border、border-radius)
    3. 排印(font、color、background、line-height、text-align)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      .mod-example {
      /* 定位 */
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      z-index: 100;
      /* 盒模型 */
      display: block;
      float: right;
      width: 100px;
      height: 100px;
      margin: 15px auto;
      padding: 10px 15px;
      border: 1px solid #ccc;
      /* 排印 */
      font: normal 13px "Helvetica Neue", sans-serif;
      line-height: 1.5;
      color: #333;
      background-color: #f5f5f5;
      text-align: center;
      }
  • 可以缩写时,建议缩写属性,如font, margin
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /* good */
    .post {
    font: 12px/1.5 arial, sans-serif;
    }

    /* bad */
    .post {
    font-family: arial, sans-serif;
    font-size: 12px;
    line-height: 1.5;
    }
  • 尽量不使用!important声明

字体

  • font-family必须使用字体族的英文名称,其中如有空格等特殊字符,必须使用双引号包裹。
    1
    2
    3
    h1 {
    font-family: "Microsoft YaHei";
    }
  • font-family按效果从优到通用顺序编写
    1
    2
    3
    h1 {
    font-family: "Helvetica Neue", Arial, "Hiragino Sans GB", "WenQuanYi Micro Hei", "Microsoft YaHei", sans-serif;
    }
  • 需要在Windows平台下显示的内容,font-size禁止小于12px,否则会模糊不清
  • ling-height建议使用数值
    1
    2
    3
    .text {
    line-height: 1.5;
    }
  • 建议减少使用自定义中文字体,通常体积很大,很影响页面加载

响应式

  • 媒体查询必须放在尽可能相关的规则附近,不得单独编排
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    /* bad */
    /* header styles */
    /* main styles */
    /* footer styles */

    @media (...) {
    /* header styles */
    /* main styles */
    /* footer styles */
    }

    /* good */
    /* header styles */
    @media (...) {
    /* header styles */
    }

    /* main styles */
    @media (...) {
    /* main styles */
    }

    /* footer styles */
    @media (...) {
    /* footer styles */
    }
  • 媒体查询有多个条件分隔时,每个条件必须另起一行
    1
    2
    3
    4
    5
    6
    7
    @media
    (-webkit-min-device-pixel-ratio: 2), /* Webkit-based browsers */
    (min--moz-device-pixel-ratio: 2), /* Older Firefox browsers (prior to Firefox 16) */
    (min-resolution: 2dppx), /* The standard way */
    (min-resolution: 192dpi) { /* dppx fallback */
    /* Retina-specific stuff here */
    }
  • 媒体查询针对每一个种屏幕(大、中、小)的建议单独组织为一个文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /* base.css */
    .element {
    }
    .element-avatar {
    }
    .element-selected {
    }
    /* base-media-small.css */
    @media (min-width: 480px) {
    .element {
    }
    .element-avatar {
    }
    .element-selected {
    }
    }

其他

  • 禁止使用JavaScript

参考

集团前端代码规约
文档与源码编写风格

0%