选择排序
流程
动态演示图
插入排序
流程
动态演示图
注意事项
插入排序是一个重点排序算法,因为当排序的数组基本有序的时候,插入排序的效率甚至高于快速排序,在最极端的情况下,数组完全有序,插入排序的时间复杂度仅为O(n)级别,只需遍历一遍无需任何操作。
归并排序
流程
动态演示图
算法优化
在编写代码的过程中,我们可以在归并排序的代码中加入一条判断,如果第二个数组中的头元素大于第一个数组中的尾元素则直接跳过merge操作,因为两个数组已经有序,第二个数组中的头元素大于第一个数组中的尾元素说明第二个数组中的最小值比第一个数组中的最大值都要大,此时两个数组直接合并已经有序,无需merge消耗时间。
算法思想应用
归并排序在拆分后依次执行归并(merge)操作,判断两个拆分后数组头那个小,小的值放到merge后的数组中第一个空缺位置,由于两个数组在进行merge之前已经经历了无数次拆分和merge已经分别有序,所以,我们可以利用这一点计算数组中的逆序对,假设第一个数组头小于第二个数组头,那么它一定小于第二个数组所有元素(因为已经有序)
快速排序
流程
动态演示图
算法优化
快排的过程可以被看作是一棵二叉树,根节点为V,左子树小于V,右子树大于V,快排的优化也就是针对这个树结构进行思考。
- 当要进行快排的数组近乎有序甚至最坏的情况完全有序时,可以想象,所有节点全部集中在右子树,极度不平衡,此时的二叉树变为一个链表,快排算法的复杂度也退化到了O(n^2)级别,此时的优化思路就是尽量让树结构保持平衡,解决方法为:对于V的选择不再从第一个节点开始,而是通过随机数随机选择一个节点开始,这样,即便数组有序我们也能保证节点不完全偏向一侧。
-
当要进行快排的数组含有大量的重复值时,同样会导致我们的树变得极为不平衡,因为相等的值总会被分到其中一侧,此时的优化思想同样是想办法让树结构尽量变得平衡,我们要将相等的元素平分到两个子树当中,采取的方法是双路快排,即定义两个变量分别从头和尾开始遍历,当第一个变量i遇到>=V的值,停止,第二个变量j开始从后往前遍历,遇到<=V的值,停止,交换i和j的值,直到i>j或i,j遍历到边界时全部遍历完成,这样,就能将相等的值平分到两个子树中。
-
针对双路快排,由于内部需要大量的交换操作,使得代码的运行效率有所下降,此时,我们还可以减少交换操作,另设变量,将数组分为<V,=V,>V三个部分,同时维护三个部分的“指针”,遇到相等值时直接维护中间部分的“指针”即可,即三路快排。
算法思想应用
由快速排序的流程,我们可以发现,针对排序元素V,一趟排序结束后,V此时一定在排好序数组的最终位置,根据这个特点,我们可以利用快速排序的思想找数组排好序后指定位置n的元素,首先通过快排确定一个元素的位置,若等于n输出即可,若大于n我们只需要继续对以V为根的左子树进行判断即可。
堆排序
介绍堆排序前,先介绍下堆的相关操作及性质。
堆
堆也可以看做一个二叉树,不过,堆的性质是其子节点永远不大于父节点,根据需要也可以变为不小于,且是一棵完全二叉树,设堆的实现方式为数组a大小为n,从索引1开始存储,根节点a[1],最后一个叶子结点a[n],某节点x的父节点a[x/2],某节点x的左孩子a[x*2],某节点x的右孩子a[x*2+1]。
根据堆的性质我们可以知道,堆的根节点永远是堆中最大(最小)的值,根据这个性质,我们可以从堆中依次取出堆的根节点,直到全部取出,即完成了排序。要完成这个操作我们还需要认识堆的三个基本操作,shiftup、shiftdown和heapify,其中shiftup主要用于向堆中添加元素从下向上浮动直到找到合适的位置,shiftdown主要用于删除最大值时维持堆的性质,heapify用于将一个数组变为堆。
shiftup
shiftup一般用于插入元素,首先我们将插入的元素放到数组末尾,然后依次和父节点向上比较,比父节点大,交换,直到元素的父节点大于自身或已经位于根节点,结束。
shiftdown
shiftdown一般用于删除最大值(根节点)后维持剩余元素保持堆的性质或和heapify组合进行数组变堆及排序操作,首先我们将根节点删除,将最后一个元素放到根节点,然后依次向下比较小于子节点则将最大的子节点和自身交换,直到下面的所有子节点都小于自身或者已经处于叶子结点,结束。
heapify
heapify一般用于实现数组变堆和排序操作,视每一个非叶子结点及其子节点为一棵二叉树,对其进行shiftdown操作,全部执行完毕后原数组便具有了堆的性质。
以数组[33,32,2,27,39,53,94,30,35,56,22,54,84,11,31]为例:
堆排序
堆排序有三种思路,分别为将所有元素依次存入堆再按取出根节点方式依次取出、利用heapify创建堆再按取出根节点方式依次取出、原地排序(直接在数组中排序,无需取出),其中前两种思路类似,差别只是在堆的创建方式上,利用heapify的方式将有一半的元素不需要管。
堆插入法
这种方法思路很简单,就是将数组中的元素一个个取出依次通过上面的插入方法插入到一个堆中,然后再从根节点一个个取出即可。
heapify建堆法
这种方法整体思路和堆插入法类似,都是利用堆的性质依次取出当前堆中的最大值,区别是不像堆插入法一个个执行shiftup插入,而是直接对整个数组操作,通过heapify直接把数组变成堆然后再依次从根节点取出。
前两种方法的大致排序思路如下,区别只在建堆过程,将取出的元素依次存入数组即实现了从大到小排序:
原地堆排序
原地堆排序,顾名思义,即在原来数组上直接操作进行排序,不需要单独空间用来存储最终结果或者数组创建的堆,实现原地堆排序首先在原数组利用heapify变成一个堆,然后将变成堆后的数组头尾节点交换(此时内部发生的转换分解为,头结点变到尾,即最大值放到当前数组最后一个,尾结点放到头部,此时正是和删除节点的思路一样,将最后一个元素放到删除的根节点位置)再对从头到n-1(假设一共有n个元素,而最后一个元素刚刚通过堆的性质已经确定为最大,位置已经确定)执行shiftdown使其再次变为堆,取出根节点放到n-1的位置,以此类推。
算法思想应用
根据堆排序的性质,我们可以用来求解一个数组中的前n个数,即循环多次取堆的根节点。
算法比较
平均复杂度 | 原地排序 | 额外空间 | 稳定性 | |
插入排序 | O(n^2) | √ | O(1) | √ |
归并排序 | O(nlogn) | × | O(n) | √ |
快速排序 | O(nlogn) | √ | O(logn) | × |
堆排序 | O(nlogn) | √ | O(1) | × |
注:其中,稳定性指的是排序前后相等元素相对位置不变,如[3,3],排序后原来前面的3还在前面,后面的还在后面