插入排序(Insertion Sort)
插入排序非常类似于扑克牌的排序
执行流程:
1 在执行过程中,插入排序会将序列分为两部分:头部是已经排好序的,尾部是待排序的。
2 从头开始扫描每一个元素
每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数组仍然保持有序。
不难写出代码:
public class sort {
public static void main(String[] args)
{
int[] array = {19, 18, 67, 199, 6, 56};
insertSort(array);
}
/**插入排序*/
public static void insertSort(int[] array)
{
//prepareInsert准备插入的元素下标。从1开始,遍历到末尾
for (int prepareInsert = 1; prepareInsert < array.length; prepareInsert++) {
//遍历已经排序好的部分,找到合适的位置,将其插入
//需要做对比的元素,从0到prepareInsert - 1,一共prepareInsert个元素
//遍历从后面开始,sortedInsert从prepareInsert - 1到0
for (int sortedInsert = prepareInsert; sortedInsert > 0; sortedInsert--) {
if (array[sortedInsert - 1] > array[sortedInsert]) {
int temp = array[sortedInsert];
array[sortedInsert] = array[sortedInsert - 1];
array[sortedInsert - 1] = temp;
}else {
break;
}
}
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
}
外面的循环,prepareInsert指的是准备插入的数据索引
里面的循环,是已经排序好的部分
MJ大神是这样写的:
/**插入排序2*/
public static void insertSort2(int[] array)
{
for (int prepareInsert = 1; prepareInsert < array.length; prepareInsert++) {
int cur = prepareInsert;
while (cur > 0 && (array[cur - 1] > array[cur])) {
int temp = array[cur];
array[cur] = array[cur - 1];
array[cur - 1] = temp;
cur--;
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
其实是跟上面的写法一样的,只是for循环变为了while循环。
逆序对(Inversion)
什么是逆序对?
数组[2, 3, 8 ,6, 1]的逆序对为:
<2, 1> < 3, 1> <8, 1> <6, 1> <8, 6>一共5个逆序对
插入排序的时间复杂度与逆序对的数量成正比关系
逆序对的数量越多,插入排序的时间复杂度越高
最坏、平均时间复杂度:O(n2)
最好时间复杂度:O(n)
空间复杂度:O(1)
属于稳定排序
当逆序对的数量极少时,插入排序的效率特别高
甚至速度比O(nlogn)级别的快速排序还要快
数据量不是特别大的时候,插入排序的效率也是非常好的。
优化方案一:
将交换转为挪动
首先,我们观察到,上面的代码是每次都要比较大小,就跟冒泡排序一样,两两比较。
有一种优化方案,将待插入的元素备份,然后比较大小:
如果前大后小,则将前面元素往后移动,直接覆盖后一个元素,继续将前前元素与备份元素做比较,以此类推。直至找到合适的位置(前小后大),将备份元素插入到合适位置。
如果前小后大,则不动,结束循环。
这种虽然还是要一个一个移动,但是,少了过程中的交换,直接将找到位置做一次交换即可。
官方点的语言:
1 先将待插入的元素备份
2 头部有序数据中比待插入元素大的,都朝尾部方向挪动一个位置
3 将待插入元素放到最终的合适位置
不难写出优化代码:
/**插入排序*/
public static void insertSort(int[] array)
{
for (int begin = 1; begin < array.length; begin++) {
int beginValue = array[begin];
for (int middle = begin; middle > 0; middle--) {
if (array[middle - 1] > beginValue) {
array[middle] = array[middle - 1];
}else {
//如果能走到这,说明找到了合适的位置,然后插入元素
array[middle] = beginValue;
break;
}
//如果走到了这,说明没有找到合适的插入位置,也就是,所有的元素都是大于要插入的元素的。
//那么,需要将待插入的元素插入到最前面
if (middle == 1) {
array[0] = beginValue;
}
}
}
}
当然,也可以使用while循环
/**插入排序*/
public static void insertSort2(int[] array)
{
for (int begin = 1; begin < array.length; begin++) {
int current = begin;
int beginValue = array[begin];
while (current > 0 && (array[current - 1] > beginValue)) {
array[current] = array[current - 1];
current--;
}
array[current] = beginValue;
}
}
好像,使用for、while写的代码更简便些。
对于如何找出合适的位置,由于前面一部分已经是有序排序,对于有序排序,有一种方法查找元素效率比一个一个比较的查找效率要高,这就是二分搜索
二分搜索(Binary Search)
二分搜索又称为二分查找
如何确定一个元素在数组中的位置?(假设数组里面全都是整数)
如果是无序的数组,从第0个位置开始遍历搜索,平均时间复杂度为O(n)
如果是有序的数组,使用二分搜索,最坏的时间复杂度为O(logn)
每次去除一半,相当于一个二叉树,很容易得出是高度最大是logn
二分搜索思路
假设在[begin, end)范围内搜索某个元素v,mid == (begin + end)/2
或者mid == begin + (end - begin)/2
m = array[mid];
如果 v<m,去[begin, mid)范围内二分搜索
如果 v>m,去[mid + 1, end)范围内二分搜索
如果 v=m,直接返回mid,就是要搜索的值所在位置
不难写出二分查找的代码:
/**二分查找,并返回index*/
public static int binarySearch(int[] array, int value)
{
if (array == null || array.length == 0) return -1;
int begin = 0;
int end = array.length;
while (begin < end) {
int middleIndex = begin + (end - begin)/2;
int middleValue = array[middleIndex];
if (value > middleValue) {
begin = middleIndex + 1;
}else if (value < middleValue) {
end = middleIndex;
}else {
return middleIndex;
}
}
return -1;
}
当然,你也可以使用递归来写:
public static int binarySearch2(int[] array, int value, int begin, int end)
{
if (array == null || array.length == 0) return -1;
int middleIndex = begin + (end - begin)/2;
int middleValue = array[middleIndex];
if (begin < end) {
if (value > middleValue) {
return binarySearch2(array, value, middleIndex + 1, end);
}else if (value < middleValue) {
return binarySearch2(array, value, begin, middleIndex);
}else {
return middleIndex;
}
}
return -1;
}
有一个问题需要注意的是,如果在数组里面有相同元素,二分查找找到的下标是不确定的。
比如[1, 1, 1, 1, 1, 1]
优化方案二:
利用二分查找,找出待插入的位置,然后将提前备份的元素插入合适的位置。
比如:
index 0 1 2 3 4 5 6 7
value 2 4 8 8 8 12 14 0
如果value = 5, 则插入位置为2
如果vaue = 1, 则插入位置为0
如果value = 15, 则插入位置为7
如果vaue = 8, 则插入位置为5
要求二分搜索返回的插入位置:第一个大于value的元素位置
明显,前面两种的二分查找并不能满足我们的要求,我们需要对二分查找进行改造。
public class test {
public static void main(String[] args) {
int[] array = {2, 4, 8, 8, 8, 12, 14};
insertSort(array);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
public static void insertSort(int[] array)
{
for (int begin = 1; begin < array.length; begin++) {
//保存需要插入的元素值
int beginValue = array[begin];
//需要插入的位置
int insertIndex = binarySearch(array, begin);
//从右往左依次后移
for (int i = begin; i > insertIndex; i--) {
array[i] = array[i - 1];
}
//将待插入值放在待插入位置
array[insertIndex] = beginValue;
}
}
/**输入一个index,寻找index的值需要插入的位置index*/
public static int binarySearch(int[] array, int index)
{
int value = array[index];
int begin = 0;
int end = index;
while (begin < end) {
int middleIndex = begin + (end - begin)/2;
int middleValue = array[middleIndex];
if (value >= middleValue) {
begin = middleIndex + 1;
}else {
end = middleIndex;
}
}
return begin;
}
}
需要注意的是,使用了二分搜索后,只是减少了比较次数,但插入排序的平均时间复杂度依然是O(n^2)。
return end;也可以,因为,while(begin < end){},能走到后面,则begin >= end