0%

Vue.js

vue官方文档 https://cn.vuejs.org/guide/introduction.html

基于vue方法文档的学习笔记,初学时主要记录基础知识,深度学习后希望能加上自己的理解!

更新时间

2024.05.18第一次更新

什么是Vue

一款用于构建用户界面的 (JavaScript )渐进式框架框架

两个功能:

  • 声明式渲染:vue的模板语法使得我们可以声明式地描述HTML和JS状态之间的关系
  • 响应性:自动跟踪JS状态并响应式地更新DOM

Vue可以使用的场景

  • 无需构建步骤,渐进式增强静态的 HTML
  • 在任何页面中作为 Web Components 嵌入
  • 单页应用 (SPA)
  • 全栈 / 服务端渲染 (SSR)
  • Jamstack / 静态站点生成 (SSG)
  • 开发桌面端、移动端、WebGL,甚至是命令行终端中的界面

单文件组件(SFC——*.vue)

vue的标志性功能

1
2
3
4
5
6
7
8
<script setup>
</script>

<template>
</template>

<style scoped scoped>
</style>

Vue组件的书写风格

选项式API (Option API)

  • 用对象来描述组件逻辑
  • 对象包括data、methos、mouted等属性,这些属性都是可选式的(我自己的理解,不一定对)
  • 选项定义的属性会暴露在函数内部的this上(即可以通过this访问到这个属性),this指向当前组件的实例
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
<script>
export default {
// data() 返回的属性将会成为响应式的状态
// 并且暴露在 `this` 上
data() {
return {
count: 0
}
},

// methods 是一些用来更改状态与触发更新的函数
// 它们可以在模板中作为事件处理器绑定
methods: {
increment() {
this.count++
}
},

// 生命周期钩子会在组件生命周期的各个不同阶段被调用
// 例如这个函数就会在组件挂载完成后被调用
mounted() {
console.log(`The initial count is ${this.count}.`)
}
}
</script>

<template>
<button @click="increment">Count is: {{ count }}</button>
</template>

组合式API(Composition API)

  • 可以使用导入的API函数描述组件逻辑
  • 组合式API与<script setup>搭配使用,其中setup是一个标识,使得我们可以更简洁地使用组合式API(会在编译时做一些处理)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import { ref, onMounted } from 'vue'

// 响应式状态
const count = ref(0)

// 用来修改状态、触发更新的函数
function increment() {
count.value++
}

// 生命周期钩子
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
<button @click="increment">Count is: {{ count }}</button>
</template>

两者之间的异同

  • 选项式API基于组件式API
  • 选项式有面对对象的思想,对初学者更友好,强制按照选项来组织代码
  • 组件式的核心思想是直接在函数作用域内定义响应式状态变量,并从多个函数中得到的状态组合起来处理复杂问题。更自由、灵活,但更难理解(确实,我不太能理解)

互动教程(组件式API+SFC)

响应式变量声明方式

说的明白点,就是动态的数据绑定,在reactive或ref中声明的变量可以响应式地用在html中

reactive()声明

  • reactive只适用于对象(包括数组和内置类型,如Map和Set)
  • reactive创建的对象时JS Proxy,行为与普通对象一致
1
2
3
4
5
6
7
8
import { reactive } from 'vue'

const counter = reactive({
count: 0
})

console.log(counter.count) // 0
counter.count++

ref()

  • ref接收任意类型数据
  • 返回值是一个对象,可以通过对象.value属性访问数据
1
2
3
4
5
6
import { ref } from 'vue'

const message = ref('Hello World!')

console.log(message.value) // "Hello World!"
message.value = 'Changed'

在模板template中使用响应式状态

响应式状态暂时我喜欢理解为响应式变量

  • 使用{{}}使用,并且ref中的对象的value可以不用message.value去访问,而是可以使用message直接访问(因为会被自动解包)
  • {{}}中不限制于变量名,也可以是表达式
1
2
3
4
5
6
//变量写法
<h1>{{ message }}</h1>
<p>Count is: {{ counter.count }}</p>

//表达式写法
<h1>{{ message.split('').reverse().join('') }}</h1>

Attribute绑定(v-bind)

  • Attribute n.属性,特质,在编程中通常用来描述数据对象的特征

  • v-bind用于绑定一个动态值,时v-开头的一种特殊Attribute

  • 绑定的值可以是calss,可以是id,也可以是一些参数

  • 可以简写为:

1
2
3
4
5
<div v-bind:id="dynamicId"></div>

//语法糖
<div :id="dynamicId"></div>

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { ref } from 'vue'

const titleClass = ref('title')
</script>

<template>
<h1 :class="titleClass">Make me red</h1>
</template>

<style>
.title {
color: red;
}
</style>

事件监听(v-on)

  • 使用v-on监听DOM事件

  • 简写为@

    1
    2
    3
    4
    5
    6
    <template>
    <button v-on:click="increment">{{ count }}</button>
    //简写
    <button @click="increment">{{ count }}</button>

    </template>
  • 在script中声明回调函数increment

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <script setup>
    import { ref } from 'vue'

    const count = ref(0)

    function increment() {
    // 更新组件状态
    count.value++
    }
    </script>

表单的双向绑定

使用v-bind+v-on

当v-on监听到表单内容的变化,就使用回调函数获取到表单的新内容,更新数据后,重新响应在v-bind绑定的组件上

使用v-model(常用于表单、单选、多选、下拉框)

  • v-model是实质是上述方法的语法糖
  • V-model将绑定的值与input中的值自动同步,不需要在使用事件处理函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//用法
<input v-model="text">


//例子
<script setup>
import { ref } from 'vue'

const text = ref('')

</script>

<template>
<input v-model="text" placeholder="Type here">
<p>{{ text }}</p>
</template>

条件渲染(v-if)

  • v-if,只有在awesome为true时,h1标签才会被渲染
  • v-else-ifv-else用法与JS中if-else if-else的用法基本一致
1
2
3
4
5
6
//v-if
<h1 v-if="awesome">Vue is awesome!</h1>

//v-else
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>

列表渲染(v-for)

  • v-for用于循环渲染

  • 给每个 todo 对象设置了唯一的 id,并且将它作为特殊的 key attribute 绑定到每个 <li>key 使得 Vue 能够精确的移动每个 <li>,以匹配对应的对象在数组中的位置。

1
2
3
4
5
6
7
8
9
10
11
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
</li>
</ul>

//在数组中新增数据
todos.value.push(newTodo)

//在数组中删除数据,过滤数据
todos.value = todos.value.filter(/* ... */)

案例

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
<script setup>
import { ref } from 'vue'

// 给每个 todo 对象一个唯一的 id
let id = 0

const newTodo = ref('')
const todos = ref([
{ id: id++, text: 'Learn HTML' },
{ id: id++, text: 'Learn JavaScript' },
{ id: id++, text: 'Learn Vue' }
])

function addTodo() {
// 新增todo
if(newTodo.value != ''){
console.log(newTodo.value);
todos.value.push({
id: id++,
text: newTodo.value
});
}
newTodo.value = ''//添加完数据后要重置为空
}

function removeTodo(todo) {
// 删除todo
todos.value = todos.value.filter((item)=>{
return item!=todo;
});

}
</script>

<template>
<form @submit.prevent="addTodo">
<input v-model="newTodo" required placeholder="new todo">
<button>Add Todo</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
<button @click="removeTodo(todo)">X</button>
</li>
</ul>
</template>

计算属性(computed)

  • 创建一个计算属性 ref,这个 ref 会动态地根据其他响应式数据源来计算其 .value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ref, computed } from 'vue'

const hideCompleted = ref(false)
const todos = ref([
/* ... */
])

const filteredTodos = computed(() => {
// 根据 `todos.value` & `hideCompleted.value`
// 返回过滤后的 todo 项目
})

- <li v-for="todo in todos">
+ <li v-for="todo in filteredTodos">

案例

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
<script setup>
import { ref, computed } from 'vue'

let id = 0

const newTodo = ref('')
const hideCompleted = ref(false)
const todos = ref([
{ id: id++, text: 'Learn HTML', done: true },
{ id: id++, text: 'Learn JavaScript', done: true },
{ id: id++, text: 'Learn Vue', done: false }
])

//当hideCompleted为true的时候,应该隐藏掉todos中done属性为ture的属性,所以过滤时返回done为false的属性的对象
const filteredTodos = computed(() => {
return hideCompleted.value
? todos.value.filter((t) => !t.done)
: todos.value
})

function addTodo() {
todos.value.push({ id: id++, text: newTodo.value, done: false })
newTodo.value = ''
}

