首页 认识O(NlogN)的排序
文章
取消

认识O(NlogN)的排序

剖析递归行为和递归行为复杂度的估算

递归求数组最大值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class RecursionArr {
  // right<arr.length,否则发送数组越界异常
  public static int process(int[] arr, int left, int right){
    if (left == right){
      return arr[left];
    }
    // (right-left)>>2 外面要加括号
    int mid = left + ((right-left)>>2);
    int num1 = process(arr, left, mid);
    int num2 = process(arr, mid+1, right);

    return Math.max(num1, num2);
  }
}

master公式

master公式是用来估算一类特殊的递归行为复杂度的方法,这类特殊的递归行为必须满足子问题规模相同才可使用master公式,公式如下:
T(N) = aT(N/b)+O(Nd),其中a代表子问题调用的次数,N/b代表子问题规模,O(Nd)代表除子问题外的额外时间复杂度,如果满足master公式则有:
1.logba < d —> O(Nd)
2.logba > d —> O(Nlogba)
3.logba = d —> O(Nd·logN)

使用master公式就可以很方便的估算一类递归行为的时间复杂度

归并排序O(NlogN)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class MergeSort {
  public static void mergeSort(int[] arr){
    if (arr == null || arr.length < 2){
      return;
    }
    process(arr, 0, arr.length - 1);
  }

  private static void process(int[] arr, int L, int R) {
    if (L == R){
      return;
    }
    int mid = L + ( (R-L) >> 1 );
    process(arr, L, mid);
    process(arr, mid+1, R);
    merge(arr, L, mid, R);
  }

  private static void 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;
    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];
    }
  }
}

很显然归并排序满足master公式,求解可得:
①母问题规模即arr数组长度为N,子问题规模为N/2,b=2
②子问题被调用了2次,a=2
③额外复杂度即help填充值和help拷贝进原数组的过程,即2O(N),那么就是O(N),d=1
④logba = d
所以归并排序的时间复杂度是O(NlogN)

另外merge和process是在同一栈空间,而merge会引用辅助变量help,所以每次递归的空间复杂度是O(N),调用完后就会销毁,所以最后的空间复杂度就是O(N)

小和问题

在一个数组中,每个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
例子:
数组:[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

求解思路如下:
小和问题

按照归并的方法,数组每次会被分为左右两个区间,在merge的过程中,区间之间数据的绝对位置是没有改变的,也就是说对于左区间的某个数A来说,未二分之前和二分之后A都是在右区间的左边

merge在合并的过程中,如果左区间数据A小于有区间数据B,那么A会小于B和B右边的所有数据,因为两个区间本身是有序的

原题链接和我的题解

逆序对问题

在一个数组中,左边的数如果比右边的数大,则选择两个数构成一个逆序对,请打印所有的逆序对

分析思路同小和问题

原题链接

注意:
在该原题中:求余公式(a+b)%p=(a%p+b%p)%p

快速排序

快速排序1.0

荷兰国旗问题

问题1

给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
思路如下:
问题1

问题2(荷兰国旗问题)

给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
颜色分类

该题解题思路比较多:

  1. 单指针,遍历两次数组,分别把nums[i]=0和nums[i]=1的交换到数组最左,其中第一次遍历0,指针最后的位置为p1,所以区间[0, p1-1]都是0,第二次遍历1,指针最后的位置是p2,所以区间[p1, p2]都是1,剩下的就都是2,这样数组就排好了。
  2. 因为数组的元素固定分别是0、1、2,所以统计出个元素的数量,然后按照左0,中1,右2的顺序放置元素,数组就排好了,需要两次遍历,第一次统计,第二次填充数组
  3. 双指针,如果双指针都从0开始,其中p0交换0,p1交换1,i负责遍历,需要注意得是:p0==p1时,p0每次交换,p1也要与p0同步+1,这步保证p1不会落后p0,但直到p0 < p1得时候,这个时候就不需要遵循先前得原则,因为p1 > p0,而如果现在p0需要交换,那么一定会把原本的1替换出去,这时需要由p1再次交换,然后两者都+1,也就是如果p1 > p0,后面就会一直大于
  4. 双指针,其中p0从0开始,p2从n-1开始,n=nums.length,p0负责交换0,p2负责交换2,i负责遍历

总结:不管怎么样第四种都是最右而且最简单得一种方法,所以还是优先采用第四种方式

快排2.0

在划分分区时:将其分为三块,<target、==target和>target三部分,这样每次分区,所有==target的就被排序好了

快排3.0

快排1.0和快排2.0都有一个问题:
快排1.0和2.0存在的问题

快排3.0为了不让target的选择过于固定,于是使用了生成随机位置的方法,最终在概率的加持下时间复杂度能保持在O(NlogN)
因为每次分层递归都需要额外的空间存储target,所以空间复杂度为O(logN),

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class QuickSort {
  public static void quickSort(int[] arr){
    if (arr == null || arr.length == 0 || arr.length == 1){
      return;
    }
    quickSort(arr, 0, arr.length-1);
  }

  public static void quickSort(int[] arr, int L, int R){
    if (L >= R){
      return;
    }
    swap(arr, L + (int) (Math.random()*(R-L+1)), R);
    int[] p = partition(arr, L, R);
    quickSort(arr, L, p[0]-1);
    quickSort(arr, p[1]+1, R);

  }

  public static int[] partition(int[] arr, int L, int R){
    int less = L-1;
    int more = R;
    while(L < more){
      if (arr[L] < arr[R]){
        swap(arr, ++less, L++);
      }else if (arr[L] > arr[R]){
        swap(arr, --more, L);
      }else {
        L++;
      }
    }
    swap(arr, more, R);
    return new int[]{less+1, more};
  }

  public static void swap(int[] arr, int a, int b){
    int tmp = arr[a];
    arr[a] = arr[b];
    arr[b] = tmp;
  }
}

原题链接

相关题目:
最小k个数

这里要求使用快排,思路:

1.不同与快排将数组分为大于prox和小于等于prox的两个区间,这里我们只考虑左区间,直到左区间长度为k
2.当左区间不足k个长度时,此时区间替换成右区间,并且我们只要分离出k-larr.length个最小的数据
3.当分离出的左区间大于k,此时区间替换成分离后得左区间,并且我需要分离出k个最小得数据 4.这样就形成得区间范围不断缩小得有效递归

本文由作者按照 CC BY 4.0 进行授权