常见的插入排序
作者:互联网
- 直接插入排序
直接插入排序的思路非常简单。将一个数组分成两个序列,一个序列是有序的,而另一个序列是无序的,每次都从无序的序列中取出一个数与有序序列当中的每一个数进行比较,直到比较到一个合适的插入位置,并且将该插入位置之后的元素(包括插入位置)往后移动,最后将该元素放置到插入的位置当中,插入排序的算法就完成了。
插入排序的过程如下图所示:
使用java进行直接插入排序实现,代码如下:
//先查找再插入 public static void Insertsort1(int[] arr) { int insertPos = -1; int insertVal = -1; for(int i=1;i<arr.length;i++) { //找到插入位置,arr[i]为待插入的元素 int j=-1; for (j=i-1;j>=0 && arr[i]<arr[j] ;j--); insertPos = j+1; insertVal = arr[i]; //将元素往后移动 for (int k=i-1;k>=insertPos;k--) { arr[k+1] = arr[k]; } //将插入位置进行插入值赋值 arr[insertPos] = insertVal; } }
可以看到这里我使用了两个变量insertPos和insertVal分别用来记录插入位置的下标以及插入的数值,这样是为了更好的去理解该算法,并且为之后的折半插入排序做铺垫。
算法复杂度分析
时间复杂度:
首先要去分析每一轮所做出的事情,先是查找到应该插入的位置,其次将该插入位置之后的元素向后挪动,再进行插入。最坏的情况下,查询直到第一个元素才可以插入,并且要进行有序元素的所有元素进行向后移动,结合每一轮的情况,可以得出总的操作的次数为:(1+1)+(2+2)+(3+3)+…+(n-1+n-1)=(n-1)*n,那么其最坏的时间复杂度为O(n^2),对于最好的情况下,也就是顺序情况下每一轮只进行一次比较因此会比较n-1次,那么时间复杂度为O(n),
平均时间复杂度也为O(n^2)。
空间复杂度:
对于当前算法而言,我使用两个变量来记录插入位置和插入值,则空间复杂度为O(1)。
该排序是一个稳定排序算法,由于遇到与元素相同的元素就停止查找,那么元素依然是排在相同元素的后面。
实际上对于该排序算法而言,可以将数组的0号位置设置为哨兵用来存放比较的元素,不仅可以节省一个空间,而且在算法中的循环条件来说,匹配到哨兵元素必然会满足条件,从而在代码上进行一定的优化,本文并没有采取该策略,而是集中于排序算法的分析与实现。
- 折半插入排序
折半插入排序融入了折半查找(二分查找)在普通的插入排序当中。
在普通插入排序当中,我们需要查找到元素所需要插入的位置,而在普通策略当中,我们对于有序序列的查找是进行简单遍历,而这种查找的时间复杂度为O(n),那么自然效率会低。
针对于有序序列的查找,二分查找的方法会比简单遍历的方法要快。而二分查找的基本思想就是比较有序序列的中间元素,然后查看是否在左区间还是右区间,并进行区间缩小继续查询,很容易分析的是其查询的时间复杂度为O(logn)。
二分查找:
首先就是二分查找的区间选取,这个是非常重要的一部分,因为要关系到算法中循环条件。
二分查找需要指定left和right指针来限定查找区间,left一般选取序列的一个元素的下标,而right的选取方式则有两种,一种是序列的最后一个元素的下标,另一种是序列的长度+left的值。两种选取方式如上图所示
这两种选取方式的区别如下,如果选取上图的第一种那么就是right不会出现下标越界的情况,其搜索区间就是[left,right]即左边是闭区间,右边也是闭区间;而对于第二种来说,其搜索区间为[left,right)即左闭右开,right这一下标取不到。
那么对于二分查找算法的描述如下:
left = 0; right = array .length – 1; while(left<=right) { mid = 取left和right的一半 if 比较的元素>=mid下标所在的数组的数: 改变left,搜索右区间 else if 比较的元素<mid下标所在的数组的数: 改变right,搜索左区间 } return left
在该算法当中返回的是left的值,left的值即为搜索元素所需要插入的位置。
分析:
关键点1:while循环中的条件是left<=right,由于在此二分查找算法中选取的初始条件为left=0,right = array .length – 1,这是左闭右闭的情况那么在最后在执行到left=right时,其搜索区间为[left,right],该区间是存在的,只不过是只有一个元素的特殊区间,而该区间需要搜索,那么while循环就需要将条件设置为left<=right。
关键点2:在往常最普通的二分查找中是搜寻数字所在的位置,如果没有找到则返回-1,找到返回其下标。那么在该算法中,无论搜索结果如何都是返回left,这个left返回的意义是:左边都是小于等于比较元素的值,即left的位置就是插入排序中插入的位置。从上图可以看出,对于一个数组进行查找插入25这个元素来说,循环结束后left和right指针指向情况。
我们通过代码可以知道对left指针进行操作的意义是:left的左边的所有元素都是小于等于查找元素的,而right的右边是大于查找元素的,那么在插入排序当中插入一个元素为了是序列保持稳定有序,那么其插入位置的左边所有元素都是小于等于插入元素,那么即可分析得到left必定为插入位置。
其实left的意义也可以理解为:数组小于等于插入元素的个数,所以对于left的两个边界问题0和array.length的意义既可以是,插入到0和array. length这两个位置;也可以是数组中小于等于插入元素的个数为0和array. length。
下面是二分查找的java代码实现:
left = 0; right = i-1; while(left<=right) { mid = left + (right-left)/2; if(arr[i]>=arr[mid]) { left = mid + 1; } else if (arr[i]<arr[mid]) { right = mid - 1; } }
在二分查找算法介绍完毕之后,那么折半插入排序也就可以写出,可以注意到直接套用直接插入的模板,使用直接插入算法中的两个变量:insertPos和insertVal即可完成之后的元素移动和插入。
折半插入排序的java排序实现代码如下:
public static void BinaryInsertSort1(int[] arr) { int insertPos = -1; int insertVal = -1; int left = -1,mid = -1,right = -1; for(int i=1;i<arr.length;i++) { //找到插入位置,arr[i]为待插入的元素 left = 0; right = i-1; while(left<=right) { mid = left + (right-left)/2; if(arr[i]>=arr[mid]) { left = mid + 1; } else if (arr[i]<arr[mid]) { right = mid - 1; } } insertPos = left; insertVal = arr[i]; //将元素往后移动 for (int k=i-1;k>=insertPos;k--) { arr[k+1] = arr[k]; } //将插入位置进行插入值赋值 arr[insertPos] = insertVal; } }
时间复杂度分析:相比于直接插入排序,折半插入排序优化了普通插入排序的查询部分。普通插入排序查询时间复杂度为O(n),而二分查找时间复杂度为O(logn),因此会比直接插入排序效率要高一点,其操作次数为:(log1 + 1) + (log2 + 2) + (log3 + 3)+…+(log(n-1) + n-1)=(1+2+3+…+n-1)+(log1+log2+…+log(n-1)),其时间复杂度为O(n^2)与直接插入排序是同一个级别的复杂度,而最好情况下顺序执行也需要完整的二分查找即log1+log2+log3+…+logn=logn!。
空间复杂度分析:很容易分析,空间复杂度为O(1)。
- 改进的插入排序
从上面的插入排序当中,可以看出来,对于插入排序的过程是先进行查询,再进行插入的,也就是查询和插入这两个操作分开进行,而折半插入排序则是对查找方式进行的简单优化。
在查询和插入的过程中,可以发现的是:对于每一个元素进行一个接一个的查询或者移动,很容易发现这其中有很多重复的操作,也就是说,可以采取边比较边挪动的操作,让二者结合起来,从而达到一轮插入的时间效率与上述效率较差的插入算法的移动元素操作相同的时间,而省去了查询的时间。
在原有插入排序的思路下,我们开始查询的过程,当向前查询时,查询的序列为有序的(正序),那么比较的元素在向前查询时小于查询元素的话就进行交换,如果大于等于的话,那么这个元素现在所处的位置是最合适的,就停止交换位置。这个可以相当于一次查询的过程或者一次元素挪动的过程,因此要比以上两种速度更快,所以现在描述插入排序基本上就是现在描述的算法过程,因为该算法是同类插入算法相同时间复杂中效率最高的。同时该排序算法仍然是稳定的。
下面是该插入算法的java代码:
//边查找变交换元素优化 public static void InsertSort2(int[] arr) { for (int i = 1; i < arr.length; i++) { for (int j = i; j > 0 && arr[j-1]>arr[j]; j--) { int temp = arr[j]; arr[j] = arr[j-1]; arr[j-1] = temp; } } }
时间复杂度分析:可以很容易分析到,其该插入排序的最坏的执行次数为:(1+2+3+…+n-1)*3=3*n*(n-1)/2,即时间复杂度为O(n^2)。
空间复杂度分析:O(1)。
上述这种边查询边交换的方式只是思路上的优化,实际上并没有进行时间上的优化,因为如下代码:
int temp = arr[j]; arr[j] = arr[j-1]; arr[j-1] = temp;
在内循环中,有三句语句需要执行,那么对于执行的次数为:3*(1+2+…+n-1),而对于查询与移动元素分离插入排序来说是2*(1+2+…+n-1),即比原先的时间还要多那么我们就需要将其进行简化。思路如下:将要插入的元素体检记录下来,然后向前查找,如果大于插入元素,就将其向后移动,直到不满足条件,就退出循环,并将查询到的位置进行插入元素赋值,最终,此种算法是插入排序当中效率最高的一种版本
下面是java代码实现:
//不交换元素,在查找之前记录插入元素,查找过程中挪动元素,查找到之后再进行插入, public static void sort4(int[] arr) { for (int i = 1; i < arr.length; i++) { int temp = arr[i],j; for (j = i; j > 0 && arr[j-1]>temp; j--) { arr[j] = arr[j-1]; } arr[j] = temp; } }
- 希尔排序
在我们分析插入排序的最坏时间复杂度的时候,往往是将数组视作完全倒序,那么此时是插入排序的效率最差,因此一种最自然的想法就是,将数组最开始调整为基本有序,使其不会造成最坏的排序序列,最后再进行插入排序,那么算法的时间会缩小,这种排序算法就是希尔排序。
希尔排序的基本的核心思路就是根据不同的增量进行分组。也就是对于一个数组来说,增量数为分组数,那么每一组的元素个数就是元素总数除以分组数,对每个组分别进行插入排序,然后缩小增量,直到最后增量为1,希尔排序退化为普通的插入排序。过程如下图所示:
第一轮:
第二轮:
第三轮:
可以通过图上看出,当我们在希尔排序进行到最后一轮的时候,也就是退化成为插入排序之后的数组序列已经基本有序,可见希尔排序的思想。
下面是希尔排序的java实现代码:
//希尔排序 public static void ShellSort1(int[] arr) { int step = arr.length/2; while(step>=1) { for (int i = step; i < arr.length; i++) { for (int j = i; j >= step; j-=step) { if(arr[j]<arr[j-step]) { int temp = arr[j]; arr[j] = arr[j-step]; arr[j-step] = temp; } } } step /= 2; } }
希尔排序的时间复杂度与选取的增量序列有关,其时间复杂度在O(n)与O(n^2)之间,最好为O(n^1.3)(通过相关数学证明),空间复杂度为O(1),希尔排序由于是分组进行插入排序,所以,相同的元素可能会被替换先后位置,那么希尔排序是不稳定的排序。
与插入排序的优化思路相同,将上述代码的循环中交换部分转换成为移动操作,即循环中只进行1次操作。这也是希尔排序的最优化版本。
优化的希尔排序代码用java代码实现如下:
//希尔排序优化 public static void ShellSort2(int[] arr) { int step = arr.length/2; while(step>=1) { for (int i = step; i < arr.length; i++) { int temp = arr[i],j; for (j = i; j >= step && temp < arr[j-step]; j-=step) { arr[j] = arr[j-step]; } arr[j] = temp; } step /= 2; } }
标签:arr,元素,int,插入排序,常见,插入,left 来源: https://www.cnblogs.com/secuy/p/16319011.html