当前位置:   article > 正文

四个时间复杂度为nlogn的排序算法

复杂度为nlogn的排序算法

三个n^2级的算法icon-default.png?t=N7T8http://t.csdnimg.cn/oQSSL

 本周分享四个时间复杂度为nlogn的算法:

  1. 希尔排序
  2. 堆排序
  3. 快速排序
  4. 归并排序

1.希尔排序

1959 年 7 月,美国辛辛那提大学的数学系博士 Donald Shell 在 《ACM 通讯》上发表了希尔排序算法,成为首批将时间复杂度降到 O(n^2) 以下的算法之一。虽然原始的希尔排序最坏时间复杂度仍然是 O(n^2) ,但经过优化的希尔排序可以达到 O(n^{1.3}) 甚至 O(n^{7/6})。

希尔排序虽然现在用到的地方很少,但之所以仍然讲解,是因为理解了希尔排序之后,能够快速地其他排序算法(学习本质上是对思维的锻炼,而不是具体的知识);

希尔排序本质上是对插入排序的一种优化,将插入排序从相邻元素的交换改变成特定下标的元素交换。它利用了插入排序的简单,又克服了插入排序每次只交换相邻两个元素的缺点。从本质上来说希尔排序是特定间隔的多次插入排序。

基本思想

● 将待排序数组按照一定的间隔分为多个子数组,对每组分别进行插入排序。(间隔分组不是取连续的一段数组,而是每跳跃一定间隔取一个值组成一组,而我们的插入排序也是对这一个间隔数组进行的插入排序)

● 逐渐缩小间隔进行下一轮排序

● 最后一轮时,取间隔为 1,也就相当于直接使用插入排序。但这时经过前面的基本排序之后,此时数组基本有序了,所以此时的插入排序只需进行少量交换便可完成

举个例子,对数组 [84, 83, 88, 87, 61, 50, 70, 60, 80, 99] 进行希尔排序的过程如下:

● 第一遍(55 间隔排序):按照间隔 55 分割子数组,共分成五组,分别是 [84,50],[83,70],[88,60],[87,80],[61,99]。对它们进行插入排序,排序后它们分别变成: [50, 84], [70, 83], [60, 88], [80, 87], [61, 99],此时整个数组变成 [50,70,60,80,61,84,83,88,87,99]

● 第二遍(22 间隔排序):按照间隔 22 分割子数组,共分成两组,分别是 [50, 60, 61, 83, 87], [70, 80, 84, 88, 99]。对他们进行插入排序,排序后它们分别变成: [50, 60, 61, 83, 87], [70, 80, 84, 88, 99],此时整个数组变成 [50,70,60,80,61,84,83,88,87,99]。这里有一个非常重要的性质:当我们完成 22 间隔排序后,这个数组仍然是保持 55 间隔有序的。也就是说,更小间隔的排序能够不断地将上一次排序的结果进行优化,使数组逐渐回归有序

● 第三遍(11 间隔排序,等于直接插入排序):按照间隔 11 分割子数组,分成一组,也就是整个数组。对其进行插入排序,经过前两遍排序,数组已经基本有序了,所以这一步只需经过少量交换即可完成排序。排序后数组变成 [50,60,61,70,80,83,84,87,88,99],整个排序完成。

代码实现

  1. public static void shellSort01(int[] nums) {
  2. //确定间隔系列 即希尔增量序列,这里是数组长度不断除以2,直到间隔为1,进行一个整体的插入排序;
  3. for (int gap = nums.length / 2; gap >= 1; gap = gap / 2) {
  4. //使用插入排序中的位移法对每一分组进行排序;
  5. //插入排序的起始位置是gap,即从第一个间隔后面的元素开始,向前进行的插入排序;
  6. for (int currentIndex = gap; currentIndex < nums.length; currentIndex++) {
  7. int currentValue = nums[currentIndex];
  8. int preIndex = currentIndex - gap;//通过当前的指向的下标,获取同一组(gap间隔)的元素下标
  9. //只要当前下标currentIndex的前一个间隔下标preIndex大于等于0并且前一个下标的元素大于当前元素,才进行位移的操作,为当前currentIndex下标指向的元素寻找合适的位置
  10. while (preIndex >= 0 && nums[preIndex] > currentValue) {
  11. nums[preIndex + gap] = nums[preIndex];
  12. preIndex -= gap;
  13. }
  14. //这里使用preIndex+gap的原因是:如果没有进入上方的while循环,preIndex+gap = currentIndex
  15. //如果进入了while循环,while循环都会执行preIndex-=gap的操作,这时候导致我们的下标指向了前一个间隔的函数,这时候我们需要向后移动一个间隔,指向正确的插入位置
  16. nums[preIndex + gap] = currentValue;
  17. }
  18. }
  19. }

