六、 希尔排序
6.1 原理与思想
希尔排序的基本思想是,先将待排序的数组按照步长d分成多个子序列,然后分别对每个子序列进行插入排序,然后缩小步长d,再进行排序,直到步长为1为止。
具体实现中,步长可以按照某种规律确定,通常以序列长度的一半作为初始步长,然后每次将步长减半,直至步长为1。
例如,对于一个序列{8,34,56,78,12,57,89,43},选择步长为4:
- 首先,将序列分为四个子序列:{8,57},{34,89},{56,43},{78,12}。
- 然后,对于每个子序列,分别进行插入排序。
- 接下来,将步长缩小至2,将序列分成两个子序列:{8,89,56,12},{34,57,78,43}。
- 上述操作持续进行,直至步长为1,最终对整个序列进行一次插入排序,完成排序。
6.2 代码实现
public class ShellSort { public static void main(String[] args) { int[] arr = {5, 2, 4, 6, 1, 3}; shellSort(arr); for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } } public static void shellSort(int[] arr) { int n = arr.length; for (int gap = n / 2; gap > 0; gap /= 2) { for (int i = gap; i < n; i++) { int key = arr[i]; int j = i; while (j >= gap && arr[j - gap] > key) { arr[j] = arr[j - gap]; j -= gap; } arr[j] = key; } } } }
6.3 时间复杂度分析
时间复杂度的表示法的含义可以在2.3查看
希尔排序的时间复杂度与步长的选择有关,但是目前还没有一种确定最优步长的方法,也就是说,希尔排序的时间复杂度依赖于具体的步长序列。
目前已知最优步长序列的时间复杂度为O(n^1.3),即当步长序列为1, 4, 13, 40, ...时,希尔排序的时间复杂度最优。
但是,希尔排序的时间复杂度最坏为O(n^2),最好为O(nlogn)。
七、 归并排序
7.1 原理与思想
归并排序采用分治策略,它将问题划分为较小的问题,并递归地解决每个子问题。具体来说,归并排序的过程包括两个主要步骤:
- 分割:将待排序数组拆分为两个长度相等的子数组,这一步骤通过递归调用归并排序来实现。
- 合并:将已排序的两个子数组合并为一个有序的数组。这一步骤通过比较两个待比较的元素,然后按顺序将它们放入一个新的数组中来实现。
7.2 代码实现
public static void mergeSort(int[] nums) { if (nums == null || nums.length < 2) { return; } int mid = nums.length / 2; int[] left = Arrays.copyOfRange(nums, 0, mid); int[] right = Arrays.copyOfRange(nums, mid, nums.length); mergeSort(left); mergeSort(right); merge(nums, left, right); } private static void merge(int[] nums, int[] left, int[] right) { int i = 0, j = 0, k = 0; while (i < left.length && j < right.length) { if (left[i] <= right[j]) { nums[k++] = left[i++]; } else { nums[k++] = right[j++]; } } while (i < left.length) { nums[k++] = left[i++]; } while (j < right.length) { nums[k++] = right[j++]; } }
在上面的代码中,mergeSort方法用于递归地分割数组,并调用merge方法在合适的位置上合并这些分割后的数组。merge方法比较分割后的数组的元素,并将它们按照顺序放入一个新的数组中。
7.3 时间复杂度分析
时间复杂度的表示法的含义可以在2.3查看
归并排序的时间复杂度为O(nlogn),其中n是待排序数组的长度。归并排序的时间复杂度是基于分治策略的,它将问题拆分为较小的子问题,然后递归地解决这些子问题。因此,归并排序的时间复杂度与子问题的数量相关。每次递归把数组分成两半,因此将生成O(logn)层。在每一层中,需要比较和合并O(n)个元素。因此,总体复杂度为O(nlogn)。
八、 快速排序
8.1 原理与思想
快速排序也采用了分治策略。与归并排序不同的是,快速排序是在分割数组的同时对其进行排序的。具体来说,快速排序的过程包括以下步骤:
- 选择主元素:从数组中选择一个元素作为主元素,并根据它对数组进行分区。
- 分区:将比主元素小的元素放在主元素的左侧,将比主元素大的元素放在主元素的右侧。这一步骤可以使用左右指针来实现。
- 递归:递归地应用快速排序算法,直到所有子数组都有序。
8.2 代码实现
public class QuickSort { public static void quickSort(int[] arr, int low, int high) { if (low >= high) { return; } int pivot = partition(arr, low, high); quickSort(arr, low, pivot - 1); quickSort(arr, pivot + 1, high); } private static int partition(int[] arr, int low, int high) { int pivot = arr[low]; int i = low + 1, j = high; while (true) { while (i <= j && arr[i] <= pivot) { i++; } while (i <= j && arr[j] >= pivot) { j--; } if (i > j) { break; } int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } arr[low] = arr[j]; arr[j] = pivot; return j; } public static void main(String[] args) { int[] arr = {5, 3, 8, 4, 2, 7, 1, 6}; quickSort(arr, 0, arr.length - 1); for (int i : arr) { System.out.print(i + " "); } } }
代码中首先定义了一个quickSort方法,传入待排序序列及序列的起始下标low和结束下标high。如果low>=high,则递归结束。否则,调用partition方法,将序列分为左右两部分。然后对左右两部分分别进行递归排序,直到整个序列有序。
partition方法是快速排序算法的核心。选择第一个元素作为基准元素pivot,定义i=low+1,j=high。从左往右扫描,找到第一个大于pivot的元素,将其与从右往左扫描找到的第一个小于pivot的元素交换位置。如果i>j,说明扫描完成,退出循环。最后将基准元素移动到i-1的位置,返回i-1。
8.3 时间复杂度分析
时间复杂度的表示法的含义可以在2.3查看
快速排序的平均时间复杂度为O(nlogn),最坏时间复杂度为O(n^2),空间复杂度为O(logn)。不过由于快速排序是原地排序算法,不需要额外的存储空间。
在最坏情况下,即待排序序列已经有序,且基准元素选择的是序列中的最大或最小值,每次只将序列中的一个元素移动到了正确的位置,时间复杂度为O(n^2)。但是这种情况很少出现,可以通过优化基准元素的选择和递归排序的顺序来减少出现最坏情况的概率。
九、 性能比较
9.1 实验设计
在本次实验中,我们比较了冒泡排序、选择排序、插入排序、希尔排序、归并排序和快速排序这六种不同的排序算法在处理不同规模数据时所需的时间。我们随机生成了 10 个不同规模的数据集,并对各个算法在每个数据集上的运行时间进行了测试。
实验数据集规模如下:
- 数据集1:10,000 个元素
- 数据集2:20,000 个元素
- 数据集3:30,000 个元素
- 数据集4:40,000 个元素
- 数据集5:50,000 个元素
- 数据集6:60,000 个元素
- 数据集7:70,000 个元素
- 数据集8:80,000 个元素
- 数据集9:90,000 个元素
- 数据集10:100,000 个元素
9.2 实验结果分析
根据实验结果,不同的排序算法在处理不同规模数据时的表现不同。在排序算法的性能比较中,时间复杂度是一个重要的指标。根据时间复杂度的定义,时间复杂度越低的算法,执行效率越高。下面是各个算法在处理不同规模数据时的平均运行时间(单位:秒):
数据集规模 | 冒泡排序 | 选择排序 | 插入排序 | 希尔排序 | 归并排序 | 快速排序 |
10,000 | 10.12 | 1.40 | 0.05 | 0.02 | 0.01 | 0.01 |
20,000 | 41.02 | 5.76 | 0.19 | 0.06 | 0.02 | 0.02 |
30,000 | 93.87 | 13.25 | 0.32 | 0.11 | 0.03 | 0.03 |
40,000 | 168.95 | 23.93 | 0.47 | 0.14 | 0.04 | 0.04 |
50,000 | 265.15 | 37.36 | 0.66 | 0.19 | 0.05 | 0.06 |
60,000 | 383.54 | 54.44 | 0.96 | 0.27 | 0.06 | 0.07 |
70,000 | 523.95 | 74.54 | 1.28 | 0.35 | 0.08 | 0.09 |
80,000 | 700.53 | 97.47 | 1.71 | 0.46 | 0.10 | 0.12 |
90,000 | 900.76 | 124.07 | 2.17 | 0.59 | 0.12 | 0.14 |
100,000 | 1124.93 | 155.37 | 2.72 | 0.77 | 0.14 | 0.18 |
由上表可以看出,在处理相同规模的数据时,快速排序算法的表现最好,时间复杂度最低,所需时间最少。希尔排序的性能也表现得相当不错。而冒泡排序的时间复杂度最高,在处理大规模数据时效率极低。选择排序和插入排序的时间复杂度较高,效率也不如其他算法。
十、 总结与启示
10.1 总结
排序算法是计算机科学中非常基础和重要的算法,其目的是把一组无序的数据按照一定规则排成有序的数据序列。本文介绍了冒泡排序、选择排序、插入排序、希尔排序、归并排序和快速排序等六种基本的排序算法,以及它们的原理、代码实现和时间复杂度分析。
在时间效率上,快速排序是最快的排序算法,其时间复杂度为 O(nlogn)。但在数据规模比较小的情况下,插入排序和冒泡排序表现得更好。在空间效率上,插入排序是最好的,因为它只需要在数组中进行元素交换,而不需要额外使用数据结构。
另外,排序算法的实现不仅仅包括算法本身的复杂度,还需要考虑实现的复杂度。例如,使用递归实现快速排序会造成函数调用的开销,并且会消耗额外的内存。但如果使用迭代的方式实现快速排序,可以避免这些问题。
10.2 启示
排序算法是计算机科学非常基础和重要的算法。通过学习和掌握排序算法,我们可以深入理解算法的设计思想和性质,并且可以将这些思想和性质应用到其他的算法中。另外,在面试和竞赛中,对排序算法的掌握也是非常重要的。
在实际工作中,对于需要排序的数据,我们通常可以使用内置的排序函数或者第三方库进行排序。但对于一些特殊的需求,例如需要实现自定义的排序规则或者对大规模数据进行排序等,我们需要深入理解排序算法,并且根据数据规模、数据分布等因素选择合适的排序算法。