JavaScript-数据结构和算法教程-一-

102 阅读23分钟

JavaScript 数据结构和算法教程(一)

原文:JavaScript Data Structures and Algorithms

协议:CC BY-NC-SA 4.0

一、大 O 符号

O①是神圣的。

-哈米德·提胡什

在学习如何实现算法之前,你应该了解如何分析算法的有效性。这一章将集中在时间和算法空间复杂性分析的 Big-O 符号的概念上。本章结束时,你将理解如何从时间(执行时间)和空间(消耗的内存)两方面分析一个算法的实现。

大 O 符号初级读本

Big-O 符号衡量算法的最坏情况复杂度。在 Big-O 符号中, n 表示输入的数量。问 Big-O 的问题如下:“当 n 接近无穷大时会发生什么?”

当你实现一个算法时,Big-O 符号很重要,因为它告诉你这个算法有多有效。图 1-1 显示了一些常见的 Big-O 符号。

img/465726_1_En_1_Fig1_HTML.jpg

图 1-1

常见的大 O 复杂性

下面几节用一些简单的例子说明了这些常见的时间复杂性。

常见示例

O(1)相对于输入空间不变。因此,O(1)被称为恒定时间。O(1)算法的一个例子是通过索引访问数组中的项。O( n )是线性时间,适用于在最坏情况下必须做 n 运算的算法。

O( n )算法的一个例子是打印从 0 到 n -1 的数字,如下所示:

1 function  exampleLinear(n) {
2                   for  (var  i = 0 ; i <  n; i++ ) {
3                              console.log(i);
4                   }
5  }

