面试算法题

合并两个有序数组

图 0

解决思路

逆向双指针,从后往前比较两个数组的元素大小。大的填入应在的位置,然后指针前移,小的不作操作。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 逆向双指针的方法
var merge = function(nums1, m, nums2, n) {
let p1 = m-1,p2 = n-1,p = m+n-1 //三个指针,第一个用来遍历数组1中的每个值。
//第二个用来遍历数组2中的每一个值,第三个用来存数据,从数组1的最后一个位置开始。
while(p1>=0 && p2>=0){ // 双指针,从两个数组的最后一个数开始,倒着比较值的大小。
if (nums2[p2]>=nums1[p1]){
nums1[p] = nums2[p2]
p2--
}
else{
nums1[p] = nums1[p1]
p1--
}
p--
}
while(p2>=0){//如果数组2中还有剩余的元素,全部放在数组1的前面
nums1[p] = nums2[p2]
p2--
p--
}
};

两数之和

图 1

解题思路

使用字典,判断目标值减去当前值的结果是否,存在于字典中。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
var twoSum = function(nums, target) {
let map = new Map()
// 判断target-当前元素的结果是否存在于map中,如果存在返回下标
for (let i=0;i<nums.length;i++){
if (map.has(target-nums[i])){
return [map.get(target-nums[i]),i]
}
//如果不存在,设置当前元素为键,当前元素下表为值
else{
map.set(nums[i],i)
}
}
}

三数之和

图 2

解决思路回溯算法:

1、回溯的三部曲,参数确定,确定终止条件,单层递归逻辑。提升效率要剪枝
2、双指针法:对数组进行排序,先确定第一个数字,然后使用双指针分别总数组的第一个和第最后一个开始,判断当前总和,综合大,右边指针前移,否者左边指针后移。

去重逻辑:第一个数字的去重,和后面两个数字的去重。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var threeSum = function (nums) {
let res = []
nums = nums.sort((a,b)=>a-b) //首先要进行排序
for (let i = 0; i < nums.length-2; i++) { //减2是因为求得是三个元素的和。
if (i > 0 && nums[i] == nums[i - 1]) continue //a去重
let left = i + 1,right = nums.length - 1 //指定双指针
while (left < right) {
let sum = nums[i]+nums[left]+nums[right]
if (sum>0) right--
if (sum<0) left++
if(sum===0){
res.push([nums[i],nums[left],nums[right]])
//后两个数去重逻辑,如果指针指向下一个元素与当前相同,跳过该元素
while(left<right && nums[left] === nums[left+1]) left++
while(left<right && nums[right] === nums[right-1]) right--
left++ //忘记调整这两个了
right--
}
}
}
return res
}

反转链表

图 3

自定义链表

解题代码

1
2
3
4
5
6
7
8
9
10
11
12
var reverseList = function(head) {
let prev = null
let cur = head
while(cur){
// 这一步我忘记了,没有把next的值保存下来,直接给cur.next赋值了
const next = cur.next
cur.next = prev
prev = cur
cur = next
}
return prev //最后一个cur是null,他前面一个才是第一个有值的节点
};

二叉树遍历

排序算法

快速排序

其核心思想是分治,选择一个基准元素,将比基准小的元素放在左边,比基准大的元素放在右边。然后地柜对左右两个部分,分别进行快速排序。

