面试算法题

promise

是什么

Promise 是用于处理异步操作的对象。它有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。我们可以手写一个简化版的 Promise,实现其基本功能。

实现思路:
Promise 构造函数接收一个执行器函数,该函数有两个参数 resolvereject,用于改变 Promise 的状态。
then 方法用于注册回调函数,在 Promise的状态改变后执行。
使用状态机来管理 pendingfulfilledrejected 状态。

实现

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
class MyPromise {hexp
constructor(executor) {
this.state = 'pending'; // 初始状态
this.value = undefined; // 成功的值
this.reason = undefined; // 失败的原因
this.onFulfilledCallbacks = []; // 存储成功回调
this.onRejectedCallbacks = []; // 存储失败回调

// resolve 函数
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled'; // 状态转为 fulfilled
this.value = value; // 传递成功的值
this.onFulfilledCallbacks.forEach(fn => fn(this.value)); // 执行成功回调
}
};

// reject 函数
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected'; // 状态转为 rejected
this.reason = reason; // 传递失败的原因
this.onRejectedCallbacks.forEach(fn => fn(this.reason)); // 执行失败回调
}
};

// 执行 executor,并捕获异常
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}

// then 方法
then(onFulfilled, onRejected) {
// 如果 `onFulfilled` 不是函数,默认返回 value
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
// 如果 `onRejected` 不是函数,默认抛出 reason
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };

if (this.state === 'fulfilled') {
onFulfilled(this.value);
}

if (this.state === 'rejected') {
onRejected(this.reason);
}

if (this.state === 'pending') {
this.onFulfilledCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected);
}
}
}

// 测试 MyPromise
const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('Success!');
}, 1000);
});

promise.then(
value => console.log('Fulfilled:', value),
reason => console.log('Rejected:', reason)
);

promise.all

是什么

Promise.all() 是一个用于并行处理多个 Promise 的方法,它接受一个包含多个 Promise 的可迭代对象,并返回一个新的 Promise。只有当所有 Promise 都成功时,这个新的 Promise 才会 resolve;如果其中任何一个 Promise 失败,Promise.all() 就会 reject,并返回第一个失败的 Promise 的原因。

实现

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
// promise.all
function promiseAll(promises){
return new Promise((resolve,reject)=>{
if(!Array.isArray(promises)) return reject(new TypeError('参数类型应该为数组'))
if (promises.length==0) return resolve([])
let resultArr = []
let completedPromises = 0
promises.forEach((promise,index)=>{
Promise.resolve(promise)
.then(result=>{
resultArr[index] = result
completedPromises ++
if (completedPromises==promises.length) resolve(resultArr)
})
.catch(error=> reject(error))
})
})
}
// 示例
const p1 = Promise.resolve(3)
const p2 = new Promise((resolve) => setTimeout(resolve, 1000, 'foo'))
const p3 = Promise.resolve(42)

promiseAll([p1, p2, p3])
.then((results) => console.log(results)) // 输出 [3, "foo", 42]
.catch((err) => console.log(err))

promise.race

是什么

Promise.race() 方法会返回一个新的 Promise,一旦传入的 Promise 中有一个完成(无论是 resolved 还是 rejected),就会立即以那个 Promise 的结果(resolved 或 rejected)进行决议。

实现

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
function promiseRace(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('Argument must be an array'));
}

// 如果传入的数组为空,Promise 将永远不会 resolve 或 reject
if (promises.length === 0) {
return; // 不返回任何内容,Promise 将保持 pending 状态
}

// 遍历每个 Promise
promises.forEach(promise => {
// 使用 Promise.resolve 将所有项处理为 Promise
Promise.resolve(promise)
.then(resolve) // 一旦某个 Promise resolve,立即 resolve 结果
.catch(reject); // 一旦某个 Promise reject,立即 reject 错误
});
});
}

// 示例
const p1 = new Promise((resolve) => setTimeout(resolve, 500, "First"));
const p2 = new Promise((resolve) => setTimeout(resolve, 100, "Second"));
const p3 = new Promise((resolve, reject) => setTimeout(reject, 200, "Error"));

promiseRace([p1, p2, p3])
.then(result => console.log(result)) // 输出 "Second" 因为 p2 最先完成
.catch(err => console.log(err));

面试算法题

web缓存

是什么

  • web缓存有两部分:浏览器缓存和http缓存

    • 浏览器缓存比如:localStorage、sessionStorage、cookies等。用于缓存一些必要数据,用户信息、需要携带到后端的参数、一些列表数据。

    • 存储大小有限制localStorage、sessionStorage有5M,cookies大概4kb

  • http缓存

    官方介绍:Web 缓存是可以自动保存常见文档副本的 HTTP 设备。当 Web 请求抵达缓存时, 如果本地有“已缓存的”副本,就可以从本地存储设备而不是原始服务器中提取这 个文档。

  • 客户端发起请求http请求,服务器需要处理http的请求,并且http去传输数据,需要带宽,带宽是要钱买的啊。而我们缓存,就是为了让服务器不去处理这个请求,客户端也可以拿到数据。

  • 一般缓存静态资源html,css,img,动态资源实时性强不适合缓存

解决的问题与优缺点

  • 减少不必要的网络传输,节省带宽。
  • 更快加载页面
  • 减少服务器负担,避免服务器过载

实际场景:

