JS数据结构与算法学习笔记大全 (温故而知新,可以为师矣。)
Posted 北极光之夜。
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JS数据结构与算法学习笔记大全 (温故而知新,可以为师矣。)相关的知识,希望对你有一定的参考价值。
目录:
一.数据结构简介(序):
1.1 概念
程序设计 = 数据结构 + 算法。
1.2 概念
数据 = 符号
(1). 其可以输入到计算机中。
(2). 能够被计算机识别和处理。
1.3 分类
数据分为:
(1).数据元素:数据的基本单位,也称为结点或者记录。
(2).数据对象: 相同特性的数据元素的集合,是数据的一个子集。
(3).数据项: 独立含义的数据的最小单位。
数据的目的是存储,存储的目的是后期的再利用。
1.4 作用
数据结构的主要作用是:阐述关系。
如何存储具有复杂关系的数据更有助于后期对数据的再利用。
1.5 结构
结构:
简单的理解就是关系,不同的数据元素之间不是独立的。而是存在特定的关系。
(1). 逻辑结构:
数据对象中数据元素之间的互相关系。
四种:
集合结构、线性结构、树形结构、图形结构.
集合结构: 数据元素同属于个集合, 他们之间没有其他的关系。他们的共同属性是:“同属于一个集合”。
线性结构: 最典型的数据关系是一对一。线性结构是种有序数据的集合。除了第一个和最后一个数据元素之外,其他数据元素都是首尾相接的。
1.必存在一个第一个元素。
2.必存在最后的一个元素。
3.除最后一个元素外,其他的数据元素均有一个唯一的后续”。
4.除第一个元素之外,其他数据元素均有一个唯一的前驱。
比如:数组, 栈,队列,等都是一个线性结构。
树形结构:数据元素一对多的关系。如 Dom Tree。
图形结构:多对多的关系。如 地图。
(2).物理结构: 数据元素存储到计算中的存储器(内存)。数据的存储结构应该正确的反应数据元素之间的逻辑关系。包括顺序存储和链式存储。
顺序存储: 该结构是把逻辑上相邻的结点存储在物理位置上相邻的存储单元中,结点之间的逻辑关系由存储单元的邻接关系来体现。
链式存储: 在计算机中用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。它不要求逻辑上相邻的元素在物理位置上也相邻.因此它没有顺序存储结构所具有的弱点,但也同时失去了顺序表可随机存取的优点。
1.6 常见的数据结构
二.算法简介(序):
2.1 概念
算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。简单来说,算法是解决问题的方法和步骤。数据结构的实现离不开算法。
2.2 JS数组
声明: JS数组不是真正意义上的数组。
传统数组是在内存中用一串连续的区域存放具有相同类型的元素。
但是,如一个js数组如下:
const arr = [123,'a',"啦啦"];
可以看出 JS 数组元素是可以任意类型的,内存地址是不连续的。所以它不是真正意义上的数组。但是它好用呀。
数组的优点:
(1)按照索引查询元素速度快;
(2)能存储大量数据;
(3)按照索引遍历数组方便;
(4)定义简单,而且访问灵活;
数组的缺点:
(1)根据内容查找元素速度很慢;
(2)数组的大小一经确定不能改变,不适合动态存储;
(3)数组只能存储一种类型的数据;
(4)增加、删除元素效率慢;
(5)未封装任何方法,所有操作都需要用户自己定义;
三.栈与队列:
3.1简介
(1)内存中的堆栈和数据结构中的堆栈不是一个概念, 内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象数据存储结构。
(2)栈又名堆栈,它是一种运算受限的线性表。它遵循后进先出(LIFO)原则。
(3)受限制意思就是在新增数据、删除数据、查找等操作时,不能随心所欲,必须遵循一定的限制(规则)。
(4)向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素。
(5)从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为
新的栈顶元素
3.1.1 js实现一个栈:
//定义一个类,相当函数
class Stack{
// 构造方法
constructor(){
this.items = [];
}
//添加方法,入栈顶
push(ele){
this.items.push(ele);
}
//出栈
pop(){
//pop() 方法用于删除并返回数组的最后一个元素。
return this.items.pop();
}
//返回栈顶元素
peek(){
return this.items[this.items.length-1];
}
//判断是否为空
isEmpty(){
return this.items.length===0;
}
//返回元素个数
size(){
return this.items.length()
}
//清空
clear(){
this.items = [];
}
}
3.2 清空数组的几种方法(扩展):
let arr = [1,2,3];
arr.length = 0;
arr = [];
arr.splice(0,arr.length);
3.3 js调用栈
执行上下文: 就是当前javascript代码被解析和执行是所在环境的抽象概念,JavaScript中运行任何的代码都是在执行上下文中运行。(执行环境)。分为以下三种:
(1) 全局执行上下文: 默认的,最基础的执行上下文。共两个过程:1.创建全局对象,在浏览器中这个全局对象就是window对象。2.将this指针指向这个全局对象。
(2)函数执行上下文: 每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。
(3)Eval函数执行上下文: eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。运行在eval函数中的代码也获得了自己的执行上下文。
执行栈: 执行栈,也叫调用栈,遵循LIFO原则。用于存储在代码执行期间创建的所有执行上下文。如下代码:
const one = ()=>{
two();
console.log('I am one');
}
const two = () =>{
console.log('I am two');
}
one();
结果:
·当上述代码在浏览器中加载时,JavaScript 引擎会创建一个全局执行上下文并且将它推入到当前的执行栈。
· 当调用 one函数时,JavaScript 引擎会为该函数创建了一个新的执行上下文并将其push推到当前执行栈的栈顶。
· 当在 one() 函数中调用 two() 函数时,Javascript 引擎为该函数创建了一个新的执行上下文并将其推到当前执行栈的栈顶。
· 当two() 函数执行完成后,它的执行上下文从当前执行栈中弹出。上下文控制权将移到当前执行栈的下一个执行上下文,即 one() 函数。
3.4 递归
从上可以知道,函数的调用的本质为压栈与出栈操作。
函数在调用栈里边有一个特例,叫做递归。
递归,就是在运行的过程中调用自己。会产生一个叫递归栈。
先进栈,到条件后就出栈。如果不出栈,由于递归调用次数过多,堆栈溢出。因为堆栈的大小是系统控制的,无法改变。
递归例子,算阶乘:
const factorial = (n) =>{
//判断出口
if(n===1){
return 1;
}
//递归
return n*factorial(n-1);
}
//输出3阶乘
console.log(factorial(3));
结果:
递归例子,斐波那契数列:
斐波那契数列指的是这样一个数列:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,…
第3项开始,每一项都等于前两项之和。
现在求输入第几项后输出数列对应的数字:
const Fibonaqi = n=>{
if(n<2){
return n;
}
return Fibonaqi(n-1)+Fibonaqi(n-2);
}
console.log(Fibonaqi(5));
这代码有个问题,会进行非常多重复的计算,需要同时保存多个调用帧,可能导致消耗内存,爆栈。假如 n 为 5 执行过程如下:
Fib(5)=Fib(4)+Fib(3) =5
Fib(4)=Fib(3)+Fib(2) =3
Fib(3)=Fib(2)+Fib(1) =2
Fib(2)=Fib(1)+Fib(0) =1
Fib(1)= 1
Fib(0)= 0
可采用尾递归方式解决。首先理解概念:
尾调用: 尾调是在函数的执行过程中最后一个动作是一个函数的调用,即这个调用的返回值被当前函数直接返回。如下:
function g(x) { }
function f(x) {
return g(x)
}
尾递归: 而尾递归指最后的尾调用位置上是这个函数本身。首先执行计算出一次的结果,然后执行递归调用,将当前步骤的结果值传递给下一个递归步骤。每次递归都不会增加调用栈的长度,只是更新当前的堆栈帧,只存在一个调用帧,避免了内存的消耗。
尾递归实现斐波那契数列:
int Fibonaqi(int n, int ret1, int ret2)
{
if (n ==0 )
{
//结果
return ret1;
}
else
{
return Fib(n - 1, ret2, ret1 +ret2);
}
}
执行过程:
Fib (5, 0,1)=Fib (4, 1, 1) =1
Fib (4,1,1)=Fib (3, 1,2) =1
Fib (3, 1,2)=Fib (2,2, 3) =2
Fib (2,2,3)=Fib (1,3, 5) =3
Fib(1,3,5)=Fib (0,5, 8) =5
尾递归的阶乘,这个好理解点:
int facttail(int n, int a)
{
if (n < 0)
return 0;
else if (n == 0)
return 1;
else if (n == 1)
return a;
else
return facttail(n - 1, n * a);
}
3.5 队列
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。遵循 FIFO,先进先出。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
3.5.1 js简单实现一个队列:
//定义一个类,实质也是一个函数
class Queue{
//定义一个构造方法
constructor(){
//实例对象
this.items = [];
}
//入队方法
enqueue(ele){
this.items.push(ele);
}
//出队方法
dequeue(){
//shift()方法可用于把数组的第一个元素从其中删除,并返回第一个元素的值。
return this.items.shift();
}
//查看队首元素
front(){
return this.items[0];
}
//查看队尾
rear(){
return this.items[this.items.length-1];
}
//查看队尾是否为空
isEmpty(){
return this.items.length === 0;
}
//查看长度
size(){
return this.items.length;
}
//清空队列
clear(){
this.items = [];
}
}
使用看:
const queue = new Queue();
queue.enqueue(2);
queue.enqueue(5);
queue.enqueue(5);
console.log(queue);
console.log(queue.size());
运行结果正确:
3.6 js异步队列
首先JavaScript是单线程的,同一个时间只能做一件事。
为什么 JavaScript 是单线程 ?这主要和js的用途有关,js是作为浏览器的脚本语言,主要是实现用户与浏览器的交互,以及操作dom;这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器就不知道以哪个线程为准。
为了利用多核 CPU 的计算能力,html5 提出 Web Worker 标准,允许 JavaScript
脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。 如果前一个任务耗时很长,如发起一个请求, 网络很慢,那后面的那个任务是不是就要一直等着。这样就很不好,那肯定不可以呀,所以js这样解决。
如在I0的时候,(输入输出的时候) ,主线程不去管IO,挂起处于等待中的任务,先运行排在后面的任务,等待I0设备返回了结果,再回过头,把挂起等待的任务继续执行下去。
于是所有的任务可以分为: 同步任务,异步任务。
同步任务: 在主线程上排队执行的任务,只有前一个任务执行完毕以后,才能够去执行下一个任务。
异步任务: 不进入主线程,而是进入任务队列,(任务队列就是主线程维护的一个任务队列,保存着异步代码),只有任务队列通知主线程,某个异步任务可以执行了,这个任务才会进入主线程执行。
有下面的代码,说出输出顺序:
console.log(1);
setTimeout(()=>{
console.log(2);
},60)
setTimeout(()=>{
console.log(3);
},0)
console.log(4);
答案如下:1,4,3,2
- 最先执行的是同步代码,执行完毕以后,立即出栈,让出主线程。
- 同步代码执行完毕,立即出栈,此时主线程是处于空闲状态了。
- 则此时主线程去读取任务队列,队列遵循的原则是先进先出。但是,有个条件,触发条件相等既优先级一样,会遵循先进先出。就是谁先进队列谁先出去主线程运行。但是如果触发条件不相同,则优先执行到达触发条件的代码先出去。明显等待0秒的优先级更高,主线程有空就立即执行。
- 主线程执行完毕以后,从事件队列中去读取任务队列的过程,我们称之为事件循环。(Event Loop)
判断以下代码输出:
// 宏任务
setTimeout(()=>{
console.log('1');
},0)
// 同步任务
const promise = new Promise((resolve,reject)=>{
console.log('2');
resolve();
//微任务
}).then(()=>{
console.log('3');
})
结果:
任务队列里又存在着宏任务队列和微任务队列,执行完同步任务后,先执行微任务再执行宏任务。
常见宏任务队列:lO、定时器、事件绑定、ajax …等
常见微任务队列:Promise.then catch、 finally、 process.nextTick…等
四.链表:
4.1 概念
官方解释是:
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。链表可以在多种编程语言中实现。
简单来说就是:
链表: 内存地址可以是连续的,也可以是不连续的。把数据元素存放在任意的存储单元里,指针来存放数据元素的地址。
数组: 大小是固定的,一经声明就要占用整块的连续的内存空间。如果说,声明的数组过大,系统可能没有足够的连续的内存空间用于分配,(out of memory)内存不足。如果声明的数组过小,当不够用时,又需要去重新一块更大的内存,还要数据拷贝。但是数组查找元素很快。
单向链表结构图:
链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(有些语言称为指针或者链接)组成。
总而言之:
- 插入和删除: 链表性能更好。
- 查询和修改: 数组性能更好。
4.2 js实现一个大致的单向链表:
// 结点的类
class Node {
constructor(element){
this.element = element;
this.next = null;
}
}
// 链表类
class LinkedList {
constructor(){
//有个链表头
this.head = null;
//链表长度
this.length = 0;
}
// 实现在链表尾部追加元素
append(element){
//新建一个结点
let node = new Node(element);
//先看链表是不是空
if(this.length===0){
//为空这个结点就为表头
this.head = node;
}else{
//变量,当前结点,通过从head开始遍历找到最后的结点
let current = this.head;
while(current.next){
current = current.next;
}
//循环结束,则current为最后一个结点
//添加新结点
current.next = node;
}
// 长度加1
this.length+=1;
}
//获取链表头
getHead(){
return this.head;
}
//toString方法,就是输出链表内容
toString(){
let current = this.head;
let linkString = '';
//遍历链表,把每个结点内容用linkString拼接起来
while(current.next){
linkString+= ','+current.element;
current = current.next;
}
//添加最后一个结点内容
linkString+= ','+current.element;
//返回内容
return linkString.slice(1);
}
//插入在任意位置插入元素,element是内容,position是位置
insert(element,position){
//先判断位置是有效值
if(position<0||position>this.length){
return false;
}
// 以下是位置有效
// 记录索引
let index = 0;
//当前结点
let current = this.head;
//上一个结点
let previous = null;
//创建元素
let node = new Node(element);
//判断插入是不是第一个
if(position===0){
node.next=this.head;
this.head=node;
}else{
while(index<position){
//一直更新保存着上一个结点
previous = current;
current = current.next;
index++;
}
//插入结点,就是当前上一个结点指向改变为新增结点,新增结点指向原本的当前结点
node.next = current;
previous.next = node;
//length++
this学习JS 数据结构与算法笔记