分析

每一遍排序的间隔在希尔排序中被称之为增量,所有的增量组成的序列称之为增量序列,也就是本例中的 [5, 2, 1]。增量依次递减,最后一个增量必须为 1,所以希尔排序又被称之为「缩小增量排序」。这里的增量序列选取是通过数组长度/2,之后在这基础上不断除以2。

增量序列的选定会极大地影响希尔排序的效率。增量序列如果选得不好,希尔排序的效率可能比插入排序效率还要低,举个例子:

像这一个例子,以8、4、2为间隔的排序都没有起作用,真正对起作用的是1间隔的排序,这时候的时间复杂度要大于直接使用插入排序的时间复杂度。

原因就在于,我们选择的增量序列之间存在着公因子,不互相为质数,这样导致小的增量根本不起作用,在排序时出现一些不必要的重复操作,从而影响排序算法的性能。

一般来说,为了保证希尔排序的效率,我们希望选择的增量序列中的元素是互质的,这样可以最大程度地减少不必要的比较和交换操作,提高排序的效率。

代码优化

这里推荐增量选取使用 knuth 增量序列:D_1 = 1; D_{k+1} = 3 * D_k + 1,也就是 1,4,13,40,...,数学界猜想它的平均时间复杂度为 O(n^{3/2});

使用 Knuth 序列进行希尔排序的代码如下:

  1. public static void shellSortByKnuth(int[] arr) {
  2. // 找到当前数组需要用到的 Knuth 序列中的最大值
  3. int maxKnuthNumber = 1;
  4. while (maxKnuthNumber <= arr.length / 3) {
  5. maxKnuthNumber = maxKnuthNumber * 3 + 1;
  6. }
  7. // 增量按照 Knuth 序列规则依次递减
  8. for (int gap = maxKnuthNumber; gap > 0; gap = (gap - 1) / 3) {
  9. // 从 gap 开始,按照顺序将每个元素依次向前插入自己所在的组
  10. for (int i = gap; i < arr.length; i++) {
  11. // currentNumber 站起来,开始找位置
  12. int currentNumber = arr[i];
  13. // 该组前一个数字的索引
  14. int preIndex = i - gap;
  15. while (preIndex >= 0 && currentNumber < arr[preIndex]) {
  16. // 向后挪位置
  17. arr[preIndex + gap] = arr[preIndex];
  18. preIndex -= gap;
  19. }
  20. // currentNumber 找到了自己的位置,坐下
  21. arr[preIndex + gap] = currentNumber;
  22. }
  23. }
  24. }

时间-空间复杂度分析

时间复杂度分析:希尔排序时间复杂度非常难以分析,它的平均复杂度界于 O(n) 到 O(n^2) 之间,普遍认为它最好的时间复杂度为 O(n^{1.3})。不同的增量序列会有不同的时间复杂度。

空间复杂度:采用的都是常数级的临时变量,所以空间复杂度为O(1)

堆排序

前置知识

堆:符合以下两个条件之一的完全二叉树:

● 根节点的值 ≥ 子节点的值,这样的堆被称之为最大堆,或大顶堆;

● 根节点的值 ≤ 子节点的值,这样的堆被称之为最小堆,或小顶堆。

完全二叉树:

若一棵二叉树的所有叶子节点都在最后一层或者倒数第二层,并且最后一层的叶子节点左边连续,倒数第二层的叶子节点右边连续,则我们称此二叉树为完全二叉树。

●   对于完全二叉树中的第 i 个数,它的左子节点下标:left = 2i + 1

●   对于完全二叉树中的第 i 个数,它的右子节点下标:right = left + 1

●   对于有 n 个元素的完全二叉树(n≥2)(n≥2),它的最后一个非叶子结点的下标:n/2 - 1(n为对应数组的长度)

完全二叉树与数组的对应关系图

基本思想

1. 用数组构建出一个大顶堆,取出堆顶的数字;

2. 调整剩余的数字,构建出新的大顶堆,再次取出堆顶的数字;

