文章目录
本系列文章共有十一篇:
冒泡排序及优化详解
快速排序及优化详解
插入排序及优化详解
希尔排序及优化详解
选择排序及优化详解
归并排序及优化详解
堆排序详解
计数排序及优化详解
桶排序详解
基数排序及优化详解
十大排序算法的比较与性能分析
一、插入排序基础
插入排序也是一种常见的排序算法,插入排序的思想是:将初始数据分为有序部分和无序部分,每一步将一个无序部分的数据插入到前面已经排好序的有序部分中,直到插完所有元素为止。
插入排序的步骤如下:每次从无序部分中取出一个元素,与有序部分中的元素从后向前依次进行比较,并找到合适的位置,将该元素插到有序组当中。
1.1 排序过程图示
假如有[5,2,3,9,4,7]六个元素,下面就以排序过程中的一个步骤(此时有序部分为[2,3,5,9],无序部分为[4,7],接下来要把无序部分的“4”元素插入到有序部分),来展示一下插入排序的运行过程。
1>首先,原始的数组元素是这样的。
其中,浅绿色代表有序部分,黄色代表无序部分。
2>在无序部分中挑出要插入到有序部分中的元素。
3>将要插入的元素与左边最近的有序部分的元素进行比较。由于4 < 9,所以9向后移,4向前移。
4>继续将要插入的元素与左边最近的有序部分的元素进行比较。由于4 < 5,所以5向后移,4继续向前移。
5>继续将4与3比较。由于4 > 3,所以不再向前比较,插入到当前位置。
6>此时有序部分,由[2,3,5,9]变成[2,3,4,5,9]。
1.2 排序过程实现
将上述过程用代码实现,示例如下:
/*有序部分中与待插入元素比较的元素的下标*/
int j = 0;
/*存储无序部分的待比较元素*/
int tmp = 0;
/* 默认假设数组中第一个元素是有序部分,从第二个元素起是无序
* 部分。从最外层循环开始,持续从无序部分取出元素,然后与
* 相邻的有序部分的元素依次向前进行比较,寻找合适位置的过程,
* 其实也是将无序部分中“不合适元素”向后移动的过程,在找到合
* 适的位置后,最后将无序部分的元素插入
*/
for(int i = 1;i<n;i++){
/*从无序部分取出第一个元素*/
tmp = arr[i];
/*i-1是有序部分最后一个元素(与待插入元素相邻)的下标*/
j = i-1;
/*判断条件为两个:
* 1) j>=0,代表对有序部分进行边界限制
* 2) 第二个为正在比较的有序部分元素向后移动的判断条件
*/
while(j>=0 && tmp<arr[j]){
/*若不是合适位置,有序部分元素持续向后移动*/
arr[j+1] = arr[j];
/*继续与有序部分的前一个元素进行比较,直到与有序
* 部分的所有元素比较完
*/
j--;
}
/*找到合适位置,将元素插入*/
arr[j+1] = tmp;
}
二、插入排序优化
插入排序的优化方式一般有两种:折半插入排序和2-路插入排序。
2.1 折半插入排序
该类优化有二分的思想,是在将待排序的元素与有序部分的元素比较时,不再挨个比较,而是用二分折中的方式进行比较,加快比较效率。示例代码如下:
int j,low,mid,high,temp;
for(int i = 1;i<n;i++){
low = 0;
high = i-1;
temp = arr[i];
/*找到合适的插入位置high+1,如果中间位置元素
*比要插入元素大,则查找区域向低半区移动,否
*则向高半区移动
*/
while(low <= high){
mid = (low+high)/2;
if(arr[mid]>temp){
high = mid-1;
}else{
low = mid+1;
}
}
/*high+1后的元素后移*/
for(j = i-1;j >= high+1;j--)
{
arr[j+1] = arr[j];
}
/*将元素插入到指定位置*/
arr[j+1] = temp;
}
2.2 2-路插入排序
该中方式是在折半插入排序的基础上再进行改进的算法,其目的是减少排序过程中移动记录的次数,付出的代价是n个记录的辅助空间。
假设原数组为 arr,另设一个相同类型的数组 tempArr,先将 arr[0] 赋值给 tempArr[0],并将tempArr[0] 看成是在有序序列中处于中间位置的元素,然后从 arr[1] 起一次插入到 tempArr[1] 之前或之后的有序序列中。
先将待插元素和tempArr[0] 做比较,若小于tempArr[0],则插入tempArr[0] 之前的有序部分;反之,将其插入tempArr[0] 之后的有序部分中。在实现算法时,可将tempArr看成一个循环数组,并设 first 和last分别指向排序过程中得到的有序序列中的第一个元素和最后一个元素在tempArr中的位置。示例代码如下:
int j, first, last, mid;
/*临时数组*/
int[ ] tempArr =new int[len];
tempArr[0] = arr[0];
/*first和last分别指临时数组tempArr中排好序的元素的第一个和最后一个位置*/
first = last = 0;
for(int i = 1; i<len; i++){
/*j 是调整系数*/
if(first > last){
j = len;
}else{
j = 0;
}
/*tempArr中间元素的位置*/
mid = ((first+last+j)/2)%len;
/*arr[i]应该插入在tempArr的前半部分*/
if(arr[i] < tempArr[mid]){
/*j指向tempArr数组中的第一个元素*/
j = first;
/*first 前移,取余是为了实现循环数组效果*/
first = (first-1+len)%len;
/*待插元素大于 j 所指元素*/
while(arr[i] > tempArr[j]){
/*j 所指元素前移,取余是为了实现循环数组效果*/
tempArr[(j-1+len)%len] = tempArr[j];
/*j 指向下一个元素*/
j = j+1;
}
/*移动结束,待插元素插在tempArr[j]前*/
tempArr[(j-1+len)%len] = arr[i];
/*arr[i]应该插入在tempArr的后半部分*/
}else{
/*j指向tempArr数组中的最后一个元素*/
j = last;
/*last后移, 指向插入后的最后一个元素*/
last++;
/*待插元素小于 j 所指元素*/
while(arr[i] < tempArr[j]){
/*j 所指元素后移*/
tempArr[(j+1)%len] = tempArr[j];
/*j 指向上一个元素*/
j = (j-1+len)%len;
}
/*移动结束,待插元素插在tempArr[j]后*/
tempArr[(j+1)%len] = arr[i];
}
}
/*把在tempArr中排好序的元素依次赋给arr*/
for(int i = 0; i < len; i++){
arr[i] = tempArr[(first+i)%len];
}
三、稳定性、复杂度和适用场景
3.1 稳定性
在使用插入排序时,元素从无序部分移动到有序部分时,必须是不相等(大于或小于)时才会移动,相等时不处理,所以直接插入排序是稳定的。
3.2 时间复杂度
在插入排序中,当待排序序列是有序时,是最优的情况,只需当前数跟前一个数比较一下就可以了,这时一共需要比较n- 1次,时间复杂度为O(n)。
最坏的情况是待排序数组是逆序的,此时需要比较次数最多,总次数记为:1+2+3+…+N-1,所以,插入排序最坏情况下的时间复杂度为O(n2)。
平均来说,array[1…j-1]中的一半元素小于array[j],一半元素大于array[j]。插入排序在平均情况运行时间与最坏情况运行时间一样,是O(n2)。
3.3 适用场景
待排序序列的元素个数不多(<=50),且元素基本有序。