数组
创建
// 直接创建
const arr = [1, 2, 3, 4]
// 利用构造函数创建
const arr = []
// 创建指定长度并有值的数组
const arr = (new Array(7)).fill(1) //得到一个长度为7,且每个元素都初始化为1的数组
访问与遍历
访问:中括号中指定想要的索引
遍历:
① for循环
const len = arr.length
for(let i=0;i<len;i++) {
// 输出数组的元素值,输出当前索引
console.log(arr[i], i)
}
② forEach 方法
arr.forEach((item, index)=> {
// 输出数组的元素值,输出当前索引
console.log(item, index)
})
③ map 方法 和forEach方法区别:map会返回新的数组
const newArr = arr.map((item, index)=> {
// 输出数组的元素值,输出当前索引
console.log(item, index)
// 在当前元素值的基础上加1
return item+1
})
从性能上看,for 循环遍历起来是最快的
二维数组
简单来说就是,数组里面套数组,别名:“矩阵”
const arr = [
[1,2,3,4,5],
[1,2,3,4,5],
[1,2,3,4,5],
[1,2,3,4,5],
[1,2,3,4,5]
]
初始化二维数组
// for 循环
const len = arr.length
for(let i=0;i<len;i++) {
// 将数组的每一个坑位初始化为数组
arr[i] = []
}
访问
对比一维数组访问,需要两层循环
// 缓存外部数组的长度
const outerLen = arr.length
for(let i=0;i<outerLen;i++) {
// 缓存内部数组的长度
const innerLen = arr[i].length
for(let j=0;j<innerLen;j++) {
// 输出数组的值,输出数组的索引
console.log(arr[i][j],i,j)
}
}
数组 API
如concat、some、slice、join、sort、pop、push 等等这些
栈和队列
栈和队列的实现一般都要依赖于数组,两者的区别在于,它们各自对数组的增删操作有着不一样的限制
数组增删方法
新增元素
① unshift —— 数组头部新增
② push —— 数组尾部新增
③ splice —— 任何位置新添元素
const arr = [1,2]
// 参数:索引值,删除个数,新添加元素的值
arr.splice(1,0,3) // [1,3,2]
删除元素
① shift —— 删除头部元素 ② pop —— 输出尾部元素 ③ splice —— 两个参数即删除
栈
后进先出(LIFO,Last In First Out)的数据结构
特征:
- 只允许从尾部添加元素
- 只允许从尾部取出元素
对应到数组的方法,刚好就是 push 和 pop
栈顶元素 === 数组尾部的元素
// 初始状态,栈空
const stack = []
// 入栈过程
stack.push('东北大板')
stack.push('可爱多')
stack.push('巧乐兹')
stack.push('冰工厂')
stack.push('光明奶砖')
// 出栈过程,栈不为空时才执行
while(stack.length) {
// 单纯访问栈顶元素(不出栈)
const top = stack[stack.length-1]
console.log('现在取出的冰淇淋是', top)
// 将栈顶元素出栈
stack.pop()
}
// 栈空
stack // []
队列
和栈是相反的结构
先进先出(FIFO,First In First Out)的数据结构
(好像排队买东西,先到先得)
特征:
- 只允许从尾部添加元素
- 只允许从头部移除元素
对应数组的push 和 shift 方法
☆注意
出栈:栈顶元素——数组的最后一个元素
队列出队:队头元素——数组的第一个元素
const queue = []
queue.push('小册一姐')
queue.push('小册二姐')
queue.push('小册三姐')
while(queue.length) {
// 单纯访问队头元素(不出队)
const top = queue[0]
console.log(top,'取餐')
// 将队头元素出队
queue.shift()
}
// 队空
queue // []
链表
链表中,数据单位的名称叫做“结点”,而结点和结点的分布,在内存中可以是离散的 每一个结点的结构都包括了两部分的内容:数据域和指针域
数据域存储的是当前结点所存储的数据值;
指针域则代表下一个结点(后继结点)的引用
结点间关系是通过 next 指针来维系,关系到结点添加、删除操作
{
// 数据域
val: 1,
// 指针域,指向下一个结点
next: { val:2, next: ... }
}
注意:想访问链表中的任何一个元素,我们都得从起点结点开始,逐个访问 next,一直访问到目标结点为止。通常会设定一个 head 指针来专门指向链表的开始位置
结点创建
// 需要一个构造函数
function ListNode(val) {
this.val = val;
this.next = null;
}
// 传值,指定next
const node1 = new ListNode(1)
node1.next = new ListNode(2)
添加
尾部新添元素
任意位置新添元素
// 如果目标结点本来不存在,那么记得手动创建
const node3 = new ListNode(3)
// 现在node3的 next 指针指向 node2(即原来的 node1.next)
node3.next = node1.next
// 把node1的 next 指针指向 node3
node1.next = node3
删除
☆ 改变指针方向
node1.next = node3.next
注意:node3 就成为了一个完全不可抵达的结点了,它会被 JS 的垃圾回收器自动回收掉
思路: 删除结点,重点不是定位目标结点,而是定位目标结点的前驱结点
// 利用 node1 可以定位到 node3
const target = node1.next
node1.next = target.next
数组和链表
JS 数组和常规数组关系:“JS 数组未必是真正的数组”
// 数组定义:存储在连续的内存空间里
const arr = [1,2,3,4]
// 如果非连续的内存
const arr = ['haha', 1, {a:1}]
相对于数组来说,链表有一个明显的优点,就是添加和删除元素都不需要挪动多余的元素
- 高效的增删操作 (时间复杂度O(1))
- 麻烦的访问操作 (时间复杂度O(n))
二叉树
树结构
- 树的层次计算规则:根结点所在的那一层记为第一层,其子结点所在的就是第二层,以此类推。
- 结点和树的高度计算规则:叶子结点高度记为1,每向上一层高度就加1,逐层向上累加至目标结点时,所得到的的值就是目标结点的高度。树中结点的最大高度,称为“树的高度”。
- 度的概念:一个结点开叉出去多少个子树,被记为结点的“度”。比如我们上图中,根结点的“度”就是3。
- 叶子结点:叶子结点就是度为0的结点(简单理解:最后一层,不再有分支)。
二叉树
由根结点、左子树和右子树组成,且左右子树都是二叉树
☆ 注意:普通的树并不会区分左子树和右子树,但在二叉树中,左右子树的位置是严格约定、不能交换的。对应到图上来看,也就意味着 B 和 C、D 和 E、F 和 G 是不能互换的。
二叉树结构:
- 数据域
- 左侧子结点(左子树根结点)的引用
- 右侧子结点(右子树根结点)的引用
// 二叉树结点的构造函数
function TreeNode(val) {
this.val = val;
// 把左侧子结点和右侧子结点都预置为空
this.left = this.right = null;
}
// 新建二叉树
const node = new TreeNode(1)
以这个结点为根结点,我们可以通过给 left/right 赋值拓展其子树信息,延展出一棵二叉树