JavaScript 数据结构和算法教程(一)
一、大 O 符号
O①是神圣的。
-哈米德·提胡什
在学习如何实现算法之前,你应该了解如何分析算法的有效性。这一章将集中在时间和算法空间复杂性分析的 Big-O 符号的概念上。本章结束时,你将理解如何从时间(执行时间)和空间(消耗的内存)两方面分析一个算法的实现。
大 O 符号初级读本
Big-O 符号衡量算法的最坏情况复杂度。在 Big-O 符号中, n 表示输入的数量。问 Big-O 的问题如下:“当 n 接近无穷大时会发生什么?”
当你实现一个算法时,Big-O 符号很重要,因为它告诉你这个算法有多有效。图 1-1 显示了一些常见的 Big-O 符号。
图 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 }
答案
-
o(n2
有两个嵌套循环。忽略 n 前面的常数。
-
o(n3
有四个嵌套循环,但最后一个循环只运行到 10。
-
O(1)
持续的复杂性。该函数从 0 到 1000。这个不取决于 n 。
-
O( n )
线性复杂度。该功能从 0 到 10 n 运行。常量在 Big-O 中被忽略。
-
o(日志2n
对数复杂度。对于给定的 n ,这只运行 log 2 n 次,因为 I 是通过乘以 2 来递增的,而不是像其他例子中那样加 1。
-
O(∞)
无限循环。这个功能不会结束。
二、JavaScript:独特的部分
本章将简要讨论 JavaScript 的语法和行为的一些例外和案例。作为一种动态的解释型编程语言,它的语法不同于传统的面向对象编程语言。这些概念是 JavaScript 的基础,将帮助您更好地理解用 JavaScript 设计算法的过程。
JavaScript 范围
范围定义了对 JavaScript 变量的访问。在 JavaScript 中,变量可以属于全局范围,也可以属于局部范围。全局变量是属于全局范围的变量,可以从程序的任何地方访问。
全球声明:全球范围
在 JavaScript 中,可以不使用任何运算符来声明变量。这里有一个例子:
1 test = "sss";
2 console.log(test); // prints "sss"
然而,这会创建一个全局变量,这是 JavaScript 中最糟糕的做法之一。不惜一切代价避免这样做。总是使用var或let来声明变量。最后,当声明不会被修改的变量时,使用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( object1、object2 )函数的实用程序库如 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来验证预期的类型。最后,对于相等性检查,使用==检查值,使用===检查类型和值。但是,只能在数字、字符串和布尔值等非对象类型上使用它们。
2
三、JavaScript 数字
本章将重点介绍 JavaScript 数字运算、数字表示、Number对象、常用数字算法和随机数生成。在本章结束时,你将理解如何在 JavaScript 中处理数字,以及如何实现质因数分解,这是加密的基础。
编程语言的数字运算允许你计算数值。以下是 JavaScript 中的数字运算符:
+ : addition
- : subtraction
/ : division
* : multiplication
% : modulus
这些操作符在其他编程语言中普遍使用,并不特定于 JavaScript。
数系
JavaScript 对数字使用 32 位浮点表示,如图 3-1 所示。在本例中,该值为 0.15625。如果符号位为 1,符号位(第 31 位)表示数字为负。接下来的 8 位(第 30 位至第 23 位)表示指数值,即 e。最后,剩余的 23 位表示分数值。
图 3-1
32 位浮点数系统
对于 32 位,该值通过以下深奥的公式计算:
图 3-1 显示了 32 位的如下分解:
符号= 0
e = (0111100) 2 = 124(以 10 为基数)
这将导致以下结果:
值= 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)会导致小数点位数不确定。
图 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 ))
-
给定三个数 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 等于指数值。
-
打印所有小于 n 的质数。
To do this, use the
isPrimefunction covered in this chapter. Simply iterate from 0 to n and print any prime numbers whereisPrime()evaluates totrue.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 次。 -
检查一组质因数。
让我们把难看的数字定义为那些唯一的质因数是 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)的时间复杂度。因此,arrayNUglyNumbers有 n 倍的时间复杂度。
摘要
回想一下,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'
在这个例子中,比较了a和b。由于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():测试字符串中的匹配。这将返回true或false。
基本正则表达式
以下是基本的正则表达式规则:
^:表示字符串/行的开始
\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 :
图 4-2
缩短后的数据库条目
- 整数 ID 被缩短为一个字符串。使用 Base62 编码缩短时,11231230 将是 VhU2。
图 4-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 中的警告吗?
图 4-3
SSL 警告
这可能意味着您尝试访问的网站没有正确的安全套接字层(SSL)证书。
图 4-4
TSL 过程
TSL 是一种标准的安全技术,用于在服务器和客户端(浏览器)之间建立加密链接。以下是 TSL 过程的简化步骤。在这个过程中,服务器对不同的密钥使用非对称加密进行加密和解密。浏览器只使用对称加密,即使用一个密钥来加密和解密数据。
-
服务器将其非对称公钥发送给浏览器。
-
浏览器为当前会话创建一个对称密钥,该密钥由服务器的非对称公钥加密。
-
服务器通过其私钥解密浏览器的会话,并检索会话密钥。
-
两个系统都有会话密钥,并将使用该密钥安全地传输数据。
这是安全的,因为只有浏览器和服务器知道会话密钥。如果浏览器第二天连接到同一个服务器,将会创建一个新的会话密钥。
SSL 警告消息表示浏览器和服务器可能没有加密该连接上的数据。
最常用的公钥加密算法是 RSA 算法。
RSA 加密
RSA 是一种基于分解大整数难度的加密算法。在 RSA 中,生成两个大素数和一个补充值作为公钥。任何人都可以用公钥加密信息,但是只有那些有质因数的人才能解码信息。
这个过程有三个阶段:密钥生成、加密和解密。
-
密钥生成:生成公钥(共享)和私钥(保密)。生成的密钥的构造方法也应该是保密的。
-
加密:秘密消息可以通过公钥加密。
-
解密:只有私钥可以用来解密消息。
以下是该算法的概述:
-
选择两个(通常是大的)素数, p 和 q 。
-
p 和 q 的乘积记为 n 。
-
( p -1)和( q -1)的乘积表示为φ。
-
-
选择两个指数, e 和 d 。
-
e 通常为 3。可以使用大于 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 位数的最大可能值。
图 4-5
2 4096
摘要
本章涵盖了各种本机实现的字符串函数,并在表 4-1 中进行了总结。
表 4-1
字符串函数摘要
|功能
|
使用
|
| --- | --- |
| charAt(index) | 在index访问单个字符 |
| substring(startIndex, endIndex) | 访问从startIndex到endIndex的部分字符串 |
| 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 }
这将打印出all、cows、are和big。
对于(的)
在of之前指定的变量是数组的element(值),如下所示:
1 for (var element of array1) {
2 console.log(element);
3 }
这将打印出all、cows、are和big。
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 });
都打印all、cows、are和big。
助手功能
以下部分讨论了其他常用的处理帮助函数。此外,还将介绍如何使用数组。
。切片(开始,结束)
这个 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.max和Math.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 ( array、beginIndex、endIndex)。
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 中的三种函数数组方法:map、filter和reduce。这些方法不会改变原始数组内容。
地图
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 )。
图 5-1
多维数组
取而代之的是“锯齿状”阵列。一个交错数组是一个数组,它的元素是数组。交错数组的元素可以有不同的维度和大小(参见图 5-2 )。
图 5-2
锯齿状阵列
这里有一个帮助器函数来创建一个类似图 5-3 中的交错数组:
图 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 )。
图 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 所示。
图 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 。
图 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 显示控制台输出。
图 5-7
控制台输出
时间复杂度: O( mn
空间复杂度: O(1)
这里, m 是行长度, n 是列长度。每个元素只被访问一次。
矩阵旋转
将矩阵向左旋转 90 度。
例如,以下内容:
101
001
111
旋转到此:
111
001
101
图 5-8 所示为旋转。
图 5-8
矩阵逆时针旋转
如图 5-8 所示,向左旋转 90 度时,会出现以下情况:
-
矩阵的第三列成为结果的第一行。
-
矩阵的第二列成为结果的第二行。
-
矩阵的第一列成为结果的第三行。
以下旋转将转动原稿的第三列:
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) | 返回从beginIndex到endIndex的数组的一部分 |
| splice(beginIndex, endIndex) | 返回从beginIndex到endIndex的数组的一部分,并通过删除这些元素来修改原始数组 |
| concat(arr) | 在数组末尾添加新元素(来自arr) |
除了标准的while和for循环机制,数组元素的迭代可以使用表 5-2 中所示的替代循环机制。
表 5-2
迭代摘要
|功能
|
使用
|
| --- | --- |
| for ( var 道具进场) | 按数组元素的索引进行迭代 |
| for | 通过数组元素的值进行迭代 |
| arr.forEach(fnc) | 在每个元素上应用fnc值 |
最后,回想一下 JavaScript 利用交错数组(数组的数组)来获得多维数组行为。使用二维数组,可以很容易地表示二维表面,如井字游戏棋盘和迷宫。
Footnotes 1https://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 。