对于归并排序来说有两种方式,一种是递归方式,一种是非递归的循环方式。
整体思想是我先让数组左右两部分有序,然后两个有序部分合并进而整体有序,称这个过程为归并。
将大问题分为同类型的小规模问题
归并排序递归算法
public class MargeSort {
public static void sort(int[] arr) {
processSort(arr, 0, arr.length - 1);
}
// 递归含义 使 [L,R] 范围有序
public static void processSort(int[] arr, int L, int R) {
if (arr.length < 2 || L == R) {
// 结束条件
return;
}
int mid = L + ((R - L) >> 1);
processSort(arr, L, mid);
processSort(arr, mid + 1, R);
marge(arr, L, mid, R);
}
/**
* @Description: 合并 [L ,mid] [mid+1,R] 两个有序数组
* @Param arr
* @Param L
* @Param mid
* @Param R
* @Return void
*/
private static void marge(int[] arr, int L, int mid, int R) {
int[] help = new int[R - L + 1];
int lStart = L;
int rStart = mid + 1;
int i = 0;
while (lStart <= mid && rStart <= R) {
help[i++] = arr[lStart] < arr[rStart] ? arr[lStart++] : arr[rStart++];
}
while (lStart <= mid) {
help[i++] = arr[lStart++];
}
while (rStart <= R) {
help[i++] = arr[rStart++];
}
i = 0;
for (int j = L; j <= R; j++) {
arr[j] = help[i++];
}
}
public static void main(String[] args) {
int[] arr;
int[] arr2;
SelectionSort selectionSort = new SelectionSort();
Random random = new Random();
// 对数器
for (int i = 0; i < 1000; i++) {
int len = random.nextInt(20) + 5;
arr = new int[len];
arr2 = new int[len];
for (int j = 0; j < len; j++) {
int item = random.nextInt(100);
arr[j] = item;
arr2[j] = item;
}
sort(arr);
selectionSort.sort(arr2);
for (int k = 0; k < len; k++) {
if (arr[k]!=arr2[k]){
System.out.println("error");
return;
}
}
}
System.out.println("success");
}
}
归并排序,时间复杂度 O(N*longN) , 空间复杂度 O(N)
归并排序非递归算法
递归算法运行起来有点像一颗树的形状,从一开始的大问题,二分为两个小问题,然后继续分,开枝散叶最后汇总。
从 JVM 层面理解,每个线程的执行,都会对应分配一个线程栈内存区域,这个区域每个方法执行都会分配一块栈帧内存空间并将其压入栈,直到执行完成则弹出,对应到递归过程直到你触发到递归出口,否则就会一直循环调用子方法拆解问题并入栈。
而将递归算法改为非递归算法的过程,有点像这个的逆过程,有点像一颗倒着的树,循序代替了栈的功能。
public static void sort2(int[] arr) {
// 子合并区域以 1 开始
int range = 1;
// 如果子合并区域小于数组则已经完成排序,子合并区域每次 2 倍增长
while (range < arr.length) {
int start = 0;
// 每次参与合并的步长 是 range 的两倍
int step = range << 1;
// 如果后续子数组不够两个合并区则最后一部分无需排序,已经有序
while (start + range < arr.length) {
int L = start;
int mid = start + range - 1;
// 防止 R 越界,即数组最后不够一个 range 时
int R = Math.min(start + step - 1, arr.length - 1);
marge(arr, L, mid, R);
start = R + 1;
}
range = step;
}
}
大 while 是 O(longN) 内部 while 是 O(N) 所以总时间复杂度和递归一样 O(N*longN) 空间复杂度 O(N)
归并排序的应用
可解决 某一类问题,可以抽象为数组中左边有多少数比某个数大,反之亦然。
比如 数组小和问题 给定一个数组,任意一个位置 i ,该元素前面比它小的数的和为该数的小和,所有数小和的累加为数组小和。
package algorithmic.base;
import util.CommonUtil;
import util.RandomUtil;
import java.util.Arrays;
// 数组最小和问题,归并排序应用
public class MargeSortGetMinSum {
// 返回 [L.R] 范围的数组小和
public static int processSortGetMin(int[] arr, int L, int R) {
if (arr.length < 2 || L == R) {
// 结束条件
return 0;
}
int mid = L + ((R - L) >> 1);
// 分解子问题
int lMin = processSortGetMin(arr, L, mid);
int rMin = processSortGetMin(arr, mid + 1, R);
// 左右合并得到的小和
int mMin = margeGetMinSum(arr, L, mid, R);
return mMin + lMin + rMin;
}
// 获取小和
private static int margeGetMinSum(int[] arr, int L, int mid, int R) {
int[] help = new int[R - L + 1];
int lStart = L;
int rStart = mid + 1;
int result = 0;
int i = 0;
while (lStart <= mid && rStart <= R) {
if (arr[lStart] < arr[rStart]) {
int current = help[i++] = arr[lStart++];
// 抽象为右组有多少个数比当前数大,可以省去遍历相加
result += current * (R - rStart + 1);
} else {
// 相等时,先入右组数,更新游标
help[i++] = arr[rStart++];
}
}
while (lStart <= mid) {
help[i++] = arr[lStart++];
}
while (rStart <= R) {
help[i++] = arr[rStart++];
}
i = 0;
for (int j = L; j <= R; j++) {
arr[j] = help[i++];
}
return result;
}
// 归并方法,时间复杂度 O(N*longN)
public static int getMinSum(int[] arr) {
return processSortGetMin(arr, 0, arr.length - 1);
}
// 暴力方法 O(N^2)
public static int getMinSumSimple(int[] arr) {
int sum = 0;
for (int i = 1; i < arr.length; i++) {
for (int j = 0; j < i; j++) {
if (arr[j] < arr[i]) {
sum += arr[j];
}
}
}
return sum;
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int[] arr = RandomUtil.randomArr();
int[] arr2 = CommonUtil.copyArr(arr);
int[] arr3 = CommonUtil.copyArr(arr);
int minSumSimple = getMinSumSimple(arr);
int minSum = getMinSum(arr2);
if (minSum != minSumSimple) {
System.out.println(Arrays.toString(arr3));
System.out.println(Arrays.toString(arr2));
System.out.println(Arrays.toString(arr));
System.out.println("error marge=" + minSum + " simple=" + minSumSimple);
return;
}
}
System.out.println("success");
}
}
这里可以使用归并来做的思路很简单,在整个归并过程中,左右组的元素都会经历一次 marge 过程 ,在这个过程中可以处理获取左右组的关系,并且排序过后不会影响后续的 marge 中左右位置的关系,利用子组有序的特性,将题目小和累加同等转换为当前数右边有多少个数比它大 result += current * (R - rStart + 1)
逆序对问题
在一个数组中,任意 i 位置数 如果大于任意后置位的数,则与该数组成一个逆序对,给定一个数组求数组中有多少逆序对
如对于数组 [1,3,4,5,2]
逆序对有 [3,2] [4,2] [5,2] 结果为 3
这里转换为对于数组中某个数,这个数前面有多少个数比它大,即在每个归并过程中右组元素归并时计算左组还剩多少元素
package algorithmic.base.marge_sort;
import util.CommonUtil;
import util.RandomUtil;
import java.util.Arrays;
//**逆序对问题**
//
//在一个数组中,任意 i 位置数 对于任意后置位的数如果大于则与该数组成一个逆序对,给定一个数组求数组中有多少逆序对
//
//```java
//如对于数组 [1,3,4,5,2]
//逆序对有 [3,2] [4,2] [5,2] 结果为 3
//```
public class MargeSortReversePair {
public static int reversePairNumSimple(int[] arr) {
int num = 0;
for (int i = 1; i < arr.length; i++) {
for (int j = 0; j < i; j++) {
if (arr[j] > arr[i]) {
num++;
}
}
}
return num;
}
public static int reversePairNum(int[] arr) {
return processSort(arr, 0, arr.length - 1);
}
// 递归含义 使 [L,R] 范围有序
public static int processSort(int[] arr, int L, int R) {
if (arr.length < 2 || L == R) {
// 结束条件
return 0;
}
int mid = L + ((R - L) >> 1);
return processSort(arr, L, mid) +
processSort(arr, mid + 1, R) +
marge(arr, L, mid, R);
}
/**
* @Description: 合并 [L ,mid] [mid+1,R] 两个有序数组
* @Param arr
* @Param L
* @Param mid
* @Param R
* @Return void
*/
private static int marge(int[] arr, int L, int mid, int R) {
int[] help = new int[R - L + 1];
int lStart = L;
int rStart = mid + 1;
int i = 0;
int num = 0;
while (lStart <= mid && rStart <= R) {
if (arr[lStart] <= arr[rStart]) {
// 相等时,先入左组数,更新游标
help[i++] = arr[lStart++];
} else {
// 抽象为左组有多少个数比当前数大
num += (mid - lStart + 1);
help[i++] = arr[rStart++];
}
}
while (lStart <= mid) {
help[i++] = arr[lStart++];
}
while (rStart <= R) {
help[i++] = arr[rStart++];
}
i = 0;
for (int j = L; j <= R; j++) {
arr[j] = help[i++];
}
return num;
}
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
int[] arr = RandomUtil.randomArr();
int[] arr2 = CommonUtil.copyArr(arr);
int reversePairNum = reversePairNum(arr);
int reversePairNumSimple = reversePairNumSimple(arr2);
if (reversePairNum != reversePairNumSimple) {
System.out.println(Arrays.toString(arr2));
System.out.println("error reversePairNum=" + reversePairNum +
" reversePairNumSimple=" + reversePairNumSimple);
return;
}
}
System.out.println("success");
}
}
快排的核心思想是分区,那最重要的就是分区算法。
给定一个值 x ,将大于 x 的值放到数组右边,小于的放到左边,这个过程就是分区
一次分区过程就相当于确定了 x 值在整个数组中的位置,刨去 x 在去两端进行分区,直到所有分区完毕即完成了排序
当我们一次分区搞定一个数,快排的时间复杂度为 O(N^2) , 只有在一次分区搞定多个相等数时才是我们想要的快排,同时对于 x 的取值要随机,此时的时间复杂度为 O(N*logN)
普通分区算法
思路是定义一个小于等于区,一开始为数组 L 范围 -1 ,遍历数组推进小于等于区的扩张
// 在 [L,R] 上进行分区 返回等于数的位置
public static int partition(int[] arr, int L, int R) {
// 小于等于区下标
int minEqIndex = L - 1;
for (int i = L; i <= R; i++) {
if (arr[i] <= arr[R]) {
swap(arr, i, ++minEqIndex);
}
}
return minEqIndex;
}
荷兰国旗分区算法
区别是我们将数组分为三份,小于区,等于区,大于区
这样我们一次分区就搞定了一组等于区的数
// 在 [L,R] 上进行分区 返回等于区下标
public static int[] partition(int[] arr, int L, int R) {
if (L > R){
return new int[]{-1, -1};
}
if (L == R){
return new int[]{L, R};
}
// 小于区下标
int minEqIndex = L - 1;
// 大于区下标
int maxEqIndex = R;
int index = L;
// 随机取一个数
int some = random.nextInt(R - L + 1) + L;
swap(arr, some, R);
while (index < maxEqIndex) {
if (arr[index] < arr[R]) {
swap(arr, index++, ++minEqIndex);
} else if (arr[index] > arr[R]) {
swap(arr, index, --maxEqIndex);
} else {
index++;
}
}
swap(arr, R, maxEqIndex);
return new int[]{minEqIndex + 1, maxEqIndex};
}
初版快排
import util.CommonUtil;
import util.RandomUtil;
import java.util.Arrays;
public class QuickSortSimple {
public static void sort(int[] arr) {
process(arr, 0, arr.length - 1);
}
// [L,R] 范围有序
public static void process(int[] arr, int L, int R) {
if (L >= R) {
return;
}
int m = partition(arr, L, R);
process(arr, L, m - 1);
process(arr, m + 1, R);
}
// 在 [L,R] 上进行分区 返回等于数的位置
public static int partition(int[] arr, int L, int R) {
// 小于等于区下标
int minEqIndex = L - 1;
for (int i = L; i <= R; i++) {
if (arr[i] <= arr[R]) {
swap(arr, i, ++minEqIndex);
}
}
return minEqIndex;
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
int[] arr = RandomUtil.randomArr();
int[] arr2 = CommonUtil.copyArr(arr);
MargeSort.sort(arr);
sort(arr2);
for (int j = 0; j < arr.length; j++) {
if (arr[j] != arr2[j]) {
System.out.println(Arrays.toString(arr));
System.out.println(Arrays.toString(arr2));
System.out.println("error");
return;
}
}
}
System.out.println("success");
}
}
经典快排
import util.CommonUtil;
import util.RandomUtil;
import java.util.Arrays;
import java.util.Random;
public class QuickSort {
public static final Random random = new Random();
public static void sort(int[] arr) {
process(arr, 0, arr.length - 1);
}
// [L,R] 范围有序
public static void process(int[] arr, int L, int R) {
if (L >= R) {
return;
}
int[] pair = partition(arr, L, R);
process(arr, L, pair[0] - 1);
process(arr, pair[1] + 1, R);
}
// 在 [L,R] 上进行分区 返回等于区下标
public static int[] partition(int[] arr, int L, int R) {
if (L > R){
return new int[]{-1, -1};
}
if (L == R){
return new int[]{L, R};
}
// 小于区下标
int minEqIndex = L - 1;
// 大于区下标
int maxEqIndex = R;
int index = L;
// 随机取一个数
int some = random.nextInt(R - L + 1) + L;
swap(arr, some, R);
while (index < maxEqIndex) {
if (arr[index] < arr[R]) {
swap(arr, index++, ++minEqIndex);
} else if (arr[index] > arr[R]) {
swap(arr, index, --maxEqIndex);
} else {
index++;
}
}
swap(arr, R, maxEqIndex);
return new int[]{minEqIndex + 1, maxEqIndex};
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
int[] arr = RandomUtil.randomArr();
int[] arr2 = CommonUtil.copyArr(arr);
MargeSort.sort(arr);
sort(arr2);
for (int j = 0; j < arr.length; j++) {
if (arr[j] != arr2[j]) {
System.out.println(Arrays.toString(arr));
System.out.println(Arrays.toString(arr2));
System.out.println("error");
return;
}
}
}
System.out.println("success");
}
}