什么是排序
- 排序(sorting)的功能是将一个数据元素的任意序列,重新排列成一个按关键字有序的序列
- 其确切定义为:
假设有n个数据称元素序列{R1,R2,…,Rn},其相应关键字的序列是{K1,K2,…Kn},通过排序要求找出下表1,2,…,n的一种排列p1,p2,…,pn,使得相应关键字满足如下的非递减(或非递增)关系: Kp1 ≤ \leq ≤ Kp2 ≤ \leq ≤… ≤ \leq ≤Kpn。这样,就得到一个按关键字有序的纪录序列:{Rp1,Rp2,…Rpn}
内部排序和外部排序
- 一类是整个排序过程在内存储器中进行,称为内部排序
- 另一类是由于待排序元素数量太大,以至于内存储器无法容纳全部数据,排序需要借助外部存储设备才能完成,这类排序称为外部排序。
- 本章介绍的排序方法都属于内部排序
稳定排序和不稳定排序
-
如果在待排序的序列中存在多个具有相同关键字的元素
-
假设Ki=Kj (1 ≤ \leq ≤ i ≤ \leq ≤n, 1 ≤ \leq ≤ j ≤ \leq ≤n, i ≠ \neq =j),若在排序之前的序列中Ri在Rj之前
-
经过排序后得到的序列中Ri仍在Rj之前,则称所用的排序方法是稳定的
-
否则,当相同关键字元素的前后关系在排序中发生变化,则称所用的排序方法是不稳定的
-
无论是稳定的还是不稳定的排序方法,均能玩车过排序的功能
-
在某些场合可能对排序有稳定性的要求,此时就应当选择稳定的排序方法
-
例如,假设一组学生纪录已经按照学号有序,现在需要根据学生的成绩排序,当分数相同时要求学号小的学生在前,显然此时对分数进行排序就必须选择稳定的排序方法
比较排序和非比较排序
大部分排序都是需要通过比较首先来判断大小,作为排序的依据
但是也有例外的,比如计数排序、基数排序,不需要进行比较
- 插入排序:将无序子序列中的一个或几个记录“插入”到有序序列中,从而增加记录的有序子序列的长度
- 交换排序:通过“交换”无序序列中的纪录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度
- 选择排序:从记录的无序子序列中“选择”关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加有序子序列的长度
- 归并排序:通过“归并”两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度
排序类型
一般说八大排序类型
另外还可以加上非比较的计数排序、选择排序中的属性选择排序、插入排序中的这般插入排序
排序效率
时间复杂度最高的就是三种基本排序:直接插入、简单选择、冒泡排序
建议优先掌握直接插入、简单选择、冒泡排序、快速排序
- 直接插入排序、简单选择排序、冒泡排序是最简单的三种排序算法,时间复杂度也最高O( n 2 n^2 n2),作为基础排序,面试中有被问到,三种都要掌握
- 三种简单排序算法虽然简单,但是效率低下;高级排序在简单排序的基础上优化,算法复杂,换取的是性能提高,同时可能需要更多的辅助空间
- 快速排序和归并排序都使用了分治和递归,所以面试时被问到的机会比较高,尤其是快速排序。
- 从时间性能上看,快速排序是所有排序算法中实际性能最好的,然而快速爬 u 需在最坏情况下(数据基本有序)的时间性能不如堆排序和归并排序,并且空间复杂度高,所以更适合数据不大的情况
- 堆排序在任何情况下,其时间复杂度为O(nlogn)。这相对于快速排序而言是堆排序的最大优点。堆排序在元素较少时由于消耗较多时间在初始建堆上,因此不值得提倡,然而当元素较多时还是很有效的排序算法
- 与快速排序和堆排序相比,归并排序的优点是它是一种稳定的排序方法,最坏情况下时间性能好
- 从方法稳定性上来看,大多数时间复杂度为O( n 2 n^2 n2)的排序均是稳定的排序方法,除简单选择排序外。而多数时间性能较好的排序,例如快速排序、堆排序、希尔排序都是不稳定的
- 基于比较的排序的时间复杂度的下限是Ω(nlogn),即这已经是最高的效率了
- 如果在面试中有面试官要求你写一个O(n)时间复杂度的排序算法,使用非比较的排序(计数排序、基数排序)可以达到线性时间O(n)复杂度的排序。只不过有前提条件,就是待排序的数要满足一定的范围的整数,而且可能需要较多辅助空间
- 需要结合具体的要求和场景来选择甚至组合使用,才能达到高效稳定的目的。没有最好的排序,只有最适合的排序
快速排序
- 快速排序是冒泡排序的改进版,也是最好的一种内排序,还涉及到分治和递归,在很多面试题中都会出现,也是作为程序员必须掌握的一种排序方法
- 冒泡排序记录的比较和交换是在相邻的单元中进行,每次交换只能上移或者下移一个单元,因而总的比较和移动次数较多。
- 快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-Conquer Method)。该方法的基本思想是:
(1)先从数列中取出一个数作为基准数(简单起见可以取第一个数)
(2)分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边(分区)
(3)再对左右区间重复第一步、第二步,直到各区间只有一个数(递归)
快速排序=冒泡+分治+递归
快速排序的过程:东拆西补或西拆东补,一边拆一边补
public class TestQuickSort{
public static void main(String[] args){
//给出无序数组
int arr[] = {72,6,57,88,60,42,83,73,48,85};
//输出无序数组
System.out.println(Arrays.toString(arr));
//快速排序
quickSort(arr);
//输出有序数组
System.out.println(Arrays.toString(arr));
}
private static int partition(int[] arr, int low, int high){
//指定左指针i和右指针j
int i=low;
int j=high;
//将第一个数作为基准值,挖坑
int x=arr[low];
//使用循环实现分区操作
while(i<j){
//从右向左移动j,找到第一个小于基准值的值arr[j]
while(arr[j]>=x && i<j){
j--;
}
//将右侧找到的小于基准数的值加入到左边的坑中,左指针i++,向中间移动一个位置
if(i<j){
arr[i] = arr[j];
i++;
}
//从左向右移动i,找到第一个大于等于基准值的值arr[i]
while(arr[i]<x && i<j){
i++;
}
//将左侧找到的大于等于基准值的值加入到右边的坑中,右指针j--,向中间移动一个位置
if(i<j){
arr[i]=arr[j];
j--;
}
}
//使用基准值填坑,这就是基准值的最终位置
arr[i] = x; //arr[j] = x;
//返回基准值的位置索引
return i; //return j;
}
private static void quickSort(int[] arr,int low, int high){
//分区操作,将一个数组分成两个分区,返回分区界限的索引
if(low<high){
int index = partition(arr,low,high);
//对左分区进行快排
quickSort(arr,low,index-1);
//对右分区进行快排
quickSort(arr,index+1,high);
}
}
public static void quickSort(int[] arr){
int low = 0;
int high = arr.length-1;
quickSort(arr,low,high);
}
}
快速排序算法的分析
- 当分区选取的基准元素为待排序元素中的最大或最小值时,为最坏情况,时间复杂度和直接插入排序的一样,移动次数达到最大值
Cmax = 1 +2 +…+(n-1) = n*(n-1)/2 = O(n2),此时最好时间复杂度为O( n 2 n^2 n2) - 当分区选取的基准元素为待排序元素中的“中值”,为最好的情况,时间复杂度为O( n l o g 2 n nlog_2n nlog2n)
- 快速排序的空间复杂度为O( l o g 2 n log_2n log2n)(用到了递归,当然占用空间多了)
- 当待排序元素类似[6,1,3,7,3]且基准元素为6时,经过分区,形成[1,3,3,6,7],两个3的相对位置发生了改变,所以快速排序是一种不稳定排序
快速排序算法的分析2
- 时间效率:快速排序算法的运行时间依赖于划分是否平衡,即根据枢轴元素pivot将序列划分为两个子序列中的元素个数
- 而划分是否平衡又依赖于所使用的枢轴元素