作者:vuefine
文献:
- Data Structures and Algorithms Using C# |Michael Mcmillan
- 数据结构与算法 | 邓俊辉
平台:.NET 2.0+
前面总结了最基本的排序:冒泡排序,随机生成了10个元素,并模拟了整个过程,共经历9轮,总计需要45次比较两两比较,模拟的结果显示,有2对元素两两比较了多达6次,显然这是冗余的!
今天,我要模拟的另一种排序算法:插入排序,insert sort,什么是插入排序? 如果不知道,也无妨,看下图,你基本便能知晓插入排序的思想。那么插入排序的算法怎么写?整个排序过程是怎样的呢?
首先我们还是,准备自己的集合(请点击查看我自己封装的专门用于排序MyArray对象),在模拟插入排序时,我又增加了几个方法,现在为了方便我再把代码贴到这里,新增加的功能接口包括:
- 在指定位置插入元素
- 统计插入元素的个数
public class MyArray
{
private MyElement[] _array;
private int _upper;
private int _cnt;
private readonly int _size;
public MyArray(int size)
{
_size = size;
_array = new MyElement[size];
_upper = size - 1;
_cnt = 0;
}
//统计插入元素的个数
public int InsertCount
{
get { return _cnt; }
}
public void Insert(MyElement item)
{
_array[_cnt++] = item;
}
//在指定位置插入元素
public void InsertAt(MyElement item,int index)
{
if(index <0 || index>_size)
throw new ArgumentException();
for (int i = _cnt;i >index; i--)
{
_array[i] = _array[i - 1];
}
_array[index] = item;
_cnt++;
}
public void Clear()
{
for (int i = 0; i < _cnt; i++)
_array[i].Val = 0;
_cnt = 0;
}
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
foreach (var item in _array)
{
stringBuilder.Append(item.Val + " ");
}
return stringBuilder.ToString();
}
public void FillRandomValue()
{
Random random = new Random(_size * 5);
for (int i = 0; i < _size; i++)
{
var element = new MyElement();
element.Val = (int)(random.NextDouble() * _size * 5);
this.Insert(element);
}
}
public void Printf()
{
Console.Write(this.ToString());
}
public MyElement GetAt(int index)
{
if (index < 0 || index > _size)
throw new ArgumentException();
return _array[index];
}
//如果a>b,交换;否则直接return
public static void Swap(MyElement a, MyElement b)
{
if (a.Val <= b.Val) return;
int t = a.Val;
if (a.Val > b.Val)
{
a.Val = b.Val;
b.Val = t;
}
}
}
新加的一个方法:InsertAt(ele,index),它的功能是在向量指定位置处插入元素,同时将插入后的所有元素统一向后移动一个位置,移动时,要从最后一个开始逐次移动!
/// <summary>
///
/// </summary>
/// <param name="item"></param>
/// <param name="index">插入元素的位置</param>
public void InsertAt(MyElement item,int index)
{
if(index <0 || index>_size)
throw new ArgumentException();
for (int i = _cnt;i >index; i--)
{
_array[i] = _array[i - 1];
}
_array[index] = item;
_cnt++;
}
插入排序算法的过程。首先,取出无序数组中的第一个元素,插入到有序数组的0号索引中。
MyArray sortedarr = new MyArray(size);
sortedarr.Insert(myarr.GetAt(0)); //插入原数组的第一个元素
接着,遍历无序数组的每一个元素,找到元素在有序数组的合适位置,然后插入到有序序列中。
for (int i = 1; i < size; i++)
{
MyElement ie = myarr.GetAt(i);
insertElementAtSortedArray(sortedarr,ie);
}
上面中insertElementAtSortedArray方法实现了将ie元素插入sortedarr的功能。那么它是如何实现的呢?请看:
public static void insertElementAtSortedArray(MyArray sortedArray, MyElement ele)
{
//确定元素ele插入后的位置
int insertIndex = decideIndexAfterInsert(sortedArray, ele);
sortedArray.InsertAt(ele, insertIndex);
}
一种二分查找版本,进而能够实现实现插入排序的稳定。
//确定元素ele插入后的位置
public static int decideIndexAfterInsert(MyArray sortedArray, MyElement ele)
{
int lo = 0;
int hi = sortedArray.InsertCount;
while (lo < hi)
{
int mi = (lo + hi) >> 1;
if (ele.Val < sortedArray.GetAt(mi).Val)
hi = mi;
else lo = mi + 1;
}
return lo;
}
二分查找完后,返回的lo为大于ele的元素的最小秩。若插入元素有多个相同元素时,总能保证插入到秩最大者处。因此,基于这个二分查找版本实现的“插入排序”是稳定的!
我们解释下为什么lo是这么个语义?
lo初始值为0, hi为待排序数组的元素个数InsertCount,lo,hi的取值范围如下
0 <= lo < InsertCount
l <=hi <= InsertCount
ele一定位于[lo,hi),注意是前闭后开,因为hi是元素个数,ele取不到hi。循环体内保证了[0,li)的元素不大于ele,[hi,InsertCount)的元素比ele大,
ele[0,li) <=ele 且
ele[hi,InsertCount) > ele
循环跳出时,li>=hi, [0,hi<=li) <=ele 且ele[hi,InsertCount)>ele 所以li的位置为插入元素ele的位置。疑问:返回hi可行吗?
整个循环,记录下lo,hi,mi的取值历程!我们看下:
在插入39时,lo=0, hi=4, 此时mi=2, 与35比较后,区间取右分支,修改lo=3;
mi = (3+4)/2=3, 与42比较后,区间取左分支,修改hi=3, 此时lo=hi,退出while循环
此时的位置3为39元素插入后的位置,插入后,序列由
11 24 35 42
变为序列:
11 24 35 39 42
那么我们看下插入排序两两比较,次数大于2次的记录:
因此,插入排序比冒泡排序,冗余比较的次数大大下降了!
那么,我们按照上篇写的统计代码的执行时间的方法(用C#描述数据结构1:统计代码执行时间对象),统计下执行随机生成的100个元素,冒泡排序和插入排序的执行时间比较,
保证两次随机产生的集合元素、顺序都一致,因为.NET中的Random是伪随机分布的,所以可以保证!
冒泡排序,排序100个元素,所用时间93ms
来看下,插入排序,排序相同的100个元素,大约31ms,
冒泡排序不愧是暴利啊!!!
插入排序,明显效率改进了不少!
总结:
1)冒泡排序很暴力
2)基于这个版本的二分查找的插入排序,是稳定的!
3)一个优秀的排序算法是通过尽可能少的比较,来完成对整个序列的排序。
程序模拟实验所用到的所有源码,包括冒泡排序,插入排序,代码运行时长统计
http://download.csdn.net/detail/daigualu/9776250