插入排序–直接插入排序、折半查找、希尔排序
1.直接插入排序:基于有序数组元素内容的插入得来。
做题思路:首先需要找到元素插入位置,再将元素插入即可。
核心步骤:将数组分为已排序区间和待排序区间(默认第一个元素为有序的,后面其他元素为无序)。若按照升序排序,则需将待排序区间第一个元素与已排序区间最后一个元素进行大小比较,若大于,则不需移动成为已排序区间最后一个元素即可;若小于,则需要将待排序区间第一个元素(插入元素)挨个从后向前对比已排序区间元素,直至找到合适位置插入即可。
核心步骤图解:
插入排序源代码:
public class Test {
public static void main(String[] args) {
int[] data=new int[]{6,4,5,1,2,3};
insertSort(data);
for(int i:data){
System.out.print(i+" ");
}
}
public static void insertSort(int[] data){//直接插入排序算法
int n=data.length;
if(n<=1){//判断若数组为空或只有一个元素时直接返回即可
return;
}
for(int i=1;i<n;i++){//外层循环控制循环次数(下标从1开始是因为默认第一个元素有序,此循环区间为待排序区间)
int value=data[i];//插入元素
//内层循环控制两元素对比次数(下标从已排序区间最后一个元素开始从后向前遍历)
int j=i-1;
for(;j>=0;j--){
if(value<data[j]){
data[j+1]=data[j];//若插入元素小于已排序元素,则需要向前移动,即相当于已排序元素向后移动
}else{//若插入元素大于已排序某元素直接跳出
break;
}
}
data[j+1]=value;//将元素插入(此时j+1是因为上面循环退出时j--之后判断)
}
}
}
我们通过代码可以看出直接插入排序最好排序时间复杂度为O(n)(情况为数组正好与最终要求一致,只需遍历一遍检查即可),最坏时间复杂度为O(n2)(情况为数组顺序正好与最终要求完全相反例如原数组为降序最终要求为升序),平均时间复杂度为O(n2)(元素相当大时才可忽略系数、低阶、常数的影响)
空间时间复杂度为O(1)(因没有新开辟空间,只是在原有数组的基础上交换)。
直接插入排序为稳定性排序和原地排序。
那么冒泡排序和直接插入排序相比哪个应用更广、效率更高呢?
我们按照开头判断排序算法效率的方法来看,时间复杂度空间复杂度两者均相同,故我们可看元素之间的交换次数。我们通过分析可知冒泡排序每次均要进行两个相邻元素之间的对比交换,而直接插入排序交换次数明显较少。故直接插入排序算法应用更广、效率更高。
测试代码:
import java.util.Random;
public class Test {
public static void main(String[] args) {
int[] data=generateRandomArray(8000,1000,10000);
bubbleSort(data);
insertSort(data);
}
public static void insertSort(int[] data){
long startTime=System.currentTimeMillis();
int n=data.length;
if(n<=1){
return;
}
for(int i=1;i<n;i++){
int value=data[i];
int j=i-1;
for(;j>=0;j--){
if(value<data[j]){
data[j+1]=data[j];
}else{
break;
}
}
data[j+1]=value;
}
long endTime=System.currentTimeMillis();
long time=endTime-startTime;
System.out.println("直接插入:"+time+"毫秒");
}
public static void bubbleSort(int[] data){
long starttime=System.currentTimeMillis();
int n=data.length;
if(n<=1){
return;
}
for(int i=0;i<n;i++){
boolean flag=false;
for(int j=0;j<n-i-1;j++){
if(data[j]>data[j+1]){
flag=true;
data[j]^=data[j+1];
data[j+1]^=data[j];
data[j]^=data[j+1];
}
}
if(!flag){
break;
}
}
long endtime=System.currentTimeMillis();
long time=endtime-starttime;
System.out.println("冒泡:"+time+"毫秒");
}
public static int[] generateRandomArray(int n,int rangeL,int rangeR){
if(rangeL>rangeR){
throw new IndexOutOfBoundsException("越界异常");
}
int[] arr=new int[n];
for(int i=0;i<n;i++){
arr[i]=new Integer(new Random().nextInt(rangeR-rangeL+1)+rangeL);
}
return arr;
}
}
2.折半插入(二分查找):直接插入优化一
思路:
第一步:将原始数组划分为已排序区间和待排序区间,默认原始数组第一个元素为有序的,故待排序区间为原始数组第二个元素到最后一个元素。依次将待排序区间元素与已排序区间中间元素比较大小,若待排序元素大于中间元素则在中间元素后面继续使用二分查找(low=mid+1),反之在前面继续查找直到找到插入位置。(high=mid-1)。
第二步:判断元素插入位置是否在已排序区间最后一个元素至前,若是则需要先向后搬移元素再插入,若否则直接插入即可(此种情况为待插入元素大于已排序区间最后一个元素,即大于已排序区间所有元素,直接插入最后一个即可)。
折半查找图解:
折半查找源代码:
import java.util.Random;
public class Test {
public static void main(String[] args) {
int[] data=generateRandomArray(20,100,1000);
sort(data);
for(int i:data){
System.out.print(i+" ");
}
}
public static void sort(int[] data){
int n=data.length;
if(n<=1){//判断数组是否为空数组或只有一个元素,若是直接返回
return;
}else {
for(int i=1;i<n;i++){//遍历待排序区间元素
int low=0;//已排序区间头下标
int high=i-1;//已排序区间尾下标
int value=data[i];//待插入元素
int j=i-1;//判断待插入元素插入位置是否大于已排序区间最后一个元素位置,若是直接插入,否则需要向后搬移元素再插入
while (low<=high){//控制循环次数
int mid=((low^high)+(low&high)/2);//求平均值不会溢出
//为保证折半查找的稳定性,判断中间值与待插入元素大小关系时应如下(若相反则会破坏其稳定性)
if(data[mid]>value){//若中间值大于待插入元素,则应在中间值前面继续二分查找,中间值后面不考虑
high=mid-1;
}else {//若中间值小于等于待插入元素,则应在中间值后面继续二分查找,中间值前面不考虑
low=mid+1;
}
}
for(;j>=high+1;j--){//若插入位置在已排序区间内,则需要从后向前挨个搬移元素
data[j+1]=data[j];
}
data[j+1]=value;//最后再插入元素
}
}
}
//在一定范围内随机生成n个数字[rangL,rangR]
public static int[] generateRandomArray(int n,int rangeL,int rangeR){
if(rangeL>rangeR){
throw new IndexOutOfBoundsException("越界异常");
}
int[] arr=new int[n];
for(int i=0;i<n;i++){
arr[i]=new Integer(new Random().nextInt(rangeR-rangeL+1)+rangeL);
}
return arr;
}
}
确保算法稳定性处代码图解:
如果判断条件写反就变成不稳定算法:
运行结果:
折半查找时间复杂度:O(log2n)
空间复杂度:O(1)
稳定性:是。
3.希尔排序:直接插入排序优化二
思路:以上直接插入排序、折半查找排序都是挨个查找交换,那么能不能跳跃式比较大小交换呢?希尔排序就是采用跳跃式比较交换方式。具体执行过程是自行定义一个step(就是你希望每次跳跃多大的距离,自行定义),将距离最近step的元素进行大小比较,若前面元素大于后面,则交换;然后不断缩短step的范围,到step=1时结束(step=1时相当于直接排序)。
希尔排序图解:
希尔排序代码:
import java.util.Random;
public class Test {
public static void main(String[] args) {
int[] data=generateRandomArray(20,100,1000);
// int[] data=new int[]{2,8,6,3,1,7,5,4};
sort(data);
for(int i:data){
System.out.print(i+" ");
}
}
public static void sort(int[] data){
int n=data.length;
if(n<=1){
return;
}else {
int step=n/2;//自定义即可没有规定
while(step>=1){
for(int i=step;i<n;i++){//遍历step之后的元素
int value=data[i];
int j=i-step;
for(;j>=0;j-=step){//控制两元素比较大小次数
if(value<data[j]){//当待排序元素小于前面元素时,将前面元素搬移至后面i处
data[j+step]=data[j];
}else{
break;
}
}
data[j+step]=value;//插入元素
}
step/=2;//控制元素比较大小之间距离
}
}
}
public static int[] generateRandomArray(int n,int rangeL,int rangeR){
if(rangeL>rangeR){
throw new IndexOutOfBoundsException("越界异常");
}
int[] arr=new int[n];
for(int i=0;i<n;i++){
arr[i]=new Integer(new Random().nextInt(rangeR-rangeL+1)+rangeL);
}
return arr;
}
}
运行结果:
时间复杂度在O(n1.5)-O(n2)之间,因比较两元素之间长度为自定义。
空间复杂度O(1)。因没有开辟新空间只是在原有基础上进行元素交换。
稳定性:否。举例待排序数组为 2 8 8 2,step=2是,前面的8会移动到后面8的后面,相等元素的相对位置改变,故不是稳定性算法排序。