Java温故而知新-插入排序

时间:2023-03-09 05:42:49
Java温故而知新-插入排序

插入排序

插入排序的基本思想是将待排序的元素依次插入序列合适的位置,然后将这个位置后面的元素依次向后移动一位
位置1 2 3 4 5 6
序列5 4 2 1 8 3
设第1位为最初始的基础元素,也就是5,待排元素就是4,此时逻辑顺序是5|4 2 1 8 3(用“|”来表示排序到哪个位置了)
那么第一趟排序下来后,
序列变成:45|2183。
待排元素就是2,第2趟后,245|183
待排元素就是1,第3趟后,1245|83
待排元素就是8,第4趟后,12458|3
待排元素就是3,第5趟后,123458|
这样就排序成功了。

上面就是插入排序,数学逻辑上非常清晰,序列可以分为两个区,有序区和混乱区。第一步,我们要做的就是每次从混乱区取出一个混乱元素插入到有序区的序列排布中,那么如何插入呢?就要看第二步的内容了。

第一步如何取出元素呢?显然是要将每个元素(除去预置的那个)都取出一遍,那么用个for循环遍历整个序列就行。

伪代码

        for(int i = 1;i < arr.length;i++){
            temp = arr[i];

        }

第二步,执行插入步骤,这一步需要做两个事情,1寻找合适的位置,2插入后将其他元素往后移动一位

(1)如何寻找合适的位置呢?

有序区的元素默认都是从小到大排列的,都是有顺序的,那么就好办了,只要找到合适的位置就行,然后移动后面元素的位置就行。

例如
序列 12458|3
3先和1比较,不合适;
3再和2比较,不合适;
3再和4比较,合适,找到位置了。
3再和2比较,2小,不符合要求,不挪动;
3再和1比较,1小,不符合要求,不挪动;

实现这个逻辑最简单就是用一个for循环,设置一个条件和边界,去遍历一遍。

(2)接下来处理一个问题就是讲元素往后挪动一位。

既然我们已经找到这个边界了,那么只需要把这个边界后面的元素依次往后挪动一位就可以了。

这里就又是一个for循环,边界是“合适位置+1”到“当前待排元素的位置”。

所以这个排序就是一个for循环里面嵌入两个for循环

下面是伪代码

for(){//处理所有元素的遍历
    temp = 待处理的元素
    for(){//这个循环用来寻找合适的位置
    }
    for(){//这个循环用来将那些元素依次向后挪动
    }
}

这样确实可以实现,算法时间复杂度可以算做O(n^2),但是用两个for循环在里面,写起来有些冗长有没有更好的解决办法呢?将编写的复杂度下降一些呢?


解决办法就是在寻找合适位置这一步时,从后往前遍历,每对比到一个不合适的元素,就把这个元素往后挪动一位。

例如
序列 12458|3
temp = 3;
3先和8比较,不合适,挪动后1245 8;
3再和2比较,不合适,挪动后124 58;
3再和4比较,合适,找到位置了,挪动后123458;
3再和2比较,2小,不符合要求,不挪动;
3再和1比较,1小,不符合要求,不挪动;

这样边比较边挪动的做法,可以将寻找位置和挪动元素的操作结合起来,放到一个遍历循环中。

下面就不赘述了,放源码。

package com.chapter.five;
/**
 * 需求 直接插入排序法
 * 思路 1将一个数组,分成两个区,一个是有序区,一个是无序区
 * 2每次取出无序区的一个元素,到有序区排序
 * 3排序的原则是待排元素依次与有序区元素进行比较,插入到合适地方
 * 4插入时,待排元素放在该位置,后面的元素依次向后移一位
 * 排成从小到大
 * @author huxingyue
 *
 */

class InsertSort {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        InsertSort insert = new InsertSort();
        int[] arr = new int[]{10,1,9,2,8,3,7,4,6,5};
        System.out.print("数组排序前是:");
        for(int i:arr){
            System.out.print(i+" ");
        }

//      insert.funLowToHigh(arr);
        insert.funHighToLow(arr);

        System.out.print("数组排序后是:");
        for(int i:arr){
            System.out.print(i+" ");
        }

    }
    void funLowToHigh(int[] arr){   //这个插入排序是从小到大的
        int temp;   //用于存放临时元素
        for(int i = 1;i < arr.length;i++){
            temp = arr[i];//设temp为取出的待排混乱元素
            int j =0;
            for(j = i-1 ; j >= 0 && temp < arr[j];j--){
                arr[j+1] = arr[j];  //如果临时值与对比元素相比,临时值小,那对比元素就往后移动一位
            }
            arr[j+1] = temp;    // 这一步j已经自减1了

        }
    }

    void funHighToLow(int[] arr){   //这个插入排序是从大到小的
        int temp;
        for(int i = 1;i < arr.length;i++){
            temp = arr[i];
            int j = 0;
            for(j = i-1;j >= 0 && temp > arr[j];j--){
                arr[j+1] = arr[j];  //如果临时值与对比元素相比,临时值大,那对比元素就往后移动一位
            }
            arr[j+1] = temp;    // 这一步j已经自减1了
        }

    }

}

写在最后,讲一讲个人对插入排序的理解。

本质上来讲,插入排序和冒泡排序都属于一类排序,即比较相邻两个数,然后操作相邻数的一个过程。

如果一个序列中有两个数,a1和a2,且a1>a2,则命名为一个比较对,插入排序的算法和比较对的数量有关系,因为仅仅存在一个比较对的时候,才会进行操作,数据向后移动,也就是说,忽略验证内层循环条件的时间,那么确定了比较对的数量,插入排序的时间复杂度变成了一个一元线性的函数,设序列中有9个比较对,序列长度为n,那么总时间就是9n,而不是我们通常意味的n*n。

关于上面的描述很潦草,具体权威的描述可以参考《数据结构与算法java版》作者Mark Allen Weiss中,关于插入排序的解读,醍醐灌顶值得一看。

如果想改善这种算法的性能几乎不大可能,因为他总是比较相邻的元素,如果想提高性能,就要比较相隔远一点的元素,然后再操作,可以参考希尔排序。或者使用全新的排序方法,例如快速排序。