ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

【数据结构与算法】【左神】02-认识O(Nlog N)的排序

2021-08-18 18:31:26  阅读:175  来源: 互联网

标签:02 arr int 复杂度 元素 左神 num Nlog 数组


1. 归并排序

1.1 归并排序的原理

  以数组 [2, 1, 3, 6, 5, 2] 为例来讲解归并排序的思路。首先,将待排序数组均分为两个数组,并将这两个数组排序。结果即 [1, 2, 3] 和 [2, 5, 6]。接下来,将这两个数组合并,使其整体有序。思路是创建一个 buffer,从这两个数组的首元素开始对比,将较小的元素放入 buffer。倘若两个子数组中取到的元素相等,将左侧的元素放入 buffer。直至某个子数组下标越界,将另一个子数组中的剩余元素放入 buffer。以上工作完成后,再将 buffer 中排好序的元素拷贝回原数组。

  接下来的描述中,黑体的元素即为下标所指的待处理元素。

  左子数组  右子数组   buffer

  [1, 2, 3]    [2, 5, 6]    []

  [1, 2, 3]    [2, 5, 6]    [1]

  [1, 2, 3]    [2, 5, 6]    [1, 2]

  [1, 2, 3]    [2, 5, 6]    [1, 2, 2]

  [1, 2, 3]    [2, 5, 6]    [1, 2, 2, 3]

  [1, 2, 3]    [2, 5, 6]    [1, 2, 2, 3, 5, 6]

  简而言之,归并排序整体就是一个简单递归,将左边排好序,右边也排好序,然后再使其整体有序。让其整体有序的过程中用了外排序方法(先将数据放在外部数组里,排序完成再拷回来)。

1.2 归并排序代码示例

 1 public class MergeSort {
 2 
 3     public static void mergeSort(int[] arr) {
 4         if (arr == null || arr.length < 2) {
 5             return;
 6         }
 7 
 8         process(arr, 0, arr.length - 1);
 9     }
10 
11     public static void process(int[] arr, int L, int R) {
12         if (L == R) {
13             return;
14         }
15 
16         int mid = L + ((R - L) >> 1);
17         process(arr, L, mid);
18         process(arr, mid + 1, R);
19         merge(arr, L, mid, R);
20     }
21 
22     public static void merge(int[] arr, int L, int M, int R) {
23         int[] help = new int[R - L + 1];
24         int i = 0;
25         int p1 = L;
26         int p2 = M + 1;
27 
28         while (p1 <= M && p2 <= R) {
29             help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
30         }
31 
32         while (p1 <= M) {
33             help[i++] = arr[p1++];
34         }
35 
36         while (p2 <= R) {
37             help[i++] = arr[p2]++;
38         }
39 
40         for (int i = 0; i < help.length; i++) {
41             arr[L + i] = help[i];
42         }
43 
44         return;
45     }
46 
47 }

1.3 归并排序的时间复杂度和额外空间复杂度

  利用 master 公式来求解时间复杂度,a = 2, b = 2,d = 1。套用 master 公式,即可得到时间复杂度为 O(Nlog N)。

  额外空间复杂度为 O(N)。

1.4 归并排序的实质

  选择排序、插入排序、冒泡排序的时间复杂度均为 O(N2)。它们就差在浪费了很多比较行为。以冒泡排序为例,第一次比较 0~N-1 之间的元素,搞定一个数。第二次比较 1~N-1 之间的元素,搞定一个数。以此类推。但这些排序是相互独立的,丢弃了大量的数据。

  归并排序之所以能把时间复杂度优化到 O(Nlog N),是因为没有浪费比较行为。每次比较完成,都会将一个元素排好序。

1.5 归并排序的扩展——小和问题

1.5.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。

1.5.2 小和问题的思路转化

  求小和的问题,可以转化为判断在某个元素的右侧,有几个元素比它大的问题。右侧有几个比它大的元素,该元素就被计算几次小和。因此,可以将求小和转化为类似归并排序的问题。但有两个问题需要明确:

    • 排序的操作是不能去掉的。因为我们在归并过程中希望直接根据下标判断,右侧有几个元素比当前元素大。这样做的前提是有序。
    • 当左侧子数组和右侧子数组当前需要判断的元素相等时,归并排序是将左侧子数组中的元素放入 buffer。求小和的操作则应将右侧子数组中的元素放入 buffer,否则怎么知道右侧有几个元素比当前元素大呢?

