算法概述
github 地址:https://github.com/ZSZ2018211261/top-ten-sorting-algorithms
0.1 算法分类
十种常见排序算法可以分为两大类:
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
0.2 算法复杂度
0.3 相关概念
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
- 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
- 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
- **空间复杂度:**是指算法在计算机
内执行时所需存储空间的度量,它也是数据规模n的函数。
1、冒泡排序(Bubble Sort)
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
1.1 算法描述
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
1.2 动图演示
1.3 代码实现
public static <K extends Comparable<K>>void sort(K[] nums){
if(nums==null)return;
int length = nums.length;
//标准冒泡排序,从前往后比,大的元素后移
for (int i = length-1; i > 0; i--) {
for (int j = 0; j < i; j++) {
if(less(nums[j+1],nums[j])){
swap(nums,j,j+1);
}
}
}
}
/**
* 情况一:处理在排序过程中数组整体已经有序的情况
* 思路:增加一个flag用于判断本轮循环有没有元素进行交换,如果没有则说明已经排好序
* @param nums
* @return
*/
public static <K extends Comparable<K>> void sortWithFlag(K[] nums){
if(nums==null)return;
int length = nums.length;
//标准冒泡排序,从前往后比,大的元素后移
for (int i = length-1; i > 0; i--) {
boolean flag = true;
for (int j = 0; j < i; j++) {
if(less(nums[j+1],nums[j])){
swap(nums,j,j+1);
flag = false;
}
}
if(flag)break;
}
}
/**
* 情况二:处理在排序过程中数组局部已经有序的情况 + 情况一
* @param nums
* @return
*/
public static <K extends Comparable<K>> void sortWithCheckBound(K[] nums){
if(nums==null)return;
int length = nums.length;
//上一次交换元素的下标
int last_swap_index = length-1;
//标准冒泡排序,从前往后比,大的元素后移
for (int i = length-1; i > 0; i--) {
boolean flag = true;
for (int j = 0; j < i; j++) {
if(less(nums[j+1],nums[j])){
swap(nums,j,j+1);
last_swap_index = j+1;
flag = false;
}
}
if(flag)break;
i=last_swap_index;
}
}
/**
* 情况三:
* 同时找到最大,最小值,排序 + 情况二 + 情况一
* @param nums
* @return
*/
public static <K extends Comparable<K>> void sortBilaterally(K[] nums){
if(nums==null)return ;
int length = nums.length;
boolean flag = true;
int l = 0;
int r = length-1;
//上一次交换元素的下标
int l_last_swap_index = l;
int r_last_swap_index = r;
//从前往后比,大的元素后移,小的元素往前移
while(l<r && flag){
flag = false;
for (int j = l; j < r; j++) {
//大的元素后移
if(less(nums[j+1],nums[j])){
swap(nums,j,j+1);
r_last_swap_index = j+1;
flag = true;
}
//小的元素往前移
if(less(nums[r+l-j],nums[r+l-j-1])){
swap(nums,r+l-j,r+l-j-1);
l_last_swap_index = r+l-j;
flag = true;
}
}
l = l_last_swap_index;
r = r_last_swap_index;
}
}
2、选择排序(Selection Sort)
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
2.1 算法描述
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
- 初始状态:无序区为R[1…n],有序区为空;
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
- n-1趟结束,数组有序化了。
2.2 算法的稳定性
是一个不稳定算法
那么,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。
2.3 动图演示
2.4 代码实现
/**
* 无优化的经典排序 是一个不稳定的算法
*
* 不稳定性举例:
*
* 2(第一个2),3,2(第二个2),1,8,5
* 下标 :0 1 2 3 4 5
* 第一轮:
* 第一次交换: 最小元素为1,所以下标为0的元素和下标为3元素交换
* 1,3,2(第二个2),2(第一个2),8,5
* 两个2的顺序出现颠倒,所以是不稳定排序
* ......
*
* @param a
* @param <K>
*/
public static <K extends Comparable<K>> void sort(K[] a){
if(a==null)return ;
int length = a.length;
if(length==0)return;
for(int i=0;i<length-1;i++){
//最小元素的下标
int min_index = i;
for(int j=i+1;j<length;j++){
if(less(a[j],a[min_index])) min_index = j;
}
swap(a,i,min_index);
}
}
/**
* 优化:同时找出最大值和最小值
* @param a
* @param <K>
*/
public static <K extends Comparable<K>> void sortWithBothEnds(K[] a){
if(a==null)return ;
int length = a.length;
if(length==0)return;
int l = 0;
int r = length-1;
while(l < r){
//最小元素的下标
int min_index = l;
//最大元素的下标
int max_index = r;
for(int j=l+1;j<=r;j++){
if(less(a[j],a[min_index])) min_index = j;
if(less(a[max_index],a[j-1])) max_index = j-1;
}
//如果左边界为最大值,并且右边界为最小值,则交换
if(l==max_index && r==min_index) {
swap(a, l, r);
}else if(l==max_index){ //左边界为最大值,先把最大值交换到右边界
swap(a,r,max_index);
swap(a,l,min_index);
}else { //右边界为最小值,先把最小值交换到左边界 或者 最大最小值不在边界上
swap(a,l,min_index);
swap(a,r,max_index);
}
l++;
r--;
}
}
2.5 算法分析
表现最稳定的排序算法之一,因为无论什么数据进去都是O(n^2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。
3、插入排序(Insertion Sort)
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
3.1 算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
3.2 动图演示
3.2 代码实现
/**
* 标准插入排序
* @param a
* @param <K>
*/
public static <K extends Comparable<K>> void insertionSort(K[] a){
if(a==null)return ;
int length = a.length;
if(length==0)return ;
for(int i=1;i<length;i++){
K tmp = a[i];
int j;
for (j = i; j > 0; j--) {
if(less(tmp,a[j-1])) a[j]=a[j-1];
else break;
}
a[j] = tmp;
}
}
/**
* 折半插入,即二分查找+插入排序
* @param a
* @param <K>
*/
public static <K extends Comparable<K>> void sortWithBinarySearch(K[] a){
if(a==null)return ;
int length = a.length;
if(length==0)return ;
for(int i=1;i<length;i++){
K tmp = a[i];
int index = getInsertIndex(a,0,i,tmp);
int j;
for (j = i; j > index; j--) {
a[j]=a[j-1];
}
a[j] = tmp;
}
}
/**
* 二路插入排序算法是在折半插入的基础上进行改进
* 折半插入在原先直接插入的基础上改进,通过折半查找,以较少的比较次数就找到了要插入的位置
* 但是在插入的过程中仍然没有减少移动次数,所以2路插入在此基础上改进,减少了移动次数,
* 但是仍然并没有避免移动记录(如果要避免的话还是得改变存储结构)
*
* 因此我们设定一个辅助数组b,大小是原来数组相同的大小
* 将b[0]设为第一个原数组第一个数,通过设置head和tail指向整个有序序列的最小值和最大值
* 即为序列的尾部和头部,并且将其设置位一个循环数组,
* 这样就可以进行双端插入 (之所以能减少移动次数的原因在于可以往2个方向移动记录,故称为2路插入)
*
* 具体操作思路:
* 1. 将原数组第一个元素赋值给b[0],作为标志元素
* 2. 按顺序依次插入剩下的原数组的元素
* (1). 将带插入元素与第一个进行比较,偌大于b[0],则插入b[0]前面的有序序列,否则插入后面的有序序列
* (2). 对前面的有序序列或后面的有序序列进行折半查找
* (3). 查找到插入位置后进行记录的移动,分别往head方向前移和往tail方向移动
* (4). 插入记录
* 3. 将排序好的b数组的数据从head到tail,按次序赋值回原数组
*
* @param a
* @param <K>
*/
// @SuppressWarnings("unchecked")
public static <K extends Comparable<K>> void twoPathInsertSort(K[] a) {
int len = a.length;
K[] b = (K[]) new Comparable[len];
b[0] = a[0];
// 分别记录temp数组中最大值和最小值的位置
int i, first, tail, k;
first = tail = 0;
for (i = 1; i < len; i++) {
// 待插入元素比最小的元素小
if (less(a[i], b[first])) {
first = (first - 1 + len) % len;
b[first] = a[i];
}
// 待插入元素比最大元素大
else if (less(b[tail], a[i])) {
tail = (tail + 1 + len) % len;
b[tail] = a[i];
}
// 插入元素 >= 最小,<= 最大
else {
k = (tail + 1 + len) % len;
// 当插入值比当前值小时,需要移动当前值的位置
while (less(a[i], b[((k - 1) + len) % len])) {
b[(k + len) % len] = b[(k - 1 + len) % len];
k = (k - 1 + len) % len;
}
// 插入该值
b[(k + len) % len] = a[i];
// 因为最大值的位置改变,所以需要实时更新tail的位置
tail = (tail + 1 + len) % len;
}
}
// 将排序记录复制到原来的顺序表里
for (k = 0; k < len; k++) {
a[k] = b[(first + k) % len];
}
}
3.4 算法分析
插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
4、希尔排序(Shell Sort)
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
4.1 算法描述
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
4.2 动图演示
4.3 代码实现
/**
* 步长为 2 的希尔排序
* @param a
* @param <K>
*/
public static <K extends Comparable<K>> void sort(K[] a){
shellSort(a,2);
}
/**
* 标准希尔排序
*
* 使用递增序列 1、4、13、40、121、364… 的希尔排序所需的比较次数不会超过 N 的若干倍乘以递增序列的长度。
*
* Q:如何通过h递增序列优化?
* A:在实际应用中,group 的取值使用以上递增序列基本就足够了。
* 但是我们为了追求性能的提升,也使用以下的序列,使性能提高 20%-40% 。
* 1、5、19、41、109、209、505、929、2161、3905、8929、16001、
* 36289、64769、146305、260609
*
* @param a 待排序数组
* @param <K>
*/
public static <K extends Comparable<K>> void shellSort(K[] a, int step){
if(a==null)return ;
int length = a.length;
if(length==0)return;
//分组group 步长step
for(int group = length/step; group > 0; group = group/step){
for(int i=group; i<length ;i++){
int j = i;
K cur = a[i];
while(j - group>=0 && less(cur,a[j - group])){
//使用覆盖,减少开销
a[j] = a[j-group];
j -= group;
}
a[j] = cur;
}
//当 0<group<step时 ,为了避免 此时 group/step == 0 跳过group为1的分组
if(group/step>0 && group/step < step)group = step;
}
}
4.4 算法分析
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。
5、归并排序(Merge Sort)
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
5.1 算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
5.2 动图演示
5.3 代码实现
private static Comparable[] tmp;
//阀值 如果待排数组元素 <= 7,则使用适合小数据的插入排序
private static final int THRESHOLD = 7;
//---------------------------------归并排序 递归实现--------------------------------------
/**
* 归并排序 递归实现 自顶向下
* @param a
* @param <K>
*/
public static <K extends Comparable<K>> void mergeSortByRecursive(K[] a){
if(a == null) return ;
int length = a.length;
if(length == 0)return;
tmp = new Comparable[length];
mergeSortByRecursive(a,0,length-1);
}
/**
* 归并排序区间在[l,r] 递归实现 自顶向下
* @param a
* @param l
* @param r
* @param <K>
*/
public static <K extends Comparable<K>> void mergeSortByRecursive(K[] a,int l,int r){
if(r <= l)return;
int mid = l + ((r - l) >> 1);
mergeSortByRecursive(a,l,mid);
mergeSortByRecursive(a,mid+1,r);
merge(a,l,mid,r);
}
//-------------------------------归并排序 通过迭代循环实现---------------------------------
/**
* 归并排序 通过迭代循环实现 自底向上
* @param a
* @param <K>
*/
public static <K extends Comparable<K>> void mergeSortByIterate(K[] a){
if(a == null) return ;
int length = a.length;
if(length == 0)return;
tmp = new Comparable[a.length];
for(int sz = 2;sz<length*2; sz*=2){ //size 为2,4,8, ... 序列
for(int i=0; i < length ; i+=sz){
//防止右边界溢出
int r = i+sz-1 < length ? i+sz-1:length-1;
merge(a,i,i+sz/2-1,r);
}
}
}
//--------------------------------------归并数组----------------------------------------
/**
* 归并数组,区间在[l,r]
* @param a
* @param l
* @param mid
* @param r
* @param <K>
*/
public static <K extends Comparable<K>> void merge(K[] a, int l, int mid, int r){
//只有一个元素,return
if(r-l<=0) return;
int i = l, j = mid+1;
for(int k = l; k<=r;k++){
//左边耗尽
if(i>mid) tmp[k] = a[j++];
//右边耗尽
else if(j > r) tmp[k] = a[i++];
//左边大于右边
else if(less(a[j],a[i])) tmp[k] = a[j++];
else tmp[k] = a[i++];
}
//复制元素
System.arraycopy(tmp,l,a,l,r-l+1);
}
//------------------------------------综合优化------------------------------------------
/**
* 综合优化 插入+归并
* @param a 待排数组
* @param <K> 类型
*/
public static <K extends Comparable<K>> void advancedSort(K[] a){
K[] cur = (K[]) new Comparable[a.length];
System.arraycopy(a,0,cur,0,a.length);
advancedSort(cur,a,0,a.length-1);
}
/**
* 综合优化 插入+归并 排序区间[l,r]
* 1.对小规模子数组采用插入排序:
* - 递归对于小规模的数组将产生过多的小数组甚至是空数组调用栈
* 2.测试数组是否已经有序:
* - 若a[mid]<=a[mid+1], 认为数组已经有序, 可以跳过merge()方法
* 3.不将元素复制到辅助数组tmp
* - 调用两种排序方法: 一种将数据从输入数组排序到辅助数组, 另一个方法反之;
*
* @param src 待排数组
* @param dst 排序后的结果
* @param l 左界限
* @param r 右界限
* @param <K> 类型
*/
public static <K extends Comparable<K>> void advancedSort(K[] src,K[] dst,int l, int r){
if(r-l < THRESHOLD){
insertSort(dst,l,r);
return;
}
int mid = l + ((r-l)>>1);
advancedSort(dst,src,l,mid);
advancedSort(dst,src,mid+1,r);
//2. 如果已经有序,则跳过合并
if(!less(src[mid+1],src[mid])){
System.arraycopy(src,l,dst,l,r-l+1);
return;
}
merge(src,dst,l,mid,r);
}
/**
* 归并数组 归并区间[l,mid] [mid+1,r]
* @param src 归并数组
* @param dst 归并后的结果
* @param l 左界限
* @param mid 中间值
* @param r 右界限
* @param <K> 类型
*/
public static <K extends Comparable<K>> void merge(K[] src,K[] dst,int l,int mid,int r){
if(r-l<=0)return;
int i = l;
int j = mid+1;
for(int k=l;k<=r;k++){
if(j>r) dst[k]=src[i++];
else if(i>mid) dst[k]=src[j++];
else if(less(src[i],src[j])) dst[k] = src[i++];
else dst[k] = src[j++];
}
}
/**
* 插入排序
* 对 r-l < 7 使用插入排序
* @param a
* @param l
* @param r
* @param <K>
*/
public static <K extends Comparable<K>> void insertSort(K[] a,int l,int r){
for(int i = l+1; i<=r; i++){
K tmp = a[i];
int j =i;
for(;j>l; j--){
if(less(tmp,a[j-1]))a[j] = a[j-1];
else break;
}
a[j] = tmp;
}
}
5.4 算法分析
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
6、快速排序(Quick Sort)
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
6.1 算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
6.2 动图演示
6.3 代码实现
//使用插排的阀值
private static final int INSERTION_SORT_THRESHOLD = 8;
/**
* 标准快排 入口方法
* @param a 待排数组
* @param <K> 数组泛型
*/
public static <K extends Comparable<K>> void quickSort1(K[] a){
if(a==null)return ;
quickSort1(a,0,a.length-1);
}
/**
* 快排-挖坑法实现(左右互换) 入口方法
* @param a 待排数组
* @param <K> 数组泛型
*/
public static <K extends Comparable<K>> void quickSort2(K[] a){
if(a==null)return ;
quickSort2(a,0,a.length-1);
}
//---------------------------------标准快排 实现----------------------------------------------
/**
* 标准快排 实现 排序区间[l,r]
* @param a 待排数组
* @param l 左边界
* @param r 右边界
* @param <K> 泛型类型
*/
public static <K extends Comparable<K>> void quickSort1(K[] a,int l,int r){
if(a.length ==0 || l>=r)return;
//获取分区下标
int partitionIndex = partition1(a,l,r);
//注意排序区间是[l,partitionIndex-1] 和 [partitionIndex+1,r]
quickSort1(a,l,partitionIndex-1);
quickSort1(a,partitionIndex+1,r);
}
/**
* 标准快排的切分算法 实现 切分区间[l,r]
* @param a 待排数组
* @param l 左边界
* @param r 右边界
* @param <K> 泛型类型
*/
private static <K extends Comparable<K>> int partition1(K[] a,int l,int r){
// //这样写有问题,用为定位到的i和j是 +1,和-1之后的
// int i = l+1, j = r;
// //基准为l
// K tmp = a[l];
//
// while(true){
// //左右移动,跳过有序的位置
// //定位到 大于tmp的下标
// while(less(a[i++],tmp)) if(i==r) break;
// //定位到 小于tmp的下标
// while(less(tmp,a[j--])) if(j==l) break;
// if(i>=j)break;
// //交换下标为i和j的元素
// swap(a,i,j);
// }
// swap(a,l,j);
//以l 和r+1 作为开始下标,避免上面情况的发生
int i = l, j = r+1;
//基准为l
K tmp = a[l];
while(true){
//左右移动,跳过有序的位置
//定位到 大于tmp的下标
//先做自增操作,如果是i++,那么交换的下标不是比较的时候的下标
while(less(a[++i],tmp)) if(i==r) break;
//定位到 小于tmp的下标
//先做自增操作,如果是j--,那么交换的下标不是比较的时候的下标
while(less(tmp,a[--j])) if(j==l) break;
if(i>=j)break;
//交换下标为i和j的元素
swap(a,i,j);
}
//此时的a[j] <= a[l]
swap(a,l,j);
return j;
}
//---------------------------快排-挖坑法(左右互换) 实现----------------------------------------------
/**
* 快排-挖坑法(左右互换) 实现 排序区间[l,r]
*
* @param a 待排数组
* @param l 左边界
* @param r 右边界
* @param <K> 泛型类型
*/
public static <K extends Comparable<K>> void quickSort2(K[] a,int l,int r){
if(a.length ==0 || l>=r)return;
//获取分区下标
int partitionIndex = partition2(a,l,r);
//注意排序区间是[l,partitionIndex-1] 和 [partitionIndex+1,r]
quickSort2(a,l,partitionIndex-1);
quickSort2(a,partitionIndex+1,r);
}
/**
* 快排的切分算法("挖坑法"-左右互换) 实现 切分区间[l,r]
* @param a 待排数组
* @param l 左边界
* @param r 右边界
* @param <K> 泛型类型
*/
private static <K extends Comparable<K>> int partition2(K[] a,int l,int r){
int i=l,j=r;
//基准
K tmp = a[l];
while(i<j){
//相等需要移动,不然可能出现死循环
// tmp <= a[j] <=> !(tmp > a[j]) <=> !less(a[j],tmp)
while(i<j && !less(a[j],tmp))j--;
a[i] = a[j];
//相等需要移动,不然可能出现死循环
// tmp >= a[i] <=> !(tmp < a[i]) <=> !less(tmp,a[i])
while(i<j && !less(tmp,a[i]))i++;
a[j] = a[i];
}
//此时 i==j
a[j] = tmp;
return j;
}
//-------------------------------------快排 优化----------------------------------------------
/**
* 快排 优化
*
* 针对排序基准轴优化
* 针对基本快排进行优化的思路主要有以下几个:
* 1. 在排序之前进行打乱操作(上述代码已经使用)
* 2. 小数组切换到插入排序: 通常THRESHOLD选择5~15均可
* 3. 取样切分, 选择子数组一小部分的中位数来切分(保证尽量均匀)
*
* 性能优化原因:
* 1. 对小规模子数组采用插入排序: 避免了对小规模的数组进行递归而产生过多的小数组甚至是空数组调用栈
* 2. 取样切分, 选择子数组一小部分的中位数来切分(保证尽量切分均匀)
*
*/
/**
* 标准快排 优化
* @param a
* @param <K>
*/
public static <K extends Comparable<K>> void advanceQuickSort1(K[] a){
if(a == null) return;
advanceQuickSort1(a,0,a.length-1);
}
/**
* @param a 待排数组
* @param l 左边界
* @param r 右边界
* @param <K> 泛型类型
*/
public static <K extends Comparable<K>> void advanceQuickSort1(K[] a,int l, int r){
if(a.length < 2 || l >= r) return;
//长度小于8,使用插排
if(r-l < INSERTION_SORT_THRESHOLD){
insertSort(a,l,r);
return;
}
int partitionIndex = advancePartition1(a,l,r);
advanceQuickSort1(a,l,partitionIndex-1);
advanceQuickSort1(a,partitionIndex+1,r);
}
/**
* 标准快排的切分算法 实现 切分区间[l,r] 优化版
* @param a 待排数组
* @param l 左边界
* @param r 右边界
* @param <K> 泛型类型
*/
public static <K extends Comparable<K>> int advancePartition1(K[] a,int l, int r){
int i=l, j= r+1, m = median(a, l, l+((r-l)>>1), r);
swap(a,l,m);
K tmp = a[l];
//当a[l]为 数组区间[j,r]的最大元素时
while(less(a[++i],tmp)){
if(i==r){
swap(a,l,r);
return r;
}
}
//当a[l]为 数组区间[j,r]的最小元素时
while(less(tmp,a[--j])){
if(j==l) return l;
}
//另外的情况
while(i<j){
swap(a,i,j);
while(less(a[++i],tmp));
while(less(tmp,a[--j]));
}
swap(a,l,j);
return j;
}
/**
* 快排(挖坑法-左右互换) 优化
* @param a
* @param <K>
*/
public static <K extends Comparable<K>> void advanceQuickSort2(K[] a){
if(a==null) return ;
advanceQuickSort2(a,0,a.length-1);
}
/**
* @param a 待排数组
* @param l 左边界
* @param r 右边界
* @param <K> 泛型类型
*/
public static <K extends Comparable<K>> void advanceQuickSort2(K[] a,int l, int r){
if(a.length < 2 || l >= r) return;
//长度小于8,使用插排
if(r-l < INSERTION_SORT_THRESHOLD){
insertSort(a,l,r);
return;
}
int m = median(a, l, l+((r-l)>>1), r);
swap(a,l,m);
int partitionIndex = partition2(a,l,r);
advanceQuickSort2(a,l,partitionIndex-1);
advanceQuickSort2(a,partitionIndex+1,r);
}
//-------------------------------------三向切分 快排 实现----------------------------------------------
/**
* 三向切分的快排 入口函数
* @param a
* @param <K>
*/
public static <K extends Comparable<K>> void threeWaySort(K[] a){
if(a==null)return;
threeWaySort(a,0,a.length-1);
}
/**
* 三向切分的快排(信息量最优的快速排序):
* 适用于大量重复元素的排序
*
* 1. 从左至右遍历数组一次:
* a. 指针lt使得a[l…i-1]中的元素都小于key
* b. 指针gt使得a[j+1…r]中的元素都大于key
* c. 指针i使得a[i…k-1]中的元素都等于key
* d. 而a[i…gt]中的元素为未确定
* 2. 处理时一开始i和lo相等, 进行三向比较:
* a. a[k] < key: 将a[i]和a[k]交换, 将i和k加一;
* b. a[k] > key: 将a[j]和a[k]交换, 将j减一;
* c. a[k] = key: 将k加一;
*
* 上述操作均会保证数组元素不变并且缩小j-k的值(这样循环才会结束)
*
* @param a 待排数组
* @param l 左边界
* @param r 右边界
* @param <K> 泛型类型
*/
public static <K extends Comparable<K>> void threeWaySort(K[] a,int l, int r){
if(a.length<2 || r<=l)return;
if(r-l<INSERTION_SORT_THRESHOLD){
insertSort(a,l,r);
return;
}
int m = median(a,l,l+((r-l)>>1),r);
swap(a,l,m);
int i=l,j=r,k=l+1;
K tmp = a[l];
while(k<=j){
int cmp = a[k].compareTo(tmp);
//这里k++,是因为从下标为i,交换到下标为k的元素a[i] 等于 tmp,可以跳过
// if(cmp < 0)swap(a,i++,k)也是可以的,会多做一次比较
if(cmp < 0)swap(a,i++,k++);
//这里k++,是因为从下标为j,交换到下标为k的元素a[j] 可能大于/小于/等于 tmp,不可以跳过
else if(cmp > 0)swap(a,j--,k);
else k++;
}
threeWaySort(a,l,i-1);
threeWaySort(a,j+1,r);
}
/**
* 获取 a[l],a[mid],a[r]的中位数
* @param a
* @param l
* @param mid
* @param r
* @param <K>
* @return
*/
public static <K extends Comparable<K>> int median(K[] a,int l,int mid,int r){
return less(a[l],a[r]) ?
(less(a[l],a[mid]) ? (less(a[mid],a[r]) ? mid : r) : l):
(less(a[r],a[mid]) ? (less(a[mid],a[l]) ? mid : l) : r);
}
7、堆排序(Heap Sort)
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
7.1 算法描述
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
7.2 动图演示
7.3 代码实现
/**
* 堆排序入口函数
* @param a 待排数组
* @param <K> 泛型类型
*/
public static <K extends Comparable<K>> void heapSort(K[] a){
if(a==null)return;
int length = a.length;
if(a.length<2)return;
//构建最大堆
buildMaxHeap(a);
for(int i=length-1;i>=0;i--){
//交换第一个和最后一个元素,也就是把最大值,放到数组末尾
swap(a,0,i);
//缩小边界
length--;
//调整堆
heapify(a,0,length);
}
}
/**
* 构建最大堆
* @param a 待排数组
* @param <K> 泛型类型
*/
public static <K extends Comparable<K>> void buildMaxHeap(K[] a){
for (int len=a.length,i= len >>1; i>=0; i--){
heapify(a,i,len);
}
}
/**
* 调整堆,针对第i个元素重建堆
* @param a
* @param i
* @param bound
* @param <K>
*/
public static <K extends Comparable<K>> void heapify(K[] a,int i,int bound){
int lagest = i;
int left = i*2+1; //左孩子
int right = i*2+2; //右孩子
if(left<bound && less(a[lagest],a[left])) lagest = left;
if(right<bound && less(a[lagest],a[right])) lagest = right;
if(lagest!=i){
swap(a,lagest,i);
heapify(a,lagest,bound);
}
}
8、计数排序(Counting Sort)
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
8.1 算法描述
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
8.2 动图演示
8.3 代码实现
public static void countSort(int[] a){
if(a==null)return ;
int length = a.length;
if(length<2)return;
countSort(a,0,length-1);
}
public static void countSort(int[] a,int l,int r){
if(l >= r)return;
//第一遍:
//找到最大最小值,确定范围
int min_value=a[0], max_value=a[0];
for(int i=1;i<a.length;i++){
if(a[i] > max_value) max_value = a[i];
else if(a[i] < min_value) min_value = a[i];
}
//新建数组,[min_value,max_value],length: max_value - min_value + 1
int[] aux = new int[max_value - min_value + 1];
//第二次遍历:
// for(int i=0;i<a.length;i++){
// aux[a[i]-min_value] += 1;
// }
for (int i:a) {
aux[i-min_value] += 1;
}
//将计数结果输入到原数组
int index=0;
for(int i=0;i<aux.length;i++){
for(int j=0;j<aux[i];j++){
a[index++] = i+min_value;
}
}
}
8.4 算法分析
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
9、桶排序(Bucket Sort)
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
9.1 算法描述
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
9.2 图片演示
9.3 代码实现
/** * 设置桶的默认数量为5 */
private static final int DEFAULT_BUCKET_SIZE = 5;
/**
* 这里仅仅作为演示, 排序要求:
* 输入的桶大小bucketSize, 应当大于待排序浮点数的整数部分, 且浮点数均大于零才行!
* (因为在getBucketIndex方法中仅仅取了浮点数的整数部分作为桶的index)
* @param arr 待排序数组
* @param bucketSize 桶大小(在本例中为浮点数整数部分最大值+1)
*/
public static void sort(double[] arr, int bucketSize) {
if(arr==null)return ;
int length = arr.length;
if(length<2) return;
List<LinkedList<Double>> buckets = new ArrayList<>();
bucketSize = Math.max(bucketSize,DEFAULT_BUCKET_SIZE);
//新建桶
for(int i=0;i<bucketSize;i++){
//选用链表作为桶的数据结构
buckets.add(new LinkedList<>());
}
//把数组元素放入桶中
for(int i=0;i<length;i++){
//选择桶
int index = getBucketIndex(arr[i]);
//往桶中插入元素
insertSort(buckets.get(index),arr[i]);
}
//遍历桶,把数据输出到原数组
int index = 0;
for(List<Double> bucket:buckets){
for (Double data:bucket) {
arr[index++] = data;
}
}
}
/**
* 计算应该在哪一个桶内
* @param data
* @return
*/
private static int getBucketIndex(double data){
// 这里例子写的比较简单,仅使用浮点数的整数部分作为其桶的索引值
// 实际开发中需要根据场景具体设计
return (int)data;
}
/**
* 选用插入排序作为桶内元素排序方法
* @param bucket 桶
* @param data 待插入数据
*/
private static void insertSort(List<Double> bucket, double data){
boolean isInsert = false;
ListIterator iterator = bucket.listIterator();
while(iterator.hasNext()){
if(data <= (double)iterator.next()){
//游标往前移,指向前一个元素
iterator.previous();
//插入数据
iterator.add(data);
isInsert = true;
break;
}
}
//如果之前没有插入数据,表明该元素最大,直接插入末尾
if(!isInsert) bucket.add(data);
}
9.4 算法分析
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
10、基数排序(Radix Sort)
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
10.1 算法描述
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点);
10.2 动图演示
10.3 代码实现
//这里仅对整数进行排序,int最多为10位
private static final int[] RADIX_DICT = {1,10,100,1000,10000,100000,1000000,10000000,100000000,1000000000};
/**
* 基数排序入口函数
* @param a 待排数组
*/
public static void radixSort1(int[] a){
if(a == null) return;
int max = getMax(a);
radixSort1(a,getLength(max),0,a.length-1);
}
/**
*
* @param a 待排数组
* @param l 左边界
* @param r 右边界
*/
public static void radixSort1(int[] a,int max,int l,int r){
if(a == null) return;
if(l >= r) return;
int length = a.length;
int[] count = new int[10];
int[] bucket = new int[length];
for(int k=1;k<=max;k++){
//将count计数数组,置零
for(int i=0;i<10;i++) count[i] = 0;
//计算数组在第k位,数值分别为0,1,2,3,4,5,6,7,8,9的个数
for(int i=0;i<length;i++) count[getIndexNum(a[i],k)]++;
//bucket中数值分别为0,1,2,3,4,5,6,7,8,9的右边界
for(int i=1;i<10;i++) count[i] += count[i-1];
//把数值放入bucket中,注意,从后往前遍历
for(int i=length-1;i>=0;i--){
int j = getIndexNum(a[i],k);
bucket[count[j]-1] = a[i];
count[j]--;
}
//把值赋给原数组a
for(int i=0;i<length;i++) a[i] = bucket[i];
}
}
/**
* 基数排序入口函数
* @param a 待排数组
*/
public static void radixSort2(int[] a){
if(a == null) return;
int max = getMax(a);
radixSort2(a,getLength(max),0,a.length-1);
}
/**
*
* @param a 待排数组
* @param l 左边界
* @param r 右边界
*/
public static void radixSort2(int[] a,int max,int l,int r){
if(a == null) return;
if(l >= r) return;
int length = a.length;
List<List<Integer>> buckets = new ArrayList<>();
//构建10个bucket,使用ArrayList作为桶的数据结构
for(int i=0;i<10;i++)buckets.add(new ArrayList<>());
for(int k=1;k<=max;k++){
//清除桶里的元素,避免干扰,设置新的桶
for(int i=0;i<10;i++)buckets.set(i,new ArrayList<>());
//把数值放入bucket中
for(int i=0;i<length;i++){
int j = getIndexNum(a[i],k);
buckets.get(j).add(a[i]);
}
//把桶的数据,赋给原数组a
int index = 0;
for (List<Integer> bucket:buckets) {
for (Integer value:bucket) {
a[index] = value;
index++;
}
}
}
}
/**
* 返回一个数的第index位的值
* @param num
* @param index
* @return
*/
private static int getIndexNum(int num,int index){
return (num/RADIX_DICT[index-1])%10;
}
/**
* 返回最大值
* @param a
* @return
*/
private static int getMax(int[] a){
int max = a[0];
for(int i=1;i<a.length;i++){
if(a[i] > max) max = a[i];
}
return max;
}
/**
* 返回一个数的位数
* @param num
* @return
*/
private static int getLength(int num){
int i = 1;
while((num/=10)>0) i++;
return i;
}
10.4 算法分析
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。
部分摘自于:十大经典排序算法(动图演示)
作者:一像素
代码为自己实现,参考
- 《算法》(第四版)
- 几种常见排序方法的优化(上)
- 几种常见排序方法的优化-下