其实日常的开发中,我们最最最最关心的,还是”更快的加载页面”;尤其是对于react/vue等SPA(单页面)应用来说,首屏加载是老生常谈的问题。这个时候,缓存就显得非常重要。不需要往后端请求,直接在缓存中读取。速度上,会有显著的提升。是一种提升网站性能与用户体验的有效策略。

知识扩展

  • 单页Web应用(single page web application,SPA),整个应用在初次加载时加载一个包含必要资源的HTML页面,之后所有的页面内容变化都是通过JavaScript动态更新DOM来实现,不需要重新加载整个页面。
  • 多页面应用,每次用户导航到新的页面时,都需要从服务器加载一个新的HTML页面。

图 0

强缓存和协商缓存

图 2

强缓存

从强制缓存的角度触发,如果浏览器判断请求的目标资源有效命中强缓存,如果命中,则可以直接从内存中读取目标资源,无需与服务器做任何通讯。

Cache-control的使用方法页很简单,只要在资源的响应头上写上需要缓存多久就好了,单位是秒。比如
在响应头中Cache-Control:’max-age=10’

Cache-Control:max-age=N,N就是需要缓存的秒数。从第一次请求资源的时候开始,往后N秒内,资源若再次请求,则直接从磁盘(或内存中读取),不与服务器做任何交互。

  • Cache-control有max-age、s-maxage、no-cache、no-store、private、public这六个属性。
    • max-age决定客户端资源被缓存多久。
    • s-maxage决定代理服务器缓存的时长。
    • no-cache表示是强制进行协商缓存。
    • no-store是表示禁止任何缓存策略。
    • public表示资源即可以被浏览器缓存也可以被代理服务器缓存。
    • private表示资源只能被浏览器缓存。

      注意,no-cache和no-store是一组互斥属性,这两个属性不能同时出现在Cache-Control中。
      注意,public和private也是一组互斥属性。他们两个不能同时出现在响应头的cache-control字段中。
      注意,max-age和s-maxage并不互斥。他们可以一起使用。

协商缓存

  • 基于last-modified的协商缓存
    基于last-modified的协商缓存实现方式是:
    1、首先需要在服务器端读出文件修改时间,
    2、将读出来的修改时间赋给响应头的last-modified字段。
    3、最后设置Cache-control:no-cache

图 3

注意圈出来的三行。
第一行,读出修改时间。
第二行,给该资源响应头的last-modified字段赋值修改时间
第三行,给该资源响应头的Cache-Control字段值设置为:no-cache.(上文有介绍,Cache-control:no-cache的意思是跳过强缓存校验,直接进行协商缓存。)
还没完。到这里还无法实现协商缓存
当客户端读取到last-modified的时候,会在下次的请求标头中携带一个字段:If-Modified-Since。

图 4

那么之后每次对该资源的请求,都会带上If-Modified-Since这个字段,而务端就需要拿到这个时间并再次读取该资源的修改时间,让他们两个做一个比对来决定是读取缓存还是返回新的资源。

图 5

使用以上方式的协商缓存已经存在两个非常明显的漏洞。这两个漏洞都是基于文件是通过比较修改时间来判断是否更改而产生的。

1.因为是更具文件修改时间来判断的,所以,在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样,就有可能文件内容明明没有修改,但是缓存依然失效了。

2.当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改时间记录的最小单位是秒,所以,如果文件在几百毫秒内完成修改的话,文件修改时间不会改变,这样,即使文件内容修改了,依然不会 返回新的文件。

为了解决上述的这两个问题。从http1.1开始新增了一个头信息,ETag(Entity 实体标签)

基础ETag的协商缓存
不用太担心,如果你已经理解了上面比较时间戳形式的协商缓存的话,ETag对你来说不会有难度。

ETag就是将原先协商缓存的比较时间戳的形式修改成了比较文件指纹。

文件指纹:根据文件内容计算出的唯一哈希值。文件内容一旦改变则指纹改变。

1.第一次请求某资源的时候,服务端读取文件并计算出文件指纹,将文件指纹放在响应头的etag字段中跟资源一起返回给客户端。

2.第二次请求某资源的时候,客户端自动从缓存中读取出上一次服务端返回的ETag也就是文件指纹。并赋给请求头的if-None-Match字段,让上一次的文件指纹跟随请求一起回到服务端。

3.服务端拿到请求头中的is-None-Match字段值(也就是上一次的文件指纹),并再次读取目标资源并生成文件指纹,两个指纹做对比。如果两个文件指纹完全吻合,说明文件没有被改变,则直接返回304状态码和一个空的响应体并return。如果两个文件指纹不吻合,则说明文件被更改,那么将新的文件指纹重新存储到响应头的ETag中并返回给客户端

单调栈

单调栈

何时用单调栈

通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。时间复杂度为O(n)。如果暴力破解复杂度为O(n^2)。

那么单调栈的原理是什么呢?为什么时间复杂度是O(n)就可以找到每一个元素的右边第一个比它大的元素位置呢?

单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素高的元素,优点是整个数组只需要遍历一次。

更直白来说,就是用一个栈来记录我们遍历过的元素,因为我们遍历数组的时候,我们不知道之前都遍历了哪些元素,以至于遍历一个元素找不到是不是之前遍历过一个更小的,所以我们需要用一个容器(这里用单调栈)来记录我们遍历过的元素。