3. 循环往复,完成整个排序。

堆排序并不是使用二叉树进行排序,而是在使用数组去模拟二叉树的结构,是一些特定的数组范围进行排序。

构建大顶堆的过程是很多初学者不明白的,其实大顶堆的构建非常简单,我们可以将这个数组的初始状态看成是一个完全二叉树,从最后一个非叶子节点开始自下而上地构建大顶堆,当我们依次向上将每一个子树都构建成大顶堆,构建到根节点的时候,数组最终形成了一个大顶堆结构。

如图所示,我们构建以3为根节点的大顶堆,再构建以2为根节点的大顶堆,最后构建以1为节点的大顶堆。其实,这一个过程就是我们的基本思想中第二步,调整剩余数组,重新构建大顶堆。值得注意的是,我们在进行大顶堆构架的时候,一定要注意交换元素后的对于其左右子树的影响,一定要保持住大顶堆的结构。

动图演示如下:

代码实现

  1. public class heapSortDemo {
  2. public static void main(String[] args) {
  3. int[] nums = {5, 0, 1, 9};
  4. heapSort(nums);
  5. System.out.println(Arrays.toString(nums));
  6. }
  7. public static void heapSort(int[] nums) {
  8. // 1.构建大顶堆
  9. constructMaxHeap(nums);
  10. /* 2.大顶堆构建完成,之后将堆顶元素依次同末尾元素进行交换即可。
  11. 注意:一旦发生了交换,构建大堆顶一定是有范围的,不再是全体数组了,不然永远都是值比较大的元素同其叶子节点的元素进行交换;*/
  12. for (int i = nums.length - 1; i >= 0; i--) {
  13. swap(nums, 0, i);
  14. maxHeapify(nums, 0, i);
  15. }
  16. }
  17. /**
  18. * 构建堆的过程就是实现一个特定结构的降序排列 类似于希尔排序
  19. * 我们采用自下而上的构建方式,从二叉树的最后一个非叶子节点开始构建
  20. * 构建的过程注意,后一次元素交换后,对于其叶子节点的影响,要同叶子节点进行比较。
  21. */
  22. public static void constructMaxHeap(int[] nums) {
  23. for (int i = nums.length / 2 - 1; i >= 0; i--) {
  24. maxHeapify(nums, i, nums.length);
  25. }
  26. }
  27. /**
  28. * 堆中每一个节点的初始化操作,以节点、子树为单位堆的构建。
  29. * 这是一个根据当前的节点,构建最大堆的方法
  30. * @param nums 待排序的数组
  31. * @param currentIndex 当前构建堆的下标
  32. * @param heapSize 本次排序的数组范围
  33. */
  34. public static void maxHeapify(int[] nums, int currentIndex, int heapSize) {
  35. int leftIndex = 2 * currentIndex + 1;
  36. int rightIndex = 2 * currentIndex + 2;
  37. // 记录根结点、左子树结点、右子树结点三者中的最大值下标
  38. int largest = currentIndex;
  39. // 与左子树结点比较
  40. if (leftIndex < heapSize && nums[leftIndex] > nums[largest]) {
  41. largest = leftIndex;
  42. }
  43. // 与右子树结点比较
  44. if (rightIndex < heapSize && nums[rightIndex] > nums[largest]) {
  45. largest = rightIndex;
  46. }
  47. if (largest != currentIndex) {
  48. // 将最大值交换为根结点
  49. swap(nums, currentIndex, largest);
  50. // 再次调整交换数字后的大顶堆
  51. maxHeapify(nums, largest, heapSize);
  52. }
  53. }
  54. public static void swap(int[] nums, int i, int j) {
  55. int temp = nums[i];
  56. nums[i] = nums[j];
  57. nums[j] = temp;
  58. }
  59. }

分析

大顶堆构建完成之后,堆顶元素同末尾元素的交换,一定要进行数组范围的缩减,这里是使用heapSize进行控制的。

时间-空间复杂度

堆排序总的时间复杂度为 O(nlog⁡n)。

堆排序的空间复杂度为 O(1),只需要常数级的临时变量。

堆排序和选择排序一样,是对数组进行部分排序,每次排好一个最大值或者最小值。适合寻找第几大的值这样的算法。

快速排序

