本博文向大家介绍了插入排序的三种实现:直接插入排序,二分查找插入排序,希尔排序。详细分析的其实现过程、时间复杂度和空间复杂度、稳定性以及优化改进策略。最后简单的做了下性能测试。
直接插入排序
(一)概念及实现
直接插入排序的原理:先将原序列分为有序区和无序区,然后再经过比较和后移操作将无序区元素插入到有序区中。
具体如下(实现为升序):
设数组为a[0…n]。
1. 将原序列分成有序区和无序区。a[0…i-1]为有序区,a[i…n] 为无序区。(i从1开始)
2. 从无序区中取出第一个元素,即a[i],在有序区序列中从后向前扫描。
3. 如果有序元素大于a[i],将有序元素后移到下一位置。
4. 重复步骤3,直到找到小于或者等于a[i]的有序元素,将a[i]插入到该有序元素的下一位置中。
5. 重复步骤2~4,直到无序区元素为0。
实现代码:
public static void Sort<T>(IList<T> arr) where T : IComparable<T>
{
if (arr == null)
throw new ArgumentNullException("arr");
int length = arr.Count;
if (length > 1)
{
int i, j;
// 将arr分成有序区和无序区,初始有序区有一个元素
// 0-(i-1) 为有序区;i-(length-1)为无序区 (i从1开始)
for (i = 1; i < length; i++)
{
T temp = arr[i];
// 边找位置边后移元素
for (j = i - 1; j >= 0 && arr[j].CompareTo(temp) > 0; j--)
arr[j + 1] = arr[j]; // 如果已排序的元素大于新元素,将该元素移到下一位置
// 将 arr[i] 放到正确位置上
arr[j + 1] = temp;
}
}
}
二)算法复杂度
1. 时间复杂度:O(n^2)
直接插入排序耗时的操作有:比较+后移赋值。时间复杂度如下:
1) 最好情况:序列是升序排列,在这种情况下,需要进行的比较操作需(n-1)次。后移赋值操作为0次。即O(n)
2) 最坏情况:序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。后移赋值操作是比较操作的次数加上 (n-1) 次。 即O(n^2)
3) 渐进时间复杂度(平均时间复杂度):O(n^2)
2. 空间复杂度:O(1)
从实现原理可知,直接插入排序是在原输入数组上进行后移赋值操作的(称“就地排序”),所需开辟的辅助空间跟输入数组规模无关,所以空间复杂度为:O(1)
(三)稳定性
直接插入排序是稳定的,不会改变相同元素的相对顺序。
(四)优化改进
1. 二分查找插入排序:因为在一个有序区中查找一个插入位置,所以可使用二分查找,减少元素比较次数提高效率。
2. 希尔排序:如果序列本来就是升序或部分元素升序,那么比较+后移赋值操作次数就会减少。希尔排序正是通过分组的办法让部分元素升序再进行整个序列排序。(原因是,当增量值很大时数据项每一趟排序需要的个数很少,但数据项的距离很长。当增量值减小时每一趟需要和动的数据增多,此时已经接近于它们排序后的最终位置。)
下面来分别介绍:二分查找插入排序和希尔排序
二分查找插入排序
(一)概念及实现
二分查找插入排序的原理:是直接插入排序的一个变种,区别是:在有序区中查找新元素插入位置时,为了减少元素比较次数提高效率,采用二分查找算法进行插入位置的确定。
具体如下(实现为升序):
设数组为a[0…n]。
1. 将原序列分成有序区和无序区。a[0…i-1]为有序区,a[i…n] 为无序区。(i从1开始)
2. 从无序区中取出第一个元素,即a[i],使用二分查找算法在有序区中查找要插入的位置索引j。
3. 将a[j]到a[i-1]的元素后移,并将a[i]赋值给a[j]。
4. 重复步骤2~3,直到无序区元素为0。
实现代码:
/// <summary>
/// 二分查找插入排序
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="arr"></param>
public static void BinarySort<T>(IList<T> arr) where T : IComparable<T>
{
if (arr == null)
throw new ArgumentNullException("arr");
int length = arr.Count;
if (length > 1)
{
int i, j;
// 将arr分成有序区和无序区,初始有序区有一个元素
// 0-(i-1) 为有序区;i-(length-1)为无序区
for (i = 1; i < length; i++)
{
// 二分查找在有序区寻找插入的位置
int index = BinarySearchIndex<T>(arr, i - 1, arr[i]);
if (i != index)
{
T temp = arr[i];
// 后移元素,腾出arr[index]位置
for (j = i - 1; j >= index; j--)
arr[j + 1] = arr[j];
// 将 arr[i] 放到正确位置上
arr[index] = temp;
}
}
}
}
/// <summary>
/// 二分查找要插入的位置得Index
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="arr">数组</param>
/// <param name="maxIndex">有序区最大索引</param>
/// <param name="data">待插入值</param>
/// <returns>插入的位置的Index</returns>
private static int BinarySearchIndex<T>(IList<T> arr, int maxIndex, T data) where T : IComparable<T> {
int start = 0;
int end = maxIndex;
int middle = -1;
while (start<=end)
{
middle = (start + end) / 2;
if (arr[middle].CompareTo(data) > 0)
{
end = middle - 1;
}
else {
// 如果是相同元素,也是插入在后面的位置
start = middle + 1;
}
}
return start;
}
(二)算法复杂度
1. 时间复杂度:O(n^2)
二分查找插入位置,因为不是查找相等值,而是基于比较查插入合适的位置,所以必须查到最后一个元素才知道插入位置。
二分查找最坏时间复杂度:当2^X>=n时,查询结束,所以查询的次数就为x,而x等于log2n(以2为底,n的对数)。即O(log2n)
所以,二分查找排序比较次数为:x=log2n
二分查找插入排序耗时的操作有:比较 + 后移赋值。时间复杂度如下:
1) 最好情况:查找的位置是有序区的最后一位后面一位,则无须进行后移赋值操作,其比较次数为:log2n 。即O(log2n)
2) 最坏情况:查找的位置是有序区的第一个位置,则需要的比较次数为:log2n,需要的赋值操作次数为n(n-1)/2加上 (n-1) 次。即O(n^2)
3) 渐进时间复杂度(平均时间复杂度):O(n^2)
2. 空间复杂度:O(1)
从实现原理可知,二分查找插入排序是在原输入数组上进行后移赋值操作的(称“就地排序”),所需开辟的辅助空间跟输入数组规模无关,所以空间复杂度为:O(1)
(三)稳定性
二分查找排序是稳定的,不会改变相同元素的相对顺序。
希尔排序
(一)概念及实现
思想:分治策略
希尔排序是一种分组直接插入排序方法,其原理是:先将整个序列分割成若干小的子序列,再分别对子序列进行直接插入排序,使得原来序列成为基本有序。这样通过对较小的序列进行插入排序,然后对基本有序的数列进行插入排序,能够提高插入排序算法的效率。
具体如下(实现为升序):
1. 先取一个小于n的整数d1作为第一个增量,将所有距离为d1的倍数的记录放在同一个组中,把无序数组分割为若干个子序列。
2. 在各子序列内进行直接插入排序。
3. 然后取第二个增量d2<d1,重复步骤1~2,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。
实现代码:
/// <summary>
/// 希尔排序
/// </summary>
public static void ShellSort<T>(IList<T> arr) where T : IComparable<T>
{
if (arr == null)
throw new ArgumentNullException("arr");
int length = arr.Count;
// 间隔增量,所有距离为space的倍数的记录放在同一个组中
int space = length / 2;
if (length > 1)
{
while (space >= 1)
{
ShellInsert(arr, length, space);
// 每次增量为原先的1/2
space = space / 2;
}
}
}
/// <summary>
/// 希尔子序列排序
/// </summary>
/// <param name="arr">待排序数组</param>
/// <param name="length">待排序数组长度</param>
/// <param name="space">间隔增量</param>
private static void ShellInsert<T>(IList<T> arr, int length, int space) where T : IComparable<T>
{
int i, j;
// 将arr子序列分成有序区和无序区,初始有序区有一个元素
// 0-(i-1) 为有序区;i-(length-1)为无序区
for (i = space; i < length; i++)
{
T temp = arr[i];
// 边找位置边后移元素
for (j = i - space; j >= 0 && arr[j].CompareTo(temp) > 0;)
{
// 如果已排序的元素大于新元素,将该元素移到下一位置
arr[j + space] = arr[j];
j = j - space;
}
// 将 arr[i] 放到正确位置上
arr[j + space] = temp;
}
}
二)算法复杂度
1. 时间复杂度: O(nlog2n)
希尔排序耗时的操作有:比较 + 后移赋值。时间复杂度如下:
1) 最好情况:序列是升序排列,在这种情况下,需要进行的比较操作需(n-1)次。后移赋值操作为0次。即O(n)
2) 最坏情况:O(nlog2n)。
3) 渐进时间复杂度(平均时间复杂度):O(nlog2n)
增量选取:希尔排序的时间复杂度与增量的选取有关,但是现今仍然没有人能找出希尔排序的精确下界。一般的选择原则是:取上一个增量的一半作为此次序列的划分增量。首次选择序列长度的一半为增量。(因此也叫缩小增量排序)
平均时间复杂度:O(nlog2n),希尔排序在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序(O(log2n))在最坏的情况下执行的效率会非常差。专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法.
2. 空间复杂度:O(1)
从实现原理可知,希尔排序是在原输入数组上进行后移赋值操作的(称“就地排序”),所需开辟的辅助空间跟输入数组规模无关,所以空间复杂度为:O(1)
(三)稳定性
希尔排序是不稳定的。因为在进行分组时,相同元素可能分到不同组中,改变相同元素的相对顺序。
(四)优化改进
根据实际运行情况,我们也可以将希尔排序中查找插入位置部分的代码替换为二分查找方式。