单调栈怎们用

  • 栈中元素存什么
    存元素下标

  • 单调栈里元素是递增呢? 还是递减呢?

    • 首先是递增和递减的判断是指,从栈口到栈底
    • 递增:求左边或者右边第一个比他大的元素
    • 递减:求左边或者右边第一个比他晓得元素
  • 单调栈的作用
    存放遍历过的元素

  • 主要判断条件
    当前遍历元素T[i]与T[st.pop()]的关系

    • 1、T[i] > T[st.pop()] i-st.pop() i进栈
    • 2、T[i] == T[st.pop()] i-st.pop() i进栈
    • 3、T[i] < T[st.pop()] i进栈

图 8

题目

每日温度

图 7

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
var dailyTemperatures = function(temperatures) {
let n = temperatures.length
let stack = [0]
let res = Array(n).fill(0)
for(let i=1;i<n;i++){
let top = stack[stack.length-1]
//小于和等于的情况,直接压入栈
if (temperatures[i] === temperatures[top]){
stack.push(i)
}
else if(temperatures[i] < temperatures[top]){
stack.push(i)
}
//当前元素大于栈顶元素的情况,要计算距离,
//删掉栈顶元素知道所有小于当前元素的栈顶元素被删完,压入当前元素
else{
while (stack.length && temperatures[i] > temperatures[stack[stack.length-1]]) {
top = stack.pop()
res[top] = i-top
}
stack.push(i)
}
}
return res
};

下一个更大元素 I

图 9

思路
用到hash,将nums1存在map中,键位nums1中的元素,值为nums1中元素的索引

使用单调栈解决问题,遍历数组nums2,根据条件进行不同处理

  • 1、当前元素小于或等于栈顶元素,当前元素压入栈
  • 2、当前元素大于栈顶元素,查看栈顶元素是否出现在nums1中,如果在,那么及已经找到的他右边的较大元素。
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
var nextGreaterElement = function (nums1, nums2) {
const m = nums1.length
const n = nums2.length
let stack = [nums2[0]]
let res = Array(m).fill(-1)
let map = new Map()
for (let i = 0; i < m; i++) {
map.set(nums1[i],i)
}
for (let j = 1; j < n; j++) {
let top = stack[stack.length-1]
if (nums2[j]<top || nums2===top) stack.push(nums2[j])
else{
// console.log(res)
while(stack.length && nums2[j]>stack[stack.length-1]){
top = stack.pop()
if (map.has(top)){
res[map.get(top)] = nums2[j]

}
}
stack.push(nums2[j])
}
}
return res
}

下一个更大元素

图 10

思路
数组循环了,思路是把数组复制一份拼接在原数组后面,但是这占用了空间,所以思路是遍历两次数组就好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var nextGreaterElements = function(nums) {
let n = nums.length
let res = Array(n).fill(-1)
let stack = [0]
for(let i=1;i<n*2;i++){
let j = i
if (i>=n) j = i - n
while (stack.length && nums[j] > nums[stack[stack.length - 1]]) {
let top = stack.pop()
res[top] = nums[j]
}
stack.push(j)
}
return res
};

接雨水

图 11

图 12

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var trap = function(height) {
let n = height.length
let res = 0
let stack = [0]
for(let i=1;i<n;i++){
while(stack.length && height[i]>height[stack[stack.length-1]]){
let top = stack.pop()
if (stack.length === 0) break
let topl = stack[stack.length-1]
res += (Math.min(height[i],height[topl])-height[top]) * (i-topl-1)
}
stack.push(i)
}
return res
};

柱状图中最大的矩形

图 13

图 14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var largestRectangleArea = function(heights) {
heights = [0,...heights,0]
let n = heights.length
let maxArea = 0
let stack =[0]
for (let i=1;i<n;i++){
while(heights[i]<heights[stack[stack.length-1]]){
let top = stack.pop()
let topl = stack[stack.length-1]
let w = i-topl-1
maxArea = Math.max(maxArea,w*heights[top])
}
stack.push(i)
}
return maxArea
};

衣橱整理

LCR 130. 衣橱整理

图 0

思路

首先需要理解题意,我刚开始理解为在一个矩阵中找到所有符合digit(i)+digit(j)<=cnt的个数。如下图我以为是找出蓝色格子数量。

实际并不是,只要找到从(0,0)触发,能通过向下或向右走一格能到达的符合digit(i)+digit(j)<=cnt的格子的个数。也就是说要找的是从(0,0)出发通过右移或下移一格能到达的蓝色格子的个数。

题解和网友说要用到深度优先遍历。

图解

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
function wardrobeFinishing(m, n, cnt) {
const digitSum = (num) => {
let sum = 0
while (num) {
sum += num % 10
num = Math.floor(num / 10)
}
return sum
}

const visited = Array.from({ length: m }, () => Array(n).fill(false))
let count = 0

const dfs = (i, j) => {
if ( i < 0 || i >= m || j < 0 || j >= n || visited[i][j] || digitSum(i) + digitSum(j) > cnt)
return
visited[i][j] = true
count++
dfs(i + 1, j) // 向下移动
dfs(i, j + 1) // 向右移动
}

dfs(0, 0) // 从(0, 0)开始搜索
return count
}

字母迷宫

字母迷宫(单词搜索)

图 0

思路

刚开始看到这题,知道用回溯算法,所以回忆了回溯算法三部曲

  • 1、确定参数
  • 2、确定终止条件
  • 3、单层搜索逻辑