快速排序算法由 C. A. R. Hoare 在 1960 年提出。它的时间复杂度也是 O(nlogn),但它在时间复杂度为 O(nlogn) 级的几种排序算法中,大多数情况下效率更高,所以快速排序的应用非常广泛。再加上快速排序所采用的分治思想非常实用,使得快速排序深受面试官的青睐,所以掌握快速排序的思想尤为重要。

基本思想

1. 从数组中取出一个数,称之为基数(pivot)

2. 遍历数组,将比基数大的数字放到它的右边,比基数小的数字放到它的左边。遍历完成后,数组被分成了左右两个区域

3. 将左右两个区域视为两个数组,重复前两个步骤,直到排序完成

pivot中文意思是轴,每一次数组的遍历就是把基数左边的数字大于基数的数字,旋转到右边;把基数右边小于基数的数字旋转到左边,形成一个基数左边都小于等于基数,右边都大于基数的数组结构。【pivot的选取是快速排序算法的核心,快速排序算法的优化也都大都围绕着pivot轴的选取pivot的增加进行。所谓双轴快排就是一次选取两个基数,将数组分为三个区域进行旋转】

对于pivot轴的选择:

基数的选择没有固定标准,随意选择区间内任何一个数字做基数都可以。

通常来讲有三种选择方式:

● 选择第一个元素作为基数 【入门推荐,方便理解,本文依次为例】

● 选择最后一个元素作为基数

● 选择区间内一个随机元素作为基数 【优化推荐,降低最坏情况出现的概率】

代码实现

  1. public class quickSortDemo {
  2. public static void main(String[] args) {
  3. int[] nums = {6, 2, 1, 3, 5, 5, 1, 4};
  4. // 为了方便调用,书写的重载方法
  5. quickSort(nums);
  6. System.out.println(Arrays.toString(nums));
  7. }
  8. public static void quickSort(int[] nums) {
  9. quickSort(nums, 0, nums.length - 1);
  10. }
  11. public static void quickSort(int[] nums, int start, int end) {
  12. //什么时候退出递归,当本次快速排序的数组元素个数为0或者为1的时候,文章下方有解释
  13. if (start >= end) return;
  14. int middle = partitionRandom(nums, start, end);
  15. quickSort(nums, start, middle - 1);
  16. quickSort(nums, middle + 1, end);
  17. }
  18. // 将 待排序数组arr 从 start 到 end 分区,左边区域比基数小,右边区域比基数大,然后返回中间值的下标
  19. public static int partition(int[] arr, int start, int end) {
  20. // 取第一个数为基数
  21. int pivot = arr[start];
  22. // 从第二个数开始分区,左边界
  23. int left = start + 1;
  24. // 右边界
  25. int right = end;
  26. // left、right 相遇时退出循环
  27. while (left < right) {
  28. // 找到第一个大于基数的位置
  29. while (left < right && arr[left] <= pivot) left++;
  30. // 交换这两个数,使得左边分区都小于或等于基数,右边分区大于基数
  31. //这里会出现left和right相等的情况,上面的while循环导致的,这里我们不对这种情况进行判断
  32. if (left != right) {
  33. swap(arr, left, right);
  34. right--;
  35. }
  36. }
  37. // 如果 left 和 right 相等,单独比较 arr[right] 和 pivot
  38. // 如果arr[right] > pivot 的话,我们需要让right--,直接进行交换的话,会导致交换后的左区第一个数大于pivot,
  39. // 而right--后的元素一定是小于pivot的,这是我们之前遍历过的。
  40. //如果arr[right] <= pivot 的话,我们可以直接交换操作
  41. if (left == right && arr[right] > pivot) right--;
  42. // 将基数和中间数交换,如果到头了,就不需要交换,此时start(pivot)右边的数全部大于pivot
  43. if (right != start) swap(arr, start, right);
  44. // 返回中间值的下标
  45. return right;
  46. }
  47. }

分析

对于递归退出的边界,这里为什么是start >= end呢?

递归的数组元素个数为0或者为1的时候退出递归。因为这时候进行任何排序都是没有任何意义的。最原始的条件应该是下方代码这样的。

  1. public static void quickSort(int[] nums, int start, int end) {
  2. int middle = partitionRandom(nums, start, end);
  3. // 当左边区域中至少有 2 个数字时,对左边区域快速排序
  4. if(start != middle && start != middle-1) quickSort(nums, start, middle - 1);
  5. // 当右边区域中至少有 2 个数字时,对右边区域快速排序
  6. if(end != middle+1 && end != middle) quickSort(nums, middle + 1, end);
  7. }

