2025-01-03
前端面试常见手撕JS代码题

codes

1. 数组,用 reduce​ 方法实现 map​ 方法

  • array.map(callback(element, index, array), thisArg)
  • array.reduce(callback(accumulator, currentValue, index, array), initialValue
1
2
3
4
5
6
7
function mapUsingReduce(arr, func) {
return arr.reduce((pre, cur, index, arr) => {
// return [...pre, func(ele)];
pre.push(func(cur, index, arr));
return pre;
}, [])
}

2. 实现函数repeat

需要实现的函数repeat()

1
2
3
4
5
function repeat (func, times, wait){} 
// 使下面代码能正常工作
const repeatFunc = repeat(console.log, 4, 3000)
repeatFunc('helloworld')
//会输出 4 次 hellworld,每次间隔 3 秒

实现如下:

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
function repeat (func, times, wait) {
return function(...args) {
let count = 0;
const interval = setInterval(() => {
if(count < times) {
func(...args);
count ++;
} else {
clearInterval(interval);
}

}, wait);
}

}

function repeat(func, times, wait) {
return function(...args) {
let count = 0;

function exec() {
if(count < times) {
func(...args);
count ++;
setTimeout(exec, wait);
}
}

exec();
}
}

3. Vue 组件:实现搜索框自动补全功能

以下是一个实现类百度搜索框功能的 Vue 3 组件。此组件会监听用户的输入,并根据输入动态显示相关的自动补全下拉列表。

组件代码

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
<template>
<div class="search-box">
<input
type="text"
v-model="query"
@input="onInput"
placeholder="请输入搜索内容..."
class="search-input"
/>
<ul v-if="suggestions.length" class="suggestions-list">
<li
v-for="(suggestion, index) in suggestions"
:key="index"
@click="selectSuggestion(suggestion)"
class="suggestion-item"
>
{{ suggestion }}
</li>
</ul>
</div>
</template>

<script>
import { ref } from 'vue';

export default {
setup() {
const query = ref(""); // 用户输入的查询内容
const suggestions = ref([]); // 自动补全列表

// 模拟从后台获取数据
const fetchSuggestions = (query) => {
const allData = ["苹果", "香蕉", "橙子", "葡萄", "菠萝", "草莓", "蓝莓", "芒果"];
if (!query) {
return [];
}
return allData.filter(item => item.includes(query));
};

// 用户输入时触发
const onInput = () => {
suggestions.value = fetchSuggestions(query.value);
};

// 用户点击某个推荐项时
const selectSuggestion = (suggestion) => {
query.value = suggestion; // 将选中的内容填充到输入框
suggestions.value = []; // 清空下拉列表
};

return {
query,
suggestions,
onInput,
selectSuggestion
};
}
};
</script>

<style scoped>
.search-box {
width: 300px;
margin: 0 auto;
position: relative;
}

.search-input {
width: 100%;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
}

.suggestions-list {
margin: 0;
padding: 0;
list-style: none;
border: 1px solid #ccc;
border-top: none;
background: #fff;
position: absolute;
width: 100%;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
}

.suggestion-item {
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #eee;
}

.suggestion-item:hover {
background: #f0f0f0;
}
</style>

功能说明

  1. 输入框绑定:
    • 使用 v-model​ 实现输入框的双向绑定。
  2. 自动补全逻辑:
    • 模拟一个后台数据源,通过 fetchSuggestions​ 方法过滤与用户输入匹配的内容。
  3. 下拉列表展示:
    • 使用 v-if​ 控制是否显示推荐列表。
    • 使用 v-for​ 动态渲染列表内容。
  4. 点击补全:
    • 用户点击某个推荐项时,填充到输入框并清空列表。
  5. 样式:
    • 输入框和下拉列表的样式使用 CSS 定义,保证视觉清晰整洁。

示例效果

  1. 用户输入 “苹” 后,下拉列表会显示 “苹果”。
  2. 用户输入 “蓝” 后,下拉列表会显示 “蓝莓”。
  3. 用户点击某个推荐项时,该项会自动填充到输入框中,并隐藏列表。

4. JavaScript常见函数API实现

4.1 实现 map()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Array.prototype.myMap = function(callback) {
const result = [];
for (let i = 0; i < this.length; i++) {
if (this.hasOwnProperty(i)) {
result.push(callback(this[i], i, this));
}
}
return result;
};

// 示例
const arr = [1, 2, 3];
const doubled = arr.myMap(x => x * 2);
console.log(doubled); // [2, 4, 6]

4.2 实现 reduce()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Array.prototype.myReduce = function(callback, initialValue) {
let accumulator = initialValue !== undefined ? initialValue : this[0];
let startIndex = initialValue !== undefined ? 0 : 1;

for (let i = startIndex; i < this.length; i++) {
if (this.hasOwnProperty(i)) {
accumulator = callback(accumulator, this[i], i, this);
}
}
return accumulator;
};

// 示例
const sum = arr.myReduce((acc, x) => acc + x, 0);
console.log(sum); // 6

4.3 实现 filter()

1
2
3
4
5
6
7
8
9
10
11
12
13
Array.prototype.myFilter = function(callback) {
const result = [];
for (let i = 0; i < this.length; i++) {
if (this.hasOwnProperty(i) && callback(this[i], i, this)) {
result.push(this[i]);
}
}
return result;
};

// 示例
const filtered = arr.myFilter(x => x > 1);
console.log(filtered); // [2, 3]

4.4 实现 push()

1
2
3
4
5
6
7
8
9
10
Array.prototype.myPush = function(...args) {
for (let i = 0; i < args.length; i++) {
this[this.length] = args[i];
}
return this.length;
};

// 示例
const lengthAfterPush = arr.myPush(4, 5);
console.log(arr, lengthAfterPush); // [1, 2, 3, 4, 5], 5

4.5 实现 pop()

1
2
3
4
5
6
7
8
9
10
Array.prototype.myPop = function() {
if (this.length === 0) return undefined;
const lastElement = this[this.length - 1];
this.length -= 1;
return lastElement;
};

// 示例
const lastElement = arr.myPop();
console.log(arr, lastElement); // [1, 2, 3, 4], 5

4.6 实现 sort()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Array.prototype.mySort = function(compareFunction) {
for (let i = 0; i < this.length - 1; i++) {
for (let j = 0; j < this.length - i - 1; j++) {
if (compareFunction ? compareFunction(this[j], this[j + 1]) > 0 : this[j] > this[j + 1]) {
[this[j], this[j + 1]] = [this[j + 1], this[j]];
}
}
}
return this;
};

// 示例
const sorted = [3, 1, 2].mySort((a, b) => a - b);
console.log(sorted); // [1, 2, 3]

4.7 实现 splice()

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
Array.prototype.mySplice = function(start, deleteCount, ...items) {
const deletedItems = [];
start = start < 0 ? Math.max(this.length + start, 0) : start;

for (let i = 0; i < deleteCount; i++) {
deletedItems.push(this[start + i]);
}

const tail = this.slice(start + deleteCount);
this.length = start;

for (let i = 0; i < items.length; i++) {
this[this.length] = items[i];
}

for (let i = 0; i < tail.length; i++) {
this[this.length] = tail[i];
}

return deletedItems;
};

// 示例
const spliced = arr.mySplice(1, 1, 99);
console.log(arr, spliced); // [1, 99, 3, 4], [2]

4.8 实现 new()

1
2
3
4
5
6
7
8
9
10
11
12
13
function myNew(constructor, ...args) {
const obj = Object.create(constructor.prototype);
const result = constructor.apply(obj, args);
return typeof result === 'object' && result !== null ? result : obj;
}

// 示例
function Person(name) {
this.name = name;
}

const person = myNew(Person, 'Alice');
console.log(person.name); // Alice

4.9 实现 bind()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.myBind = function(context, ...args) {
const fn = this;
return function(...innerArgs) {
return fn.apply(context, [...args, ...innerArgs]);
};
};

// 示例
function greet(greeting, name) {
return `${greeting}, ${name}`;
}

const sayHelloToAlice = greet.myBind(null, 'Hello', 'Alice');
console.log(sayHelloToAlice()); // Hello, Alice

4.10 实现 call()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function.prototype.myCall = function(context, ...args) {
context = context || globalThis;
const fnSymbol = Symbol();
context[fnSymbol] = this;
const result = context[fnSymbol](...args);
delete context[fnSymbol];
return result;
};

// 示例
function sayName(age) {
return `${this.name} is ${age} years old`;
}

const obj = { name: 'Alice' };
console.log(sayName.myCall(obj, 25)); // Alice is 25 years old

4.11 实现 apply()

1
2
3
4
5
6
7
8
9
10
11
Function.prototype.myApply = function(context, args) {
context = context || globalThis;
const fnSymbol = Symbol();
context[fnSymbol] = this;
const result = context[fnSymbol](...(args || []));
delete context[fnSymbol];
return result;
};

// 示例
console.log(sayName.myApply(obj, [30])); // Alice is 30 years old

‍5. 二分查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function binarySearch(arr, target) {
let left = 0;
let right = arr.length - 1;

while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) {
return mid; // 找到目标,返回索引
} else if (arr[mid] < target) {
left = mid + 1; // 目标在右侧
} else {
right = mid - 1; // 目标在左侧
}
}
return -1; // 未找到目标
}

// 示例
const sortedArray = [1, 2, 5, 5, 6, 9];
const target = 5;
console.log("二分查找结果索引:", binarySearch(sortedArray, target));

返回数组中首次出现目标数字的位置,没有则返回-1 . (二分)

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
function binarySearchFirstOccurrence(arr, target) {
let left = 0;
let right = arr.length - 1;
let result = -1; // 初始化为 -1 表示未找到

while (left <= right) {
const mid = Math.floor((left + right) / 2);

if (arr[mid] === target) {
result = mid; // 找到目标值,记录当前索引
right = mid - 1; // 继续向左查找,确保是首次出现
} else if (arr[mid] < target) {
left = mid + 1; // 目标值在右侧
} else {
right = mid - 1; // 目标值在左侧
}
}

return result;
}

// 示例用法
const arr = [1, 2, 2, 2, 3, 4, 5];
console.log(binarySearchFirstOccurrence(arr, 2)); // 输出: 1
console.log(binarySearchFirstOccurrence(arr, 3)); // 输出: 4
console.log(binarySearchFirstOccurrence(arr, 6)); // 输出: -1

6. 排序算法

1. 冒泡排序 (Bubble Sort)

原理
比较相邻的两个元素,如果它们的顺序错误就交换,重复此过程直到整个数组有序。

代码

1
2
3
4
5
6
7
8
9
10
11
function bubbleSort(arr) {
let n = arr.length;
for (let i = 0; i < n - 1; i++) {
for (let j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; // 交换
}
}
}
return arr;
}

时间复杂度

  • 最坏情况:O(n²)
  • 最好情况:O(n) (数组已排序)
  • 平均情况:O(n²)

空间复杂度:O(1)


2. 选择排序 (Selection Sort)

原理
在未排序部分中找到最小的元素,与当前未排序部分的第一个元素交换,逐步将数组分为已排序和未排序两部分。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
function selectionSort(arr) {
let n = arr.length;
for (let i = 0; i < n - 1; i++) {
let minIndex = i;
for (let j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]; // 交换
}
return arr;
}

时间复杂度

  • 最坏情况:O(n²)
  • 最好情况:O(n²)
  • 平均情况:O(n²)

空间复杂度:O(1)


3. 插入排序 (Insertion Sort)

原理
将数组分为已排序和未排序部分,将未排序部分的第一个元素插入到已排序部分的正确位置。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
function insertionSort(arr) {
let n = arr.length;
for (let i = 1; i < n; i++) {
let key = arr[i];
let j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
return arr;
}

时间复杂度

  • 最坏情况:O(n²)
  • 最好情况:O(n) (数组已排序)
  • 平均情况:O(n²)

空间复杂度:O(1)


4. 快速排序 (Quick Sort)

原理
选择一个基准元素,将数组分为两部分,小于基准的放左边,大于基准的放右边,递归排序两部分。

代码

1
2
3
4
5
6
7
8
function quickSort(arr) {
if (arr.length <= 1) return arr;
const pivot = arr[Math.floor(arr.length / 2)];
const left = arr.filter(x => x < pivot);
const right = arr.filter(x => x > pivot);
const middle = arr.filter(x => x === pivot);
return [...quickSort(left), ...middle, ...quickSort(right)];
}

or

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function quickSort(arr) {
if (arr.length <= 1) {
return arr; // 数组长度小于等于1时,直接返回
}
const pivot = arr[Math.floor(arr.length / 2)]; // 选择中间元素作为基准
const left = [];
const right = [];
const equal = [];

for (let num of arr) {
if (num < pivot) {
left.push(num); // 小于基准的元素放在左侧
} else if (num > pivot) {
right.push(num); // 大于基准的元素放在右侧
} else {
equal.push(num); // 等于基准的元素放在中间
}
}
return [...quickSort(left), ...equal, ...quickSort(right)];
}

// 示例
const array = [5, 2, 9, 1, 5, 6];
console.log("快速排序结果:", quickSort(array));

时间复杂度

  • 最坏情况:O(n²) (已排序数组时)
  • 最好情况:O(n log n)
  • 平均情况:O(n log n)

空间复杂度:O(n)(递归调用栈)


5. 归并排序 (Merge Sort)

原理
将数组分为两部分,递归排序两部分,然后合并两部分为一个有序数组。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function mergeSort(arr) {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
}

function merge(left, right) {
let result = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
result.push(left[i]);
i++;
} else {
result.push(right[j]);
j++;
}
}
return result.concat(left.slice(i)).concat(right.slice(j));
}

时间复杂度

  • 最坏情况:O(n log n)
  • 最好情况:O(n log n)
  • 平均情况:O(n log n)

空间复杂度:O(n)(辅助数组)


6. 堆排序 (Heap Sort)

原理
利用堆(最大堆或最小堆)的性质,反复取堆顶元素并调整堆,直到数组有序。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function heapSort(arr) {
const heapify = (arr, n, i) => {
let largest = i;
let left = 2 * i + 1;
let right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) largest = left;
if (right < n && arr[right] > arr[largest]) largest = right;
if (largest !== i) {
[arr[i], arr[largest]] = [arr[largest], arr[i]];
heapify(arr, n, largest);
}
};

let n = arr.length;
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
heapify(arr, n, i);
}
for (let i = n - 1; i > 0; i--) {
[arr[0], arr[i]] = [arr[i], arr[0]];
heapify(arr, i, 0);
}
return arr;
}

时间复杂度

  • 最坏情况:O(n log n)
  • 最好情况:O(n log n)
  • 平均情况:O(n log n)

空间复杂度:O(1)


7. TEMPLATE模版函数

1
2
3
4
5
6
7
function template(templateStr, obj) {
return templateStr.replace(/\{(\w+)\}/g, (_, key) => obj[key] || '');
}

// 示例用法:
const result = template("{name} is {age}", { name: "tom", age: 20 });
console.log(result); // 输出: "tom is 20"

解释:

  1. 正则表达式

    • \{(\w+)\}​ 是正则表达式,用于匹配模板字符串中形如 {key}​ 的占位符:
      • \{​ 和 \}​ 匹配花括号 {}​。
      • (\w+)​ 表示一个或多个由字母、数字或下划线组成的单词字符。括号表示捕获组,用于提取占位符中的键名(例如 name​ 或 age​)。
    • g​ 是全局匹配标志,表示替换模板字符串中的所有占位符。
  2. replace方法

    • replace​ 方法会查找模板字符串中的占位符,并通过提供的回调函数替换这些占位符。
    • 回调函数的参数:
      • 第一个参数 _​ 是完整匹配到的字符串,例如 {name}​。
      • 第二个参数 key​ 是捕获组的内容,例如 name​。
    • 在回调函数中,通过 obj[key]​ 获取对应的值。如果对象中不存在该键,则默认返回空字符串 ''​。
  3. 处理逻辑

    • 替换占位符 {key}​ 为 obj​ 中对应键的值,例如 {name}​ 被替换为 tom​。
    • 如果模板字符串中的键在 obj​ 中不存在,使用空字符串代替。

示例说明:

1
template("{name} is {age}", { name: "tom", age: 20 });
  • 模板字符串为 "{name} is {age}"​。
  • 对象为 { name: "tom", age: 20 }​。
  • 正则表达式匹配:
    • {name}​ 被替换为 tom​。
    • {age}​ 被替换为 20​。
  • 最终结果为 "tom is 20"​。

8. AJAX 的基本实现

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
function ajax({ method, url, data, headers = {}, async = true, success, error }) {
const xhr = new XMLHttpRequest(); // 创建 XMLHttpRequest 对象

// 处理 GET 请求的 URL 参数拼接
if (method.toUpperCase() === 'GET' && data) {
const queryParams = new URLSearchParams(data).toString();
url += (url.includes('?') ? '&' : '?') + queryParams;
}

xhr.open(method, url, async); // 初始化请求

// 设置请求头
for (const key in headers) {
xhr.setRequestHeader(key, headers[key]);
}

// 监听请求状态变化
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) { // 请求完成
if (xhr.status >= 200 && xhr.status < 300) {
success && success(xhr.responseText); // 成功回调
} else {
error && error(xhr.status, xhr.statusText); // 错误回调
}
}
};

// 发送请求
if (method.toUpperCase() === 'POST' && data) {
// 如果是 POST 请求,发送 JSON 数据
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
} else {
xhr.send(); // GET 请求无需发送请求体
}
}

// 示例用法
ajax({
method: 'GET',
url: '/api/users',
data: { id: 1, name: 'Alice' },
success: (response) => console.log('Success:', response),
error: (status, statusText) => console.error('Error:', status, statusText),
});

ajax({
method: 'POST',
url: '/api/login',
data: { username: 'admin', password: '123456' },
success: (response) => console.log('Logged in:', response),
error: (status, statusText) => console.error('Error:', status, statusText),
});

9. 实现instanceof

instanceof原理

  • instanceof​ 的主要作用是检查一个对象是否存在于某个构造函数的原型链上。

语法:

