归并排序

73 阅读2分钟

归并排序介绍

  1. 整体是递归,左边排好序+右边排好序+merge让整体有序
  2. 让其整体有序的过程里用了排外序方法
  3. 利用master公式来求解时间复杂度
  4. 当然可以用非递归实现

归并排序复杂度

  1. 根据T(N) = 2*T(N/2) + O(N^1)可知时间复杂度为O(N*logN)
  2. merge过程需要辅助数组,所以额外空间复杂度为O(N)
  3. 归并排序的实质是把比较行为变成了有序信息并传递,比O(N^2)的排序快

递归版本

public static void mergeSort1(int[] arr) {
   if (arr == null || arr.length < 2) {
      return;
   }
   process(arr, 0, arr.length - 1);
}
public static void process(int[] arr, int L, i
   if (L == R) { // base case
      return;
   }
   int mid = L + ((R - L) >> 1);
   process(arr, L, mid); // 左边排好序
   process(arr, mid + 1, R); // 右边排好序
   merge(arr, L, mid, R); // 最后merge一下
}
public static void merge(int[] arr, int L, int
   int[] help = new int[R - L + 1];
   int i = 0;
   int p1 = L;
   int p2 = M + 1;
   while (p1 <= M && p2 <= R) {
      help[i++] = arr[p1] <= arr[p2] ? arr[p++] : arr[p2++];
   }
   while (p1 <= M) { // 边界处理,防止右边拷贝完了,左边还没
      help[i++] = arr[p1++];
   }
   while (p2 <= R) { // 边界处理,防止左边拷贝完了,右边还没
      help[i++] = arr[p2++];
   }
   for (i = 0; i < help.length; i++) {
      arr[L + i] = help[i];
   }
}

非递归版本

public static void mergeSort2(int[] arr) {
   if (arr == null || arr.length < 2) {
      return;
   }
   int N = arr.length;
   // 步长
   int mergeSize = 1;
   while (mergeSize < N) { // log N
      // 当前左组的,第一个位置
      int L = 0;
      while (L < N) {
         if (mergeSize >= N - L) { // 边界,最后一个,没有右组就停下
            break;
         }
         int M = L + mergeSize - 1;
         int R = M + Math.min(mergeSize, N - M - 1);
         merge(arr, L, M, R);
         L = R + 1;
      }
      // 防止溢出,因为mergeSize会放大两倍而超过N的值
      if (mergeSize > N / 2) {
         break;
      }
      // 每次扩大两倍步长进行merge,比如第一次merge的两组分别为一个,第二次merge的两组就是分别两个
      mergeSize <<= 1;
   }
}

题目一

  • 在一个数组中,一个数左边比它小的数的总和,叫数的小和,所有数的小和累加起来,叫数组小和。求数组小和。
  • 例子: [1,3,4,2,5]
    • 1左边比1小的数:没有
    • 3左边比3小的数:1
    • 4左边比4小的数:1、3
    • 2左边比2小的数:1
    • 5左边比5小的数:1、3、4、 2
    • 所以数组的小和为1+1+3+1+1+3+4+2=16
  • 思路: 转化成求数组中的每个数,右边比它大的数有几个。结果就是,每个数 * 这个数右边比它大的数的个数的结果,全部加起来。归并排序的思路是,求每个数在2个数中、4个数中、8个数中。。。右边比它大的数的个数。
public static int smallSum(int[] arr) {
   if (arr == null || arr.length < 2) {
      return 0;
   }
   return process(arr, 0, arr.length - 1);
}

public static int process(int[] arr, int l, int r) {
   if (l == r) {
      return 0;
   }
   // l < r
   int mid = l + ((r - l) >> 1);
   return 
         process(arr, l, mid) // 左组最小和 
         + 
         process(arr, mid + 1, r)  // 右组最小和
         + 
         merge(arr, l, mid, r); // 左右组排序,并计算总最小和
}