我们对边界进行简化:

  • 当进行左边区域的递归时

如果start == middle 表示将要递归的数组范围内元素的个数为0;

如果start == middle-1表示将要递归的数组范围内元素的个数为1 ;

我们带着这样的条件进入递归中,可以得出start和end的关系

当start == middle时,进行递归后,start=start,middle-1=end---> start = end+1;

当start == middle-1时,进入递归后,start=start,end=middle-1--->start = end;

  • 当进行右边区域的递归时

如果end == middle 表示将要递归的数组范围内元素的个数为0;

如果end == middle+1 表示将要递归的数组范围内元素的个数为1;

我们带着这样的条件进入递归中,可以得出start和end的关系

当end== middle时,进行递归后,start=middle+1,end=end---> start = end+1;

当end == middle+1时,进入递归后,start=middle+1,end=end--->start = end;

综上所述,进入递归后的终止条件可以改变成 start!=end || start!=end+1。

根据我们的程序设定,middle>=start&&middle<=end一定成立,除了start==end||start==end+1外,其他情况下start都小于end,所以我们可以将这一个判断条件再次改为,只要left>=right就退出递归。由于我们是在下一次递归中进行这一系列的推理。递归的终止条条件应该放到partition前面,递归退出的条件改变为

● start == end: 表明区域内只有一个数字
● start == end + 1: 表明区域内一个数字也没有,

  1. public static void quickSort(int[] arr, int start, int end) {
  2. // 如果区域内的数字少于 2 个,退出递归
  3. if (start >= end) return;
  4. // 将数组分区,并获得中间值的下标
  5. int middle = partition(arr, start, end);
  6. // 对左边区域快速排序
  7. quickSort(arr, start, middle - 1);
  8. // 对右边区域快速排序
  9. quickSort(arr, middle + 1, end);
  10. }

优化

双指针分区算法

  1. public static int doublePointerPartition1(int[] arr, int start, int end) {
  2. int pivot = arr[start];
  3. int left = start + 1;
  4. int right = end;
  5. while (left <= right) {
  6. //从left开始寻找大于pivot的值
  7. while (left <= right && arr[left] < pivot) left++;
  8. //从right开始寻找大于pivot的值
  9. while (left <= right && arr[right] >= pivot) right--;
  10. if (left <= right) {
  11. swap(arr, left, right);
  12. left++;
  13. right--;
  14. }
  15. }
  16. if (start != right) swap(arr,start,right);
  17. return right;
  18. }

归并排序

1945 年,约翰·冯·诺伊曼提出了归并排序。归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法的一个非常典型的应用。

基本思想

归并排序主要由两个环节组成:拆分-合并

拆分环节:将长度为n的数组不断进行拆分,直到拆分成长度为1的数组。

合并环节:将拆分成长度为1的数组不断地进行特定排序的合并,直到数组的长度为n。

为什么我们要将数组拆分成长度为1的数组才进行合并操作:

当我们将数组拆分成长度为1的数组,这时候就没有了有序无序的概念。这时候我们就可以将两个长度为1的数组合并成有序数组。

代码实现

  1. public class MergerSortDemo {
  2. public static void main(String[] args) {
  3. int[] nums = new int[]{6, 2, 5, 9, 7, 1, 4, 8};
  4. mergeSort(nums);
  5. System.out.println(Arrays.toString(nums));
  6. }
  7. public static void mergeSort(int[] nums) {
  8. if (nums.length == 1) return;
  9. //我们将归并排序的结果返回成一个新的数组;
  10. int[] result = mergeSort(nums, 0, nums.length - 1);
  11. //通过for循环将排序好的结果赋值给原数组
  12. for (int i = 0; i < result.length; i++) {
  13. nums[i] = result[i];
  14. }
  15. }
  16. private static int[] mergeSort(int[] nums, int start, int end) {
  17. //归并排序的终止条件是什么?本次拆分只有一个元素 [向下到底]
  18. if (start == end) return new int[]{nums[start]};
  19. int middle = (start + end) / 2;
  20. //向左进行分区递归
  21. int[] left = mergeSort(nums, start, middle);
  22. //向右进行分区递归
  23. int[] right = mergeSort(nums, middle + 1, end);
  24. //左右都分好了,进行合并的操作 [向上返回]
  25. return merge(left, right);
  26. }
  27. private static int[] merge(int[] left, int[] right) {
  28. int[] result = new int[left.length + right.length];
  29. int index1 = 0, index2 = 0;
  30. while (index1 < left.length && index2 < right.length) {
  31. if (left[index1] < right[index2]) result[index1 + index2] = left[index1++];
  32. else if (right[index2] < left[index1]) result[index1 + index2] = right[index2++];
  33. }
  34. while (index1 < left.length) result[index1 + index2] = left[index1++];
  35. while (index2 < right.length) result[index1 + index2] = right[index2++];
  36. return result;
  37. }
  38. }