function removeTodo(todo) {
todos.value = todos.value.filter((t) => t !== todo)
}
</script>

<template>
<form @submit.prevent="addTodo">
<input v-model="newTodo" required placeholder="new todo">
<button>Add Todo</button>
</form>
<ul>
<li v-for="todo in filteredTodos" :key="todo.id">
<input type="checkbox" v-model="todo.done">
<span :class="{ done: todo.done }">{{ todo.text }}</span>
<button @click="removeTodo(todo)">X</button>
</li>
</ul>
<button @click="hideCompleted = !hideCompleted">
{{ hideCompleted ? 'Show all' : 'Hide completed' }}
</button>
</template>

<style>
.done {
text-decoration: line-through;
}
</style>

生命周期和模板引用

  • 当我们需要手动操作DOM时,会需要使用模板引用,也就是指向模板中的一个DOM元素的ref
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<p ref="pElementRef">hello</p>
</template>


<script setup>
//使用同名ref访问该引用
const pElementRef = ref(null)


//要在挂载之后执行代码,我们可以使用 onMounted() 函数:
import { onMounted } from 'vue'
//ref引用的是一个DOM元素,在这个例子中,就是一个p标签
onMounted(() => {
// 此时组件已经挂载。
pElementRef.value.textContent = "你好"
})
</script>

此时,这个 ref 使用 null 值来初始化。这是因为当<script setup> 执行时,DOM 元素还不存在。模板引用 ref 只能在组件挂载后访问。

  • onMounted被称为生命周期钩子——它允许我们注册一个在组件的特定生命周期调用的回调函数。还有一些其他的钩子如 onUpdatedonUnmounted

侦听器(watch)

有时我们需要响应性地执行一些“副作用”——例如,当一个数字改变时将其输出到控制台。我们可以通过侦听器来实现它:

1
2
3
4
5
6
7
8
import { ref, watch } from 'vue'

const count = ref(0)

watch(count, (newCount) => {
// 没错,console.log() 是一个副作用
console.log(`new count is: ${newCount}`)
})

滑动窗口专项训练

更新记录

2024.5.17 第一次记录

一般解题步骤

待更新

1.定义需要维护的变量

可能是:

  • 哈希表(map或set)
  • 最短/最长长度

2.初始化滑动窗口,startend一般都初始化为0(如果是双指针两边夹的情况,一般都是贪心?)

解题模板

由于自己的理解还不够深刻,这里借鉴大佬的思路

https://leetcode.cn/problems/longest-substring-with-at-most-two-distinct-characters/solutions/879777/hua-dong-chuang-kou-zhen-di-jian-dan-yi-73bii

大佬的是python的版本

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
class Solution:
def problemName(self, s: str) -> int:
# Step 1: 定义需要维护的变量们 (对于滑动窗口类题目,这些变量通常是最小长度,最大长度,或者哈希表)
x, y = ..., ...

# Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
start = 0
for end in range(len(s)):
# Step 3: 更新需要维护的变量, 有的变量需要一个if语句来维护 (比如最大最小长度)
x = new_x
if condition:
y = new_y

'''
------------- 下面是两种情况,读者请根据题意二选1 -------------
'''
# Step 4 - 情况1
# 如果题目的窗口长度固定:用一个if语句判断一下当前窗口长度是否超过限定长度
# 如果超过了,窗口左指针前移一个单位保证窗口长度固定, 在那之前, 先更新Step 1定义的(部分或所有)维护变量
if 窗口长度大于限定值:
# 更新 (部分或所有) 维护变量
# 窗口左指针前移一个单位保证窗口长度固定

# Step 4 - 情况2
# 如果题目的窗口长度可变: 这个时候一般涉及到窗口是否合法的问题
# 如果当前窗口不合法时, 用一个while去不断移动窗口左指针, 从而剔除非法元素直到窗口再次合法
# 在左指针移动之前更新Step 1定义的(部分或所有)维护变量
while 不合法:
# 更新 (部分或所有) 维护变量
# 不断移动窗口左指针直到窗口再次合法

# Step 5: 返回答案
return ...

按照大佬是思路,改成JS的版本

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
/**
* @param {string} s
*/
var problemName = function(s) {
// Step 1: 定义需要维护的变量们 (对于滑动窗口类题目,这些变量通常是最小长度,最大长度,或者哈希表)
let [x, y] = ...

// Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
let st = 0;
for(let end = 0; end < s.length; s++){
//Step 3: 更新需要维护的变量, 有的变量需要一个if语句来维护 (比如最大最小长度)
x = new_x;
if(condition){
y = new_y;
}
// Step 4 - 情况1
// 如果题目的窗口长度固定:用一个if语句判断一下当前窗口长度是否超过限定长度
// 如果超过了,窗口左指针前移一个单位保证窗口长度固定, 在那之前, 先更新Step 1定义的(部分或所有)维护变量
if(窗口长度大于限定值){
// 更新 (部分或所有) 维护变量
// 窗口左指针前移一个单位保证窗口长度固定
}

// Step 4 - 情况2
// 如果题目的窗口长度可变: 这个时候一般涉及到窗口是否合法的问题
// 如果当前窗口不合法时, 用一个while去不断移动窗口左指针, 从而剔除非法元素直到窗口再次合法
// 在左指针移动之前更新Step 1定义的(部分或所有)维护变量
while(不合法){
//更新 (部分或所有) 维护变量
//不断移动窗口左指针直到窗口再次合法
}
}
// Step 5: 返回答案
return ...
};

实战训练

无重复字符的最长子串

3. 无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的最长子串的长度。

示例 1:

1
2
3
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

1
2
3
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

1
2
3
4
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

提示:

  • 0 <= s.length <= 5 * 104
  • s 由英文字母、数字、符号和空格组成

code:

不用map,用set和单纯数组都行,数组的话就用include方法来查看字母是否已经存在于数组中,但是最好还是不要数组了,数组里面删除一个元素会很麻烦,主要还是哈希+滑动窗口的思想。

用set的话会简单一点点

带注释版本

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
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
if(s.length==0) return 0;
//无重复字符的最长子串
//ex
// abcabcbb
// abc -> maxLen = 3
//滑动窗口
//1.定义需要维护的变量
let maxLen = -1;
let hash = new Map();
//2.定义窗口的首尾端,然后滑动窗口
let st = 0;
for(let end = 0; end < s.length; end++){//第一次debug是end++写成s++了
//维护变量
let c = s[end];

//情况2:窗口可变,检查窗口是否合法,不合法就调整st指针直至合法
//在该题目中,不合法指的是,字符串中出现重复字符
if(hash.has(c)){//c字符不是第一次出现,窗口不合法
//只要连续移动字符,直到新的窗口的字符中不包含第一次出现的c字符位置
while( st < end && s[st] != c){
hash.delete(s[st]);//第二次debug,这一句和下面一句的顺序反了,如果先++在delet,那么相当于delet的是下一个字符,第一个字符永远都不会被移除
st++;
}
//此时的st应该位于第一个窗口的第一个c字符处
st++;
//现在窗口合法了
}else{//第一次出现
hash.set(c, 1);
}

//窗口的长度为end - st + 1 (左闭右闭区间)
//如果是左闭右开区间,就是end - st
maxLen = Math.max(maxLen, end - st + 1);
}
return maxLen;
};

不带注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
if(s.length==0) return 0;
let maxLen = -1;
let hash = new Map();
let st = 0;
for(let end = 0; end < s.length; end++){
let c = s[end];
if(hash.has(c)){
while( st < end && s[st] != c){
hash.delete(s[st]);
st++;
}
st++;
}else{
hash.set(c, 1);
}
maxLen = Math.max(maxLen, end - st + 1);
}
return maxLen;
};

找到字符串中所有字母异位词

438. 找到字符串中所有字母异位词

给定两个字符串 sp,找到 s 中所有 p异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

示例 1:

1
2
3
4
5
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。

示例 2:

1
2
3
4
5
6
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。

Code:

窗口不合理的情况比较复杂