public static int merge(int[] arr, int L, int m, int r) {
   int[] help = new int[r - L + 1];
   int i = 0;
   int p1 = L;
   int p2 = m + 1;
   int res = 0;
   while (p1 <= m && p2 <= r) {
      // 每次比较时,收集右组有几个数,比当前左组的那个数大
      res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
      // 不能是小于等于,等于时一定要先拷贝右边的
      help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
   }
   while (p1 <= m) {
      help[i++] = arr[p1++];
   }
   while (p2 <= r) {
      help[i++] = arr[p2++];
   }
   for (i = 0; i < help.length; i++) {
      arr[L + i] = help[i];
   }
   return res;
}

题目二

在一个数组中,任何一个前面的数a,和任何一个后面的数b,如果(a,b)是降序的,就称为逆序对返回数组中所有的逆序对。 思路: 也就是求,每个数右边有几个数比它小,然后加起来。

public static int reverPairNumber(int[] arr) {
   if (arr == null || arr.length < 2) {
      return 0;
   }
   return process(arr, 0, arr.length - 1);
}

// arr[L..R]既要排好序,也要求逆序对数量返回
// 所有merge时,产生的逆序对数量,累加,返回
// 左 排序 merge并产生逆序对数量
// 右 排序 merge并产生逆序对数量
public static int process(int[] arr, int l, int r) {
   if (l == r) {
      return 0;
   }
   // l < r
   int mid = l + ((r - l) >> 1);
   return process(arr, l, mid) + process(arr, mid + 1, r) + merge(arr, l, mid, r);
}

// 之所以倒着merge,是因为本题是求右边小的数的个数,和上一题相反
public static int merge(int[] arr, int L, int m, int r) {
   int[] help = new int[r - L + 1];
   int i = help.length - 1;
   int p1 = m;
   int p2 = r;
   int res = 0;
   while (p1 >= L && p2 > m) {
      res += arr[p1] > arr[p2] ? (p2 - m) : 0;
      help[i--] = arr[p1] > arr[p2] ? arr[p1--] : arr[p2--];
   }
   while (p1 >= L) {
      help[i--] = arr[p1--];
   }
   while (p2 > m) {
      help[i--] = arr[p2--];
   }
   for (i = 0; i < help.length; i++) {
      arr[L + i] = help[i];
   }
   return res;
}

题目三

  • 在一个数组中,对于每个数num,求有多少个后面的数 * 2 依然<num,求总个数
  • 比如:[3,1,7,0,2]
    • 3的后面有:1,0
    • 1的后面有:0
    • 7的后面有:0,2
    • 0的后面没有
    • 2的后面没有
    • 所以总共有5个
  • leetcode.cn/problems/re…
public static int reversePairs(int[] arr) {
   if (arr == null || arr.length < 2) {
      return 0;
   }
   return process(arr, 0, arr.length - 1);
}

public static int process(int[] arr, int l, int r) {
   if (l == r) {
      return 0;
   }
   // l < r
   int mid = l + ((r - l) >> 1);
   return process(arr, l, mid) + process(arr, mid + 1, r) + merge(arr, l, mid, r);
}

public static int merge(int[] arr, int L, int m, int r) {
   // [L....M] [M+1....R]
   int ans = 0;
   int windowR = m + 1;
   for (int i = L; i <= m; i++) {
      while (windowR <= r && (long) arr[i] > (long) arr[windowR] * 2) {
         windowR++;
      }
      ans += windowR - m - 1;
   }
   int[] help = new int[r - L + 1];
   int i = 0;
   int p1 = L;
   int p2 = m + 1;
   while (p1 <= m && p2 <= r) {
      help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
   }
   while (p1 <= m) {
      help[i++] = arr[p1++];
   }
   while (p2 <= r) {
      help[i++] = arr[p2++];
   }
   for (i = 0; i < help.length; i++) {
      arr[L + i] = help[i];
   }
   return ans;
}