数组刷题记录
Date:
数组刷题记录
66. 加一
- 描述:一个数组,表示一个大整数,加一,返回加一后的数组
- 实现:
- 取个位,若<9,个位加一返回
- 考虑进位,可以提前unshift一个0,倒着遍历,实现进位
- 最后返回的时候有必要的话把第一个0去掉
724. 寻找数组的中心下标
- 描述:求一个下标I,使得[0-I-1]的和等于[I+1-n]的和
- 实现:
- 初始化I为0,left=0,right = sum - nums[0]
- 向右遍历,left += nums[I],right -= nums[I+1]
189. 轮转数组
- 描述:把一个数组,向右轮转k个位置,有点像环形链表的感觉
- 实现:
- 整个数组翻转,后面的就到前面了
- 翻转前k个,轮转的K个就对了
- 翻转后n-k个
- 注意:
- 当k>n时,k=k%n
48. 旋转图像
- 描述:把一个N*N矩阵,顺时针旋转90度
- 实现:
- 元素(i,j)的旋转规律为
(i,j) -> (j,n-i-1)
- 在一圈的元素中,遍历一行的n-1个元素,然后进行循环交换 a->b->c->d->a
(i,j) -> (j,n-i-1)
, 同样的, 通过代入可得,(j,n-i-1) -> (n-i-1,n-j-1)
….
- 元素(i,j)的旋转规律为
- 注意:
- 一层一层的向内旋转,那么外层遍历层数为
n/2 | 0
- 每一层遍历的元素为
level, n-level-1
- 一层一层的向内旋转,那么外层遍历层数为
54. 螺旋矩阵
- 描述:给定一个m*n的矩阵,按照顺时针螺旋顺序遍历,返回矩阵中的所有元素
- 实现:
- 一层层遍历,外层循环遍历层,层数 =
min(m,n) / 2 | 0
- 循环体内部分别处理四条边,注意左闭右开
- top: 从左到右遍历列
[layer, n-layer-1)
- right: 从上到下遍历行
[layer, m-layer-1)
- bottom: 从右到左遍历列
[n-layer-1, layer)
- left: 从下到上遍历行
[m-layer-1, layer)
- 一层层遍历,外层循环遍历层,层数 =
- 注意处理奇数边,容易遗漏
- if
min(m,n) % 2 === 1
或layerNum === 0
- 可能是剩下单独一行
[layer, n-layer)
- 可能是剩下单独一列
[layer, m-layer)
- if
498. 对角线遍历
- 描述:给定一个m*n的矩阵,按照对角线顺序遍历,返回矩阵中的所有元素
- 实现:
- 不要想着按层或者按条去遍历,而是从
[0,0]
开始,走一步看一步 - 规律:i+j 为偶数时,向右上走,i+j 为奇数时,向左下走
- 不要想着按层或者按条去遍历,而是从
- 注意:
- 边界比较多,往右上走时,需要考虑在第一行,在最后一列的情况
- 往左下走时,需要考虑在最后一行,在第一列的情况
快速排序
- 时不时得写一下,容易忘记
- 要点:
- 一个核心函数:实现pivot前面的都小于pivot,pivot后面的都大于pivot,返回pivot的index
- 我习惯于取第一个元素为pivot
- 从后往前遍历j,
>=
时,j--
,找到第一个小于pivot的元素 - 然后从前往后遍历i,
<=
时,i++
,找到第一个大于pivot的元素 - 交换i和j的元素
- 最后i和j相遇跳出循环,把arr[0]和arr[i]交换。
- 一个调度函数:递归调用,基于pivot的index,递归自调用
- 一个核心函数:实现pivot前面的都小于pivot,pivot后面的都大于pivot,返回pivot的index
167. 两数之和II
- 描述:给定一个升序数组,找到两个数,使得它们相加之和等于目标数
- 实现:
- 双指针,一个从左到右,一个从右到左
- 因为是升序的了,sum > target 时,右指针左移,sum < target 时,左指针右移
- 注意:
- 返回的下标需要 + 1
125. 验证回文串
- 描述:给定一个字符串,去除非字母数字的字符,大写转小写,判断它是否是回文串
- 注意:
- 记得字符串大小写转换
toLowerCase()
toUpperCase()
- 记得字符串大小写转换
11. 盛最多水的容器
- 描述:给定一个height数组,height[i]和height[j]组成一个容器,求容器中能盛最大容量
- 实现:
- 容量:
(j-i) * min(height[i], height[j])
- 用一个变量记录最大容量,在遍历过程中更新
对撞指针,移动高度小的指针,让拖后腿的加把劲
- 面积 = 两个指针的距离 * 两个指针中较小的值
- 移动指针,移动较小的值的指针
- 容量:
26. 删除有序数组中的重复项
- 描述:给定一个非递减数组,删除重复项,返回新的长度
- 实现:
- 快慢指针,快指针遍历,慢指针记录非重复项
- 快指针遍历到非重复项,慢指针就赋值
1343. 大小为K且平均值大于等于阈值的子数组数目
- 描述:给定一个数组和一个阈值,求长度为K,且平均值
>=
阈值的子数组数目 - 实现:
- 子序列问题,考虑滑动窗口实现,不用记录元素,只需要记录窗口内元素的和
这个结构特别好,很清晰 ```js let left = 0, right = 0 while(right < arr.length) { window.push(arr[right])
if(right - left + 1 > windowSize){ // 定长窗口
// 维护和更新求解的答案... window.shift() left++ }
right++ }
```
3. 无重复字符的最长子串
- 描述:求一个字符串中,不含重复字符的最长子串的长度
- 实现:
- 子串问题,滑动窗口实现,这题的核心是不含重复元素的判断
- 可以用数组,每次用
indexOf
判断 - 可以用Map, Set, 每次用
has
判断, 因为不需要放什么值,所以用Set即可1.考虑塞入s[right], 先清理再塞入,清理的时候就是窗口收缩 2.考虑求解目标,维护ans 3.right ++
215. 数组中的第K个最大元素
- 描述:给一个数组,求数组中第K大的元素
- 注意:本题测试案例贼TM搞,第41是一个包含几万个重复元素的,导致常规的快速选择二路划分会超时
- 快速选择实现
- 超时版:虽然pivot是随机选的,但是当数组中元素大部分相同时,会退化成O(n^2)
function qkSelect(nums, left, right, targetIndex) { if (left == right) return nums[left] const randomIndex = Math.floor(Math.random() * (right - left + 1)) + left; [nums[left], nums[randomIndex]] = [nums[randomIndex], nums[left]] const pivot = nums[left] let i = left, j = right while (i < j) { // 前小后大 while (i < j && nums[j] >= pivot) j-- while (i < j && nums[i] <= pivot) i++ if (i < j) [nums[i], nums[j]] = [nums[j], nums[i]] } nums[left] = nums[i] nums[i] = pivot if (targetIndex == i) return nums[targetIndex] if (targetIndex > i) return qkSelect(nums, i + 1, right, targetIndex) else return qkSelect(nums, left, i - 1, targetIndex) }
- 优化版: 三路划分[小于pivot, 等于pivot, 大于pivot]三个区间
function qkSelect(nums, left, right, targetIndex) { if (left >= right) return nums[left] const pivot = nums[left] // 三路划分 // 此时 [left, lt) < pivot, [lt, gt] == pivot, (gt, right] > pivot let lt = left, gt = right, i = left + 1 while (i <= gt) { if (nums[i] < pivot) { [nums[i], nums[lt]] = [nums[lt], nums[i]] lt++ i++ } else if (nums[i] > pivot) { [nums[i], nums[gt]] = [nums[gt], nums[i]] gt-- } else { i++ } } // 判断 targetIndex 是否落在等于区 if (targetIndex >= lt && targetIndex <= gt) { return nums[targetIndex] } else if (targetIndex < lt) { return qkSelect(nums, left, lt - 1, targetIndex) } else { return qkSelect(nums, gt + 1, right, targetIndex) } }
- 超时版:虽然pivot是随机选的,但是当数组中元素大部分相同时,会退化成O(n^2)
- 堆排序实现
- 建一个小顶堆,可以维护一个大小为k的小顶堆
- 建堆时,如有空间,就直接入堆
- 如果无空间,判断当前元素是否大于堆顶,大于就pop堆顶,然后入堆
最后,整个堆就是整个数组中前k个最大的,堆顶就是第k大的元素
- 注意,把小顶堆写成一个类是最舒服的,核心函数
_up
和_down
,写成递归的最舒服 注意, 维护一个k的小顶堆, 是在外部建堆的时候做的事情。 ```js class MinHeap {
constructor() { this.heap = [] } _up(id) { if (id == 0) return const fatherId = (id - 1) / 2 | 0 if (this.heap[fatherId] > this.heap[id]) { // swap and up [this.heap[fatherId], this.heap[id]] = [this.heap[id], this.heap[fatherId]] this._up(fatherId) } } _down(id) { if (id >= this.heap.length) return let leftID = id * 2 + 1 let rightID = id * 2 + 2 let smallestID = id if (leftID < this.heap.length && this.heap[smallestID] > this.heap[leftID]) { smallestID = leftID } if (rightID < this.heap.length && this.heap[smallestID] > this.heap[rightID]) { smallestID = rightID } if (smallestID != id) { // swap and down [this.heap[smallestID], this.heap[id]] = [this.heap[id], this.heap[smallestID]] this._down(smallestID) } } peek() { return this.heap[0] } insert(num) { this.heap.push(num) this._up(this.heap.length - 1) } shift() { if (this.heap.length == 0) return null if (this.heap.length == 1) return this.heap.pop() const res = this.heap[0] const last = this.heap.pop() this.heap[0] = last this._down(0) return res }
}
var findKthLargest = function (nums, k) {
const minHeap = new MinHeap() for (let num of nums) { if (minHeap.heap.length < k) minHeap.insert(num) else if (num > minHeap.peek()) { // 这里是重点,只有当当前num比堆顶大的时候,才入堆 // 这样最终我们的堆里才是最大的k个数 minHeap.shift() minHeap.insert(num) } } return minHeap.peek() };
```