但是看了半天之确定了参数,终止条件和单层逻辑都理出来。看了讲解知道了原因。

第一个字母的匹配逻辑和后续字母匹配逻辑不一致。

  • 第一个字母就在矩阵中挨个遍历,哪个元素等于target[0],就可以开始下一个字母。

  • 而非第一个字的匹配,是要在第一个匹配上的字母的上下左右寻找。

前者按照行列遍历,后者按照方向遍历。因此有必要分成两部分,先找到第一个字母吗,然后后续字母遵照一样的逻辑处理。

思路详解

  • 1、参数:i,j,k分别表示矩阵的行、矩阵的列以及当前遍历到target字符的哪一位字符
  • 2、确定终止条件
    • 如果当前位置的字符不等于 target[k],则当前路径不可能构成目标单词,返回 false。
    • 如果 k == target.length - 1 且当前字符匹配,说明已经成功找到目标单词,返回 true。
    • 注意,还应该有一个终止条件是检查 (i, j) 是否越界或者该位置已经被访问过,以避免重复访问。
  • 3、单层搜索逻辑:
    • 对于矩阵中的每一个元素,如果它是 target[0],则从这个元素开始,尝试所有可能的搜索路径。
    • 一旦找到匹配的第一个字母,后续的搜索应该限制在当前元素的上下左右四个方向上。
    • 搜索过程中,需要标记当前路径已访问过的位置,以避免循环访问。搜索后,需要撤销标记(回溯),以便其他搜索路径可以使用这些位置。

代码

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
var wordPuzzle = function(grid, target) {
const h = grid.length,
w = grid[0].length;
const direction = [
[0, 1],
[0, -1],
[1, 0],
[-1, 0]
];
const visited = Array.from({ length: h }, () => Array(w).fill(false));

const check = (i, j, k) => {
if (grid[i][j] != target[k]) return false;
else if (k == target.length - 1) return true; // 修正比较操作

visited[i][j] = true;
//向四分方向寻找
for (let [dx, dy] of direction) { // 添加let声明
let newi = i + dx, newj = j + dy;
if (newi >= 0 && newi < h && newj >= 0 && newj < w && !visited[newi][newj]) { // 修正条件判断
if (check(newi, newj, k + 1)) {
return true; // 找到匹配后立即返回true
}
}
}
visited[i][j] = false;
return false;
};
// 第一个字母的判断
for (let i = 0; i < h; i++) {
for (let j = 0; j < w; j++) {
if (check(i, j, 0)) {
return true;
}
}
}
return false;
}

copilot 代码

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
var wordPuzzle = function(grid, target) {
const h = grid.length,
w = grid[0].length;
const directions = [
[0, 1], // 右
[0, -1], // 左
[1, 0], // 下
[-1, 0] // 上
];

const visited = Array.from({ length: h }, () => Array(w).fill(false));

const dfs = (i, j, k) => {
if (i < 0 || i >= h || j < 0 || j >= w || visited[i][j] || grid[i][j] !== target[k]) {
return false;
}
if (k === target.length - 1) {
return true;
}
visited[i][j] = true;
for (const [di, dj] of directions) {
if (dfs(i + di, j + dj, k + 1)) {
return true;
}
}
visited[i][j] = false; // 回溯
return false;
};

for (let i = 0; i < h; i++) {
for (let j = 0; j < w; j++) {
if (grid[i][j] === target[0] && dfs(i, j, 0)) {
return true;
}
}
}
return false;
};

基于cesium和高德地图API的路径规划

基于cesium和高德地图API的路径规划

cesium

  • 作为一个开源的、世界级的、展现3D全球地图的JavaScript类库,Cesium毫无疑问已然成为WebGIS开发中三维地球框架的首选,(https://cesium.com/)

  • cesium的组成和功能,如下表所示

Component Description
Viewer 提供3D、2D、2.5D的地图视图
DataSource 支持多种数据源,包括地形、影像、矢量数据等
Entity 用于在地图上展示点、线、面等几何图形
Widget 提供了一系列UI组件,如指南针、缩放控件等
Scene 管理和渲染3D场景
Camera 控制视角和视图
CesiumWidget 不带UI控件的Cesium Viewer的简化版本
Primitives 用于渲染几何图形和图像
ImageryLayer 管理地图影像图层
TerrainProvider 提供地形数据

高德地图api

  • 高德地图api在js中的应用,有两大类一种是已经封装好的web服务,一种是js API
类别 描述 使用场景
Web服务 高德地图提供的一系列服务器端API,可以通过HTTP请求访问。这些服务包括地理编码、路径规划、静态地图等。 当你需要在服务器端处理地图数据,或者在客户端进行简单的HTTP请求以获取地图数据时使用。
JavaScript API 一套在网页中直接嵌入和操作地图的接口。提供了丰富的功能,如地图显示、交互、覆盖物添加等。 当你需要在网页中嵌入交互式地图,实现复杂的地图操作和数据可视化时使用。

前者已经封装好api,直接访问即可返回数据,而后者是需要在前端页面中嵌入的地图功能。

输入提示(JS API)

安装

1
npm install @amap/amap-jsapi-loader

引入

ES Module:

1
2
3
4
5
6
7
8
9
10
11
12
import AMapLoader from '@amap/amap-jsapi-loader'
// 初始化地图时要配置key,和密钥
// 因为我没配置密钥导致我的提示一直不出来,搞了好久才成功
AMapLoader.load({
"key": "你的高德地图API密钥", // 请替换成你的高德地图API密钥
"version": "2.0", // API版本号
"plugins": [] // 需要使用的插件列表
}).then((AMap) => {
// 高德地图API已成功加载,现在可以通过AMap对象进行地图开发了
}).catch(e => {
console.log(e); // 加载错误
});
1
2
3
4
5
6
7
8
// 在index.html中通过CDN的方式引入
<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode:'安全密钥',
}
</script>
<script src="https://webapi.amap.com/maps?v=2.0&key=我的key"></script>
<script src="https://webapi.amap.com/ui/1.1/main.js"></script>
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
//使得id为inputId的input元素可以实现搜索提示
const initAutocomplete = (inputId) => {
// 延迟执行,确保DOM元素已经渲染
nextTick(() => {
AMap.plugin('AMap.AutoComplete', () => {
const autoOptions = {
input: inputId
};
const autoComplete = new AMap.Autocomplete(autoOptions);
//选中某个poi之后的操作,获取poi的经纬度用于后续路径规划
autoComplete.on('select', (e) => {
console.log('select', e)
if (inputId === 'tipinput1') {
startPoint.value = e.poi.name;
startCoor.value.lng = e.poi.location.lng;
startCoor.value.lat = e.poi.location.lat;
// startCity.value = e.poi.sdcode;
} else {
endPoint.value = e.poi.name;
endCoor.value.lng = e.poi.location.lng;
endCoor.value.lat = e.poi.location.lat;
// endCity.value = e.poi.adcode ;
}
});
});
});
};