带注释版本

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
/**
* @param {string} s
* @param {string} p
* @return {number[]}
*/
var findAnagrams = function(s, p) {
//哈希(异位词)+滑动窗口(子串)
//异位词的特点是:
//1.长度相等
//2.每个字符的出现次数相等
let hash = new Map();
let hash2 = new Map();
let res = [];
//初始化hash
for(let i = 0; i < p.length; i++){
if(hash.has(p[i])){
hash.set(p[i], hash.get(p[i]) + 1);
}else hash.set(p[i], 1);
}
//初始化窗口,并开始滑动
let st = 0;
for(let end = 0; end < s.length; end++){
let c = s[end];
if(hash2.has(c)){
hash2.set(c, hash2.get(c) + 1);
}else hash2.set(c, 1);

//窗口不合法1,没有这个字符,两个指针都跳到这个指针后面
if(!hash.has(c)){
while(st != end + 1){//debug1,一开始没有跳转到st = end+1,只是st++,这样是不对的,而且跳转完之后,一定要记得移除hash2中前面的(已经不在滑动窗口中的)字母
hash2.set(s[st], hash2.get(s[st]) - 1);
st++;
}
}else if(hash2.get(c) > hash.get(c)){
//窗口不合法2,有这个字符,但是字符数多了,移动st,直到窗口中的c的字符数与hash中的字符数一致
while(hash2.get(c) != hash.get(c)){
hash2.set(s[st], hash2.get(s[st]) - 1);
st++;
}
}
//窗口不合法3,窗口长度超过了
while(end - st + 1 > p.length){
hash2.set(s[st], hash2.get(s[st]) - 1);//debug2,滑动了窗口,但是忘记处理hash2了
st++;
}
if(end - st + 1 === p.length){
res.push(st);
}
}
return res;
};

不带注释版本

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
/**
* @param {string} s
* @param {string} p
* @return {number[]}
*/
var findAnagrams = function(s, p) {
let hash = new Map();
let hash2 = new Map();
let res = [];
for(let i = 0; i < p.length; i++){
if(hash.has(p[i])){
hash.set(p[i], hash.get(p[i]) + 1);
}else hash.set(p[i], 1);
}
let st = 0;
for(let end = 0; end < s.length; end++){
let c = s[end];
if(hash2.has(c)){
hash2.set(c, hash2.get(c) + 1);
}else hash2.set(c, 1);
if(!hash.has(c)){
while(st != end + 1){
hash2.set(s[st], hash2.get(s[st]) - 1);
st++;
}
}else if(hash2.get(c) > hash.get(c)){
while(hash2.get(c) != hash.get(c)){
hash2.set(s[st], hash2.get(s[st]) - 1);
st++;
}
}
while(end - st + 1 > p.length){
hash2.set(s[st], hash2.get(s[st]) - 1);
st++;
}
if(end - st + 1 === p.length){
res.push(st);
}
}
return res;
};

动态规划专项训练

参考:代码随想录官⽹www.programmercarl.com

更新记录

第一次更新 2024.05.13

第二次更新 2024.05.17

一般解题步骤

  1. 确定dp数组(dp table)以及下标的含义

​ dp数组(状态转移数组)可以是一维的可以是二维的

  1. 确定递推公式 (状态转移方程)
  2. dp数组如何初始化
  3. 确定遍历顺序
  4. 举例推导dp数组

实战训练

基础问题

1.斐波那契数

509. 斐波那契数

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 01 开始,后面的每一项数字都是前面两项数字的和。也就是:

1
2
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n)

示例 1:

1
2
3
输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1

示例 2:

1
2
3
输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2

示例 3:

1
2
3
输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3

提示:

  • 0 <= n <= 30

code:

用模拟的思想做的,非dp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @param {number} n
* @return {number}
*/
var fib = function(n) {
if(n === 0) return 0;
if(n === 1) return 1;
let num1 = 0, num2 = 1;
let cur;
for(let i = 2; i <= n; i++){
cur = num1 + num2;
num1 = num2;
num2 = cur;
}
return cur;
};

用动态规划来做

  1. 确定dp数组(dp table)以及下标的含义

    dp[i]表示第i个斐波那契数

  2. 确定递推公式

    题目 中有 dp[i] = dp[i - 1] + dp[i - 2]

  3. dp数组如何初始化

    题目中有 dp[0] = 0 , dp[1] = 1

  4. 确定遍历顺序

    是从后向前还是从前向后,是一层还是两层,如果是两层,哪层在外面

  5. 打印推导dp数组(debug用的)

70. 爬楼梯

70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:

1
2
3
4
5
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

示例 2:

1
2
3
4
5
6
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

提示:

  • 1 <= n <= 45

code:

在前面知识的基础上,我大概能了解到迈上第n阶的方法其实是有前面的迈上n-1和迈上n-2阶的种数有关的,但是如果没有关键点的想,很容易想岔去。

下面的代码是我第一个自己思考的代码,是错误的,一开始想的是想的是比如想要到第4阶,有可能是dp[1] + dp[3], dp[2]+ dp[2], dp[3] + dp[1];但是如果这样想的话,所有种类中是会有重复值的。所以我也想到了要去重,但是去重的规律并没有找对;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @param {number} n
* @return {number}
*/
var climbStairs = function(n) {
//dp[i]表示爬到i阶楼梯,有dp[i]种方法
let dp = [0, 1, 2];
//初始化dp[]
//状态转移方程
for(let i = 3; i <= n; i++){
let cn = 0;
for(let k = 1; k < i; k++){
cn += (dp[k] * dp[i - k]);
}
//去除重复值
cn = cn - 2 * i + 5;
dp.push(cn);
}
return dp[n];
};

看了卡哥的视频解说,反复想了一下理解了,其实思路是可以有很多种的,但是如果想用动态规划的思路来写,关键点在于状态转移,如果从0阶开始向上迈,那么状态转移方程其实很不好把握,容易像我之前一样想岔了,会想:先走几阶有几种方法,后走几阶有几种方法,很容易把自己绕进去。

但是如果想要专注于状态转移,应该从台阶上往下看,而且必须注意状态转移的条件(即每次只能爬1或2个台阶),那么对于一个人来说,他走到n层台阶的上一个状态只可能有两种:

  1. 上一个状态是走到了n-1个台阶,他是迈了一步才走到n台阶的
  2. 上一个状态是走到了n-2个台阶,他是迈了两步才走到n台阶的

可以想到,这样递归出来的种类数其实是很干净的,不会存在重复值,因为不同于我第一次的想法,我第一次想的是(**step1:**从0-k有几种方法; **step2:**从k-n有几种方法),而动态规划中,强制了我的step2只能有一种方法。举个例子:

易得dp[1] = 1, dp[2]= 2

如果我想求dp[4]

先看看我原本的想法:可能性有先走一步,再走三步;先走二步,再走两步;先走三步,再走一步

那么这个dp[4]=dp[1]*dp[3]+ dp[2]*dp[2]+dp[3]*dp[1],但是这样的递归推导是不干净的,因为里面存在了很多的重复值

再看看动态规划的思想:如果我想根据step1+step2的思路来思考的话,首先强制step2的方法只能有一种,即当走过step1方法到达中间的某个台阶后,我只能通过一种方法来到达台阶n。

虽然step只能有一种方法,但是step可以有两种情况:即先走 2步,然后走2步到n;或者先走3步,然后走1步到n;

由此可得到:dp[4] = d[2] * 1 + d[3] * 1;

所以我才得出了dp的关键在于:到达这个状态的上一个状态是怎么样的,因为上一个状态总是通过一次操作(一次状态转移)达到这个状态,这样递归下来的数据才是干净的。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @param {number} n
* @return {number}
*/
var climbStairs = function(n) {
//dp[i]表示爬到i阶楼梯,有dp[i]种方法
let dp = [0, 1, 2];
//状态转移方程
for(let i = 3; i <= n; i++){
dp.push(dp[i - 1] + dp[i - 2]);//可以不用维护数组,直接用变量来写,写法上一题斐波那契数写法一致,不写了这里
}
return dp[n];
};

使用最小花费爬楼梯

746. 使用最小花费爬楼梯

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

示例 1:

1
2
3
4
5
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。

示例 2:

1
2
3
4
5
6
7
8
9
10
输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。

提示:

  • 2 <= cost.length <= 1000
  • 0 <= cost[i] <= 999

Code:

这道题的难点在于搞清楚ct[i]到底指的是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @param {number[]} cost
* @return {number}
*/

//ct[i]指的是到达i(此时i是楼顶)的最小消费
var minCostClimbingStairs = function(cost) {
//ct[i]达到i台阶的需要最小消费
if(cost.length==1) return cost[0];
if(cost.length==2) return Math.min(cost[0],cost[1]);
let ct1 = Math.min(cost[0],cost[1]);
let ct = [0, 0];
for(let i = 2; i <= cost.length; i++){
let cur = Math.min(ct[i - 1]+cost[i - 1], ct[i-2]+cost[i-2]);
ct.push(cur);
}
return ct[ct.length - 1];
};

不同路径【Mid】

62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

img

