在股票交易中,如果前一天的股价高于后一天的股价,则可以认为存在一个「交易逆序对」。请设计一个程序,输入一段时间内的股票交易记录 record
,返回其中存在的「交易逆序对」总数。
示例 1:
输入:record = [9, 7, 5, 4, 6] 输出:8 解释:交易中的逆序对为 (9, 7), (9, 5), (9, 4), (9, 6), (7, 5), (7, 4), (7, 6), (5, 4)。
提示:
0 <= record.length <= 50000
LCR 170. 交易逆序对的总数 - 力扣(LeetCode)
这个题目要用归并排序来写,暴力解法很容易想得到。刚好借这个题目来复习一下归并排序。
归并排序的代码如下:
public class MergeSort {
// 归并排序的主方法
public static void mergeSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return; // 如果数组为空或只有一个元素,直接返回
}
int[] temp = new int[arr.length]; // 临时数组,用于合并
mergeSort(arr, 0, arr.length - 1, temp);
}
// 递归分治
private static void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = left + (right - left) / 2; // 计算中间位置
mergeSort(arr, left, mid, temp); // 对左半部分排序
mergeSort(arr, mid + 1, right, temp); // 对右半部分排序
merge(arr, left, mid, right, temp); // 合并两个有序部分
}
}
// 合并两个有序数组
private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left; // 左半部分的起始索引
int j = mid + 1; // 右半部分的起始索引
int k = 0; // 临时数组的起始索引
// 将两个有序数组合并到临时数组中
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
// 将左半部分剩余的元素复制到临时数组
while (i <= mid) {
temp[k++] = arr[i++];
}
// 将右半部分剩余的元素复制到临时数组
while (j <= right) {
temp[k++] = arr[j++];
}
// 将临时数组中的元素复制回原数组
k = 0;
while (left <= right) {
arr[left++] = temp[k++];
}
}
需要注意这段代码:
// 将临时数组中的元素复制回原数组
k = 0;
while (left <= right) {
arr[left++] = temp[k++];
}
写的时候容易漏掉,临时数组的元素一定要返回到原数组中。
我们会发现在归并排序的过程中都会比较两个子序列中两个数的大小,在合并两个子序列的过程中,右边的数组指针指向i项,比左边的元素小,由于左右子序列都是已经排好序的了,那么右边数组就有i项比左边现在这个元素小,也就是有i个子序列。注意一开始指针指向子数组末尾。
基于这个思想,只要在归并排序的过程中统计逆序对个数就行了。
class Solution {
public int reversePairs(int[] record) {
if (record == null || record.length <= 1) {
return 0; // 如果数组为空或只有一个元素,直接返回
}
int[] temp = new int[record.length]; // 临时数组,用于合并
int num = mergeSort(record, 0, record.length - 1, temp);
return num;
}
private int mergeSort(int[] arr, int left, int right, int[] temp){
if(left >= right){
return 0;
}
int mid = left + (right - left)/2;
int l = mergeSort(arr, left, mid, temp);
int r = mergeSort(arr, mid + 1, right, temp);
if (arr[mid] <= arr[mid + 1]) {
return l + r;
}
int cur = merge(arr, left, mid, right, temp);
return l + r + cur;
}
private int merge(int[] arr, int left, int mid, int right, int[] temp){
int i = mid;
int j = right;
int k = right;//临时数组的索引
int reversePairsNum = 0;//逆序对个数。
while (i >= left && j > mid) {
if (arr[i] > arr[j]) {
reversePairsNum += (j - mid);
temp[k] = arr[i];
k--;
i--;
} else {
temp[k] = arr[j];
k--;
j--;
}
}
// 将左半部分剩余的元素复制到临时数组
while (i >= left) {
temp[k--] = arr[i--];
}
// 将右半部分剩余的元素复制到临时数组
while (j > mid) {
temp[k--] = arr[j--];
}
// 将临时数组中的元素复制回原数组
for (i = left; i <= right; i++) {
arr[i] = temp[i];
}
return reversePairsNum;
}
}
很多细节没注意到,一开始写成i > left导致总是漏掉最小元素的逆序对,我觉得还是归并的熟练度不够。我是按照剑指offer里面思路写的,把归并排序逆过来了。比较直观。但是逆过来的时候等号取不取得到要注意一下。建议手动排序一下。
力扣上采用的是正向的归并排序,不容易出错:
private int mergeAndCount(int[] record, int left, int mid, int right, int[] temp) {
for (int i = left; i <= right; i++) {
temp[i] = record[i];
}
int i = left;
int j = mid + 1;
int count = 0;
for (int k = left; k <= right; k++) {
if (i == mid + 1) {
record[k] = temp[j];
j++;
} else if (j == right + 1) {
record[k] = temp[i];
i++;
} else if (temp[i] <= temp[j]) {
record[k] = temp[i];
i++;
} else {
record[k] = temp[j];
j++;
count += (mid - i + 1);
}
}
return count;
}
}
作者:力扣官方题解
链接:https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/solutions/216984/shu-zu-zhong-de-ni-xu-dui-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
其实二者的本质是一样的,【3,5,7,9】和 【4,6,8,10】当5>4的时候就把(7,4) (9,4) 算进去。所以每次count只要加上mid-i +1即可。
我一开始想用j - mid发现在偶数是对的,奇数就是错的,因为比如[3,5,7,8][4,6,9]这样的数组,因为奇数个的会把mid取在left这边,就会导致8为第一项的逆序对无法加入,因为不会出现8>?的判断条件。