1.5.3 小和问题示例代码

  1 public class Code02_SmallSum {
  2 
  3     public static int smallSum(int[] arr) {
  4         if (arr == null || arr.length < 2) {
  5             return 0;
  6         }
  7         return mergeSort(arr, 0, arr.length - 1);
  8     }
  9 
 10     public static int mergeSort(int[] arr, int l, int r) {
 11         if (l == r) {
 12             return 0;
 13         }
 14         int mid = l + ((r - l) >> 1);
 15         return mergeSort(arr, l, mid) 
 16                 + mergeSort(arr, mid + 1, r) 
 17                 + merge(arr, l, mid, r);
 18     }
 19 
 20     public static int merge(int[] arr, int l, int m, int r) {
 21         int[] help = new int[r - l + 1];
 22         int i = 0;
 23         int p1 = l;
 24         int p2 = m + 1;
 25         int res = 0;
 26         while (p1 <= m && p2 <= r) {
 27             res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
 28             help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
 29         }
 30         while (p1 <= m) {
 31             help[i++] = arr[p1++];
 32         }
 33         while (p2 <= r) {
 34             help[i++] = arr[p2++];
 35         }
 36         for (i = 0; i < help.length; i++) {
 37             arr[l + i] = help[i];
 38         }
 39         return res;
 40     }
 41 
 42     // for test
 43     public static int comparator(int[] arr) {
 44         if (arr == null || arr.length < 2) {
 45             return 0;
 46         }
 47         int res = 0;
 48         for (int i = 1; i < arr.length; i++) {
 49             for (int j = 0; j < i; j++) {
 50                 res += arr[j] < arr[i] ? arr[j] : 0;
 51             }
 52         }
 53         return res;
 54     }
 55 
 56     // for test
 57     public static int[] generateRandomArray(int maxSize, int maxValue) {
 58         int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
 59         for (int i = 0; i < arr.length; i++) {
 60             arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
 61         }
 62         return arr;
 63     }
 64 
 65     // for test
 66     public static int[] copyArray(int[] arr) {
 67         if (arr == null) {
 68             return null;
 69         }
 70         int[] res = new int[arr.length];
 71         for (int i = 0; i < arr.length; i++) {
 72             res[i] = arr[i];
 73         }
 74         return res;
 75     }
 76 
 77     // for test
 78     public static boolean isEqual(int[] arr1, int[] arr2) {
 79         if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
 80             return false;
 81         }
 82         if (arr1 == null && arr2 == null) {
 83             return true;
 84         }
 85         if (arr1.length != arr2.length) {
 86             return false;
 87         }
 88         for (int i = 0; i < arr1.length; i++) {
 89             if (arr1[i] != arr2[i]) {
 90                 return false;
 91             }
 92         }
 93         return true;
 94     }
 95 
 96     // for test
 97     public static void printArray(int[] arr) {
 98         if (arr == null) {
 99             return;
100         }
101         for (int i = 0; i < arr.length; i++) {
102             System.out.print(arr[i] + " ");
103         }
104         System.out.println();
105     }
106 
107     // for test
108     public static void main(String[] args) {
109         int testTime = 500000;
110         int maxSize = 100;
111         int maxValue = 100;
112         boolean succeed = true;
113         for (int i = 0; i < testTime; i++) {
114             int[] arr1 = generateRandomArray(maxSize, maxValue);
115             int[] arr2 = copyArray(arr1);
116             if (smallSum(arr1) != comparator(arr2)) {
117                 succeed = false;
118                 printArray(arr1);
119                 printArray(arr2);
120                 break;
121             }
122         }
123         System.out.println(succeed ? "Nice!" : "Fucking fucked!");
124     }
125 
126 }

1.6 归并排序的扩展——逆序对问题

1.6.1 逆序对问题概述

  在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对。给定数组,请计算逆序对数量。

1.6.2 逆序对问题思路转化

  逆序对问题可以转化为求某个元素右侧有几个元素比它小的问题。

1.6.3 逆序对问题代码示例

 

 

2 荷兰国旗问题

2.1 问题一

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

2.1.1 思路转化

  首先,设置一个“≤ 区”,在这个区域中的元素都是小于等于给定 num 的。在程序起始阶段,“≤ 区”为空,此时“≤ 区”后面的元素是数组首元素。

  此外,还要使用一个变量 i,向后遍历数组。此过程分为两种情况:

    • arr[i] <= num
      • 此时将 arr[i] 和“≤ 区”后面的元素交换,“≤ 区”右扩,i++;
    • arr[i] > num
      • i++。

  以数组 [3, 5, 4, 7, 6, 3, 5, 8] 为例,分析算法过程(蓝色为 i 指向的元素,红色部分为“≤ 区”):

  (1)[3, 5, 4, 7, 6, 3, 5, 8]

    初始状态。此时 3 就是“≤ 区”后面的元素,因此是自己和自己交换。此处容易弄错,需要注意。

    3 和自己交换,“≤ 区”右扩,i++。

  (2)[3, 5, 4, 7, 6, 3, 5, 8]

    5 和自己交换,“≤ 区”右扩,i++。

  (3)[3, 5, 4, 7, 6, 3, 5, 8]

    4 和自己交换,“≤ 区”右扩,i++。

  (4)[3, 5, 4, 7, 6, 3, 5, 8]

    不做交换,“≤ 区”不右扩,i++。

  (5)[3, 5, 4, 7, 6, 3, 5, 8]

    不做交换,“≤ 区”不右扩,i++。

  (6)[3, 5, 4, 7, 6, 3, 5, 8]

    3 和 7 交换,“≤ 区”右扩,i++。

  (7)[3, 5, 4, 3, 6, 7, 5, 8]

    5 和 6 交换,“≤ 区”右扩,i++。

  (8)[3, 5, 4, 3, 5, 7, 6, 8]

    不做交换,“≤ 区”不右扩,i++。

  (9)最终结果为 [3, 5, 4, 3, 5, 7, 6, 8]。