路径规划(web 服务)

只需处理数据,把输入数据根据api要求拼接成想要url即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//封装好的axios
import instance from "./outServeRequest.js"
// 访问高德地图api,获取数据;v3有polyline,v5没有
const url = '/amap/v3/direction/'
const key = '我的key'
export function getRoute(routeOpt,originCoor,destinationCoor,) {
let routeUrl = ''
if (routeOpt === 'integrated') {
//后面两个city的值是城市的adcode
routeUrl = `${url}/transit/${routeOpt}?&key=${key}&origin=${originCoor.lng},${originCoor.lat}&destination=${destinationCoor.lng},${destinationCoor.lat}&city=110000&cityd=110000`
} else {
routeUrl = `${url}${routeOpt}?&key=${key}&origin=${originCoor.lng},${originCoor.lat}&destination=${destinationCoor.lng},${destinationCoor.lat}`
}
return routeUrl
}
// 发起get请求
export function getRouteData(routeOpt,originCoor,destinationCoor) {
const routeUrl = getRoute(routeOpt,originCoor,destinationCoor)
return instance.get(routeUrl)
}

使用api时常看控制台的网络服务,看看请求是否发送,响应是什么。如果发生错误可以根据响应代码对照高德地图api提供的错误代码查看错误类型()

前后端的跨域问题

后端跨域问题配置,只能解决后端提供的api

而第三方提供的api修改后端跨域无法解决,由于我用的打包工具时vite,在解决第三方跨域问题时需要配置vite.config.js,设置代理为'/amap'。配置之后前端api访问时的地址也需要更改需要使用代理,'/amap/v3/direction'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vite.config.js
export default defineConfig({
server: {
proxy: {
// 代理所有到 /amap 的请求到 https://restapi.amap.com
'/amap': {
//第三方地址,这里用amap代理了,那么请求的地址就应该时amap开头
target: 'https://restapi.amap.com', // 目标地址
changeOrigin: true, // 必须设置为true,以便代理服务器发送请求时更改Origin头信息
rewrite: (path) => path.replace(/^\/amap/, ''), // 将请求地址中的 /amap 替换为空,因为实际的API地址中不包含 /amap
secure: false,
xfwd: true,
}
}
}
})
1
2
3
4
5
6
前端使用代理
const url = `/amap/v3/assistant/coordinate/convert?key=${key}&locations=${locations}&coordsys=gps`

不使用代理前的url为
'https://restapi.amap.com/v3/assistant/coordinate/...'

坐标系转换

