数组进行增删改查操作后,每一项的索引进行重新排序。
let data = [
{ id: 1, value: 'Apple' },
{ id: 2, value: 'Banana' },
{ id: 3, value: 'Cherry' },
{ id: 4, value: 'Durian' },
];
// 根据id找到要删除的元素的索引
const indexToRemove = data.findIndex(item => item.id === 3);
// 从数组中删除要删除的元素
if (indexToRemove !== -1) {
data.splice(indexToRemove, 1);
}
// 重新调整每个元素的索引
data = data.map((item, index) => ({ ...item, id: index + 1 }));
console.log(data);
// 输出结果:
// [
// { id: 1, value: 'Apple' },
// { id: 2, value: 'Banana' },
// { id: 3, value: 'Durian' }
// ]
说明:
- 我们首先找到要删除的元素的索引,这里使用了
Array.prototype.findIndex()方法,它返回一个数组元素的索引,如果没有找到,返回 -1。 - 然后我们使用
Array.prototype.splice()方法,从数组中删除要删除的元素。 - 最后,我们使用
Array.prototype.map()方法,重新调整每个元素的索引。在回调函数中,我们使用 ES6 的对象展开语法来创建一个新的对象,将原来的属性复制过来,并重新设置 id 属性为新的索引值。
注意,这里的重新排序是根据数组元素顺序的重新排列,并不是对数组元素的值进行排序。
leetcode-1-两数求和
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,并返回它们的数组下标 示例输入:
const nums = [2, 7, 11, 15];
const target = 9;
代码实现:
function twoSum(nums, target) {
const map = {};
for (let i = 0; i < nums.length; i++) {
const remaining = target - nums[i];
if (map[remaining] !== undefined) {
return [map[remaining], i];
}
map[nums[i]] = i;
}
return null;
}
const nums = [2, 7, 11, 15];
const target = 9;
const result = twoSum(nums, target);
console.log(result);
// 输出结果:
// [0,1]
说明:
- 我们可以使用哈希表来解决这个问题。我们遍历数组中的每个元素,并将元素的值作为哈希表的 key,索引作为 value。
- 对于每个元素,我们都检查哈希表中是否存在一个值等于目标值与当前元素之差的元素,如果存在,就返回这两个元素的索引。
- 如果不存在,就将当前元素添加到哈希表中,然后继续遍历数组中的下一个元素。
- 如果遍历完整个数组后仍没有找到符合条件的两个元素,就返回 null。
leetcode-11-盛最多水的容器
题目描述:
给定 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点(i, ai)。在坐标内画 n 条垂直线,使得线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
示例输入:
const heights = [1, 8, 6, 2, 5, 4, 8, 3, 7];
代码实现:
function maxArea(heights) {
let maxArea = 0;
let left = 0;
let right = heights.length - 1;
while (left < right) {
const area = Math.min(heights[left], heights[right]) * (right - left);
maxArea = Math.max(maxArea, area);
if (heights[left] < heights[right]) {
left++;
} else {
right--;
}
}
return maxArea;
}
const heights = [1, 8, 6, 2, 5, 4, 8, 3, 7];
const result = maxArea(heights);
console.log(result);
说明:
- 我们使用双指针法来解决这个问题。我们定义两个指针,一个指向数组的开头,一个指向数组的结尾。
- 此时两根线段的距离肯定是最大的,接下来我们要缩小宽度。
- 因为短的线段的高度会影响面积的大小,所以我们每次将较短的线段向中间移动,以期望找到更高的线段。
- 我们计算当前面积,并将其与 maxArea 进行比较,每次更新最大面积。
- 当左指针小于右指针时,重复上述过程,直到左右指针相遇。
leetcode-15-三数之和
题目描述:
给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素,使得它们的和为零。找出所有满足条件且不重复的三元组。
代码实现:
function threeSum(nums) {
nums.sort((a, b) => a - b);
const results = [];
for (let i = 0; i < nums.length; i++) {
if (i > 0 && nums[i] === nums[i - 1]) {
continue;
}
let j = i + 1;
let k = nums.length - 1;
while (j < k) {
const sum = nums[i] + nums[j] + nums[k];
if (sum === 0) {
results.push([nums[i], nums[j], nums[k]]);
while (j < k && nums[j] === nums[j + 1]) {
j++;
}
while (j < k && nums[k] === nums[k - 1]) {
k--;
}
j++;
k--;
} else if (sum < 0) {
j++;
} else {
k--;
}
}
}
return results;
}
const nums = [-1, 0, 1, 2, -1, -4];
const result = threeSum(nums);
console.log(result);
说明:
- 我们先将数组排序,这样可以方便我们的双指针法。
- 我们固定一个数
nums[i],然后将问题转化为在剩余的数组中查找两个数之和为-nums[i]的问题。 - 我们定义两个指针
j和k,分别指向i + 1和数组的最后一个元素。 - 如果
nums[i] + nums[j] + nums[k] === 0,则将[nums[i], nums[j], nums[k]]添加到结果数组中。 - 然后,我们对指针进行操作,将重复的数字跳过。
- 如果
nums[i] + nums[j] + nums[k] < 0,则将j向右移动。 - 如果
nums[i] + nums[j] + nums[k] > 0,则将k向左移动。 - 当
i遍历整个数组时,算法结束,返回结果数组
leetcode-20-有效的括号
题目描述:
给定一个只包含字符 (、)、{、}、[、] 的字符串 s,判断该字符串是否有效。
解法:首先,一个有效的括号字符串必须满足两个条件:
- 左括号和右括号必须以相同的顺序匹配。
- 当出现右括号时,它必须对应最近的左括号。
利用栈(stack)这种数据结构可以很容易地解决这个问题。我们遍历输入的字符串,如果遇到左括号就将其入栈,如果遇到右括号就将栈中最顶上的元素出栈并与当前右括号进行匹配。如果栈顶元素与当前右括号不匹配,或者栈为空,那么就说明字符串无效。最后,如果遍历完字符串后栈为空,则说明该字符串是有效的。 下面是代码实现:
var isValid = function (s) {
let stack = [] // 长度和数租长度有关 可能会一直增加
let obj = { '(': ')', '[': ']', '{': '}' }
for (let i = 0; i < s.length; i++) {
const ele = s[i]
if (ele in obj) {
stack.push(ele)
} else {
if (ele != obj[stack.pop()]) {
// 不匹配
return false
}
}
}
// 栈是不是空的 return false
// 空的 return true
return !stack.length
}
let s = '([)]'
isValid(s)
leetcode-26-删除排序数组中的重复项
题目描述:
给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
示例 1:
输入: [1,1,2]
输出: 2
解释: 数组中的元素分别为 1、1 和 2。你的函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1、2。不需要考虑数组中超出新长度后面的元素。
示例 2:
输入: [0,0,1,1,1,2,2,3,3,4]
输出: 5
解释: 数组中的元素分别为 0、1、2、3、4 和 未定义。你的函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0、1、2、3、4。不需要考虑数组中超出新长度后面的元素。
解法:
由于给定的是有序数组,可以利用双指针方法对数组进行原地操作。具体而言,我们可以用一个快指针 i 和一个慢指针 j 遍历整个数组,其中快指针 i 表示当前遍历的元素位置,慢指针 j 表示下一个要覆盖元素的位置,而遍历的方式即每次判断 nums[i] 是否等于 nums[j] 。
时间复杂度:O(n),其中 n 是数组的长度。
空间复杂度:O(1),只需要常数的额外空间。
下面是代码实现:
var removeDuplicates = function (nums) {
const n = nums.length
if (n === 0) {
return 0
}
let j = 0
for (let i = 1; i < n; i++) {
if (nums[i] !== nums[j]) {
j++
nums[j] = nums[i]
}
console.log('~~~', i, j, nums)
}
return j + 1
}
let nuuu = [1, 1, 2, 2, 4]
console.log(removeDuplicates(nuuu))
// i = 1 j = 0 nums[1] = 1 nums[0] = 1
// i = 2 j = 0 nums[2] = 2 nums[0] = 1 j++ j= 1 nums[1] = 2 2 1 [ 1, 2, 2, 2, 4 ]
// i = 3 j = 1 nums[3] = 2 nums[1] = 2 3 1 [ 1, 2, 2, 2, 4 ]
// i = 4 j = 1 nums[4] = 4 nums[1] = 2 j++ j= 2 nums[2] = 4 4 2 [ 1, 2, 4, 2, 4 ]
这里需要注意的是,由于题目中要求函数返回的是删除重复元素后数组的新长度,因此这里返回的是慢指针 j 加一的结果。
补充: 双指针方法是一种常见的解决数组或链表中问题的方法。这种方法通常使用两个指针,一个慢指针和一个快指针,来遍历数组或链表。通过调整指针的移动方式和位置,来解决具体问题。
具体而言,慢指针与快指针都从数组或链表的起点开始,然后快指针以比慢指针快的速度遍历数组或链表,每次移动一个元素,慢指针以比快指针慢的速度遍历,每次移动一个元素或跳过一些元素。根据问题的具体要求,指针的移动方式和位置各不相同。
leetcode-21-合并两个有序链表
题目描述:
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = []
输出:[]
解法:
针对本题,我们可以考虑利用双指针法和循环实现。具体而言,我们可以分别设两个指针指向两个链表中的起始位置,然后逐个比较两个指针所指的节点,将值较小的节点接在新链表的末尾,并移动指针,继续比较。最终将较长的链表中剩余的部分接在新链表的末尾,得到合并后的链表。
时间复杂度:O(m+n),其中 m 和 n 分别是两个链表的长度。
空间复杂度:O(1),只需要常数级别的额外空间。
这里我们假设链表节点的定义如下,可以在代码中简单定义一下:
function ListNode(val) {
this.val = val === undefined ? 0 : val;
this.next = null;
}
var mergeTwoLists = function(l1, l2) {
let dummy = new ListNode();
let tail = dummy;
while (l1 !== null && l2 !== null) {
if (l1.val < l2.val) {
tail.next = l1;
l1 = l1.next;
} else {
tail.next = l2;
l2 = l2.next;
}
tail = tail.next;
}
tail.next = l1 !== null ? l1 : l2;
return dummy.next;
};
需要注意的是,在新链表的开头先插入一个虚拟节点,可以简化链表合并的过程,避免空指针的问题。最后返回虚拟节点之后的链表即可。
补充链表知识点:
链表是通过每个节点中存储一个指向下一个节点的指针来实现的。在 JavaScript 中,这个指针通常用一个称为 next 的属性来表示。因此,在链表中,每个节点都必须包含一个值和一个指向下一个节点的 next 指针。
链表中每个节点的定义如下:
function ListNode(val) {
this.val = val;
this.next = null;
}
这里的 val 属性表示节点存储的值,next 表示指向下一个节点的指针。也可以将 val 属性定义为任何类型的值,例如数字、字符串、对象等。
在JavaScript中,通过创建节点对象,再通过让节点对象之间相互连接的方式来构建链表。例如,以下代码将创建一个包含三个节点的链表:
let node1 = new ListNode(1);
let node2 = new ListNode(2);
let node3 = new ListNode(3);
node1.next = node2;
node2.next = node3;
将上述代码解释一下,首先我们通过 new 关键字创建了三个节点对象 node1、node2、node3,并且为它们的 val 属性分别赋值为 1、2、3,由于它们的 next 属性默认为 null(即没有指向任何节点),因此我们需要手动将它们按照顺序连接起来,即 node1 的 next 属性指向 node2,node2 的 next 属性指向 node3,这样就完成了链表的构建。
值得注意的是,JavaScript 中的链表一般不是固定长度的,可以随时对链表进行节点的添加、删除和修改等操作,而这些操作都不需要对整个链表进行重构或移动节点,只需要修改相应节点的 next 指针即可。这是链表相对于数组的优势之一。
leetcode-27-移除元素
题目描述:
给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
输入: nums = [3,2,2,3], val = 3
输出: 2, nums = [2,2]
解释: 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。
示例 2:
输入: nums = [0,1,2,2,3,0,4,2], val = 2
输出: 5, nums = [0,1,3,0,4]
解释: 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0、1、3、0 和 4。你不需要考虑数组中超出新长度后面的元素。
解法:
题目要求我们在数组中原地移除所有元素值等于 val 的元素,由于我们不需要考虑数组中超出新长度后面的元素,可以采用双指针法,使用两个指针分别指向数组的起始位置和末尾位置,通过调整指针的移动方式和位置,来原地移除指定元素。
具体而言,我们可以用一个慢指针 i 和一个快指针 j 遍历整个数组,其中快指针 j 表示当前遍历的元素位置,慢指针 i 表示下一个要覆盖元素的位置。如果 nums[j] 等于 val,则将 j 向后移动一位,跳过这个元素;否则,将 nums[j] 赋值给 nums[i],并将 i 和 j 都向后移动一位,继续遍历。
时间复杂度:O(n),其中 n 是数组的长度。
空间复杂度:O(1),只需要常数的额外空间。
下面是 JavaScript 的代码实现:
var removeElement = function (nums, val) {
const n = nums.length
let i = 0
for (let j = 0; j < n; j++) {
if (nums[j] !== val) {
nums[i] = nums[j]
i++
}
}
return i
}
let nums = [3, 2, 2, 3, 4]
let val = 3
removeElement(nums, val)
// i = 0 j = 0 不走判断
// j = 1 nums[1] = 2 走判断 nums[0] = 2 i++ i = 1
// j = 2 nums[2] = 2 走判断 nums[1] = 2 i++ i = 2
// j = 3 nums[3] = 3 不走判断 return i = 2
// j = 4 nums[4] = 4 走判断 nums[2] = 4 i++ i = 3
这里需要注意,这个方法返回的是修改后数组的新长度,也就是慢指针 i 的位置。最后,这个位置之后的数组元素可以直接忽略,因为这些元素都是等于 val 的。
leetcode-35-搜索插入位置
题目描述:
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,则返回它可以被按顺序插入的位置。
假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5
输出: 2
示例 2:
输入: [1,3,5,6], 2
输出: 1
示例 3:
输入: [1,3,5,6], 7
输出: 4
示例 4:
输入: [1,3,5,6], 0
输出: 0
题目要求我们在一个升序数组中查找给定的目标值,并返回其索引。如果目标值不存在于数组中,则要返回它可以被插入的位置。
一种直接的思路是顺序遍历数组,找到第一个大于等于目标值的位置,然后将目标值插入该位置。但是这种方法的时间复杂度是 O(n),无法满足题目的要求。
另一种更优秀的方法是二分查找。由于数组是有序的,我们首先通过二分查找找到目标值应该所在的区间。如果目标值恰好在数组中,则返回它的索引;否则,返回它应该插入的位置,这个位置即为右边指针所指向的位置。
时间复杂度:O(logn),其中n是数组的长度。
空间复杂度:O(1),只需要常数级别的额外空间。
下面是 JavaScript 的代码实现:
var searchInsert = function(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
let mid = Math.floor((left + right) / 2);
if (nums[mid] === target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
};
let nums = [1, 2, 3, 4, 5, 6, 7]
let val = 5
searchInsert(nums, val)
// ~~~ 0 6 3
// ~~~~~ 4 6
// ~~~ 4 6 5
// ~~~~~ 4 4
// ~~~ 4 4 4 return 4
let nums = [1, 2, 3, 4, 5, 6, 7]
let val = 8
searchInsert(nums, val)
// ~~~ 0 6 3
// ~~~~~ 4 6
// ~~~ 4 6 5
// ~~~~~ 6 6
// ~~~ 6 6 6
// ~~~~~ 7 6 return 7
这里我们定义了左右两个指针 left 和 right,在每次循环时计算中间位置 mid,然后根据比较值 nums[mid] 和目标 target 的大小关系,调整左右指针的位置,直到找到目标值或确定它的插入位置。
leetcode-58-最后一个单词的长度
题目描述:
给定一个仅包含大小写字母和空格 ’ ’ 的字符串 s,返回其最后一个单词的长度。如果字符串从左向右滚动显示,那么最后一个单词就是最后出现的单词。如果不存在最后一个单词,请返回 0 。
说明:一个单词是指由字母组成,但不包含任何空格的字符串。
示例 1:
输入: “Hello World”
输出: 5
示例 2:
输入: " "
输出: 0
针对本题,我们可以使用 split 方法将字符串转换为单词数组,然后取数组中最后一个元素的长度即可。
但是由于题目要求我们避免使用数组和其他额外空间,因此可以考虑从字符串的末尾开始向左遍历,跳过末尾的空格,找到第一个空格之后的一个非空格字符,记录其位置并返回。
时间复杂度:O(n),其中 n 是字符串的长度。
空间复杂度:O(1),只需要常数级别的额外空间。
下面是 JavaScript 的代码实现:
/**
* @param {string} s
* @return {number}
*/
var lengthOfLastWord = function(s) {
let len = 0;
let tail = s.length - 1;
// 去掉末尾的空格
while (tail >= 0 && s[tail] === ' ') {
tail--;
}
// 记录最后一个单词的长度
while (tail >= 0 && s[tail] !== ' ') {
len++;
tail--;
}
return len;
};
这里我们定义了一个指针 i,开始时指向字符串的末尾。在第一个 while 循环中,遍历字符串末尾的空格,找到第一个非空格字符的位置。在第二个 while 循环中,计算最后一个单词的长度。最终返回 len 即可。
我们也可以使用正则表达式,将字符串中的末尾空格和最后一个单词分离出来,然后计算最后一个单词的长度。这样,整个问题就可以使用一行代码来解决。
代码如下:
var lengthOfLastWord = function(s) {
return s.trim().split(' ').pop().length;
};
这里我们使用了字符串自带的 trim() 方法,它可以将字符串两端的空格去除。然后使用 split() 方法将字符串以空格分隔成一个单词数组,最后使用 pop() 方法取出数组中最后一个单词,计算其长度并返回结果。
需要注意的是,如果字符串为空或只包含空格,那么 pop() 方法返回的是空字符串,因此需要将这种情况单独处理一下,可以加上一个简单的判断条件:
var lengthOfLastWord = function(s) {
s = s.trim();
if (s === '') {
return 0;
}
return s.split(' ').pop().length;
};
leetcode-66-加一
题目描述:
给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。你可以假设除了整数 0 之外,这个整数不会以零开头。
示例 1:
输入: [1,2,3]
输出: [1,2,4]
解释: 输入数组表示数字 123。
示例 2:
输入: [4,3,2,1]
输出: [4,3,2,2]
解释: 输入数组表示数字 4321。
题目要求我们给一个数组表示的非负整数加一,从数组的最后一位开始向前遍历,如果当前位的值小于九,则直接加一返回;否则将当前位的值设为零,并向前继续递归,直到遍历完所有的位数。如果遍历完了所有的位数,仍然需要在数组的最前面加上一个进位为 1 的元素。
时间复杂度:O(n),其中 n 是数组的长度。
空间复杂度:O(1),只需要常数级别的额外空间。
下面是 JavaScript 的代码实现:
var plusOne = function (digits) {
const n = digits.length
for (let i = n - 1; i >= 0; i--) {
console.log('~~~', i)
if (digits[i] < 9) {
digits[i]++
return digits // 小于9从这抛出
} else {
digits[i] = 0
}
console.log('~~~', digits)
}
digits.unshift(1) // 只有全为9的时候才会走这里,其他情况均是从上面抛出
return digits
}
let arr = [4, 3, 2, 9, 9]
let arr1 = [9, 9, 9, 9, 9]
console.log(plusOne(arr))
输出:[ 4, 3, 3, 0, 0 ]
console.log(plusOne(arr1))
输出:[ 1, 0, 0, 0, 0, 0 ]
这里我们定义了一个指针 i,从数组的最后一位开始向前遍历。如果当前位的值小于九,则直接将其加一,并返回结果;否则,将当前位的值设为零,并向前继续递归,直到遍历完所有的位数为止。
最后,如果数组的所有位都是 9,则需要在数组的最前面加上一个元素 1,即可得到加一后的结果。
leetcode-88-合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 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] ,其中斜体加粗标注的为 nums1 中的元素。
以下是求解方法:
/**
* @param {number[]} nums1
* @param {number} m
* @param {number[]} nums2
* @param {number} n
* @return {void} Do not return anything, modify nums1 in-place instead.
*/
var merge = function(nums1, m, nums2, n) {
let i = m - 1, j = n - 1, k = m + n - 1;
while (i >= 0 && j >= 0) {
if (nums1[i] < nums2[j]) {
nums1[k] = nums2[j];
j--;
} else {
nums1[k] = nums1[i];
i--;
}
k--;
}
while (j >= 0) {
nums1[k] = nums2[j];
j--;
k--;
}
};
let nums1 = [1, 2, 3],
nums2 = [2, 5, 6];
merge(nums1, 3, nums2, 3);
// 3 6 nums1[5] = 6
// 3 5 nums1[4] = 5
// 3 2 nums1[3] = 3
// 2 2 nums1[2] = 2
// 1 2 nums1[1] = 2
// 1 0 不走循环了
这个算法的基本思路是倒序遍历两个数组,依次比较两个指针所指向的元素大小,然后将较大的元素放在 nums1 的末尾。如果 nums1 中的元素全部遍历完了,而 nums2 中还有元素没有放入 nums1 中,就继续将 nums2 中的元素放入 nums1 中。
leetcode第94题-二叉树的中序遍历
给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。
示例 1:
输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
以下是求解方法:
/**
* @param {TreeNode} root
* @return {number[]}
*/
var inorderTraversal = function(root) {
const res = []; // 存放中序遍历结果的数组
inorder(root, res);
return res;
};
function inorder(root, res) {
if (root !== null) {
inorder(root.left, res);
res.push(root.val);
inorder(root.right, res);
}
}
这个算法采用递归方式实现二叉树的中序遍历,即按照左子树->根节点->右子树的顺序遍历二叉树,并将遍历结果存放在数组中返回。
中序遍历是二叉树遍历中最常见的遍历方式之一,它可以保证对于二叉搜索树的中序遍历结果是一个有序的数组。因此,求解二叉树的中序遍历可以用来对二叉搜索树进行排序,并且还可以获取其中的最小值和最大值等信息。
补充二叉树知识点:
(1)二叉树的前序遍历
二叉树的前序遍历是指按照如下方式遍历二叉树:
- 首先遍历根节点;
- 然后遍历左子树;
- 最后遍历右子树。
具体实现方式可以用递归或迭代的方式实现。在递归中,先访问根节点,然后递归遍历左子树和右子树。在迭代中,可以利用栈来模拟递归过程。
二叉树的前序遍历是二叉树遍历中另一个常见的遍历方式。它可以用来建立二叉树,即先访问根节点,然后递归遍历左右子树,可以得到一棵树的唯一结构信息,而不需要知道该树的中序遍历或后序遍历。此外,前序遍历还可以用来复制一棵二叉树。
(2)二叉树的中序遍历
二叉树的中序遍历是指按照如下方式遍历二叉树:
- 首先遍历左子树;
- 然后遍历根节点;
- 最后遍历右子树。
具体实现方式可以用递归或迭代的方式实现。在递归中,先递归遍历左子树,然后访问根节点,最后递归遍历右子树。在迭代中,可以利用栈来模拟递归过程。
二叉树的中序遍历是二叉树遍历中最常见的遍历方式之一,它可以保证对于二叉搜索树的中序遍历结果是一个有序的数组。因此,求解二叉树的中序遍历可以用来对二叉搜索树进行排序,并且还可以获取其中的最小值和最大值等信息。
(3)二叉树的后序遍历
二叉树的后序遍历是指按照如下方式遍历二叉树:
- 首先遍历左子树;
- 然后遍历右子树;
- 最后遍历根节点。
具体实现方式可以用递归或迭代的方式实现。在递归中,先递归遍历左子树和右子树,最后访问根节点。在迭代中,可以利用栈来模拟递归过程。
二叉树的后序遍历是二叉树遍历中另一个常见的遍历方式。它可以用来确定一棵二叉树的唯一结构信息,并且可以将二叉搜索树转化成一个数组。此外,后序遍历还可以用于回溯算法,在 DFS 中确定当前状态和其子状态的执行顺序。
leetcode第100题-相同的树
以下是 LeetCode 第100题:相同的树,使用 JavaScript 语言的题解:
题目描述:
给定两个二叉树,编写一个函数来检验它们是否相同。
如果两个二叉树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
给定的两个树:
1 1
/ \ / \
2 3 2 3
[1,2,3], [1,2,3]
输出: true
给定的两个树:
1 1
/ \
2 2
[1,2], [1,null,2]
输出: false
给定的两个树:
1 1
/ \ / \
2 1 1 2
[1,2,1], [1,1,2]
输出: false
如果两棵树都为空,则它们是相同的,返回 true; 如果两棵树中有一棵树为空,则它们不相同,返回 false; 如果两棵树的根节点的值不相等,则它们不相同,返回 false; 否则就递归比较它们的左子树和右子树,只有当两棵树的左右子树都相等才说明它们相同。
代码实现:
/*
@param {TreeNode} p
@param {TreeNode} q
@return {boolean}
*/
var isSameTree = function(p, q) {
if(p === null && q === null) { // 两棵树都为空
return true;
}
if(p === null || q === null) { // 两棵树中有一个为空
return false;
}
if(p.val !== q.val) { // 根节点的值不相等
return false;
}
// 递归比较左子树和右子树
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
};
时间复杂度:O(n),其中 n 是树的节点个数,因为需要遍历两棵树的所有节点。
空间复杂度:O(h),其中 h 是树的高度,这里使用的是递归,最坏情况下栈的空间复杂度为树的高度。
leetcode第101题-对称二叉树
以下是 LeetCode 第101题:对称二叉树,使用 JavaScript 语言的题解:
题目描述:
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
一个是左子树带一个右子树,一个是右子树带一个右子树
1
/ \
2 2
\ \
3 3
解题思路:
可以使用递归的方式来判断二叉树是否是对称的。只有当左右子树都相等时才是对称的,根据这个特点可以写出如下的解法:
首先判断树是否为空,若为空则是对称树;如果树不为空,则需要判断左右子树是否相等,只有当左右子树相等时,才判断左子树的左子树和右子树的右子树是否相等,以及左子树的右子树和右子树的左子树是否相等,否则返回false。
代码实现:
/* *
@param {TreeNode} root
@return {boolean}
*/
var isSymmetric = function(root) {
if(root === null) { // 空树也是对称二叉树
return true;
}
return checkSymmetric(root.left, root.right);
};
// 递归判断左右子树是否相等
function checkSymmetric(left, right) {
if(left === null && right === null) {
return true; // 两个子树为空,相等
}
if(left === null || right === null) {
return false; // 两个子树不全为空
}
if(left.val !== right.val) {
return false; // 两个子树的值不相等
}
// 递归比较左子树的左子树和右子树的右子树,以及左子树的右子树和右子树的左子树
return checkSymmetric(left.left, right.right) && checkSymmetric(left.right, right.left);
}
时间复杂度:O(n),其中 n 是树的节点个数,因为需要遍历整个数判断是否对称。
空间复杂度:O(h),其中 h 是树的高度,因为使用的是递归,最坏情况下栈的空间复杂度为树的高度。
leetcode第136题-只出现一次的数字
给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1 :
输入:nums = [2,2,1]
输出:1
示例 2 :
输入:nums = [4,1,2,1,2]
输出:4
示例 3 :
输入:nums = [1]
输出:1
解题思路:
可以使用哈希表来解决该问题。遍历数组,对于每个数,使用哈希表存储该数出现的次数。最后遍历哈希表,找出出现次数为1的数,这些数就是只出现一次的数字。
/**
* @param {number[]} nums
* @return {number[]}
*/
var findUniqueNumbers = function(nums) {
let map = new Map();
for(let i = 0; i < nums.length; i++) { // 遍历数组,统计每个数字出现的次数
if(map.has(nums[i])) {
map.set(nums[i], map.get(nums[i]) + 1);
} else {
map.set(nums[i], 1);
}
}
let res = [];
for(let [num, count] of map) { // 遍历哈希表,找出只出现一次的数字
if(count === 1) {
res.push(num);
}
}
return res;
};
// 测试
console.log(findUniqueNumbers([4, 5, 5, 6, 6, 7, 8, 9])); // 输出 [4, 7, 8, 9]
时间复杂度:O(n),其中 n 是数组长度,因为需要遍历整个数组。
空间复杂度:O(n),需要使用哈希表存储所有不同的数。 该算法是时间复杂度和空间复杂度都比较低的解题方法,且不需要额外空间,在面试过程中也比较常见,需要掌握。
补充Map()知识点:
ES6中Map是一种新的数据类型,用于存储键值对,其中键和值可以是任意类型的数据,而不仅限于字符串和数值。
Map类似于 Object,但Object中键值对的键必须是字符串、Symbol或者数值,而Map中键可以是任何类型的数据,例如是一个Object、Function等类型。
Map对象是一个集合,由一个或多个键值对组成,可以跟踪键及其对应的值,可以快速地查找特定键对应的值,也可以通过键、值或键值对本身来进行迭代。
Map常用方法:
- set(key, value):向Map对象中添加键值对,并返回该Map对象。
- get(key):获取Map对象中指定键对应的值,如果键不存在则返回undefined。
- has(key):判断Map对象中是否存在指定的键,并返回一个布尔值。
- delete(key):删除Map对象中指定键对应的键值对,并返回一个布尔值,表示是否删除成功。
- clear():删除Map对象中的所有键值对。
代码示例如下:
// 定义一个空的Map对象
let map = new Map();
// 添加键值对
map.set("name", "张三");
map.set(1, "aaa");
map.set({"age": 18}, "成年");
// 获取某个键值对的值
console.log(map.get("name"));
// 删除某个键及对应的值
map.delete(1);
console.log(map.has(1));
// 清空Map对象
map.clear();
// 迭代Map对象
let keys = map.keys(); // 返回键的迭代器
let values = map.values(); // 返回值的迭代器
let entries = map.entries(); // 返回键值对的迭代器
for(let key of keys) {
console.log(key);
}
for(let value of values) {
console.log(value);
}
for(let entry of entries) {
console.log(entry);
}
Map对象的迭代方法可以使用for…of循环遍历,也可以使用forEach方法遍历,与Array中的forEach方法类似,forEach方法接受三个参数:callback函数、迭代次数、迭代时this的值。
leetcode第179题-最大数
题目描述:
给定一组非负整数,重新排列它们的顺序使之组成一个最大的整数。
示例1:
输入: [10,2]
输出: 210
示例2:
输入: [3,30,34,5,9]
输出: 9534330
解题思路:
题目要求将给定的一组非负整数重新排列,使其组成一个最大的整数。这个最大的整数肯定是这个数列的所有元素按照某种排列方式组成的。我们可以先将这些数用字符串形式保存,然后按照一定的规则进行排序,最后把字符串形式的结果转换成整数输出即可。
排序规则:对两个字符串进行比较,假如将它们拼接起来后,谁组成的整数更大,这个字符串就大。
比如3,30,它们拼接成的结果是330和303,这里直接比较是不准确的,所以我们要自己定义一种比较方式:
从高位到低位的顺序,依次比较两个数字对应位上的大小,假如在某一位上数字x对应的位大于数字y对应的位,则x大于y,否则y大于x。
如果两个字符串在某一位上数字大小相同,则比较下一位。
注意点:
在比较两个数字的对应位大小时,为了避免出现位数不够的情况,需要对短的数字进行循环取余和循环取模,使其长度和长的数字相等。
特别地,如果结果的第一个字符为0,则说明结果是0,直接返回即可。
算法流程:
- 将数组中的所有数字转换为字符串,并将它们压入一个新数组中。
- 对新数组进行排序,排序的规则是对任意两个字符串进行组合,谁组成的整数更大,这个字符串就大。
- 将排序后的字符串数组按顺序进行拼接,得到一个数形式的字符串。
- 将这个字符串转换成整数返回,注意特判第一个字符为0的情况。
代码实现:
/**
* @param {number[]} nums
* @return {string}
*/
var largestNumber = function(nums) {
let strArr = nums.map(num => num.toString()); // 将整数转换为字符串保存到一个新数组中
strArr.sort((a, b) => {
let sa = a + b; // a拼接b
let sb = b + a; // b拼接a
for(let i = 0; i < sa.length; i++) {
// 如果a在某一位上的数字大于b该位上的数字,则a大于b
// 如果a在某一位上的数字小于b该位上的数字,则b大于a
if(sa.charCodeAt(i) > sb.charCodeAt(i)) {
return -1; // -1表示a排在b的前面
} else if(sa.charCodeAt(i) < sb.charCodeAt(i)) {
return 1; // 1表示b排在a的前面
}
}
return 0; // 如果所有位的数字都相同,则返回0
});
let res = strArr.join(''); // 将字符串数组按顺序拼接成一个数形式的字符串
if(res.charAt(0) === '0') { // 如果结果的第一个字符是0,说明结果是0
return '0';
} else {
return res; // 否则直接返回结果
}
};
// 测试
console.log(largestNumber([10, 2])); // 输出 "210"
console.log(largestNumber([3, 30, 34, 5, 9])); // 输出 "9534330"
时间复杂度:O(nlogn),其中 n 是数组长度,因为需要对整个数组进行排序。
空间复杂度:O(n),需要使用一个额外的数组保存所有数字的字符串形式。
leetcode第203题-移除链表元素
题目描述:
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回新的头节点。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1
输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7
输出:[]
解题思路:
本题要求删除链表中所有节点值为 val 的节点。我们可以使用双指针法,遍历整个链表,并记录当前节点以及当前节点的前一个节点。如果当前节点的值等于 val,则将当前节点从链表中删除,即让当前节点的前一个节点指向当前节点的下一个节点。需要注意的是,如果头节点的值等于 val,我们需要特殊处理,因为此时头节点也需要被删除。
算法流程:
- 创建一个哑节点 dummy,将其 next 指针指向头节点。
- 创建两个指针 pre 和 cur,分别指向哑节点和头节点。
- 遍历整个链表,如果当前节点的值等于 val,则将当前节点从链表中删除,即让当前节点的前一个节点指向当前节点的下一个节点;否则,将 pre 指针和 cur 指针都向前移动一个节点。
- 返回哑节点 dummy 的 next 指针,即新的头节点。
代码实现:
const ListNode = (val, next) => {
return {
val: val === undefined ? 0 : val,
next: next === undefined ? null : next,
};
};
var removeElements = (head, val) => {
let dummy = ListNode(0, head); // 创建一个哑节点,将其 next 指针指向头节点
let pre = dummy,
cur = head; // 创建两个指针 pre 和 cur,分别指向哑节点和头节点
while (cur !== null) {
// 遍历整个链表
if (cur.val === val) {
// 如果当前节点的值等于 val,则将当前节点从链表中删除
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next; // 将指针都向前移动一个节点
}
return dummy.next; // 返回哑节点的 next 指针,即新的头节点
};
// 测试
let head = {
val: 1,
next: {
val: 2,
next: {
val: 6,
next: {
val: 3,
next: {
val: 4,
next: {
val: 5,
next: {
val: 6,
next: null,
},
},
},
},
},
},
};
let val = 6;
console.log("123123", removeElements(head, val)); // 输出 [1,2,3,4,5]
时间复杂度:O(n),其中 n 是链表长度。需要遍历整个链表一次。
空间复杂度:O(1)。只使用了常数个额外的指针。
leetcode第206题-反转链表
题目描述:
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶:你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
解题思路:
链表的反转问题可以采用迭代和递归两种方法来解决。对于迭代方法,我们可以创建一个指针,不断往前移动,并在移动过程中翻转每个节点的指针方向。对于递归方法,我们可以先递归到链表的尾部,然后从尾部开始不断地翻转指针方向,直到整个链表都被翻转。
解题代码:
- 迭代法
var reverseList = function(head) {
let prev = null;
let curr = head;
while (curr !== null) {
const next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
};
- 递归法
var reverseList = function(head) {
if (head === null || head.next === null) {
return head;
}
const p = reverseList(head.next);
head.next.next = head;
head.next = null;
return p;
};
leetcode第226题-反转二叉树
题目描述:翻转一棵二叉树。
输入:
4
/ \
2 7
/ \ / \
1 3 6 9
输出:
4
/ \
7 2
/ \ / \
9 6 3 1
解题思路:
可以采用递归或迭代的方式实现二叉树的翻转。递归的方式比较简单,可以先翻转左子树,再翻转右子树,最后交换左右子树的位置即可。迭代的方式可以使用一个队列来实现,先加入根节点,然后不断地取出队列中的节点,翻转其左右子树,再加入左右子树,直到队列为空。
解题代码:
- 递归法
var invertTree = function(root) {
if (root === null) {
return null;
}
invertTree(root.left);
invertTree(root.right);
const temp = root.left;
root.left = root.right;
root.right = temp;
return root;
};
2. 迭代法
var invertTree = function(root) {
if (root === null) {
return null;
}
const queue = [root];
while (queue.length > 0) {
const node = queue.shift(); // 删除开头元素
const temp = node.left;
node.left = node.right;
node.right = temp;
if (node.left) {
queue.push(node.left);
}
if (node.right) {
queue.push(node.right);
}
}
return root;
};