编程原理—如何用javascript代码解决一些问题

关于编程,我最喜欢的就是解决问题。我不相信有谁天生具有解决问题的能力。这是一种通过反复锻炼而建立并维持的能力。像任何练习一样,有一套指导方针可以帮助你更有效地提高解决问题的能力。我将介绍5个最重要的软件设计原则,这些原则指导了我的解决问题的过程,并向您展示如何应用这些原则来解决实际问题。

1. Keep It Simple

如果您有任何需要从这篇文章中获得的信息,那就是“保持简单”的原则。这个原则通常被称为KISS,它代表“Keep it simple,stupid”。

仅仅因为我们必须解决的问题很复杂,并不意味着我们的解决方案必须很复杂。

一般来说,我们通过将复杂问题分解为简单的子问题,来开发解决复杂问题的简单解决方案,以便我们可以轻松解决这些问题。

来看一个简单的例子:

假设你要写一个函数,来检查一个字符串中间的括号的对称匹配。例如,只有一个’(‘ 将会返回 false,只有一个’)(‘ 将会返回 false,’()’ 将会返回true, 然后字符串’foo’将会返回 true。

所以,我们怎么做?

第一步将应用 KISS 原则, 为算法来写出一个概要。

给到一个字符串, 是由一些字符组成。

  1. 从最左边一个字符开始,然后查看后面的每一个字符。
  2. 记录每一个出现的 ( .
  3. 记录出现的 )来终止之前的 ( .
  4. 忽视那些不是 ( 或者 )的字符 .
  5. 如果只有一个 ) 而没有( , 返回 false.
  6. 一旦我们完成了最后一个字符的观测, 如果没有剩余单独的 ( 或者 ) ,返回 true.

2. Separation of Concerns (SoC)

我们的算法有三个逻辑域:
Continue or Stop? — 是否继续观测字符,或停止并返回真或假的逻辑。
Parentheses Tally —用于统计( 和 )字符出现的逻辑。
What to Look At Next? — 决定接下来观测什么的逻辑。
让我们将上面的六个步骤映射到以下三个逻辑域:
Continue or Stop? Parentheses Tally What to Look At Next
如果只有一个 ) 而没有( , 返回 false. 记录每一个出现的 ( . 从最左边一个字符开始,然后查看后面的每一个字符。
一旦我们完成了最后一个字符的观测, 如果没有剩余单独的 ( 或者 ) ,返回 true. 记录出现的 )来终止之前的 (
忽视那些不是 ( 或者 )的字符

我们刚刚做的是我们将算法分为三个逻辑域,每个域都处理特定的问题。每个领域特定的问题都可以由软件组件独立解决。这三个软件组件都是为完成特定任务而设计的,无需关心其他组件正在做什么。这表明了关注点分离或所谓SoC的原则。

SoC是开发具有许多变动部分的复杂应用程序或软件系统的关键。Web应用程序会分别介绍表示形式(网页的外观),业务逻辑(网页内容)和内容交付(通过JSON API访问资源,查询数据库等)之间的关系。

Poorly executed SoC

我见过设计不佳的软件,其中软件虽然分为不同的域,但关注点没有分离。考虑下面的例子:

Write a function foo that doubles then increments every number in an array of numbers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22


// Helper Functions
function (arrIn) {
let arrOut = []
for (let i = 0; i < arrIn.length; i++) {
arrOut[i] = arrIn[i] * 2
}
return arrOut
}
function increment(arrIn) {
let arrOut = []
for (let i = 0; i < arrIn.length; i++) {
arrOut[i] = arrIn[i] + 1
}
return arrOut
}

// foo
function foo(arrIn) {
return increment(double(arrIn))
}

为什么这是一个 SoC 失败的案例 ?

好吧,double方法increment方法 都涉及遍历数组来修改每个元素。注意 double方法increment方法 函数看起来几乎完全相同除了它们如何修改数组元素的不一样之外。 这被称为 boilerplate

只要你的代码中有 boilerplate 文件,你就知道可以在关注分离点时有更好的提升空间。 这涉及到下一个原则:

3. Don’t Repeat Yourself (DRY)

不要重复自己(DRY)原则和 SoC 是共存的。 DRY原则旨在通过形成抽象来减少软件中的重复和boilerplate,我可以写出一篇关于抽象的单独文章,但这里是如何应用抽象来编写不重复的代码的关键点:

  • 创建通用的软件模式的函数。我们称之为高阶函数。
  • 使用高级函数来替换你代码中的boilerplate

高阶函数的一些例子包括:

  • map - 根据给定规则修改数组中的每个元素。
  • filter - 获取通过给定条件的数组的子集
  • reduce - 根据给定的规则将数组中的所有内容组合在一起

让我们应用 DRY 原理并重写foo 以使用 map 高阶函数来抽取 boilerplate

1
2
3
4
5
6
7
8
// Well executed separation of concerns and DRY code

const double = elem => elem * 2
const increment = < 大专栏  编程原理—如何用javascript代码解决一些问题span class="function">elem => elem + 1

function foo(arrIn) {
return arrIn.map(elem => increment(double(elem)))
}

好的,这是一个漫长的弯路。我们可以回到hasBalancedParen问题吗?

是! 我们即将达成最终解决方案,但还有一个我们需要的原则:

4. Divide and Conquer (分治算法)

当您在算法设计的背景下听到“Divide and Conquer”的时候,请考虑递归。术语-递归-有数学基础,通常需要重复应用规则来更新自己,以创建一个无限序列,序列中的下一个事物依赖于序列中的上一个事物。例如,分形,斐波那契数列和帕斯卡三角形都是由递归构造。

对于算法中的分治,我们感兴趣的是重复应用规则,而不是构建一个序列,我们想要解构问题空间并最终返回最终解决方案。在如下代码中,分治算法的递归函数具有一个基本结构:

1
2
3
4
5
6
function recursiveFun(input, output) {
if we hit a base case, updated output as necessary and return the final output

otherwise, simplify the input. update the output then
return recursiveFun(simplerInput, updatedOutput)
}

重点是简化输入(参数)直到问题变得非常简单去解决那么我们就能立即在函数的最开始返回结果。这也是所谓的 base case .(个人理解为最低级的条件,最少也要满足的条件)。如果我们忘记给递归函数添加一个 base case 会如何? 很简单,内存爆炸。

注意,我们并不需要去使用递归函数来实现分治算法。我们可以使用一个循环以及一个可变数据结构。我并不是可变数据结构的爱好者,虽然它有时候效率更高。

让我们看看一个简单的例子关于使用递归函数和一个可变数据结构来实现分治:

Option 1: Recursive Function
1
2
3
4
5
6
7
8
9
10
11
12
function sum(arr) {

function add(arr, sum) {
// Base Case
if (arr.length === 0) return sum

// Recursive Step
return add(arr.slice(1), sum + arr[0])
}

return add(arr, 0) // <-- Initial Values
}
Option 2: Mutable Data Structure
1
2
3
4
5
6
7
8
9
10
11
12
function sum(arr) {

// Initial Values
let result = 0
let elems = arr // <-- mutable data structure

while(elems.length > 0) { // <-- Base Case
result += elems.pop() // <-- Recursive step
}

return result
}

函数式编程和单行程序的爱好者将使用更高阶的函数reduce执行如下,JavaScript为您提供Array原型的开箱即用功能:

const sum = (arr) => arr.reduce((res, elem) => res + elem, 0)

使用分治,我们有了解决最后一个重要问题的原则,来解决 hasBalancedParen 问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Parentheses Tally
const newOpenCnt = (c, openCnt) => {
if(c === '(') return openCnt + 1
if(c === ')') return openCnt - 1
return openCnt
}

function isBalanced(str, openCnt) {
// Continue or Stop?
if (typeof str !== 'string') return false
if (openCnt < 0) return false
if (str.length === 0) return openCnt === 0

// What to Look At Next?
const fst = str[0]
const rst = str.slice(1)
return isBalanced(rst, newOpenCnt(fst, openCnt))
}

function hasBalancedParen(str) {
return isBalanced(str, 0)
}
原文地址:https://www.cnblogs.com/lijianming180/p/12032650.html