1
2
输入:m = 3, n = 7
输出:28

示例 2:

1
2
3
4
5
6
7
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

1
2
输入:m = 7, n = 3
输出:28

示例 4:

1
2
输入:m = 3, n = 3
输出:6
用dp来写

code

d[x][y]表示从start开始到点(x,y)有d[x][y]种走法

关键点在于只能向下走或者向右走,所以x和y都智能递增,是不可能回头的,所以对于边缘d[x][1]和d[1][y]都应该初始化为1;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @param {number} m
* @param {number} n
* @return {number}
*/
var uniquePaths = function(m, n) {
//初始化d[x][y]边缘元素为1
let d = new Array(m + 1);
for(let x = 1; x <= m; x++){
d[x] = new Array(n + 1);
if(x == 1) d[1].fill(1);
d[x][1] = 1;
}
for(let x = 2; x <= m; x++){
for(let y = 2; y <= n; y++){
d[x][y] = d[x][y - 1] + d[x - 1][y];
}
}
console.log(d);
return d[m][n];
};
DFS的基本写法

在JavaScript中,深度优先搜索(DFS)的一般写法与其他编程语言类似。下面是一个基本的DFS函数示例,用于在一个图或树结构中进行深度优先搜索:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function dfs(node, visited) {
if (visited.has(node)) {
return;
}

// 处理当前节点
visited.add(node);

// 遍历当前节点的相邻节点
for (let neighbor of node.neighbors) {
if (!visited.has(neighbor)) {
dfs(neighbor, visited);
}
}
}

// 初始化访问记录
let visited = new Set();

// 从起始节点开始进行DFS
dfs(startNode, visited);

在这段代码中,dfs()函数表示深度优先搜索的递归函数,接受一个节点node和一个记录访问情况的visited集合作为参数。在DFS过程中,首先检查当前节点是否已经被访问过,如果已经访问过则直接返回;否则将当前节点标记为已访问,并递归地对当前节点的相邻节点进行DFS遍历。

用dfs,会超时,时间复杂度会达到2^(m+n)次,所以不合适,但是思路是可行的,如果m和n较小的话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @param {number} m
* @param {number} n
* @return {number}
*/
var uniquePaths = function(m, n) {
function dfs(x, y) {
// 如果越界,则认为这条路径不可行
if (x >= m || y >= n) return 0;
// 当到达终点时,返回1
if (x === m - 1 && y === n - 1) return 1;
// 向右走 + 向下走
return dfs(x + 1, y) + dfs(x, y + 1);
}

return dfs(0, 0);
};

不同路径II

63. 不同路径 II

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 10 来表示。

示例 1:

img

1
2
3
4
5
6
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

示例 2:

img

1
2
输入:obstacleGrid = [[0,1],[0,0]]
输出:1

提示:

  • m == obstacleGrid.length
  • n == obstacleGrid[i].length
  • 1 <= m, n <= 100
  • obstacleGrid[i][j]01
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
/**
* @param {number[][]} obstacleGrid
* @return {number}
*/
var uniquePathsWithObstacles = function(obstacleGrid) {
//当(x,y)点是障碍点时,那么达到x,y的路径只能是0条,多加一个判断条件
//初始化d[x][y]
let m = obstacleGrid.length;
let n = obstacleGrid[0].length;
let d = new Array(m);
for(let x = 0; x < m; x++){
d[x] = new Array(n);
let y = 0;
while(x==0 && obstacleGrid[0][y]!=1 && y < n){
d[0][y++] = 1
}
while(x==0 && y < n){
d[0][y++] = 0;
}
if(obstacleGrid[x][0]!=1){
d[x][0] = 1;
} else {
while(x < m){//这里有个易错点
//如果在这个代码块种不加if(x < m)d[x] = new Array(n);,那么因为当x++的时候,下一个d[x]是没有定义为数组的,如果此时使用d[x][0]就会报不能给undefined赋值的错误
d[x][0] = 0;
x++;
if(x < m)d[x] = new Array(n);
}
}
}
for(let x = 1; x < m; x++){
for(let y = 1; y < n; y++){
if(obstacleGrid[x][y]==1){
d[x][y] = 0;
}else{
d[x][y] = d[x-1][y] + d[x][y-1];
}
}
}
console.log(d);
return d[m-1][n-1];

};

整数拆分

343. 整数拆分

给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积

示例 1:

1
2
3
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

示例 2:

1
2
3
输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

提示:

  • 2 <= n <= 58

code:

把数尽量拆成平均的

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @param {number} n
* @return {number}
*/
var integerBreak = function(n) {
let maxNum = -1;
for(let k = 2; k <= n ; k++){
let m = n % k;//余数
maxNum = Math.max(((((n-m)/k) + 1))**(m) * (((n-m)/k)) ** (k - m), maxNum);
}
return maxNum;
};

dp思想:对于递推公式还是有点疑惑

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
/**
* @param {number} n
* @return {number}
*/
var integerBreak = function(n) {
// 10
// 1 9
// 2 8
// 3 7
//4 6
// 5 5
// 6 4
// 7 3
// 8 2
// 9 1
let dp = new Array(n+1).fill(0);//要先提前初始化为0
dp[0] = 0;
dp[1] = 0;
dp[2] = 1;
// 3
for(let i = 3; i <= n; i++){
for(let j = 1; j <= i/2; j++){
dp[i] = Math.max(j*(i-j), j*dp[i-j],dp[i]);
}
}
return dp[n];

};

不同的二叉搜索树

96. 不同的二叉搜索树

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

示例 1:

img

1
2
输入:n = 3
输出:5

示例 2:

1
2
输入:n = 1
输出:1

提示:

  • 1 <= n <= 19

Code:

如果清楚二叉搜索树的特性和知道用动态规划来做这道题,这道题其实还不算特别难,但是需要画图分析一下,其实就是找规律的过程。

由于二叉搜索树的特性(比跟节点小的数一定放在左子树中,比根节点大的数一定放在右子树中),所以就需要考虑当j作为根节点的时候,会有几种情况,在对j0遍历到n;这里为什么从0开始遍历,后面会解释。

在实际分析前,需要注意的一点的,如果一棵树只有3个节点,那无论这三个节点里面的数是(1,2,3),还是(2,3,4),或者是(11,12,13)等等,只要是连续的数字,那么他们的二叉搜索树的种类个数就一定是相等的。即树有几种可能性是考虑树中有几个节点,而不是考虑树中节点的实际值。

具体来说,考虑n=4的情况,在n=4时,分为4种情况讨论:

  • 当root=1,那么可以得知对于2,3,4节点来说,这三个节点都将放在以1为根节点的树的右子树下,因为 2,3,4都大于1,那么对于右子树而言,只需要计算(2,3,4)节点的可能种类即d[3];在考虑左子树,由于没有比1还小的树节点,所以此时的左子树是为空的,即d[0]。一般来说,如果考虑d[0]的实际意义,可能会给d[0]赋值为0;

    但是需要考虑到,一棵树的种类,如果确定了根节点,那么这颗树的可能种类就是左子树的可能种类个数乘上右子树的可能种类个数,即d[3]*d[0],如果我们给d[0]赋值为0,那么d[3]*d[0]==0,这很明显和实际情况不符。再加上题目中1<=n<=19,所以我们不妨将d[0]赋值为1

  • 那么当roo=2,我们同理分析,只有(1)节点(1个节点)在右子树中,而(2,3)节点(2个节点)在左子树中,那么可能性就是d[1]*d[2]

  • 同理,当root=3,有(1,2)2个节点在右子树,只有(4)1个节点在左子树中,所以可能性是d[2]*d[1]

  • 当root=4,有(1,2,3)3个节点在右子树,0个节点在左子树,即d[3]*d[0]

    总结分析得 root==1 d[3]*d[0]

    总结分析得 root==2 d[2]*d[1]

    总结分析得 root==3 d[1]*d[2]

    总结分析得 root==4 d[0]*d[3]

    ​ 即 root==j d[i-j-1]*d[j]j0i-1(0和i-1都去得到)

    从实际意义上来理解其实也很好,如果是一颗有i个节点的树,其中一个节点已经确定了是根节点了,所以只有剩下的i-1个节点分别分配在左右子树中,所以左右子树的节点个数相加是i-1,所以上面的d[i-j-1]*d[j]中的i-j-1加上j是一定等于i-1的。

现在只需要对上面的4种情况(i==4种情况)进行求和就可以得出dp[4]dp[i]

