归并排序 小和问题、逆序对问题

329 阅读3分钟

题目

在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。 例子:[1,3,4,2,5],1左边比1小的数,没有; 3左边比3小的数1; 4左边比4小的数1、3; 2左边比2小的数1; 5左边比5小的数,1、3、4、2

  • 求所有数左边比它小的数的和,等价于求每个数右边有多少个数比它大,然后将每个数*比它大数的进行累加求和
    • 比如1的右边有4个比它大,3大右边有2个比它,4的右边有1个比它大,2的右边有1个比它大,5的右边没有,固最终的小和值为:1*4+3*2+4*1+2*1=16
  • 而对于这种左边集合的值和右边集合的值相比的思路,和归并排序相同,所以通过归并排序就能完成遍历,而对于求每个数大于的个数,因为归并排序每次左右集合是有序的,所以一旦左边的某个数比右边的某个数小,则一定小于右边那个数之后的所有数,所以通过下标就能求出个数
  • 相较于归并排序中左右集合相同的数,优先取左集合不同,这里需要优先取右集合,因为取右集后使得右集合下标增加,最终下标到集合重点才是所有比左边比较多数大的个数,否则如[1,1,2,2],[1,1,3,3],对于左集合第一个数1,优先取左的话,右边的下标到末尾就是4个数,如果优先取右,则右边的集合的下标就是2,最终比1大的个数就是2,符合预期
  • 每个数会不遗漏、不会重复比较
    • 归并排序的每个集合比较顺序为:[1],[3]比,[1,3]和[4]比,[2]和[5]比,[1,3,4]和[2,5]比
    • 不遗漏:每个数会依次和它右边的所有集合比较
    • 不重复,因为每次比较的右集合是不同的,并且右集合,合起来是除了当前数以外的右边所有数
let arr = [1, 3, 4, 2, 5];
let total = 0;

function mergeSort(arr, left, right) {
  if (left == right) {
    return;
  }
  let mid = (right - left) >> 1;
  mid += left;
  mergeSort(arr, left, mid);
  mergeSort(arr, mid + 1, right);

  processMerge(arr, left, mid, right);
}

function processMerge(arr, left, mid, right) {
  let helper = [];
  let pointL = left,
    pointR = mid + 1,
    i = 0;

  //每次判断的左右集合都是有序的
  while (pointL <= mid && pointR <= right) {
    //这里和经典归并不同,相等优先取右集合的
    if (arr[pointL] < arr[pointR]) {
      //如果左边集合的某个数比右边集合的某个数小,则一定比右边集合剩余的数小
      total += arr[pointL] * (right - pointR + 1);

      helper[i++] = arr[pointL++];
    } else {
      helper[i++] = arr[pointR++];
    }
  }

  //左边集合已放完,剩下的全是比左边大的
  while (pointL <= mid) {
    helper[i++] = arr[pointL++];
  }

  while (pointR <= right) {
    helper[i++] = arr[pointR++];
  }

  // 修改原数组
  for (let j = 0; j < helper.length; j++) {
    arr[left + j] = helper[j];
  }
}

mergeSort(arr, 0, arr.length - 1);
console.log(total);

题目

在一个数组中,任意取两个数(不能两次都取自身),如果右边的数比左边的数小,则称为一个逆序对,求一共有多少个逆序对

  • 也就是求每个数右边有多少个数比它小,而归并排序就会进行左右两边比较,如果是升序比较,如果右边集合都比左边小,那么右边集合会被挨个放入,如果是降序比较,如果右边的集合都比左边小,那么左边的集合都会被挨个放入,且右边集合的指针不会变化,此时就能根据右边指针到右集合边界的数量求得有多少个数比左边指针指向的数小
  • 固采取降序排列的归并排序,当左右两边相同时,优先移动右指针(只取比左指针指向的数小的那部分)
let arr = [3, 2, 4, 5, 0];
let total = 0;

function mergeSort(arr, left, right) {
  if (left == right) {
    return;
  }
  let mid = (right - left) >> 1;
  mid += left;
  mergeSort(arr, left, mid);
  mergeSort(arr, mid + 1, right);

  processMerge(arr, left, mid, right);
}

function processMerge(arr, left, mid, right) {
  let helper = [];
  let pointL = left,
    pointR = mid + 1,
    i = 0;

  //每次判断的左右集合都是有序的
  while (pointL <= mid && pointR <= right) {
    //这里和经典归并不同,相等优先取右集合的
    if (arr[pointL] <= arr[pointR]) {
      //如果左边集合的某个数比右边集合的某个数小,则一定比右边集合剩余的数小

      helper[i++] = arr[pointR++];
    } else {
      total += right - pointR + 1;
      helper[i++] = arr[pointL++];
    }
  }

  //左边集合已放完,剩下的全是比左边大的
  while (pointL <= mid) {
    helper[i++] = arr[pointL++];
  }

  while (pointR <= right) {
    helper[i++] = arr[pointR++];
  }

  // 修改原数组
  for (let j = 0; j < helper.length; j++) {
    arr[left + j] = helper[j];
  }
}

mergeSort(arr, 0, arr.length - 1);
console.log(arr);
console.log(total);