数据结构相当“锅碗瓢盆”,算法相当于“菜谱”两者互相关联密不可分
- 时间复杂度 就是函数 用大O表示 算法在运行过程中消耗的时间大小
执行几次 就表示 O几 比如 O(1) 、O(n) 取最大值 - 空间复杂度 也是函数 用大O表示 算法在运行过程中临时占用存储空间的大小
占用几次 就表示 O几 比如 O(1) 、O(n) 取最大值
数据结构 计算机存储组织数据的方式,算法 解决问题的清晰指令 数据结构为算法服务 算法围绕数据结构操作
栈
后进先出 js没有栈结构 需要用array模拟 java有栈,所谓的栈顶元素就是 数组长度-1
- 入栈: push()
- 出栈: pop() (移除数组的最后一项并返回它)
- 栈顶元素:stack[stack.length-1] 数组的最后一位。
场景:函数调用堆栈 js执行过程严格按照堆栈的模式,最后调用的函数 最早执行完
js的解释器就是用栈的方式运行代码
const fun1 = () => {
fun2()
}
const fun2 = () => {
fun3()
}
const fun3 = () => {
}
fun1();
通过debug测试 进入fun1方法内部 会依此调用fun2,fun3,调用结束后会从fun3依次从调用堆栈中删除 会先删除fun3,万全符合,后进先出的场景
20.有效的括号
1、提前记录好 右括号类型), }, ] 和 左括号类型 (, {, [ 的映射表,当遍历中遇到左括号的时候,就放入栈 stack;
2、当遇到右括号时,就把 stack 顶的元素 pop 出来,看一下是否是这个右括号所匹配的左括号(比如 ( 和 ) 是一对匹配的括号),不匹配则 返回 false;
3、当遍历结束后,栈中不应该剩下任何元素,返回 true ,否则返回 false。
/**
* @param {string} s
* @return {boolean}
*/
var isValid = function(s) {
const charMap = {
')': '(',
'}': '{',
']': '['
}
const stack = []
for(let i = 0; i < s.length; i ++) {
const c = s[i]
if(charMap[c]) {
const charPop = stack.pop()
// 栈顶、和当前字符串 是否一对匹配的括号,不匹配则 返回 `false`
if(charPop !== charMap[c]) return false
} else {
stack.push(c)
}
}
return stack.length === 0
};
数据结构之“队列”
先进先出,场景:食堂排队打饭先进先出保证有序。js中用数组模拟队列
- 入队:push()
- 出对:shift()
- 队头:queue[0]
js是单线程的运行机制,无法同时处理多个异步任务,使用任务队列先进先出处理异步任务
思考:js是单线程的运行机制,无法同时处理多个异步任务和同步任务,所以面试中会有异步和同步的任务那个先执行的考察!
链表
多个元素组成的列表,元素存储不连续通过next指针连在一起,为什么不用数组?
数组与链表
- 数组:数组中间增删改查元素,往往需要移动元素。
- 链表:链表中间增删改查元素,不需要移动元素只需要更改next指向即可。
- 链表:存贮的元素不是连续的,之间通过next连接
js中没有链表一般用object模拟链表
注意链表常用操作:遍历链表、插入、删除
遍历链表的算法:1、先申明一个指针 2、在循环里把当前的指针指向下一个链表
const a = { val: 'a' };
const b = { val: 'b' };
const c = { val: 'c' };
const d = { val: 'd' };
a.next = b;
b.next = c;
c.next = d;
// 遍历链表
let p = a;
while (p) {
console.log(p.val);
p = p.next;
}
// 插入
const e = { val: 'e' };
c.next = e;
e.next = d;
// 删除
c.next = d;
前端与链表
原型链本质就是链表,原型链通过 __proto__连接各个原型对象 比如:(function.prototype,object.prototype)而不是next
原型链长啥样子?
注意到对象的原型链 特别短 这个很重要 有相关的面试题考察
关于原型链的两个知识点
instanceof的原理,并用代码实现
const instanceofs = (A,B) => {
let p = A;
while (p) {
if (p === B.prototype) {
return true;
}
p = p.__proto__;
}
return false;
}
如果在对象上没有找到a属性就会沿着原型链去找,关于函数和数组的原型链可以查考上面的图片
let obj = {}
let f = function(){}
Object.prototype.a = '11';
Function.prototype.b = '22'
console.log(obj.a)
console.log(obj.b)
console.log(f.a);
console.log(f.b)
11
undefined
11
22
删除排序链表中的重复元素
使用 cur, next 两个指针表示当前值和下一值, 若 cur 指针的值与 next 指针的值相等, 则将 next 指针往后移动一位即可。
输入:head = [1,1,2,3,3]
输出:[1,2,3]
var deleteDuplicates = function(head) {
if (!head) {
return head;
}
let cur = head;
while (cur.next) {
if (cur.val === cur.next.val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
};
数据结构之“集合”
无序且唯一的数据结构,es6中的Set就是集合结构,js中的集合用 set 没有交集的概念 需要通过数组的筛选去实现
应用场景
- 因为是无序唯一 可以去重复
- 判读某个元素是否在集合中
- 求交集
es6中set的使用方法
let mySet = new Set();
增加操作
mySet.add(1);
mySet.add(5);
mySet.add(5);
mySet.add('some text');
let o = { a: 1, b: 2 };
mySet.add(o);
mySet.add({ a: 1, b: 2 });
添加两个5只会保留一个 添加两个相同的对象会都存在 因为它们的引用地址不一样
const has = mySet.has(o);
是否存在
mySet.delete(5);
删除
for(let [key, value] of mySet.entries()) console.log(key, value);
遍历
Set转换数组
const myArr = Array.from(mySet);
数组转Set
const mySet2 = new Set([1,2,3,4]);
求交集和差集
const intersection = new Set([...mySet].filter(x => mySet2.has(x)));
const difference = new Set([...mySet].filter(x => !mySet2.has(x)));
数据结构之“字典”
存储唯一值的数据结构,它以键值对的形式来储存 es6中 map 就是字典
1两数之和 349两个数组的交集 20有效的括号 3无重复字符的最长子串(涉及到新建动态区间)76最小覆盖子串(用双指针维护一个滑动窗口 比较困难)
es6中map的使用方法
const m = new Map();
// 增
m.set('a', 'aa');
m.set('b', 'bb');
// 删
m.delete('b');
m.clear();
// 改
m.set('a', 'aaa');
两数之和
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
const twoSum = (nums, target) => {
const prevNums = {}; // 存储出现过的数字,和对应的索引
for (let i = 0; i < nums.length; i++) { // 遍历元素
const curNum = nums[i]; // 当前元素
const targetNum = target - curNum; // 满足要求的目标元素
const targetNumIndex = prevNums[targetNum]; // 在prevNums中获取目标元素的索引
if (targetNumIndex !== undefined) { // 如果存在,直接返回 [目标元素的索引,当前索引]
return [targetNumIndex, i];
} else { // 如果不存在,说明之前没出现过目标元素
prevNums[curNum] = i; // 存入当前的元素和对应的索引
}
}
}
两个数组的交集
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的
var intersection = function(nums1, nums2) {
nums1=nums1.sort((a,b)=>a-b);
nums2=nums2.sort((a,b)=>a-b);
let res=new Set();
let i=0;
let j=0;
while(i<nums1.length && j<nums2.length){
if(nums1[i]<nums2[j]){
i++;
}else if(nums1[i]>nums2[j]){
j++;
}else{
res.add(nums1[i]);
i++;
j++;
}
}
return [...res];
};
有效的括号
输入:s = “()[]{}”
输出:true
输入:s = “(]”
输出:false
var isValid = function(s) {
const n = s.length;
if (n % 2 === 1) {
return false;
}
const pairs = new Map([
[')', '('],
[']', '['],
['}', '{']
]);
const stk = [];
for (let ch of s){
if (pairs.has(ch)) {
if (!stk.length || stk[stk.length - 1] !== pairs.get(ch)) {
return false;
}
stk.pop();
}
else {
stk.push(ch);
}
};
return !stk.length;
};
树–多叉树
分层数据的抽象模型 js中没有树 通过object和array构建一个树
深度优先遍历(尽可能深的搜索树的分支 ==》 相当于看书从目录章节内容从头看到尾)
广度优先遍历(先访问离根节点最近的分支 ==》先看目录再看章节最后看内容)
const tree = {
val: '1',
children: [
{
val: '2',
children: [
{
val: '21',
children: []
}
]
},
{
val: '3',
children: [
{
val: '31',
children: []
}
]
}
]
}
// 深度遍历就是普通的递归
const dfs = (tree) =>{
console.log(tree.val);
if(tree.children && tree.children.length){
tree.children.forEach(dfs)
}
}
// 广度遍历用到了队列
const bfs = (root) =>{
const q = [root];
while(q.length>0){
const t = q.shift();
console.log(t.val);
t.children.forEach((child) =>{
q.push(child);
})
}
}
树—二叉树
104二叉树的最大深度(需用到递归设置level) 111二叉树的最小深度(解法有问题提交报错) 102二叉树的层序遍历(运行报错) 94二叉树的中序遍历112路径总和
递归版
// 先序遍历 访问根节点 对根节点的左子树进行先序遍历 对根节点的右子树进行先序遍历 简称 根左右 采用递归的方式
const perOrder = (root) =>{
if(!root) return;
console.log(root.val);
perOrder(root.left)
perOrder(root.right)
}
// 中序遍历 对根节点的左子树进行中序遍历 访问根节点 对根节点的右子树进行中序遍历 简称左根右 采用递归的方式
const inOrder = (root) =>{
if (!root) return;
inOrder(root.left);
console.log(root.val);
inOrder(root.right);
}
// 后序遍历 对根节点的左子树进行后序遍历 对根节点的右子树进行后序遍历 访问根节点 简称左右根 采用递归的方式
const endOrder = (root) =>{
if (!root) return;
endOrder(root.left);
endOrder(root.right);
console.log(root.val);
}
非递归版 (重要考察)
// 先序遍历 根左右
const preOrder1 = (root) =>{
if (!root) return;
const stack = [root];
while(stack.length){
const t = stack.pop();
console.log(t.val);
if (t.right) stack.push(t.right)
if (t.left) stack.push(t.left)
}
}
// 中序遍历 左根右
const inOrder1 = (root) =>{
if (!root) return;
const stack = [];
let p = root;
while(stack.length || p){
while(p){
stack.push(p)
p = p.left
}
const n = stack.pop();
console.log(n.val);
p = n.right;
}
}
// 后序遍历 左右根
const endOrder1 = (root) =>{
if (!root) return;
const outStack = [];
const stack = [root];
while(stack.length){
const t = stack.pop();
// console.log(t.val);
outStack.push(t);
if (t.left) stack.push(t.left)
if (t.right) stack.push(t.right)
}
while(outStack.length){
const n = outStack.pop();
console.log(n.val);
}
}
图
由边链接的点 比如 路线 航班图 二元关系 一条边连接两个点,js中用object和array表示,深度优先遍历(访问根节点,对根节点没访问过的相邻节点挨个深度遍历) 广度优先遍历 (递归队列的方式)
图的表示法 邻接矩阵 邻接表
65有效数字(比较困难 理解困难)417太平洋大西洋水流问题(深度优先遍历)
深度优先遍历
let set = new Set();
const dfs11 = (node) =>{
console.log(node);
set.add(node);
graph[node].forEach((c) =>{
if (!set.has(c)){
dfs(c);
}
})
}
广度优先遍历
const set11 = new Set();
set.add(2);
const q = [2]
while(q.length){
const t = q.shift();
console.log(t);
graph[t].forEach((c) =>{
if (!set.has(c)){
q.push(c);
set.add(t);
}
})
}
堆
完全二叉树 特殊的树 没成节点完全填满 如果没有填满只缺少右边的若干节点,所有节点都大于等于或(小于等于)它的子节点 大于等于叫最大堆 小于等于叫最小堆
js中用 数组表示堆 可以根据公式获取节点的位置
// 任意节点 获取它的左侧子节点的位置 公式 2index+1
// 任意节点 获取它的右侧子节点的位置 公式 2index+2
// 任意节点 获取它的父节点位置 公式 (index-1)/2 只求商数 不看余数
// 用途:高效快速找出最小值 最大值 第k个最大值 第k个最小值
// 最小堆
class MinHeap {
constructor(){
this.heap = [];
}
swap(i1, i2){
const temp = this.heap[i1];
this.heap[i1] = this.heap[i2];
this.heap[i2] = temp;
}
getParentIndex(i){
return Math.floor((i -1) / 2)
}
getLeftIndex(i){
return 2*i+1
}
getRightIndex(i){
return 2*i+2
}
// 上移的方法
shiftUp(index){
if (index == 0) return;
const parent = this.getParentIndex(index);
if (this.heap[parent]>this.heap[index]){
this.swap(parent, index)
this.shiftUp(parent)
}
}
// 下移的方法
shiftDown(index){
const left = this.getLeftIndex(index);
const right = this.getRightIndex(index);
if (this.heap[left] < this.heap[index]){
this.swap(left, index)
this.shiftDown(left)
}
if (this.heap[right] < this.heap[index]){
this.swap(right, index)
this.shiftDown(right)
}
}
insert(v){
this.heap.push(v);
this.shiftUp(this.heap.length - 1)
}
pop(){
this.heap[0] = this.heap.pop();
this.shiftDown(0)
}
// 堆顶元素
peek(){
return this.heap[0]
}
// 堆的大小
size(){
return this.heap.length
}
}
排序(算法) 搜索(算法)
冒泡排序 比较所有相邻元素 如果第一个比第二个大交换他们 一轮下来最后一个数最大 执行n-1轮 比较简单 // 两个循环 性能不太好
Array.prototype.bubbleSort = function () {
for(let i = 0;i<this.length-1; i++){
for(let j = 0; j<this.length-1 -i; j+=1){
if (this[j]>this[j+1]){
const temp = this[j];
this[j] = this[j+1];
this[j+1] = temp;
}
}
}
}
选择排序 找到数组中最小值 选中他并放到第一位 接着找到第二个 依次类推 执行n-1轮 和冒泡排序一样 性能不太好
Array.prototype.selectionSort = function () {
// 找到数组中最小值的下标
for(let i = 0; i<this.length-1; i++){
let index = i;
for(let j = i; j<this.length; j++){
if (this[j] < this[index]){
index = j
}
}
if(index !== i) {
const temp = this[i];
this[i] = this[index]
this[index] = temp
}
}
}
**插入排序 从第二个数开始往前比 比他大就往后排 依次类推直到最后一位 **
Array.prototype.insertionSort = function(){
// 从第二个数开始往前比 的方法 1、申明一个下标 从第二个下标开始
for(let i = 1; i<this.length; i++){ // 从第二个数遍历
const two = this[i];
let j = i;
while(j>0){
if (this[j-1]>two){
this[j] = this[j-1]
} else {
break;
}
j--
}
this[j] = two
}
}
归并排序
分:把数组劈成两半 递归对子数组执行’分’的操作 直到分成一个个单独的数
合:把两个数合并为有序数组 再对有效数组合并 直到全部子数组合并成一个完整的数组
Array.prototype.mergeSort = function(){
const rec = (arr) =>{
if (arr.length == 1) { return arr }
const mid = Math.floor(arr.length/2);
const left = arr.slice(0,mid);
const right = arr.slice(mid, arr.length)
// 准备了有序数组 准备合并
const orderLeft = rec(left);
const orderRight = rec(right);
const res = []
while(orderLeft.length || orderRight.length){
if (orderLeft.length && orderRight.length){
res.push(orderLeft[0] < orderRight[0] ? orderLeft.shift() : orderRight.shift())
} else if (orderLeft.length) {
res.push(orderLeft.shift())
} else if (orderRight.length) {
res.push(orderRight.shift())
}
}
return res;
}
// // 将结果 需要拷贝到this 上的 一个算法
const res = rec(this);
res.forEach((n,i) =>{this[i] = n})
}
快速排序 比以上四种排序的性能都要好
分区:在数组中任意找一个基准 所有比基准小的元素放到基准前面 所有比基准大的元素放到基准的后面
递归:递归的对基准前后的子数组进行分区
Array.prototype.quickSort = function(){
// 实现递归分区算法
const rec = (arr) =>{
if (arr.length === 1) { return arr }
const left = [];
const right = [];
const mid = arr[0];
for(let i = 1; i < arr.length; i+=1){
if (arr[i] < mid){
left.push(arr[i])
} else {
right.push(arr[i])
}
}
return [...rec(left), mid, ...rec(right)]
}
const res = rec(this);
res.forEach((n,i) =>{this[i] = n})
}
顺序搜索 低效 入门算法 相当于indexOf
Array.prototype.sxSort = function(item){
for(let i = 0; i < this.length; i++){
if (this[i] == item){
return i
}
}
return -1;
}
二分搜索 前提是一个有序的数组 (有序以后就能判断是在中间元素的左侧还是右侧)
从数组中间元素开始搜索 如果是目标值则终止搜索 如果目标值大于或者小于中间元素 则在大于或小于中间元素的那一半数组里搜索
// 21合并两个有序链表 374猜数字大小(完全应用了下面的算法)但是结果出错
// 下面的算法 比递归在空间复杂度上更小
Array.prototype.binarySort = function (item) {
let low = 0;
let hight = this.length-1;
while(low <= hight){
const mid = Math.floor((low+hight)/2);
const ele = this[mid];
if (ele < item){
low = mid+1
} else if (ele > item){
hight = mid-1
} else {
return mid
}
}
return -1;
}
算法思路
分而治之
// 分而治之 算法设计中的方法 是一种算法思想
// 它将一个问题拆分成和原问题相识的小问题(独立) 递归解决 再将结果合并
// 归并排序 就是根据分而治之设计的
// 快速排序 也是根据分而治之设计的
// 涉及到的算法
// 226翻转二叉树 可以采用分而治之的思路 翻转函数 其实就是个递归 (比较简单 思路很重要)
// 100相同的树 分 分别获取树的左子树和右子树 执行报错需另找算法
// 101对称二叉树 判断二叉树是不是静态对称的 100和101算法很相似 都用到了递归函数
动态规划
// 算法设计中的方法 是一种思想
// 把问题分解为相互重叠的子问题
// 70爬楼梯 定义子问题 f(n) = f(n-1)+f(n-2) 反复执行子问题
// 198打家劫舍 f(k) = Math.max(f(k-2)+Ak,f(k-1)) 所谓的子问题就是定一个公式 反复执行子问题 理解太困难
贪心算法
// 算法设计思想 通过局部最优 达到全局最优 但是有时候不一定全局最优
// 455分饼干 算法相对简单 122买卖股票的最佳时机 II
回溯算法 算法思想
// 先选择一条路 如果走不通再回到原点的思路 有很多排列方式 需要递归模拟所有的排列方式
// 46全排列 每个组合不重复:定义个递归算法 时间复杂度难
46全排列 给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
const permute = (nums) => {
const res = [];
const used = {};
function dfs(path) {
if (path.length == nums.length) { // 个数选够了
res.push(path.slice()); // 拷贝一份path,加入解集res
return; // 结束当前递归分支
}
for (const num of nums) { // for枚举出每个可选的选项
// if (path.includes(num)) continue; // 别这么写!查找是O(n),增加时间复杂度
if (used[num]) continue; // 使用过的,跳过
path.push(num); // 选择当前的数,加入path
used[num] = true; // 记录一下 使用了
dfs(path); // 基于选了当前的数,递归
path.pop(); // 上一句的递归结束,回溯,将最后选的数pop出来
used[num] = false; // 撤销这个记录
}
}
dfs([]); // 递归的入口,空path传进去
return res;
};
有待验证
const fuse = (arr) =>{
const result = [];
const backtrack = (path)=>{
if (path.length == arr.length){
result.push(path);
return;
}
arr.forEach((n) =>{
if (path.includes(n)){ return }
backtrack(path.concat(n))
})
}
deepFor([]);
return result
}
// 78子集 理解起来有点困难 递归遍历 比较复杂