1
obj instanceof Constructor;

工作原理:

  1. 获取对象 obj​ 的原型链(__proto__​)。
  2. 依次向上查找,是否有原型等于构造函数 Constructor.prototype​。
  3. 如果找到相同的原型,返回 true​;如果到原型链顶端(null​)还未找到,返回 false​。

手写实现

可以根据原理,用循环实现一个简单的 instanceof​:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function myInstanceof(obj, constructor) {
// 如果 obj 是 null 或 undefined,直接返回 false
if (obj == null) return false;

// 获取对象的原型
let proto = Object.getPrototypeOf(obj);

// 遍历原型链
while (proto) {
if (proto === constructor.prototype) {
return true; // 找到匹配的原型
}
proto = Object.getPrototypeOf(proto); // 继续向上查找
}

return false; // 未找到匹配的原型
}

// 测试
console.log(myInstanceof([], Array)); // true
console.log(myInstanceof([], Object)); // true
console.log(myInstanceof({}, Array)); // false
console.log(myInstanceof(null, Object)); // false
console.log(myInstanceof(new Date(), Date)); // true

深入优化和特殊情况处理

1. 考虑非函数的构造函数

instanceof​ 的右操作数必须是一个函数,如果不是函数会抛出 TypeError。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function myInstanceof(obj, constructor) {
if (typeof constructor !== 'function') {
throw new TypeError('Right-hand side of instanceof is not callable');
}

if (obj == null) return false; // 处理 null 和 undefined

let proto = Object.getPrototypeOf(obj);
while (proto) {
if (proto === constructor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}

return false;
}

// 测试
try {
console.log(myInstanceof([], null)); // 抛出 TypeError
} catch (e) {
console.error(e.message); // 输出: Right-hand side of instanceof is not callable
}

2. 与原生 instanceof的一致性测试

1
2
3
4
console.log(myInstanceof([], Array) === ([] instanceof Array)); // true
console.log(myInstanceof([], Object) === ([] instanceof Object)); // true
console.log(myInstanceof(new Date(), Date) === (new Date() instanceof Date)); // true
console.log(myInstanceof(null, Object) === (null instanceof Object)); // true

10. 合并两个有序数组

Leetcode: 88. 合并两个有序数组

题目描述

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

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

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

示例 1:

输入: 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:

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

示例 3:

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

解答

1. 合并+排序

1
2
3
4
var merge = function(nums1, m, nums2, n) {
nums1.splice(m, nums1.length - m, ...nums2);
nums1.sort((a, b) => a - b);
}

2. 双指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var merge = function(nums1, m, nums2, n) {

let res = [];
let p = 0, q = 0;
while(p < m || q < n) {
if(p === m) {
res.push(nums2[q++]);
} else if (q === n) {
res.push(nums1[p++]);
} else if(nums1[p] <= nums2[q]) {
res.push(nums1[p++]);
} else {
res.push(nums2[q++]);
}
}
// nums1 = res;
for (let i = 0; i < m + n; ++i) {
nums1[i] = res[i];
}
};

3. 双指针(优化)

1
2
3
4
5
6
7
8
9
10
11
12
13
var merge = function(nums1, m, nums2, n) {
let p = m - 1;
let q = n - 1;
let i = m + n - 1;

while(q >= 0) {
if(p >= 0 && nums1[p] > nums2[q]) {
nums1[i--] = nums1[p--];
} else {
nums1[i--] = nums2[q--];
}
}
};

11. 移动元素

Leetcode: 27. 移除元素

题目描述

给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。

假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:

  • 更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。
  • 返回 k

用户评测:

评测机将使用以下代码测试您的解决方案:

int[] nums = [...]; // 输入数组
int val = ...; // 要移除的值
int[] expectedNums = [...]; // 长度正确的预期答案。
                            // 它以不等于 val 的值排序。

int k = removeElement(nums, val); // 调用你的实现

assert k == expectedNums.length;
sort(nums, 0, k); // 排序 nums 的前 k 个元素
for (int i = 0; i < actualLength; i++) {
    assert nums[i] == expectedNums[i];
}

如果所有的断言都通过,你的解决方案将会 通过

示例 1:

输入: nums = [3,2,2,3], val = 3
输出: 2, nums = [2,2,_,_]
解释: 你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。

示例 2:

输入: nums = [0,1,2,2,3,0,4,2], val = 2
输出: 5, nums = [0,1,4,0,3,_,_,_]
解释: 你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。
注意这五个元素可以任意顺序返回。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。

提示:

  • 0 <= nums.length <= 100
  • 0 <= nums[i] <= 50
  • 0 <= val <= 100

解答

1
2
3
4
5
6
7
8
9
10
var removeElement = function(nums, val) {  
let len = nums.length;
let k = 0;
for(let i = 0; i < len; i++) {
if(nums[i] !== val) {
nums[k++] = nums[i];
}
}
return k;
};

12. 删除有序数组中的重复项

Leetcode: 26. 删除有序数组中的重复项

题目描述

给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。

考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:

  • 更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
  • 返回 k 。

判题标准:

系统会用下面的代码来测试你的题解:

int[] nums = [...]; // 输入数组
int[] expectedNums = [...]; // 长度正确的期望答案

int k = removeDuplicates(nums); // 调用

assert k == expectedNums.length;
for (int i = 0; i < k; i++) {
    assert nums[i] == expectedNums[i];
}

如果所有断言都通过,那么您的题解将被 通过

 
示例 1:

输入: nums = [1,1,2]
输出: 2, nums = [1,2,_]
解释: 函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。

示例 2:

输入: nums = [0,0,1,1,1,2,2,3,3,4]
输出: 5, nums = [0,1,2,3,4]
解释: 函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。

提示:

  • 1 <= nums.length <= 3 * 104
  • -104 <= nums[i] <= 104
  • nums 已按 非严格递增 排列

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @param {number[]} nums
* @return {number}
*/
var removeDuplicates = function(nums) {
let n = nums.length;
// nums[0] 一定保留
let k = 1;
for (let i = 1; i < n; i++) {
if(nums[i] !== nums[k - 1]){
nums[k++] = nums[i];
}
}
return k;
};

13. 多数元素

Leetcode: 169. 多数元素

题目描述

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

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

示例 2:

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

提示:

  • n == nums.length
  • 1 <= n <= 5 * 104
  • -109 <= nums[i] <= 109

 进阶: 尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @param {number[]} nums
* @return {number}
*/
var majorityElement = function(nums) {
nums.sort((a, b) => a - b);
return nums[Math.floor(nums.length / 2)]
};

var majorityElement = function(nums) {
let half = nums.length / 2;
let map = new Map();
for(let n of nums) {
if(map.has(n)) {
let cur = map.get(n);
map.set(n, cur + 1);
} else {
map.set(n, 1);
}
if(map.get(n) > half) return n;
}
}

14. 轮转数组

Leetcode: 189. 轮转数组

题目描述

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

示例 1:

输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]

示例 2:

输入: nums = [-1,-100,3,99], k = 2
输出: [3,99,-1,-100]
解释: 
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]

提示:

  • 1 <= nums.length <= 105
  • -231 <= nums[i] <= 231 - 1
  • 0 <= k <= 105

进阶:

  • 尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
  • 你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?

解答

  1. 裁剪+拼接
1
2
3
4
5
6
7
var rotate = function(nums, k) {  
k = k % nums.length;
let f = nums.slice(0, nums.length - k);
let b = nums.slice(nums.length - k);
// nums = b.concat(f);
nums.splice(0, nums.length, ...b.concat(f));
};
  1. 翻转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var rotate = function(nums, k) {
k = k % nums.length; // 防止 k 大于数组长度
nums.reverse(); // 第一步:反转整个数组
reverse(nums, 0, k - 1); // 第二步:反转前 k 个元素
reverse(nums, k, nums.length - 1); // 第三步:反转剩余部分
};

function reverse(arr, start, end) {
while (start < end) {
[arr[start], arr[end]] = [arr[end], arr[start]]; // 交换两个元素
start++;
end--;
}
}

15. [!] 无重复字符的最长子串

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

题目描述

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

子串

****的长度。

示例 1:

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

示例 2:

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

示例 3:

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

提示:

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

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
let occ = new Set();
const len = s.length;
let r = 0, maxLen = 0;
for(let l = 0; l < len; l++){
if(l !== 0) occ.delete(s.charAt(l - 1));
while(r < len && !occ.has(s.charAt(r))) {
occ.add(s.charAt(r));
r++;
}
maxLen = Math.max(maxLen, r - l);
}
return maxLen;
};

16. 两数之和

Leetcode: 1. 两数之和

题目描述

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

你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。

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

示例 1:

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

示例 2:

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

示例 3:

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

提示:

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

进阶: 你可以想出一个时间复杂度小于 $O(n^2)$ 的算法吗?

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
let hashmap = {};
// key: number
// val: number index
// for(i in nums) {
for (let i = 0; i < nums.length; i++) {
let n = target - nums[i];
if(hashmap.hasOwnProperty(n)) {
return [hashmap[n], i];
}
else {
hashmap[nums[i]] = i;
}
}
return [];
};

17. [进阶] 三数之和

Leetcode: 15. 三数之和

题目描述

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

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

示例 1:

输入: 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:

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

示例 3:

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

提示:

  • $3 <= nums.length <= 3000$
  • $-10^5 <= nums[i] <= 10^5$

解答

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
/**
* @param {number[]} nums
* @return {number[][]}
*/
/**
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function(nums) {
let ans = [];
const len = nums.length;
if(nums == null || len < 3) return ans;
nums.sort((a, b) => a - b); // 排序
for (let i = 0; i < len ; i++) {
if(nums[i] > 0) break; // 如果当前数字大于0,则三数之和一定大于0,所以结束循环
if(i > 0 && nums[i] == nums[i-1]) continue; // 去重
let L = i+1;
let R = len-1;
while(L < R){
const sum = nums[i] + nums[L] + nums[R];
if(sum == 0){
ans.push([nums[i],nums[L],nums[R]]);
while (L<R && nums[L] == nums[L+1]) L++; // 去重
while (L<R && nums[R] == nums[R-1]) R--; // 去重
L++;
R--;
}
else if (sum < 0) L++;
else if (sum > 0) R--;
}
}
return ans;
};

18. 合并区间

Leetcode: 56. 合并区间

题目描述

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。

示例 1:

输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例 2:

输入: intervals = [[1,4],[4,5]]
输出: [[1,5]]
解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。

提示:

  • 1 <= intervals.length <= 104
  • intervals[i].length == 2
  • 0 <= starti <= endi <= 104

解答

  • 思路: 首先,我们将列表中的区间按照左端点升序排序。然后我们将第一个区间加入 merged 数组中,并按顺序依次考虑之后的每个区间:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @param {number[][]} intervals
* @return {number[][]}
*/
var merge = function(intervals) {
if(intervals.length === 0) return [];
intervals = intervals.sort((a, b) => {
return a[0] - b[0];
})
// console.log(intervals);
let merged = [];
for(let i = 0; i < intervals.length; i++){
let l = intervals[i][0];
let r = intervals[i][1];
if(merged.length === 0 || merged[merged.length - 1][1] < l){
merged.push([l,r]);
} else {
merged[merged.length - 1][1] = Math.max(merged[merged.length - 1][1], r)
}
}
return merged;
};

19. 有效的括号

Leetcode: 20. 有效的括号

题目描述

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

 

示例 1:

  • 输入: s = "()"
  • 输出: true

示例 2:

  • 输入: s = "()[]{}"
  • 输出: true

示例 3:

  • 输入: s = "(]"
  • 输出: false

示例 4:

  • 输入: s = "([])"
  • 输出: true

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @param {string} s
* @return {boolean}
*/
var isValid = function(s) {
let stack = [];
for(let i = 0; i < s.length; i++) {
const c = s.charAt(i);
if(c === '(' || c === '[' || c === '{') stack.push(c);
else if (c === ')' && stack.pop() !== '(') return false;
else if (c === ']' && stack.pop() !== '[') return false;
else if (c === '}' && stack.pop() !== '{') return false;
}
return !stack.length;
};

20. 环形链表

Leetcode: 141. 环形链表

题目描述

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

解答

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
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/

/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function(head) {
let f = s = head;
while(f && f.next){
f = f.next.next;
s = s.next;
if(s === f)
{
return true;
}
}
return false;

};

21. 两数相加

Leetcode: 2. 两数相加

题目描述

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 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
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
var addTwoNumbers = function(l1, l2) {
let ans = new ListNode();
let current = ans;
let d = 0;
while(l1 || l2){
const n1 = l1 ? l1.val : 0;
const n2 = l2 ? l2.val : 0;
d = d + n1 + n2;
// console.log("dd "+d);
// current.val = (l1.val + l2.val + d) % 10;
// d = (l1.val + l2.val + d) / 10;
current.next = new ListNode(d % 10);
// console.log("DD "+d%10)
current = current.next;
d = Math.floor(d / 10);
if (l1) {
l1 = l1.next;
}
if (l2) {
l2 = l2.next;
}
}
if(d){
current.next = new ListNode(d);
}
return ans.next;

};

22. 合并两个有序链表

Leetcode: 21. 合并两个有序链表

题目描述

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

解答

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
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} list1
* @param {ListNode} list2
* @return {ListNode}
*/
var mergeTwoLists = function(list1, list2) {
let m = list1;
let n = list2;
let ans = new ListNode();
let a = ans;
while(m && n){
if(m.val < n.val){
a.next = m;
m = m.next;
}
else{
a.next = n;
n = n.next;
}
a = a.next;
}
if(m){
a.next = m;
} else if(n){
a.next = n;
}
return ans.next;
};

23. 反转链表

Leetcode:

题目描述

  1. 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
  2. 给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
  3. 给你链表的头节点 head ,每 k **个节点一组进行翻转,请你返回修改后的链表。
    • k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k **的整数倍,那么请将最后剩余的节点保持原有顺序。
    • 你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

