知识导航
一.插入排序
(1)插入排序的原理与基本实现
插入排序的原理类似于,我们平常打扑克牌发牌时的动作
当我们接到一个新的牌,我们将新得到的牌插入到前面已经整理号顺序的一段有序的牌序列 ,反复执行这个过程。牌自然就有序了。
插入 排序的原理就是这样,从第二个元素开始每一个元素作为一个新的元素插入到前面的序列中,具体的执行过程如下
(1) 在执行过程中,插入排序会将序列分为2部分
✓ 头部是已经排好序的,尾部是待排序的
(1) 从头开始扫描每一个元素
✓ 每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然保持有序
基本实现方式
public void InsertSort(Integer [] array)
{
for(int begin=1;begin<array.length;begin++)
{
int cur=begin;
//cur>0防止cur左越界,防止cur小于0,等于0的时候也没有必要
//再向左进行比较了
while(cur>0&&array[cur]<array[cur-1])
{
int a=array[cur];
array[cur]=array[cur-1];
array[cur-1]=a;
//继续向左移动一位进行比较
cur--;
}
}
}
(2)逆序对与稳定性分析
◼ 什么是逆序对?
❤️数组 <2,3,8,6,1> 的逆序对为:(2,1) (3,1) ,(8,1), (8,6) (6,1),共5个逆序对 (简言之就是我们现在要对数组进行从小到大的排序,如果一个前面的数比后面的数大,那么这两个数组成的数对就是逆序对)
(1) 插入排序的时间复杂度与逆序对的数量成正比关系
(2) 逆序对的数量越多,插入排序的时间复杂度越高(逆序对越多进行的交换就越多)
对于逆序对最多的情况如下(数组全都是按逆序排列的):
则交换的次数就是1+2+3+4+…n-1。
(1)最好时间复杂度:O(n)
(2)最坏时间复杂度;O(n2) (内外部循环都是n)
◼ 空间复杂度:O(1) (插入排序属于原地算法并没有额外开辟空间)
◼ 属于稳定排序
(3)插入排序的优化(改交换为覆盖)
前面我们提到在进行比较的过程中只要待插入元素比当前元素小就要进行交换,其实这样有些浪费时间,我们可以将交换优化为覆盖,先将当前待插入元素缓存,然后依次与前面发元素进行比较吗,找到插入位置过程中依次将元素后移,然后将缓存的数据加入到序列中,这样说可能有些难以理解,请看下面的动图:
二.二分搜索
(1)二分搜索的实现原理
如何确定一个元素在数组中的位置?(假设数组里面全都是整数)
(1)如果是无序数组,从第 0 个位置开始遍历搜索,平均时间复度:O(n)
(2)如果是有序数组查找则没有必要进行复杂度为O(n)的一个元素一个元素的查找,我们可以利用二分查找。
二分查找的过程
假设在有序数组中查找某个元素v,mid=(begin+end)>>1;
如果v<mid,在更小的范围[begin,mid)内查询。
如果v>mid,在范围[mid+1,end)内查询
直到v==mid找到指定元素,返回下标。
实例1:搜索10
实例2:搜索3
由于这里设计的end是arr.length,是数组长度加1.所以end-begin是数组的元素个数(也就是数组的实际长度),所以只有begin<end的时候才有要查找的元素,一旦begin==end就表明,查找了所有元素都没有找到。
代码实现
public static int binarySearch1(Integer [] array,int k){
if(array==null||array.length==0)return -1;
int begin=0;
//这么写的好处是可以轻易得出数据的个数(元素个数)
int end=array.length;
while(begin<end)
{
//(end+begin)
int mid=(end+begin)>>1;
//[mid+1,end)
if(k>array[mid]){
begin=mid+1;
}else if(k<array[mid]){
end=mid;
}else{
return mid;
}
}
return -1;
}
(2)插入排序的二分搜索优化
上面我们提到了二分查找的基本实现,如果我们利用二分查找的思想,查找目标值应该插入的位置,直接将规定的部分数组后移,再将查找到位置赋值,效率一定会得到一定的提高(因为不用一个一个的比较找位置了),请看图示。
将二分查找代码改为查找插入位置
public static int binarySearch2(Integer [] array,int k){
if(array==null||array.length==0)return -1;
int begin=0;
//这么写的好处是可以轻易得出数据的个数(元素个数)
int end=array.length;
while(begin<end)
{
//(end+begin)
int mid=(end+begin)>>1;
//[mid+1,end)
//为什么相等没有分出来,因为现在是再查找插入位置而不是再找元素下标
if(k>array[mid]){
begin=mid+1;
}else {
//找插入位置不是找对应元素的位置,所以即使相等也要继续进行搜索
//因为找插入位置其实就是找第一大于该元素的数据元素的下标
end=mid;
}
}
//返回应该插入的位置
return begin;
//2,5,4,67,658,745.插入60就是在找第一个比他的元素的下标
}
三.归并排序
1.归并排序的原理与实现
归并顾名思义就是将数组无限二分(对每次二分的子数组分别进行排序)直到不能再继续往下分的时候,将子数组再次合并为完整数组此时数组就是有序的。具体 发流程就是:
(1) 不断地将当前序列平均分割成2个子序列
✓ 直到不能再分割(序列中只剩1个元素)
(2) 不断地将2个子序列合并成一个有序序列
✓ 直到最终只剩下1个有序序列
问:为什么最后就有序了?
因为再不断归并的过程中,由于对于每一个子数组都进行了有序化,所以逆序对再不断减少,直到最后逆序对逐渐转化为0,该数组完成了排序。
归并排序的代码实现
代码规划:此类继自承辅助的抽象类Sort.java(可以再我上一篇文章中的末尾看sort.java类的具体代码 【数据结构与算法】第十三篇:冒泡,选择,堆排序)
package 排序;
@SuppressWarnings({"unchecked","unused"})
public class MergeSort <E extends Comparable<E>> extends Sort<E> {
//创建一个数组缓存左半部分的数组
//因为原地进行数组前后段操作会造成混乱
//因为分割,合并操作都需要这个缓存数组。所以定义为成员变量
private E [] leftArray;
@Override
public void sort() {
leftArray=(E[])new java.lang.Comparable[array.length>>1];
//这里为了取数组长度,end还是设为数组长度+1
sort(0,array.length);
}
private void sort(int begin,int end){
//所谓的分割也只是各取数组的一半进行操作并不是真正意义上的分割:各取一半进行排序
if(end-begin<2)return;
int mid=(begin+end)>>1;
//在递归调用中begin不一定是0,不能用0代替begin
sort(begin,mid);//[begin,mid)
sort(mid,end);//[mid,end)
merge(begin,mid,end);
}
private void merge(int begin,int insert,int end){
//从零开始截取前半段
int li=0,le=insert-begin;//begin不一定是0
int ri=insert,re=end;
int ai=begin;//从整个单元数组开头进行扫描(记录覆盖到哪里了)
//备份左边数组
for(int i=li;i<le;i++)
{
leftArray[i]=array[begin+i];
}
//左边先走完->则不用关数组了
//右边先走完了,左边依次覆盖到剩余位置
//左边好没走完
while(li<le) {
//右边也没有走完
//规避数组越界
//左边等于还是小于都进行覆盖不仅保证了稳定性还保证了正确性
if (ri<re&&com(array[ri],leftArray[li]) < 0) {
array[ai++] = array[ri++];
} else {
array[ai++] = leftArray[li++];
}
}
}
}
2.归并排序的复杂度分析
常见递推式对应的时间复杂度
对于归并排序
如图所示归并排序的时间消耗为T(n)=2T(n/2)+T(n)
对应到图表中可以得出此种算法的时间复杂度为O(nlogn)
推导过程:作为了解
四.效率对比
到此我们已经介绍了冒泡,选择,堆,插入,归并排序。
我们通过时间测试工具来进行一个效率对比。
在数据规模足够大的前提下(传入一个数组:数组元素随机生成,元素数量10000,范围1-100000)
可见数据规模很大时,归并排序还是十分优秀的🥳🥳🥳🥳