本文知识点记录自左程云 算法与数据结构基础到高级教程 图片截取自视频中
04_归并排序
时间复杂度:O(N*logN)
前三种排序浪费了大量的“比较行为”,而归并排序,在递推到递归出口,要进行回溯的时候,每一次排序都会把两个各自排好序的小部分并在一起,成为一个更大的部分,再使其重新变为一个整体有序的部分。这样不断归并,直到数组的规模已经回到原始大小。
//归并排序
public static void mergeSort(int[] arr, int l, int r){
if(l == r){
return;
}
int mid = l + ((r - l) >> 1);
mergeSort(arr,l,mid);
mergeSort(arr,mid + 1,r);
merge(arr,l,mid,r);
}
//合并左右两个有序小数组,使其整体有序
public static void merge(int[] arr, int l, int m, int r){
int[] aux = new int[r-l+1];
int p1 = l, p2 = m + 1;
int i = 0;
while(p1 <= m && p2 <= r){
aux[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while(p1 <= m){
aux[i++] = arr[p1++];
}
while(p2 <= r){
aux[i++] = arr[p2++];
}
for (int j = 0; j < aux.length; j++) {
arr[l+j] = aux[j];
}
}
小和问题
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。等价于找当前数右边,比当前数大的数字个数,这个数字个数再乘以当前数,当前数不断在数组中向前推进,把每一个数得到的小和相加就是整个数组的小和。
找一个数左边比其小的数→找当前数右边比它大的数
例如{2,5,8,3}的小和为:11
public class Test_smallSum {
/***/
public static int smallSum(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int sum = 0;
int mid = l + ((r - l) >> 1);
sum += smallSum(arr, l, mid) + smallSum(arr, mid + 1, r) + merge(arr, l, mid, r);
return sum;
}
//外排序,合并两个小数组,使其变为较大规模的数组,递增排序
public static int merge(int[] arr, int l, int m, int r){
int[] aux = new int[r-l+1];
int p1 = l, p2 = m + 1;
int i = 0,ret = 0;
while(p1 <= m && p2 <= r){
ret += arr[p1] < arr[p2] ? arr[p1]*(r-p2+1) : 0;
aux[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while(p1 <= m){
aux[i++] = arr[p1++];
}
while(p2 <= r){
aux[i++] = arr[p2++];
}
for (int j = 0; j < aux.length; j++) {
arr[l+j] = aux[j];
}
return ret;
}
public static void main(String[] args) {
int[] test = new int[]{3, 7, 4, 9, 8, 3, 6, 1, 4, 3, 2};
System.out.println(smallSum(test,0,test.length-1));
}
}
//输出结果为53
逆序对问题
逆序对,数学术语,设 A 为一个有 n 个数字的有序集 (n>1),其中所有数字各不相同。
如果存在正整数 i, j 使得 1 ≤ i < j ≤ n 而且 A[i] > A[j],则 <A[i], A[j]> 这个有序对称为 A 的一个逆序对,也称作逆序数。
找一个数的左边比其大的数→找当前数的右边比它小的数
public class Test_InversePairNum {
/**逆序对,数学术语,设 A 为一个有 n 个数字的有序集 (n>1),其中所有数字各不相同。
如果存在正整数 i, j 使得 1 ≤ i < j ≤ n 而且 A[i] > A[j],则 <A[i], A[j]> 这个有序对称为 A 的一个逆序对,也称作逆序数。
打印所有的逆序对,并返回逆序对数*/
//归并排序
public static int inverseNum(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int num = 0;
int mid = l + ((r - l) >> 1);
num += inverseNum(arr, l, mid) + inverseNum(arr, mid + 1, r) + merge(arr, l, mid, r);
return num;
}
//外排序,合并两个小数组,使其变为较大规模的数组,递减排序
public static int merge(int[] arr, int l, int m, int r){
int[] aux = new int[r-l+1];
int p1 = l, p2 = m + 1;
int i = 0,ret = 0;
while(p1 <= m && p2 <= r){
if(arr[p1] > arr[p2]){
ret += r-p2+1;
for (int j = p2; j <= r; j++) {
System.out.print("<"+arr[p1]+","+arr[j]+">\t");
}
}
aux[i++] = arr[p1] > arr[p2] ? arr[p1++] : arr[p2++];
}
while(p1 <= m){
aux[i++] = arr[p1++];
}
while(p2 <= r){
aux[i++] = arr[p2++];
}
for (int j = 0; j < aux.length; j++) {
arr[l+j] = aux[j];
}
return ret;
}
public static void main(String[] args) {
int[] arr = new int[]{3,8,2,4,9,0,1,7,5,6};
int num = inverseNum(arr,0,arr.length-1);
System.out.println();
System.out.println("共有"+num+"对逆序对");
System.out.println("打印排序后的数组:");
for(int i : arr){
System.out.print(i+"\t");
}
}
}
//输出
<8,2> <3,2> <8,4> <7,6> <7,5> <9,7> <9,6> <9,5> <9,1> <9,0> <8,7> <8,6> <8,5> <8,1> <8,0> <4,1> <4,0> <3,1> <3,0> <2,1> <2,0>
共有21对逆序对
打印排序后的数组:
9 8 7 6 5 4 3 2 1 0
小和问题和逆序对问题使用排序的目的是为了让每一次合并后,合并后的新数组不重不漏地统计了满足题意的数。
05_快速排序
问题一:
给定一个数组arr,一个整数num,把arr中小于等于num的数放在数组左边,大于num的数放在数组右边,两部分内部不要求有序
基于上述问题,让数组最后一个值充当num,将num前面的数组划分为两部分以后,让大于num的区域的第一个数与num交换,这样得到问题一的题解。新得到两部分都执行原始数组执行过的操作,开始递归,递归出口为传入的数组规模为2。
这就是快速排序1.0版本的逻辑
荷兰国旗问题
给定一个数组arr,一个整数num,把arr中小于num的数放在数组左边,等于num的数放在数组中间,大于num的数放在数组右边,三部分内部不要求有序
还是让数组最后一个值充当num,num前面的数组划分为三部分以后,将num与大于num的区域第一个值交换,得到三部分。等于num的区间已经排好序,不用再更改位置,在大于num和小于num的两个区间重复执行原始数组执行过的操作。递归出口为传入的数组规模为2。
这是快速排序2.0版本的逻辑
若数组为类似
{9,8,7,6,5,4,3,2,1} 或 {1,2,3,4,5,6,7,8,9}
会导致每次分割的位置很偏,最差情况的时间复杂度为O(N^2)
对于每次的分割点为数组的哪里,是受数据影响的,所以快速排序1.0 和2.0版本的时间复杂度为O(N^2)
若每次随机选数组里的一个值,与数组最后一个元素交换作为num,这样得到的时间复杂度是多种情况时间复杂度的数学期望(各个情况等概率出现,加权求和,每种情况的权重相同),O(NlogN)
这是快速排序3.0版本的逻辑
其空间复杂度为O(logN)
也是随数值在O(N)-O(logN)波动的,求大量数据的一个概率值为O(logN)
//快速排序,时间复杂度为O(NlogN),空间复杂度为O(logN)的版本
public static void quickSort(int[] arr, int l, int r){
if(arr == null || l >= r){
return;
}
swap(arr, l+(int)(Math.random()*(r-l+1)),r);
int[] p = process(arr, l, r);
quickSort(arr, l,p[0]-1);
quickSort(arr,p[1]+1,r);
}
/**
* 当arr[i]<num时,交换arr[i]与arr[p1+1],然后让p1++,再让i++;
* 当arr[i]=num时,i++;
* 当arr[i]>num时,交换arr[i]与arr[p2-1],然后p2--,i不发生自增!!!
* 当i==p2时,结束循环,arr已经被分为以num为边界的三部分
* @param arr
* @param l
* @param r
* @return
*/
public static int[] process(int[] arr, int l, int r){
int num = arr[r];
int i = l;
int p1 = l - 1, p2 = r + 1; //p1代表小于num的区域右边界,p2代表大于num的区域左边界
while(i <= r && i < p2){
if(arr[i] < num){
swap(arr,i,p1+1);
p1++;
i++;
}else if(arr[i] > num) {
swap(arr, i, p2 - 1);
p2--;
}else{
i++;
}
}
int[] ret = new int[2];
ret[0] = p1 + 1;
ret[1] = p2 - 1;
return ret;
}
//swap
public static void swap(int[] arr, int a, int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
//print
public static void print(int[] arr){
for(int i : arr){
System.out.print(i+"\t");
}
}