归并排序 = 递归 + 合并
代码实现
通过递归将大数组拆分成每一项,然后对两项进行有序合并。归并排序中重点为合并,即将两个有序数组合并为一个有序数组。
我们可以设置两个指针,一个指向左边数组的第一项,一个指向右边数组第一项,比较两个指针对应的数,将小的放入临时数组中,然后对应小的指针右移,不断循环直到两个指针都移到对应数组的末尾后。
最后把临时数组覆盖到原数组对应位置就可以了。
var mergeSort = function (arr, left, right) {
if (left >= right) return;
let mid = Math.floor((left + right) / 2);
//将左边数组变有序
mergeSort(arr, left, mid)
//将右边数组变有序
mergeSort(arr, mid + 1, right)
//将左右两个有序数组合并
let p1 = left, p2 = mid + 1, saveArr = [];
while (p1 <= mid || p2 <= right) {
if ((p2 > right) || (p1 <= mid && arr[p1] < arr[p2])) {
saveArr.push(arr[p1])
p1++
} else {
saveArr.push(arr[p2])
p2++
}
}
//将合并后数组覆盖原数组
for (let i = left; i <= right; i++) {
arr[i] = saveArr[i - left]
}
}
let arr = [5, 9, 1, 4, 3, 2, 8, 7, 6]
mergeSort(arr, 0, arr.length - 1)
console.log(arr); //[1, 2, 3, 4, 5, 6, 7, 8, 9]
归并排序作用并不局限于排序,其重要的思想就是将一个大问题拆分为若干个小问题,通过解决小问题与横跨小问题之间的问题来解决大问题。下面通过实际问题来了解其强大之处。
题目
1.逆序对问题
思路
此题重点在于合并过程中记录逆序对的数量。
首先对数组进行降序排列,在合并两个数组过程中,每当左边数组的值放入临时数组时,由于两边的数组都是降序,所以当前值比右边数组指针下标到数组末尾的所有值都大。即右边数组末尾下标 - 右指针下标 = 逆序对个数
代码
/**
* @param {number[]} nums
* @return {number}
*/
var mergeSort = function (arr, l, r) {
if (l >= r) return 0;
let mid = Math.floor((l + r) / 2),
//记录左右数组的逆序对个数
Lnum = mergeSort(arr, l, mid),
Rnum = mergeSort(arr, mid + 1, r),
num = Lnum + Rnum;
let left = l, right = mid + 1, sortArr = [];
while (left <= mid || right <= r) {
if ((right > r) || (left <= mid && arr[left] > arr[right])) {
sortArr.push(arr[left]);
// 左边的值加入临时数组,记录逆序对数量
num += r - right + 1
left++
} else {
sortArr.push(arr[right]);
right++
}
}
for (let i = l; i <= r; i++) {
arr[i] = sortArr[i - l]
}
return num
}
var reversePairs = function (nums) {
return mergeSort(nums, 0, nums.length - 1)
};
2.排序链表
思路
这题就是普通的递归排序,只不过从数组变成链表。
个人感觉在拆分上难一点,一开始我是通过遍历一遍获得链表长度,然后再让指针跑到一半的位置,需要遍历两次。后来看到别人的快慢指针感觉不错就抄了:)
而合并过程没什么好说的,就是指针的左右横跳。
代码
/**
* function ListNode(val, next) {
* this.val = (val === undefined ? 0 : val)
* this.next = (next === undefined ? null : next)
* }
**/
var merge = function (leftList, rightList) {
let temp = new ListNode(), p = temp;
// 将两个有序链表合并
while (leftList && rightList) {
if (leftList.val < rightList.val) {
p.next = leftList
leftList = leftList.next
} else {
p.next = rightList
rightList = rightList.next
}
p = p.next
}
// 将剩下的一条链表加到结果链表的末尾
if (leftList) {
p.next = leftList
} else if (rightList) {
p.next = rightList
}
//返回合并后链表
return temp.next
}
/**
* @param {ListNode} head
* @return {ListNode}
*/
var sortList = function (head) {
if (!head || !head.next) return head;
let fast = head.next, slow = head;
// 快慢指针,快指针走到末尾,慢指针走到一半
while (fast) {
slow = slow.next
fast = fast.next
if (fast) fast = fast.next;
}
// 拆成两条有序链表
let rightList = slow.next;
slow.next = null
let leftList = head
let l = sortList(leftList)
let r = sortList(rightList)
return merge(l, r)
};
3.排列两棵二叉搜索树中的所有元素
思路
由于二叉搜索树的特性:左节点比根节点小,右节点比根节点大
。
所以对二叉树进行中序遍历就会得到一个有序数组,然后对两个有序数组合并即可。
代码
/**
* 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)
* }
*/
// 中序遍历
var inorder = function (root) {
if (!root) return [];
let leftArr = inorder(root.left),
rightArr = inorder(root.right);
return [...leftArr, root.val, ...rightArr]
}
/**
* @param {TreeNode} root1
* @param {TreeNode} root2
* @return {number[]}
*/
var getAllElements = function (root1, root2) {
// 获得两个有序数组
let leftList = inorder(root1),
rightList = inorder(root2);
// 合并两个有序数组
let p1 = 0, p2 = 0, temp = [];
while (p1 < leftList.length && p2 < rightList.length) {
if (leftList[p1] < rightList[p2]) {
temp.push(leftList[p1])
p1++
} else {
temp.push(rightList[p2])
p2++
}
}
if (p1 < leftList.length) {
temp.push(...leftList.slice(p1))
} else if (p2 < rightList.length) {
temp.push(...rightList.slice(p2))
}
return temp
};
4.共同祖先
思路
一共三种情况:
- p, q有一个是root;
- p, q分别在root左节点,右节点上;
- p, q都在root的同一侧;
代码
/**
* @param {TreeNode} root
* @param {TreeNode} p
* @param {TreeNode} q
* @return {TreeNode}
*/
var lowestCommonAncestor = function(root, p, q) {
if(!root)return root;
//第一种情况,返回当前节点
if(root == p || root == q)return root;
//寻找左右子树是否存在p,q
let left = lowestCommonAncestor(root.left, p, q)
let right = lowestCommonAncestor(root.right, p, q)
//第二种情况,返回当前节点
if(left && right)return root;
//第三种情况,存在哪一侧就返回那一侧寻找到的结果
return left ? left : right
};
5.最深叶子节点的和
思路
记录两个值:
- 已经查找到的节点最大深度
- 现在记录到的最大深度节点的和值
代码
var numSum = function (root, deep, arr) {
if (!root) return;
// 深度增加,覆盖旧值
if (deep > arr[0]) {
arr[1] = root.val
arr[0] = deep
} else if (deep == arr[0]) {
// 深度相同,累加
arr[1] += root.val
}
// 递归左右子节点
numSum(root.left, deep + 1, arr)
numSum(root.right, deep + 1, arr)
}
/**
* @param {TreeNode} root
* @return {number}
*/
var deepestLeavesSum = function (root) {
// 第一项记录深度,第二项记录和值
let arr = [0, 0]
numSum(root, 0, arr)
return arr[1]
};
6.子数组和排序后的区间和
思路
先遍历整个数组,把数组里面的所有子数组和都计算出来,然后进行归并排序得到有序数组,最后对10^9 + 7 取
模后返回即可。
代码
var rangeSum = function (nums, n, left, right) {
let list = []
// 计算所有子数组和
for (let i = 0; i < n; i++) {
let temp = 0
for (let j = i; j < n; j++) {
temp += nums[j]
list.push(temp)
}
}
// 归并排序
mergeSort(list, 0, list.length - 1)
// 计算区间和
let Sum = 0;
for (let i = left-1; i <= right-1; i++) {
Sum += list[i]
Sum %= 1000000007
}
return Sum
};
7. 区间和的个数
思路
求区间和值都可以转换为前缀和问题。
前缀和概念:
利用这一特性,原数组下标 i 到下标 j 之间的和值为前缀和数组下标 j 的值减去下标 i 的值。
即 arr[i] + ... + arr[j] = prefix[j] - prefix[i]
代码
/**
* @param {number[]} nums
* @param {number} lower
* @param {number} upper
* @return {number}
*/
// 前缀和
var prefix = function(arr) {
let temp = [0]
for (let i = 0; i < arr.length; i++) {
temp.push(temp[i]+arr[i])
}
return temp
}
var countRangeSum = function (nums, lower, upper) {
let preList = prefix(nums);
return mergeSort(preList,0,preList.length-1, lower, upper)
};
var mergeSort = function (arr, left, right, lower, upper) {
if (left >= right) return 0;
let mid = Math.floor((left + right) / 2);
// 记录左右两边数组符合区间和的个数
let leftNum = mergeSort(arr, left, mid, lower, upper)
let rightNum = mergeSort(arr, mid + 1, right, lower, upper)
let num = leftNum + rightNum
// 统计右边减左边符合的下标对数量
let i = left;
let l = mid + 1;
let r = mid + 1;
while (i <= mid) {
while (l <= right && arr[l] - arr[i] < lower) l++;
while (r <= right && arr[r] - arr[i] <= upper) r++;
num += (r - l);
i++;
}
// 将左右两个有序数组合并
let p1 = left, p2 = mid + 1, saveArr = [];
while (p1 <= mid || p2 <= right) {
if ((p2 > right) || (p1 <= mid && arr[p1] < arr[p2])) {
saveArr.push(arr[p1])
p1++
} else {
saveArr.push(arr[p2])
p2++
}
}
// 将合并后数组覆盖原数组
for (let i = left; i <= right; i++) {
arr[i] = saveArr[i - left]
}
return num
}
8. 计算右侧小于当前元素的个数
思路
首先排序规则为降序排序,这样当左数组元素加入结果数组时,右数组剩余元素数量就是当前元素大于右数组元素的数量。
注意:由于排序会改变原数组元素位置,需要新建一个对象数组记录元素的值与元素的原位置,然后对对象数组排序即可。
代码
/**
* @param {number[]} nums
* @return {number[]}
*/
var countSmaller = function (nums) {
// 新建对象数组,记录元素原位置
let objList = nums.map((i, inx) => {
return {
value: i,
index: inx
}
})
let resArr = new Array(nums.length).fill(0)
mergeSort(objList, 0, nums.length - 1, resArr)
return resArr
};
var mergeSort = function (arr, left, right, resArr) {
if (left >= right) return;
let mid = Math.floor((left + right) / 2);
mergeSort(arr, left, mid, resArr)
mergeSort(arr, mid + 1, right, resArr)
//每次合并都记录个数
let p1 = left, p2 = mid + 1, saveArr = [];
while (p1 <= mid && p2 <= right) {
if (arr[p1].value > arr[p2].value) {
saveArr.push(arr[p1])
//左数组加入结果数组时记录元素个数
resArr[arr[p1].index] += (right - p2 + 1)
p1++
} else {
saveArr.push(arr[p2])
p2++
}
}
if (p1 <= mid) {
saveArr.push(...arr.slice(p1, mid + 1))
} else if (p2 <= right) {
saveArr.push(...arr.slice(p2, right + 1))
}
//将合并后数组覆盖原数组
for (let i = left; i <= right; i++) {
arr[i] = saveArr[i - left]
}
}