这种规律从i=3就开始了,所以可以从i=3开始遍历,一直到题目所求的i==n

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @param {number} n
* @return {number}
*/
var numTrees = function(n) {
//d[i]表示有i个节点的二叉搜索树的种数
let dp = new Array(n+1).fill(1);
dp[2] = 2;
for(let i = 3; i <= n; i++){
let cn = 0;
for(let j = 0; j < i; j++){
cn += (dp[j] * dp[i-j-1]);
}
dp[i] = cn;
}
return dp[n];
};

背包问题

416. 分割等和子集

416. 分割等和子集

给你一个 只包含正整数非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

1
2
3
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

示例 2:

1
2
3
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

思路

取和不取的问题,对于一个正数数组,数组中所有值的和事确定的,所以分成两个子集后,两个子集的元素和就为整个和的一半

leetcode热题hot100【JS】

更新时间

2024.5.12 第一次更新

2024.5.17 第二次更新

刷算法题可能用到的函数和表示方式

常用的表达方式

1
2
//2的10次方
let n = 2 ** 10;

数组

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
//数组中插入数据
let arr = [];
arr.push(1);

//arr = [1]
arr.indexOf(1);//0


//比较两个数组是否相等(只适用于数组里面的元素都是原始值)
function arraysAreEqual(array1, array2) {
// 首先检查数组长度
if (array1.length !== array2.length) return false;

// 然后检查每个元素是否相等
for (let i = 0; i < array1.length; i++) {
if (array1[i] !== array2[i]) return false;
}

// 如果通过了上述检查,那么数组相等
return true;
}

//数组遍历
let strs = ["hello", "world", "JavaScript"];

//使用for循环
for(let i = 0; i < strs.length; i++){
console.log(i,strs[i]);
}

//使用for of 与entries()
for (let [i, str] of strs.entries()) {
console.log(i, str);
}

//数字数组的排序
//在JavaScript中,如果sort()方法没有提供比较函数,它会将数组元素转换成字符串,然后按照字符串的Unicode码点顺序进行排序。
//会导致例如这个数组[100,4,200,1,3,2]排序不成功
//例如,数字100和200在转换为字符串后,会根据它们的第一个字符("1"和"2")来进行排序,但当比较2和100时,由于"2"的Unicode码点小于"1"的Unicode码点,所以2会排在100之前。
//需要加入比较函数
nums = [100,4,200,1,3,2];
nums.sort((a, b) => a - b);
//这个比较函数简单地从a中减去b。如果结果是负数,那么a将排在b之前;如果结果是正数,b将排在a之前;如果结果是0,那么它们的顺序不变。
// a-b就是 从小到大排序


//初始化一个长度为n,值为0的数组
let n = 5; // 假设我们想要一个长度为5的数组
let arr = Array(n).fill(0);
console.log(arr); // 输出: [0, 0, 0, 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
26
27
//单个字符转换成ASCII码字
let c = 'a';
let ASCIINum = c.charCodeAt(0);//97

let c1 = 'ab';
let ASCIINum = c.charCodeAt(2);//98

//将字符串分割成单个字符的数组
let str = "hello";
str.split("").forEach(char => console.log(char));
// ['h','e','l','l','o']

//字符串字母排序
function sortString(str) {
// 将字符串转换为数组
let arr = str.split('');
// 对数组进行排序
arr.sort();
// 将数组转换回字符串
return arr.join('');
}

let originalString = "hello";
let sortedString = sortString(originalString);

console.log(sortedString); // 输出: ehllo

Map

在JavaScript中,Map 是一种新的数据结构,它允许你存储键值对(key-value pairs)。与对象不同,Map 的键可以是任何类型的值,包括函数、对象或任何原始类型。下面是一些基本的 Map 对象用法:

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
//使用 `has` 方法可以检查 `Map` 中是否存在某个键:
//创建一个 Map
let map = new Map();

//设置键值对
//使用 `set` 方法可以添加或更新键值对:
map.set('key', 'value');
map.set(123, 456);
map.set({}, 'Object');
map.set(function() {}, 'Function');

//获取值
console.log(map.get('key')); // 输出: 'value'
console.log(map.get(123)); // 输出: 456

//检查键是否存在
console.log(map.has('key')); // 输出: true
console.log(map.has('notExist')); // 输出: false

//删除键值对
map.delete('key');

//获取Map的大小
//使用 `size` 属性可以获取 `Map` 中键值对的数量:
console.log(map.size);

//遍历Map
//`Map` 对象可以通过 `forEach` 方法遍历,也可以使用迭代器方法(如 `keys()`, `values()`, 和 `entries()`)进行遍历。

//使用 `forEach` 遍历:
map.forEach((value, key) => {
console.log(key, value);
});

//使用迭代器遍历:
for (let [key, value] of map.entries()) {
console.log(key, value);
}

// 或者直接遍历Map对象
for (let [key, value] of map) {
console.log(key, value);
}

//清空Map
map.clear();

Set

在JavaScript中,Set是一种新的数据结构,它类似于数组,但是它的一个主要特点是其内部的值都是唯一的,没有重复的值。Set对象允许你存储任何类型的唯一值,无论是原始值或是对象引用。

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
//创建一个 Set
let mySet = new Set();

//也可以在创建时初始化`Set`:
let mySet = new Set([1, 2, 3, 4, 4, 4]);
console.log(mySet); // Set(4) {1, 2, 3, 4}


//注意,即使数组中包含重复的元素,`Set`仍然确保只保存唯一的值。
//添加元素
mySet.add(5);
mySet.add("hello");
mySet.add({a: 1, b: 2});

//检查元素
console.log(mySet.has(1)); // true
console.log(mySet.has(6)); // false

// 删除元素
mySet.delete(1);

//遍历 Set
//`Set`对象可以使用`forEach()`方法和`for...of`循环来遍历:
mySet.forEach(value => {
console.log(value);
});

for (let item of mySet) {
console.log(item);
}

//获取 Set 的大小
console.log(mySet.size);

//清空 Set
mySet.clear();

Set是JavaScript ES6中引入的一种集合类型,它提供了一种存储唯一值的高效方式。

100题实战

哈希

两数之和【数组、哈希】

1. 两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:

1
2
3
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

示例 2:

1
2
输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

1
2
输入:nums = [3,3], target = 6
输出:[0,1]

提示:

  • 2 <= nums.length <= 104
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109
  • 只会存在一个有效答案

code:

第一次写没有考虑到两个数的下标不能一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
for(let i = 0; i < nums.length; i++){
let res = [];
let index;
if(( index = nums.indexOf(target - nums[i])) != -1 && index != i){
res.push(i);
res.push(index);
return res;
}
}

};

字母异位词分组【数组、哈希、字符串】

49. 字母异位词分组

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

字母异位词 是由重新排列源单词的所有字母得到的一个新单词。

示例 1:

1
2
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]

示例 2:

1
2
输入: strs = [""]
输出: [[""]]

示例 3:

1
2
输入: strs = ["a"]
输出: [["a"]]

提示:

  • 1 <= strs.length <= 104
  • 0 <= strs[i].length <= 100
  • strs[i] 仅包含小写字母

code

一开始的想法是对每一个字符串的ASCII码求和,和一样的字符串就是字母异位词,但是仔细想一下就知道这样不对,即使字符不一样,也可能会出现SASCII码一样的情况

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
//错误的
/**
* @param {string[]} strs
* @return {string[][]}
*/
var groupAnagrams = function(strs) {
let hash = new Map();
for(let i = 0; i < strs.length; i++){
//遍历每一个字符串
let sumASCII = 0;
strs[i].split('').forEach(c => sumASCII += c.charCodeAt(0));

if(hash.has(sumASCII)){//存在同样的
let arr = hash.get(sumASCII);
arr.push(strs[i]);
hash.set(sumASCII, arr);
}else{
let arr = [];
arr.push(strs[i]);
hash.set(sumASCII,arr);
}
}
let res = [];
hash.forEach( (value, key) => {
res.push(value);
});
return res;
};

看了官方解法,有两种解法,排序和计数,很好的解法

排序

思路和上面的ASCII码的思路是一样的

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
/**
* @param {string[]} strs
* @return {string[][]}
*/
var groupAnagrams = function(strs) {
let mp = new Map();//哈希表,<str, arr>

for(let [i, str] of strs.entries()){
//排序
let arr = str.split('').sort();
let newStr = arr.join('');

if(mp.has(newStr)){//存过了
let subArr = mp.get(newStr);
subArr.push(str);
mp.set(newStr, subArr);
}else{
let subArr = [];
subArr.push(str);
mp.set(newStr, subArr);
}
}

let res = [];
mp.forEach((value, key) => res.push(value));
return res;

};

