继面试中的手撕代码(一)之后,由于篇幅过长,所以新开了一篇文章记录面试过程中可能让实现的代码~
LRU算法
LRU(Least Recently Used)算法是一种常用的缓存淘汰策略 ,它的核心思想是移除最近最少使用的缓存条目,以保留最近使用的数据。实现LRU算法的一种常见方法是使用双向链表和哈希表的结合。
下面是一个用JavaScript实现LRU算法的例子,附带详细的注释来解释实现的思路:
javascript
class LRUCache {
constructor(capacity) {
this.capacity = capacity; // 缓存容量
this.cache = new Map(); // 使用Map来存储缓存数据
}
// 获取缓存数据
get(key) {
if (this.cache.has(key)) {
// 如果缓存中存在该key,将其删除并重新放入Map,表示它被最近使用过
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
} else {
// 如果缓存中不存在该key,返回-1表示未找到
return -1;
}
}
// 插入缓存数据
put(key, value) {
if (this.cache.has(key)) {
// 如果缓存中已经存在该key,删除原有位置的数据
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// 如果缓存已满,删除最久未使用的数据(Map中的第一个数据)
this.cache.delete(this.cache.keys().next().value);
}
// 将新数据插入到Map的尾部表示它是最近使用过的
this.cache.set(key, value);
}
}
// 使用示例
const lruCache = new LRUCache(2); // 创建容量为2的LRU缓存
lruCache.put(1, 1); // 缓存变为 {1=1}
lruCache.put(2, 2); // 缓存变为 {1=1, 2=2}
console.log(lruCache.get(1)); // 返回 1,缓存变为 {2=2, 1=1}
lruCache.put(3, 3); // 缓存变为 {1=1, 3=3}
console.log(lruCache.get(2)); // 返回 -1(未找到)
lruCache.put(4, 4); // 缓存变为 {3=3, 4=4}
console.log(lruCache.get(1)); // 返回 -1(未找到)
console.log(lruCache.get(3)); // 返回 3
console.log(lruCache.get(4)); // 返回 4
实现思路解释:
-
使用Map来存储缓存数据: Map是一个键值对的集合,可以通过键快速查找值。在LRU缓存中,Map的键是缓存的键,值是缓存的值。
-
get方法的实现: 当尝试获取某个键的值时,如果该键存在于缓存中,就将它删除并重新插入Map中。这样做的效果是,最近被访问的项会被移到Map的末尾,保持了最近使用的顺序。
-
put方法的实现: 当要插入新的键值对时,如果该键已经存在于缓存中,就将它删除。如果缓存已满,就删除Map中第一个(最久未使用的)项。然后,将新的键值对插入到Map的末尾,表示它是最近使用的项。
这种实现方式保证了在缓存达到容量上限时,删除最久未使用的项,从而保持了缓存的最近使用顺序,实现了LRU算法。
此算法的时间复杂度是O(1),因为Map是基于哈希表实现的。
将十进制数转换为任何进制
javascript
function decimalToBase(n, base) {
if (n === 0) {
return '0'; // 处理特殊情况:十进制0直接转换为0
}
if (base < 2 || base > 36) {
return 'Invalid base'; // 处理不支持的进制
}
const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // 定义0到35对应的字符
let result = '';
while (n > 0) {
const remainder = n % base; // 计算余数
result = digits[remainder] + result; // 将余数对应的字符加入结果
n = Math.floor(n / base); // 更新n为商的整数部分
}
return result;
}
这个函数接受两个参数:要转换的十进制数 n
和目标进制 base
。它首先检查特殊情况,如果输入的十进制数是0,直接返回 '0'。然后,它检查进制是否在2到36之间,因为我们使用了0到9和A到Z这些字符来表示各个进制的数字。
接下来,它使用一个循环来不断计算余数和商,将余数对应的字符添加到结果字符串中,然后更新n为商的整数部分。最终,它返回转换后的结果字符串。
以下是一些示例用法:
javascript
console.log(decimalToBase(10, 2)); // 输出 '1010',将十进制10转换为二进制
console.log(decimalToBase(255, 16)); // 输出 'FF',将十进制255转换为十六进制
console.log(decimalToBase(31, 8)); // 输出 '37',将十进制31转换为八进制
console.log(decimalToBase(0, 5)); // 输出 '0',特殊情况:将十进制0转换为任何进制都是'0'
这个函数可以将十进制数转换为任何进制,并支持2到36之间的进制。可以根据需要修改 digits
字符串来适应其他字符集。
将其他进制转化为十进制
js
function convertToDecimal(number, base) {
if (base < 2 || base > 36) {
return "不支持的进制";
}
number = (number + '').toUpperCase(); // 将输入转换为大写字符串
const digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; // 用于表示各个进制的字符
let result = 0;
// 从字符串的最后一位(低位)开始遍历
for (let i = number.length - 1; i >= 0; i--) {
const digit = digits.indexOf(number[i]); // 获取当前字符在字符集中的索引
if (digit === -1 || digit >= base) {
return "无效的输入"; // 如果字符不在字符集中或大于等于基数,返回错误
}
result += digit * Math.pow(base, number.length - 1 - i);
}
return result;
}
// 示例用法
console.log(convertToDecimal("101010", 2)); // 42(二进制转十进制)
console.log(convertToDecimal("52", 8)); // 42(八进制转十进制)
console.log(convertToDecimal("2A", 16)); // 42(十六进制转十进制)
console.log(convertToDecimal("1111", 5)); // 156(五进制转十进制)
promise.all
js
function myPromiseAll(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
reject(new TypeError('Promises must be an array'));
return;
}
let results = [];
let completedPromises = 0;
for (let i = 0; i < promises.length; i++) {
promises[i]
.then((result) => {
results[i] = result;
completedPromises++;
if (completedPromises === promises.length) {
resolve(results);
}
})
.catch((error) => {
reject(error);
});
}
if (promises.length === 0) {
resolve(results);
}
});
}
// 示例用法
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
myPromiseAll([promise1, promise2, promise3])
.then((results) => {
console.log('All promises resolved:', results);
})
.catch((error) => {
console.error('One of the promises was rejected:', error);
});
promise.allSettled
js
function myPromiseAllSettled(promises) {
return new Promise((resolve) => {
if (!Array.isArray(promises)) {
resolve(new TypeError('Promises must be an array'));
return;
}
let settledPromises = 0;
let results = [];
for (let i = 0; i < promises.length; i++) {
promises[i]
.then(
(value) => {
results[i] = { status: 'fulfilled', value };
},
(reason) => {
results[i] = { status: 'rejected', reason };
}
)
.finally(() => {
settledPromises++;
if (settledPromises === promises.length) {
resolve(results);
}
});
}
if (promises.length === 0) {
resolve(results);
}
});
}
// 示例用法
const promise1 = Promise.resolve(1);
const promise2 = Promise.reject('Error 2');
const promise3 = new Promise((resolve) => setTimeout(() => resolve(3), 1000));
myPromiseAllSettled([promise1, promise2, promise3])
.then((results) => {
console.log('All promises settled:', results);
});
promise.race
js
function myPromiseRace(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
reject(new TypeError('Promises must be an array'));
return;
}
for (let i = 0; i < promises.length; i++) {
promises[i]
.then((value) => {
// 第一个完成的 Promise,解决新的 Promise
resolve(value);
})
.catch((reason) => {
// 第一个被拒绝的 Promise,拒绝新的 Promise
reject(reason);
});
}
if (promises.length === 0) {
reject(new Error('No promises to race'));
}
});
}
// 示例用法
const promise1 = new Promise((resolve) => setTimeout(() => resolve('Result 1'), 1000));
const promise2 = new Promise((resolve, reject) => setTimeout(() => reject('Error 2'), 500));
const promise3 = new Promise((resolve) => setTimeout(() => resolve('Result 3'), 1500));
myPromiseRace([promise1, promise2, promise3])
.then((value) => {
console.log('First completed:', value);
})
.catch((reason) => {
console.error('First rejected:', reason);
});
promise.any
js
function myPromiseAny(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
reject(new TypeError('Promises must be an array'));
return;
}
let rejectedPromises = 0;
for (let i = 0; i < promises.length; i++) {
promises[i]
.then((value) => {
// 第一个完成的 Promise,解决新的 Promise
resolve(value);
})
.catch((reason) => {
// 记录被拒绝的 Promise
rejectedPromises++;
// 如果所有 Promise 都被拒绝,拒绝新的 Promise
if (rejectedPromises === promises.length) {
reject(new AggregateError('All promises were rejected'));
}
});
}
if (promises.length === 0) {
reject(new Error('No promises to race'));
}
});
}
// 示例用法
const promise1 = new Promise((resolve) => setTimeout(() => resolve('Result 1'), 1000));
const promise2 = new Promise((resolve, reject) => setTimeout(() => reject('Error 2'), 500));
const promise3 = new Promise((resolve) => setTimeout(() => resolve('Result 3'), 1500));
myPromiseAny([promise1, promise2, promise3])
.then((value) => {
console.log('First completed:', value);
})
.catch((reason) => {
console.error('All rejected:', reason);
});
promise
Promise
是 JavaScript 中用于处理异步操作的对象。以下是一个简单的使用 JavaScript 实现 Promise
的例子:
javascript
class CustomPromise {
constructor(executor) {
this.status = 'pending';
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.status === 'pending') {
this.status = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach((callback) => callback(this.value));
}
};
const reject = (reason) => {
if (this.status === 'pending') {
this.status = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach((callback) => callback(this.reason));
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (value) => value;
onRejected = typeof onRejected === 'function' ? onRejected : (reason) => { throw reason; };
const newPromise = new CustomPromise((resolve, reject) => {
if (this.status === 'fulfilled') {
setTimeout(() => {
try {
const result = onFulfilled(this.value);
resolvePromise(newPromise, result, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
if (this.status === 'rejected') {
setTimeout(() => {
try {
const result = onRejected(this.reason);
resolvePromise(newPromise, result, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
if (this.status === 'pending') {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const result = onFulfilled(this.value);
resolvePromise(newPromise, result, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const result = onRejected(this.reason);
resolvePromise(newPromise, result, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
}
});
return newPromise;
}
catch(onRejected) {
return this.then(null, onRejected);
}
}
// `resolvePromise` 函数是用来处理 `then` 方法中的回调函数返回的结果,确保返回的结果符合 `Promise/A+` 规范。
function resolvePromise(newPromise, result, resolve, reject) {
// 循环引用检测: 如果新的 `Promise` 对象和返回值是同一个对象,那么存在循环引用,这是不被允许的,因此会抛出一个 `TypeError`。
if (newPromise === result) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
// 递归处理: 如果返回值是一个 `Promise` 对象,我们需要递归调用 `resolvePromise` 处理它的状态和值。
if (result instanceof CustomPromise) {
result.then(
(value) => resolvePromise(newPromise, value, resolve, reject),
(reason) => reject(reason)
);
} else {
// 正常的结果处理:如果返回值不是 `Promise` 对象,直接调用 `resolve` 函数。
resolve(result);
}
}
// 示例用法
const promise = new CustomPromise((resolve, reject) => {
setTimeout(() => {
const random = Math.random();
if (random > 0.5) {
resolve('Resolved!');
} else {
reject('Rejected!');
}
}, 1000);
});
promise
.then((value) => {
console.log('Success:', value);
return 'New Value';
})
.then((value) => {
console.log('Chained Success:', value);
throw new Error('Custom Error');
})
.catch((reason) => {
console.error('Error:', reason);
});
在这个例子中,CustomPromise
类包含了 then
和 catch
方法,用于注册异步操作的成功和失败回调。 resolve
和 reject
函数用于改变 Promise
对象的状态。这个简化版的实现并没有处理所有边界情况,真实的 Promise
实现更加复杂和健壮。在实际应用中,建议使用原生的 Promise
。
数组和字符串
数组去重
js
function removeRepeat(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
let isRepeat = false;
for (let j = 0; j < result.length; j++) {
if (result[j] === arr[i]) {
isRepeat = true;
break;
}
};
if (!isRepeat) {
result.push(arr[i]);
}
};
return result
}
var arr = [9,9,5,4,1, 2, 2, 3, 4, 4, 5];
var result = removeRepeat(arr);
console.log(result); // 输出 [ 9, 5, 4, 1, 2, 3 ]
以下方案时间复杂度为O(n):
js
function removeRepeatElement(arr) {
let map = {};
let result = [];
for (let i = 0; i < arr.length; i++) {
if (!map[arr[i]]) {
map[arr[i]] = true;
result.push(arr[i])
}
}
return result;
}
console.log(removeRepeatElement([1,1,1,2,2,2,2,3,3,3,3,4,4,4]))
寻找数组中的最大/最小元素。
时间复杂度(O(n)),其中 n 是数组的长度。
js
function findMaxElement(arr) {
let maxElement = arr[0];
for (let i = 1; i < arr.length; i++) {
if (maxElement < arr[i]) {
maxElement = arr[i]
}
}
return maxElement
}
console.log('max', findMaxElement([2, 1, 6, 2, 4, 1]))
function findMinElement(arr) {
let minElement = arr[0];
for (let i = 1; i < arr.length; i++) {
if (minElement > arr[i]) {
minElement = arr[i]
}
}
return minElement
}
console.log('min', findMinElement([2, 1, 6, 2, 4, -1]))
在数组中查找特定的元素
可以使用数组的indexOf()方法或includes()方法来查找数组中特定元素。这两种方法都会在数组中进行线性搜索,因此时间复杂度为 O(n),其中 n 是数组的长度。
以下手动实现复杂度也是O(n):
js
function findElement(arr, target) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) {
return i; // 返回特定元素的索引
}
}
return -1; // 如果数组中不存在该元素,返回 -1
}
反转数组
采用双指针法,时间复杂度是O(n/2),其中 n 是数组的长度,因为它只需要遍历数组的一半元素。
js
function reverseArray(arr) {
let left = 0; // 左指针
let right = arr.length - 1; // 右指针
// 交换左右指针所指向的元素,然后移动指针,直到左指针大于等于右指针
while (left < right) {
// 交换左右指针所指向的元素
let temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
// 移动指针
left++;
right--;
}
return arr;
}
// 示例用法
let arr = [1, 2, 3, 4, 5];
let reversedArray = reverseArray(arr);
console.log(reversedArray); // 输出 [5, 4, 3, 2, 1]
合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
示例 : 输入: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] 。
使用双指针法来解决:
js
function mergeSortedArrays(nums1, m, nums2, n) {
let i = m - 1; // nums1的末尾索引
let j = n - 1; // nums2的末尾索引
let k = m + n - 1; // 合并后数组的末尾索引
while (i >= 0 && j >= 0) {
if (nums1[i] > nums2[j]) {
nums1[k] = nums1[i];
i--;
} else {
nums1[k] = nums2[j];
j--;
}
k--;
}
// 如果nums2中还有剩余元素,将其复制到nums1的前面
while (j >= 0) {
nums1[k] = nums2[j];
j--;
k--;
}
}
// 示例用法
let nums1 = [1, 2, 3, 0, 0, 0];
let m = 3;
let nums2 = [2, 5, 6];
let n = 3;
mergeSortedArrays(nums1, m, nums2, n);
console.log(nums1); // 输出 [1, 2, 2, 3, 5, 6]
这个算法的基本思路是,从两个有序数组的末尾开始,比较两个数组的元素,将较大的元素放入合并后的数组的末尾。然后依次向前移动指针,直到其中一个数组遍历完成。如果此时第二个数组中还有剩余的元素,直接将其复制到合并后的数组的前面。
这种算法的时间复杂度是 O(m + n),其中 m 和 n 分别是两个输入数组的长度。这是因为我们需要遍历两个数组中的所有元素一次。而空间复杂度是 O(1),因为我们只是使用了常量级的额外空间来存储指针和临时变量。
实现一个字符串的反转
思路和实现数组反转一样,时间复杂度是 O(n/2),其中 n 是字符串的长度,因为它只需要遍历字符串的一半字符。
js
function reverseString(str) {
let left = 0; // 左指针
let right = str.length - 1; // 右指针
let strArray = str.split(''); // 将字符串转换为字符数组,便于交换操作
// 交换左右指针所指向的字符,然后移动指针,直到左指针大于等于右指针
while (left < right) {
// 交换左右指针所指向的字符
let temp = strArray[left];
strArray[left] = strArray[right];
strArray[right] = temp;
// 移动指针
left++;
right--;
}
// 将字符数组转换回字符串
let reversedStr = strArray.join('');
return reversedStr;
}
// 示例用法
let str = "Hello, World!";
let reversedString = reverseString(str);
console.log(reversedString); // 输出 "!dlroW ,olleH"
链表
什么是链表
链表(Linked List)是一种数据结构,用于存储一系列元素的集合。与数组不同,链表中的元素并不在内存中按顺序放置,而是通过节点(Node)来连接。
每个节点包含两部分:
- 数据域(Data): 存储节点所持有的数据。
- 指针域(Next Pointer): 指向下一个节点的引用。
链表的最后一个节点的指针通常指向空值(null或undefined),表示链表的末尾。
链表可以分为多种类型,包括单链表、双向链表和循环链表等。以下是几种常见的链表解构示例:
1. 单链表(Singly Linked List):
单链表中,每个节点只包含一个指向下一个节点的指针。
plaintext
Node 1: [Data | Next] -> Node 2: [Data | Next] -> Node 3: [Data | null]
2. 双向链表(Doubly Linked List):
双向链表中,每个节点包含两个指针,一个指向前一个节点,一个指向后一个节点。
plaintext
Node 1: [Prev | Data | Next] <-> Node 2: [Prev | Data | Next] <-> Node 3: [Prev | Data | Next]
3. 循环链表(Circular Linked List):
循环链表中,最后一个节点的指针指向第一个节点,形成一个循环。
plaintext
Node 1: [Data | Next] -> Node 2: [Data | Next] -> Node 3: [Data | Next] -> Node 1
链表的灵活性在于,可以根据需要选择不同类型的链表,并且可以动态地插入和删除节点,而不需要像数组那样移动大量元素。链表在某些情况下比数组更具优势,例如在需要频繁插入和删除元素时。
链表的反转
迭代法
链表的反转可以通过迭代或递归两种方法来实现。以下是使用迭代方法实现链表反转的JavaScript代码:
javascript
function ListNode(val) {
this.val = val;
this.next = null;
}
function reverseLinkedList(head) {
let prev = null;
let current = head;
while (current !== null) {
let nextTemp = current.next;
current.next = prev;
prev = current;
current = nextTemp;
}
return prev;
}
// 示例用法
let list = new ListNode(1);
list.next = new ListNode(2);
list.next.next = new ListNode(3);
list.next.next.next = new ListNode(4);
let reversedList = reverseLinkedList(list);
在这个迭代方法中,我们使用两个指针prev
和current
,prev
指向已反转部分的头部,current
指向待反转部分的头部。在每一步中,我们将current.next
指向prev
,然后将prev
和current
指针分别向后移动一个节点,直到current
指针到达链表末尾。
链表反转的时间复杂度是 O(n),其中 n 是链表的长度,因为它需要遍历整个链表。在最坏的情况下,需要搜索整个链表,所以时间复杂度是线性的。空间复杂度是 O(1),因为它只需要常量级的额外空间。
递归法
递归实现链表反转是一种更为简洁但也更难理解的方法。以下是使用递归方法实现链表反转的JavaScript代码:
javascript
function ListNode(val) {
this.val = val;
this.next = null;
}
function reverseLinkedList(head) {
if (head === null || head.next === null) {
return head;
}
let newHead = reverseLinkedList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
// 示例用法
let list = new ListNode(1);
list.next = new ListNode(2);
list.next.next = new ListNode(3);
list.next.next.next = new ListNode(4);
let reversedList = reverseLinkedList(list);
在这个递归方法中,我们将链表分为两部分:第一个节点(head
)和剩余部分(head.next
)。递归函数reverseLinkedList
负责反转剩余部分,并且将head
节点的next
指针指向head
,再将head
节点的next
指针置为null
,从而实现链表的反转。最后返回新的头节点。
递归方法的时间复杂度是 O(n),其中 n 是链表的长度,因为递归函数需要访问链表的每个节点一次。递归的空间复杂度是 O(n),因为在递归过程中,系统需要维护一个递归调用栈,其深度最多为链表的长度。在最坏的情况下,递归深度为 n,所以空间复杂度为 O(n)。
检测链表中是否有环
检测链表中是否有环可以使用快慢指针(Floyd's cycle-finding algorithm)来实现。快指针每次移动两步,慢指针每次移动一步,如果链表中有环,它们最终会在某个点相遇。以下是使用快慢指针检测链表中是否有环的JavaScript代码:
javascript
function ListNode(val) {
this.val = val;
this.next = null;
}
function hasCycle(head) {
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next; // 慢指针每次移动一步
fast = fast.next.next; // 快指针每次移动两步
if (slow === fast) {
return true; // 慢指针和快指针相遇,说明有环
}
}
return false; // 遍历完整个链表没有相遇,说明无环
}
在这个算法中,如果链表中有环,快指针最终会追上慢指针,从而使得它们相遇。如果链表中没有环,快指针会到达链表的末尾(即为null
),此时退出循环,返回false
。
这个算法的时间复杂度是 O(n),其中 n 是链表的长度。快慢指针每次都会遍历整个链表,因此时间复杂度与链表的长度成正比。空间复杂度是 O(1),因为算法只使用了常数级别的额外空间。
寻找链表的中间节点
要寻找链表的中间节点,可以使用快慢指针法。快指针每次移动两步,慢指针每次移动一步,当快指针到达链表末尾时,慢指针就会处于链表的中间位置。以下是使用快慢指针法寻找链表的中间节点的JavaScript代码:
javascript
function ListNode(val) {
this.val = val;
this.next = null;
}
function findMiddle(head) {
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next; // 慢指针每次移动一步
fast = fast.next.next; // 快指针每次移动两步
}
return slow; // 返回慢指针所指的节点,即为中间节点
}
在这个算法中,当快指针到达链表的末尾时,慢指针就会处于链表的中间位置。如果链表的长度是奇数,慢指针会指向中间节点;如果链表的长度是偶数,慢指针会指向中间两个节点的左边节点。
这个算法的时间复杂度是 O(n),其中 n 是链表的长度。快慢指针每次都会遍历整个链表,因此时间复杂度与链表的长度成正比。空间复杂度是 O(1),因为算法只使用了常数级别的额外空间。
合并两个有序链表
合并两个有序链表是一个常见的链表操作。可以使用递归或迭代的方法来实现。以下是使用迭代方法合并两个有序链表的JavaScript代码:
javascript
function ListNode(val) {
this.val = val;
this.next = null;
}
function mergeTwoLists(l1, l2) {
let dummy = new ListNode(-1); // 创建一个虚拟头节点
let current = dummy; // 当前节点指向虚拟头节点
while (l1 !== null && l2 !== null) {
if (l1.val < l2.val) {
current.next = l1; // 将当前节点的下一个节点指向l1
l1 = l1.next; // l1指针向后移动
} else {
current.next = l2; // 将当前节点的下一个节点指向l2
l2 = l2.next; // l2指针向后移动
}
current = current.next; // 当前节点向后移动
}
// 处理剩余的节点
if (l1 !== null) {
current.next = l1;
} else {
current.next = l2;
}
return dummy.next; // 返回合并后的链表头节点
}
在这个算法中,使用了一个虚拟头节点 dummy
,以简化边界条件的处理。然后,使用指针 current
指向当前节点,通过比较 l1
和 l2
当前节点的值,选择较小的节点连接到 current
的下一个节点,同时将对应的指针向后移动。当其中一个链表到达末尾时,将另一个链表剩余部分直接连接到 current
的下一个节点。
这个算法的时间复杂度是 O(n + m),其中 n 和 m 分别是两个输入链表的长度。算法需要遍历两个链表中的所有节点。空间复杂度是 O(1),因为只使用了常数级别的额外空间。
删除链表中特定的节点
删除链表中的特定节点通常是指删除给定节点的操作。由于链表的特性,我们可以通过修改节点的值和指针来实现删除节点的效果。具体操作步骤如下:
- 将要删除节点的值更新为下一个节点的值。
- 将要删除节点的指针指向下一个节点的下一个节点。
以下是用 JavaScript 实现删除链表中的特定节点的代码:
javascript
function ListNode(val) {
this.val = val;
this.next = null;
}
function deleteNode(node) {
if (node === null || node.next === null) {
return false; // 无法删除最后一个节点或者空节点
}
let nextNode = node.next;
node.val = nextNode.val;
node.next = nextNode.next;
return true;
}
在这个算法中,deleteNode
函数接受一个节点作为输入参数。首先,检查输入节点是否为空或者是最后一个节点,如果是,则无法删除,直接返回 false
。然后,将输入节点的值更新为下一个节点的值,将输入节点的指针指向下一个节点的下一个节点,即可删除特定节点。
这个算法的时间复杂度是 O(1),因为删除操作只需要常数级别的操作。空间复杂度是 O(1),因为只使用了常数级别的额外空间。
树
二叉树的遍历
二叉树的遍历分为前序遍历、中序遍历和后序遍历,它们分别表示遍历根节点的顺序。以下是分别使用递归方法实现二叉树的前序、中序和后序遍历的JavaScript代码:
前序遍历(Preorder Traversal):
前序遍历的顺序是:根节点 -> 左子树 -> 右子树。
javascript
function preorderTraversal(root) {
if (root === null) {
return [];
}
return [root.val, ...preorderTraversal(root.left), ...preorderTraversal(root.right)];
}
中序遍历(Inorder Traversal):
中序遍历的顺序是:左子树 -> 根节点 -> 右子树。
javascript
function inorderTraversal(root) {
if (root === null) {
return [];
}
return [...inorderTraversal(root.left), root.val, ...inorderTraversal(root.right)];
}
后序遍历(Postorder Traversal):
后序遍历的顺序是:左子树 -> 右子树 -> 根节点。
javascript
function postorderTraversal(root) {
if (root === null) {
return [];
}
return [...postorderTraversal(root.left), ...postorderTraversal(root.right), root.val];
}
在这三种遍历方法中,使用了递归来遍历二叉树的每个节点。递归调用的次数与树的节点数成正比。因此,这三种遍历方法的时间复杂度均为 O(n),其中 n 是二叉树的节点数。
空间复杂度取决于递归调用的深度,也就是二叉树的高度。在最坏情况下,二叉树为链状结构,高度为 n,递归调用栈的空间复杂度为 O(n)。在平均情况下,二叉树的高度通常为 O(log n),因此递归调用栈的空间复杂度为 O(log n)。
二叉树的最大深度
要实现二叉树的最大深度,你可以使用深度优先搜索(DFS)或广度优先搜索(BFS)的方法。下面是一个使用深度优先搜索的JavaScript代码示例,以及复杂度解释:
javascript
class TreeNode {
constructor(val) {
this.val = val;
this.left = null;
this.right = null;
}
}
function maxDepth(root) {
if (root === null) {
return 0; // 空树的深度为0
}
const leftDepth = maxDepth(root.left);
const rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
}
这个代码示例定义了一个二叉树节点类 TreeNode
,每个节点包括一个值 val
和两个指向左子树和右子树的指针 left
和 right
。
然后,使用 maxDepth
函数来计算二叉树的最大深度。这个函数使用递归来遍历二叉树,计算左子树和右子树的深度,然后返回它们中较大的深度再加上1。
算法的时间复杂度是 O(n),其中 n 是二叉树的节点数量。因为我们对每个节点都只遍历一次,递归函数的时间复杂度是线性的。空间复杂度是 O(h),其中 h 是二叉树的高度,因为递归调用会占用系统堆栈的空间,最坏情况下,空间复杂度是二叉树的高度。
示例用法:
javascript
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
console.log(maxDepth(root)); // 输出 3,因为从根节点到叶子节点的最长路径是 1 -> 3 -> 5
这个函数可用于计算任意二叉树的最大深度。
二叉树的最小深度
要实现二叉树的最小深度,同样可以使用深度优先搜索(DFS)或广度优先搜索(BFS)的方法。下面是一个使用广度优先搜索的JavaScript代码示例,以及复杂度解释:
javascript
class TreeNode {
constructor(val) {
this.val = val;
this.left = null;
this.right = null;
}
}
function minDepth(root) {
// 如果树为空,深度为0
if (root === null) {
return 0;
}
const queue = []; // 创建一个队列来进行BFS
queue.push(root); // 将根节点放入队列
let depth = 1; // 初始化深度为1
while (queue.length > 0) {
const levelSize = queue.length; // 记录当前层级的节点数量
// 遍历当前层级的所有节点
for (let i = 0; i < levelSize; i++) {
const currentNode = queue.shift(); // 从队列中取出一个节点
// 如果当前节点是叶子节点,返回深度
if (currentNode.left === null && currentNode.right === null) {
return depth;
}
// 将当前节点的左子节点和右子节点加入队列
if (currentNode.left !== null) {
queue.push(currentNode.left);
}
if (currentNode.right !== null) {
queue.push(currentNode.right);
}
}
depth++; // 进入下一层级
}
return depth;
}
这个代码示例定义了一个二叉树节点类 TreeNode
,每个节点包括一个值 val
和两个指向左子树和右子树的指针 left
和 right
。
然后,使用 minDepth
函数来计算二叉树的最小深度。这个函数使用广度优先搜索,使用队列来层级遍历二叉树。在遍历的过程中,我们记录每一层的节点数量,一旦找到叶子节点,就返回当前深度。
算法的时间复杂度是 O(n),其中 n 是二叉树的节点数量。因为我们对每个节点都只遍历一次,队列中的节点数不会超过树的节点总数。空间复杂度是 O(m),其中 m 是二叉树的宽度,即同一层级的节点数量。
示例用法:
javascript
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
console.log(minDepth(root)); // 输出 2,因为从根节点到最近的叶子节点的最短路径是 1 -> 2
这个函数可用于计算任意二叉树的最小深度。
判断二叉树是否为平衡二叉树
什么是平衡二叉树
平衡二叉树(Balanced Binary Tree),也称为AVL树(以发明者的名字命名,Adelson-Velsky和Landis树),是一种二叉搜索树(Binary Search Tree,BST)的特殊形式。在平衡二叉树中,左子树和右子树的高度之差不会超过1,这保证了树的高度相对较小,从而提高了查找、插入和删除操作的效率。
以下是平衡二叉树的主要特点:
-
高度平衡: 在平衡二叉树中,任何节点的左子树和右子树的高度差(平衡因子)不超过1。这意味着树的高度相对较小,通常近似于 log2(n),其中 n 是树中节点的数量。
-
二叉搜索树性质: 平衡二叉树仍然是二叉搜索树,也就是说,对于每个节点,其左子树的所有节点的值都小于它的值,而右子树的所有节点的值都大于它的值。
-
支持高效的操作: 由于树的平衡性,平衡二叉树支持高效的查找、插入和删除操作。这些操作的时间复杂度通常是 O(log n),其中 n 是树中节点的数量。
-
自平衡性: 在插入或删除节点时,平衡二叉树会自动进行平衡操作,以保持树的平衡性。这通常涉及旋转操作,使树重新平衡。
平衡二叉树的目标是避免最坏情况下的操作时间复杂度变为线性,而保持操作的时间复杂度较低。这使得它在需要高效查找和修改数据的情况下非常有用,如数据库索引、排序算法等。
尽管平衡二叉树提供了很好的平衡性能,但它在某些情况下可能会过于复杂,导致一些操作的实现不够高效。因此,还有其他类型的自平衡树结构,如红黑树,它们在某些情况下更适合使用。
实现
要判断一个二叉树是否是平衡二叉树,可以使用递归的方法来检查每个节点的左右子树的高度差是否小于等于1,并且每个子树也是平衡二叉树。下面是用JavaScript实现这个判断的示例代码,带有注释以解释每个部分,并解释复杂度:
javascript
class TreeNode {
constructor(val) {
this.val = val;
this.left = null;
this.right = null;
}
}
function isBalanced(root) {
// 检查树的平衡性的辅助函数
function checkBalanced(node) {
if (node === null) {
return [true, 0]; // 空树是平衡的,高度为0
}
// 递归检查左子树
const [leftBalanced, leftHeight] = checkBalanced(node.left);
if (!leftBalanced) {
return [false, 0]; // 如果左子树不平衡,无需继续检查
}
// 递归检查右子树
const [rightBalanced, rightHeight] = checkBalanced(node.right);
if (!rightBalanced) {
return [false, 0]; // 如果右子树不平衡,无需继续检查
}
// 检查当前节点的平衡性
const isCurrentBalanced = Math.abs(leftHeight - rightHeight) <= 1;
const currentHeight = Math.max(leftHeight, rightHeight) + 1;
return [isCurrentBalanced, currentHeight];
}
const [balanced, _] = checkBalanced(root);
return balanced;
}
这个代码示例定义了一个二叉树节点类 TreeNode
,每个节点包括一个值 val
和两个指向左子树和右子树的指针 left
和 right
。
然后,使用 isBalanced
函数来判断一个二叉树是否是平衡二叉树。这个函数使用递归来检查每个节点的平衡性,同时计算每个节点的高度。
- 内部辅助函数
checkBalanced
接受一个节点作为参数,递归检查左子树和右子树的平衡性。如果左子树或右子树不平衡,立即返回false
,表示整棵树不平衡。 - 如果左子树和右子树都平衡,那么检查当前节点的平衡性,如果当前节点平衡,返回
true
和当前子树的高度。 - 在递归过程中,使用
Math.abs(leftHeight - rightHeight) <= 1
来检查当前节点的平衡性,如果高度差小于等于1,就认为是平衡的。高度计算是通过比较左子树和右子树的高度,然后取较大值,再加上1。
算法的时间复杂度是 O(n),其中 n 是二叉树的节点数量。因为我们对每个节点都只遍历一次,递归函数的时间复杂度是线性的。空间复杂度是 O(h),其中 h 是二叉树的高度,因为递归调用会占用系统堆栈的空间,最坏情况下,空间复杂度是二叉树的高度。
示例用法:
javascript
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
console.log(isBalanced(root)); // 输出 true,因为这颗树是平衡二叉树
这个函数可用于判断任意二叉树是否是平衡二叉树。
二叉树中寻找指定元素
在二叉树中寻找特定元素通常是通过深度优先搜索(DFS)或广度优先搜索(BFS)来完成的。下面是一个使用深度优先搜索的JavaScript代码示例,以及对代码和复杂度的解释:
javascript
class TreeNode {
constructor(val) {
this.val = val;
this.left = null;
this.right = null;
}
}
function findElement(root, target) {
if (root === null) {
return false; // 如果根节点为空,返回 false
}
if (root.val === target) {
return true; // 如果当前节点的值等于目标值,返回 true
}
// 递归搜索左子树和右子树
const foundInLeft = findElement(root.left, target);
const foundInRight = findElement(root.right, target);
return foundInLeft || foundInRight; // 如果在左子树或右子树中找到目标值,返回 true
}
这个代码示例定义了一个二叉树节点类 TreeNode
,每个节点包括一个值 val
和两个指向左子树和右子树的指针 left
和 right
。
然后,使用 findElement
函数来在二叉树中寻找特定元素。这个函数使用深度优先搜索,通过递归来搜索二叉树中的每个节点。
- 如果根节点为空,表示树为空,返回
false
。 - 如果当前节点的值等于目标值,表示找到目标元素,返回
true
。 - 否则,递归搜索左子树和右子树,将它们的搜索结果存储在
foundInLeft
和foundInRight
变量中。 - 最后,返回
foundInLeft
或foundInRight
的逻辑或结果,表示在左子树或右子树中找到了目标元素。
算法的时间复杂度是 O(n),其中 n 是二叉树的节点数量。因为我们对每个节点都只遍历一次,递归函数的时间复杂度是线性的。空间复杂度是 O(h),其中 h 是二叉树的高度,因为递归调用会占用系统堆栈的空间,最坏情况下,空间复杂度是二叉树的高度。
示例用法:
javascript
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
console.log(findElement(root, 3)); // 输出 true,因为树中存在值为3的节点
console.log(findElement(root, 6)); // 输出 false,因为树中不存在值为6的节点
这个函数可用于在二叉树中查找特定元素是否存在。