分析

数组的拆分和合并过程可以使用二叉树来进行模拟,我们的拆分过程是向下递归,我们的合并过程是向上返回。我们对数组的拆分顺序类似于二叉树的前序遍历,直到数组的能够并拆分成两个长度范围为1的左右区域时,我们才进行合并的操作。

优化

归并排序的优化体现在空间复杂度的优化上面。在上面的代码中,我们每一次递归都返回left或者right数组,不断地开辟空间。为了减少在递归过程中不断开辟空间的问题,我们可以在归并排序之前,先开辟出一个临时空间,在递归过程中统一使用此空间进行归并即可。

  1. class UpdatedMergerSort {
  2. public static void main(String[] args) {
  3. int[] nums = new int[]{6, 2, 5, 9, 7, 1, 4, 8};
  4. mergeSort(nums);
  5. System.out.println(Arrays.toString(nums));
  6. }
  7. public static void mergeSort(int[] nums) {
  8. if (nums.length == 1) return;
  9. int[] result = new int[nums.length];
  10. mergeSort(nums, 0, nums.length - 1, result);
  11. }
  12. /**
  13. * 归并算法进行递归拆分的主要步骤:
  14. * @param nums 待排序数组
  15. * @param start 待排序数组起始
  16. * @param end 待排序数组结尾
  17. * @param result 多余空间,方便进行归并
  18. */
  19. private static void mergeSort(int[] nums, int start, int end, int[] result) {
  20. if (start == end) return;
  21. int middle = (start + end) / 2;
  22. // 左边区域进行归并排序
  23. mergeSort(nums, start, middle, result);
  24. // 右边区域进行归并排序
  25. mergeSort(nums, middle + 1, end, result);
  26. //对左区和右去的部分数组进行向上归并
  27. merge(nums, start, end, result);
  28. // 在合并操作之后,我们要对已经排好序的数组返回进行更新,不然会导致我们在利用本次排序的结果是出现错误。
  29. for (int i = start; i <= end; i++) {
  30. nums[i] = result[i];
  31. }
  32. }
  33. /**
  34. * 并排的核心算法,用于合并两个范围的元素合并成一个数组;
  35. *
  36. * @param nums 待排序数组
  37. * @param start 起始
  38. * @param end 终点
  39. * @param result 结果储存空间,用于存放数组
  40. * 1.未优化前是数组下标同数组的长度进行比较的。
  41. * 2.优化后数组下标同数组的下标进行比较的,所以这个时候需要在判断条件上加上==
  42. */
  43. private static void merge(int[] nums, int start, int end, int[] result) {
  44. int middle = (start + end) / 2;
  45. int start1 = start, end1 = middle; // 左边数组的范围
  46. int start2 = middle + 1, end2 = end;// 右边数组的范围
  47. int index1 = start1,index2 = start2;// 左边右边数组的下标
  48. int index = start1;//result数组的下标
  49. while (index1 <= end1 && index2 <= end2){
  50. if (nums[index1] <= nums[index2]) result[index++] = nums[index1++];
  51. else if (nums[index2] < nums[index1]) result[index++] = nums[index2++];
  52. }
  53. while(index1 <= end1) result[index++] = nums[index1++];
  54. while(index2 <= end2) result[index++] = nums[index2++];
  55. }
  56. }

时间复杂度 & 空间复杂度

归并排序的复杂度比较容易分析,拆分数组的过程中,会将数组拆分 logn 次,每层执行的比较次数都约等于 n 次,所以时间复杂度是 O(nlogn)。

空间复杂度是 O(n),主要占用空间的就是我们在排序前创建的长度为 n的 result 数组。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/67380
推荐阅读
相关标签
  

闽ICP备14008679号