解答

  1. easy
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
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
if(head === null){
return null;
}
let cur = head;
let pre = null;
while(cur !== null){
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
};
  1. medium
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
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} left
* @param {number} right
* @return {ListNode}
*/
var reverseBetween = function(head, left, right) {
// 1 -> 2 -> [3 -> 4 -> 5] -> 6 -> 7
// 1 -> 2 -> [5 -> 4 -> 3] -> 6 -> 7
// start: 2
// end: 3
// last step: pre: 5 & cur: 6
let p = dummyHead = new ListNode();
p.next = head;
let pre, cur, start, end;
for(let i = 0; i < left - 1; i++) {
p = p.next;
}
start = p;
end = pre = p.next;
cur = pre.next;
for(let i = 0; i < right - left; i++) {
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
start.next = pre;
end.next = cur;
return dummyHead.next;
};
  1. hard
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
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} k
* @return {ListNode}
*/
var reverseKGroup = function(head, k) {
let pre = null, cur = head;
let p = head;
for(let i = 0; i < k; i ++){
if(p == null) return head;
p = p.next;
}
for(let i = 0; i < k; i ++){
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
head.next = reverseKGroup(cur, k);
return pre;
};

24. 二叉树的层序遍历相关

二叉树的最大深度

Leetcode: 104. 二叉树的最大深度

题目描述

给定一个二叉树 root ,返回其最大深度。

二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数

示例:

image.png

输入: root = [3,9,20,null,null,15,7]
输出: 3

解答

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
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var maxDepth = function(root) {
if(root == null) {
return 0;
}
/* 递归
return Math.max(maxDepth(root.left)+1, maxDepth(root.right)+1);
*/
let queue = [root];
let lev = 0;
while(queue.length) {
let size = queue.length;
while(size --) {
let front = queue.shift();
if(front.left) queue.push(front.left);
if(front.right) queue.push(front.right);
}
lev ++;
}
return lev;
};

二叉树的层序遍历

Leetcode: 102. 二叉树的层序遍历

题目描述

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

示例:

image.png

输入: root = [3,9,20,null,null,15,7]
输出: [[3],[9,20],[15,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
26
27
28
29
30
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
*/
var levelOrder = function (root) {
if (!root) return [];
let queue = [root];
let res = [];
let level = 0;
while (queue.length) {
res.push([]);
let size = queue.length;
while (size --) {
let cur = queue.shift(); // 当前节点出队
res[level].push(cur.val);
if (cur.left) queue.push(cur.left); // 左儿子入队
if (cur.right) queue.push(cur.right); // 右儿子入队
}
level ++;
}
return res;
};

二叉树的右视图

Leetcode: 199. 二叉树的右视图

题目描述

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

示例 1:

  • 输入: root = [1,2,3,null,5,null,4]
  • 输出: [1,3,4]

解释:

示例 2:

  • 输入: root = [1,2,3,4,null,null,null,5]
  • 输出: [1,3,4,5]

解释:

示例 3:

  • 输入: root = [1,null,3]
  • 输出: [1,3]

示例 4:

  • 输入: root = []
  • 输出: []

提示:

  • 二叉树的节点个数的范围是 [0,100]
  • -100 <= Node.val <= 100

解答

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
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var rightSideView = function (root) {
if (!root) return [];
let queue = [root];
let res = [];
while (queue.length) {
res.push(queue[0].val); // 一层只取队首(即最右侧节点)
let size = queue.length;
while (size --) {
let cur = queue.shift(); // 当前节点出队
if (cur.right) queue.push(cur.right); // 右先儿子入队
if (cur.left) queue.push(cur.left); // 左儿子入队
}
}
return res;
};

25. [回溯与全排列] 无重复字符串的排列组合

Leetcode: 面试题 08.07. 无重复字符串的排列组合

题目描述

无重复字符串的排列组合。编写一种方法,计算某字符串的所有排列组合,字符串每个字符均不相同。

示例 1:

 输入:S = "qwe"
输出:["qwe", "qew", "wqe", "weq", "ewq", "eqw"]

示例 2:

 输入:S = "ab"
输出:["ab", "ba"]

提示:

  1. 字符都是英文字母。
  2. 字符串长度在[1, 9]之间。

解答

  • 思路:

    image.png

    图片取自RyanSKJ题解

  • 代码实现:

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
/**
* @param {string} S
* @return {string[]}
*/
var permutation = function(S) {
// string to array
let temp = S.split('');
let len = temp.length;
let res = [];
dfs(0);
return res;

// 定义DFS递归函数
function dfs(k) {
// end condition
if(k === len) {
res.push(temp.join(''));
return;
}
for(let i = k; i < len; i++) {
[temp[i], temp[k]] = [temp[k], temp[i]];
dfs(k + 1);
[temp[i], temp[k]] = [temp[k], temp[i]];
}
}
};

全排列实现(LCR 083. 全排列)

给定一个不含重复数字的整数数组 nums ,返回其 所有可能的全排列 。可以 按任意顺序 返回答案。

示例 1:

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

示例 2:

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

示例 3:

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

注意:
res.push([...nums]) 中使用扩展运算符生成 nums 的副本,确保结果中存储的每个排列是独立的,不会受到后续递归的修改影响

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
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permute = function(nums) {
// this.nums = nums;
let len = nums.length;
let res = [];
dfs(0);
return res;

// 定义DFS递归函数
function dfs(k) {
// end condition
if(k === len) {
res.push([...nums]);
return;
}
for(let i = k; i < len; i++) {
[nums[i], nums[k]] = [nums[k], nums[i]];
dfs(k + 1);
[nums[i], nums[k]] = [nums[k], nums[i]];
}
}
};

To Be Continue…

Read More

2024-12-24
前端面试准备

文章已加密,查看请联系博主!
Read More

2024-10-31
Learning "Build a Large Language Model (From Scratch)" Chapter 02

2.1 Understanding word embeddings

2.2 Tokenizing text

2.3 Converting tokens into token IDs

2.4 Adding special context tokens

2.5 Byte pair encoding

2.6 Data sampling with a sliding window

The previous section covered the tokenization steps and conversion from string tokens into integer token IDs in great detail. The next step before we can finally create the embeddings for the LLM is to generate the input-target pairs required for training an LLM.
前一节详细介绍了分词步骤和将字符串词元转换为整数词元ID的过程。在最终为LLM创建嵌入之前,下一步是生成训练LLM所需的输入-目标对。

What do these input-target pairs look like? As we learned in chapter 1, LLMs are pretrained by predicting the next word in a text, as depicted in figure 2.12.
这些输入-目标对是什么样的?正如我们在第1章中学到的,LLM通过预测文本中的下一个单词进行预训练,如图2.12所示。

Figure 2.12 Given a text sample, extract input blocks as subsamples that serve as input to the LLM, and the LLM’s prediction task during training is to predict the next word that follows the input block. During training, we mask out all words that are past the target. Note that the text shown in this figure would undergo tokenization before the LLM can process it; however, this figure omits the tokenization step for clarity.
图2.12 给定一个文本样本,提取输入块作为子样本,作为LLM的输入,LLM在训练期间的预测任务是预测紧跟在输入块后的下一个单词。在训练期间,我们屏蔽了所有超出目标的单词。请注意,图中显示的文本在LLM处理之前会进行分词;然而,为了清晰起见,本图省略了分词步骤。

In this section we implement a data loader that fetches the input-target pairs depicted in Figure 2.12 from the training dataset using a sliding window approach.
在本节中,我们实现了一个数据加载器,该加载器使用滑动窗口方法从训练数据集中获取图2.12中描绘的输入-目标对。

To get started, we will first tokenize the whole The Verdict short story we worked with earlier using the BPE tokenizer introduced in the previous section:
首先,我们将使用上一节介绍的BPE分词器对整个The Verdict短篇小说进行分词:

1
2
3
4
5
6
with open("the-verdict.txt", "r", encoding="utf-8") as f:  # 打开文本文件
raw_text = f.read() # 读取文本内容

enc_text = tokenizer.encode(raw_text) # 对文本进行编码
print(len(enc_text)) # 打印编码后的词元数量
# 执行上述代码将返回5145,这是应用BPE分词器后的训练集中词元的总数量。

Executing the code above will return 5145, the total number of tokens in the training set, after applying the BPE tokenizer.
执行上述代码将返回5145,这是应用BPE分词器后的训练集中词元的总数量。

Next, we remove the first 50 tokens from the dataset for demonstration purposes as it results in a slightly more interesting text passage in the next steps:
接下来,我们从数据集中移除前50个词元以进行演示,因为这会在接下来的步骤中生成一个稍微有趣的文本段落:

1
enc_sample = enc_text[50:]  # 移除前50个词元

One of the easiest and most intuitive ways to create the input-target pairs for the next-word prediction task is to create two variables, x and y, where x contains the input tokens and y contains the targets, which are the inputs shifted by 1:
创建输入-目标对进行下一个单词预测任务的最简单和最直观的方法之一是创建两个变量,x 和 y,其中 x 包含输入词元,y 包含目标,即输入右移1位:

1
2
3
4
5
6
7
8
context_size = 4  # 上下文大小确定输入中包含的词元数量  #A
x = enc_sample[:context_size] # 获取上下文大小的输入
y = enc_sample[1:context_size+1] # 获取右移1位的目标
print(f"x: {x}") # 打印输入
print(f"y: {y}") # 打印目标
# 运行上述代码打印以下输出:
# x: [290, 4920, 2241, 287]
# y: [4920, 2241, 287, 257]

Running the above code prints the following output:
运行上述代码打印以下输出:

1
2
3
x: [290, 4920, 2241, 287]
y: [4920, 2241, 287, 257]
# 处理包含目标的输入,目标是输入右移一个位置,然后我们可以创建图2.12中所描绘的下一个单词预测任务,如下所示:

Processing the inputs along with the targets, which are the inputs shifted by one position, we can then create the next-word prediction tasks depicted earlier in figure 2.12, as follows:
处理包含目标的输入,目标是输入右移一个位置,然后我们可以创建图2.12中所描绘的下一个单词预测任务,如下所示:

1
2
3
4
5
6
7
8
9
for i in range(1, context_size+1):  # 遍历上下文大小
context = enc_sample[:i] # 获取当前上下文
desired = enc_sample[i] # 获取目标词元
print(context, "---->", desired) # 打印上下文和目标
# 上述代码打印以下内容:
# [290] ----> 4920
# [290, 4920] ----> 2241
# [290, 4920, 2241] ----> 287
# [290, 4920, 2241, 287] ----> 257

The code above prints the following:
上述代码打印以下内容:

1
2
3
4
5
[290] ----> 4920
[290, 4920] ----> 2241
[290, 4920, 2241] ----> 287
[290, 4920, 2241, 287] ----> 257
# 箭头(---->)左边的所有内容表示LLM将接收的输入,箭头右边的词元ID表示LLM应预测的目标词元ID。

Everything left of the arrow (—->) refers to the input an LLM would receive, and the token ID on the right side of the arrow represents the target token ID that the LLM is supposed to predict.
箭头(—->)左边的所有内容表示LLM将接收的输入,箭头右边的词元ID表示LLM应预测的目标词元ID。

For illustration purposes, let’s repeat the previous code but convert the token IDs into text:
为了说明,我们重复前面的代码,但将词元ID转换为文本:

1
2
3
4
5
6
7
8
9
for i in range(1, context_size+1):  # 遍历上下文大小
context = enc_sample[:i] # 获取当前上下文
desired = enc_sample[i] # 获取目标词元
print(tokenizer.decode(context), "---->", tokenizer.decode([desired])) # 打印解码后的上下文和目标
# 下面的输出显示了输入和输出在文本格式中的样子:
# and ----> established
# and established ----> himself
# and established himself ----> in
# and established himself in ----> a

The following outputs show how the input and outputs look in text format:
下面的输出显示了输入和输出在文本格式中的样子:

1
2
3
4
5
and ----> established
and established ----> himself
and established himself ----> in
and established himself in ----> a
# 我们现在已经创建了输入-目标对,可以将它们用于LLM训练。

We’ve now created the input-target pairs that we can turn into use for the LLM training in upcoming chapters.
我们现在已经创建了输入-目标对,可以将它们用于LLM训练。

There’s only one more task before we can turn the tokens into embeddings, as we mentioned at the beginning of this chapter: implementing an efficient data loader that iterates over the input dataset and returns the inputs and targets as PyTorch tensors, which can be thought of as multidimensional arrays.
在我们将词元转换为嵌入之前,还有最后一个任务:实现一个高效的数据加载器,它遍历输入数据集并将输入和目标作为PyTorch张量返回,可以将其视为多维数组。

In particular, we are interested in returning two tensors: an input tensor containing the text that the LLM sees and a target tensor that includes the targets for the LLM to predict, as depicted in Figure 2.13.

特别是,我们希望返回两个张量:一个包含LLM看到的文本的输入张量,另一个包含LLM要预测的目标的目标张量,如图2.13所示。

Figure 2.13 To implement efficient data loaders, we collect the inputs in a tensor, x, where each row represents one input context. A second tensor, y, contains the corresponding prediction targets (next words), which are created by shifting the input by one position.

图2.13 为了实现高效的数据加载器,我们将输入收集到一个张量x中,每行表示一个输入上下文。第二个张量y包含相应的预测目标(下一个单词),通过将输入右移一个位置创建。

While Figure 2.13 shows the tokens in string format for illustration purposes, the code implementation will operate on token IDs directly since the encode method of the BPE tokenizer performs both tokenization and conversion into token IDs as a single step.

虽然图2.13出于说明目的以字符串格式显示词元,但代码实现将直接对词元ID进行操作,因为BPE分词器的编码方法将分词和转换为词元ID作为一个步骤执行。

For the efficient data loader implementation, we will use PyTorch’s built-in Dataset and DataLoader classes. For additional information and guidance on installing PyTorch, please see section A.1.3, Installing PyTorch, in Appendix A.

为了实现高效的数据加载器,我们将使用PyTorch的内置Dataset和DataLoader类。有关安装PyTorch的更多信息和指导,请参见附录A的A.1.3节“安装PyTorch”。

Listing 2.5 A dataset for batched inputs and targets
清单2.5 用于批处理输入和目标的数据集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import torch  # 导入torch
from torch.utils.data import Dataset, DataLoader # 从torch.utils.data导入Dataset和DataLoader

class GPTDatasetV1(Dataset): # 定义GPTDatasetV1类,继承自Dataset
def __init__(self, txt, tokenizer, max_length, stride): # 初始化方法
self.input_ids = [] # 初始化输入ID列表
self.target_ids = [] # 初始化目标ID列表

token_ids = tokenizer.encode(txt) # 对文本进行分词

for i in range(0, len(token_ids) - max_length, stride): # 使用滑动窗口将文本分块
input_chunk = token_ids[i:i + max_length] # 获取输入分块
target_chunk = token_ids[i + 1: i + max_length + 1] # 获取目标分块
self.input_ids.append(torch.tensor(input_chunk)) # 将输入分块转换为张量并添加到列表中
self.target_ids.append(torch.tensor(target_chunk)) # 将目标分块转换为张量并添加到列表中

def __len__(self): # 返回数据集的长度
return len(self.input_ids) # 返回输入ID的长度

def __getitem__(self, idx): # 获取指定索引的数据
return self.input_ids[idx], self.target_ids[idx] # 返回输入和目标的张量

The GPTDatasetV1 class in listing 2.5 is based on the PyTorch Dataset class and defines how individual rows are fetched from the dataset, where each row consists of a number of token IDs (based on a max_length) assigned to an input_chunk tensor. The target_chunk tensor contains the corresponding targets. I recommend reading on to see how the data returned from this dataset looks like when we combine the dataset with a PyTorch DataLoader – this will bring additional intuition and clarity.

清单2.5中的GPTDatasetV1类基于PyTorch的Dataset类,定义了如何从数据集中获取单个行,其中每行由分配给input_chunk张量的一定数量的词元ID(基于max_length)组成。target_chunk张量包含相应的目标。我建议继续阅读以了解当我们将数据集与PyTorch的DataLoader结合使用时,从该数据集中返回的数据是什么样子的,这将带来更多的直观理解和清晰度。

If you are new to the structure of PyTorch Dataset classes, such as shown in listing 2.5, please read section A.6, Setting up efficient data loaders, in Appendix A, which explains the general structure and usage of PyTorch Dataset and DataLoader classes.

如果你不熟悉PyTorch的Dataset类的结构,如清单2.5所示,请阅读附录A的A.6节“设置高效的数据加载器”,其中解释了PyTorch的Dataset和DataLoader类的一般结构和用法。

The following code will use the GPTDatasetV1 to load the inputs in batches via a PyTorch DataLoader:

以下代码将使用GPTDatasetV1通过PyTorch的DataLoader按批次加载输入:

Listing 2.6 A data loader to generate batches with input-with pairs

清单2.6 一个用于生成输入-目标对批处理的数据加载器

1
2
3
4
5
6
7
8
9
10
11
12
def create_dataloader_v1(txt, batch_size=4, max_length=256,  # 创建数据加载器
stride=128, shuffle=True, drop_last=True, num_workers=0):
tokenizer = tiktoken.get_encoding("gpt2") # 初始化分词器
dataset = GPTDatasetV1(txt, tokenizer, max_length, stride) # 创建数据集
dataloader = DataLoader( # 创建数据加载器
dataset,
batch_size=batch_size,
shuffle=shuffle,
drop_last=drop_last, # 如果最后一个批次小于指定批次大小,则丢弃
num_workers=num_workers # 使用的CPU进程数量
)
return dataloader # 返回数据加载器

Let’s test the dataloader with a batch size of 1 for an LLM with a context size of 4 to develop an intuition of how the GPTDatasetV1 class from listing 2.5 and the create_dataloader_v1 function from listing 2.6 work together:

让我们使用批次大小为1的数据加载器来测试具有上下文大小为4的LLM,以了解清单2.5中的GPTDatasetV1类和清单2.6中的create_dataloader_v1函数如何协同工作:

1
2
3
4
5
6
7
8
9
10
11
with open("the-verdict.txt", "r", encoding="utf-8") as f:  # 打开文本文件
raw_text = f.read() # 读取文本内容

dataloader = create_dataloader_v1( # 创建数据加载器
raw_text, batch_size=1, max_length=4, stride=1, shuffle=False
)
data_iter = iter(dataloader) # 将数据加载器转换为Python迭代器 #A
first_batch = next(data_iter) # 获取下一个批次的数据
print(first_batch) # 打印第一个批次的数据
# 执行上述代码打印以下内容:
# (tensor([[ 40, 367, 2885, 1464]]), tensor([[ 367, 2885, 1464, 1807]]))

Executing the preceding code prints the following:

执行上述代码打印以下内容:

1
2
(tensor([[  40,  367, 2885, 1464]]), tensor([[ 367, 2885, 1464, 1807]]))
# first_batch变量包含两个张量:第一个张量存储输入词元ID,第二个张量存储目标词元ID。

The first_batch variable contains two tensors: the first tensor stores the input token IDs, and the second tensor stores the target token IDs. Since the max_length is set to 4, each of the two tensors contains 4 token IDs. Note that an input size of 4 is relatively small and only chosen for illustration purposes. It is common to train LLMs with input sizes of at least 256.

first_batch变量包含两个张量:第一个张量存储输入词元ID,第二个张量存储目标词元ID。由于max_length设置为4,每个张量包含4个词元ID。请注意,4的输入大小相对较小,仅用于说明目的。训练LLM时通常使用至少256的输入大小。

To illustrate the meaning of stride=1, let’s fetch another batch from this dataset:

为了说明stride=1的含义,让我们从这个数据集中获取另一个批次:

1
2
3
4
second_batch = next(data_iter)  # 获取下一个批次的数据
print(second_batch) # 打印第二个批次的数据
# 第二个批次包含以下内容:
# (tensor([[ 367, 2885, 1464, 1807]]), tensor([[2885, 1464, 1807, 3619]]))

The second batch has the following contents:
第二个批次包含以下内容:

1
2
(tensor([[  367, 2885, 1464, 1807]]), tensor([[2885, 1464, 1807, 3619]]))
# 如果我们将第一个批次与第二个批次进行比较,可以看到第二个批次的词元ID与第一个批次相比右移了一个位置

If we compare the first with the second batch, we can see that the second batch’s token IDs are shifted by one position compared to the first batch (for example, the second ID in the first batch’s input is 367, which is the first ID of the second batch’s input). The stride setting dictates the number of positions the inputs shift across batches, emulating a sliding window approach, as demonstrated in Figure 2.14.

如果我们将第一个批次与第二个批次进行比较,可以看到第二个批次的词元ID与第一个批次相比右移了一个位置(例如,第一个批次输入的第二个ID是367,这是第二个批次输入的第一个ID)。stride设置决定了输入在批次之间移动的位置数,模拟滑动窗口方法,如图2.14所示。

Figure 2.14 When creating multiple batches from the input dataset, we slide an input window across the text. If the stride is set to 1, we shift the input window by 1 position when creating the next batch. If we set the stride equal to the input window size, we can prevent overlaps between the batches.

图2.14 当从输入数据集创建多个批次时,我们在文本中滑动输入窗口。如果stride设置为1,则在创建下一个批次时将输入窗口右移1个位置。如果我们将stride设置为等于输入窗口大小,可以防止批次之间的重叠。

image-20241124014352590

EXERCISE 2.2 DATA LOADERS WITH DIFFERENT STRIDES AND CONTEXT SIZES
练习 2.2 具有不同stride和context大小的数据加载器

To develop more intuition for how the data loader works, try to run it with different settings such as max_length=2 and stride=2 and max_length=8 and stride=2.

为了更好地理解数据加载器的工作原理,请尝试使用不同的设置运行它,例如max_length=2和stride=2以及max_length=8和stride=2。

Batch sizes of 1, such as we have sampled from the data loader so far, are useful for illustration purposes. If you have previous experience with deep learning, you may know that small batch sizes require less memory during training but lead to more noisy model updates. Just like in regular deep learning, the batch size is a trade-off and hyperparameter to experiment with when training LLMs.

批次大小为1,如我们迄今从数据加载器中采样的那些,非常适合作为说明。如果你有深度学习的经验,你可能知道小批次大小在训练期间需要更少的内存,但会导致更噪声的模型更新。就像在常规深度学习中一样,批次大小是一种权衡和超参数,在训练LLM时需要进行实验。

Before we move on to the two final sections of this chapter that are focused on creating the embedding vectors from the token IDs, let’s have a brief look at how

we can use the data loader to sample with a batch size greater than 1:

在我们进入本章的最后两个部分之前,这两个部分重点是从词元ID创建嵌入向量,让我们简要了解如何使用数据加载器以大于1的批次大小进行采样:

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
dataloader = create_dataloader_v1(raw_text, batch_size=8, max_length=4, stride=4)

data_iter = iter(dataloader) # 将数据加载器转换为Python迭代器
inputs, targets = next(data_iter) # 获取下一个批次的数据
print("Inputs:\n", inputs) # 打印输入
print("\nTargets:\n", targets) # 打印目标
# 上面的代码打印以下内容:
# Inputs:
# tensor([[ 40, 367, 2885, 1464],
# [ 1807, 3619, 402, 271],
# [10899, 2138, 257, 7026],
# [15632, 438, 2016, 257],
# [ 922, 5891, 1576, 438],
# [ 568, 340, 373, 645],
# [ 1049, 5975, 284, 502],
# [ 284, 3285, 326, 11]])
#
# Targets:
# tensor([[ 367, 2885, 1464, 1807],
# [ 3619, 402, 271, 10899],
# [ 2138, 257, 7026, 15632],
# [ 438, 2016, 257, 922],
# [ 5891, 1576, 438, 568],
# [ 340, 373, 645, 1049],
# [ 5975, 284, 502, 284],
# [ 3285, 326, 11, 287]])

Note that we increase the stride to 4. This is to utilize the data set fully (we don’t skip a single word) but also avoid any overlap between the batches, since more overlap could lead to increased overfitting.

注意,我们将步幅增加到4。这是为了充分利用数据集(我们不会跳过一个单词),但也避免了批次之间的任何重叠,因为更多的重叠可能导致过拟合的增加。

In the final two sections of this chapter, we will implement embedding layers that convert the token IDs into continuous vector representations, which serve as input data format for LLMs.

在本章的最后两节中,我们将实现嵌入层,将词元ID转换为连续的向量表示,作为LLM的输入数据格式。

2.7 Creating token embeddings

2.8 Encoding word positions

Read More

2024-09-29
Learning "Build a Large Language Model (From Scratch)"

《Build a Large Language Model (From Scratch)》


Setup

参考

  • setup/01_optional-python-setup-preferences
  • .setup/02_installing-python-libraries

按照步骤配置环境:

1
2
3
4
5
6
git clone --depth 1 https://github.com/rasbt/LLMs-from-scratch.git
cd LLMs-from-scratch
conda create -n LLMs python=3.10
conda activate LLMs
conda install jupyterlab watermark
pip install -r requirements.txt

在这里遇到了以下问题:

image-20240929162323569

解决方案:

1
hash -r
image-20240929162500733

解释:

  • hash 是一个 Bash 内建命令,用于查找并记住命令的位置。如果你在安装了新的软件之后,想要立即使用它,但是 Bash 仍然使用旧的命令,那么你可以使用 hash -r 命令来刷新 Bash 的命令缓存。

Chapter 01

Chapter 02

目录:

  • 2.1 Understanding word embeddings
  • 2.2 Tokenizing text
  • 2.3 Converting tokens into token IDs
  • 2.4 Adding special context tokens
  • 2.5 Byte pair encoding
  • 2.6 Data sampling with a sliding window
  • 2.7 Creating token embeddings
  • 2.8 Encoding word positions
  • 2.9 Summary

背景知识

  1. 什么是Token?
  • Token是指文本的最小单位,可以是一个字、一个单词或一个子词。
    • 例如,句子 “I love NLP” 可以被分解为 [“I”, “love”, “NLP”]。
  1. 什么是Token ID?
    • 每个Token会被映射到一个唯一的数字ID。
    • 假设我们有一个词汇表:[“I”, “love”, “NLP”, “AI”],其中:
    • “I” 的ID是0
    • “love” 的ID是1
    • “NLP” 的ID是2
    • “AI” 的ID是3
  2. 为什么需要嵌入(Embedding)?
    • 模型无法直接处理文字或数字ID,我们需要将这些ID转为具有实际意义的向量(连续值表示)。
    • 比如,“NLP”的ID是2,我们可能需要一个三维向量 [1.2, -0.4, 0.6] 来表示它。

2.6 数据采样与滑动窗口

1
dataloader = create_dataloader_v1(raw_text, batch_size=8, max_length=4, stride=4)

其中参数:

  • raw_text 是原始文本。
  • batch_size 是批量大小。
  • max_length 是滑动窗口的大小。
  • stride 是滑动窗口的步长。

2.7 Token Embedding 创建

步骤1:定义Token ID和词汇表

假设我们有4个Token,其ID是 [2, 3, 5, 1],词汇表的大小为6。

import torch

input_ids = torch.tensor([2, 3, 5, 1]) # Token的ID
vocab_size = 6 # 假设词汇表有6个单词
output_dim = 3 # 我们希望嵌入是3维向量

- input_ids 是一个张量,表示我们的输入文本。
- vocab_size 是词汇表的大小,也就是可能的Token ID的总数。
- output_dim 是嵌入向量的维度。

步骤2:创建嵌入层

torch.manual_seed(123) # 设置随机种子,结果可复现
embedding_layer = torch.nn.Embedding(vocab_size, output_dim) # 创建嵌入层

- torch.nn.Embedding 是PyTorch提供的嵌入层,用于将Token ID映射为向量。
- 嵌入层的权重矩阵是随机初始化的。

权重矩阵的形状为 (vocab_size, output_dim)。
假设随机初始化后为:

tensor([[ 0.1, -0.2, 0.3],
[ 0.4, 0.5, -0.6],
[ 0.7, -0.8, 0.9],
[ 1.0, -1.1, 1.2],
[-1.3, 1.4, -1.5],
[ 1.6, -1.7, 1.8]])

- 每一行是一个Token ID对应的嵌入向量。
- 比如,Token ID为2的嵌入向量是 [0.7, -0.8, 0.9]。

步骤3:查询嵌入向量

embedding_vector = embedding_layer(torch.tensor([2]))
print(embedding_vector)

输出:

tensor([[0.7, -0.8, 0.9]])

解释:

- 这段代码从嵌入层中查找Token ID为2的向量,也就是矩阵中的第3行(索引从0开始)。

扩展到批量处理:

print(embedding_layer(input_ids))

输出:

tensor([[ 0.7, -0.8, 0.9], # ID 2
[ 1.0, -1.1, 1.2], # ID 3
[ 1.6, -1.7, 1.8], # ID 5
[ 0.4, 0.5, -0.6]]) # ID 1

2.8 位置编码(Positional Encoding)

问题:为什么需要位置编码?

嵌入层只能将Token ID映射为向量,但无法表示Token的位置。
例如:

- 对于句子 “I love NLP” 和 “NLP love I”,它们的Token是一模一样的,但顺序完全不同。

为了让模型区分位置,我们需要给每个Token加上位置信息。

步骤1:创建位置嵌入

context_length = 4 # 假设句子长度为4
output_dim = 3 # 嵌入维度与Token嵌入相同
pos_embedding_layer = torch.nn.Embedding(context_length, output_dim) # 位置嵌入层

假设位置嵌入层初始化后的权重矩阵是:

1
2
3
4
tensor([[ 0.1,  0.2, -0.3],  # 位置0
[ 0.4, -0.5, 0.6], # 位置1
[-0.7, 0.8, -0.9], # 位置2
[ 1.0, -1.1, 1.2]]) # 位置3

步骤2:查找位置嵌入

1
2
3
position_ids = torch.arange(context_length)  # 生成位置ID [0, 1, 2, 3]
pos_embeddings = pos_embedding_layer(position_ids) # 查询位置向量
print(pos_embeddings)

输出:

1
2
3
4
tensor([[ 0.1,  0.2, -0.3],  # 位置0
[ 0.4, -0.5, 0.6], # 位置1
[-0.7, 0.8, -0.9], # 位置2
[ 1.0, -1.1, 1.2]]) # 位置3

步骤3:将Token嵌入和位置嵌入相加

假设之前的Token嵌入为

1
token_embeddings = embedding_layer(input_ids)

将Token嵌入与位置嵌入相加

1
2
input_embeddings = token_embeddings + pos_embeddings
print(input_embeddings)

输出:

1
2
3
4
tensor([[ 0.8, -0.6,  0.6],  # Token ID 2 + 位置 0
[ 1.4, -1.6, 1.8], # Token ID 3 + 位置 1
[ 0.9, -0.9, 0.9], # Token ID 5 + 位置 2
[ 1.4, -0.6, 0.6]]) # Token ID 1 + 位置 3

解释:

  • 每个Token的向量中都加入了对应位置的位置信息。
  • 新生成的向量将用于模型的下一步处理。

总结

  1. 嵌入层:
    • 将Token ID映射为连续向量表示。
    • 初始向量随机生成,随着训练不断优化。
  2. 位置嵌入:
    • 给每个Token附加位置信息,帮助模型理解顺序。
  3. 最终输入:
    • Token嵌入和位置嵌入相加,生成模型的输入。

Chapter 03

Read More

2024-09-06
Configure Latex on MacOS

安装MacTex

安装VSCode插件

配置VSCode

设置默认编译器

在VSCode中,打开设置,搜索”latex-workshop.latex.recipes”,将 “args” 部分的内容修改为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"name": "latexmk",
"command": "latexmk",
// "args": [
// "-synctex=1",
// "-interaction=nonstopmode",
// "-file-line-error",
// "-pdf",
// "-outdir=%OUTDIR%",
// "%DOC%"
// ],
"args": [
"-xelatex",
"-synctex=1",
"-interaction=nonstopmode",
"-file-line-error",
"%DOC%"
],
"env": {}

保存编译

在VSCode中,打开设置,搜索”latex-workshop.latex.autoBuild.run”,将其设置为”onSave”,这样每次保存tex文件时就会自动编译。

使用上次使用的编译器

在VSCode中,打开设置,搜索”latex-workshop.latex.recipe.default”,将其设置为”lastUsed”,这样第一次编译tex文件手动选择选择编译器,后面就会默认使用该选择了。

目前遇到的问题

  • MacOS上编译得到的pdf文件中文字体偏细, 与Windows上的不一样

参考

Read More

2024-08-01
Basic Algorithms

Chapter1 基础算法

快速排序 Quick Sort

785.快速排序

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
#include <iostream>
using namespace std;

const int N = 1e6 + 10;

int n;
int q[N];

void quick_sort(int q[],int l,int r)
{
if(l>=r) return;
int i = l-1;
int j = r+1;
int x = q[(l+r)/2];

while(i<j){
do i++; while(q[i]<x);
do j--; while(q[j]>x);
if(i<j) swap(q[i],q[j]);
}

quick_sort(q,l,j);
quick_sort(q,j+1,r);
}


int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&q[i]);

quick_sort(q,0,n-1);

for(int i=0;i<n;i++) printf("%d ",q[i]);

return 0;
}

注意边界问题

quick_sort(q,l,j);
quick_sort(q,j+1,r);//此时x不能取q[r]

可以更改为

quick_sort(q,l,i-1);
quick_sort(q,i,r);//此时x不能取q[l]

归并排序

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
#include<iostream>
using namespace std;

const int N = 1e6+10;
int n;
int q[N],tmp[N];

void merge_sort(int q[],int l,int r)
{
if(l>=r)return;
int mid = l+r >> 1;
merge_sort(q,l,mid);
merge_sort(q,mid+1,r);
int k = 0;
int i = l,j = mid+1;
while(i<=mid && j<=r) {
if (q[i]<=q[j]) tmp[k++] = q[i++];
else tmp[k++] = q[j++];
}
while(i<=mid) tmp[k++] = q[i++];
while(j<=r) tmp[k++] = q[j++];
for(i=l,j=0;i<=r;i++,j++) q[i]=tmp[j];
}

int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&q[i]);