同样,O(n??【2】??)是二次时间,O(n3 是三次时间。这些复杂性的例子如下所示:

1 function  exampleQuadratic(n) {
2                   for  (var  i = 0 ; i <  n; i++ ) {
3                                 console.log(i);
4                                for  (var  j =  i; j <  n; j++ ) {
5                                             console.log(j);
6                              }
7                   }
8  }

1 function  exampleCubic(n) {
2                   for  (var  i = 0 ; i <  n; i++ ) {
3                                  console.log(i);
4                                for  (var  j =  i; j <  n; j++ ) {
5                                              console.log(j);
6                                                  for  (var  k =  j; j <  n; j++ ) {
7                                                          console.log(k);
8                                                  }
9                                }
10           }
11 }

最后,对数时间复杂度的一个示例算法是打印 2 和 n 之间的 2 的幂的元素。例如,exampleLogarithmic(10)将打印以下内容:

2,4,8,16,32,64

对数时间复杂性的效率在大量输入(如一百万项)的情况下是显而易见的。虽然 n 是一百万,但是exampleLogarithmic将只打印 19 项,因为 log 2 (1,000,000) = 19.9315686。实现这种对数行为的代码如下:

1 function  exampleLogarithmic(n) {
2                   for  (var  i = 2 ; i <=  n; i= i*2 ) {
3                          console.log(i);
4                  }
5  }

大 O 符号的规则

让我们把一个算法的复杂度表示为 f( n )。 n 表示输入次数,f( n ) time 表示需要的时间,f( n ) space 表示算法需要的空间(附加内存)。算法分析的目标是通过计算 f( n )来了解算法的效率。然而,计算 f( n )可能具有挑战性。Big-O 符号提供了一些帮助开发人员计算 f( n )的基本规则。

  • 系数法则:若 f( n )为 O(g( n ),则 kf( n )为 O(g( n )),对于任意常数 k > 0。第一个规则是系数规则,它排除与输入大小 n 无关的系数。这是因为随着 n 接近无穷大,另一个系数变得可以忽略不计。

  • 求和规则:若 f( n )为 O(h( n )、g( n )为 O(p( n )),则 f( n )+g( n )为 O(h( n )+p( n )。求和规则简单地说明,如果合成的时间复杂度是两个不同时间复杂度的和,则合成的 Big-O 符号也是两个不同的 Big-O 符号的和。

  • 乘积法则:如果 f( n )是 O(h(n))g(n)是 O(p( n )),那么 f( n )g( n )是 O(h( n )p( n ))。类似地,乘积法则表明,当时间复杂性增加时,Big-O 也会增加。

  • 传递规则:若 f( n )为 O(g( n )),g( n )为 O(h( n )),则 f( n )为 O(h( n ))。传递规则是一种简单的方式来说明相同的时间复杂度具有相同的 Big-O。

  • 多项式法则:若 f( n 为 k 次多项式,则 f( n )为 O(nk)。直观地说,多项式规则表明多项式时间复杂性具有相同多项式次数的 Big-O。

  • 对数幂法则:对数( n k)对于任意常数 k > 0 为 O(对数( n ))。对于幂规则的对数,对数函数中的常数在 Big-O 符号中也会被忽略。

应特别注意前三条规则和多项式规则,因为它们是最常用的。我将在下面的小节中讨论这些规则。

系数法则:“去掉常数”

我们先来回顾一下系数法则。这个规则是最容易理解的规则。它只需要您忽略任何与输入大小无关的常量。输入大时,Big-O 中的系数可以忽略不计。因此,这是 Big-O 记数法最重要的规则。

  • 若 f( n )为 O(g( n )),则 kf( n )为 O(g( n )),对于任意常数 k > 0。

这意味着 5f( n )和 f( n )都有相同的大 O 符号 O(f( n ))。

下面是一个时间复杂度为 O( n )的代码块的例子:

1   function a(n){
2       var count =0;
3       for (var i=0;i<n;i++){
4           count+=1;
5       }
6       return count;
7   }

这段代码有 f( n ) = n 。这是因为它增加了count n 次。因此,该函数的时间复杂度为 O( n ):

1   function a(n){
2       var count =0;
3       for (var i=0;i<5*n;i++){
4           count+=1;
5       }
6       return count;
7   }

这个块有 f( n ) = 5 n 。这是因为它从 0 运行到 5 n 。然而,前两个例子都有 O( n )的 Big-O 符号。简单来说,这是因为如果 n 接近无穷大或者另一个大数,那四个额外的运算就没有意义了。它将执行第次。任何常数在 Big-O 符号中都是可以忽略的。

下面的代码块演示了另一个具有线性时间复杂度的函数,但是在第 6 行中有一个额外的操作:

1   function a(n){
2       var count =0;
3       for (var i=0;i<n;i++){
4           count+=1;
5       }
6       count+=3;
7       return count;
8   }

最后,这段代码有 f( n ) = n +1。上一次操作的结果是+1(计数+=3)。这仍然有一个大 O 符号 O( n )。这是因为 1 操作不依赖于输入 n 。随着 n 趋近于无穷大,它将变得可以忽略不计。

求和规则:“将 Big-Os 相加”

求和规则理解起来很直观;可以增加时间复杂度。想象一个包含两个其他算法的主算法。主算法 Big-O 符号只是另外两个 Big-O 符号的总和。

  • 如果 f( n )是 O(h(n))g(n)是 O(p( n )),那么 f( n )+g( n )是 O(h( n )+p( n )。

在应用这个规则之后,记住应用系数规则是很重要的。

下面的代码块演示了一个带有两个主循环的函数,这两个循环的时间复杂度必须单独考虑,然后求和:

 1   function a(n){
 2       var count =0;
 3       for (var i=0;i<n;i++){
 4           count+=1;
 5       }
 6       for (var i=0;i<5*n;i++){
 7           count+=1;
 8       }
 9       return count;
10   }

在这个例子中,第 4 行有 f( n ) = n ,第 7 行有 f( n ) = 5 n 。这导致 6 个 n 个。但是应用系数法则,最后的结果是 O( n ) = n

产品规则:“乘以大操作系统”

乘积法则简单地说明了 Big-Os 可以相乘到什么程度。

  • 如果 f( n )是 O(h(n))g(n)是 O(p( n )),那么 f( n )g( n )是 O(h( n )p( n ))。

以下代码块演示了一个具有两个嵌套的for循环的函数,该函数应用了乘积规则:

 1   function (n){
 2       var count =0;
 3       for (var i=0;i<n;i++){
 4           count+=1;
 5           for (var i=0;i<5*n;i++){
 6               count+=1;
 7           }
 8       }
 9       return count;
10   }

在这个例子中,f(n)= 5n**n*,因为第 7 行运行 5 n 次,总共 n 次迭代。因此,这导致总共 5 个 n 个2 个操作。应用系数法则,结果是 O(n)=n2

多项式规则:“大到 k 的幂”

多项式规则表明,多项式时间复杂性具有相同多项式次数的 Big-O 符号。

数学上,它如下:

  • 如果 f( n )是 k 次多项式,那么 f( n )是 O(nk)。

下面的代码块只有一个二次时间复杂度的for循环:

1   function a(n){
2       var count =0;
3       for (var i=0;i<n*n;i++){
4           count+=1;
5       }
6       return count;
7   }

在这个例子中,f(n)=nˇ2,因为第 4 行运行了 n * n 次迭代。

这是对 Big-O 符号的一个快速概述。随着这本书的进展,还会有更多的内容。

摘要

Big-O 对于分析和比较算法的效率非常重要。对 Big-O 的分析从查看代码和应用规则来简化 Big-O 符号开始。以下是最常用的规则:

  • 消除系数/常数(系数规则)

  • 将大 0 相加(求和规则)

  • 乘法大操作系统(产品规则)

  • 通过查看循环来确定 Big-O 符号的多项式(多项式规则)

练习

计算每个练习代码片段的时间复杂度。

练习 1

1   function someFunction(n) {
2
3       for (var i=0;i<n*1000;i++) {
4           for (var j=0;j<n*20;j++) {
5               console.log(i+j);
6           }
7       }
8
9   }

练习 2

 1   function someFunction(n) {
 2
 3       for (var i=0;i<n;i++) {
 4           for (var j=0;j<n;j++) {
 5               for (var k=0;k<n;k++) {
 6                   for (var l=0;l<10;l++) {
 7                       console.log(i+j+k+l);
 8                   }
 9               }
10           }
11       }
12
13   }

练习 3

1   function someFunction(n) {
2
3       for (var i=0;i<1000;i++) {
4           console.log("hi");
5       }
6
7   }

练习

1   function someFunction(n) {
2
3       for (var i=0;i<n*10;i++) {
4           console.log(n);
5       }
6
7   }

练习 5

1   function someFunction(n) {
2
3       for (var i=0;i<n;i*2) {
4           console.log(n);
5       }
6
7   }

练习 6

1   function someFunction(n) {
2
3       while (true){
4           console.log(n);
5       }
6   }

答案

  1. o(n2

    有两个嵌套循环。忽略 n 前面的常数。

  2. o(n3

    有四个嵌套循环,但最后一个循环只运行到 10。

  3. O(1)

    持续的复杂性。该函数从 0 到 1000。这个不取决于 n

  4. O( n )

    线性复杂度。该功能从 0 到 10 n 运行。常量在 Big-O 中被忽略。

  5. o(日志2n

    对数复杂度。对于给定的 n ,这只运行 log 2 n 次,因为 I 是通过乘以 2 来递增的,而不是像其他例子中那样加 1。

  6. O(∞)

    无限循环。这个功能不会结束。

二、JavaScript:独特的部分

本章将简要讨论 JavaScript 的语法和行为的一些例外和案例。作为一种动态的解释型编程语言,它的语法不同于传统的面向对象编程语言。这些概念是 JavaScript 的基础,将帮助您更好地理解用 JavaScript 设计算法的过程。

JavaScript 范围

范围定义了对 JavaScript 变量的访问。在 JavaScript 中,变量可以属于全局范围,也可以属于局部范围。全局变量是属于全局范围的变量,可以从程序的任何地方访问。

全球声明:全球范围

在 JavaScript 中,可以不使用任何运算符来声明变量。这里有一个例子:

1  test = "sss";
2  console.log(test); // prints "sss"

然而,这会创建一个全局变量,这是 JavaScript 中最糟糕的做法之一。不惜一切代价避免这样做。总是使用varlet来声明变量。最后,当声明不会被修改的变量时,使用const

带变量的声明:功能范围

在 JavaScript 中,var是一个用于声明变量的关键字。这些变量声明一直“浮动”到顶部。这就是所谓的可变提升。脚本底部声明的变量不会是 JavaScript 程序运行时最后执行的东西。

这里有一个例子:

1  function scope1(){
2          var top = "top";
3          bottom = "bottom";
4          console.log(bottom);
5
6          var bottom;
7  }
8  scope1(); // prints "bottom" - no error

这是如何工作的?前面的和下面写的是一样的:

1  function scope1(){
2          var top = "top";
3          var  bottom;
4          bottom = "bottom"
5          console.log(bottom);
6  }
7  scope1(); // prints "bottom" - no error

位于函数最后一行的bottom变量声明被浮动到顶部,记录变量就可以了。

关于var关键字要注意的关键是变量的作用域是最近的函数作用域。这是什么意思?

在下面的代码中,scope2函数是最接近print变量的函数范围:

1  function scope2(print){
2          if(print){
3                   var insideIf = '12';
4          }
5          console.log(insideIf);
6  }
7  scope2(true); // prints '12' - no error

举例来说,前面的函数相当于以下函数:

1  function scope2(print){
2          var insideIf;
3
4          if(print){
5                   insideIf = '12';
6          }
7          console.log(insideIf);
8  }
9  scope2(true); // prints '12' - no error

在 Java 中,这种语法会抛出一个错误,因为insideIf变量通常只在那个if语句块中可用,而不在它之外。

这是另一个例子:

1  var a = 1;
2  function four() {
3    if (true) {
4      var a = 4;
5    }
6
7    console.log(a); // prints '4'
8  }

4被打印,而不是1的全局值,因为它被重新声明并在该范围内可用。

带字母的声明: Block Scope

另一个可以用来声明变量的关键字是let。以这种方式声明的任何变量都在最近的块范围内(意味着在声明它们的{}内)。

1  function scope3(print){
2          if(print){
3                   let insideIf = '12';
4          }
5          console.log(insideIf);
6  }
7  scope3(true); // prints ''

在这个例子中,没有任何东西被记录到控制台,因为insideIf变量只在if语句块中可用。

等式和类型

JavaScript 的数据类型与 Java 等传统语言不同。让我们探讨一下这是如何影响诸如相等比较之类的事情的。

变量类型

在 JavaScript 中,有七种基本数据类型:布尔型、数字型、字符串型、未定义型、对象型、函数型和符号型(不讨论符号)。这里突出的一点是,undefined 是一个赋给刚刚声明的变量的原始值。typeof是用于返回变量类型的原始运算符。

 1  var is20 = false; // boolean
 2  typeof is20; // boolean
 3
 4  var  age = 19;
 5  typeof age; // number
 6
 7  var  lastName = "Bae";
 8  typeof lastName; // string
 9
10  var fruits = ["Apple", "Banana", "Kiwi"];
11  typeof fruits; // object
12
13  var me = {firstName:"Sammie", lastName:"Bae"};
14  typeof me; // object
15
16  var nullVar = null;
17  typeof nullVar; // object
18
19  var function1 = function(){
20          console.log(1);
21  }
22  typeof function1 // function
23
24  var blank;
25  typeof blank; // undefined

真假检验

真/假检查用于if语句。在许多语言中,if()函数中的参数必须是布尔类型。然而,JavaScript(和其他动态类型语言)在这方面更加灵活。这里有一个例子:

1  if(node){
2          ...
3  }

这里,node是一些变量。如果该变量为空、null 或未定义,它将被评估为false

以下是评估为false的常用表达式:

  • false

  • Zero

  • 空字符串(''"")

  • NaN

  • undefined

  • null

以下是评估为true的常用表达式:

  • true

  • 0 以外的任何数字

  • 非空字符串

  • 非空对象

这里有一个例子:

1  var printIfTrue = ";
2
3  if (printIfTrue) {
4          console.log('truthy');
5  } else {
6          console.log('falsey'); // prints 'falsey'
7  }

=== vs ==

JavaScript 是一种脚本语言,变量在声明时不会被赋予类型。相反,类型在代码运行时被解释。

因此,===用于比==更严格地检查相等性。===检查类型和值,而==只检查值。

1  "5" == 5 // returns true
2  "5" === 5 // returns false

"5" == 5返回true,因为"5"在比较之前被强制为一个数字。另一方面,"5" === 5返回false,因为"5"的类型是字符串,而 5 是数字。

目标

大多数强类型语言如 Java 使用isEquals()来检查两个对象是否相同。您可能想简单地使用==操作符来检查 JavaScript 中的两个对象是否相同。

但是,这不会计算到true

1  var o1 = {};
2  var o2 = {};
3
4  o1 == o2 // returns false
5  o1 === o2 // returns false

虽然这些对象是等价的(相同的属性和值),但它们并不相等。也就是说,变量在内存中有不同的地址。

这就是为什么大多数 JavaScript 应用程序使用具有isEqual( object1object2 )函数的实用程序库如 lodash1或下划线、 2 来严格检查两个对象或值。这是通过实现一些基于属性的相等性检查来实现的,其中比较对象的每个属性。

在此示例中,将比较每个属性以获得准确的对象相等结果。

 1  function isEquivalent(a, b) {
 2      // arrays of property names
 3      var aProps = Object.getOwnPropertyNames(a);
 4      var bProps = Object.getOwnPropertyNames(b);
 5
 6      // If their property lengths are different, they're different objects
 7      if (aProps.length != bProps.length) {
 8          return false;
 9      }
10
11      for (var  i = 0; i < aProps.length; i++) {
12          var propName = aProps[i];
13
14          // If the values of the property are different, not equal
15          if (a[propName] !== b[propName]) {
16              return false;
17          }
18      }
19
20     // If everything matched, correct
21     return  true;
22  }
23  isEquivalent({'hi':12},{'hi':12}); // returns true

但是,这对于只有字符串或数字作为属性的对象仍然有效。

1  var obj1 = {'prop1': 'test','prop2': function (){} };
2  var obj2 = {'prop1': 'test','prop2': function (){} };
3
4  isEquivalent(obj1,obj2); // returns false

这是因为函数和数组不能简单地使用==操作符来检查相等性。

1  var function1 = function(){console.log(2)};
2  var function2 = function(){console.log(2)};
3  console.log(function1 == function2); // prints 'false'

虽然这两个函数执行相同的操作,但是函数在内存中有不同的地址,因此相等运算符返回false。基本的相等检查操作符=====只能用于字符串和数字。要实现对象的等价性检查,需要检查对象中的每个属性。

摘要

JavaScript 有一种不同于大多数编程语言的变量声明技术。var在函数范围内声明变量,let在块范围内声明变量,变量在全局范围内可以不用任何运算符声明;然而,任何时候都应该避免全局范围。对于类型检查,应该使用typeof来验证预期的类型。最后,对于相等性检查,使用==检查值,使用===检查类型和值。但是,只能在数字、字符串和布尔值等非对象类型上使用它们。

Footnotes 1

https://lodash.com/

  2

http://underscorejs.org/

 

三、JavaScript 数字

本章将重点介绍 JavaScript 数字运算、数字表示、Number对象、常用数字算法和随机数生成。在本章结束时,你将理解如何在 JavaScript 中处理数字,以及如何实现质因数分解,这是加密的基础。

编程语言的数字运算允许你计算数值。以下是 JavaScript 中的数字运算符:

+ : addition
- : subtraction
/ : division
* : multiplication
% : modulus

这些操作符在其他编程语言中普遍使用,并不特定于 JavaScript。

数系

JavaScript 对数字使用 32 位浮点表示,如图 3-1 所示。在本例中,该值为 0.15625。如果符号位为 1,符号位(第 31 位)表示数字为负。接下来的 8 位(第 30 位至第 23 位)表示指数值,即 e。最后,剩余的 23 位表示分数值。

img/465726_1_En_3_Fig1_HTML.jpg

图 3-1

32 位浮点数系统

对于 32 位,该值通过以下深奥的公式计算:

\mathrm{value}={\left(-1\right)}^{\mathrm{sign}}\times {2}^{\mathrm{e}-127}\times \left(1+\sum \limits_{t=1}^{23}{b}_{23-t}{2}^{-t}\right)

图 3-1 显示了 32 位的如下分解:

符号= 0

e = (0111100) 2 = 124(以 10 为基数)

1+\sum \limits_{i=1}^{23}{b}_{23-i}{2}^{-i}=1+0+0.25+0

这将导致以下结果:

值= 1 x 2124-127x 1.25 = 1 x 2-3x 1.25 = 0.15625

对于小数,这种浮点数系统会在 JavaScript 中导致一些舍入错误。例如,0.1 和 0.2 无法精确表示。

因此,0.1 + 0.2 === 0.3 得出false

1  0.1 + 0.2 === 0.3; // prints 'false'

要真正理解为什么 0.1 不能正确地表示为 32 位浮点数,必须理解二进制。用二进制表示许多小数需要无限多的位数。这是因为二进制数由 2 n 表示,其中 n 是整数。

在努力计算 0.1 的同时,长除法会一直进行下去。如图 3-2 所示,1010 在二进制中代表 10。试图计算 0.1 (1/10)会导致小数点位数不确定。

img/465726_1_En_3_Fig2_HTML.jpg

图 3-2

长除法为 0.1

JavaScript 数字对象

幸运的是,JavaScript 中有一些Number对象的内置属性可以帮助解决这个问题。

整数舍入

由于 JavaScript 使用浮点来表示所有数字,因此整数除法不起作用。

像 Java 这样的编程语言中的整数除法只是简单地计算除法表达式的商。

例如,Java 中的 5/4 是 1,因为商是 1(尽管还有 1 的余数)。但是,在 JavaScript 中,它是一个浮点。

1  5/4; // 1.25

这是因为 Java 要求您将整数显式地输入为整数。因此,结果不能是浮点。但是,如果 JavaScript 开发人员想要实现整数除法,他们可以执行以下操作之一:

Math.floor - rounds down to nearest integer
Math.round - rounds to nearest integer
Math.ceil  - rounds up to nearest integer

Math.floor(0.9); // 0
Math.floor(1.1); // 1

Math.round(0.49); // 0
Math.round(0.5); // 1

Math.round(2.9); // 3
Math.ceil(0.1); // 1 Math.ceil(0.9); // 1 Math.ceil(21); // 21 Math.ceil(21.01); // 22

号码。希腊语字母之第五字

Number.EPSILON返回两个可表示数字之间的最小间隔。这对于浮点近似的问题很有用。

1  function numberEquals(x, y) {
2      return Math.abs(x - y) < Number.EPSILON;
3  }
4
5  numberEquals(0.1 + 0.2, 0.3); // true

该功能通过检查两个数字之间的差值是否小于Number.EPSILON来工作。记住Number.EPSILON是两个可表示的数之间的最小差值。0.1+0.2 和 0.3 的差别会比Number.EPSILON小。

最大限度

Number.MAX_SAFE_INTEGER返回最大整数。

1  Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2; // true

这会返回true,因为它不能再高了。但是,它不适用于浮点小数。

1  Number.MAX_SAFE_INTEGER + 1.111 === Number.MAX_SAFE_INTEGER + 2.022; // false

Number.MAX_VALUE返回可能的最大浮点数。

Number.MAX_VALUE等于 1.7976931348623157e+308。

1  Number.MAX_VALUE + 1 === Number.MAX_VALUE + 2; // true

与 like Number.MAX_SAFE_INTEGER不同,它使用双精度浮点表示,也适用于浮点。

1  Number.MAX_VALUE + 1.111 === Number.MAX_VALUE + 2.022; // true

最低限度

Number.MIN_SAFE_INTEGER返回最小的整数。

Number.MIN_SAFE_INTEGER等于-9007199254740991。

1  Number.MIN_SAFE_INTEGER - 1 === Number.MIN_SAFE_INTEGER - 2; // true

这将返回true,因为它不能再小了。但是,它不适用于浮点小数。

1   Number.MIN_SAFE_INTEGER - 1.111 === Number.MIN_SAFE_INTEGER - 2.022; // false

Number.MIN_VALUE返回可能的最小浮点数。

Number.MIN_VALUE等于 5e-324。这不是一个负数,因为它是最小的浮点数,这意味着Number.MIN_VALUE实际上大于Number.MIN_- SAFE_INTEGER

Number.MIN_VALUE也是最接近零的浮点。

1  Number.MIN_VALUE - 1 == -1; // true

这是因为这类似于写0 - 1 == -1

无穷

唯一大于Number.MAX_VALUE的是Infinity,唯一小于Number.MAX_SAFE_INTEGER的是-Infinity

1  Infinity > Number.MAX_SAFE_INTEGER; // true
2  -Infinity < Number.MAX_SAFE_INTEGER // true;
3  -Infinity -32323323 == -Infinity -1; // true

这等于true,因为没有比-Infinity更小的了。

尺寸汇总

这个不等式总结了 JavaScript 数字从最小(左)到最大(右)的大小:

-Infinity < Number.MIN_SAFE_INTEGER < Number.MIN_VALUE < 0 < Number.MAX_SAFE_IN- TEGER < Number.MAX_VALUE < Infinity

数字算法

讨论最多的涉及数字的算法之一是测试一个数字是否是质数。现在我们来回顾一下。

素性检验

素性测试可以通过从 2 到 n 的迭代来完成,检查模除(余数)是否等于零。

 1  function isPrime(n){
 2      if (n <= 1) {
 3              return false;
 4      }
 5
 6      // check from 2 to n-1
 7      for (var i=2; i<n; i++) {
 8              if (n%i == 0) {
 9                      return false;
10          }
11      }
12
13      return true;
14  }

时间复杂度: O( n

时间复杂度为 O( n ),因为该算法检查从 0 到 n 的所有数。

这是一个很容易改进的算法的例子。想想这个方法是怎么迭代 2 到 n 的。有没有可能找到一个模式,让算法更快?首先,可以忽略 2 的任何倍数,但可能有更多的优化。

让我们列出一些质数。

2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97

这很难注意到,但是所有的素数都是 6k ^ 1 的形式,除了 2 和 3,其中 k 是某个整数。这里有一个例子:

5 = (6-1) , 7 = ((1*6) + 1), 13 = ((2*6) + 1) etc

还要认识到,为了测试质数 n ,循环只需测试到 n 的平方根。这是因为如果 n 的平方根不是素数, n 就不是数学定义上的素数。

 1  function isPrime(n){
 2      if (n <= 1) return false;
 3      if (n <= 3) return true;
 4
 5      // This is checked so that we can skip
 6      // middle five numbers in below loop
 7      if (n%2 == 0 || n%3 == 0) return false;
 8
 9      for (var i=5; i*i<=n; i=i+6){
10          if (n%i == 0 || n%(i+2) == 0)
11             return false;
12      }
13
14      return true;
15  }

时间复杂度: O( sqrt ( n ))

这个改进的解决方案大大降低了时间复杂度。

质因数分解

另一个需要理解的有用算法是确定一个数的质因数分解。质数是加密(在第四章中介绍)和哈希(在第十一章中介绍)的基础,而质因数分解是确定哪些质数乘以一个给定的数的过程。给定 10,它将打印 5 和 2。

 1  function primeFactors(n){
 2          // Print the number of 2s that divide n
 3          while (n%2 == 0) {
 4              console.log(2);
 5              n = n/2;
 6          }
 7
 8          // n must be odd at this point. So we can skip one element (Note i = i +2)
 9          for (var i = 3; i*i <= n; i = i+2) {
 10             // While i divides n, print i and divide n
11              while (n%i == 0) {
12                  console.log(i);
13                  n = n/i;
14              }
15          }
16          // This condition is to handle the case when n is a prime number
17          // greater than 2
18          if (n > 2) {
19                  console.log(n);
20          }
21  }
22  primeFactors(10); // prints '5' and '2'

时间复杂度: O( sqrt ( n ))

这种算法的工作原理是打印任何能被 I 整除且没有余数的数。如果一个质数被传递到这个函数中,它将通过打印 n 是否大于 2 来处理。

随机数生成器

随机数生成对于模拟条件很重要。JavaScript 内置了生成数字的函数:Math.random()

  • Math.random()返回一个介于 0 和 1 之间的浮点数。

你可能想知道如何得到大于 1 的随机整数。

要获得高于 1 的浮点,只需将Math.random()乘以范围。加上或减去它来设定基数。

Math.random() * 100; // floats between 0  and  100
Math.random() * 25 + 5; // floats between 5  and  30
Math.random() * 10 - 100; // floats between -100 and -90

要获得随机整数,只需使用Math.floor()Math.round()Math.ceil()舍入到一个整数。

Math.floor(Math.random() * 100); // integer between 0 and 99
Math.round(Math.random() * 25) + 5; // integer between 5 and 30
Math.ceil(Math.random() * 10) - 100; // integer between -100 and -90

练习

  • 【maxDivide(数,除数) : O(对数除数(数))的时间复杂度

  • maxDivide 的时间复杂度是一个对数函数,它依赖于除数和个数。当测试 2、3 和 5 的素数时,2 的对数(log 2 ( n ))产生最高的时间复杂度。

  • isUgly 的时间复杂度 : O(log 2 ( n ))

  • 数组编号的时间复杂度 : O( n (对数 2 ( n ))

  1. 给定三个数 x、y 和 p,计算(xˇy)% p(这是模幂运算。)

    这里,x 是底数,y 是指数,p 是模数。

    模幂运算是对一个模数执行的一种幂运算,它在计算机科学中很有用,并用于公钥加密算法领域。

    At first, this problem seems simple. Calculating this is a one-line solution, as shown here:

    1  function modularExponentiation ( base, exponent, modulus ) {
    2          return Math.pow(base,exponent) % modulus;
    3  }
    
    

    这正是问题所要求的。然而,它不能处理大指数。

    记住这是用加密算法实现的。在强密码术中,基数通常至少是 256 位(78 位)。

    例如,考虑这种情况:

    基数:6 x 10 77 ,指数:27,模数:497

    在这种情况下,(6x1077)27是一个非常大的数,不能存储在 32 位浮点中。

    还有另一种方法,涉及到一些数学。人们必须观察以下数学性质:

    For arbitrary a and b,

    c % m = (a  b) % m
    c % m = [(a % m)  (b % m)] % m
    
    

    使用这个数学属性,您可以迭代 1 的指数,每次通过将当前模数值乘以上一次模数值来重新计算。

    Here is the pseudocode:

    1  Set value = 1, current exponent = 0.
    2  Increment current exponent by 1.
    3  Set value = (base  value) mod modulus until current exponent is reached exponent
    
    

    例如:基数:4,指数:3,模数:5

    4% 3% 5 = 64% 5 = 4

    值=(最后值 x 基数)%模数:

    值= (1 x 4) % 5 = 4 % 5 = 4

    值= (4 x 4) % 5 = 16 % 5 = 1

    值= (1 x 4) % 5 = 4 % 5 = 4

    Finally, here is the code:

     1  function modularExponentiation ( base, exponent, modulus ) {
     2          if (modulus == 1) return 0;
     3
     4          var value = 1;
     5
     6          for ( var i=0; i<exponent; i++ ){
     7                  value = (value * base) % modulus;
     8          }
     9          return value;
    10  }
    
    

    时间复杂度: O( n

    时间复杂度为 O( n ),其中 n 等于指数值。

  2. 打印所有小于 n 的质数。

    To do this, use the isPrime function covered in this chapter. Simply iterate from 0 to n and print any prime numbers where isPrime() evaluates to true.

     1  function allPrimesLessThanN(n){
     2          for (var i=0; i<n; i++) {
     3                  if (isPrime(i)){
     4                          console.log(i);
     5                  }
     6          }
     7  }
     8
     9  function isPrime(n){
    10      if (n <= 1) return false;
    11      if (n <= 3) return true;
    12
    13      // This is checked so that we can skip
    14      // middle five numbers in below loop
    15      if (n%2 == 0 || n%3 == 0) return false;
    16
    17      for (var i=5; i*i<=n; i=i+6){
    18          if (n%i == 0 || n%(i+2) == 0)
    19             return false;
    20      }
    21
    22      return true;
    23  }
    24
    25  allPrimesLessThanN(15);
    26
    27  // prints 2, 3, 5, 7, 11, 13
    
    

    时间复杂度: O( nsqrt ( n ))

    这是因为时间复杂度为 O( sqrt ( n ))的isPrime(本章前面已经介绍过)运行了 n 次。

  3. 检查一组质因数。

    让我们把难看的数字定义为那些唯一的质因数是 2、3 或 5 的数字。序列 1,2,3,4,5,6,8,9,10,12,15,…显示了前 11 个难看的数字。按照惯例,包含 1。

    To do this, divide the number by the divisors (2, 3, 5) until it cannot be divided without a remainder. If the number can be divided by all the divisors, it should be 1 after dividing everything.

     1  function maxDivide (number, divisor) {
     2          while (number % divisor == 0) {
     3                  number /= divisor;
     4          }
     5          return number;
     6  }
     7
     8  function isUgly (number){
     9          number = maxDivide(number, 2);
    10          number = maxDivide(number, 3);
    11          number = maxDivide(number, 5);
    12          return number === 1;
    13  }
    
    

    Iterate this over n, and now the list of ugly numbers can be returned.

     1  function arrayNUglyNumbers (n) {
     2          var counter = 0, currentNumber = 1, uglyNumbers = [];
     3
     4          while ( counter != n ) {
     5
     6                  if ( isUgly(currentNumber) ) {
     7                          counter++;
     8                          uglyNumbers.push(currentNumber);
     9                  }
    10
    11                  currentNumber++;
    12          }
    13
    14          return uglyNumbers;
    15  }
    
    

isUgly功能受限于maxDivide(number, 2)的时间复杂度。因此,arrayNUglyNumbersn 倍的时间复杂度。

摘要

回想一下,JavaScript 中的所有数字都是 32 位浮点格式。为了获得尽可能小的浮点增量,应该使用Number.EPILSON。JavaScript 的最大和最小数量可以用下面的不等式来概括:

-Infinity < Number.MIN_SAFE_INTEGER < Number.MIN_VALUE < 0
< Number.MAX_SAFE_INTEGER < Number.MAX_VALUE < Infinity

质数验证和质因数分解是在各种计算机科学应用中使用的概念,比如加密,在第四章中有介绍。最后,JavaScript 中的随机数生成通过Math.random()工作。

四、JavaScript 字符串

这一章将集中讨论字符串、JavaScript String对象和String对象的内置函数。您将学习如何访问、比较、分解和搜索现实生活中常用的字符串。此外,本章将探讨字符串编码、解码、加密和解密。到本章结束时,你将理解如何有效地使用 JavaScript 字符串,并对字符串编码和加密有一个基本的了解。

JavaScript 字符串原语

JavaScript 的原生String原语带有各种常见的字符串函数。

字符串访问

访问字符时,使用.chartAt()

1  'dog'.charAt(1); // returns "o"

.charAt(index)获取一个索引(从 0 开始)并返回字符串中该索引位置的字符。

对于字符串(多字符)访问,您可以使用.substring(startIndex, endIndex),它将返回指定索引之间的字符。

1  'YouTube'.substring(1,2); // returns 'o'
2  YouTube'.substring(3,7); // returns 'tube'

如果您没有传递第二个参数(endIndex),它将返回从指定的开始位置到结束位置的所有字符值。

1  return 'YouTube'.substring(1); // returns 'outube'

字符串比较

大多数编程语言都有比较字符串的功能。在 JavaScript 中,这可以通过使用小于和大于操作符来实现。

1  var a = 'a';
2  var b = 'b';
3  console.log(a < b); // prints 'true'

这对于在排序算法时比较字符串非常有用,这将在本书后面介绍。

但是,如果您正在比较两个不同长度的字符串,它将从字符串的开头开始比较,直到较小字符串的长度。

1  var a = 'add';
2  var b = 'b';
3
4  console.log(a < b); // prints 'true'

在这个例子中,比较了ab。由于a小于b,所以a < b的计算结果为true

1  var a = 'add';
2  var b = 'ab';
3  console.log(a < b); // prints 'false'

在这个例子中,在比较了'a''b'之后,比较了'd''b'。处理无法继续,因为'ab'的一切都已被关注。这和比较'ad''ab'是一样的。

1  console.log('add'<'ab' == 'ad'<'ab'); // prints 'true'

字符串搜索

要在字符串中查找特定的字符串,可以使用.indexOf(searchValue[, fromIndex])。这需要一个作为要搜索的字符串的参数,以及一个用于搜索的起始索引的可选参数。它返回匹配字符串的位置,但是如果什么也没有找到,那么返回-1。请注意,该函数区分大小写。

1  'Red Dragon'.indexOf('Red');    // returns 0
2  'Red Dragon'.indexOf('RedScale'); // returns -1
3  'Red Dragon'.indexOf('Dragon', 0); // returns 4
4  'Red Dragon'.indexOf('Dragon', 4); // returns 4
5  'Red Dragon'.indexOf(", 9);    // returns 9

要在更大的字符串中检查搜索字符串的出现,只需检查-1 是否从. indexOf 返回。

1  function existsInString (stringValue, search) {
2          return stringValue.indexOf(search) !== -1;
3  }
4  console.log(existsInString('red','r')); // prints 'true';
5  console.log(existsInString('red','b')); // prints 'false';

您可以使用附加参数在字符串中的某个索引后进行搜索。一个例子是计算某些字母的出现次数。在以下示例中,将计算字符'a'的出现次数:

1  var str         = "He's my king from this day until his last day";
2  var count       = 0;
3  var pos         = str.indexOf('a');
4  while (pos !== -1) {
5    count++;
6    pos = str.indexOf('a', pos + 1);
7  }
8  console.log(count); // prints '3'

最后,如果字符串以指定的输入开始,startsWith返回 true(布尔值),并且endsWith检查字符串是否以指定的输入结束。

1  'Red Dragon'.startsWith('Red'); // returns true
2  'Red Dragon'.endsWith('Dragon'); // returns true
3  'Red Dragon'.startsWith('Dragon'); // returns false
4  'Red Dragon'.endsWith('Red'); // returns false

字符串分解

要将一个字符串分解成多个部分,可以使用.split(separator),这是一个非常有用的函数。它接受一个参数(分隔符)并创建一个子字符串数组。

1  var test1 = 'chicken,noodle,soup,broth';
2  test1.split(","); // ["chicken", "noodle", "soup", "broth"]

传递一个空分隔符将创建一个包含所有字符的数组。

1  var test1 = 'chicken';
2  test1.split(""); // ["c", "h", "i", "c", "k", "e", "n"]

当一个字符串中列出多个项目时,这很有用。可以将字符串转换成数组,以便轻松地遍历它们。

字符串替换

.replace(string, replaceString)用另一个字符串替换字符串变量中的指定字符串。

1  "Wizard of Oz".replace("Wizard","Witch"); // "Witch of Oz"

正则表达式

正则表达式( regexes )是定义搜索模式的一组字符。学习如何使用正则表达式本身就是一项艰巨的任务,但是作为一名 JavaScript 开发人员,了解正则表达式的基础知识非常重要。

JavaScript 还带有原生对象RegExp,用于正则表达式。

RegExp对象的构造函数有两个参数:正则表达式和可选的匹配设置,如下所示:

i      Perform case-insensitive matching
g      Perform a global match (find all matches rather than stopping after first match)
m      Perform multiline matching

RegExp有以下两个功能:

  • search():测试字符串中的匹配。这将返回匹配的索引。

  • match():匹配测试。这将返回所有匹配项。

JavaScript String对象还有以下两个与 regex 相关的函数,它们接受RegExp对象作为参数:

  • exec():测试字符串中的匹配。这将返回第一个匹配项。

  • test():测试字符串中的匹配。这将返回truefalse

基本正则表达式

以下是基本的正则表达式规则:

^:表示字符串/行的开始

\d:查找任意数字

[abc]:查找括号之间的任何字符

[^abc]:查找括号中除以外的任何字符

*[0-9]:查找括号之间的任意数字

[⁰-9]:查找括号中除以外的任何数字

*(x|y):查找任何指定的备选项

以下返回索引 11,它是字符D的索引,该字符是匹配的正则表达式的第一个字符:

1  var str = "JavaScript DataStructures";
2  var n = str.search(/DataStructures/);
3  console.log(n); // prints '11'

常用的正则表达式

正则表达式对于检查 JavaScript 中用户输入的有效性非常有帮助。一种常见的输入检查是验证它是否有任何数字字符。

以下是开发人员经常使用的五种正则表达式。

任何数字字符
   /\d+/

1  var reg = /\d+/;
2  reg.test("123"); // true
3  reg.test("33asd"); // true
4  reg.test("5asdasd"); // true
5  reg.test("asdasd"); // false

仅数字字符
  /^\d+$/

1  var reg = /^\d+$/;
2  reg.test("123"); // true
3  reg.test("123a"); // false
4  reg.test("a"); // false

浮动数字字符
   /^[0-9]*.[0-9]*[1-9]+$/

1  var reg = /^[0-9]*.[0-9]*[1-9]+$/;
2  reg.test("12"); // false
3  reg.test("12.2"); // true

仅字母数字字符
   /[a-zA-Z0-9]​/

1  var reg = /[a-zA-Z0-9]/;
2  reg.test("somethingELSE"); // true
3  reg.test("hello"); // true
4  reg.test("112a"); // true
5  reg.test("112"); // true
6  reg.test("^"); // false

查询字符串
/([^?=&]+)(=([^&]*))/

在 web 应用程序中,出于路由或数据库查询的目的,web URLs 经常在 URL 中传递参数。

例如,对于 URL http://your.domain/product.aspx?category=4&product_id=2140&query=lcd+tv ,URL 可能会对后端 SQL 查询做出如下响应:

1  SELECT LCD, TV FROM database WHERE Category = 4 AND Product_id=2140;

要解析这些参数,正则表达式会很有用。

1 var  uri = 'http://your.domain/product.aspx?category=4&product_id=2140&query=lcd+tv' ;
2 var  queryString =  {};
3  uri.replace(
4                  new RegExp ("([^?=&]+)(=([^&]*))?" , "g" ),
5                  function ($0, $1, $2, $3) { queryString[$1] =  $3; }
6  );
7  console.log('ID: ' +  queryString['product_id' ]); // ID: 2140
8  console.log('Name: ' +  queryString['product_name' ]); // Name: undefined
9  console.log('Category: ' +  queryString['category' ]); // Category: 4

编码

编码是计算机科学中的一个通用概念,它以一种专门的格式来表示字符,以便有效地传输或存储。

所有计算机文件类型都以特定的结构编码。

例如,当您上传 PDF 时,编码可能如下所示:

 1  JVBERi0xLjMKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2VzIDMgMCBS\
 2  ID4+CmVuZG9iagoyIDAgb2JqCjw8IC9UeXBlIC9PdXRsaW5lcyAvQ291bnQgMCA+PgplbmRvYmoKMyAwIG9i\
 3  ago8PCAvVHlwZSAvUGFnZXMKL0tpZHMgWzYgMCBSCl0KL0NvdW50IDEKL1Jlc291cmNlcyA8PAovUHJvY1Nl\
 4  dCA0IDAgUgovRm9udCA8PCAKL0YxIDggMCBSCj4+Cj4+Ci9NZWRpYUJveCBbMC4wMDAgMC4wMDAgNjEyLjAw\
 5  MCA3OTIuMDAwXQogPj4KZW5kb2JqCjQgMCBvYmoKWy9QREYgL1RleHQgXQplbmRvYmoKNSAwIG9iago8PAov\
 6  Q3JlYXRvciAoRE9NUERGKQovQ3JlYXRpb25EYXRlIChEOjIwMTUwNzIwMTMzMzIzKzAyJzAwJykKL01vZERh\
 7  dGUgKEQ6MjAxNTA3MjAxMzMzMjMrMDInMDAnKQo+PgplbmRvYmoKNiAwIG9iago8PCAvVHlwZSAvUGFnZQov\
 8  UGFyZW50IDMgMCBSCi9Db250ZW50cyA3IDAgUgo+PgplbmRvYmoKNyAwIG9iago8PCAvRmlsdGVyIC9GbGF0\
 9  ZURlY29kZQovTGVuZ3RoIDY2ID4+CnN0cmVhbQp4nOMy0DMwMFBAJovSuZxCFIxN9AwMzRTMDS31DCxNFUJS\
10  FPTdDBWMgKIKIWkKCtEaIanFJZqxCiFeCq4hAO4PD0MKZW5kc3RyZWFtCmVuZG9iago4IDAgb2JqCjw8IC9U\
11  eXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovTmFtZSAvRjEKL0Jhc2VGb250IC9UaW1lcy1Cb2xkCi9FbmNv\
12  ZGluZyAvV2luQW5zaUVuY29kaW5nCj4+CmVuZG9iagp4cmVmCjAgOQowMDAwMDAwMDAwIDY1NTM1IGYgCjAw\
13  MDAwMDAwMDggMDAwMDAgbiAKMDAwMDAwMDA3MyAwMDAwMCBuIAowMDAwMDAwMTE5IDAwMDAwIG4gCjAwMDAw\
14  MDAyNzMgMDAwMDAgbiAKMDAwMDAwMDMwMiAwMDAwMCBuIAowMDAwMDAwNDE2IDAwMDAwIG4gCjAwMDAwMDA0\
15  NzkgMDAwMDAgbiAKMDAwMDAwMDYxNiAwMDAwMCBuIAp0cmFpbGVyCjw8Ci9TaXplIDkKL1Jvb3QgMSAwIFIK\
16  L0luZm8gNSAwIFIKPj4Kc3RhcnR4cmVmCjcyNQolJUVPRgo=.....

这是一个 Base64 编码的 PDF 字符串。像这样的数据通常在上传 PDF 文件时被传递到服务器。

Base64 编码

btoa()函数从一个字符串创建一个 Base64 编码的 ASCII 字符串。字符串中的每个字符都被视为一个字节(8 位:8 个 0 和 1)。

.atob()函数对使用 Base64 编码的数据字符串进行解码。例如,Base64 编码字符串中的字符串“hello I love learning to computer program”如下所示:agvsbg 8 GSS BSB 3 zlig xlyxjuaw 5 nihrvignvbxb 1 dgvyihby B2 dyyw 0。

1  btoa('hello I love learning to computer program');
2  //  aGVsbG8gSSBsb3ZlIGxlYXJuaW5nIHRvIGNvbXB1dGVyIHByb2dyYW0
1  atob('aGVsbG8gSSBsb3ZlIGxlYXJuaW5nIHRvIGNvbXB1dGVyIHByb2dyYW0');
2  // hello I love learning to computer program

https://en.wikipedia.org/wiki/Base64 了解 Base64 的更多信息。

字符串缩短

你有没有想过像这样的网址缩短网站。ly 工作?一种简化的 URL 压缩算法遵循一定的结构,如下图所示为 www.google.com :

img/465726_1_En_4_Fig2_HTML.png

图 4-2

缩短后的数据库条目

  1. 整数 ID 被缩短为一个字符串。使用 Base62 编码缩短时,11231230 将是 VhU2。

img/465726_1_En_4_Fig1_HTML.png

图 4-1

数据库条目

  1. 数据库为 URL 创建一个唯一的基于整数的 ID。在图 4-1 , www.google.com 数据库中有条目 11231230。

对于缩短部分,可以使用下面的算法。有 62 个可能的字母和数字,由 26 个小写字母、26 个大写字母和 10 个数字(0 到 9)组成。

1 var  DICTIONARY = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" .split(");
2
3 function  encodeId(num) {
4               var  base =  DICTIONARY.length;
5               var  encoded = " ;
6
7               if  (num === 0 ) {
8                    return  DICTIONARY[0 ];
9               }
10
11                     while  (num > 0 ) {
12                        encoded +=  DICTIONARY[(num %  base)];
13                        num = Math .floor(num /  base);
14          }
15
16              return  reverseWord(encoded);
17  }
18
19 function  reverseWord(str) {
20              var  reversed = "" ;
21              for  (var  i =  str.length - 1 ; i >= 0 ; i-- ) {
22                                reversed +=  str.charAt(i);
23          }
24          return  reversed;
25  }
26
27 function  decodeId(id) {
28              var  base =  DICTIONARY.length;
29              var  decoded = 0 ;
30
31              for  (var  index = 0 ; index <  id.split("" ).length; index++ ) {
32                          decoded =  decoded *  base + DICTIONARY.indexOf(id.charAt(index));
33              }
34
35              return  decoded; 

36  }
37
38  console.log(encodeId(11231230 )); // prints 'VhU2'
39  console.log(decodeId('VhU2' )); // prints '11231230'

加密

在保护人们的在线信息时,加密极其重要。你在谷歌 Chrome 浏览器中见过图 4-3 中的警告吗?

img/465726_1_En_4_Fig3_HTML.jpg

图 4-3

SSL 警告

这可能意味着您尝试访问的网站没有正确的安全套接字层(SSL)证书。

img/465726_1_En_4_Fig4_HTML.png

图 4-4

TSL 过程

TSL 是一种标准的安全技术,用于在服务器和客户端(浏览器)之间建立加密链接。以下是 TSL 过程的简化步骤。在这个过程中,服务器对不同的密钥使用非对称加密进行加密和解密。浏览器只使用对称加密,即使用一个密钥来加密和解密数据。

  1. 服务器将其非对称公钥发送给浏览器。

  2. 浏览器为当前会话创建一个对称密钥,该密钥由服务器的非对称公钥加密。

  3. 服务器通过其私钥解密浏览器的会话,并检索会话密钥。

  4. 两个系统都有会话密钥,并将使用该密钥安全地传输数据。

这是安全的,因为只有浏览器和服务器知道会话密钥。如果浏览器第二天连接到同一个服务器,将会创建一个新的会话密钥。

SSL 警告消息表示浏览器和服务器可能没有加密该连接上的数据。

最常用的公钥加密算法是 RSA 算法。

RSA 加密

RSA 是一种基于分解大整数难度的加密算法。在 RSA 中,生成两个大素数和一个补充值作为公钥。任何人都可以用公钥加密信息,但是只有那些有质因数的人才能解码信息。

这个过程有三个阶段:密钥生成、加密和解密。

  • 密钥生成:生成公钥(共享)和私钥(保密)。生成的密钥的构造方法也应该是保密的。

  • 加密:秘密消息可以通过公钥加密。

  • 解密:只有私钥可以用来解密消息。

以下是该算法的概述:

  1. 选择两个(通常是大的)素数, pq

    1. pq 的乘积记为 n

    2. ( p -1)和( q -1)的乘积表示为φ

  2. 选择两个指数, ed

    1. e 通常为 3。可以使用大于 2 的其他值。

    2. d 是使得(e × d) % phi = 1 的值。

Encryption

process is as shown:
        m - message:
        m^e % n = c
        c - encrypted message

Decryption process is as shown:
        c^d % n = m

这是计算 d 的实现:

 1  function modInverse(e, phi) {
 2      var m0 = phi, t, q;
 3      var x0 = 0, x1 = 1;
 4
 5      if (phi == 1)
 6        return 0;
 7
 8      while (e > 1) {
 9          // q is quotient
10          q = Math.floor(e / phi);
11
12          t = phi;
13
14          // phi is remainder now, process same as
15          // Euclid's algo
16          phi = e % phi, e = t;
17
18          t = x0;
19
20          x0 = x1 - q * x0;
21
22          x1 = t;
23      }
24
25      // Make x1 positive
26      if (x1 < 0)
27         x1 += m0;
28
29      return x1;
30  }
31  modInverse(7,40); // 23

还需要生成公钥和私钥的密钥对。

 1  function RSAKeyPair(p, q) {
 2          // Need to check that they are primes
 3          if (! (isPrime(p) && isPrime(q)))
 4                  return;
 5
 6          // Need to check that they're not the same
 7          if (p==q)
 8                  return;
 9
10          var n = p * q,
11                  phi = (p-1)*(q-1),
12                  e = 3,
13                  d = modInverse(e,phi);
14
15          // Public key: [e,n], Private key: [d,n]
16          return [[e,n], [d,n]]
17  }

让我们选择 5 和 11 作为质数,看看message是 50 的例子。

1  RSAKeyPair(5,11); //Public key: [3,55], Private key: [27,55]

   p = 5, 11
   n = p x q = 55
   phi = (5-1) x (11-1) = 4 x 10 = 40
   e = 3
   (e x d) % phi = 1 (3 x d) % 40 = 1
   (81) % 40 = 1\. 81 = 3 x d = 3 x 27
   d = 27

   Encryption:
           m - message: 50
           m^e % n = c
           50³ % 55 = 40

   Encrypted message.,c:
           40
Decryption:
c^d % n = m
40²⁷ % 55 = 50

这完全加密了 50,而接收者可以将其解密回 50。通常,对于 RSA 算法,所选择的素数非常大。这是因为大数的质因数分解需要很长的计算时间。今天的标准是使用 4096 位的 prime 产品。即使是先进的计算机,计算它的质因数也需要数年时间。图 4-5 显示了 4096 位数的最大可能值。

img/465726_1_En_4_Fig5_HTML.jpg

图 4-5

2 4096

摘要

本章涵盖了各种本机实现的字符串函数,并在表 4-1 中进行了总结。

表 4-1

字符串函数摘要

|

功能

|

使用

| | --- | --- | | charAt(index) | 在index访问单个字符 | | substring(startIndex, endIndex) | 访问从startIndexendIndex的部分字符串 | | str1 > str2 | 如果str1在字典上比str2大,则返回true | | indexOf(str, startIndex) | 从startIndex开始的期望str的索引 | | str.split(delimiter) | 用指定的delimiter将一个字符串拆分成一个数组 | | str.replace(original,new) | 用new替换original |

此外,JavaScript 原生Regex对象可以用于常用的字符串验证。表 4-2 提供了一个总结。

表 4-2

正则表达式摘要

|

正则表达式模式

|

使用

| | --- | --- | | /\d+/ | 任何数字字符 | | /^\d+$/ | 仅数字字符 | | /^[0-9]*.[0-9]*[1-9]+$/ | 浮点数字字符 | | /[a-zA-Z0-9] / | 仅字母数字字符 |**

五、JavaScript 数组

这一章将集中讨论 JavaScript 数组的使用。作为一个 JavaScript 开发者,你会经常用到数组;它是最常用的数据结构。JavaScript 中的数组有很多内置方法。事实上,对于每个用例,有各种方法可以进行相同类型的数组操作。本章结束时,你将理解如何使用数组,并能够根据情况选择正确的方法。

阵列简介

数组是最基本的数据结构之一。如果你以前编程过,你很可能使用过数组。

1   var array1 = [1,2,3,4];

对于任何数据结构,开发人员感兴趣的是与四个基本操作相关的时间和空间复杂性:访问、插入、删除和搜索。(关于 Big-O 符号的回顾,请参考第一章。)

插入

插入意味着在数据结构中添加一个新元素。JavaScript 用.push(element)方法实现数组插入。此方法在数组末尾添加一个新元素。

1   var array1 = [1,2,3,4];
2   array1.push(5); //array1 = [1,2,3,4,5]
3   array1.push(7); //array1 = [1,2,3,4,5,7]
4   array1.push(2); //array1 = [1,2,3,4,5,7,2]

该运算的时间复杂度理论上为 O(1)。应该注意的是,实际上,这取决于运行代码的 JavaScript 引擎。这适用于所有本地支持的 JavaScript 对象。

删除

JavaScript 用.pop()方法实现数组删除。此方法移除数组中最后添加的元素。这也将返回移除的元素。

1   var array1 = [1,2,3,4];
2   array1.pop(); //returns 4, array1 = [1,2,3]
3   array1.pop(); //returns 3, array1 = [1,2]

.push类似,.pop的时间复杂度为 O(1)。

另一种从数组中移除元素的方法是使用.shift()方法。此方法将移除第一个元素并返回它。

1   array1 = [1,2,3,4];
2   array1.shift(); //returns 1, array1 = [2,3,4]
3   array1.shift(); //returns 2, array1 = [3,4]

接近

访问指定索引处的数组只需要 O(1),因为该进程使用该索引直接从内存中的地址获取值。这是通过指定索引来完成的(记住索引从 0 开始)。

1   var array1 = [1,2,3,4];
2   array1[0]; //returns 1
3   array1[1]; //returns 2

循环

迭代是访问数据结构中包含的每一项的过程。JavaScript 中有多种方法可以遍历数组。它们都具有 O( n )的时间复杂度,因为迭代正在访问 n 个元素。

for(变量;条件;修改)

for是最常见的迭代方法。它通常以这种形式使用:

1   for ( var i=0, len=array1.length; i<len; i++ ) {
2       console.log(array1[i]);
3   }

前面的代码简单来说就是初始化变量i,在执行主体(i<len)之前检查条件是否为假,然后修改(i++),直到条件为假。同样,你可以使用一个while循环。但是,计数器必须设置在室外。

1   var counter=0;
2   while(counter<array1.length){
3       // insert code here
4       counter++;
5   }

您可以使用while循环实现无限循环,如下所示:

1   while(true){
2       if (breakCondition) {
3           break;
4       }
5   }

类似地,for循环可以通过不设置条件来实现无限循环,如下所示:

1   for ( ; ;) {
2       if (breakCondition) {
3           break
4       }
5   }

对于(在)

另一种迭代 JavaScript 数组的方法是逐个调用索引。在in之前指定的变量是数组的index,如下所示:

1   var array1 = ['all','cows','are','big'];
2
3   for (var index in array1) {
4       console.log(index);
5   }

这将打印以下内容:0,1,2,3

要打印内容,请使用:

1   for (var index in array1) {
2       console.log(array1[index]);
3   }

这将打印出allcowsarebig

对于(的)

of之前指定的变量是数组的element(值),如下所示:

1   for (var element of array1) {
2       console.log(element);
3   }

这将打印出allcowsarebig

forEach()

forEach和其他迭代方法的最大区别是forEach不能中断迭代或者跳过数组中的某些元素。forEach通过遍历每一个元素,更加具有表现力和明确性。

1   var array1 = ['all','cows','are','big'];
2
3   array1.forEach( function (element, index){
4       console.log(element);
5   });
6
7   array1.forEach( function (element, index){
8       console.log(array1[index]);
9   });

都打印allcowsarebig

助手功能

以下部分讨论了其他常用的处理帮助函数。此外,还将介绍如何使用数组。

。切片(开始,结束)

这个 helper 函数返回一个现有数组的一部分,而不修改数组。.slice()接受两个参数:数组的开始索引和结束索引。

1   var array1 = [1,2,3,4];
2   array1.slice(1,2); //returns [2], array1 = [1,2,3,4]
3   array1.slice(2,4); //returns [3,4], array1 = [1,2,3,4]

如果只传递开始的索引,那么结尾将被认为是最大的索引。

1   array1.slice(1); //returns [2,3,4], array1 = [1,2,3,4]
2   array1.slice(1,4); //returns [2,3,4], array1 = [1,2,3,4]

如果没有传递任何东西,这个函数只返回数组的一个副本。需要注意的是,array1.slice() === array1评估为false。这是因为尽管数组的内容是相同的,但是这些数组所在的内存地址是不同的。

1   array1.slice(); //returns [1,2,3,4], array1 = [1,2,3,4]

这对于在 JavaScript 中复制数组很有用。记住 JavaScript 中的数组是基于引用的,这意味着如果你给一个数组赋值一个新的变量,对该变量的修改会应用到原来的数组。

 1   var array1 = [1,2,3,4],
 2       array2 = array1;
 3
 4   array1 // [1,2,3,4]
 5   array2 // [1,2,3,4]
 6
 7   array2[0] = 5;
 8
 9   array1 // [5,2,3,4]
10   array2 // [5,2,3,4]

array2的 changing 元素意外改变了原数组,因为它是对原数组的引用。要创建一个新的数组,可以使用.from()

 1   var array1 = [1,2,3,4];
 2   var array2 = Array.from(array1);
 3
 4   array1 // [1,2,3,4]
 5   array2 // [1,2,3,4]
 6
 7   array2[0] = 5;
 8
 9   array1 // [1,2,3,4]
10   array2 // [5,2,3,4]

.from()取 O( n ,其中 n 是数组的大小。这很直观,因为复制数组需要复制数组的所有 n 个元素。

。拼接(开始、尺寸、元素 1、元素 2…)

这个 helper 函数通过删除现有元素和/或添加新元素来返回和更改数组的内容。

.splice()接受三个参数:起始索引、要删除的内容的大小和要添加的新元素。在第一个参数指定的位置添加新元素。它返回删除的元素。

1   var array1 = [1,2,3,4];
2   array1.splice(); //returns [], array1 = [1,2,3,4]
3   array1.splice(1,2); //returns [2,3], array1 = [1,4]

这个例子演示了删除。[2,3]被返回,因为它从索引 1 开始选择了两个项目。

1   var array1 = [1,2,3,4];
2   array1.splice(); //returns [], array1 = [1,2,3,4]
3   array1.splice(1,2,5,6,7); //returns [2,3],array1 = [1,5,6,7,4]

任何东西(任何对象类型)都可以添加到数组中。这就是 JavaScript 的美妙之处(也是奇怪的地方)。

1   var array1 = [1,2,3,4];
2   array1.splice(1,2,[5,6,7]); //returns [2,3], array1 = [1,[5,6,7],4]
3   array1 = [1,2,3,4];
4   array1.splice(1,2,{'ss':1}); //returns [2,3], array1 = [1,{'ss':1},4]

.splice()是,最坏的情况,O( n )。类似于复制,如果指定的范围是整个数组,每个 n 项都必须被删除。

。concat()

这将在数组末尾添加新元素,并返回数组。

1   var array1 = [1,2,3,4];
2   array1.concat(); //returns [1,2,3,4], array1 = [1,2,3,4]
3   array1.concat([2,3,4]); //returns [1,2,3,4,2,3,4],array1 = [1,2,3,4]

。长度属性

属性返回数组的大小。将此属性更改为较小的大小会从数组中删除元素。

1   var array1 = [1,2,3,4];
2   console.log(array1.length); //prints 4
3   array1.length = 3; // array1 = [1,2,3]

传播算子

由三个句点表示的扩展运算符(...),用于在应该没有参数的地方扩展参数。

1   function addFourNums(a, b, c, d) {
2      return a + b + c + d;
3   }
4   var numbers = [1, 2, 3, 4];
5   console.log(addFourNums(...numbers)); // 10

Math.maxMath.min函数都接受无限数量的参数,因此您可以使用 spread 操作符进行以下操作。

要查找数组中的最大值,请使用以下命令:

1   var array1 = [1,2,3,4,5];
2   Math.max(array1); // 5

要查找数组中的最小值,请使用以下命令:

1   var array2 = [3,2,-123,2132,12];
2   Math.min(array2); // -123

练习

所有练习的代码都可以在 GitHub 上找到。 1

在一个数组中找出两个相加为一个数的数组元素

问题:给定数组arr,找到并返回数组中两个加起来为weight的索引,如果没有加起来为weight的组合,则返回-1。

比如像[1,2,3,4,5]这样的数组,有哪些数字加起来是 9?

答案当然是 4 和 5。

简单的解决方案是通过两个for循环来尝试每种组合,如下所示:

 1   function findSum(arr, weight) {
 2       for (var i=0,arrLength=arr.length; i<arrLength; i++){
 3           for (var j=i+1; j<arrLength; j++) {
 4               if (arr[i]+arr[j]==weight){
 5                   return [i,j];
 6               }
 7           }
 8       }
 9       return -1;
10   }

这个解决方案遍历一个数组,查看是否存在匹配对。

数组的 n 元素上的两个for循环产生高时间复杂度。但是,没有创建额外的内存。类似于时间复杂度如何描述相对于输入大小 n 完成算法所需的时间,空间复杂度描述实现所需的额外存储器。空间复杂度 O(1)是常数。

**时间复杂度:**O(n2

空间复杂度: O(1)

我们来想想在 O( n )的线性时间内如何做到这一点。

如果存储了任何以前看到的数组元素并且可以很容易地进行检查,那会怎么样呢?

输入如下:

1   var arr = [1,2,3,4,5];
2   var weight = 9;

这里 4 和 5 是组合,它们的索引是[3,4]。当访问 5 时,如何确定解决方案存在?

如果当前值为 5,权重为 9,则剩余的所需权重仅为 4 (9-5=4)。由于在数组中 4 显示在 5 之前,这个解决方案可以在 O( n )中工作。最后,为了存储看到的元素,使用一个 JavaScript 对象作为哈希表。散列表的实现和使用将在后面的章节中讨论。存储和检索 JavaScript 对象属性在时间上是 O(1)。

 1   function findSumBetter(arr, weight) {
 2       var hashtable = {};
 3
 4       for (var i=0, arrLength=arr.length; i<arrLength; i++) {
 5           var currentElement = arr[i],
 6               difference = weight - currentElement;
 7
 8           // check the right one already exists
 9           if (hashtable[currentElement] != undefined) {
10               return [i, hashtable[weight-currentElement]];
11           } else {
12               // store index
13               hashtable[difference] = i;
14           }
15       }
16       return -1;
17   }

时间复杂度: O( n

空间复杂度: O( n

存储到哈希表中并从哈希表中查找一个项目只需要 O(1)。空间复杂度增加到 O( n )来存储哈希表中的访问过的数组索引。

实现数组。Slice()函数从头开始

让我们回顾一下.slice()函数的作用。

.slice()接受两个参数:数组的开始索引和最后一个结束索引。它返回现有数组的一部分,而不修改数组函数arraySlice ( arraybeginIndexendIndex)。

1 function arraySlice(array, beginIndex, endIndex) {
2 // If no parameters passed, return the array
3     if  (! beginIndex && ! endIndex) {
4         return  array;
5    }
6
7 // If only beginning index is found, set endIndex to size
8  endIndex =  array.length;
9
10 var  partArray =  [];
11
12 // If both begin and end index specified return the part of the array
13 for  (var  i =  beginIndex; i <  endIndex; i++ ) {
14    partArray.push(array[i]);
15  }
16
17         return  partArray;
18  }
19  arraySlice([1 , 2 , 3 , 4 ], 1 , 2 ); // [2]
20  arraySlice([1 , 2 , 3 , 4 ], 2 , 4 ); // [3,4]

时间复杂度: O( n

空间复杂度 : O( n

时间复杂度为 O( n ),因为必须访问数组中的所有 n 项。复制数组时空间复杂度也是 O( n )容纳所有 n 项。

求两个大小相同的排序数组的中间值

回想一下,偶数集合中的中位数是两个中间数的平均值。如果数组是排序的,这就简单了。

这里有一个例子:

[1,2,3,4]的中位数为(2+3)/2 = 2.5。

 1   function medianOfArray(array) {
 2       var length = array.length;
 3       // Odd
 4       if (length % 2 == 1) {
 5           return array[Math.floor(length/2)];
 6       } else {
 7       // Even
 8           return (array[length/2]+array[length/2 - 1])/2;
 9       }
10   }

现在,您可以遍历这两个数组,比较哪个更大,以跟踪中位数。如果两个数组大小相同,则总大小将是一个偶数。

这是因为两个偶数和两个奇数加起来就是一个偶数。请参阅第八章了解更多背景信息。

因为两个数组都是排序的,所以这个函数可以递归调用。每次,它都会检查哪个中值更大。

如果第二个数组的中值较大,则第一个数组被切成两半,只有较高的一半被递归传递。

如果第一个数组的中值较大,则第二个数组被切成两半,只有较高的一半作为下一个函数调用的第一个数组被传入,因为函数中的array2参数必须总是大于array1参数。最后,需要用pos表示的数组的大小来检查数组的大小是偶数还是奇数。

这是另一个例子:

数组 1 = [1,2,3]和数组 2 = [4,5,6]

这里,array1的中位数是 2,array2的中位数是 5。因此,中位数必须在[2,3]和[4,5]之间。由于只剩下四个元素,中值可以计算如下:

max(arr1[0],arr2[0]) + min(arr1[1],arr 2[1])/2;

 1 function  medianOfArray(array) {
 2     var  length =  array.length;
 3     // Odd
 4     if  (length % 2 == 1 ) {
 5         return  array[Math .floor(length / 2 )];
 6    } else  {
 7     // Even
 8         return  (array[length / 2 ] +  array[length / 2 - 1 ]) / 2 ;
 9    }
10  }
11 // arr2 is the bigger array
12 function  medianOfTwoSortedArray(arr1, arr2, pos) {
13     if  (pos <= 0 ) {
14         return -1 ;
15    }
16     if  (pos == 1 ) {
17         return  (arr1[0] +  arr2[0]) / 2 ;
18    }
19     if  (pos == 2 ) {
20         return  (Math .max(arr1[0], arr2[0]) + Math .min(arr1[1], arr2[1])) / 2 ;
21    }
22
23     var  median1 =  medianOfArray(arr1),
24         median2 =  medianOfArray(arr2);
25
26     if  (median1 ==  median2) {
27         return  median1;
28    }
29
30     var  evenOffset =  pos % 2 == 0 ? 1 : 0 ,
31        offsetMinus = Math .floor(pos / 2 ) -  evenOffset,
32        offsetPlus = Math .floor(pos / 2 ) +  evenOffset;
33
34
35     if  (median1 <  median2) {
36         return  medianOfTwoSortedArray(arr1.slice(offsetMinus), arr2.slice(offsetMinus), offsetPlus);
37    } else  {
38         return  medianOfTwoSortedArray(arr2.slice(offsetMinus), arr1.slice(offsetMinus), offsetPlus);
39    }
40  }
41
42  medianOfTwoSortedArray([1 , 2 , 3 ], [4 , 5 , 6 ], 3 ); // 3.5
43  medianOfTwoSortedArray([11 , 23 , 24 ], [32 , 33 , 450 ], 3 ); // 28
44  medianOfTwoSortedArray([1 , 2 , 3 ], [2 , 3 , 5 ], 3 ); // 2.5

**时间复杂度:**O(log2(n))

通过每次将数组大小减半,实现了对数时间复杂度。

在 K 排序数组中寻找公共元素

1   var arr1    = [1, 5, 5, 10];
2   var arr2    = [3, 4, 5, 5, 10];
3   var arr3    = [5, 5, 10, 20];
4   var output  = [5 ,10];

在这个有三个数组的例子中, k =3。

为此,只需迭代每个数组并计算每个元素的实例数。但是,不要跟踪重复的(5 和 5.5 应该在一次数组迭代中计算一次)。为此,在递增之前检查最后一个元素是否相同。这只有在排序的情况下才会起作用。

迭代完所有三个数组后,遍历哈希表的属性。如果该值与 3 匹配,则意味着该数字出现在所有三个数组中。这可以通过将 k 循环检查放入另一个for循环中来推广到 k 个数组。

 1 function  commonElements(kArray) {
 2     var  hashmap =  {},
 3        last, answer =  [];
 4
 5     for  (var  i = 0 , kArrayLength =  kArray.length; i <  kArrayLength; i++ ) {
 6         var  currentArray =  kArray[i];
 7            last = null ;
 8         for  (var  j = 0 , currentArrayLen =  currentArray.length;
 9             j <  currentArrayLen;   j++ ) {
10             var  currentElement =  currentArray[j];
11             if  (last !=  currentElement) {
12                 if  (! hashmap[currentElement]) {
13                    hashmap[currentElement] = 1 ;
14                } else  {
15            hashmap[currentElement]++ ;
16                }
17            }
18         last =  currentElement;
19        }
20     }
21
22     // Iterate through hashmap
23     for  (var  prop in  hashmap) {
24         if  (hashmap[prop] ==  kArray.length) {
25            answer.push(parseInt (prop));
26        }
27    }
28     return  answer;
29  }
30
31  commonElements([[1 ,2 ,3 ],[1 ,2 ,3 ,4 ],[1 ,2 ]]); // [ 1, 2 ]

时间复杂度: O( kn

空间复杂度: O( n

这里, n 是最长的数组长度, k 是数组的个数。

JavaScript 函数数组方法

JavaScript 的某些部分可以像函数式编程语言一样编写。与命令式编程不同,JavaScript 并不关注程序的状态。它不使用循环,只使用函数(方法)调用。你可以从 Anto Aravinth (Apress,2017)的开始函数式 JavaScript 中了解更多关于 JavaScript 的函数式编程。

在本节中,将只探讨 JavaScript 中的三种函数数组方法:mapfilterreduce。这些方法不会改变原始数组内容。

地图

map 函数将传递的函数转换应用于数组中的每个元素,并返回应用了这些转换的新数组。

例如,您可以将每个元素乘以 10,如下所示:

1   [1,2,3,4,5,6,7].map(function (value){
2       return value*10;
3   });
4   // [10, 20, 30, 40, 50, 60, 70]

过滤器

filter 函数只返回满足传递的条件参数的数组元素。同样,这不会改变原始数组。

例如,这会过滤大于 100 的元素:

1   [100,2003,10,203,333,12].filter(function (value){
2       return value > 100;
3   });
4   // [2003, 203, 333]

减少

reduce 函数使用传递的转换函数参数将数组中的所有元素组合成一个值。

例如,这将添加所有元素:

1   var sum = [0,1,2,3,4].reduce( function (prevVal, currentVal, index, array) {
2       return prevVal + currentVal;
3   });
4   console.log(sum); // prints 10

这个函数也可以将initialValue作为第二个参数,它初始化 reduce 值。例如,在前面的示例中提供 1 的initialValue将产生 11,如下所示:

1   var sum = [0,1,2,3,4].reduce( function (prevVal, currentVal, index, array) {
2       return prevVal + currentVal;
3   }, 1);
4   console.log(sum); // prints 11

多维数组

与 Java 和 C++不同,JavaScript 没有多维数组(见图 5-1 )。

img/465726_1_En_5_Fig1_HTML.jpg

图 5-1

多维数组

取而代之的是“锯齿状”阵列。一个交错数组是一个数组,它的元素是数组。交错数组的元素可以有不同的维度和大小(参见图 5-2 )。

img/465726_1_En_5_Fig2_HTML.jpg

图 5-2

锯齿状阵列

这里有一个帮助器函数来创建一个类似图 5-3 中的交错数组:

img/465726_1_En_5_Fig3_HTML.jpg

图 5-3

三乘三矩阵

1   function Matrix(rows, columns) {
2       var jaggedarray = new Array(rows);
3       for (var i=0; i < columns; i +=1) {
4           jaggedarray[i]=new Array(rows);
5       }
6       return jaggedarray;
7   }
8   console.log(Matrix(3,3));

要访问交错数组中的元素,请指定一行和一列(参见图 5-4 )。

img/465726_1_En_5_Fig4_HTML.jpg

图 5-4

三乘三的数字矩阵

 1   var matrix3by3 = [[1,2,3],[4,5,6],[7,8,9]];
 2   matrix3by3[0]; // [1,2,3]
 3   matrix3by3[1]; // [4,5,6]
 4   matrix3by3[1]; // [7,8,9]
 5
 6   matrix3by3[0][0]; // 1
 7   matrix3by3[0][1]; // 2
 8   matrix3by3[0][2]; // 3
 9
10   matrix3by3[1][0]; // 4
11   matrix3by3[1][1]; // 5
12   matrix3by3[1][2]; // 6
13
14   matrix3by3[2][0]; // 7
15   matrix3by3[2][1]; // 8
16   matrix3by3[2][2]; // 9

练习

所有练习的代码都可以在 GitHub 上找到。 2

螺旋印刷

让我们用矩阵做一个例题。给定一个矩阵,按照螺旋顺序打印元素,如图 5-5 所示。

img/465726_1_En_5_Fig5_HTML.jpg

图 5-5

螺旋印刷

起初,这看起来是一项艰巨的任务。然而,这个问题可以分解为五个主要部分。

  • 从左向右打印

  • 从上到下打印

  • 从右向左打印

  • 从下到上打印

  • 对这四种操作进行限制

换句话说,保留四个关键变量,这四个变量表明:

  • 顶行

  • 底端行

  • 左列

  • 右列

每当四个print函数中的一个被成功执行时,简单地增加四个变量中的一个。例如,打印完第一行后,将其递增 1。

 1   var M = [
 2       [1, 2, 3, 4, 5],
 3       [6, 7, 8, 9, 10],
 4       [11, 12, 13, 14, 15],
 5       [16, 17, 18, 19, 20]
 6   ];
 7   function spiralPrint(M) {
 8       var topRow = 0,
 9           leftCol = 0,
10           btmRow = M.length - 1,
11           rightCol = M[0].length - 1;
12
13       while (topRow < btmRow && leftCol < rightCol) {
14           for (var col = 0; col <= rightCol; col++) {
15               console.log(M[topRow][col]);
16           }
17           topRow++;
18           for (var row = topRow; row <= btmRow; row++) {
19               console.log(M[row][rightCol]);
20           }
21           rightCol--;
22           if (topRow <= btmRow) {
23               for (var col = rightCol; col >= 0; col--) {
24                   console.log(M[btmRow][col]);
25               }
26               btmRow--;
27           }
28           if (leftCol <= rightCol) {
29               for (var row = btmRow; row > topRow; row--) {
30                   console.log(M[row][leftCol]);
31               }
32               leftCol++;
33           }
34       }
35   }
36   spiralPrint(M);

时间复杂度: O( mn

空间复杂度: O(1)

这里, m 是行数, n 是列数。矩阵中的每个项目只被访问一次。

井字游戏

给定一个代表井字游戏棋盘的矩阵,确定是否有人赢了,是否是平局,或者游戏是否还没有结束。 3

这里有一些例子。

这里,X 赢了:

         OX-
         -XO
         OX

这里是作为一个矩阵:[['O', 'X', '-'], ['-' ,'X', 'O'], ['O', 'X', '-']]

在这里,O 赢了:

        O-X
        -O-
        -XO

这里是作为一个矩阵:[['O','-','X'], ['-','O','-'], ['-','X','O']]

为此,使用for循环检查所有三行,使用for循环检查所有列,并检查对角线。

 1   function checkRow ( rowArr, letter ) {
 2       for ( var i=0; i < 3; i++) {
 3           if (rowArr[i]!=letter) {
 4               return false;
 5           }
 6       }
 7       return true;
 8   }
 9
10   function checkColumn ( gameBoardMatrix, columnIndex, letter ) {
11       for ( var i=0; i < 3; i++) {
12           if (gameBoardMatrix[i][columnIndex]!=letter) {
13               return false;
14           }
15       }
16       return true;
17   }
18
19   function ticTacToeWinner ( gameBoardMatrix, letter) {
20
21       // Check rows
22       var rowWin = checkRow(gameBoardMatrix[0], letter)
23       || checkRow(gameBoardMatrix[1], letter)
24       || checkRow(gameBoardMatrix[2], letter);
25
26       var colWin = checkColumn(gameBoardMatrix, 0, letter)
27       || checkColumn(gameBoardMatrix, 1, letter)
28       || checkColumn(gameBoardMatrix, 2, letter);
29
30       var diagonalWinLeftToRight = (gameBoardMatrix[0][0]==letter && gameBoardMatrix[1][1]==letter && gameBoardMatrix[2][2]==letter);
31       var diagonalWinRightToLeft = (gameBoardMatrix[0][2]==letter && gameBoardMatr ix[1][1]==letter && gameBoardMatrix[2][0]==letter);
32
33       return rowWin || colWin || diagonalWinLeftToRight || diagonalWinRightToLeft;
34   }

35
36   var board = [['O','-','X'],['-','O','-'],['-','X','O']];
37   ticTacToeWinner(board, 'X'); // false
38   ticTacToeWinner(board, 'O'); // true

路由选择

在图 5-6 中,给定位置 x ,找到出口 e

img/465726_1_En_5_Fig6_HTML.jpg

图 5-6

寻找一条路

\n是 JavaScript 中用来换行的字符集,就像在许多标准编程语言中一样。将它与反斜线结合起来,可以在变量到字符串的赋值过程中创建换行符。

1   var board =
2   `%e%%%%%%%%%\n
3   %...%.%...%\n
4   %.%.%.%.%%%\n
5   %.%.......%\n
6   %.%%%%.%%.%\n
7   %.%.....%.%\n
8   %%%%%%%%%x%`;

var rows = board.split("\n")

然后在数组上使用.map将某些字符分成每一列。

function generateColumnArr (arr) {
    return arr.split("");
}
var mazeMatrix = rows.map(generateColumnArr);

这将生成适当的矩阵,其中每一行都是字符的数组,棋盘是这些行的数组。

现在,首先找到入口, e ,和出口, x 。该函数将返回要搜索的字符的行位置 i 和列位置 j :

1 function  findChar(char , mazeMatrix) {
2                 var  row =  mazeMatrix.length,
3                        column =  mazeMatrix[0 ].length;
4
5                 for  (var  i = 0 ; i <  row; i++ ) {
6                               for  (var  j = 0 ; j <  column; j++ ) {
7                                                    if  (mazeMatrix[i][j] == char ) {
8                                                    return  [i, j];
9                                            }
10                             }
11         }
12  }

当然,还需要一个函数将矩阵很好地打印为字符串,如下所示:

 1 function  printMatrix(matrix) {
 2                var  mazePrintStr = "" ,
 3                       row =  matrix.length,
 4                       column =  matrix[0 ].length;
 5
 6                for  (var  i = 0 ; i <  row; i++ ) {
 7
 8                               for  (var  j = 0 ; j <  column; j++ ) {
 9                                             mazePrintStr +=  mazeMatrix[i][j];
10                            }
11
12                            mazePrintStr += "\n" ;
13
14         }
15         console.log(mazePrintStr);
16  }

最后,定义一个名为path的函数。这递归地检查上,右,下,左。

         Up:      path(x+1,y)
         Right:   path(x,y+1)
         Down:    path(x-1,y)
         Left:    path(x,y-1)
function mazePathFinder(mazeMatrix) {
    var row = mazeMatrix.length,
        column = mazeMatrix[0].length,
        startPos = findChar('e', mazeMatrix),
        endPos = findChar('x', mazeMatrix);

    path(startPos[0], startPos[1]);

    function path(x, y) {
        if (x > row - 1 || y > column - 1 || x < 0 || y < 0) {
            return false;
        }
        // Found
        if (x == endPos[0] && y == endPos[1]) {
            return true;
        }
        if (mazeMatrix[x][y] == '%' || mazeMatrix[x][y] == '+') {
            return false;
        }
        // Mark the current spot
        mazeMatrix[x][y] = '+';
        printMatrix(mazeMatrix);

        if (path(x, y - 1) || path(x + 1, y) || path(x, y + 1) || path(x - 1, y)) {
            return true;
        }
        mazeMatrix[x][y] = '.';
        return false;
    }
}

图 5-7 显示控制台输出。

img/465726_1_En_5_Fig7_HTML.jpg

图 5-7

控制台输出

时间复杂度: O( mn

空间复杂度: O(1)

这里, m 是行长度, n 是列长度。每个元素只被访问一次。

矩阵旋转

将矩阵向左旋转 90 度。

例如,以下内容:

    101
    001
    111

旋转到此:

    111
    001
    101

图 5-8 所示为旋转。

img/465726_1_En_5_Fig8_HTML.jpg

图 5-8

矩阵逆时针旋转

如图 5-8 所示,向左旋转 90 度时,会出现以下情况:

  1. 矩阵的第三列成为结果的第一行。

  2. 矩阵的第二列成为结果的第二行。

  3. 矩阵的第一列成为结果的第三行。

以下旋转将转动原稿的第三列:

 1   var matrix = [[1,0,1],[0,0,1],[1,1,1]];
 2
 3
 4   function rotateMatrix90Left (mat){
 5       var N = mat.length;
 6
 7       // Consider all squares one by one
 8       for (var x = 0; x < N / 2; x++) {
 9           // Consider elements in group of 4 in
10           // current square
11           for (var y = x; y < N-x-1; y++) {
12               // store current cell in temp variable
13               var temp = mat[x][y];
14
15               // move values from right to top
16               mat[x][y] = mat[y][N-1-x];
17
18               // move values from bottom to right
19               mat[y][N-1-x] = mat[N-1-x][N-1-y];
20
21               // move values from left to bottom
22               mat[N-1-x][N-1-y] = mat[N-1-y][x];
23
24               // assign temp to left
25               mat[N-1-y][x] = temp;
26           }
27       }
28   }
29  rotateMatrix90Left(matrix);
30   console.log(matrix); // [[1,1,1],[0,0,1],[1,0,1]]

时间复杂度: O( mn

空间复杂度: O(1)

这里, m 是行长度, n 是列长度。每个元素只被访问一次。空间复杂度为 O(1 ),因为原始数组被修改而不是创建新数组。

摘要

本章涵盖了各种本机实现的数组函数,并在表 5-1 中进行了总结。

表 5-1

数组函数摘要

|

功能

|

使用

| | --- | --- | | push(element) | 将元素添加到数组的末尾 | | pop() | 移除数组的最后一个元素 | | shift() | 移除数组的第一个元素 | | slice(beginIndex, endIndex) | 返回从beginIndexendIndex的数组的一部分 | | splice(beginIndex, endIndex) | 返回从beginIndexendIndex的数组的一部分,并通过删除这些元素来修改原始数组 | | concat(arr) | 在数组末尾添加新元素(来自arr) |

除了标准的whilefor循环机制,数组元素的迭代可以使用表 5-2 中所示的替代循环机制。

表 5-2

迭代摘要

|

功能

|

使用

| | --- | --- | | for ( var 道具进场) | 按数组元素的索引进行迭代 | | for | 通过数组元素的值进行迭代 | | arr.forEach(fnc) | 在每个元素上应用fnc值 |

最后,回想一下 JavaScript 利用交错数组(数组的数组)来获得多维数组行为。使用二维数组,可以很容易地表示二维表面,如井字游戏棋盘和迷宫。

Footnotes 1

https://github.com/Apress/js-data-structures-and-algorithms

  2

https://github.com/Apress/js-data-structures-and-algorithms

  3

欲了解更多井字游戏规则,请访问 https://en.wikipedia.org/wiki/Tic-tac-toe