利用JavaScript学习基础的数据结构: 数组, 栈, 队列.
数组
一般来说, 数组存储一系列同一种数据类型的值.但因为JavaScript是动态类型(Dynamic Typing), 数组中的类型不需要一致, 可以包含不同类型的值.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| var arr = [ 1, false, { name: 'Tony', address: '111 Main St.' }, function(name){ var greeting = "Hello "; console.log(greeting + name); }, "hello" ];
arr[3](arr[2].name);
|
在JavaScript中,数组是一个可以修改的对象.如果添加元素,它就会动态增长.在C和Java等其他语言里,我们要决定数组的大小,想添加元素就要创建一个全新的数组,不能简单地往其中添加所需的元素.
添加和删除数组首尾元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| var numbers = [0,1,2,3,4,5,6,7,8,9];
numbers[numbers.length]=10;
numbers.push(12); numbers.push(12,23);
for(var i = numbers.length; i>=0; i--){ numbers[i] = numbers[i-1]; } numbers[0] = -1;
numbers.unshift(-2); numbers.unshift(-3, -4);
numbers.pop();
for(var i = 0; i < numbers.length; i++){ numbers[i] = numbers[i+1]; }; numbers.pop();
numbers.shift();
|
- 通过push和pop方法,就能用数组来模拟栈
- 通过shift和unshift方法,就能用数组模拟基本的队列数据结构
添加和删除数组任意元素
1 2 3 4 5 6 7
| var numbers = [0,1,2,3,4,5,6,7,8,9];
numbers.splice(5,3);
numbers.splice(5,0,2,3,4);
|
二维和多维数组
JavaScript只支持一维数组,并不支持矩阵(二维数组).但可以用数组套数组,实现矩阵或任一多维数组.
创建3x3矩阵:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| var matrix3x3x3 = [];
for(var i=0; i<3; i++){ matrix3x3x3[i] = []; for(var j=0; j<3; j++){ matrix3x3x3[i][j] = []; for(var z=0; z<3; z++){ matrix3x3x3[i][j][z] = i+j+z; } } }
function printMatrix(matrix){ for(var i=0; i<matrix.length; i++){ for(var j=0; j<matrix[i].length; j++){ for(var z=0; z<matrix[i][j].length;z++){ console.log(matrix[i][j][z]); }; } } };
printMatrix(matrix3x3x3);
|
栈
两种数据结构类似于数组,但在添加和删除元素时更为可控.它们就是栈和队列.
栈是一种遵从后进先出(LIFO)原则的有序集合.新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端就叫栈底.在栈里,新元素都靠近栈顶,旧元素都接近栈底.
栈也被用在编程语言的编译器和内存中保存变量、方法调用等.
栈的创建和使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| function Stack(){ var items = [];
this.push = function(element){ items.push(element); };
this.pop = function(element){ return items.pop(); };
this.peek = function(){ return items[items.length-1]; };
this.isEmpty = function(){ return items.length === 0; };
this.size = function(){ return items.length; };
this.clear = function(){ items = []; };
this.print = function(){ console.log(items.toString()); }; };
var stack = new Stack();
console.log(stack.isEmpty());
stack.push(5); stack.push(8); stack.push(12); stack.push(2);
console.log(stack.peek());
console.log(stack.size());
console.log(stack.isEmpty());
stack.pop(); console.log(stack.peek());
stack.print();
|
利用栈解决十进制和二进制的转换问题:
要把十进制转化成二进制,我们可以将该十进制数字和2整除(二进制是满二进一),直到结果是0为止.举个例子,把十进制的数字10转化成二进制的数字,过程大概是这样:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function divideBy2(decNum){ var remStack = new Stack(), rem, binaryString = '';
while(decNum > 0){ rem = Math.floor(decNum % 2); remStack.push(rem); decNum = Math.floor(decNum / 2); }
while(!remStack.isEmpty()){ binaryString += remStack.pop().toString(); }
return binaryString; };
|
在这段代码中,当函数参数满足和2做整除的条件时(行{1}),我们会获得当前参数与2相除的余数,放到栈中(行{2}、{3}).然后让参数和2相除(行{4}).另外注意:JavaScript有数字类型,但是它不会区分究竟是整数还是浮点数.因此,要使用Math.floor函数让除法的操作仅返回整数部分.最后,用pop方法把栈中的元素都移除,把出栈的元素变成连接成字符串(行{5}).
修改以上算法, 把 十进制转换成任何进制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function baseConverter(decNum, base){ var remStack = new Stack(), rem, baseString = ''; var digits = '0123456789ABCDEF';
while(decNum > 0){ rem = Math.floor(decNum % base); remStack.push(rem); decNum = Math.floor(decNum / base); }
while(!remStack.isEmpty()){ baseString += digits[remStack.pop()]; }
return baseString;
};
|
在将十进制转成二进制时,余数是0或1;在将十进制转成八进制时,余数是0到8之间的数;但是将十进制转成16进制时,余数是0到8之间的数字加上A、B、C、D、E和F(对应10、11、12、13、14和15).因此,需要对栈中的数字做转化.(行{6}和行{7}).
队列
队列和栈有很多相似之处,但有个重要区别,队列是遵循FIFO(First In First Out,先进先出,也称为先来先服务)原则的一组有序的项.队列在尾部添加新元素,并从顶部移除元素.最新添加的元素必须排在队列的末尾.
在现实中,最常见的队列的例子就是排队.
在计算机科学中,一个常见的例子就是打印队列.比如说我们需要打印五份文档.我们会打开每个文档,然后点击打印按钮.每个文档都会被发送至打印队列.第一个发送到打印队列的文档会首先被打印,以此类推,直到打印完所有文档.
队列的创建和使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| function Queue(){ var items = [];
this.enqueue = function(element){ items.push(element); };
this.dequeue = function(){ return items.shift(); };
this.front = function(){ return items[0]; };
this.isEmpty = function(){ return items.length === 0; };
this.clear = function(){ items = []; };
this.size = function(){ return items.length; };
this.print = function(){ console.log(items.toString()); }; };
var queue = new Queue();
console.log(queue.isEmpty());
queue.enqueue('John'); queue.enqueue('Jack'); queue.enqueue('Amy');
queue.print();
console.log(queue.size());
queue.dequeue();
console.log(queue.front());
|
Queue类和Stack类非常类似.唯一的区别是dequeue方法和front方法,这是由于先进先出和后进先出原则的不同所造成的.
优先队列
默认队列有一些修改版本.其中一个修改版就是优先队列.元素的添加和移除是基于优先级的.
一个现实的例子就是机场登机的顺序.头等舱和商务舱乘客的优先级要高于经济舱乘客.另一个现实中的例子是医院的(急诊科)候诊室.医生会优先处理病情比较严重的患者.
实现一个优先队列,有两种选项:设置优先级,然后在正确的位置添加元素;或者用入列操作添加元素,然后按照优先级移除它们.在这个示例中,将会在正确的位置添加元素,因此可以对它们使用默认的出列操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| function PriorityQueue(){ var items = [];
function QueueElement(element, priority){ this.element = element; this.priority = priority; };
this.enqueue = function(element, priority){ var queueElement = new QueueElement(element, priority);
if(this.isEmpty()){ items.push(queueElement); } else { var added = false; for(var i=0; i<items.length; i++){ if(queueElement.priority < items[i].priority){ items.splice(i, 0, queueElement); added = true; break; } } if(!added){ items.push(queueElement); } } };
}
|
默认的Queue类和PriorityQueue类实现上的区别是,要向PriorityQueue添加元素,需要创建一个特殊的元素(行{1}).这个元素包含了要添加到队列的元素(可以是任意类型)及其在队列中的优先级.
1 2 3 4 5
| var priorityQueue = new PriorityQueue(); priorityQueue.enqueue("John", 2); priorityQueue.enqueue("Jack", 1); priorityQueue.enqueue("Camila", 1); priorityQueue.print();
|
以上代码是一个使用PriorityQueue类的示例.在下图中可以看到每条命令的结果(以上代码的结果):

