数据结构和算法分析 排序
交换次数和比较次数是衡量一部分排序算法的复杂度的
排序例子 O(N2)
这是一个人人都能实现的例子。通过循环判断,将最小的元素放在最前面。
-
public static void sort(int[] arr) {
-
// 每次对比下标i到arr.length-1的值 然后将最小值放在i下标的位置
-
for (int i = 0; i < arr.length; i++) {
-
// 记录最小值的下标
-
int minIndex = i;
-
for (int j = i; j < arr.length; j++) {
-
if (arr[j] < arr[minIndex]) {
-
minIndex = j;
-
}
-
}
-
// 交换最小值与i
-
if(i != minIndex)
-
{
-
System.out.println("交换");
-
int temp = arr[i];
-
arr[i] = arr[minIndex];
-
arr[minIndex] = temp;
-
}
-
}
-
}
这个排序比较的次数是N*(N-1)/2次,交换的次数取决于原始数组和排序后的数组的重合程度(下标相同且数值相同),这基本是随机的,实际应用中不会有巧合的重合。时间复杂度为O(N2)。这种排序基本书上都不会讲,没有意义。
冒泡排序O(N2)
-
public static void bubblingSort(int[] arr)
-
{
-
for(int i = 0; i < arr.length; i++)
-
{
-
for(int j = 0; j < arr.length - i - 1; j++)
-
{
-
if(arr[j] > arr[j+1])
-
{
-
//交换
-
int temp = arr[j];
-
arr[j] = arr[j+1];
-
arr[j+1] = temp;
-
}
-
}
-
-
}
-
}
这个排序比较的次数是N*(N-1)/2次,交换的次数取决于原始数组的有序程度。如果原始数组就是有序的,那么交换次数为0。如果原始数组是随机的,交换次数也不会太大,因为每一个循环都在讲数组趋于有序,越往后交换的次数越少。
时间复杂度为O(N2)。
插入排序O(N2)
插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
-
public static void insertSort(int[] arr) {
-
int j, key;
-
int len = arr.length;
-
for (int i = 1; i < len; i++) {
-
key = arr[i];
-
//将第i个元素 插入到已经有序的前i-1个序列中
-
//查询插入位置的时候 从i-1往前进行遍历 不能反过来 因为此处不仅需要遍历 还需要往后平移
-
for (j = i - 1; j >= 0; j--) {
-
//没有找到插入位置 证明插入位置在前面 那么需要往后平移一位 方便前面的插入
-
//第一次平移会平移到i处
-
if (arr[j] > key)
-
{
-
arr[j + 1] = arr[j];
-
}
-
else
-
{
-
//找到插入位置 不再往前遍历了
-
break;
-
}
-
}
-
//插入到位置
-
arr[j + 1] = key;
-
}
-
}
可以写的更简单一点:
-
public static void insertSort(int[] arr) {
-
int j, key;
-
int len = arr.length;
-
for (int i = 1; i < len; i++) {
-
key = arr[i];
-
for (j = i - 1; j >= 0&&arr[j] > key; j--) {
-
arr[j + 1] = arr[j];
-
}
-
arr[j + 1] = key;
-
}
-
}
这个排序比较的次数取决于原始数组的有序程度,最差是N*(N-1)/2次,如果原始数组是有序的,那么可以达到N。
交换的次数也取决于原始数组的有序程度。如果原始数组就是有序的,那么交换次数为0。
所以如果预先就是排好序的,那么它的时间时间复杂度可以达到O(N)。
相对于复杂的Nlog(N)排序算法,数据量比较小而且基本有序的集合比较适合这些排序算法。
上面的这种插入排序也叫直接插入排序,还有:
折半插入排序:其它部分不变,只是寻找插入位置的时候,使用折半查找算法。
希尔算法:下一章节讲述。
希尔算法O(N2)
希尔算法是基于插入排序的一下两点性质而提出改进方法的:
*插入排序在对几乎已经排好的数据操作时,效率高,即可达到线性排序的效率;
*但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:
把记录按步长 gap 分组,对每组记录采用直接插入排序方法进行排序。
随着步长逐渐减小,所分成的组包含的记录越来越多,当步长的值减小到 1 时,整个数据合成为一组,构成一组有序记录,则完成排序。
-
public static void shellSort(int[] arr) {
-
int len = arr.length;
-
int i,j;
-
//循环定义步长
-
for (int gap = len / 2; gap > 0; gap = gap / 2) {
-
//从i=gap开始 因为gap之前的序列都只有一个值 是排序好的
-
for(i = gap; i < len; i++)
-
{
-
int temp = arr[i];
-
for(j = i ; j>=gap&&temp < arr[j-gap]; j = j - gap)
-
{
-
arr[j] = arr[j-gap];
-
}
-
arr[j] = temp;
-
}
-
}
-
}
希尔排序相对于直接插入排序的优势:
{ 2, 5, 8, 4, 6, 1, 9 } 我们要把1移动到最前面,如果使用直接插入排序和折半插入排序(折半插入排序只能减少插入插入点的对比次数,不能减少交换次数),都需要交换5次之多。
如果使用希尔排序,第一次交换后1和8互换,第二次交换后就会到首位。只需要两次交换。
希尔排序的增量序列和时间复杂度:
- 希尔增量序列:首先,我们上面演示的例子使用的增量序列叫希尔增量序列。它的最坏情况下的复杂度为O(N2)。
- Hibbard 增量序列:hi=2i−1,Hibbard 增量序列的递推公式为: h1=1,hi=2∗hi−1+1,Hibbard 增量序列的最坏时间复杂度为 Θ(N3/2);平均时间复杂度约为 O(N5/4)。
- Sedgewick 增量序列,通项公式为: hi=max(9∗4j−9∗2j+1,4k−3∗2k+1),Sedgewick 增量序列的最坏时间复杂度为 O(N4/3);平均时间复杂度约为 O(N7/6)。
Sedgewick 增量序列是目前表现最好的增量序列,但是随着数学的研究,可能会有更好的增量序列出现。
对希尔排序的不同增量序列的复杂度研究设计比较多的数学公式,我就不研究了。
希尔算法的性能在实践中是可以完全接受的,即使对数以万计的N也是如此,所以如果数据量不是非常大,希尔算法的使用还是非常广泛的。