前言:作为数据结构与算法专栏的第二篇博客,本篇博客的主要内容是关于JavaScript中常用的数组方法,受限的线性数据结构栈与队列,创建栈和队列可以通过数组和链表两种方式,由于链表的相关内容暂时没有学习,暂时以数组的方式实现栈和队列的封装。因此,在本篇的第一部分,将对JavaScript中主要的数组方法进行梳理,便于理解后续封装栈和队列的代码。
一、JavaScript中常见的数组方法
数组作为基本的数据结构,熟练掌握其主要的元素增删改查方法对于我们高效率的开发十分重要。下面根据分类梳理一下JS中常见的数组方法:
1.1 数组首部和尾部插入删除元素
//1 push方法,在数组的末尾添加元素,其返回值是数组修改后的长度
let arr1=[12,13,14];
let count=arr1.push(15);
console.log(count); //5
//2 pop方法,移出数组的最后一个元素,返回的是移出的那个值
let arr2=[1,2,3];
let n1=arr2.pop();
console.log(n1); //3
//3 shift方法,删除数组的第一个元素,并返回的是删除元素的值,如果数组为空则返回的是Undefined。
let arr3=[4,5,6];
let n2=arr3.shift();
console.log(n2); //4
//4 unshift方法,将元素插入到数组的首部,返回的值是修改后数组的长度
let arr4=[7,8,9.10];
let count1=arr4.unshift(6);
console.log(conut1); //5
//以上4个基本的操作数组首部和尾部方法,删除的方法返回的是被删除的元素,插入的方法返回的是修改后数组的长度。
1.2 功能最强大的方法splice(改,增,删)
//splice可谓是操作数组功能最为强大的方法
//splice(n,m,x) n表示从索引n开始,为必选参数;m为向后截取的元素个数;x为代替截取部分
//将截取到的部分以新的数组方式返回
//example
//向数组指定索引值位置添加元素
let arr=[12,13,14,15];
let newArr=arr.splice(1,0,19); //从索引值为1开始截取0个元素,并将19插入在索引值为1的位置
console.log(newArr,arr); //[] [12,19,13,14,15];
//用元素代替删除后的指定元素,用于将指定位置的元素进行替换掉
let arr1=[12,13,14,15];
let newArr1=arr1.splice(1,1,19); //从索引值为1的地方截取一个元素13,并作为返回值,在13的位置插入元素19,原数组发生改变
console.log(newArr1,arr1); //[13] [12,19,14,15]
//截取数组从指定索引部分开始的分段
let arr1=[12,13,14,15];
let newArr1=arr1.splice(1,3);
console.log(newArr1,arr1); //[13,14,15] [12]
1.3 查找数组中的分段slice方法
// slice(n,m)方法并不会改变原数组
// 从索引n值开始,查询到索引m结束,并不包含索引值为m的元素,如果其值大于对应的arr.length则默认查询到数组的末尾元素。
//将查到的元素以新数组的方式返回,并不会对原数组产生影响。
//如果传入的是小数,则会自动调用Math.floor()方法将其向下取整。
//非数值元素传入则会按照规则转换为数值类型
//example
let arr1=[12,13,14,15];
let newArr=arr1.slice(1,3); //将查询到的索引值为1,2的元素存入新数组作为返回值,原数组不发生变化。
console.log(newArr,arr1); //[13,14] [12,13,14,15]
1.4 将数组转换为字符串的方法toString和join
//1 toString不会改变原数组,不需要传入参数
let arr1=[12,13,14];
console.log(arr1.toString(),arr1); //12,13,14 [12,13,14]
//2 join也不会改变原数组,但是需要传递一个指定的分割符作为参数,字符串类型
let arr2=[100,111,122];
let newArr=arr2.join('-');
console.log(newArr,arr2); //100-111-122 [100,111,122]
1.5 检测数组中是否包含某一项indexOf、lastIndexOf、includes
// 1 indexOf(n,m)不会改变原数组
// n值为需要检索的元素值,m为开始检索的索引值位置,不填则默认为0
// 返回值是要检索的这一项第一次出现时的索引值,如果未查找到则返回-1
let arr1=[100,200,300,400];
console.log(arr1.indexOf(200)); //1
// 2 lastIndexOf()也不会改变原数组
// 查找需要检索的元素在数组中最后一次出现的索引值
let arr2=[100,200,300,400,200];
console.log(arr2.lastIndexOf(200)); //4
//3 includes()不会改变原数组,验证数组中是否包含某元素
//如果数组中包括查找的元素则返回true,否则返回值为false。
let arr3=[100,200,300,400]
console.log(arr3.includes(200)); //true
console.log(arr3.includes(500)); //false
1.6 ES6中常用的数组方法map、reduce、filter、forEach
ES6语法规范中为我们提供了4个非常好用的数组方法,在数组中传进函数对数组元素进行增删改查。
//1 map方法
arr.map(function(item,index,arry){
});
item 当前元素
index 当前元素下标
arry 当前数组
let array = [
{name: 'anli', age: 16, id: 1},
{name: 'budy', age: 12, id: 2},
{name: 'ruise', age: 68, id: 3},
{name: 'hwdu', age: 28, id: 4},
{name:' why', age: 68, id: 5}
]
let newArry = arry.map(item => item.name) ; //处理后返回仅由name属性值组成的数组。
console.log(newArry); //["anli", "budy", "ruise", "hwdu", "why"]
//example 检查天气是否炎热
let weather=[25,36,19,17,29,32];
let result=weather.map((item)=>item>32?'hot':'cool');
console.log(result); //['cool','hot','cool','cool','cool','hot']
map方法
//2 reduce方法
reduce(function(prev,item,index,arry){
},init);
//prev 第一次调用时的初始值,之后为上一次调用之后的返回值
//item 表示当前正在处理的元素
//index 表示当前正在处理的数组元素的索引值
//init 初始值
//reduce方法常用于以下的功能
//1、求给定的数组项元素之和
let arr1=[10,20,30,40];
let resultNumber=arr1.reduce((prev,item)=>{
return prev+item;
},0);
console.log(resultNumber); // 100
//2、数组中的元素最大值
let arr2=[10,20,30,40];
let resultNumber2=arr2.reduce((prev,item)=>{
return Math.max(prev,item);
},0);
console.log(resultNumber2); // 40
//3、数组的快速去重,这一点十分重要
let arr3=[10,20,30,40,10,20,40];
let newArray=arr2.reduce((prev,item)=>{
prev.indexOf(item)==-1&&prev.push(item);
return prev;
},[]);
console.log(newArray); // [10,20,30,40];
reduce方法
//3 filter方法
// 可以对数组按照设定的条件进行过滤,并将满足条件的元素构成新的数组返回。
arr.filter(function(item,index,arr){
})
//item 数组项的本身
//index 数组项的下标
//arr 对象数组本身
let arr1=[199,200,201,202,203,204];
let newArr=arr1.filter((item)=>item>202);
console.log(arr1,newArr); //[199,200,201,202,203,204] [203,204]
filter方法
//4 forEach方法
// 用于遍历数组,将遍历到的数组元素传递给回调函数,遍历的数组不能是空
//forEach()方法对数组的每一个元素执行一次提供的函数。
arr.forEach(function(value,index,array){
});
let arr1=[12,13,14,15];
arr1.forEach(function(element){
console.log(element);
})
//输出结果为12,13,14,15,16
//for循环和forEach循环的区别
1)for循环可以使用break跳出循环,但forEach不能。
2)for循环可以控制循环起点(i初始化的数字决定循环的起点),forEach只能默认从索引0开始。
3)for循环过程中支持修改索引(修改 i),但forEach做不到(底层控制index自增,我们无法左右它)
forEach方法
上述就是常见的数组操作方法,在完成基本的语法和用法复习之后,我们就可以开始基于数组封装栈和队列了。
二、数据结构---从栈启航
2.1什么是栈?
栈是可以理解成操作受限的数组,其遵循LIFO原则,即后进先出,栈的基本操作主要有:栈的初始化、判空、容量大小、取栈顶元素、在栈顶进行插入和删除。在栈顶插入元素称为入栈,在栈顶删除元素称为出栈。操作栈内元素见下图:
在了解了栈的基本操作原理,来尝试一下做个题检查自己是否真的理解出栈入栈的原则了。
exercise1
1,2,3,4,5,6六个元素顺序入栈,并不是一次性入栈,下列出栈方式不可能的一项是()
A、5 4 3 6 1 2
B、4 5 3 2 1 6
C、3 4 6 5 2 1
D、2 3 4 1 5 6
先思考一下吧,答案会在本篇博客的末尾给出。
2.2 基于数组和对象封装一个栈数据结构
class Stack {
constructor() {
this.item = [];
}
//创建操作数组的各类方法
push(element) {
this.item.push(element); //栈顶添加元素,返回的是添加元素后的数组。
}
pop() {
return this.item.pop(); //删除栈顶元素,需要注意的是pop方法返回的是删除后的栈
}
peek() {
return this.item[this.item.length - 1]; //返回栈顶的元素,索引值从0开始的,因此栈的长度减一就是栈顶元素的索引值
}
size() {
return this.item.length; //返回栈的长度值
}
isEmpty() {
return this.item.length === 0; //判断栈是否为空
}
//清空栈
clear() {
this.item = [];
}
//转化为字符串
toString(){
let resultString='';
for(let i=0;i<this.item.length;i++){
resultString+=this.item[i]+'';
}
return resultString;
}
}
//使用栈
let stack = new Stack();
console.log(stack.isEmpty()); //输出值为true
stack.push(1999);
stack.push(2000);
console.log(stack.item); //输出[1999,2000]
stack.pop();
console.log(stack.item); //输出[1999]
stack.push(2003);
stack.push(2008);
console.log(stack.size()); //输出 3
console.log(stack.item); //输出 [1999,2003,2008]
console.log(stack.isEmpty()) //输出 false
基于数组实现栈结构的封装
class StackObject {
constructor() {
//声明一个变狼count,记录对象中元素的个数。
this.count = 0;
this.item = {};
}
push(element) {
this.item[this.count] = element;
this.count++; //向栈中添加对象
}
size() {
return this.count;
}
isEmpty() {
return this.count === 0;
}
pop() {
if (this.isEmpty()) {
return undefined;
} else {
this.count--;
let result = this.item[this.count];
delete this.item[this.count];
return result; //删除栈顶元素并进行弹出
}
}
peek() {
if (this.count == 0) {
return undefined;
} else {
return this.item[this.count - 1];
}
}
clear() {
this.count = 0;
this.item = []; //清空此栈的内容。
}
//创建一个toString对象方法
toString() {
if (this.isEmpty()) {
return '';
} else {
let objString = `${this.item[0]}`;
for (let i = 0; i < this.count; i++) {
objString = `${objString},${this.item[i]}`;
}
return objString;
}
}
}
//函数调用栈,当不同的函数之间相互调用,外层函数的相关信息依次入栈,直到最内层的函数到栈顶开始执行后依次出栈,直到最外层函数调用后出栈。函数的递归的深度过高时,会产生栈溢出的现象。
基于对象实现栈结构的封装
2.3 栈结构的简单应用案例
不同进制数据之间相互转换是非常常见的一个现象,计算机只能识别二进制数据,我们日常所用的十进制数据也只有转换为二进制之后才能被计算机进行处理,那么如何利用栈结构实现将任意的十进制数据转化成二进制输出呢?手动计算取余的方式相信大家都直到,如何封装一个函数实现十进制到二进制之间的相互转换呢?其具体的实现代码如下:
//需要在上面已经封装栈结构的基础上进行函数的封装。
function decimalToBinary(decNumber){
//1 定义栈的对象
let stack=new Stack();
// 2 循环操作,不确定循环次数,使用while循环体
while(decNumber>0){
//循环体里面将每次取余的余数压入栈内
stack.push(decNumber%2);
//获取每次取余后的数再次传入循环体中,除以2后向下取整重新赋值。
decNumber=Math.floor(decNumber/2);
}
//第二个循环体,当栈内不为空时,将栈内的元素循环出栈
let binaryString='';
while(!stack.isEmpty()){
binaryString+=stack.pop()+'';
}
return binaryString; //将转换完成后的二进制以字符串的形式输出。
}
let binaryNumber=decimalToBinary(100);
console.log(binaryNumber); //输出 1100100
掌握了十进制到二进制之间的函数封装,那么你能自己写出由十六进制转换为二进制的转换函数吗?其实原理都一样,自己动手尝试一下吧。
三、数据结构---从队列入门
3.1 什么是队列?
队列也是一种操作受限的线性表,遵循FIFO原则,即先进先出原则。队列其实在生活中是很常见的,比如排队做核酸,在正常情况下,自然是排在队前面的人先做,也就是先到先得。在计算机中一般为了提高资源利用效率,会开启多个队列,但是一般不会让多个队列同时运行,依照进入队列的顺序依次处理线程。队列同样可以基于链表和数组实现,暂时先实现基于数组的栈结构封装。队列的的数据操作示意图如下所示:
3.2 队列的封装以及简单的应用案例--击鼓传花
function Queue(){
//属性
this.items=[]
//方法
//1 将元素进入队列
this.enqueue=function(element){
this.items.push(element);
}
//2 从队列中删除元素
this.dequeue=function(){
return this.items.shift();
}
//3 查看队列前端的元素,也就是队列首部的元素
this.front=function(){
return this.items[0];
}
//4 查看队列是否为空
this.isEmpty=function(){
return this.items.length===0;
}
//5 查看队列当前的数组的大小
this.size=function(){
return this.items.length;
}
//6 字符串输出
this.toString=function(){
if(this.isEmpty()){
return '';
}else{
let formString='';
for(let i=0;i<this.items.length;i++){
formString+=this.items[i]+'';
}
return formString;
}
}
}
//创建实例调用队列,进行测试
let queue=new Queue();
queue.enqueue(1999);
queue.enqueue(2000);
queue.enqueue(2001);
console.log(queue.items);
console.log(queue.front());
console.log(queue.dequeue());
console.log(queue.items);
console.log(queue.isEmpty());
console.log(queue.size());
console.log(queue.toString());
基于数组实现普通队列的封装
上面的测试代码经过输出得到以下内容:
可知所封装的队列中的方法功能全部实现,通过构造函数创建对象实例也是一种快速创建对象的方法,在以大写字母开头的构造函数中进行属性和方法的封装。
击鼓传花是一个比较经典的面试题。在实际游戏中,击鼓传花的规则是,参与游戏的人员围成一个圈,击鼓声持续时,花在每个人手上依次循环传递,鼓声停止时花所在的那个人被淘汰出游戏,如此循环往复,直到剩余一人成为最终赢家。
将上述的游戏过程和规则抽象出来:
1、游戏开始时,指定一个数字,每个人从一开始数数,数到指定数字的人被淘汰出游戏,下一个人又从1开始数数。直到剩余最后一人成为最终赢家。
2、利用队列数据结构来看,就是队列前面未数到指定数的人离开队列头部,重新进入队列的尾部,如此循环往复,可以利用队列结构很好的解决这个问题。
具体的实现代码如下:
//此函数需要传递进入两个参数,一个是参与游戏的人员名单,另一个是游戏开始时所指定的数字
function playGame(nameList,number){
//1 基于上面所封装好的队列构造函数创建一个队列实例
let queue=new Queue();
//2 将所有参与游戏的人员依次进入队列
for(let i=0;i<nameList.length;i++){
queue.enqueue(nameList[i]);
}
console.log('已经全部进入队列,队列大小为',queue1.size()); //测试代码,检查是否所有的人都以进入队列中。
//3 所有人进入队列后,游戏开始
// 未到指定数字的人应该离开队列头部重新进入队列尾部,直到队列中的人数小于等于1的时候停止
while(nameList.length>1){
for(let i=0;i<number;i++){
queue.enqueue(queue.dequeue());
}
queue.dequeue(); //将数到指定数字的人删除出队列。
}
// 4 队列中仅剩一个人的时候,游戏结束,通过获取队列的头部元素来获取此值
let winner=queue.front();
console.log('GameOver队列当前的大小为',queue1.size());
console.log(winner);
console.log('最终赢家为',winner);
}
//测试代码,检查是否达到了理想的游戏效果
let nameList=['liu','li','sheng','hu','jiang','wu'];
//为了提高游戏的乐区,每次传入的数值为随机生成的。在传入前可以打印查看。
num=parseInt(Math.random()*10);
console.log('当前传入的随机数为',num);
playGame(nameList,num);
执行上述函数后查看打印输出可以得到如下的结果,可知满足设定的游戏规则,最终的游戏赢家在传入的数组中的索引值也完全匹配。
3.3 优先级队列
在生活中的大部分场合,排队都是按照先来后到的原则,也就是遵循队列中的FIFO原则。但是在某些场合,按照优先级排队的现象是存在的,比如医院的急诊科应该根据伤者的身体状况安排手术的时间顺序,在登机时头等舱和经济仓的登机顺序肯定是不一样的。抽象到数据结构中,优先级队列就是用于处理这种特殊的排队规则的,按照元素的优先级决定元素在队列中的插入顺序。优先级队列中的方法除了需要按照优先级插入元素以外,其余的删除,查看,判空等方法和普通队列一样。
function QueuePriority(){
//声明属性
this.items=[];
//构造内部构造函数,将外部传入的元素和对应优先级快速创建元素对象
function elementPriority(element,priority){
this.element=element;
this.priority=priority;
}
//根据元素的优先级将元素插入到指定位置
this.enqueue=function(element,priority){
//1、当队列内部为空时,直接将元素插入到对列中即可
let queueElement=new elementPriority(element,priority) //通过内部构造函数创建一个对象
if(this.isEmpty()){
//如果队列为空,则直接将元素插入到第一个位置。
this.items.push(queueElement);
}else{
let added=false; //判断循环到最后一个元素是否已经加入,若未加入则在队列尾部插入。
for(let i=0;i<this.items.length;i++){
if(queueElement.priority<this.items[i].priority){
//使用splice方法在指定的位置插入元素
this.items.splice(i,0,queueElement);
added=true;
break;
}
}
//在队列中未找到满足优先级条件的位置,则直接将元素插在队列的末尾
if(!added){
this.items.push(queueElement);
}
}
}
//2 从队列中删除元素
this.dequeue=function(){
return this.items.shift();
}
//3 查看队列前端的元素,也就是队列首部的元素
this.front=function(){
return this.items[0];
}
//4 查看队列是否为空
this.isEmpty=function(){
return this.items.length===0;
}
//5 查看队列当前的数组的大小
this.size=function(){
return this.items.length;
}
//6 字符串输出
this.toString=function(){
if(this.isEmpty()){
return '';
}else{
let formString='';
for(let i=0;i<this.items.length;i++){
formString+=this.items[i].element+'-'+this.items[i].priority;
}
return formString;
}
}
}
//测试代码,检查插入优先级队列中的元素是否满足优先级的顺序排列规则
let testQueue=new QueuePriority();
testQueue.enqueue('lsx',233);
testQueue.enqueue('zdy',263);
testQueue.enqueue('lls',245);
testQueue.enqueue('hyw',198);
let queueResult=testQueue.toString();
console.log(queueResult);
console.log(testQueue.items);
测试代码的输出如下,可知满足优先级队列中按照优先级插入元素的规则:
四、总结归纳
本篇博客中对常见的数组方法进行了简单的介绍,基于数组实现了栈和队列的封装。在介绍栈的元素进出规则之后,留下了一个选择题,这道题的答案是C选项,需要注意的是元素入栈的同时可以出栈,明白这个的话就很容易理解为什么选择C选项了 在此不再过多的解释说明了。
栈和队列是两种非常基础的数据结构,比较容易理解,用途也非常广阔,熟练掌握其数据结构特点对于我们理解后续更为复杂的数据结构会有很大帮助。学习数据结构与算法的时候单纯的通过阅读文档是不太好的,还需要辅助以相应的练习题来巩固,下一篇博客中我们将学习总结链表的基础知识点和简单应用。
总有疾风起,人生不言弃!