【要二刷】最长连续序列【并查集、数组、哈希表】

128. 最长连续序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:

1
2
3
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。

示例 2:

1
2
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9

提示:

  • 0 <= nums.length <= 105
  • -109 <= nums[i] <= 109

思路是哈希表,但是刚开始没有想到有负数,通过用例 67/75

主要是超过时间限制了,10的9次方的数,遍历一遍就超时了

单出用hashtable[num]++;来计数判断不行了

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
//个别用例超时
/**
* @param {number[]} nums
* @return {number}
*/
var longestConsecutive = function(nums) {
if(nums.length===0) return 0;
let maxNum = 0;
let minNum = 0;
nums.map( (v) =>{
maxNum = Math.max(maxNum, v);
minNum = Math.min(minNum, v);
});
//
let hashTable1 = Array(maxNum + 1).fill(0);
let hashTable2
if(minNum < 0){//有负数
hashTable2 = Array( -1 * minNum + 1).fill(0);
}

for(let i = 0; i < nums.length; i++){
if(nums[i] >= 0){
hashTable1[nums[i]]++;
}else{
hashTable2[ -1 * nums[i]]++;
}
}

let res = 0;

let maxLen1 = -1;
let maxLen2 = -1;
let l1 = 0;
let l2 = 0;
//正数
let f1 = false;
for(let i = 0; i <= maxNum ; i++){
if(hashTable1[i] != 0){
res++;
}else{//有中断
f1 = true;
maxLen1 = Math.max(maxLen1, res);
res = 0;//重新计数
}
if(!f1) l1++;
}
maxLen1 = Math.max(maxLen1, res);
//负数
res = 0
let f2 = false;
for(let i = 1; i <= ( -1 * minNum) ; i++){
if(hashTable2[i] != 0){
res++;
}else{//有中断
f2 = true;
maxLen2 = Math.max(maxLen2, res);
res = 0;//重新计数
}
if(!f2) l1++;
}
maxLen2 = Math.max(maxLen2, res);

//计算负-0-正

return Math.max(maxLen1, maxLen2, l1+l2);

};

稍微看了一下题解思路,说是主要判断这个数的上一个数是否在哈希表中,感觉有点思路了,尝试写一下,没写出来,看完题解思路了才写的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @param {number[]} nums
* @return {number}
*/
var longestConsecutive = function(nums) {
let mySet = new Set();
nums.map( v => {mySet.add(v);});
let res = 0, cn = 1;
for(let i = 0; i < nums.length; i++){
//判断该数字是否是连续子串的开头
cn = 1;
if(!mySet.has(nums[i] - 1)){//是开头
let st = nums[i] + 1;
while(mySet.has(st)){
cn++;
st++;
}
res = Math.max(res, cn);
}
}
return res;
};

双指针

移动零【数组、双指针】

283. 移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

示例 1:

1
2
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

示例 2:

1
2
输入: nums = [0]
输出: [0]

提示:

  • 1 <= nums.length <= 104
  • -231 <= nums[i] <= 231 - 1

进阶:你能尽量减少完成的操作次数吗?

code

快慢指针的思路

慢指针指向待更新的数

快指针指向非0需要转移的数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var moveZeroes = function(nums) {
// 0 1 0 3 12
//在 0 的位置对非 0 赋值
let len = nums.length;
let f = 0, s = 0;
for(s = 0; s < len; s++){
while( f < len && nums[f] == 0) f++;
if(f >= len) break;
nums[s] = nums[f++];
}
while(s<len){
nums[s++] = 0;
}
return nums;

};

盛最多水的容器【双指针、贪心】

11. 盛最多水的容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0)(i, height[i])

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明:你不能倾斜容器。

示例 1:

img

1
2
3
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

示例 2:

1
2
输入:height = [1,1]
输出:1

提示:

  • n == height.length
  • 2 <= n <= 105
  • 0 <= height[i] <= 104

code

前几天刚做过,复习了一下

双指针,两边夹