merge_sort(q,0,n-1);

for(int i=0;i<n;i++) printf("%d ",q[i]);

return 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
bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
例题:

给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。

对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 00 开始计数)。

如果数组中不存在该元素,则返回 -1 -1

输入格式

第一行包含整数 n 和 q,表示数组长度和询问个数。

第二行包含 n 个整数(均在 1∼100001∼10000 范围内),表示完整数组。

接下来 q 行,每行包含一个整数 k,表示一个询问元素。

输出格式

共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。

如果数组中不存在该元素,则返回 -1 -1

数据范围

1≤n≤100000

1≤q≤10000

1≤k≤10000

输入样例

1
2
3
4
5
6 3
1 2 2 3 3 4
3
4
5

输出样例

1
2
3
3 4
5 5
-1 -1

答案

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
#include <iostream>
using namespace std;

const int N = 100010;
int arr[N];

int main()
{
int n,q,k;
scanf("%d%d",&n,&q);
for(int i=0;i<n;i++) scanf("%d",&arr[i]);
for(int i=0;i<q;i++) {
scanf("%d",&k);
int l=0,r=n-1;
while(l<r)
{
int mid = l+r >> 1;
if(arr[mid]>=k) r = mid;
else l = mid+1;
}
if(arr[l]!=k) cout<<"-1 -1"<<endl;
else{
cout<<l<<" ";
int l=0,r=n-1;
while(l<r)
{
int mid = l+r+1 >> 1;
if(arr[mid]<=k) l = mid;
else r = mid-1;
}
cout<<r<<endl;
}
}
}

浮点数二分

不考虑边界问题更简单

例题:

给定一个浮点数 n,求它的三次方根。

输入格式

共一行,包含一个浮点数 n。

输出格式

共一行,包含一个浮点数,表示问题的解。
注意,结果保留 66 位小数。

数据范围

−10000≤n≤10000

输入样例

1
1000.00

输出样例

1
10.000000

答案:

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
#include <iostream>
#include <algorithm>

using namespace std;

int main()
{
double n;
cin>>n;
bool isNegtive = false;
if(n<0) {
isNegtive = true;
n = n*-1.0;
}
double l=0,r=max(1.0,n);
while((r-l)>1e-8)
{
double mid = (l+r)/2;
if(mid*mid*mid>=n){
r = mid;
}
else l = mid;
}
if(!isNegtive) printf("%lf\n",l);
else printf("%lf\n",-1*l);

return 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
39
40
41
#include <iostream>
#include <vector>

using namespace std;

const int N = 1e6+10;

vector<int> add(vector<int>&A,vector<int>&B)
{
vector<int> C;
int t = 0;
for (int i = 0; i<A.size()||i<B.size(); i ++ ){
if(i<A.size()) t+=A[i];
if(i<B.size()) t+=B[i];
C.push_back(t%10);
t/=10;//上一位的进位
}
if(t) C.push_back(1);
return C;
}

int main()
{
string a,b;
vector<int> A,B;

cin>>a>>b;
for(int i = a.size()-1;i>=0;i--){
A.push_back(a[i]-'0');
}
for(int i = b.size()-1;i>=0;i--){
B.push_back(b[i]-'0');
}

auto C = add(A,B);

for(int i = C.size()-1;i>=0;i--){
printf("%d",C[i]);
}
return 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <iostream>
#include <vector>

using namespace std;

bool cmp(vector<int>&A,vector<int>&B){
if(A.size()!=B.size()) return A.size()>B.size();
else {
for(int i=A.size()-1;i>=0;i--){
if(A[i]!=B[i]) return A[i]>B[i];
}
}
return true;
}

vector<int> sub(vector<int>&A,vector<int>&B)
{
//要求——A>=B
//if A>=B 直接计算A-B
//else 计算-(B-A)

vector<int> C;
int t = 0;
for (int i = 0; i<A.size(); i ++ ){
t = A[i] - t;
if(i<B.size()) t -= B[i];
C.push_back((t+10)%10);
if(t<0) t = 1;
else t = 0;
}
while(C.size()>1 && C.back()==0) C.pop_back();
return C;
}

int main()
{
string a,b;
vector<int> A,B;

cin>>a>>b;
for(int i = a.size()-1;i>=0;i--){
A.push_back(a[i]-'0');
}
for(int i = b.size()-1;i>=0;i--){
B.push_back(b[i]-'0');
}
if(cmp(A,B)){
auto C = sub(A,B);
for(int i = C.size()-1;i>=0;i--){
printf("%d",C[i]);
}
}
else{
auto C = sub(B,A);
printf("-");
for(int i = C.size()-1;i>=0;i--){
printf("%d",C[i]);
}
}

return 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
39
#include <iostream>
#include <vector>

using namespace std;

vector<int> mul(vector<int> A , int b)
{
vector<int> C;
int t = 0;
for(int i=0;i < A.size() || t ; i++){
if(i<A.size()){
t += A[i] * b;
}
C.push_back(t % 10);
t /= 10;
// C.push_back((A[i] * b + t) % 10);
// t = (A[i] * b + t) / 10;
}
while(C.size()>1 && C.back()==0) C.pop_back();
// 去除前导0
return C;
}

int main()
{
string a;
int b;
vector<int> A;
cin>>a>>b;
for(int i = a.size()-1;i>=0;i--){
A.push_back(a[i]-'0');
}

auto C = mul(A,b);

for(int i = C.size()-1;i>=0;i--){
printf("%d",C[i]);
}
}

高精度除法

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
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

vector<int> div(vector<int> &A, int b ,int &r){
vector<int> C;// 商
r = 0;// 余数
// 从最高位开始算--不同于其他几种计算
for(int i = A.size() - 1;i >= 0 ; i--){
r = r * 10 + A[i];
C.push_back(r / b);
r %= b;
}

reverse(C.begin() ,C.end());

while(C.size()>1 && C.back()==0) C.pop_back();
// 去除前导0
return C;
}

int main()
{
string a;
int b;
vector<int> A;
cin>>a>>b;
for(int i = a.size()-1;i>=0;i--){
A.push_back(a[i]-'0');
}

int r;
auto C = div(A,b,r);

for(int i = C.size()-1;i>=0;i--){
printf("%d",C[i]);
}
cout<<endl;
cout<<r<<endl;
}

前缀和

一维前缀和

  • 更新 S[i] = S[i-1] + a[i]
  • 计算a[l,r] ans = S[r] - S[l-1]
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
#include <iostream>
using namespace std;

const int N = 100010;
int a[N],S[N];

int main()
{
int n,m;
scanf("%d%d", &n, &m);

for (int i = 1;i<=n;i++) scanf("%d", &a[i]);

S[0] = 0;
for(int i = 1;i<=n;i++){
S[i] = S[i-1] + a[i];
}
for (int i = 0;i < m;i++){
int l,r;
scanf("%d%d", &l, &r);
printf("%d\n",S[r]-S[l-1]);
}

return 0;

}

二维前缀和(子矩阵的和)

  • 更新 S[i][j] =a[i][j] + S[i][j-1] + S[i-1][j] - S[i-1][j-1]
  • 计算(x1,y1)(x2,y2) ans = S[x2][y2] - S[x1-1][y2] - S[x2][y1-1] + S[x1-1][y1-1]
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
#include <iostream>
#include <cstring>
#include <algorithm>

const int N = 1010;
int a[N][N],S[N][N];

int main()
{
int n,m,q;
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
scanf("%d",&a[i][j]);
}
}
S[0][0] = 0;
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
S[i][j] =a[i][j] + S[i][j-1] + S[i-1][j] - S[i-1][j-1];
}
}

