本系列所有文章都将会收录到GitHub
中统一收藏与管理,欢迎ISSUE
和Star
。
GitHub传送门:Kiner算法算题记
这道题算一个比较综合性的题目,既涉及到了之前学过的链表,也涉及到了堆,还涉及到了归并排序的逻辑。首先,我们来想一下,合并三个链表的过程,是不是跟归并排序的过程及其相似,在几个链表中找到他们的最小值,然后放入到一个临时的链表中,然后继续找下一个最小值。而在找链表最小值的时候,又可以使用我们之前学过的小顶堆
来快速获取最小值对应的节点。
/*
* @lc app=leetcode.cn id=23 lang=typescript
*
* [23] 合并K个升序链表
*/
// @lc code=start
/**
* Definition for singly-linked list.
* class ListNode {
* val: number
* next: ListNode | null
* constructor(val?: number, next?: ListNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
* }
*/
export type HeapDataStruct<T> = {
idx?: number,
data: T
};
export type CompareFn<T> = (x: HeapDataStruct<T>, y: HeapDataStruct<T>) => boolean;
class Heap<T> {
private arr: HeapDataStruct<T>[] = [];
private count: number = 0;
constructor(private cmpFn: CompareFn<T>) {
}
// 比较方法,决定了我们要构建的堆是一个大顶堆还是小顶堆
private _compare(curIdx: number, pIdx: number): boolean {
return this.cmpFn(this.arr[curIdx], this.arr[pIdx])
}
private _shift_up(idx: number): void {
// 然后对堆进行向上调整
// 我们需要通过子节点坐标得到父节点的坐标,因为我们这边的下标是从0开始的,所以左子树根节点坐标为:2 * i + 1,右子树根节点坐标为:2 * i + 2。那么我们父节点的编号:parseInt((子节点编号 - 1) / 2),如知道左节点编号为:2 * i + 1,那么他的父节点坐标就是: parseInt((2 * i + 1 - 1) / 2) = parseInt(i);如果知道有节点的编号:2 * i + 2,那么他的父节点的坐标为:parseInt((2 * i + 2 - 1) / 2) = parseInt((2*i+1)/2) = i
// 上面我们讲过,我们需要如果在一个大顶堆中,如果我们父节点的值小于子节点的值的话,就要交换这两个值
// while(idx && this.arr[parseInt(String((idx - 1) / 2))].data < this.arr[idx].data) {
while(idx && this._compare(parseInt(String((idx - 1) / 2)), idx)) {
// 父节点编号
const pIdx = parseInt(String((idx - 1) / 2));
// 交换父节点和当前节点的值
[ this.arr[pIdx], this.arr[idx] ] = [ this.arr[idx], this.arr[pIdx] ];
// 然后再让idx变成父节点的编号,这样就能依次向上调整了
idx = pIdx;
}
}
private _shift_down(idx: number): void {
// 交换之后,根节点再依次向下调整
// 最大的子节点的下标
let n = this.count - 1;
// 当idx的子节点的下标比最大的子节点的下标小,就说明还有子节点,就要往下继续调整
while(idx * 2 + 1 <= n) {
// max代表在根节点、左子树、右子树中最大值的下标
let max = idx;
// 如果左子树的根节点的值比当前值大,就把max更新为左子树根节点的下标
// if(this.arr[max].data < this.arr[2*idx+1].data) max = 2*idx+1;
if(this._compare(max, 2*idx+1)) max = 2*idx+1;
// 如果右子树根节点的值比当前节点大,就把max更新为右子树根节点的下标
// 需要注意的是,我们当前的节点,可能存在左子树,但不一定存在右子树,所以需要多加一个2*idx+2<=n的条件
if(2*idx+2<=n && this._compare(max, 2*idx+2)) max = 2*idx+2;
// if(2*idx+2<=n && this.arr[max].data < this.arr[2*idx+2].data) max = 2*idx+2;
// 如果我的最大值的下标就是当前的这个值,那就不需要向下调整了,直接结束循环
if(max === idx) break;
// 然后交换这个最大值的下标和根节点
[ this.arr[idx], this.arr[max] ] = [ this.arr[max], this.arr[idx] ];
// 然后把idx改为原最大值的下标,继续向下调整
idx = max;
}
}
push(item: HeapDataStruct<T>): void {
this.arr[this.count++] = item;
this._shift_up(this.count-1);
// this.output('push: ');
}
pop(): HeapDataStruct<T>|null {
if(this.size()===0) return null;
const tmp = this.arr[0];
// 根据上面的讲解,我们首先需要让队尾元素与根节点交换
[ this.arr[0], this.arr[this.count-1] ] = [ this.arr[this.count-1], this.arr[0] ];
// 交换之后记得count要减1
this.count--;
// 进行向下调整
this._shift_down(0);
// this.output('pop: ');
return tmp;
}
size(): number {
return this.count;
}
top(): HeapDataStruct<T> {
return this.arr[0];
}
output(pre: string = ""): void {
console.log(pre+JSON.stringify(this.getArray()));
}
// 将堆中的数据里面的data单独拿出来放到数组返回
getArray(sort: (a: HeapDataStruct<T>, b: HeapDataStruct<T>) => number = () => 0): T[] {
return this.arr.slice(0, this.count).sort(sort).map(item=>item.data);
}
}
function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
// 建立一个小顶堆,用于每次获取最小的一个链表节点
const heap = new Heap<ListNode>((a, b) => a.data.val > b.data.val);
// 将所有的链表头结点放入小顶堆中(如果是空链表就不用放进去了)
for(const listNode of lists) {
if(!listNode) continue;
heap.push({
data: listNode
});
}
// 建立一个虚拟头结点用来存储合并后的链表,并让q指针初始指向该虚拟头结点
let head: ListNode = new ListNode(), q = head;
// 如果小顶堆不为空,则进入循环
while(heap.size()) {
// 每次弹出小顶堆的堆顶元素,即最小的元素
const minListNode = heap.pop();
// 把这个节点挂在新链表的后面
q.next = minListNode.data;
// 让q指针移动到当前节点,方便下一轮将下一个节点挂在当前节点后边
q = minListNode.data;
// 如果当前节点有下一个节点,继续将下一个节点加入到小顶堆中
if(minListNode.data && minListNode.data.next) heap.push({
data: minListNode.data.next
})
}
// 返回新链表虚拟头结点的下一个节点即可
return head.next;
};
// @lc code=end
这一题我们在之前学习快速排序时使用快速排序的思路实现了一次,现在,我们学习了归并排序,我们就用归并排序的方式实现。首先,我们需要知道链表总共的长度,然后再根据长度将链表划分为左右两个部分,使用归并思想分别对左右两部分进行排序,最终合并左右两部分有序链表
/*
* @lc app=leetcode.cn id=148 lang=typescript
*
* [148] 排序链表
*/
// @lc code=start
/**
* Definition for singly-linked list.
* class ListNode {
* val: number
* next: ListNode | null
* constructor(val?: number, next?: ListNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
* }
*/
// 使用快排方式实现
function _sortList_quickSort(head: ListNode | null): ListNode | null {
if(!head) return head;
let p = head, q, h1=null,h2=null,mid;
let min = Number.MAX_SAFE_INTEGER,max = Number.MIN_SAFE_INTEGER;
// 找出最小值和最大值
while(p) {
min = Math.min(p.val, min);
max = Math.max(p.val, max);
p = p.next;
}
if(min === max) return head;
// 求中间值
mid = (min + max) >> 1;
// 复位p
p = head;
// 将小于中间值的节点挂在h1,将大于中间值的节点挂在h2
while(p) {
q = p.next;
if(p.val <= mid) {
p.next = h1;
h1 = p;
} else {
p.next = h2;
h2 = p;
}
p = q;
}
// 递归操作h1和h2
h1 = sortList(h1);
h2 = sortList(h2);
// 将h1尾结点与h2的头结点链接起来成为一个新链表
p = h1;
while (p.next) {
p = p.next;
}
p.next = h2;
return h1;
}
function _mergeSort(head: ListNode | null, n: number): ListNode | null {
// 当只有一个节点或没有节点时无需排序
if(!head || !head.next) return head;
// 计算链表左边的节点数量和右边的节点数量
// 左边链表节点数量就拿链表总长度除以2舍弃小数部分,右边长度就直接用总长度减去左边长度
let l = n >> 1, r = n - l;
// 我们首先需要将链表断开成长度分别为l和r的两个链表
// 左指针指向原链表头部,初始时右指针先指向左指针,稍后会将右指针移动到正确位置,并将链表拆分成两个分别以lp和rp
// 作为头结点的链表,p是一个临时节点,用于临时存储链表有半部分的前一个节点
let lp = head, rp = lp, p = new ListNode();
// 让rp向右移动至链表左半部分的最后一个节点的位置,之所以不移动到右半边的第一个位置,是因为我们要断开链表
// 就需要知道链表头结点的上一个节点是谁才能断开
for(let i=1;i<l;i++,rp = rp.next);
// 使用p临时存储右半边链表的上一个节点,让rp指向右半边链表的头结点,然后断开左右链表
p = rp, rp = rp.next,p.next = null;
// 至此,我们就生成了分别以lp和rp为头结点的两个链表了
// 然后,根据归并排序的思想,分别排序左半边链表和右半边链表
lp = _mergeSort(lp, l);
rp = _mergeSort(rp, r);
// 排序完毕之后,我们就可以合并了
// 定义一个虚拟头结点用来存储最终合并的链表,并让p指针指向这个虚拟头结点
let res = new ListNode();
p = res;
// 只要左右链表有一个没有遍历结束就继续
while(lp || rp) {
// 如果右边链表为空或者左边链表不为空且左边链表的值小于右边链表的值
if(!rp || (lp && lp.val < rp.val)) {
// 将做链表的头结点放入到结果链表当中
p.next = lp;
// 左边链表指针右移一位
lp = lp.next;
// 结果链表指针右移一位
p = p.next;
} else {
// 将右边链表加入到结果链表
p.next = rp;
// 右边链表指针右移一位
rp = rp.next;
// 结果链表右移一位
p = p.next;
}
}
// 结果就是我们虚拟头结点的下一个节点
return res.next;
}
// 使用归并排序方式实现
function _sortList_mergeSort(head: ListNode | null): ListNode | null {
let n = 0;
let p = head;
while(p) p = p.next,n++;
return _mergeSort(head, n);
}
function sortList(head: ListNode | null): ListNode | null {
// return _sortList_quickSort(head);
return _sortList_mergeSort(head);
};
// @lc code=end
二叉搜索树又称为二叉排序树,其中序遍历的结果是一个有序序列,我们可以先获取两个二叉搜索树的中序遍历结果,然后剩下的就是利用归并排序思想合并两个有序数组了
/*
* @lc app=leetcode.cn id=1305 lang=typescript
*
* [1305] 两棵二叉搜索树中的所有元素
*/
// @lc code=start
/**
* Definition for a binary tree node.
* class TreeNode {
* val: number
* left: TreeNode | null
* right: TreeNode | null
* constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
* }
*/
// 对一个二叉树进行中序遍历,并将结果存到数组中
function getTreeInOrderedResult(root: TreeNode, arr: number[]): void {
if(!root) return;
getTreeInOrderedResult(root.left, arr);
arr.push(root.val);
getTreeInOrderedResult(root.right, arr);
}
function getAllElements(root1: TreeNode | null, root2: TreeNode | null): number[] {
const lVals: number[] = [], rVals: number[] = [];
// 分别获得两个二叉搜索树的中序遍历结果(二叉搜索树的中序遍历结果是一个有序的序列)
getTreeInOrderedResult(root1, lVals);
getTreeInOrderedResult(root2, rVals);
// 然后根据归并排序的思想将两个有序序列合并
let lp = 0,rp = 0,res = [];
// 归并排序合并思想,不再赘述
while(lp < lVals.length || rp < rVals.length) {
if(rp>=rVals.length || (lp<lVals.length && lVals[lp] <= rVals[rp])) res.push(lVals[lp++])
else res.push(rVals[rp++])
}
return res;
};
// @lc code=end
依据题意,区间和值其实就是所提共数组下标所对应的封闭区间内元素的和值,如例1中:nums = [-2,5,-1], lower = -2, upper = 2
我们来列举一下所有区间以及他对应的和值([-2,5,-1]的数组索引分别为:0,1,2):
-2
一个元素,因此区间和值为-2
-2
和5
两个元素,区间和值为-3
2
5
4
-1
综上所述,区间和满足在-2
~2
的有(0,0) (0,2) (2,2)总共3个。
接下来我们再来想想,我们要怎么来求区间和呢?其实我们要求区间和,首先我们就要先求出数组的前缀和,前缀和的通俗理解是:第一项为0,以后每一项都是前面元素之和,如上面的数组[-2,5,-1]的前缀和数组为[0,-2,3,2],有了前缀和数组之后,我们的区间和值就等于前缀和数组中两项相减的值。即;区间和=前缀和两项相减。那么,我们就把原文题转换成了,在前缀和数组中满足:lower <= sum(j) - sum(i) <= upper | i < j
这样的一个问题。
将上面的公式转化一下:sum(j) - lower <= sum(i) >= sum(j) - upper
,这样,我们就得出了sum(i)
的一个特性:
sum(i) >= sum(j) - lower && sum(i) >= sum(j) - upper
/*
* @lc app=leetcode.cn id=327 lang=typescript
*
* [327] 区间和的个数
*/
// @lc code=start
function countTowPart(nums: number[], l1: number, r1: number, l2: number, r2: number, lower: number, upper: number): number {
let res = 0;
// 定义两个指针,初始都指向l1
let lp = l1, rp = l1;
// 在右区间中循环查找每一个元素,看看他们在左区间中有多少个满足条件的数
for(let j=l2; j <= r2; j++) {
// 根据上面推到的公式:sum(i) >= sum(j) - lower && sum(i) >= sum(j) - upper
let a = nums[j] - upper;
let b = nums[j] - lower;
// 接下来就要找到大于等于a的第一个位置和大于b的第一个位置即可
// 由于我们的nums拆分的左右两个分区数组经过下面归并排序的过程已经有序了,因此,我们借助数组的有序性
// 当左指针没有到达左分区末尾,并且不满足上面我们推到的公式:sum(i) >= sum(j) - upper
// 因此,如果当nums[lp] < a时,就要让区间左指针向右移动
while(lp <= r1 && nums[lp] < a) lp++;
while(rp <= r1 && nums[rp] <= b) rp++;
// 符合条件的元素数量就是左右两个指针的差值
res += rp - lp;
}
return res;
}
function mergeSort(nums: number[], l: number, r: number, lower: number, upper: number): number {
let res = 0;
if(l >= r) return res;
let mid = (l + r) >> 1;
// 分别计算所半边数量和右半边数量
res += mergeSort(nums, l, mid, lower, upper);
res += mergeSort(nums, mid + 1, r, lower, upper);
// 计算横跨两边的数量,只需在右边找一下有多少左边的元素满足条件即可
res += countTowPart(nums, l, mid, mid+1, r, lower, upper);
// 进行归并过程
let tmp: number[] = [], lp = l, rp = mid + 1;
while(lp <= mid || rp <= r) {
if(rp > r||(lp <= mid && nums[lp] < nums[rp])) tmp.push(nums[lp++])
else tmp.push(nums[rp++]);
}
// 将临时数组替换到元素组中,完成排序
nums.splice(l, tmp.length, ...tmp);
return res;
}
function countRangeSum(nums: number[], lower: number, upper: number): number {
// 先求出目标数组的前缀和数组
const sums: number[] = [];
// 前缀和数组第一项固定为0,后面每一项都是前几项之和
sums[0] = 0;
// 计算前缀和
for(let i=0;i<nums.length;i++) sums[i+1] = sums[i] + nums[i];
return mergeSort(sums, 0, sums.length - 1, lower, upper);
};
// @lc code=end
这题我们同样也能使用归并排序的过程求解,与之前的归并排序不同的是,我们之前是从小到大排序,而这里,因为我们要查找比当前数小的数的个数,所以要保证右侧的数比较小,因此,要使用从大到小的排序,然后我们只需要计算在右侧区间中的元素数量并累加即可得到我们想要的答案,不过在此期间,我们需要使用一个自定义数据结构来存储索引、值、计数器之间的关系,方便最终将结果输出。
/*
* @lc app=leetcode.cn id=315 lang=typescript
*
* [315] 计算右侧小于当前元素的个数
*
* https://leetcode-cn.com/problems/count-of-smaller-numbers-after-self/description/
*
* algorithms
* Hard (41.96%)
* Likes: 585
* Dislikes: 0
* Total Accepted: 45.1K
* Total Submissions: 107.4K
* Testcase Example: '[5,2,6,1]'
*
* 给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于
* nums[i] 的元素的数量。
*
*
*
* 示例:
*
* 输入:nums = [5,2,6,1]
* 输出:[2,1,1,0]
* 解释:
* 5 的右侧有 2 个更小的元素 (2 和 1)
* 2 的右侧仅有 1 个更小的元素 (1)
* 6 的右侧有 1 个更小的元素 (1)
* 1 的右侧有 0 个更小的元素
*
*
*
*
* 提示:
*
*
* 0 <= nums.length <= 10^5
* -10^4 <= nums[i] <= 10^4
*
*
*/
// @lc code=start
// 用于存储索引、值和目标计数器的数据结构
interface Data {
idx: number,
val: number,
count: number
}
// 归并排序
function mergeSort(nums: Data[], l: number, r: number): void {
// 以下就是归并排序过程,不再赘述
if(l >= r) return;
let mid = (l + r) >> 1;
mergeSort(nums, l, mid);
mergeSort(nums, mid + 1, r);
let lp = l, rp = mid + 1, tmp: Data[] = [];
while(lp <= mid || rp <= r) {
// 与之前的归并排序不同的是,此处我们要从大到小排序,这样就能够保证,我们右边每一个数都肯定比左边的数小
// 我们只要计算右边元素的数量即可
if(rp > r || (lp <= mid && nums[lp].val > nums[rp].val)) {
// 累加当前元素的元素数量
nums[lp].count += (r - rp + 1);
tmp.push(nums[lp++]);
} else {
tmp.push(nums[rp++]);
}
}
// 更新原数组
nums.splice(l, tmp.length, ...tmp);
}
function countSmaller(nums: number[]): number[] {
// 为了存储每个数字的值与索引的关系,也为了方便存储每个数组对应的结果值,先按照数据结构要求,将原始数组中
// 的数据存储到自定义数据结构中,每个数字后面比当前数字小的数量初始为0
const arr: Data[] = [];
for(let i=0;i<nums.length;i++) arr.push({
idx: i,
val: nums[i],
count: 0
});
// 在进行归并排序的过程中,不断计算每个值后面比他小的数的数量
mergeSort(arr, 0, nums.length - 1);
// 用于存储所有数量的结果数组
const res: number[] = [];
// 将所有的计数器按照期对应的索引存储到结果数组当中
arr.forEach((item) => res[item.idx] = item.count);
return res;
};
// @lc code=end
遇到求区间和
问题,我们第一时间就要想到先求前缀和
,有了前缀和
数组后,就可以通过前缀和
的差值求出区间和。如例题:
nums: [-2,1,-3,4,-1,2,1,-5,4]
前缀和数组sums: [0,-2,-1,-4,0,-1,1,2,-3,1]
求出了前缀和数组之后,我们只需要让前缀和数组除了第一项之外的每一项减去前面几项中的最小值,就能够得到当前项的区间和了:
索引1的子序和:-2
索引2的子序和:-1 - (-2) = 1
索引3的子序和:-4 - (-2) = -2
索引4的子序和:0 - (-4) = 4
索引5的子序和:-1 - (-4) = 3
索引6的子序和:1 - (-4) = 5
索引7的子序和:2 - (-4) = 6
索引8的子序和:-3 - (-4) = 1
索引9的子序和:1 - (-4) = 5
[-2,1,-2,4,3,5,6,1,5],所以,我们最大子序列和就是这个数组的最大值,即6。
/*
* @lc app=leetcode.cn id=53 lang=typescript
*
* [53] 最大子序和
*
* https://leetcode-cn.com/problems/maximum-subarray/description/
*
* algorithms
* Easy (54.06%)
* Likes: 3243
* Dislikes: 0
* Total Accepted: 515.4K
* Total Submissions: 951.1K
* Testcase Example: '[-2,1,-3,4,-1,2,1,-5,4]'
*
* 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
*
*
*
* 示例 1:
*
*
* 输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
* 输出:6
* 解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
*
*
* 示例 2:
*
*
* 输入:nums = [1]
* 输出:1
*
*
* 示例 3:
*
*
* 输入:nums = [0]
* 输出:0
*
*
* 示例 4:
*
*
* 输入:nums = [-1]
* 输出:-1
*
*
* 示例 5:
*
*
* 输入:nums = [-100000]
* 输出:-100000
*
*
*
*
* 提示:
*
*
* 1
* -10^5
*
*
*
*
* 进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。
*
*/
// @lc code=start
function maxSubArray(nums: number[]): number {
if(nums.length===1) return nums[0];
// 要求区间和,就先求前缀和
const sums: number[] = [0];
// 生成前缀和数组
for(let i=0;i<nums.length;i++) sums[i+1] = sums[i] + nums[i];
let min = sums[0], max = Number.MIN_SAFE_INTEGER;
for(let i=1;i<sums.length;i++) {
// 如果当前值减去当前值之前的最小值比当前的最大值大,则更新为最大值
if(sums[i] - min > max) max = sums[i] - min;
// 如果当前值比最小值小,则更新最小值
if(sums[i] < min) min = sums[i];
}
return max;
};
// @lc code=end