1、时间复杂度
算法的时间复杂度是一个函数,其定量的描述了一个算法运行时间和输入规模之间的关系。通常用O表示,且不包括这个函数的低阶和首项系数。如果一个算法的执行时间为2n2+5n+4,那么该算法时间复杂度就可以表示为O(n2)。
一般的时间复杂度,由好到坏大概有这么几种O(1)、O(logn)、O(n)、O(nlogn)、O(nk)(k>=2),一般情况下,当算法时间复杂度高于O(n2)时,性能就变得相当差,此时就该想办法寻求更优的方案。
O(n^2)的情形
for(int i=0;i<n;i++){
//code…
for(int j=0;j<n;j++){
//code…
}
}
O(nlogn)的情形
for(int i=0;i<n;i++){
//code…
for(int j=i;j<n;j++){
//code…
}
}
O(logn)的情形
for(int i=0;i<n;i+=2){
//code…
}
O(1)的情形
//与n无关的有限次的表达式,例如赋值,简单的运算等
2、空间复杂度
空间复杂度是一个算法执行过程中所消耗的临时空间的一个度量。同时间复杂度一样,也不包括这个度量函数的低阶项和首项系数。相对的应的,空间复杂度也有O(1)、O(logn)、O(n)、O(nlogn)、O(n^k)(k>=2)。
3、稳定性
在排序算法中,评估一个算法的优劣,除了时间复杂度和空间复杂度以外,还有一个衡量指标就是稳定性。在一个待排序的序列中,可能存在多个相等的项,经过排序后如果这些项的相对次序保持不变,则我们说这个算法是稳定的,否则就是不稳定的。
研究稳定性的意义在于,如果算法是稳定的,那么第一个元素排序的结果可以被第二个等值的元素在排序时所用,也就是说可以避免多余的比较。
4.桶排序
1、桶排序思想
一个简单例子:
对6个人的英语测试成绩(110分)进行排序。假如分数是[6,5,8,8,10,9],用桶排序的思想就是准备10个桶,编号依次为110,将成绩放入对应的桶中,例如6分放入6号桶,两个8分放入8号桶…然后按照桶的标号顺序逐一输出(有就输出,没有就不输出),这就是桶排序的基本思想。
事实上,这只是一个简易版,试想一下,如果待排序的元素跨度范围比较大,例如1~10000,是不是需要10000个桶?实际上这种情况下,一个桶里并非总放一个元素,很多时候一个桶里放多个元素。其实真正的桶排序和散列表有一样的原理。
实际排序中,通常对每个桶中的元素继续使用其他排序算法进行排序,所以更多时候,桶排序会结合其他排序算法一起使用。
3、桶排序性能特点
桶排序实际上只需要遍历一遍所有的待排元素,然后依次放入指定的位置。如果加上输出排序的时间,就要遍历所有的桶。因此桶排序的时间复杂度是O(n+m),n是待排元素的个数,m是桶的个数,也就是待排元素的范围。这个算法算是相当快的排序算法了,但是空间复杂度比较大。
当待排元素的大小范围比较大,但待排元素个数比较少时,空间浪费就比较严重,待排元素分布月均匀,空间利用率越高,事实上这种情况很少见。
通过以上性能分析,可以得出桶排序的特点:速度快且简单,但同时空间利用率较低。当待排数据跨度很大时,空间利用率是无法忍受的。
4、桶排序适用场景
根据桶排序的特点,桶排序一般适用于一些特定的环境,比如数据范围较为局限或者有一些特定的要求,比如需要通过哈希映射快速获取某些值,需要统计每个数的数量。但是这一切都以确认数据的范围为前提,如果范围跨度过大,则考虑用其他算法。
5.快速排序
1、快速排序思想及原理
事实上,快速排序是堆冒泡排序的一种改进。
它的基本思想是:通过一趟排序将要排序的数据分割为两部分,第一部分所有数据比第二部分的所有数据小,按照这种思路将两部分数据再次分别进行快速排序,可以使用递归完成,最终使得整个数据序列有序。
具体来讲,在待排数据中找一个基准数据(通常取第一个数),接下来将待排数据中比基准数据小的放在待排数据的左侧,将比待排数据中比基准数据大的放在待排数据右侧。此时,左右两个分区的元素相对有序,接着采用上述思路继续对左右两个分区继续排序,直到各分区只有一个元素位置。这里用到了一个典型的分治思想。下面举例说明:
待排序列依次为47、29、71、99、78、19、24、47。为了区分两个47,将后面的47下面增加一个下划线。
步骤:
1、选取一个基准数,一般选第0个元素47。
2、将比基准数小的移动到左侧,比基准数大的移动到右侧,相等的不移动,此时基准数位置为K。
3、对左右两侧重复步骤1和步骤2,直到左右侧细分到只有一个元素。
快排的难点也就是在第2步,怎么移动各个数据?
<1> 首先从数列的右边开始往左找,设下标为i,也就是i–操作,找到第一个比基准数小的值,让他与基准数交换;
<2> 接着开始从左往右找,设下标为j,也就是j++,找到第一个比基准数大的值,让他与基准数交换位置;
<3> 重复1和2,直到i和j相遇时结束,最后基准值所在位置为k。
2、java快排代码
public class QuickSort {
private int[] array;
public QuickSort(int[] array){
this.array = array;
}
public void printSort(){
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
}
public void sort(){
quicksort(array,0,array.length -1);
}
private void quicksort(int[] array,int begin,int end){
if(begin<end){ //i和j没相遇之前比较各数据与基准值大小
int base = array[begin]; //取第一个值为基准值
int i = begin; //左标记为i
int j = end; //右标记为j
//一趟排序,找到比基准值大的在基准值右,比基准值小的在基准值左
while(i<j){
//从右往左扫描
while(i<j && array[j]>base){ //从右往左扫,如果元素比基准值大
j--; //则右边标记--,直到找到第一个比基准值小的,停止扫描
}
if(i<j){
array[i]=array[j]; //交换右扫描第一个比基准值小的数
i++; //i标记右移一位
}
//从左往右扫描
while(i<j && array[i]<base){//从左往右扫,如果元素比基准值小
i++; //则左标记++,直到找到第一个比基准值大的,停止扫描
}
if(i<j){
array[j]=array[i]; //交换左扫描第一个比基准值大的数
j--; //j标记左移一位
}
} //此时基准值左右两侧相对有序
array[i] = base; //此时i为中间位置k
quicksort(array,begin,i-1); //左侧按照快排思路,递归
quicksort(array,i+1,end); //右侧按照快排思路,递归
}
}
}
测试代码
public class SortTest {
public static void main(String[] args) {
teseQuickSort();
}
private static void teseQuickSort(){
int[] array = {3,5,7,3,8,9,6,1,0};
QuickSort qs = new QuickSort(array);
qs.sort();
qs.printSort();
}
}
3、快排的特点及性能
快排是在冒泡排序之上改进而来的,冒泡排序每次只能交换相邻的两个元素,而快排则是跳跃式的交换,交换距离很大,总的比较次数和交换次数少了很多,速度也快了很多。
快排的平均时间复杂度为O(nlogn),事实上,大多数情况下,排序速度要快于这个时间复杂度。快排实际上是采用的一种分而治之的思想,把问题分解为一个个的小问题去逐一解决,最终在把结果组合起来。
快排因为递归调用,所以空间复杂度为O(logn)。
快排是一种不稳定的排序算法,在经过排序后,等值的元素的相对位置可能发生改变。
快排基本上被认为是相同数量级中所有排序算法中平均性能最好的。
4、快排的适用场景
快排由于相对简单而且性能不错,所以快排适用于几乎绝大多数场合。
6.插入排序
1、插入排序的思想及原理
插入排序一般分为直接插入排序和二分插入排序,本文只介绍直接插入排序,两者的区别仅在于插入的方式不一样。
插入排序是在待排序数组里插入数据。一般我们认为插入排序就是往一个已经排好序的数列中插入一个元素,使得插入这个数以后,数组仍然有序。
下面具体介绍下插入排序的思路:
首先需要明确待排序的数组由两部分组成,一部分是已经排好序的部分,另一部分是待排序的部分。
接着我们每次选取待排序部分的第一个元素,分别与前面排好序的元素进行比较。当大于前面元素时,可以将该元素直接进入已排好序的部分; 当小于前面元素时,需要把这个元素拿出来暂存,将前面的元素后移,继续与前面的元素相比,直到比较到数组第一个元素或者出现第一个小于拿出的这个元素,这时停止比较、移动,直接把这个元素放到当前空位上。
一直重复步骤2,直到待排元素已经没有元素可进行插入时,停止操作,当前数列为已排好序的数列。
2、插入排序java代码实现
首先最外层必定有个大循环,用于待排序部分的数列。还需要一个内层循环,分别与前面排好序的部分进行比较和移动,直到找到位置可以进行插入。参照扑克牌摸牌后排序
public class InsertSort {
private int[] array;
public InsertSort(int[] array){
this.array = array;
}
public void insertSort(){
if(array==null){
throw new RuntimeException("没有待排数组");
}
int length = array.length;
if(length>0){
for(int i=1;i<length;i++){
int temp = array[i]; //记录未排好序的第一个元素为temp
int j = i;
for(;j>0&&array[j-1]>temp;j--){ //原理中的步骤2
array[j] = array[j-1]; //移位
}
array[j] = temp; //插入
}
}
}
public void print(){ //用于打印排完序后的数组
for(int i=0;i<array.length;i++){
System.out.println(array[i]);
}
}
}
测试程序
public class SortTest {
public static void main(String[] args) {
insertSortTest();
}
private static void insertSortTest(){
int[] array = {3,5,0,7,1,4,6};
InsertSort is = new InsertSort(array);
is.insertSort();
is.print();
}
}
3、插入排序的特点及性能
插入排序和玩扑克牌摸牌后在手中排序一样的原理,比较容易理解。插入排序在序列近似有序时,效率比较高,因为此时减少了比较和移动的次数。
从原理和代码来看,插入排序的时间复杂度尾O(n^2),外层循环执行n次,内层在最坏的情况下也执行n次,并且除了比较操作还有移动操作。最好的情况是序列近似有序,这时内层循环只需比较及移动较少个元素即可完成。当序列本身有序时,插入排序的时间复杂度为O(n)。因此,在数列越有序,效率越高。
空间复杂度为O(1),是常量级的。因为只用了一个变量暂存每次未排好序的首个元素。
插入排序是稳定的排序算法,因为是在相对排好序的基础上进行比较和移动,所以可以保持相对顺序不变,所以是稳定的排序算法。
4、插入排序的适用场景
插入排序的特点是在近似有序的情况下效率比较高。但因为其时间复杂度为O(n^2),所以通常并不单独适用。在所有的排序算法中,我们优先使用快速排序。快速排序在分区规模达到一定的值时(比如10左右),我们改用插入排序算法排该分区。因为此时的分区内数据往往是近似有序的,所以使用快排并不一定优于插入排序。在很多高级语言在内部对快速排序的实现中,也是在分区达到一定规模改用插入排序来排该分区。