2.2 问题二(荷兰国旗问题)

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

2.2.1 思路转化

  设置一个“< 区”,在这个区域中的元素都是小于给定 num 的。在程序起始阶段,“< 区”为空,此时“< 区”后面的元素是数组元素。

  设置一个“> 区”,在这个区域中的元素都是大于给定 num 的。在程序起始阶段,“> 区”为空,此时“> 区”前面的元素是数组元素。

  此外,还要使用一个变量 i,向后遍历数组。此过程分为三种情况:

    • arr[i] < num
      • 将 arr[i] 和“< 区”后面的元素交换,“< 区”右扩,i++;
    • arr[i] = num
      • i++;
    • arr[i] > num
      • 将 arr[i] 和“> 区”前面的元素交换,“> 区”左扩,i 不变;

  以数组 [3, 5, 4, 7, 6, 3, 5, 8] 为例,分析算法过程(蓝色为 i 指向的元素,红色部分为“< 区”,绿色部分为“> 区”):

  (1)[3, 5, 4, 7, 6, 3, 5, 8]

    3 和自己交换,“< 区右扩”,i++。

  (2)[3, 5, 4, 7, 6, 3, 5, 8]

    i++。

  (3)[3, 5, 4, 7, 6, 3, 5, 8]

    4 和 5 交换,“< 区右扩”,i++。

  (4)[3, 4, 5, 7, 6, 3, 5, 8]

    7 和 8 交换,“> 区左扩”,i 不变。

  (5)[3, 4, 5, 8, 6, 3, 5, 7]

    8 和 5 交换,“> 区左扩”,i 不变。

  (6)[3, 4, 5, 5, 6, 3, 8, 7]

    i++。

  (7)[3, 4, 5, 5, 6, 3, 8, 7]

    6 和 3 交换,“> 区左扩”,i 不变。

  (8)[3, 4, 5, 5, 3, 6, 8, 7]

    3 和 5 交换,“< 区右扩”,i++。

  (9)最终结果为 [3, 4, 3, 5, 5, 6, 8, 7]。

 

3. 快速排序

3.1 快排 V1.0

  快排 V1.0 和荷兰问题的第一问思路类似。将数组的最后一个元素作为 num,将其前面的元素按“≤ num”和“> num”分成两个部分。划分完成后,将 num 和“> num”部分的首元素交换。其后,将划分出的两个区域分别递归,重复上述过程。

  时间复杂度为 O(N2)。

3.2 快排 V2.0

  快排 V2.0 和荷兰问题的第二问思路类似。将数组的最后一个元素作为 num,将其前面的元素按“< num”、“= num”和“> num”分成三个部分。划分完成后,将 num 和“> num”部分的首元素交换。其后,将划分出的“< num”和“> num”区域分别递归,重复上述过程。

  时间复杂度为 O(N2)。

3.3 快排 V1.0 和快排 V2.0 的时间复杂度局限性

  不难看出,快排 V1.0 和快排 V2.0 的时间复杂度依赖于 num 的选取。划分值越靠近两侧,复杂度越高。划分值越靠近中间,复杂度越低。对于极端的测试数据,譬如 [1, 2, 3, 4, 5] 这样的数组,时间复杂度为 O(N2)。

  改善快速排序的时间复杂度,就要对 num 的选取做文章。这就引出了随即快速排序。

3.4 快排 V3.0(随机快速排序)

3.4.1 随机快速排序原理

  在数组范围中,等概率随机选一个数作为划分值 num。根据这个 num 把数组按“< num”、“= num”和“> num”分成三个部分。划分完成后,将 num 和“> num”部分的首元素交换。其后,将划分出的“< num”和“> num”区域分别递归,重复上述过程。

  时间复杂度为O(N*logN)。

3.4.2 随机快速排序示例代码

 

3.5 快速排序的额外空间复杂度

  概率平均情况:O(log N)。

  最差情况:O(N)。

 

标签:02,arr,int,复杂度,元素,左神,num,Nlog,数组
来源: https://www.cnblogs.com/murongmochen/p/15143770.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有