第一个被添加的元素是优先级为2的John.因为此前队列为空,所以它是队列中唯一的元素.接下来,添加了优先级为1的Jack.由于Jack的优先级高于John,它就成了队列中的第一个元素.然后,添加了优先级也为1的Camila.Camila的优先级和Jack相同,所以它会被插入到Jack之后(因为Jack先被插入队列);Camila的优先级高于John,所以它会被插入到John之前.
我们在这里实现的优先队列称为最小优先队列,因为优先级的值较小的元素被放置在队列最前面(1代表更高的优先级).最大优先队列则与之相反,把优先级的值较大的元素放置在队列最前面.
循环队列
还有另一个修改版的队列实现,就是循环队列.循环队列的一个例子就是击鼓传花游戏(Hot Potato).在这个游戏中,孩子们围成一个圆圈,把花尽快地传递给旁边的人.某一时刻传花停止,这个时候花在谁手里,谁就退出圆圈结束游戏.重复这个过程,直到只剩一个孩子(胜者).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| function hotPotato(nameList, num){ var queue = new Queue();
for(var i=0; i<nameList.length; i++){ queue.enqueue(nameList[i]); }
var eliminated = ''; while(queue.size() > 1){ for(var i=0; i<num; i++){ queue.enqueue(queue.dequeue()); } eliminated = queue.dequeue(); console.log(eliminated + '在击鼓传花游戏中被淘汰. '); } return queue.dequeue(); }
var names = ['John', 'Jack', 'Camila', 'Ingrid', 'Carl']; var winner = hotPotato(names, 7); console.log('胜利者: ' + winner);
|
实现一个模拟的击鼓传花游戏,要用到这一章开头实现的Queue类(行{1}).我们会得到一份名单,把里面的名字全都加入队列(行{2}).给定一个数字,然后迭代队列.从队列开头移除一项,再将其添加到队列末尾(行{3}),模拟击鼓传花(如果你把花传给了旁边的人,你被淘汰的威胁立刻就解除了).一旦传递次数达到给定的数字,拿着花的那个人就被淘汰了(从队列中移除——行{4}).最后只剩下一个人的时候,这个人就是胜者(行{5}).
参考