快速排序复杂度
平均时间复杂度(Onlogn)、最坏情况O(n^2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function quickSort(arr) {
//终止条件
if (arr.length <= 1) return arr
//找基准,选择最后一个元素作为基准
let std = arr.length - 1
//用来存储左右两个数组
let leftArr = [],
rightArr = []
for (let i = 0; i < std && i != std; i++) {
if (arr[i] <= arr[std]) leftArr.push(arr[i])
else rightArr.push(arr[i])
}
//分别处理左右两个子数组
return [...quickSort(leftArr), arr[std], ...quickSort(rightArr)]
}
console.log(quickSort([5, 2, 9, 1, 7, 6,3,80,34]))

选择排序

冒泡排序

插入排序

堆排序

归并排序

归并排序是另一种分治算法,它将数组递归地拆分为两个子数组,直到每个子数组只有一个元素。然后将这些子数组有序地合并,直到所有元素合并成一个有序数组。

归并排序是一种稳定排序,而且它的最坏情况复杂度和平均复杂度都是 O(n log n)。

单调栈

单调栈

何时用单调栈

通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。时间复杂度为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

子序列

子序列与动态规划问题

最长递增子序列

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

图 0

思路
本题也是代码随想录中子序列问题的第一题,如果没接触过这种题目的话,本题还是很难的,甚至想暴力去搜索也不知道怎么搜。 子序列问题是动态规划解决的经典问题,当前下标i的递增子序列长度,其实和i之前的下表j的子序列长度有关系,那又是什么样的关系呢。

  • 1、dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度
    为什么一定表示 “以nums[i]结尾的最长递增子序” ,因为我们在做递增比较的时候,如果比较 nums[j] 和 nums[i] 的大小,那么两个递增子序列一定分别以nums[j]为结尾 和 nums[i]为结尾, 要不然这个比较就没有意义了,不是尾部元素的比较那么 如何算递增呢。
  • 2、状态转移方程
    • if (nums[i]>nums[j]) dp[i] = max(dp[i],dp[j]+1)这里的含义是,取dp[j]+1的最大值。
  • 3、初始化,子序列的长度最短为1
  • 4、遍历顺序,外层遍历i(1 ~ n),内层遍历j(0 ~ i-1)
  • 还要注意并不是dp[n-1]才是最终的值,最大值应该是dp数组中的最大值
1
2
3
4
5
6
7
8
9
10
11
12
var lengthOfLIS = function(nums) {
let n = nums.length
let dp = Array(n).fill(1)
let result = 1
for(let i=1;i<n;i++){
for(let j=0;j<i;j++){
if (nums[i]>nums[j]) dp[i] = Math.max(dp[i],dp[j]+1)
}
if (dp[i]>result) result = dp[i]
}
return result
};

最长递增子序列

https://leetcode.cn/problems/longest-continuous-increasing-subsequence/description/

图 1

思路
这里的序列要连续,因此当前的状态要由前一个决定

  • 1、dp[i]:以下标i为结尾的连续递增的子序列长度为dp[i]
  • 2、if(nums[i]>nums[i-1]) dp[i] = dp[i-1]+1
  • 3、初始化dp[i]都等于1
  • 4、循环,只需要一层循环遍历i即可
  • 当然同样注意最终答案不是dp[n-1],而是dp数组中的最大值
1
2
3
4
5
6
7
8
9
10
var findLengthOfLCIS = function(nums) {
let n = nums.length
let dp = Array(n).fill(1)
let result =1
for(let i=1;i<n;i++){
if(nums[i]>nums[i-1]) dp[i] = dp[i-1]+1
if (dp[i]>result) result = dp[i]
}
return result
};

最长重复子数组

https://leetcode.cn/problems/maximum-length-of-repeated-subarray/description/

图 2

难点
动态数组怎么表示,又是什么含义

思路
如果要暴力破解的话,把每个数组的子数组遍历出来,然后两两比较,得出最长重复子数组

  • 1、二维dp[i][j],以i-1为结尾的nums[1]和以j-1为结尾的nums[2],的最长重复子数组的长度;本质上也是两两比较了,但是由于我记录了前一个状态的值,在进行下一个状态比较时,子需要对比一个数即可结合前一个状态得出现在状态的值。

    • 为什么i,j表示下标为i-1和j-1呢?因为有dp[i-1],dp[j-1],因此i和j的最小值应该为1。dp数组遍历时候从1开始,但是nums数组要从0号元素开始比较。
  • 2、递推公式:if(nums[i-1]==nums[j-1]) dp[i][j] = dp[i-1][j-1] +1;
    图 4

  • 3、初始化:dp[i][0] = 0,dp[0][j] = 0;因为i和j遍历时都从1开始

  • 4、遍历顺序:

  • 这题也是结果在过程中,而不是遍历完之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
var findLength = function(nums1, nums2) {
let n = nums1.length
let m = nums2.length
let dp = Array.from({length:n+1},()=>Array(m+1).fill(0))
let result = 0
for (let i=1;i<=n;i++){
for(let j=1;j<=m;j++){
if(nums1[i-1] == nums2[j-1]) dp[i][j]=dp[i-1][j-1]+1
if (dp[i][j] > result) result = dp[i][j]
}
}
return result
};

最长公共子序列

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

图 3

思路
这道题就是可以不连续,可以不连续前面一维数组做过,不过现在dp需要时二维数组,因此这题主要突破点再递推公式上

  • 1、dp[i][j]表示以i-1为结尾的第一个字符串和j-1结尾的第二个字符串的最长公共子序列长度

  • 2、递推公式:主要就是两大情况: text1[i - 1] 与 text2[j - 1]相同,text1[i - 1] 与 text2[j - 1]不相同。

    • if (text1[i-1]==text2[j-1]) dp[i][j] = dp[i-1][j-1]+1;
    • else dp[i][j] = max(dp[i-1][j],dp[i][j-1])
  • 3、初始化,均为0即可,

  • 4、遍历顺序,那么为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。
    图 5

1
2
3
4
5
6
7
8
9
10
11
12
var longestCommonSubsequence = function(text1, text2) {
let n = text1.length
let m = text2.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 (text1[i-1]==text2[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 dp[n][m]
};

不相交的线

https://leetcode.cn/problems/uncrossed-lines/description/

图 2

思路
其实也就是说A和B的最长公共子序列是[1,4],长度为2。 这个公共子序列指的是相对顺序不变(即数字4在字符串A中数字1的后面,那么数字4也应该在字符串B数字1的后面)

这么分析完之后,大家可以发现:本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!

1
2
3
4
5
6
7
8
9
10
11
12
var maxUncrossedLines = function(nums1, nums2) {
let n = nums1.length
let m = nums2.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 (nums1[i-1]==nums2[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 dp[n][m]
};