int x1,x2,y1,y2;
while(q--){
scanf("%d%d%d%d", &x1, &y1 , &x2, &y2);
int ans = S[x2][y2] - S[x1-1][y2] - S[x2][y1-1] + S[x1-1][y1-1];
printf("%d\n",ans);
}

return 0;
}

一维差分

  • 输入一个长度为 n 的整数序列。

    接下来输入 m个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。

    请你输出进行完所有操作后的序列。

  • 前缀和的逆运算

  • 构造 等同于 操作(i,i,a[i])

  • 操作等价于

    • b[l] += c;
    • ``b[r+1] -= c;`
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
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;
int a[N],b[N];

void op(int l,int r,int c){
b[l] += c;
b[r+1] -= c;
}

int main()
{
int n,m;
scanf("%d%d", &n, &m);
for(int i=1;i<=n;i++){
scanf("%d", &a[i]);
}
for(int i=1;i<=n;i++){
op(i,i,a[i]);
}

/*接下来输入 m个操作,
* 每个操作包含三个整数 l,r,c
* 表示将序列中 [l,r]之间的每个数加上 c
*/
while (m--) {
int l,r,c;
scanf("%d%d%d", &l, &r, &c);
op(l,r,c);
}

for (int i = 1; i <= n; i++){
b[i] += b[i-1];
}
for (int i = 1; i <= n; i++){
printf("%d ",b[i]);
}

return 0;


}

二维差分

  • 输入一个 n 行 m列的整数矩阵,再输入 q 个操作,每个操作包含五个整数 x1,y1,x2,y2,c
    其中 (x1,y1)和 (x2,y2)表示一个子矩阵的左上角坐标和右下角坐标。

    每个操作都要将选中的子矩阵中的每个元素的值加上 c

    请你将进行完所有操作后的矩阵输出。

  • 操作等价于

    • b[x1][y1] += c
    • b[x2+1][y1] -= c
    • b[x1][y2+1] -= c
    • b[x2+1][y2+1] += c
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
#include <iostream>
#include <cstring>
#include <algorithm>

const int N = 1010;
int a[N][N],b[N][N];

void op(int x1,int x2,int y1,int y2,int c){
b[x1][y1] += c;
b[x2+1][y1] -= c;
b[x1][y2+1] -= c;
b[x2+1][y2+1] += c;
}

int main()
{
int n,m,q;
scanf("%d%d%d", &n, &m, &q);

for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
scanf("%d",&a[i][j]);
}
}
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
op(i,i,j,j,a[i][j]);
}
}

int x1,x2,y1,y2,c;
while(q--){
scanf("%d%d%d%d%d", &x1, &y1 , &x2, &y2, &c);
op(x1,x2,y1,y2,c);
}

for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
b[i][j] = b[i][j] + b[i][j-1] + b[i-1][j] - b[i-1][j-1];
}
}

for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
printf("%d ",b[i][j]);
}
puts("");
}


return 0;
}

双指针算法

  • 基本模版

    1
    2
    3
    4
    5
    6
    for(i = 0,j = 0;i < n;i++){
    while(j<i && check(i,j))
    j++;

    // ......
    }
  • 核心思想

    将O(n^2^)算法优化到O(n)

799.最长连续不重复子序列

给定一个长度为 n 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。

输入格式

第一行包含整数 n。
第二行包含 n 个整数(均在 0∼1050∼105 范围内),表示整数序列。

输出格式

共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。

数据范围

1≤n≤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
29
30
#include <iostream>
#include <cstring>
#include <algorithm>


using namespace std;

const int N = 1e5 + 10;
int n;
int a[N],s[N];


int main()
{
scanf("%d", &n);
for(int i = 0;i < n;i++) {
scanf("%d", &a[i]);
}
int maxLen = 0;
for (int i = 0,j = 0; i < n; i++){
s[a[i]]++;
while(s[a[i]] > 1){
s[a[j]]--;
j++;
}
maxLen = max(maxLen,i - j + 1);
}
cout << maxLen;
return 0;
}

800.数组元素的目标和

给定两个==升序排序==的有序数组 A 和 B,以及一个目标值 x。
数组下标从 0 开始。
请你求出满足 A[i]+B[j]=x 的数对 (i,j)。
数据保证有唯一解。

输入格式
第一行包含三个整数 n,m,x ,分别表示 A 的长度,B 的长度以及目标值 x
第二行包含 n 个整数,表示数组 A
第三行包含 m 个整数,表示数组 B

输出格式
共一行,包含两个整数 i 和 j

数据范围
数组长度不超过 10^5^
同一数组内元素各不相同。
1 ≤ 数组元素 ≤ 10^9^

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
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n,m,x;
int A[N],B[N];


int main()
{
scanf("%d%d%d", &n, &m, &x);
for (int i = 0; i < n; i++) {
scanf("%d", &A[i]);
}
for (int i = 0; i < m; i++) {
scanf("%d", &B[i]);
}
int i, j;
for(i = 0, j = m - 1; i < n; i++) {
while (j >= 0 && A[i] + B[j] > x) j--;
if(j >= 0 && A[i] + B[j] == x) {
cout << i << ' ' << j;
return 0;
}
}
return 0;
}

位运算

n的二进制表示中的第k位是几?

1
2
3
4
int n = 10;
for(int k = 3;k >= 0;k--){
cout << (n>>k & 1);
}

lowbit(x) 返回x的最后一位1

x & -x = x & (~x + 1)

eg:

x = 1010……..==1==000……0
x = 0101……..==0==111……1
~x + 1 = 0101……..==1==000……0
x & (
x + 1) = 0000……..==1==000……0

1
2
int x;
cout << (x & -x);

801.二进制中1的个数

给定一个长度为 n 的数列,请你求出数列中每个数的二进制表示中 1 的个数。

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
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

int lowbit(int x) // 返回末尾的1
{
return x & -x;
}

int main()
{
int n;
cin >> n;
while (n--){
int x;
cin>>x;
int res = 0;
while(x){
x -= lowbit(x);
res++;
}
cout<<res<<' ';
}
return 0;
}

(整数)离散化

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end()); // 去掉重复元素

// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于x的位置
{
int l = 0, r = alls.size() - 1;
while (l < r)
{
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1; // 映射到1, 2, ...n
}

802.区间和 难☹️

假定有一个无限长的数轴,数轴上每个坐标上的数都是 0
现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加 c
接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r] 之间的所有数的和

输入格式

第一行包含两个整数 n 和 m
接下来 n 行,每行包含两个整数 x 和
再接下来 m 行,每行包含两个整数 l 和 r

输出格式

共 m 行,每行输出一个询问中所求的区间内数字和。

数据范围

−10^9^≤x≤10^9^,
1≤n,m≤10^5^
−10^9^≤l≤r≤10^9^
−10000≤c≤10000

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

typedef pair<int, int> PII;

const int N = 300010;
int n,m;
int a[N],s[N];

vector<int> alls;
vector<PII> add, query;

int find(int x) // 二分
{
int l = 0, r = alls.size() - 1;
while(l<r){
int mid = l+r >> 1;
if(alls[mid] >= x){
r = mid;
}
else{
l = mid + 1;
}
}
return r + 1;
}


int main()
{
scanf("%d%d", &n, &m);
while (n--) {
int x,c;
scanf("%d%d", &x, &c);
add.push_back({x,c});

alls.push_back(x);
}
while (m--) {
int l,r;
scanf("%d%d", &l, &r);
query.push_back({l,r});
alls.push_back(l);
alls.push_back(r);
}

// 排序 & 去重
sort(alls.begin(),alls.end());
alls.erase(unique(alls.begin(), alls.end()),alls.end());
// 处理插入
for(auto item : add){
int x = find(item.first);
a[x] += item.second;
}
// 预处理前缀和
for(int i = 1;i <= alls.size(); i++){
s[i] = s[i-1] + a[i];
}
// 处理询问
for(auto item : query){
int l = find(item.first);
int r = find(item.second);
cout << s[r] - s[l-1] << endl;
}
return 0;
}

注意:

  1. vector<int> alls 中存的是需要离散化的下标

  2. const int N = 300010; 查询最多10^5^次,插入最多10^5^次,最多需要离散化2*查询+插入 = 3*10^5^次

  3. 实现unique函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    vector<int>::iterator unique(vector<int> &a)
    {
    for(int i = 0;i < a.size(); i++){
    if(!i || a[i]!=a[i-1]){
    a[j++] = a[i];
    }
    }
    return a.begin() + j;
    }

    alls.erase(unique(alls.begin(), alls.end()),alls.end());改为

    alls.erase(unique(alls),alls.end());

区间合并

  1. 按区间左端点排序
  2. 扫描整个区间,把所有可能有交集的区间进行合并

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 将所有存在交集的区间合并
void merge(vector<PII> &segs)
{
vector<PII> res;

sort(segs.begin(), segs.end());

int st = -2e9, ed = -2e9;
for (auto seg : segs)
if (ed < seg.first)
{
if (st != -2e9) res.push_back({st, ed});
st = seg.first, ed = seg.second;
}
else ed = max(ed, seg.second);

if (st != -2e9) res.push_back({st, ed});

segs = res;
}

803. 区间合并

给定 n 个区间 [li,ri],要求合并所有有交集的区间。
注意如果在端点处相交,也算有交集。
输出合并完成后的区间个数。

Eg. [1,3] & [2,6] -> [1,6]

输入格式

第一行包含整数 n。
接下来 n 行,每行包含两个整数 l 和 r。

输出格式

共一行,包含一个整数,表示合并区间完成后的区间个数。

数据范围

1 ≤ n ≤ 100000
−10^9^ ≤ li ≤ ri ≤ 10^9^

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
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;
typedef pair<int,int> PII;

vector<PII> segs;

void merge(vector<PII> &segs){
vector<PII> res;

sort(segs.begin(),segs.end());

int st = -2e9, ed = -2e9;
for(auto seg : segs){
if(ed < seg.first){
if(st!= -2e9) res.push_back({st,ed});
st = seg.first;
ed = seg.second;
}
else ed = max(ed, seg.second);
}
if(st!= -2e9) res.push_back({st,ed});

segs = res;
}

int main()
{
int n;
scanf("%d", &n);
while (n--) {
int l,r;
scanf("%d%d", &l, &r);
segs.push_back({l,r});
}

merge(segs);

cout << segs.size() << endl;

return 0;

}

Chapter2 数据结构

链表

笔试中一般不采用动态链表

单链表

826. 单链表

实现一个单链表,链表初始为空,支持三种操作:

  1. 向链表头插入一个数;
  2. 删除第 k 个插入的数后面的数;
  3. 在第 k 个插入的数后插入一个数。

现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。

注意: 题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

  1. H x,表示向链表头插入一个数 x。
  2. D k,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)。
  3. I k x,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 00)。

输出格式

共一行,将整个链表从头到尾输出。

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

/* head存储链表头,
e[i]存储节点i的值,
ne[i]存储节点i的next指针,
idx表示当前用到了哪个节点
*/
int head, e[N], ne[N];
int idx;

void init() // 初始化
{
head = -1;
idx = 0;
}

void insert_head(int x) // 插入头节点
{
e[idx] = x;
ne[idx] = head;
head = idx;
idx++;
}

void insert(int k, int x) // 在k坐标后插入节点(值为x)
{
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx;
idx++;
}

void remove_head() // 删除头节点head
{
head = ne[head];
}

void remove(int k) // 删除下标k后的点
{
ne[k] = ne[ne[k]];
}

int main()
{
int M;
scanf("%d", &M);
init();
while (M--) {
int k, x;
char op;

cin >> op;
if (op == 'H') {
cin >> x;
insert_head(x);
}
else if (op == 'D') {
cin >> k;
if (k == 0) {
remove_head();
}
remove(k - 1);
}
else {
cin >> k >> x;
insert(k - 1,x);
}
}

for(int i = head; i != -1; i = ne[i]){
cout << e[i] << ' ';
}
cout << endl;

return 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
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;

// 初始化
void init()
{
//0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
}

// 在节点a的右边插入一个数x
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a, r[idx] = r[a];
l[r[a]] = idx, r[a] = idx ++ ;
}

// 删除节点a
void remove(int a)
{
l[r[a]] = l[a];
r[l[a]] = r[a];
}
827. 双链表

实现一个双链表,双链表初始为空,支持 55 种操作:

  1. 在最左侧插入一个数;
  2. 在最右侧插入一个数;
  3. 将第 k 个插入的数删除;
  4. 在第 k 个插入的数左侧插入一个数;
  5. 在第 k 个插入的数右侧插入一个数

现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。

注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

  1. L x,表示在链表的最左端插入数 x。
  2. R x,表示在链表的最右端插入数 x。
  3. D k,表示将第 k 个插入的数删除。
  4. IL k x,表示在第 k 个插入的数左侧插入一个数。
  5. IR k x,表示在第 k 个插入的数右侧插入一个数。

输出格式

共一行,将整个链表从左到右输出。

数据范围

1 ≤ M ≤ 100000
所有操作保证合法。

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;
int m;
int e[N], l[N], r[N], idx;

//初始化
//0号点:head pointer
//1号点:tail pointer
void init()
{
r[0] = 1;
l[1] = 0;
idx = 2;
}

//在k号点右边边插入值为x的点
void insert_right(int k, int x)
{
e[idx] = x;
l[idx] = k;
r[idx] = r[k];
l[r[k]] = idx;
r[k] = idx;
idx++;
}

//在k号点左边插入值为x的点:直接实现
void insert_left(int k, int x)
{
e[idx] = x;
l[idx] = l[k];
r[idx] = k;
r[l[k]] = idx;
l[k] = idx;
idx++;
}

//在k号点左边插入值为x的点:调用法
void insert_left_call(int k, int x)
{
insert_right(l[k], x);
}

//删除第k个点
void delete_k(int k)
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}


int main()
{
scanf("%d", &m);
init();
while (m--){
string op;
cin >> op;
if(op == "L"){
int x;
cin >> x;
insert_right(0, x);
}
else if(op == "R"){
int x;
cin >> x;
insert_left(1, x);
}
else if(op == "D"){
int k;
cin >> k;
delete_k(k + 1);
}
else if(op == "IL"){
int k, x;
cin >> k >> x;
insert_left(k + 1, x);
}
else if(op == "IR"){
int k, x;
cin >> k >> x;
insert_right(k + 1, x);
}
}

for(int i = r[0]; i != 1; i = r[i]){
cout << e[i] << " ";
}
cout << endl;

return 0;
}

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int stk[N], tt;

int m;

void init()
{
tt = 0;
}

void push(int x)
{
stk[++tt] = x;
}

void pop()
{
tt--;
}

bool isEmpty()
{
if(tt > 0)
return false;
else
return true;
}

int query()
{
return stk[tt];
}

int main()
{
scanf("%d", &m);
init();
while(m--){
string op;
cin >> op;
if(op == "push"){
int x;
cin >> x;
push(x);
}
else if(op == "pop"){
pop();
}
else if(op == "empty"){
string e;
e = isEmpty() ? "YES" : "NO";
cout << e << endl;
}
else if(op == "query"){
cout << query() << endl;
}
}
return 0;
}

队列

队尾插入,队头推出

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
52
53
54
55
56
57
58
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int m;
int q[N], hh, tt;

void init(){
tt = -1;
hh = 0;
}

void push(int x){
q[++tt] = x;
}

void pop(){
hh++;
}

bool isEmpty(){
return hh <= tt ? false : true;
}

int query(){
return q[hh];
}

int main()
{
scanf("%d", &m);
init();
while(m--){
string op;
cin >> op;
if(op == "push"){
int x;
cin >> x;
push(x);
}
else if(op == "pop"){
pop();
}
else if(op == "empty"){
string e;
e = isEmpty() ? "YES" : "NO";
cout << e << endl;
}
else if(op == "query"){
cout << query() << endl;
}
}
return 0;
}

单调栈

830. 单调栈

给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。

输入格式

第一行包含整数 N,表示数列长度。

第二行包含 N 个整数,表示整数数列。

输出格式

共一行,包含 N 个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。

数据范围

1 ≤ N ≤ 10^5^
1 ≤ 数列中元素 ≤ 10^9^

样例

3 4 2 7 5
-1 3 -1 2 2

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
#include <iostream>

using namespace std;

const int N = 100010;

int n;
int stk[N], tt;

int main()
{
scanf("%d", &n);
while (n--){
int x;
cin >> x;
while(tt && stk[tt] >= x) tt--;
if(tt)
// cout << stk[tt] << ' ';
printf("%d ",stk[tt]);
else
// cout << -1 << ' ';
printf("-1 ");

stk[++tt] = x;
}
}

单调队列

154. 滑动窗口——难

给定一个大小为 n≤10^6^ 的数组。

有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k 个数字。
每次滑动窗口向右移动一个位置。

以下是一个例子:

该数组为 [1 3 -1 -3 5 3 6 7],k 为 3。

窗口位置 最小值 最大值
[1 3 -1] -3 5 3 6 7 -1 3
1 [3 -1 -3] 5 3 6 7 -3 3
1 3 [-1 -3 5] 3 6 7 -3 5
1 3 -1 [-3 5 3] 6 7 -3 5
1 3 -1 -3 [5 3 6] 7 3 6
1 3 -1 -3 5 [3 6 7] 3 7

你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式

输入包含两行。

第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。
第二行有 n 个整数,代表数组的具体数值。

同行数据之间用空格隔开。

输出格式

输出包含两个。

第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例

1
2
8 3
1 3 -1 -3 5 3 6 7

输出样例

1
2
-1 -3 -3 -3 3 3
3 3 5 5 6 7

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
52
53
54
55
#include <iostream>
#include <cstdio>

using namespace std;

const int N = 1000010;

int n, k;
int a[N], q[N], tt, hh;

int main()
{
scanf("%d%d", &n, &k);

for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
}

//最小值
hh = 0;
tt = -1;
for (int i = 0; i < n; i++) {

// 判断队头是否已经滑出窗口
if(hh <= tt && i - k + 1 > q[hh]){
hh++;
}
while(hh <= tt && a[q[tt]] > a[i]){
tt--;
}
q[++tt] = i;
if(i >= k-1){
printf("%d ", a[q[hh]]);
}
}
printf("\n");
//最大值
hh = 0;
tt = -1;
for (int i = 0; i < n; i++) {

// 判断队头是否已经滑出窗口
if(hh <= tt && i - k + 1 > q[hh]){
hh++;
}
while(hh <= tt && a[q[tt]] <= a[i]){
tt--;
}
q[++tt] = i;
if(i >= k-1){
printf("%d ", a[q[hh]]);
}
}
return 0;
}

理解

image-20230721205330280

init() : tt = -1, hh = 0; // 此时hh>tt 队空

窗口应该在(i-k+1)到i范围内

  • 出队判定 : 队列不为空& 队头下标 q[hh] ≥ i - k + 1
  • (i-k+1)到i范围内 ,如果==队尾下标对应值比i下标对应值还要大==就丢弃(此时是以(i-k+1)到i为窗口范围,但实际i还未入队)
  • i下标入队 q[++tt] = i;

Leetcode

剑指 Offer 59 - I. 滑动窗口的最大值

KMP算法
Read More

2023-11-15
CG-Assignment2

本文为计算机图形学作业2报告, 为本人计算机图形学课程作业, 仅供参考, 未经允许不得转载, 抄袭.

1.引言

内容应包含但不局限于项目名称,项目简介。篇幅占整体内容的10%

本次作业在第一次作业的基础上,增加一个bezier曲面,并对场景添加光照和纹理效果。具体要求如下:

  1. 以bezier曲面模拟一面旗帜,曲面至少包含5*5个控制点;

  2. 在场景中使用phong光照模型来得到合理的光照效果;

  3. 对场景中的模型添加纹理贴图, 图片自行选择,不同类型的模型采用不同的贴图。

2. 主体介绍

2.1 程序简介

  1. 编程环境

    MacOS,C++11,IDE: CLion,cmake,使用OpenGL,Glad,GLFW3.3.8,glm0.9.9.1

  2. 文件结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    (base) alanzeng@AlandeMacBook-Pro CGA2 % tree
    CGA2
    ├── 1.model_loading.fs
    ├── 1.model_loading.vs
    ├── BezierFace.cpp
    ├── CMakeLists.txt
    ├── OBJ_Files
    ├── TJU.jpg
    ├── cmake-build-debug
    ├── glad
    ├── glm-0.9.9.1
    ├── includes
    │   ├── BezierFace.h
    │   ├── camera.h
    │   ├── shader.h
    │   └── stb_image.h
    └── model_loading.cpp

2.2 算法

在这一部分需要描述你的项目实现过程中用了哪些算法,需要你从算法背后的原理入手结合自己的项目进行描述,切忌段落式的粘贴代码,如果是自己写的算法,进行适当的算法复杂度分析。
示例:a) Lambert 漫反射光照模型 b) 正投影相机 c) Phong光照模型 d) 一些图形的绘制举例 e) 碰撞测试特例算法

注:一定要结合自己的项目去写这一部分,落到实处,切忌直接从网上直接粘贴理论知识。

Phong光照模型

Phong光照模型是计算三维计算机图形中光照效果的经典方法之一,它被广泛用于渲染三维场景和物体。Phong光照模型考虑了三种主要的光照成分:环境光、漫反射光和镜面光。

  1. 环境光(Ambient Light): 环境光是物体表面上的均匀光照,不依赖于光源的方向。它用于模拟场景中光源以外的间接光照明。环境光通常是一个常数,用来表示整体光照的强度。

  2. 漫反射光(Diffuse Light): 漫反射光是光线以角度入射到物体表面并均匀反射的光。它依赖于光线入射角和物体表面的法线(法向量),这个成分使物体的表面看起来具有粗糙的外观。漫反射光的强度在表面上不均匀,取决于入射角度和法向量之间的夹角。

  3. 镜面光(Specular Light): 镜面光是光线以特定角度入射到物体表面并以同样的角度反射的光,类似于镜面反射。这个成分使物体的表面看起来光滑,具有高光亮度。镜面光的强度取决于入射光线的方向、视线方向和物体的材质属性。

Phong光照模型可以表示为以下方程:

$$
I = k_a \cdot I_a + k_d \cdot I_d \cdot (N \cdot L) + k_s \cdot I_s \cdot (R \cdot V)^n
$$

其中:

  • $I$ 是最终的光照强度。
  • $k_a$、$k_d$、$k_s$ 分别是环境光、漫反射光和镜面光的系数。
  • $I_a$、$I_d$、$I_s$ 分别是环境光、漫反射光和镜面光的颜色强度。
  • $N$ 是表面法向量。
  • $L$ 是入射光线的方向向量。
  • $R$ 是反射光线的方向向量。
  • $V$ 是视线方向向量。
  • $n$ 是材质的镜面反射指数。

在片段着色器中, 运用Phong光照模型进行计算

片段着色器中定义了Material(材质)和Light(光照)结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Material {
sampler2D diffuse;
vec3 specular;
float shininess;
};

struct Light {
vec3 position;
vec3 direction;
float cutOff;
float outerCutOff;

vec3 ambient;
vec3 diffuse;
vec3 specular;

float constant;
float linear;
float quadratic;
};

具体算法实现

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
void main()
{
// ambient
vec3 ambient = light.ambient * texture(material.diffuse, TexCoords).rgb;

// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(light.position - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = light.diffuse * diff * texture(material.diffuse, TexCoords).rgb;

// specular
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = light.specular * (spec * material.specular);

// spotlight (soft edges)
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = (light.cutOff - light.outerCutOff);
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
diffuse *= intensity;
specular *= intensity;

// attenuation
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;

vec3 result = ambient + diffuse + specular;
FragColor = vec4(result, 1.0);
}

BezierFace

贝塞尔曲面(Bezier Surface)是一种用于三维图形和计算机辅助设计(CAD)中的曲面表示方法。它是由法国工程师皮埃尔·贝塞尔(Pierre Bézier)于20世纪50年代发明的,并在汽车设计领域广泛应用。贝塞尔曲面由多个控制点定义,这些点控制了曲面的形状。它是一种常用的二次和三次曲面建模技术。ß

贝塞尔曲面的定义基于控制点和基函数。控制点是用来确定曲面的形状的点,而基函数是一种数学函数,它们根据控制点的位置来计算曲面上的点。一般来说,贝塞尔曲面是由两个参数u和v来定义的,这些参数的范围通常是从0到1。

一个常见的二次贝塞尔曲面可以用以下公式来表示:

$$
P(u, v) = (1-u)^2 * (1-v) * P0 + 2 * (1-u) * u * (1-v) * P1 + u^2 * (1-v) * P2\ + (1-u)^2 * v * P3 + 2 * (1-u) * u * v * P4 + u^2 * v * P5 +\ (1-u)^2 * (1-v)^2 * P6 + 2 * (1-u) * u * (1-v)^2 * P7 + u^2 * (1-v)^2 * P8
$$

其中,P0到P8是控制点的坐标,(u, v)是曲面上的点坐标。这个公式描述了如何根据控制点的位置以及参数u和v的值来计算曲面上的点。

贝塞尔曲面的优点之一是它在控制点的位置和权重上具有直观性,可以通过移动控制点来调整曲面的形状。此外,贝塞尔曲面是局部控制的,这意味着一个控制点的改变只会影响曲面上的局部区域。

在计算机图形中,贝塞尔曲面通常用于建模、渲染和处理复杂的曲面形状,例如汽车外壳、船体、飞机机身等。

本次实验中,我们要构建一个由$5\times5$控制点构成的贝塞尔曲面, 具体的实现请见2.3.3 BezierFace.h

纹理贴图

纹理贴图是计算机图形学中强大且广泛应用的技术,它可以增强三维场景的真实感和视觉质量。不同类型的纹理和纹理技术可以用来模拟各种不同的视觉效果,从基本的颜色贴图到复杂的环境映射和法线贴图。

在本次实验中我们使用了stb_image库来加载贴图, 通过一下方法来具体实现纹理贴图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

int width, height, nrComponents;
stbi_set_flip_vertically_on_load(true);
unsigned char* data = stbi_load(path, &width, &height, &nrComponents, 0);
if (data) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else {
std::cout << "Texture failed to load at path: " << path << std::endl;
}
stbi_image_free(data);
return textureID;

2.3 实现细节

在这一部分,将自己的项目拆解成各个部分进行说明。切忌段落式的粘贴代码。

第二章应占整体篇幅的40%。

2.3.1 shader.h

与上课时给出的shader.h代码相同

使用C++文件流读取着色器内容,储存到几个string对象里

以下实现了对着色器的编译和链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned int vertex, fragment;
// vertex shader
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
checkCompileErrors(vertex, "VERTEX");
// fragment Shader
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
checkCompileErrors(fragment, "FRAGMENT");
// shader Program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
checkCompileErrors(ID, "PROGRAM");
// delete the shaders as they're linked into our program now and no longer necessary
glDeleteShader(vertex);
glDeleteShader(fragment);

定义了

1
void use() const { glUseProgram(ID); }

2.3.2 camera.h

提供ProcessKeyboard, ProcessMouseMovement, ProcessMouseScroll方法,用于实现鼠标键盘控制

Camera 类 定义了位置,接收变化的变量,欧拉角,选项和构造函数,此外还有键盘鼠标操作函数和相机更新函数

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
class Camera
{
public:
// camera Attributes
glm::vec3 Position;
glm::vec3 Front;
glm::vec3 Up;
glm::vec3 Right;
glm::vec3 WorldUp;
// euler Angles
float Yaw;
float Pitch;
// camera options
float MovementSpeed;
float MouseSensitivity;
float Zoom;

// constructor with vectors
Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f),
float yaw = YAW, float pitch = PITCH) :
Front(glm::vec3(0.0f, 0.0f, -1.0f)),
MovementSpeed(SPEED),
MouseSensitivity(SENSITIVITY),
Zoom(ZOOM)
{
Position = position;
WorldUp = up;
Yaw = yaw;
Pitch = pitch;
updateCameraVectors();
}
// constructor with scalar values
Camera(float posX, float posY, float posZ, float upX, float upY, float upZ, float yaw, float pitch) :
Front(glm::vec3(0.0f, 0.0f, -1.0f)),
MovementSpeed(SPEED),
MouseSensitivity(SENSITIVITY),
Zoom(ZOOM)
{
Position = glm::vec3(posX, posY, posZ);
WorldUp = glm::vec3(upX, upY, upZ);
Yaw = yaw;
Pitch = pitch;
updateCameraVectors();
}

// returns the view matrix calculated using Euler Angles and the LookAt Matrix
glm::mat4 GetViewMatrix()
{
return glm::lookAt(Position, Position + Front, Up);
}
};

ProcessKeyboard处理从任何类似键盘的输入系统接收的输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void ProcessKeyboard(Camera_Movement direction, float deltaTime)
{
float velocity = MovementSpeed * deltaTime;
if (direction == FORWARD)
Position += Front * velocity;
if (direction == BACKWARD)
Position -= Front * velocity;
if (direction == LEFT)
Position -= Right * velocity;
if (direction == RIGHT)
Position += Right * velocity;
if (direction == DOWN)
Position -= Up * velocity;
if (direction == UP)
Position += Up * velocity;
}

ProcessMouseMovement处理从鼠标输入系统接收的输入。期望x和y方向上的偏移值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch = true)
{
xoffset *= MouseSensitivity;
yoffset *= MouseSensitivity;

Yaw += xoffset;
Pitch += yoffset;

// make sure that when pitch is out of bounds, screen doesn't get flipped
if (constrainPitch)
{
if (Pitch > 89.0f)
Pitch = 89.0f;
if (Pitch < -89.0f)
Pitch = -89.0f;
}

// update Front, Right and Up Vectors using the updated Euler angles
updateCameraVectors();
}

ProcessMouseScroll处理从鼠标滚轮事件接收的输入。只需要在垂直轮轴上输入

1
2
3
4
5
6
7
8
void ProcessMouseScroll(float yoffset)
{
Zoom -= (float)yoffset;
if (Zoom < 1.0f)
Zoom = 1.0f;
if (Zoom > 45.0f)
Zoom = 45.0f;
}

updateCameraVectors

1
2
3
4
5
6
7
8
9
10
11
12
void updateCameraVectors()
{
// calculate the new Front vector
glm::vec3 front;
front.x = cos(glm::radians(Yaw)) * cos(glm::radians(Pitch));
front.y = sin(glm::radians(Pitch));
front.z = sin(glm::radians(Yaw)) * cos(glm::radians(Pitch));
Front = glm::normalize(front);
// also re-calculate the Right and Up vector
Right = glm::normalize(glm::cross(Front, WorldUp));
Up = glm::normalize(glm::cross(Right, Front));
}

2.3.3 BezierFace.h

定义类贝塞尔曲面类, 其中包含顶点个数, 索引个数, 顶点, 法线, 纹理, 索引, 控制点, 曲线阶数等等属性

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
#pragma once
#include <vector>
#include <glm/glm.hpp>
using namespace std;

class BezierFace
{
//顶点个数
int numVertices;
//索引个数
int numIndices;
//顶点
vector<glm::vec3> vertices;
//法线
vector<glm::vec3> normals;
//纹理
vector<glm::vec2> texCoords;
//索引
vector<int> indices;
//计算数据
void init(int prec);
//控制点
float* controlPoints;
vector<glm::vec3> controlPointsVector;
//曲线阶数
int step;
float toRadians(float degrees);
float Bernstein(float u, int index);
public:
BezierFace() = default;
BezierFace(int step, float controlPoints[], int prec);
int getNumVertices();
int getNumIndices();
vector<glm::vec3> getVertices();
vector<glm::vec3> getNormals();
vector<glm::vec2> getTexCoords();
vector<int> getIndices();

};
构造函数
1
2
3
4
5
6
BezierFace::BezierFace(int step, float controlPoints[], int prec)
{
this->step = step;
this->controlPoints = controlPoints;
init(prec);
}
void init(int prec)具体实现(在BezierFace.cpp中)

该函数实现了Bezier曲面初始化

  • 初始化参数

    • prec(precision):这个参数控制曲面的精度,它影响了生成的顶点密度。我们使用 (prec + 1) * (prec + 1) 个顶点和 prec * prec * 6 个索引。
  • 数据初始化

    • 我们首先初始化了用于存储顶点、法线、和纹理坐标的容器 verticesnormalstexCoords,以及存储索引的容器 indices。这些容器将在后续的计算中使用。
  • 控制点

    • 控制点是定义Bezier曲面的关键元素,它们存储在 controlPoints 数组中。我们将这些控制点提取出来并存储在 controlPointsVector 容器中,以便后续计算使用。
  • 计算顶点

    • 在计算Bezier曲面上的顶点时,我们使用了嵌套的循环,遍历 (i, j) 的参数空间,其中 ij 分别表示沿 u 和 v 方向的参数值。对于每个 (i, j) 坐标点,我们使用贝塞尔曲面的公式计算了 x、y 和 z 坐标,并将它们存储在 vertices 中。
  • 计算法线和纹理坐标

    • 在这段代码中,法线的计算似乎与顶点坐标相同。然而,在实际应用中,法线通常需要根据曲面的几何特性来计算。同样,纹理坐标也在这里计算并存储在 texCoords 中。
  • 计算索引

  • 为了定义Bezier曲面的三角形片元,我们使用了嵌套循环来计算索引。这些索引存储在 indices 中,以便在渲染时使用。

这个初始化过程将为Bezier曲面提供一个网格,其中包含离散的顶点、法线和纹理坐标,以及定义曲面几何的索引。在实验的后续部分,我们将使用这些数据来渲染Bezier曲面并展示其效果。

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
void BezierFace::init(int prec)
{
numVertices = (prec + 1) * (prec + 1);
numIndices = prec * prec * 6;
for (int i = 0; i < numVertices; i++)
{
vertices.push_back(glm::vec3());
normals.push_back(glm::vec3());
texCoords.push_back(glm::vec2());
}
for (int i = 0; i < numIndices; i++)
{
indices.push_back(0);
}
for (int i = 0; i < (step + 1) * (step + 1) * 3; i += 3)
{
controlPointsVector.push_back(glm::vec3(controlPoints[i], controlPoints[i + 1], controlPoints[i + 2]));
}
for (int i = 0; i <= prec; i++)
{
for (int j = 0; j <= prec; j++)
{
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
float u = (float)i / prec;
float v = (float)j / prec;

for (int ii = 0; ii <= step; ii++)
{
for (int jj = 0; jj <= step; jj++)
{
int index = ii * (step + 1) + jj;
x += controlPointsVector[index].x * Bernstein(u, ii) * Bernstein(v, jj);
y += controlPointsVector[index].y * Bernstein(u, ii) * Bernstein(v, jj);
z += controlPointsVector[index].z * Bernstein(u, ii) * Bernstein(v, jj);
}
}
vertices[i * (prec + 1) + j] = glm::vec3(x, y, z);
normals[i * (prec + 1) + j] = glm::vec3(x, y, z);
texCoords[i * (prec + 1) + j] = glm::vec2((float)j / prec, (float)i / prec);
}
}
//计算索引
for (int i = 0; i < prec; i++) {
for (int j = 0; j < prec; j++) {
indices[6 * (i * prec + j) + 0] = i * (prec + 1) + j;
indices[6 * (i * prec + j) + 1] = i * (prec + 1) + j + 1;
indices[6 * (i * prec + j) + 2] = (i + 1) * (prec + 1) + j;
indices[6 * (i * prec + j) + 3] = i * (prec + 1) + j + 1;
indices[6 * (i * prec + j) + 4] = (i + 1) * (prec + 1) + j + 1;
indices[6 * (i * prec + j) + 5] = (i + 1) * (prec + 1) + j;
}
}
}
Bernstein函数

初始化函数中可以看到其中使用了Bernstein函数, 在Bezier曲线和Bezier曲面的计算中,Bernstein基函数(Bernstein basis functions)起着重要的作用。它们是一组多项式函数,用于确定Bezier曲线和曲面上的点的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
float BezierFace::Bernstein(float t, int index) {
switch (index)
{
default:
case 0:
return pow(1.0 - t, 4);
break;
case 1:
return 4 * t * pow(1.0 - t, 3);
break;
case 2:
return 6 * pow(t, 2) * pow(1.0 - t, 2);
break;
case 3:
return 4 * pow(t, 3) * (1.0 - t);
break;
case 4:
return pow(t, 4);
break;
}
}

2.3.4 Model模型

具体实现和实验一中完全一致, 不再赘述

2.3.5 Mesh

具体实现和实验一中完全一致, 不再赘述

2.3.6 model_loading(Main方法)

主方法

加载Shader,Model,Camera
  • Camera camera(glm::vec3(0.0f, 0.0f, 10.0f));
  • Shader ourShader("../1.model_loading.vs", "../1.model_loading.fs");
  • Model ourModel("../OBJ_Files/airboat.obj");
鼠标键盘操作基于实验一, 这里不再赘述
  1. 首先引用相关的库, 其中通过#define STB_IMAGE_IMPLEMENTATIONstb_image.h编译为cpp文件使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define STB_IMAGE_IMPLEMENTATION
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include <stb_image.h>


#include <shader.h>
#include <camera.h>
#include <model.h>
#include <mesh.h>
#include <BezierFace.h>

#include <iostream>
  1. 定义函数, 和一些基本参数

    • 包含鼠标, 键盘控制函数, 对于贝塞尔曲面顶点的加载, 纹理的加载
    1
    2
    3
    4
    5
    6
    7
    void mouse_callback(GLFWwindow* window, double xpos, double ypos);
    void mouse_button_callback(GLFWwindow* window, int button, int action, int mods);
    void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
    void processInput(GLFWwindow *window);
    void setUpVertices();
    unsigned int loadTexture(char const* path);
    void framebuffer_size_callback(GLFWwindow* window, int width, int height);
    • 基本参数: 窗口大小, 相机, 光照, 时间以及贝塞尔曲面控制点位置
      • 此处控制点数组存有75个元素, 对应25(5*5)个控制点
    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
    // settings
    const unsigned int SCR_WIDTH = 800;
    const unsigned int SCR_HEIGHT = 600;

    // camera
    Camera camera(glm::vec3(0.5f, 0.5f, 3.5f));
    float lastX = SCR_WIDTH / 2.0f;
    float lastY = SCR_HEIGHT / 2.0f;
    bool firstMouse = true;

    // timing
    float deltaTime = 0.0f;
    float lastFrame = 0.0f;

    // lighting
    glm::vec3 lightPos(0.5f, 0.5f, 2.0f);

    // Control Points
    float controlPoints[] = {
    -1.5, -1.5, 2.0,
    -0.5, -1.5, 2.0,
    0.0, -1.5, 1.0,
    0.5, -1.5, -1.0,
    1.5, -1.5, 2.0,
    -1.5, -0.5, 1.0,
    -0.5, 1.5, 2.0,
    0.0, 0.5, -1.0,
    0.5, 0.5, 1.0,
    1.5, -0.5, -1.0,
    -1.5, 0.0, 0.0,
    -0.5, -1.5, -1.0,
    0.0, 0.0, 0.0,
    0.5, -1.0, 2.0,
    1.5, 1.0, 0.0,
    -1.5, 0.5, 2.0,
    -0.5, 0.5, 1.0,
    0.0, 0.5, 1.0,
    0.5, 0.5, 1.0,
    1.5, -1.5, 1.5,
    -1.5, 1.5, -2.0,
    -0.5, 1.5, -2.0,
    0.0, 1.5, 2.0,
    0.5, 0.5, 1.0,
    1.5, 1.5, -1.0
    };

    BezierFace myBezier = BezierFace(4, controlPoints, 100);
    GLuint VAO, VBO[3];
  2. 初始化, 加载窗口

    这部分代码固定不变, 同时打开了深度检测

    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
    		// glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    #ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    #endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "ModelLoading", NULL, NULL);
    if (window == NULL)
    {
    std::cout << "Failed to create GLFW window" << std::endl;
    glfwTerminate();
    return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    glfwSetMouseButtonCallback(window,mouse_button_callback);
    glfwSetScrollCallback(window, scroll_callback);

    // tell GLFW to capture our mouse
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
    std::cout << "Failed to initialize GLAD" << std::endl;
    return -1;
    }
    // configure global opengl state
    // -----------------------------
    glEnable(GL_DEPTH_TEST);
  3. 配置shader和model, 以及贝塞尔曲面的顶点和纹理信息

    这里贴图采用了天津大学的校旗图案

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // build and compile shaders
    // -------------------------
    Shader ourShader("../1.model_loading.vs", "../1.model_loading.fs");
    // load models
    Model ourModel("../OBJ_Files/airboat.obj");
    // setup Vertices
    setUpVertices();
    // load Textures
    unsigned int texture = loadTexture("../TJU.jpg");
  4. 在循环中绘制图

    其中为shader配置了参数, 因为单独添加Bezier曲面, Bezier曲面的信息单独在main函数中加载

    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
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    while (!glfwWindowShouldClose(window))
    {
    // per-frame time logic
    // --------------------
    float currentFrame = static_cast<float>(glfwGetTime());
    deltaTime = currentFrame - lastFrame;
    lastFrame = currentFrame;

    // input
    // -----
    processInput(window);

    // render
    // ------
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // don't forget to enable shader before setting uniforms
    ourShader.use();
    // be sure to activate shader when setting uniforms/drawing objects
    ourShader.use();
    ourShader.setVec3("light.position", camera.Position);
    ourShader.setVec3("light.direction", camera.Front);
    ourShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
    ourShader.setVec3("viewPos", camera.Position);

    // light properties
    ourShader.setVec3("light.ambient", 0.1f, 0.1f, 0.1f);
    // we configure the diffuse intensity slightly higher; the right lighting conditions differ with each lighting method and environment.
    // each environment and lighting type requires some tweaking to get the best out of your environment.
    ourShader.setVec3("light.diffuse", 0.8f, 0.8f, 0.8f);
    ourShader.setVec3("light.specular", 1.0f, 1.0f, 1.0f);
    ourShader.setFloat("light.constant", 1.0f);
    ourShader.setFloat("light.linear", 0.09f);
    ourShader.setFloat("light.quadratic", 0.032f);

    // material properties
    ourShader.setFloat("material.shininess", 32.0f);
    ourShader.setVec3("material.specular", 0.5f, 0.5f, 0.5f);


    // view/projection transformations
    glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
    glm::mat4 view = camera.GetViewMatrix();
    ourShader.setMat4("projection", projection);
    ourShader.setMat4("view", view);

    // render the loaded model
    glm::mat4 model = glm::mat4(1.0f);
    model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f)); // translate it down so it's at the center of the scene
    model = glm::scale(model, glm::vec3(1.0f, 1.0f, 1.0f)); // it's a bit too big for our scene, so scale it down
    ourShader.setMat4("model", model);




    model = glm::mat4(1.0f);
    model = glm::translate(model, lightPos);
    model = glm::scale(model, glm::vec3(0.2f)); // a smaller cube
    ourShader.setMat4("model", model);
    ourShader.setMat4("projection", projection);
    ourShader.setMat4("view", view);
    ourShader.setMat4("transform", glm::mat4(1.0f));


    //--

    glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);
    glEnableVertexAttribArray(1);

    glBindBuffer(GL_ARRAY_BUFFER, VBO[2]);
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, (void*)0);
    glEnableVertexAttribArray(2);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, texture);
    glEnable(GL_CULL_FACE);
    glFrontFace(GL_CCW);
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, myBezier.getNumIndices());
    glBindVertexArray(0);

    ourModel.Draw(ourShader);


    // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
    // -------------------------------------------------------------------------------
    glfwSwapBuffers(window);
    glfwPollEvents();
    }
  5. 最后终止

    1
    2
    glfwTerminate();
    return 0;
  6. 对这些函数进行实现

    • void mouse_callback(GLFWwindow* window, double xpos, double ypos);

    • void mouse_button_callback(GLFWwindow* window, int button, int action, int mods);

    • void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);

    • void framebuffer_size_callback(GLFWwindow* window, int width, int height);

    • void processInput(GLFWwindow* window);

    • ==void setUpVertices();==

    • ==unsigned int loadTexture(char const* path);==

      前五个函数处理键盘鼠标操作, 在实验一中有具体实现, 在此不再赘述, 此处展示setUpVertices()loadTexture()方法, 用于实现Bezier曲面

      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
      void setUpVertices()
      {
      vector<float> pvalues;
      vector<float> tvalues;
      vector<float> nvalues;
      vector<int> ind = myBezier.getIndices();
      vector<glm::vec3> verts = myBezier.getVertices();
      vector<glm::vec2> tex = myBezier.getTexCoords();
      vector<glm::vec3> norm = myBezier.getNormals();
      for (int i = 0; i < myBezier.getNumIndices(); i++)
      {
      pvalues.push_back(verts[ind[i]].x);
      pvalues.push_back(verts[ind[i]].y);
      pvalues.push_back(verts[ind[i]].z);
      tvalues.push_back(tex[ind[i]].s);
      tvalues.push_back(tex[ind[i]].t);

      nvalues.push_back(norm[ind[i]].x);
      nvalues.push_back(norm[ind[i]].y);
      nvalues.push_back(norm[ind[i]].z);
      }

      glGenVertexArrays(1, &VAO);
      glGenBuffers(3, VBO);
      glBindVertexArray(VAO);
      glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);
      glBufferData(GL_ARRAY_BUFFER, pvalues.size() * 4, &pvalues[0], GL_STATIC_DRAW);

      glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
      glBufferData(GL_ARRAY_BUFFER, nvalues.size() * 4, &nvalues[0], GL_STATIC_DRAW);

      glBindBuffer(GL_ARRAY_BUFFER, VBO[2]);
      glBufferData(GL_ARRAY_BUFFER, tvalues.size() * 4, &tvalues[0], GL_STATIC_DRAW);

      }

      unsigned int loadTexture(char const* path)
      {
      unsigned int textureID;
      glGenTextures(1, &textureID);
      glBindTexture(GL_TEXTURE_2D, textureID);

      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

      int width, height, nrComponents;
      stbi_set_flip_vertically_on_load(true);
      unsigned char* data = stbi_load(path, &width, &height, &nrComponents, 0);
      if (data) {
      glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
      glGenerateMipmap(GL_TEXTURE_2D);
      }
      else {
      std::cout << "Texture failed to load at path: " << path << std::endl;
      }
      stbi_image_free(data);
      return textureID;
      }

3. 结果与讨论

3.1 结果

在这一部分,你可以对你的项目结果适当的进行截图展示和说明。

Bezier曲面, 贴图天津大学校旗图案展示

image-20231106195347713

通过调整相机远近, 可以看到完整模型与Bezier曲面

image-20231106194529608

​ 同时可以通过键盘鼠标操作旋转视角

image-20231106195524997

3.2 讨论

在这一部分,你需要对你在开发过程中遇到的一些困难进行描述和你是如何攻克的,在方法上和结果上展开叙述。
第三章占整体篇幅的30%。

在实现Bezier曲面时,我面临了一些挑战。首先,我需要考虑如何加载和处理控制点。Bezier曲面通常由控制点定义,因此我需要找到一种有效的方式来加载这些控制点。我采取了以下措施来解决这些问题:

  • 数据结构选择:我选择了适当的数据结构,使用二维数组来存储控制点,以便能够轻松地访问和操作它们。
  • 数据输入:我考虑了数据的来源,可以是文件、用户输入或通过程序生成。我实现了文件格式的解析,以便从文件加载控制点。
  • Bezier曲面算法:我深入学习了Bezier曲面的计算算法,以确保我能够正确生成曲面。
  • 纹理贴图:在为Bezier曲面应用纹理贴图时,我必须考虑如何映射纹理坐标到曲面上。这可能涉及到在曲面上创建UV映射或使用其他技术,以确保纹理正确贴合曲面。

整合Bezier曲面和实验一的困难:

整合Bezier曲面和实验一是另一个具有挑战性的任务。最初,我发现两者无法同时显示出来,这可能是因为没有正确地绑定和解绑VAO导致的。为了解决这个问题,我采取了以下措施:

  • VAO和渲染状态:我确保了解OpenGL渲染管线的工作方式,以及如何正确地绑定和解绑VAO。通过及时的绑定和解绑VAO,我成功地整合了Bezier曲面和实验一,使它们能够同时显示。

    1
    2
    3
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, myBezier.getNumIndices());
    glBindVertexArray(0);
  • Shader程序:我确保两者之间的着色器程序设置和变量传递是正确的。在调试过程中,我检查了着色器程序是否编译和链接成功,并确保正确传递了变量和纹理。

  • OpenGL状态管理:我了解了OpenGL的状态管理,包括混合、深度测试、清除颜色缓冲等。在整合两者时,我确保了正确管理OpenGL状态,以避免不必要的问题。

  • 坐标转换和变换:我也考虑到了物体的变换,如旋转、平移或缩放,以确保这些变换适用于Bezier曲面,使它们在同一坐标系中正确呈现。

通过逐步调试和测试,我克服了这些挑战,成功地将Bezier曲面整合到实验一中,以实现更复杂的图形应用程序。这个过程不仅增加了我的对OpenGL和图形编程的理解,还提高了我的解决问题和整合不同组件的能力。这一章的详细描述将有助于读者了解我在实验中面对的挑战以及我是如何成功应对它们。

4. 收获和建议

课程收获,项目开发收获,课程建议等各个方面展开来叙述。内容上要有意义,量上要占整篇报告的10%左右。

在整个课程和项目开发过程中,我获得了许多宝贵的经验和知识,这些经验将对我产生深远的影响。以下是我在这个过程中的主要收获和一些建议:

课程收获:

  • 深入理解计算机图形学:这门课程使我更深入地理解了计算机图形学的核心概念,包括OpenGL渲染管线、着色器编程、纹理映射等。我现在更清楚地知道如何创建和渲染三维图形。
  • 编程技能提升:通过实际的项目开发,我不仅提高了C++编程技能,还学会了如何使用OpenGL进行图形编程。这些技能对我的编程能力产生了积极影响,不仅局限于图形学,还可应用于其他领域。

项目开发收获:

  • Bezier曲面实现:在这个项目中,我成功地实现了Bezier曲面的渲染,这是一个非常有挑战性的任务。我深入了解了Bezier曲面的数学原理,以及如何在OpenGL中实现它。

课程建议:

  • 更多实际练习:尽管理论知识很重要,但更多的实际练习和项目可以更好地巩固学习。建议在课程中增加更多的实际编程和项目任务,以加强学生的实际编程技能。

总的来说,这门课程和项目开发经历为我提供了宝贵的机会,让我深入研究计算机图形学和图形编程。我相信我所获得的知识和技能将对我的学术和职业生涯产生长远的影响,同时也期待在未来的学术和职业发展中继续学习和成长。感谢这个精彩的学习经验!

Read More

2023-10-19
CG-Assignment1

本文为计算机图形学作业2报告, 为本人计算机图形学课程作业, 仅供参考, 未经允许不得转载, 抄袭.

1.引言

本次项目完成作业1,实现了对obj文件的加载, 并对模型进行键盘和鼠标控制

具体实现如下:

  1. 实现obj文件的加载
  2. 对模型加键盘控制,通过键盘可以实现模型的缩放、平移和旋转。
    例如,按压右/左方向键,模型可以向右/左移动。具体按键和其对应的功能自行设定,但需要在报告中详细给出文字说明。
  3. 对相机加鼠标控制,主要包含以下两个功能:
    a. 模型本身保持固定,拖动鼠标左键实现视角 (lookat matrix)的改变
    b. 模型本身保持固定,拖动鼠标右键实现视点(viewpoint position)的移动

2. 主体介绍

2.1 程序简介

  1. 编程环境

    MacOS,C++11,IDE: CLion,cmake,使用OpenGL,Glad,GLFW3.3.8,glm0.9.9.1以及Assimp

  2. 文件结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Computer_Graphics_Assign1

    ├── model_loading.cpp
    ├── 1.model_loading.fs
    ├── 1.model_loading.vs
    ├── CMakeLists.txt
    ├──
    ├── OBJ_Files
    │ ├── airboat.obj
    │ └── bunny.obj

    ├── includes
    │ ├── camera.h
    │ ├── model.h
    │ ├── shader.h
    │ └── mesh.h
    └── ...Glad & Glm & cmake-build-debug

2.2 算法

在这一部分需要描述你的项目实现过程中用了哪些算法,需要你从算法背后的原理入手结合自己的项目进行描述,切忌段落式的粘贴代码,如果是自己写的算法,进行适当的算法复杂度分析。
示例:a) Lambert 漫反射光照模型 b) 正投影相机 c) Phong光照模型 d) 一些图形的绘制举例 e) 碰撞测试特例算法
注:一定要结合自己的项目去写这一部分,落到实处,切忌直接从网上直接粘贴理论知识。

2.2.1 OBJ文件的加载

首先是对于OBJ文件的加载,我使用了一个非常流行的模型导入库——Assimp

Assimp能够导入很多种不同的模型文件格式,它会将所有的模型数据加载至Assimp的通用数据结构中。当Assimp加载完模型之后,就能够从Assimp的数据结构中提取所需的所有数据。

img
  • 所有的场景/模型数据都包含在Scene对象
  • 一个Mesh对象本身包含了渲染所需要的所有相关数据,像是顶点位置、法向量、纹理坐标、面(Face)和物体的材质。
  • 最终仍要将这些数据转换为OpenGL能够理解的格式,才能渲染这个物体
2.2.2 键盘鼠标控制操作

详细思想和代码实现见Camera和Model_loading处

2.3 实现细节

在这一部分,将自己的项目拆解成各个部分进行说明。切忌段落式的粘贴代码。
第二章应占整体篇幅的40%。

2.3.1 Mesh网格

定义一个顶点结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Vertex {
// position
glm::vec3 Position;
// normal 法线
glm::vec3 Normal;
// texCoords
glm::vec2 TexCoords;
// tangent 切线
glm::vec3 Tangent;
// bitangent 双切线
glm::vec3 Bitangent;
//bone indexes which will influence this vertex
int m_BoneIDs[MAX_BONE_INFLUENCE];
//weights from each bone
float m_Weights[MAX_BONE_INFLUENCE];
};

定义网格类

构造函数导入导入数据,调用setupMesh()方法,在setupMesh()函数中初始化缓冲,并最终使用Draw()函数来绘制网格

构造函数

1
2
3
4
5
6
Mesh(vector<Vertex> vertices, vector<unsigned int> indices)
{
this->vertices = vertices;
this->indices = indices;
setupMesh();
}

其中包含setupMesh()

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
void setupMesh()
{
// create buffers/arrays
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);

glBindVertexArray(VAO);
// load data into vertex buffers
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);

// set the vertex attribute pointers
// vertex Positions
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
// vertex normals
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
// vertex tangent
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Tangent));
// vertex bitangent
glEnableVertexAttribArray(4);
glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Bitangent));
// ids
glEnableVertexAttribArray(5);
glVertexAttribIPointer(5, 4, GL_INT, sizeof(Vertex), (void*)offsetof(Vertex, m_BoneIDs));
// weights
glEnableVertexAttribArray(6);
glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, m_Weights));
glBindVertexArray(0);
}

以及提供Draw(),绘制每个mesh

1
2
3
4
5
6
7
8
void Draw(Shader &shader)
{
// draw mesh
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, static_cast<unsigned int>(indices.size()), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);

}

2.3.2 Model模型

定义Model类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Model 
{
public:
vector<Mesh> meshes;
string directory;
// 构造函数
Model(char *path)
{
loadModel(path);
}
void Draw(Shader shader);
private:
void loadModel(string path);
void processNode(aiNode *node, const aiScene *scene);
Mesh processMesh(aiMesh *mesh, const aiScene *scene);
vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type,
string typeName);
};

构造函数

其中包含了一个Mesh对象的vector,构造函数包含loadModel()函数和draw()函数

draw()函数遍历了所有网格,并调用它们各自的Draw函数

1
2
3
4
5
void Draw(Shader shader)
{
for(unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
}

loadModel()函数,需要传入一个文件路径path,之后使用Assimp来加载模型至Assimp的一个叫做scene的数据结构中

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
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

// loadModel
void loadModel(string path)
{
// Assimp抽象掉了加载不同文件格式的所有技术细节,只需要一行代码就能完成所有的工作
// 声明了Assimp命名空间内的一个Importer,之后调用了它的ReadFile函数
Assimp::Importer import;
const aiScene *scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);

if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
{
cout << "ERROR::ASSIMP::" << import.GetErrorString() << endl;
return;
}
directory = path.substr(0, path.find_last_of('/'));

processNode(scene->mRootNode, scene);
}

// processNode
void processNode(aiNode *node, const aiScene *scene)
{
// 处理节点所有的网格(如果有的话)
for(unsigned int i = 0; i < node->mNumMeshes; i++)
{
aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
meshes.push_back(processMesh(mesh, scene));
}
// 接下来对它的子节点重复这一过程
for(unsigned int i = 0; i < node->mNumChildren; i++)
{
processNode(node->mChildren[i], scene);
}
}

之后调用processMesh()函数,这样就可以将从obj模型文件中加载出的数据处理到我设置的Mesh类中

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
Mesh processMesh(aiMesh *mesh, const aiScene *scene) {
// data to fill
vector<Vertex> vertices;
vector<unsigned int> indices;

// walk through each of the mesh's vertices
for (unsigned int i = 0; i < mesh->mNumVertices; i++) {
Vertex vertex;
glm::vec3 vector;
// assimp使用自己的vector类,不能直接转换为glm的vec3类
// 所以首先将数据传输到这个占位符glm::vec3。
// positions
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;
// normals
if (mesh->HasNormals()) {
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
}
vertices.push_back(vertex);
}
// 遍历每个网格的面(面是一个网格的三角形)并检索相应的顶点索引
for (unsigned int i = 0; i < mesh->mNumFaces; i++) {
aiFace face = mesh->mFaces[i];
for (unsigned int j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}

return Mesh(vertices, indices);
}

2.3.3 shader.h

与上课时给出的shader.h代码相同

使用C++文件流读取着色器内容,储存到几个string对象里

以下实现了对着色器的编译和链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned int vertex, fragment;
// vertex shader
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
checkCompileErrors(vertex, "VERTEX");
// fragment Shader
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
checkCompileErrors(fragment, "FRAGMENT");
// shader Program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
checkCompileErrors(ID, "PROGRAM");
// delete the shaders as they're linked into our program now and no longer necessary
glDeleteShader(vertex);
glDeleteShader(fragment);

定义了

1
void use() const { glUseProgram(ID); }

2.3.4 camera.h

提供ProcessKeyboard, ProcessMouseMovement, ProcessMouseScroll方法,用于实现鼠标键盘控制

Camera 类 定义了位置,接收变化的变量,欧拉角,选项和构造函数,此外还有键盘鼠标操作函数和相机更新函数

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
class Camera
{
public:
// camera Attributes
glm::vec3 Position;
glm::vec3 Front;
glm::vec3 Up;
glm::vec3 Right;
glm::vec3 WorldUp;
// euler Angles
float Yaw;
float Pitch;
// camera options
float MovementSpeed;
float MouseSensitivity;
float Zoom;

// constructor with vectors
Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f),
float yaw = YAW, float pitch = PITCH) :
Front(glm::vec3(0.0f, 0.0f, -1.0f)),
MovementSpeed(SPEED),
MouseSensitivity(SENSITIVITY),
Zoom(ZOOM)
{
Position = position;
WorldUp = up;
Yaw = yaw;
Pitch = pitch;
updateCameraVectors();
}
// constructor with scalar values
Camera(float posX, float posY, float posZ, float upX, float upY, float upZ, float yaw, float pitch) :
Front(glm::vec3(0.0f, 0.0f, -1.0f)),
MovementSpeed(SPEED),
MouseSensitivity(SENSITIVITY),
Zoom(ZOOM)
{
Position = glm::vec3(posX, posY, posZ);
WorldUp = glm::vec3(upX, upY, upZ);
Yaw = yaw;
Pitch = pitch;
updateCameraVectors();
}

// returns the view matrix calculated using Euler Angles and the LookAt Matrix
glm::mat4 GetViewMatrix()
{
return glm::lookAt(Position, Position + Front, Up);
}
};

ProcessKeyboard处理从任何类似键盘的输入系统接收的输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void ProcessKeyboard(Camera_Movement direction, float deltaTime)
{
float velocity = MovementSpeed * deltaTime;
if (direction == FORWARD)
Position += Front * velocity;
if (direction == BACKWARD)
Position -= Front * velocity;
if (direction == LEFT)
Position -= Right * velocity;
if (direction == RIGHT)
Position += Right * velocity;
if (direction == DOWN)
Position -= Up * velocity;
if (direction == UP)
Position += Up * velocity;
}

ProcessMouseMovement处理从鼠标输入系统接收的输入。期望x和y方向上的偏移值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch = true)
{
xoffset *= MouseSensitivity;
yoffset *= MouseSensitivity;

Yaw += xoffset;
Pitch += yoffset;

// make sure that when pitch is out of bounds, screen doesn't get flipped
if (constrainPitch)
{
if (Pitch > 89.0f)
Pitch = 89.0f;
if (Pitch < -89.0f)
Pitch = -89.0f;
}

// update Front, Right and Up Vectors using the updated Euler angles
updateCameraVectors();
}

ProcessMouseScroll处理从鼠标滚轮事件接收的输入。只需要在垂直轮轴上输入

1
2
3
4
5
6
7
8
void ProcessMouseScroll(float yoffset)
{
Zoom -= (float)yoffset;
if (Zoom < 1.0f)
Zoom = 1.0f;
if (Zoom > 45.0f)
Zoom = 45.0f;
}

updateCameraVectors

1
2
3
4
5
6
7
8
9
10
11
12
void updateCameraVectors()
{
// calculate the new Front vector
glm::vec3 front;
front.x = cos(glm::radians(Yaw)) * cos(glm::radians(Pitch));
front.y = sin(glm::radians(Pitch));
front.z = sin(glm::radians(Yaw)) * cos(glm::radians(Pitch));
Front = glm::normalize(front);
// also re-calculate the Right and Up vector
Right = glm::normalize(glm::cross(Front, WorldUp));
Up = glm::normalize(glm::cross(Right, Front));
}

2.3.5 model_loading(Main方法)

主方法

加载Shader,Model,Camera
  • Camera camera(glm::vec3(0.0f, 0.0f, 10.0f));
  • Shader ourShader("../1.model_loading.vs", "../1.model_loading.fs");
  • Model ourModel("../OBJ_Files/airboat.obj");
鼠标键盘操作

processInput (定义了键盘按键对应上下左右前后操作) 见processInput(GLFWwindow *window)

  • W上/ S下/ A左/ D右/ Q前/ E后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);

if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS)
camera.ProcessKeyboard(FORWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS)
camera.ProcessKeyboard(BACKWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.ProcessKeyboard(LEFT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.ProcessKeyboard(RIGHT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.ProcessKeyboard(UP, deltaTime);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.ProcessKeyboard(DOWN, deltaTime);
}

mouse_button_callback, mouse_callback, scroll_callback 实现了鼠标的操作

  • mouse_button_callback是检测鼠标点击左键右键时候对相机的更新操作

  • mouse_callback是检测到鼠标移动时对相机的更新操作

  • ==这里定义了两个bool值来判断,只有在按压左键/右键时才对鼠标的移动进行处理==

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
bool leftMouseButtonDown = false;
bool rightMouseButtonDown = false;

void mouse_callback(GLFWwindow* window, double xposIn, double yposIn)
{
if (leftMouseButtonDown) {
float xpos = static_cast<float>(xposIn);
float ypos = static_cast<float>(yposIn);

if (firstMouse) {
lastX = xpos;
lastY = ypos;
firstMouse = false;
}

float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top

lastX = xpos;
lastY = ypos;

camera.ProcessMouseMovement(xoffset, yoffset);
}

if (rightMouseButtonDown) {
float xpos = static_cast<float>(xposIn);
float ypos = static_cast<float>(yposIn);
float xOffset = xpos - lastX;
float yOffset = lastY - ypos; // 注意y方向翻转

xOffset *= 0.1f;
yOffset *= 0.1f;

// 更新相机位置
camera.Position += camera.Right * xOffset + camera.Up * yOffset;

lastX = xpos;
lastY = ypos;
}

}

void mouse_button_callback(GLFWwindow* window, int button, int action, int mods) {
if (button == GLFW_MOUSE_BUTTON_LEFT) {
if(action == GLFW_PRESS){
leftMouseButtonDown = true;
glfwSetCursorPosCallback(window, mouse_callback);
}
else{leftMouseButtonDown = false;}

}
if (button == GLFW_MOUSE_BUTTON_RIGHT) {
if(action == GLFW_PRESS){
rightMouseButtonDown = true;
glfwSetCursorPosCallback(window, mouse_callback);
}
else{leftMouseButtonDown = false;}
}
}
  • 鼠标滚轮

    1
    2
    3
    4
    void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
    {
    camera.ProcessMouseScroll(static_cast<float>(yoffset));
    }
加载窗口,使用之前配置的Model, Mesh, Camera, Shader
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include <shader.h>
#include <camera.h>
#include <model.h>

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void mouse_button_callback(GLFWwindow* window, int button, int action, int mods);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

// camera
Camera camera(glm::vec3(0.0f, 0.0f, 10.0f));
float lastX = SCR_WIDTH / 2.0f;
float lastY = SCR_HEIGHT / 2.0f;
bool firstMouse = true;

// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;

glm::vec3 lightPos(1.2f, 1.0f, 2.0f);


int main()
{
// glfw: initialize and configure
// ------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "ModelLoading", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

glfwSetMouseButtonCallback(window,mouse_button_callback);
glfwSetScrollCallback(window, scroll_callback);

// tell GLFW to capture our mouse
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}



// configure global opengl state
// -----------------------------
glEnable(GL_DEPTH_TEST);

// build and compile shaders
// -------------------------
Shader ourShader("../1.model_loading.vs", "../1.model_loading.fs");
Shader lightCubeShader("../configFiles/2.2.light_cube.vs", "../configFiles/2.2.light_cube.fs");

// Shader ourShader("../1.colors.vert", "../1.colors.vert");

// load models
// -----------
Model ourModel("../OBJ_Files/airboat.obj");


// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// per-frame time logic
// --------------------
float currentFrame = static_cast<float>(glfwGetTime());
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;

// input
// -----
processInput(window);

// render
// ------
// glClearColor(0.05f, 0.05f, 0.05f, 1.0f);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// don't forget to enable shader before setting uniforms
ourShader.use();
// be sure to activate shader when setting uniforms/drawing objects
ourShader.setVec3("lightColor", 1.0f, 1.5f, 1.0f);
ourShader.setVec3("lightPos", lightPos);
ourShader.setVec3("viewPos", camera.Position);

// view/projection transformations
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
ourShader.setMat4("projection", projection);
ourShader.setMat4("view", view);

// render the loaded model
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f)); // translate it down so it's at the center of the scene
model = glm::scale(model, glm::vec3(1.0f, 1.0f, 1.0f)); // it's a bit too big for our scene, so scale it down
ourShader.setMat4("model", model);
ourModel.Draw(ourShader);
// ourModel2.Draw(ourShader);
// ourModel3.Draw(ourShader);

//--
lightCubeShader.use();
lightCubeShader.setMat4("projection", projection);
lightCubeShader.setMat4("view", view);
model = glm::mat4(1.0f);
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f)); // a smaller cube
lightCubeShader.setMat4("model", model);
//--

// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}

// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}

2.3.6 着色器配置文件

由于obj文件没有纹理和材质的信息,因此我加入了光照使得可以看清模型的立体结构

无光照效果如下

image-20231018215433970

加入光照后效果如下

image-20231019165328316
1.model_loading.vs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

out vec3 FragPos;
out vec3 Normal;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal;
gl_Position = projection * view * vec4(FragPos, 1.0);
}
1.model_loading.fs
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
#version 330 core
out vec4 FragColor;

in vec3 Normal;
in vec3 FragPos;

uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;

void main()
{
vec3 objectColor = vec3(1.0f, 0.5f, 0.31f);

// ambient
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;

// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;

// specular
float specularStrength = 0.5;
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;

vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);

}

3. 结果与讨论

3.1 结果

初始效果
image-20231019165801231 ##### 向前 image-20231019165841034 ##### 向后 image-20231019165917879 ##### 向上 image-20231019165942015 ##### 旋转 image-20231019170021162

其他具体操作见视频

3.2 讨论

在这一部分,你需要对你在开发过程中遇到的一些困难进行描述和你是如何攻克的,在方法上和结果上展开叙述。
第三章占整体篇幅的30%。

  1. 模型加载和显示问题:最初尝试自己解析和加载OBJ文件时,我遇到了各种问题,包括正确读取文件、解析顶点、法线和纹理坐标数据等。这些问题耗费了很多时间。然而,通过使用Assimp库,加载模型变得更加容易和高效。
  2. 相机控制和光照调试:实现键盘和鼠标控制相机位置和方向时,需要深入理解OpenGL的视图和投影矩阵,以及如何处理用户输入。同时,由于最开始没有材质纹理和光照,显示效果很糟糕,我尝试自己学习添加光照,添加光照效果也需要仔细调整材质、光照强度和颜色等参数,以获得满意的渲染结果。
  3. 着色器编写:编写顶点和片段着色器来实现光照模型是一个有挑战性的任务。理解光照模型的原理,以及如何在着色器中计算环境光、漫反射和镜面反射等部分也很有挑战

4. 收获和建议

课程收获,项目开发收获,课程建议等各个方面展开来叙述。内容上要有意义,量上要占整篇报告的10%左右。

  • 更深入的理解OpenGL:通过项目,我加深了对OpenGL的理解,包括顶点和片段着色器、VBO、VAO等概念。我学会了如何加载和渲染3D模型,以及如何处理用户输入来控制相机位置和方向。
  • 熟悉了Assimp库:使用Assimp库来加载模型是一个非常有用的技能,它使加载不同格式的3D模型变得更加容易。我学到了如何使用Assimp导入模型数据,以及如何在OpenGL中使用这些数据。
Read More