贪心的想法,由于装水的面积是右桶的长短决定的,所以较短的那条边先向中间走(这是保证从长到短的宽度下的最优解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @param {number[]} height
* @return {number}
*/
var maxArea = function(height) {
let maxNum = -1;
let st = 0, end = height.length - 1;
while(st < end){
maxNum = Math.max(maxNum, (end - st) * Math.min(height[st], height[end]));
if(height[st] <= height[end]){
st++;
}else end--;
}
return maxNum;
};

【优先二刷】三数之和 【数组,双指针】

15. 三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:

1
2
3
4
5
6
7
8
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:

1
2
3
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。

示例 3:

1
2
3
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

提示:

  • 3 <= nums.length <= 3000
  • -105 <= nums[i] <= 105

code:

排序去重是一个很重要的点,有很多题目,都是组成元素一样,但是顺序不一样,此时,应该考虑排序

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
/**
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function(nums) {
let res = [];
let set = new Set();
for(let i = 0; i < nums.length; i++){
for(let j = i; j < nums.length; j++){
if( i != j){
let sum = nums[i] + nums[j];
let index = nums.indexOf(-sum);
if(index != -1 && index != i && index != j){
let arr = [];
arr.push(nums[i]);
arr.push(nums[index]);
arr.push(nums[j]);
arr.sort();
if(!set.has(arr)){
res.push(arr);
set.add(arr);
}
}
}
}
}
return res;
};

但是这样写的话在JS中是不能用set去重的,因为set中的元素是对象,对象引用的是指针,就算排序后的数组中的每一个元素都是一样的,set也会认为这是不同的数组。然后尝试将数组使用join函数转换为字符串在传入set去重

但是还是有问题,测试用例过227/313,不知道哪里错了,看不出来.

还有效率问题:您的代码使用了三层循环(实际上是两层循环加一个线性搜索 indexOf),这导致时间复杂度高达 O(n^3),在 nums 数组较大时会非常低效。

思考:其实经常想不到sort排序的解法,应该想到的

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
/**
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function(nums) {
let res = [];
let set = new Set();
for(let i = 0; i < nums.length; i++){
for(let j = i; j < nums.length; j++){
if( i != j){
let sum = nums[i] + nums[j];
let index = nums.indexOf(-sum);
if(index != -1 && index != i && index != j){
let arr = [];
arr.push(nums[i]);
arr.push(nums[index]);
arr.push(nums[j]);
arr.sort();
let str = arr.join('');
if(!set.has(str)){
res.push(arr);
set.add(str);
}
}
}
}
}
return res;
};

转换思路,咨询了一下AI模型,跑了后还是对的

排序:首先对数组进行排序,这样可以更容易地避免重复的三元组,并且可以使用双指针技术来减少不必要的搜索。

双指针:使用一个外层循环遍历每个元素,然后在剩余部分使用两个指针(一个从左边开始,另一个从右边开始)来寻找两个数,使得这三个数的和为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
/**
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function(nums) {
//排序+循环嵌套双指针(两边夹)
//因为有重复元素,还要去重
nums.sort((a,b) => a - b);//递增
let res = [];
for(let i = 0; i < nums.length - 2; i++){//-2时因为i是三元组的第一个,要构成三元组就不可能是最后一个
//对于三元组中的第一个数,也要去重,同一个数,只操作一次
if( i > 0 && nums[i] == nums[i - 1]) continue;
//if( i + 1 < nums.length - 2 && nums[i + 1] == nums[i]) continue;这样的去重逻辑是错误的,不要这样写
//双指针两边夹
let left = i + 1, right = nums.length - 1;
while(left < right){
let sum = nums[i] + nums[left] + nums[right];
if(sum === 0){//找到一个三元组
res.push([nums[i], nums[right], nums[left]]);
while(left < right && nums[left] == nums[left + 1]) left++;
while(left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
}else if(sum < 0){
left++;
}else right--;
}
}
return res;
};

【!!dp,还没写】接雨水

42. 接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:

img

1
2
3
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

示例 2:

1
2
输入:height = [4,2,0,3,2,5]
输出:9

提示:

  • n == height.length
  • 1 <= n <= 2 * 104
  • 0 <= height[i] <= 105

滑动窗口

无重复字符的最长子串

3. 无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的最长子串的长度。

示例 1:

1
2
3
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

1
2
3
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

1
2
3
4
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

提示:

  • 0 <= s.length <= 5 * 104
  • s 由英文字母、数字、符号和空格组成

code:

不用map,用set和单纯数组都行,数组的话就用include方法来查看字母是否已经存在于数组中,但是最好还是不要数组了,数组里面删除一个元素会很麻烦,主要还是哈希+滑动窗口的思想。

用set的话会简单一点点

带注释版本

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
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
if(s.length==0) return 0;
//无重复字符的最长子串
//ex
// abcabcbb
// abc -> maxLen = 3
//滑动窗口
//1.定义需要维护的变量
let maxLen = -1;
let hash = new Map();
//2.定义窗口的首尾端,然后滑动窗口
let st = 0;
for(let end = 0; end < s.length; end++){//第一次debug是end++写成s++了
//维护变量
let c = s[end];

//情况2:窗口可变,检查窗口是否合法,不合法就调整st指针直至合法
//在该题目中,不合法指的是,字符串中出现重复字符
if(hash.has(c)){//c字符不是第一次出现,窗口不合法
//只要连续移动字符,直到新的窗口的字符中不包含第一次出现的c字符位置
while( st < end && s[st] != c){
hash.delete(s[st]);//第二次debug,这一句和下面一句的顺序反了,如果先++在delet,那么相当于delet的是下一个字符,第一个字符永远都不会被移除
st++;
}
//此时的st应该位于第一个窗口的第一个c字符处
st++;
//现在窗口合法了
}else{//第一次出现
hash.set(c, 1);
}

//窗口的长度为end - st + 1 (左闭右闭区间)
//如果是左闭右开区间,就是end - st
maxLen = Math.max(maxLen, end - st + 1);
}
return maxLen;
};

不带注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
if(s.length==0) return 0;
let maxLen = -1;
let hash = new Map();
let st = 0;
for(let end = 0; end < s.length; end++){
let c = s[end];
if(hash.has(c)){
while( st < end && s[st] != c){
hash.delete(s[st]);
st++;
}
st++;
}else{
hash.set(c, 1);
}
maxLen = Math.max(maxLen, end - st + 1);
}
return maxLen;
};

找到字符串中所有字母异位词

438. 找到字符串中所有字母异位词

给定两个字符串 sp,找到 s 中所有 p异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

示例 1:

1
2
3
4
5
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。

示例 2:

1
2
3
4
5
6
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。

Code:

窗口不合理的情况比较复杂

带注释版本

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
/**
* @param {string} s
* @param {string} p
* @return {number[]}
*/
var findAnagrams = function(s, p) {
//哈希(异位词)+滑动窗口(子串)
//异位词的特点是:
//1.长度相等
//2.每个字符的出现次数相等
let hash = new Map();
let hash2 = new Map();
let res = [];
//初始化hash
for(let i = 0; i < p.length; i++){
if(hash.has(p[i])){
hash.set(p[i], hash.get(p[i]) + 1);
}else hash.set(p[i], 1);
}
//初始化窗口,并开始滑动
let st = 0;
for(let end = 0; end < s.length; end++){
let c = s[end];
if(hash2.has(c)){
hash2.set(c, hash2.get(c) + 1);
}else hash2.set(c, 1);

//窗口不合法1,没有这个字符,两个指针都跳到这个指针后面
if(!hash.has(c)){
while(st != end + 1){//debug1,一开始没有跳转到st = end+1,只是st++,这样是不对的,而且跳转完之后,一定要记得移除hash2中前面的(已经不在滑动窗口中的)字母
hash2.set(s[st], hash2.get(s[st]) - 1);
st++;
}
}else if(hash2.get(c) > hash.get(c)){
//窗口不合法2,有这个字符,但是字符数多了,移动st,直到窗口中的c的字符数与hash中的字符数一致
while(hash2.get(c) != hash.get(c)){
hash2.set(s[st], hash2.get(s[st]) - 1);
st++;
}
}
//窗口不合法3,窗口长度超过了
while(end - st + 1 > p.length){
hash2.set(s[st], hash2.get(s[st]) - 1);//debug2,滑动了窗口,但是忘记处理hash2了
st++;
}
if(end - st + 1 === p.length){
res.push(st);
}
}
return res;
};

不带注释版本

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
/**
* @param {string} s
* @param {string} p
* @return {number[]}
*/
var findAnagrams = function(s, p) {
let hash = new Map();
let hash2 = new Map();
let res = [];
for(let i = 0; i < p.length; i++){
if(hash.has(p[i])){
hash.set(p[i], hash.get(p[i]) + 1);
}else hash.set(p[i], 1);
}
let st = 0;
for(let end = 0; end < s.length; end++){
let c = s[end];
if(hash2.has(c)){
hash2.set(c, hash2.get(c) + 1);
}else hash2.set(c, 1);
if(!hash.has(c)){
while(st != end + 1){
hash2.set(s[st], hash2.get(s[st]) - 1);
st++;
}
}else if(hash2.get(c) > hash.get(c)){
while(hash2.get(c) != hash.get(c)){
hash2.set(s[st], hash2.get(s[st]) - 1);
st++;
}
}
while(end - st + 1 > p.length){
hash2.set(s[st], hash2.get(s[st]) - 1);
st++;
}
if(end - st + 1 === p.length){
res.push(st);
}
}
return res;
};

子串

【优先二刷】和为 K 的子数组

560. 和为 K 的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数

子数组是数组中元素的连续非空序列。

示例 1:

1
2
输入:nums = [1,1,1], k = 2
输出:2

示例 2:

1
2
输入:nums = [1,2,3], k = 3
输出:2

code:

一刷本来想用滑动窗口做,但是发现有负数,没法用滑动窗口做。

这个问题可以通过使用“前缀和”加上“哈希表”来高效解决。基本思路是,我们遍历数组,计算每个位置的前缀和,即从数组开始到当前元素的累计和。然后,对于每个前缀和,我们检查是否存在一个之前的前缀和,其值等于“当前前缀和 - k”。如果存在,这意味着这两个前缀和之间的元素的和正好为k。我们使用哈希表来存储每个前缀和出现的次数,这样就可以在O(1)时间内查找到之前的前缀和。

在这道题学会了前缀和的用法,这确实是我比较少用的一个点

学习了:

  • 前缀和的思想

  • 用对象来构建哈希表

  • let of的用法,

    • let of访问的是value

    • let in访问的是索引号

    • 如果想同时访问value和index可以用for循环或者nums.forEach((value, index) => {console.log(1);});

forEach()方法在遍历数组时访问的是原数组。这意味着在forEach()循环中,您可以直接访问并操作原数组的元素。

forEach()里面的回调函数确实可以修改原数组的元素。但是,需要注意的是,虽然您可以修改数组的元素,比如通过索引更改元素的值,您却不能通过forEach()直接修改数组的结构,例如增加或删除元素,对数组结构的修改不会影响到遍历过程。这是因为forEach()遍历的范围在第一次调用回调函数之前就已经确定了。

下面是一个示例,展示了如何通过forEach()修改原数组的元素:

1
2
3
4
5
6
7
8
9
let nums = [1, 2, 3, 4];

// 使用 forEach() 修改数组元素的值
nums.forEach((value, index, array) => {
// 将每个元素值加倍
array[index] = value * 2;
});

console.log(nums); // 输出: [2, 4, 6, 8]

在这个例子中,我们通过forEach()遍历数组,并将每个元素的值加倍。虽然我们在forEach()的回调函数中直接修改了数组元素的值,但是请注意,如果尝试在遍历过程中添加或删除元素,可能不会影响当前的遍历过程,因为遍历的范围是在遍历开始前就已经确定了。

由此可见,这是一个数学题TAT,思路如下:

a1 a2 a3 a4 a5 a6
cn1 = a1 + a2
cn2 = a1 + a2 + a3 + a4

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

//如果 a3 + a4 = k =>有一个子串和位k

/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var subarraySum = function(nums, k) {
let pre_sum_hash = {0 : 1};
let sum = 0;
let res = 0;
for(let num of nums){
sum += num;
// sum - otherPreSum = k => sum - k = otherPreSum
let otherPreSum = sum - k;
if(pre_sum_hash[otherPreSum] !== undefined){
res += pre_sum_hash[otherPreSum];
}
//更新哈希表
if(pre_sum_hash[sum] !== undefined){
pre_sum_hash[sum]++;
}else{
pre_sum_hash[sum] = 1;
}
}
return res;
};

第七章——函数表达式

函数表达式的特征

定义函数的两种方法:

  • 函数声明

    1
    2
    3
    function functionName(arg0, arg1, arg2){
    //函数体
    }

    有一些浏览器给函数定义了一个name属性,可以通过这个属性访问到给函数指定的名字,这个值为function关键字后面的标识符。

    alert(function.name);//“funtionName”

  • 函数表达式

    1

两个定义函数的方法的异同:

由函数声明的函数在JS脚本构建时,会函数提升,而由函数表达式定义的函数不会。

使用函数实现递归

使用闭包定义私有变量

2.双指针:两个输入,两个都需要遍历(快慢指针)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let fn = (arr1, arr2) => {
let i = 0, j = 0, ans = 0;

while(i < arr1.length && j < arr2.length){
//根据题意补充代码
if(){
i++;
}else{
j++;
}
while(i < arr1.length){
//
i++;
}
while(j < arr2.length){
//
j++;
}
return ans;
}
}

例题

88. 合并两个有序数组

给你两个按 非递减顺序 排列的整数数组 nums1nums2,另有两个整数 mn ,分别表示 nums1nums2 中的元素数目。

请你 合并 nums2nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n

示例 1:

1
2
3
4
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。

示例 2:

1
2
3
4
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 [] 。
合并结果是 [1] 。

示例 3:

1
2
3
4
5
输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [] 和 [1] 。
合并结果是 [1] 。
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。

思路一:双指针,但是不借助额外的数组,原数组nums1的值就会被覆盖,但是题目没有说不能借助额外数组,所以就先放到新的数组里面,在把新数组赋值给nuns1

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
/**
* @param {number[]} nums1
* @param {number} m
* @param {number[]} nums2
* @param {number} n
* @return {void} Do not return anything, modify nums1 in-place instead.
*/
var merge = function(nums1, m, nums2, n) {

let i = 0, j = 0;
//思路一:双指针,但是不借助额外的数组,原数组nums1的值就会被覆盖,但是题目没有说不能借助额外数组,所以就先放到新的数组里面,在把新数组赋值给nuns1
let newArr = new Array(m + n);
let k = 0;
while(i < m && j < n){
if(nums2[j] <= nums1[i]){
newArr[k++] = nums2[j++];
}else{
newArr[k++] = nums1[i++];
}
}
while(i < m) newArr[k++] = nums1[i++];
while(j < n) newArr[k++] = nums2[j++];
for(let p = 0; p < m + n; p++){
nums1[p] = newArr[p];
}
return nums1;
};

思路二:逆向双指针,因为num1的数组的末尾m个都是0,是不害怕被覆盖的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @param {number[]} nums1
* @param {number} m
* @param {number[]} nums2
* @param {number} n
* @return {void} Do not return anything, modify nums1 in-place instead.
*/
var merge = function(nums1, m, nums2, n) {
let i = m - 1, j = n - 1, k = m + n - 1;
while(i >= 0 && j >= 0){
if(nums1[i] >= nums2[j]){
nums1[k--] = nums1[i--];
}else{
nums1[k--] = nums2[j--];
}
}
while(i >= 0) nums1[k--] = nums1[i--];
while(j >= 0) nums1[k--] = nums2[j--];
return nums1;
};

双指针:一个输入,两端遍历(两边夹)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//箭头函数 ()=>{}
//()表示参数
//{}表示函数体
let fn = arr =>{
let left = 0, ans = 0, right = arr.length -1;

while(left < right){
//根据题意补充代码
if(){
left++;
}else{
right--;
}
}
return ans;
}

例题

11. 盛最多水的容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0)(i, height[i])

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明:你不能倾斜容器。