高德地图的坐标系和cesium的坐标系是不同的,当然不同数据的坐标系都有差异,我们需要进行坐标系的转换。
可以借助现有的库,我用的是gcoord(https://github.com/hujiulong/gcoord)

安装

npm 安装

1
npm install gcoord --save

或者直接在页面中通过 script 标签引入:

1
<script src="https://unpkg.com/gcoord/dist/gcoord.global.prod.js"></script>

引入

CommonJS:

1
const gcoord = require('gcoord');

ES Module:

1
import gcoord from 'gcoord';

通过 script 标签引入可以直接使用全局变量 gcoord 或 window.gcoord

transform API

transform(input, from, to)

进行坐标转换

参数

  • input GeoJSON | string | Array GeoJSON对象,或GeoJSON字符串,或经纬度数组
  • from CRS 当前坐标系
  • to CRS 目标坐标系

返回值

GeoJSON | Array

示例

1
2
3
// 将GCJ02坐标转换为WGS84坐标
var result = gcoord.transform([123, 45], gcoord.GCJ02, gcoord.WGS84);
console.log(result); // [122.99395597, 44.99804071]
1
2
3
4
5
6
7
// 转换GeoJSON坐标
var geojson = {
"type": "Point",
"coordinates": [123, 45]
}
gcoord.transform(geojson, gcoord.GCJ02, gcoord.WGS84);
console.log(geojson.coordinates); // [122.99395597, 44.99804071]

返回数组或GeoJSON对象(由输入决定),注意:当输入为GeoJSON时,transform会改变输入对象

前一种输入为数组,后一种为一个geojson,geojson的coordinates可以是二维数组

CRS为坐标系,目标支持以下几种坐标系

CRS 坐标格式 说明
gcoord.WGS84 [lng,lat] WGS-84坐标系,GPS设备获取的经纬度坐标
gcoord.GCJ02 [lng,lat] GCJ-02坐标系,google中国地图、soso地图、aliyun地图、mapabc地图和高德地图所用的经纬度坐标
gcoord.BD09 [lng,lat] BD-09坐标系,百度地图采用的经纬度坐标
gcoord.BD09LL [lng,lat] 同BD09
gcoord.BD09MC [x,y] BD-09米制坐标,百度地图采用的米制坐标,单位:米
gcoord.BD09Meter [x,y] 同BD09MC
gcoord.Baidu [lng,lat] 百度坐标系,BD-09坐标系别名,同BD-09
gcoord.BMap [lng,lat] 百度地图,BD-09坐标系别名,同BD-09
gcoord.AMap [lng,lat] 高德地图,同GCJ-02
gcoord.WebMercator [x,y] Web Mercator投影,墨卡托投影,同EPSG3857,单位:米
gcoord.WGS1984 [lng,lat] WGS-84坐标系别名,同WGS-84
gcoord.EPSG4326 [lng,lat] WGS-84坐标系别名,同WGS-84
gcoord.EPSG3857 [x,y] Web Mercator投影,同WebMercator,单位:米
gcoord.EPSG900913 [x,y] Web Mercator投影,同WebMercator,单位:米

具体实现

踩坑总结

二叉树

二叉树

二叉树分类

二叉树遍历

  • 前序:根左右
  • 中序:左根右
  • 后序:左右根

递归法

递归算法的三个要素。每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!

确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。

确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。

确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
前序方法1
var preorderTraversal = function(root) {
let res = []
const dfs = ()=>{
if (root === null) return
res.push(root.val)
dfs(root.left)
dfs(root.right)
}
//调用
dfs(root)
return res
};
前序方法2:
var preorderTraversal = function (root) {
return root
? [
root.val,
...preorderTraversal(root.left),
...preorderTraversal(root.right)
]
: []
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
中序方法1:
var inorderTraversal = function(root) {
let res=[]
const dfs = (r)=>{
if (r===null) return
dfs(r.left)
res.push(r.val)
dfs(r.right)
}
dfs(root)
return res
};
中序方法2:
var inorderTraversal = function (root) {
return root
? [
...inorderTraversal(root.left),
root.val,
...inorderTraversal(root.right)
]
: []
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
后序方法1
var postorderTraversal = function(root) {
let res = []
const dfs = (r)=>{
if (r===null) return
dfs(r.left)
dfs(r.right)
res.push(r.val)
}
dfs(root)
return res
};
后序方法2
var postorderTraversal = function (root) {
return root
? [
...postorderTraversal(root.left),
...postorderTraversal(root.right),
root.val
]
: []
}

迭代法–栈

图 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
前序
// 入栈 右 -> 左
// 出栈 中 -> 左 -> 右
var preorderTraversal = function (root) {
let res = []
if (!root) return res
let queue = [root]
while(queue.length){
cur = queue.pop()
res.push(cur.val)
cur.right && queue.push(cur.right)
cur.left && queue.push(cur.left)
}
return res
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 入栈 左 -> 右
// 出栈 中 -> 右 -> 左 结果翻转
后序
var postorderTraversal = function (root) {
let res = []
if (!root) return res
let stack = [root]
while(stack.length){
cur = stack.pop()
res.push(cur.val)
cur.left && stack.push(cur.left)
cur.right && stack.push(cur.right)
}
return res.reverse()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
中序
var inorderTraversal = function (root) {
// 入栈:左->右
// 出栈:左 中 右
let res = []
if (!root) return res
const stack = []
cur = root
while (cur || stack.length){
if (cur){
stack.push(cur)
cur = cur.left
}
else{
cur = stack.pop()
res.push(cur.val)
cur = cur.right
}
}
return res
}

观察上述代码发现,前后序可以通过调整代码顺序,实现.但中序却喝前后序的方法不同,那有没有统一的迭代方法呢?

我们以中序遍历为例,在二叉树:听说递归能做的,栈也能做! (opens new window)中提到说使用栈的话,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。

那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。

如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var inorderTraversal = function (root) {
// 入栈:左->右
// 出栈:左 中 右
let res = []
const stack = []
if (root) stack.push(root)
while (stack.length) {
const node = stack.pop()
if (!node) {
res.push(stack.pop().val)
continue
}
if (node.right) stack.push(node.right) // 右
stack.push(node) // 中
stack.push(null)
if (node.left) stack.push(node.left) // 左
}
return res
}

回文子串

回文子串

https://leetcode.cn/problems/palindromic-substrings/description/

图 0

思路

  • 1、dp[i][j] 表示以i开始,j结尾的字符串是否是回文子串
  • 2、递推公式:
    • if (s[i]==s[j])
      • if (i==j) return true
      • if (j-i==1) return true
      • if (j-i>1) return dp[i+1][j-1]
    • else dp[i][j] = false
  • 3、初始化:统统false
  • 4、遍历:i倒序,j正序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
超时了,129/132
var countSubstrings = function(s) {
let num = 0
function isHuiwen(t) {
let p = 0;q = t.length-1
while (p<q){
if (t[p] != t[q]){
return false
}
p++;
q--
}
return true
}
for (let i=0;i<s.length;i++){
for (let j=i+1;j<=s.length;j++){
console.log(isHuiwen(s.substring(i, j)))
if (isHuiwen(s.substring(i,j))) num ++
}
}
return num
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
动态规划
var countSubstrings = function(s) {
let num = 0
let n = s.length
let dp = Array.from({length:n},()=>Array(n).fill(false))
for(let i=n-1;i>=0;i--){
//j是尾巴,尾巴j大于等于头i
for(let j=i;j<n;j++){
if (s[i]==s[j]){
if (j-i>1) {
dp[i][j] = dp[i+1][j-1]
}
else dp[i][j] = true
}
if (dp[i][j] === true) num++
}
}
console.log(dp)
return num
};

最长回文子序列

https://leetcode.cn/problems/longest-palindromic-subsequence/description/

图 1

思路

  • 1、dp[i][j]表示 以i开头j结尾的字符串中最长回文子序列的长度
  • 2、递推公式:
    • if (s[i]==s[j]) dp[i][j] = dp[i+1][j-1]+2
    • else max(dp[i+1][j],dp[i][j-1]),分别把s[j]和s[i]加入后的最长回文子序列的长度。
  • 3、初始化:
  • 遍历顺序,i倒序,j正序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var longestPalindromeSubseq = function(s) {
let n = s.length
let dp = Array.from({length:n},()=>Array(n).fill(0))
for (let i=0;i<n;i++){
dp[i][i] =1
}
for(let i=n-1;i>=0;i--){
for(let j=i+1;j<n;j++){
if (i==j) dp[i][j] = 1
if (s[i]==s[j]) dp[i][j] = dp[i+1][j-1] + 2
else dp[i][j] = Math.max(dp[i+1][j],dp[i][j-1])
}
}
return dp[0][n-1]
};

编辑距离

编辑距离类问题

判断子序列

https://leetcode.cn/problems/is-subsequence/description/

图 6

思路
这题和之前的最长公共子序列有什么关系呢?
关系在于,这题的最长公共子序列长度和S1的长度相等说明有关系。

这也是编辑距离的入门题目,下面是编辑距离的基本思路

  • 1、二维dp数组,dp[i][j],以i-1结尾的s1和以j-1结尾的s2的最长公共子序列。
  • 2、递推公式:
    • if (s1[i-1]==s2[i-2]) dp[i-1][j-1]+1;
    • else dp[i][j] = max(dp[i-1][j],dp[i][j-1])
  • 3、初始化:均为0
  • 4、遍历顺序:
1
2
3
4
5
6
7
8
9
10
11
12
13
var isSubsequence = function(s, t) {
let n = s.length
let m = t.length
let dp = Array.from({length:n+1},()=>Array(m+1).fill(0))
for (let i=1;i<=n;i++){
for(let j=1;j<=m;j++){
if(s[i-1]==t[j-1]) dp[i][j] = dp[i-1][j-1] + 1
else dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1])
}
}
if (dp[n][m] == n) return true
return false
};

不同的子序列

https://leetcode.cn/problems/distinct-subsequences/description/

图 3

思路
这道题目如果不是子序列,而是要求连续序列的,那就可以考虑用KMP。

这道题目相对于72. 编辑距离,简单了不少,因为本题相当于只有删除操作,不用考虑替换增加之类的。

这道题可以理解为怎么删除元素可以由s变为t;

  • 1、二维dp数组,dp[i][j]以i-1为结尾的s中以j-1为结尾的t的个数
  • 2、递推公式:
    • if (s[i-1]==t[j-1]) dp[i][j] = dp[i-1][j-1] +dp[i-1][j],使用s[i-1]这个元素+不使用s[i-1]这个元素
    • else dp[i][j] = dp[i-1][j],模拟删除当前元素,也就是不考录当前的元素。
  • 3、初始化:
    • dp[i][0] = 1(); t是空字符串的话,把s中元素全部删除可以得到t
    • dp[0][j] = 0; s为空字符串,t不可能出现在s中,所以未0
    • dp[0][0]= 1; s和t都为空字符串的话,只有一种可能
  • 4、遍历顺序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var numDistinct = function(s, t) {
let n = s.length
let m = t.length
let dp = Array.from({length:n+1},()=>Array(m+1).fill(0))
for (let i=0;i<=n;i++) {dp[i][0]=1}
for (let i=1;i<=n;i++){
for (let j=1;j<=m;j++){
//相等,可以选择使用s[i-1]或者不使用s[i-1]
if (s[i-1]==t[j-1]) dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
else dp[i][j] = dp[i-1][j] //不相等说明s[i-1]用不了得删掉。
}
}
return dp[n][m]
};

两个字符串的删除操作

https://leetcode.cn/problems/delete-operation-for-two-strings/description/

图 4

思路1
求出最长公共子序列,然后用n+m-2x,n,m,s分别表示字符串1、字符串2和最长公共子字符串的长度。

1
2
3
4
5
6
7
8
9
10
11
12
var minDistance = function(word1, word2) {
let n = word1.length
let m = word2.length
let dp = Array.from({length:n+1},()=>Array(m+1).fill(0))
for (let i=1;i<=n;i++){
for(let j=1;j<=m;j++){
if (word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1] + 1
else dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1])
}
}
return n+m-2*dp[n][m]
};

思路2

  • 1、二维dp数组,dp[i][j]以i-1为结尾的word1和以j-1为结尾的word2相同需要的最少操作。
  • 2、递推公式:始终记得dp数组的含义
    • if (word1[i-1] == word2[i-1]) dp[i][j] = dp[i-1][j-1],如果相等说明不需要操作。
    • else dp[i][j] = min(dp[i-1][j]+1,dp[i][j-1]+1,dp[i-1][j-1]+2)
    • 由于dp[i][j-1]= dp[i-1][j-1]+1
  • 3、初始化:dp[i][0] = i,dp[0][j] = j,dp[0][0] = 0
  • 遍历顺序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var minDistance = function(word1, word2) {
let n = word1.length
let m = word2.length
let dp = Array.from({length:n+1},()=>Array(m+1).fill(0))
//这两个for循环用于初始化dp数组
for (let i=0;i<=n;i++){
dp[i][0] = i
}
for (let j=0;j<=m;j++){
dp[0][j] = j
}
for (let i=1;i<=n;i++){
for(let j=1;j<=m;j++){
if (word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1]
else dp[i][j] = Math.min(dp[i-1][j]+1,dp[i][j-1]+1)
}
}
return dp[n][m]
};

编辑距离

https://leetcode.cn/problems/edit-distance/description/

图 5

思路
看起来好复杂哦。

题目要求讲word1转为word2的最少操作次数,但实际上我们可以从word2转为word3,或者word1和word2同时操作都可以

  • 1、dp[i][j]: 以i-1为结尾的word1和j-1为结尾的word2的最少的操作次数

  • 2、递推公式:

    • if (word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1],相同不需要操作

    • else dp[i][j] = min(dp[i][j-1] + 1,dp[i-1][j]+1,dp[i-1][j-1]+1)

      • 上操作了
      • 增:word2[i-1]增加一个字符, dp[i][j] = dp[i][j-1] + 1
      • 删:删掉word1[i-1] ,dp[i][j] = dp[i-1][j]+1
      • 替换:dp[i][j] = dp[i-1][j-1]+1
        eg: word1 = ‘ab’ , word2 = ‘a’ , 可以是ab删掉b,也可以a增加b。dp数组如下图所示意的:
      a d
      1
      2
      3
      4
      5
      6
      7
        +-----+-----+             +-----+-----+-----+
      | 0 | 1 | | 0 | 1 | 2 |
      +-----+-----+ ===> +-----+-----+-----+
      a | 1 | 0 | a | 1 | 0 | 1 |
      +-----+-----+ +-----+-----+-----+
      d | 2 | 1 |
      +-----+-----+
  • 3、初始化:

    • dp[i][0] = i
    • dp[0][j] = j
  • 4、遍历顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var minDistance = function(word1, word2) {
let n= word1.length
let m = word2.length
let dp = Array.from({length:n+1},()=>Array(m+1).fill(0))
for (let i=0;i<=n;i++){
dp[i][0] = i
}
for (let j=0;j<=m;j++){
dp[0][j] = j
}
for (let i=1;i<=n;i++){
for (let j=1;j<=m;j++){
if (word1[i-1]==word2[j-1]) dp[i][j] = dp[i-1][j-1]
else dp[i][j] = Math.min(dp[i][j-1]+1,dp[i-1][j]+1,dp[i-1][j-1]+1)
}
}
return dp[n][m]
};

博客域名配置

博客域名配置

域名购买

我在阿里云买了域名10元一个,因为我已经不是新用户了,呜呜呜呜

1、注册并登录

2、查询域名,挑一个喜欢的实惠的买下来。

直接这样操作下来就会发现付款的时候有提示说,没有实名认证。但也可以付款只不过要在付款后进行实名认证,认证成功后完成过户才正常使用。

  • 认证的话跟着指引一步步来就行
  • 过户的操作:进入我的域名控制台–>域名列表–>管理–>域名持有者信息修改(过户)–>提交申请,通过后会有短信提醒。
    图 0
    图 1
    图 2

3、域名可以正常使用之后要取配置github pages和域名的解析

3.1、github pages:

打开仓库–>点击新建一个文件–>新建一个名为CNAME的为文件,文件内容为刚申请的域名–>保存修改后,打开设置–>修改名字(个人觉得这步可以去掉,只要后面解析的时候保证名字一样即可)–>点击左边导航栏的Pages–>在Custom domain中输入自己的域名。
不过这里肯定解析不出来啦,因为还没配置域名解析呢(见3.2)!
图 4
图 5
图 3
图 6
图 8

3.2、配置域名解析

去阿里云平台,找到域名控制台的域名列表,点击解析;配置图见下面图2,图3的名字和图2中配置保持一致。然后再到pages页面稍等片刻就会发现DNS check成功了,说明可以直接通过域名访问博客了。
图 9
图 10
图 11
图 12
图 13

注意
为了防止每次hexo d都需要去github pages重新配置域名。需要在本地项目的source文件夹下新建一个CNAME去配置你的域名。

图 14