示例 1:

img

1
2
3
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

示例 2:

1
2
输入:height = [1,1]
输出:1

提示:

  • n == height.length
  • 2 <= n <= 105
  • 0 <= height[i] <= 104

做题思路:

双指针

长*宽的最大值,迭代

难点在于在什么情况下移动左指针,在什么情况下移动右指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @param {number[]} height
* @return {number}
*/
var maxArea = function(height) {
let left = 0, right = height.length - 1, maxAns = -1;

while(left < right){
let w = right - left;
let h = Math.min(height[left], height[right]);
maxAns = Math.max(w * h, maxAns);
if(height[left] < height[right]){
left++;
}else{
right--;
}
}
return maxAns
};

参考官方题解

https://leetcode.cn/problems/container-with-most-water/solutions/207215/sheng-zui-duo-shui-de-rong-qi-by-leetcode-solution

v-bind

是Vue.js中的一个指令,用于动态地绑定一个或多个属性,或者传递属性到组件。它可以将数据的值绑定到HTML元素的属性上,或者是父组件向子组件传递数据。

基本用法

  • 绑定HTML属性:可以将数据绑定到元素的属性上。例如,如果你想根据数据动态改变的src属性,可以这样写:
1
<img v-bind:src="imageSrc">

这里,imageSrc是一个变量名,它的值会被设置为img的src属性。

  • 缩写:v-bind:有一个缩写,即冒号:。上面的例子可以简写为:
1
<img :src="imageSrc">

绑定多个属性

v-bind也可以通过使用对象语法一次绑定多个属性。例如:

1
<div v-bind="{ id: someId, 'data-name': name }"></div>

这里,someId和name是变量,它们的值将分别绑定到div的id属性和data-name属性。

绑定到组件的props

当使用组件时,v-bind用于将数据从父组件传递到子组件的props:

1
<my-component :some-prop="someData"></my-component>

这里,someData是父组件中的数据,some-prop是子组件的prop,someData的值将传递给子组件的some-prop。

使用场景

动态绑定元素的类和样式。

将数据传递给组件的props。

根据数据动态改变元素的属性,如src、href等。

v-bind:是Vue开发中非常常用的一个指令,它提高了代码的灵活性和可维护性,使得数据和视图之间的绑定更加直观和方便。

V-bind可以绑定变量、绑定类、绑定属性等等

GitHub于2021年8月13日移除了对密码身份验证的支持,因此您需要使用其他身份验证方式来访问GitHub仓库。

推荐的替代方式是使用个人访问令牌(Personal Access Token)作为身份验证凭据。您可以在GitHub上生成一个个人访问令牌,并将其用作密码来访问您的仓库。

以下是解决此问题的一般步骤:

  1. 在GitHub上生成个人访问令牌:

    • 登录GitHub账号,转到Settings -> Developer settings -> Personal access tokens。
    • 点击Generate new token,选择所需的权限,并生成访问令牌。
    • 复制生成的访问令牌。
  2. 在Git中使用个人访问令牌:

    • 当Git提示输入用户名和密码时,用户名为您的GitHub用户名,密码为您生成的个人访问令牌。

通过这种方式,您应该能够成功进行身份验证并访问您的GitHub仓库。如果问题仍然存在,请参考提供的链接以获取更多信息或尝试其他解决方案。

在Vue 3中,withDefaults和defineProps是用于定义和设置组件的props的工具,特别是在使用script setup语法糖时。

defineProps

defineProps函数用于在Vue组件中声明props的类型。它通常与TypeScript一起使用,以提供类型安全。在script setup语法中,defineProps用于定义接收自父组件的数据的属性。

withDefaults

withDefaults函数在Vue 3中用于为defineProps定义的props提供默认值。它的参数结构如下:

1
withDefaults(defineProps<Type>(), defaultProps)
  • 第一个参数是defineProps调用的结果,defineProps()用于定义组件的props,并且可以指定一个TypeScript接口或类型来静态类型检查这些props。

  • 第二个参数是一个对象,其中的键是prop的名字,值是这个prop的默认值。默认值可以是直接的值,或者是返回值的函数,这样每次使用默认值时都会调用该函数来获取一个新的值。

例如

withDefaults函数用于为defineProps定义的props提供默认值。这是处理props可能未被父组件传递时的情况的一种方式,确保组件有一个可靠的默认状态。

1
2
3
4
5
6
7
8
9
import { defineProps, withDefaults } from "vue";

interface Props {
postList: any[];
}

const props = withDefaults(defineProps<Props>(),{
postList: () => [],
});
  • defineProps():这里使用Props接口来定义props的结构,Props接口指定了postList是一个数组。

  • withDefaults(…, { postList: () => [] }):这里为postList提供了一个默认值。默认值是通过一个函数() => []来指定的,这意味着如果没有提供postList,它将默认为一个空数组。使用函数来返回默认值是一种常见的做法,因为这确保了每次使用默认值时都会创建一个新的数组实例,避免了不同实例间共享同一个数组的问题。

postList: () => []的意思

这里postList: () => []表示postList的默认值是一个空数组。使用箭头函数()返回一个新的空数组[],这样做的好处是每次调用这个默认值时都会创建一个新的数组实例,避免了潜在的引用类型数据共享问题,这是在JavaScript中处理数组和对象默认值的推荐做法。

总结来说,这种写法确保了组件的postList prop在没有从父组件接收到值时,会安全地使用一个新的空数组作为默认值,同时保持了类型安全和响应性。