一:排序中的一些概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。例如:
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
二:排序分类
三:常见排序算法的介绍
3.1直接插入排序
3.1.1基本思想
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。实际中我们玩扑克牌时,就用了插入排序的思想。
3.1.2图解+步骤
直接插入排序类似于我们玩扑克牌时候码牌,如上面左图所示。
这时候又来了一张红桃9,我会从后往前依次进行比较。红桃A比9大,那红桃A就往后移动一位;接着比较红桃K,比9大,红桃K也向后移动一位…依次类推,直到比较到红桃10,红桃10也比9大,红桃10向后移动一位,把红桃9插入到最前面了。这就是插入排序。
步骤如下:
- 步骤1: 从第一个元素开始,该元素可以认为已经被排序;
- 步骤2: 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 步骤3: 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 步骤4: 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 步骤5: 将新元素插入到该位置后;
- 步骤6: 重复步骤2~5。
3.1.3代码实现
package Sort;
import java.util.Arrays;
public class TestSort {
/**
* 直接插入排序
* @param array
*/
public static void insertSort(int[] array) {
for(int i = 1;i < array.length;i++) {
int tmp = array[i];
int j = i-1;
for (; j >= 0 ; j--) {
if(array[j] > tmp) {
array[j+1] = array[j];
}else {
//array[j+1] = tmp;
break;
}
}
array[j+1] = tmp;
}
}
public static void main(String[] args) {
int[] array = {2,1,53,4,5,10,9,36,23,6,19};
System.out.println("排序前:"+ Arrays.toString(array));
insertSort(array);
System.out.println("排序后:"+ Arrays.toString(array));
}
}
3.1.4 直接插入排序总结
- 时间复杂度:O(N^2),最坏情况下,就是数组完全逆序的情况,其时间复杂度是一个等差数列.
- 空间复杂度:O(1),没有创建额外的空间.
- 稳定性:稳定
- 适用场景:当数据量小,并且已经趋于有序的时候,使用直接插入排序.
3.2希尔排序
3.2.1基本思想
希尔排序是希尔(Donald Shell) 于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是直接插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(N^2)的第一批算法之一.
3.2.2图解+步骤
我们可以发现,在进行希尔排序的过程中,随着分组数的不断减少,整体数据越来越趋于有序.当gap=1时,就相当于对整体进行了一次直接插入排序,此时我们需要把握住两点:
- 此时整个数组已趋于有序,所以使用直接插入排序,其效率几乎可以达到O(N),是非常高效的;
- 最后一轮排序时gap=1,此时一定可以达到整个数组有序的效果;所以无论前面我们如何进行分组,例如分为521组,631组,甚至741组,都可以,但是最后一组一定是gap=1.
关于如何分组的讨论:
对于希尔排序究竟如何分组才最高效,众说纷纭,不同的书籍中也给出了不同的见解.但正如我们前文所说,无论哪一种分法,最后一个增量值必须等于1.
步骤如下:
- 步骤1:选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 步骤2:按增量序列个数k,对序列进行k 趟排序;
- 步骤3:每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
希尔排序,分而治之;分成多组,各组有序;
增量序列,最终为一;提高效率,希尔排序.
3.2.3代码实现
package Sort;
import java.util.Arrays;
public class TestSort {
/**
* 希尔排序
*/
public static void shellSort(int[] array){
int gap = array.length/2;
while (gap > 1){
shell(array,gap);
gap /= 2;
}
shell(array,1);
}
private static void shell(int[] array, int gap) {
for(int i = gap;i < array.length;i++) {
int tmp = array[i];
int j = i-gap;
for (; j >= 0 ; j -= gap) {
if(array[j] > tmp) {
array[j+gap] = array[j];
}else {
break;
}
}
array[j+gap] = tmp;
}
}
public static void main(String[] args) {
int[] array = {2,1,53,4,5,10,9,36,23,6,19};
System.out.println("排序前:"+ Arrays.toString(array));
shellSort(array);
System.out.println("排序后:"+ Arrays.toString(array));
}
}
3.2.4总结
- 时间复杂度:与增量有关,是所取增量(gap)的一个函数.根据严蔚敏版教材,为O(N1.3)~O(N1.5).
- 空间复杂度:O(1),没有创建额外的空间.
- 稳定性:不稳定
- 适用场景:当数据量较大时,可以采用希尔排序,降低时间复杂度,对直接插入排序进行优化.
3.3选择排序
3.3.1基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
3.3.2图解+步骤
步骤如下:
- 步骤1:初始状态:无序区为R[1…n],有序区为空;
- 步骤2:第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个的新无序区;
- 步骤3:n-1趟结束,数组有序化了。
3.3.3代码实现
package Sort;
import java.util.Arrays;
public class TestSort {
/**
* 选择排序
* @param
*/
public static void selectSort(int[] array) {
for (int i = 0; i < array.length; i++) {
int minIndex = i;
for (int j = i+1; j < array.length; j++) {
if(array[j] < array[minIndex]) {
minIndex = j;
}
}
swap(array,minIndex,i);
}
}
private static void swap(int[] array,int i,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
public static void main(String[] args) {
int[] array = {2,1,53,4,5,10,9,36,23,6,19};
System.out.println("排序前:"+ Arrays.toString(array));
selectSort(array);
System.out.println("排序后:"+ Arrays.toString(array));
}
}
3.3.4选择排序总结
- 时间复杂度:O(N^2).
- 空间复杂度:O(1),没有创建额外的空间.
- 稳定性:不稳定
- 适用场景:一般情况下不咋用.
3.4堆排序
3.4.1基本思想
堆排序(Heapsort) 是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。需要注意的是排升序要建大堆,排降序建小堆。
3.4.2图解+步骤
- 步骤1:将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 步骤2:将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 步骤3:由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
3.4.3代码实现
package Sort;
import java.util.Arrays;
public class TestSort {
/**
* 堆排序
* @param array
*/
public static void heapSort(int[] array){
createHeap(array); //建立大根堆
int end = array.length - 1;
while (end >= 0){
swap(array,0,end);
shiftDown(array,0,end);
end--;
}
}
private static void createHeap(int[] array) {
for(int p = (array.length-1-1)/2; p >= 0;p--){
shiftDown(array,p,array.length);
}
}
private static void shiftDown(int[] array, int root, int len) {
int parent = root;
int child = (2*parent) + 1;
while(child < len){
if (child + 1 < len && array[child] < array[child+1]) {
child++;
}
if(array[child] > array[parent]){
swap(array,child,parent);
parent = child;
child = 2*parent + 1;
} else {
break;
}
}
}
public static void main(String[] args) {
int[] array = {2,1,53,4,5,10,9,36,23,6,19};
System.out.println("排序前:"+ Arrays.toString(array));
heapSort(array);
System.out.println("排序后:"+ Arrays.toString(array));
}
}
3.4.4堆排序总结
- 时间复杂度:O(N*logN).
- 空间复杂度:O(1),没有创建额外的空间.
- 稳定性:不稳定
3.5冒泡排序
3.5.1基本思想
冒泡排序 是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误,就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
3.5.2图解+步骤
对于冒泡排序,每一轮排序过后,该轮的最后一个元素都会有序.也就是说,冒泡排序是一种不断将最大的元素"冒泡"到最后的排序.
具体步骤如下:
- 步骤1: 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 步骤2: 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 步骤3: 针对所有的元素重复以上的步骤,除了最后一个;
- 步骤4: 重复步骤1~3,直到排序完成。
3.5.3代码实现
package Sort;
import java.util.Arrays;
public class TestSort {
/**
* 冒泡排序
* @param array
*/
public static void bubbleSort(int[] array){
for (int i = 0; i < array.length-1; i++) {
boolean flag = false;
for (int j = 0; j < array.length-1-i; j++) {
if(array[j] > array[j+1]){
flag = true;
int tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
}
}
if(!flag){
break;
}
}
}
public static void main(String[] args) {
int[] array = {2,1,53,4,5,10,9,36,23,6,19};
System.out.println("排序前:"+ Arrays.toString(array));
bubbleSort(array);
System.out.println("排序后:"+ Arrays.toString(array));
}
}
3.5.4总结
- 时间复杂度:O(N^2).
- 空间复杂度:O(1),没有创建额外的空间.
- 稳定性:稳定
- 使用场景:如果数组是趋于有序的,使用冒泡[]排序是极好的.在进行优化后,如果数组有序,那么冒泡排序的时间复杂度将达到O(N).
3.6快速排序
3.6.1基本思想
通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
3.6.2步骤
具体步骤如下:
- 步骤1:从数列中挑出一个元素,称为 “基准”(pivot );
- 步骤2:重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 步骤3:递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
将区间按照基准值划分为左右两部分的方法有很多种,此处我会介绍:Hoare法,挖坑法,以及前后指针法.这三种方法,又以挖坑法最为常见.
3.6.3不同方法实现快速排序
3.6.3.1Hoare法
3.6.3.1.1具体操作
- Hoare法需要两个"指针",low指向前,high指向后;
- 此处我们将数组的第一个元素作为基准,让low和high分别向中间移动;low指针遇到比基准大的值就停下来,high指针遇到比基准小的值就停下来,此时二者交换;
- 重复第二步,直到low 与high相遇,此时将相遇位置的值与基准值进行交换;
- 此时基准值的左边都是比基准值小的元素,基准值的右边都是比基准值大的元素.
- 再依次递归基准值左边的数组和基准值右边的数组,直到整个数组有序,递归结束,排序完成.
特别注意:
- 如果是左边元素作为基准值,那么右边的high先移动;如果是右边元素作为基准值,那么左边的low先移动.
3.6.3.1.2代码实现
package Sort;
import java.util.Arrays;
public class TestSort {
/**
* 快速排序
* @param array
*/
public static void quickSort(int[] array) {
quick(array,0,array.length-1);
}
private static void quick(int[] array, int left, int right) {
if(left >= right) return;
int pivot = partitionHoare(array,left,right);
quick(array,left,pivot-1);
quick(array,pivot+1,right);
}
private static int partitionHoare(int[] array, int low, int high) {
//以选择数组中第一个元素作为基准值为例
//首先,需要保存下数组中的第一个元素
int i = low;
int tmp = array[low];
while(low < high){
//1.如果low与high没有交叉且high下标元素>=基准值,high--继续找
while(low < high && array[high] >= tmp){
high--;
}
//出循环,说明此时high就是你要找的数字
//2.如果low与high没有交叉且low下标元素<=基准值,low++继续找
while(low < high && array[low] <= tmp){
low++;
}
//出循环,说明此时low就是你要找的数字
swap(array,low,high);
}
//出循环,此时low与high相遇,再将相遇处的值与基准值交换
swap(array,low,i);
return low;
}
public static void main(String[] args) {
int[] array = {2,1,53,4,5,10,9,36,23,6,19};
System.out.println("排序前:"+ Arrays.toString(array));
quickSort(array);
System.out.println("排序后:"+ Arrays.toString(array));
}
}
3.6.3.2挖坑法
3.6.3.2.1具体操作
- 设定一个基准值(一般为序列的最左边元素,也可以是最右边的元素)此时最左边的是一个坑。
- 开辟两个指针,分别指向序列的头结点和尾结点(选取的基准值在左边,则先从右边出发。反之,选取的基准值在右边,则先从左边出发)。
- 从右指针出发依次遍历序列,如果找到一个值比所选的基准值要小,则将此指针所指的值放在坑里,左指针向前移。
- 后从左指针出发(选取的基准值在左边,则后从左边出发。反之,选取的基准值在右边,则后从右边出发),依次遍历序列,如果找到一个值比所选的基准值要大,则将此指针所指的值放在坑里,右指针向前移。
- 依次循环步骤4,5,直到左指针和右指针重合时,我们把基准值放入这两个指针重合的位置。
对于挖坑法,需要注意所谓的"坑"并不是一个空元素,而是一个要被覆盖掉的"元素".
3.6.3.2.2代码实现
package Sort;
import java.util.Arrays;
public class TestSort {
/**
* 快速排序
* @param array
*/
public static void quickSort(int[] array) {
quick(array,0,array.length-1);
}
private static void quick(int[] array, int left, int right) {
if(left >= right) return;
int pivot = partitionHole(array,left,right);
quick(array,left,pivot-1);
quick(array,pivot+1,right);
}
/*
这是Hole法
*/
private static int partitionHole(int[] array, int low, int high) {
int tmp = array[low];
while(low < high){
while(low < high && array[high] >= tmp){
high--;
}
array[low] = array[high];
while(low < high && array[low] <= tmp){
low++;
}
array[high] = array[low];
}
array[low] = tmp;
return low;
}
// private static void insertSortRange(int[] array, int left, int right) {
// }
public static void main(String[] args) {
int[] array = {2,1,53,4,5,10,9,36,23,6,19};
System.out.println("排序前:"+ Arrays.toString(array));
quickSort(array);
System.out.println("排序后:"+ Arrays.toString(array));
}
}
3.6.3.3前后指针法
3.6.3.3.1具体操作
- 1.定义两个指针,一个是i,一个是d;
- 每次for循环,i++;每次遇到当前元素小于基准值,d++;
- 同时每次遇到当前元素小于基准值,还要交换下标i和下标d的值.
- 当i>.high时,交换array[d-1]和array[low]的值,该次任务结束.
3.6.3.3.2代码实现
package Sort;
import java.util.Arrays;
public class TestSort {
/**
* 快速排序
* @param array
*/
public static void quickSort(int[] array) {
quick(array,0,array.length-1);
}
private static void quick(int[] array, int left, int right) {
if(left >= right) return;
int pivot = partitionPoint(array,left,right);
quick(array,left,pivot-1);
quick(array,pivot+1,right);
}
/*
前后指针法
*/
private static int partitionPoint(int[] array, int left, int right) {
int d = left + 1;
int pivot = array[left];
for (int i = left + 1; i <= right; i++) {
if (array[i] < pivot) {
swap(array, i, d);
d++;
}
}
swap(array, d - 1, left);
return d - 1;
}
public static void main(String[] args) {
int[] array = {2,1,53,4,5,10,9,36,23,6,19};
System.out.println("排序前:"+ Arrays.toString(array));
quickSort(array);
System.out.println("排序后:"+ Arrays.toString(array));
}
}
3.6.4总结
1.时间复杂度:O(N*logN).
但是,当数组有序时,此时会构造出一棵单分支的二叉树,其时间复杂度退化为O(N2).有些人会谈,此时快速排序退化成了冒泡排序,因为他们的时间复杂度是一样的,但是这种说法是不严谨的,快速排序和冒泡排序就是两种截然不同的排序,快速排序也并不满足冒泡排序的思想.
2.空间复杂度: 最好情况下:O(logN);最坏情况下,O(N),就是一棵单分支树.
3.稳定性:不稳定
当需使用快速排序时,优先考虑挖坑法,Hoare次之,最后才是前后指针法.
其次,鉴于快速排序的空间复杂度较高,需要在栈上开辟很大的内存空间,所以我们需要对快速排序进行一定的优化,方才能使用.优化方法主要有以下几种:
3.6.5快排优化
3.6.5.1优化方法一:借助直接插入排序
3.6.5.1.1优化思路
每次递归的时候,数据都是在慢慢变有序的!!当数据量少且趋于有序的时候,我们可以使用直接插入排序进行优化.
3.6.5.1.2代码实现
package Sort;
import java.util.Arrays;
public class TestSort {
/**
* 快速排序
* @param array
*/
public static void quickSort(int[] array) {
quick(array,0,array.length-1);
}
private static void quick(int[] array, int left, int right) {
if(left >= right) return;
//在某个区间的时候,使用直接插入排序{优化区间内的比较}
if(right-left+1 <= 5){
//进行插入排序
insertSortRange(array,left,right);
return;
}
int pivot = partitionHole(array,left,right);
quick(array,left,pivot-1);
quick(array,pivot+1,right);
}
private static void insertSortRange(int[] array, int left, int right) {
for(int i = left+1;i <= right;i++) {
int tmp = array[i];
int j = i-1;
for (; j >= left ; j--) {
if(array[j] > tmp) {
array[j+1] = array[j];
}else {
//array[j+1] = tmp;
break;
}
}
array[j+1] = tmp;
}
}
/*
这是Hole法
*/
private static int partitionHole(int[] array, int low, int high) {
int tmp = array[low];
while(low < high){
while(low < high && array[high] >= tmp){
high--;
}
array[low] = array[high];
while(low < high && array[low] <= tmp){
low++;
}
array[high] = array[low];
}
array[low] = tmp;
return low;
}
public static void main(String[] args) {
int[] array = {2,1,53,4,5,10,9,36,23,6,19};
System.out.println("排序前:"+ Arrays.toString(array));
quickSort(array);
System.out.println("排序后:"+ Arrays.toString(array));
}
}
需要注意的是,这种方法并没有在根本上实现优化.
3.6.5.2优化方法二:随机选取基准法
3.6.5.2.1优化思路
快速排序的运行时间与划分是否对称有关。最坏情况下,每次划分过程产生两个区域分别包含n-1个元素和1个元素,其时间复杂度会达到O(N^2)。在最好的情况下,每次划分所取的基准都恰好是中值,即每次划分都产生两个大小为n/2的区域。此时,快排的时间复杂度为O(NlogN)。所以基准的选择对快排而言至关重要。
在前面的文章中,使用数组最左边的元素作为基准值.随机选取基准,就是在除最左边元素的其余所有元素中,随机选取一个元素作为基准值,将这个值与最左边的元素进行交换.其余步骤,则与前文所述快速排序的方法是相同的.
虽然使用随机基准能解决待排数组基本有序的情况,但是由于这种随机性的存在,对其他情况的数组也会有影响(若数组元素是随机的,使用固定基准常常优于随机基准)。随机数算法随机选择一个元素作为划分基准,算法的平均性能较好,从而避免了最坏情况的多次发生。然而随机数算法毕竟是一门考验人品的学问,也很难保证选取到比较合适的那个基准值.
如对下图所示的数组进行排序,如果我们在除第一个元素6之外的其余所有元素中,使用随机选取基准法选取一个元素的话,如果你选到的恰好是1,那这纯属多此一举.显然这种方法也不是最优解.
3.6.5.2.2代码实现
package Sort;
import java.sql.SQLOutput;
import java.util.Arrays;
import java.util.Random;
public class TestSort {
/**
* 快速排序
* @param array
*/
public static void quickSort(int[] array) {
quick(array,0,array.length-1);
}
private static void quick(int[] array, int left, int right) {
if(left >= right) return;
// 1.在某个区间的时候,使用直接插入排序{优化区间内的比较}
if(right-left+1 <= 5){
//进行插入排序
insertSortRange(array,left,right);
return;
}
// 2.随机选取基准法
int index = RandomIndex(array,left,right);
swap(array,left,index);
//int pivot = partitionHoare(array,left,right);
int pivot = partitionHole(array,left,right);
//int pivot = partitionPoint(array,left,right);
quick(array,left,pivot-1);
quick(array,pivot+1,right);
}
private static int RandomIndex(int[] array, int left, int right) {
Random random = new Random();
int index = random.nextInt(right-left+2) + left;//生成left+1到right之间的随机数
return index;
}
private static void insertSortRange(int[] array, int left, int right) {
for(int i = left+1;i <= right;i++) {
int tmp = array[i];
int j = i-1;
for (; j >= left ; j--) {
if(array[j] > tmp) {
array[j+1] = array[j];
}else {
//array[j+1] = tmp;
break;
}
}
array[j+1] = tmp;
}
}
/*
这是Hole法
*/
private static int partitionHole(int[] array, int low, int high) {
int tmp = array[low];
while(low < high){
while(low < high && array[high] >= tmp){
high--;
}
array[low] = array[high];
while(low < high && array[low] <= tmp){
low++;
}
array[high] = array[low];
}
array[low] = tmp;
return low;
}
public static void main(String[] args) {
int[] array = {2,1,53,4,5,10,9,36,23,6,19};
System.out.println("排序前:"+ Arrays.toString(array));
quickSort(array);
System.out.println("排序后:"+ Arrays.toString(array));
}
}
上述代码使用了随机选取基准法.因为数据量很少,也就难以体现其效率的提升.我们会在后文,对各种排序的效率进行简单测试.
3.6.5.3优化方法三:三数取中法
3.6.5.3.1优化思路
因为随机选取基准法的"随机性"过高,较难保证选取到合适的基准.一种更加常用的算法是"三数取中法". 以下图所示的数组为例,三数取中,指的是left,right,mid三数,其中mid = left+(right-left)>>>1;
如下图所示数组,在第一轮排序中,我们会在4,6,8中选择中间大小的数与当前基准值4交换,也就是选择6与4交换.这样在每轮排序后,可以尽可能保证分组的均匀,避免出现"单分支"的情况,最大程度地提高算法效率.
3.6.5.3.2代码实现
package Sort;
import java.sql.SQLOutput;
import java.util.Arrays;
import java.util.Random;
public class TestSort {
/**
* 快速排序
* @param array
*/
public static void quickSort(int[] array) {
quick(array,0,array.length-1);
}
private static void quick(int[] array, int left, int right) {
if(left >= right) return;
// 1.在某个区间的时候,使用直接插入排序{优化区间内的比较}
if(right-left+1 <= 5){
//进行插入排序
insertSortRange(array,left,right);
return;
}
// 三数取中法
int index = medianOfThreeIndex(array,left,right);
swap(array,left,index);
int pivot = partitionHole(array,left,right);
quick(array,left,pivot-1);
quick(array,pivot+1,right);
}
private static int medianOfThreeIndex(int[] array, int left, int right) {
int mid = left + ((right-left)>>>1);
//int mid = (left + right) / 2;
if(array[left] < array[right]) {
if(array[mid] < array[left]) {
return left;
}else if( array[mid] > array[right]) {
return right;
}else {
return mid;
}
}else {
if(array[mid] < array[right]) {
return right;
}else if(array[mid] > array[left]) {
return left;
}else {
return mid;
}
}
}
private static void insertSortRange(int[] array, int left, int right) {
for(int i = left+1;i <= right;i++) {
int tmp = array[i];
int j = i-1;
for (; j >= left ; j--) {
if(array[j] > tmp) {
array[j+1] = array[j];
}else {
//array[j+1] = tmp;
break;
}
}
array[j+1] = tmp;
}
}
/*
这是Hole法
*/
private static int partitionHole(int[] array, int low, int high) {
int tmp = array[low];
while(low < high){
while(low < high && array[high] >= tmp){
high--;
}
array[low] = array[high];
while(low < high && array[low] <= tmp){
low++;
}
array[high] = array[low];
}
array[low] = tmp;
return low;
}
public static void main(String[] args) {
int[] array = {2,1,53,4,5,10,9,36,23,6,19};
System.out.println("排序前:"+ Arrays.toString(array));
quickSort(array);
System.out.println("排序后:"+ Arrays.toString(array));
}
}
对于"快排"的优化,可以参考这篇文章:
快速排序的优化操作
3.6.6非递归实现快速排序
3.6.6.1基本思路
非递归实现快速排序,需要用到栈这个数据结构.需要注意的是,实现非递归快速排序,一定要在进行了一轮排序的基础上.
3.6.6.2代码实现
package Sort;
import java.sql.SQLOutput;
import java.util.Arrays;
import java.util.Random;
import java.util.Stack;
public class TestSort {
/*
这是Hoare法
*/
private static int partitionHoare(int[] array, int low, int high) {
//以选择数组中第一个元素作为基准值为例
//首先,需要保存下数组中的第一个元素
int i = low;
int tmp = array[low];
while(low < high){
//1.如果low与high没有交叉且high下标元素>=基准值,high--继续找
while(low < high && array[high] >= tmp){
high--;
}
//出循环,说明此时high就是你要找的数字
//2.如果low与high没有交叉且low下标元素<=基准值,low++继续找
while(low < high && array[high] <= tmp){
low++;
}
//出循环,说明此时low就是你要找的数字
swap(array,low,high);
}
//出循环,此时low与high相遇,再将相遇处的值与基准值交换
swap(array,low,i);
return low;
}
/**
* 非递归实现快速排序
* @param array
*/
public static void quickSortNor(int[] array){
Stack <Integer> stack = new Stack<>();
int left = 0;
int right = array.length - 1;
//必须建立在一次排序的基础上
int pivot = partitionHoare(array,left,right);
//左边有两个数据以上,需要入栈
if(pivot > left+1){
stack.push(left);
stack.push(pivot-1);
}
//右边有两个数据以上,需要入栈
if(pivot < right-1){
stack.push(pivot+1);
stack.push(right);
}
while(!stack.isEmpty()){
right = stack.pop();
left = stack.pop();
pivot = partitionHoare(array,left,right);
//左边有两个数据以上
if(pivot > left+1){
stack.push(left);
stack.push(pivot-1);
}
//右边有两个数据以上,需要入栈
if(pivot < right-1){
stack.push(pivot+1);
stack.push(right);
}
}
}
public static void main(String[] args) {
int[] array = {2,1,53,4,5,10,9,36,23,6,19};
System.out.println("排序前:"+ Arrays.toString(array));
quickSortNor(array);
System.out.println("排序后:"+ Arrays.toString(array));
}
}
3.7归并排序
3.7.1基本思想
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
3.7.2图解+步骤
归并排序其实分为两部分:分解+合并.
具体步骤如下:
- 步骤1:把长度为n的输入序列分成两个长度为n/2的子序列;
- 步骤2:对这两个子序列分别采用归并排序;
- 步骤3:将两个排序好的子序列合并成一个最终的排序序列。
3.7.3代码实现
package Sort;
import java.sql.SQLOutput;
import java.util.Arrays;
import java.util.Random;
import java.util.Stack;
public class TestSort {
/**
* 归并排序
* @param array
*/
private static void merge(int[] array,int low,int mid,int high) {
int s1 = low;
int e1 = mid;
int s2 = mid+1;
int e2 = high;
int[] tmpArr = new int[high-low+1];
int k = 0;//代表tmpArr的下标
//证明两个段都有数据
while (s1 <= e1 && s2 <= e2) {
if(array[s1] <= array[s2]) {
tmpArr[k++] = array[s1++];
}else {
tmpArr[k++] = array[s2++];
}
}
//剩余元素,全部挪过来
while (s1 <= e1) {
tmpArr[k++] = array[s1++];
}
//剩余元素,全部挪过来
while (s2 <= e2) {
tmpArr[k++] = array[s2++];
}
for (int i = 0; i < tmpArr.length; i++) {
array[i+low] = tmpArr[i];
}
}
private static void mergeSortInternal(int[] array,int low,int high) {
if(low >= high) return;
int mid = low+((high-low)>>>1);
mergeSortInternal(array,low,mid);
mergeSortInternal(array,mid+1,high);
merge(array,low,mid,high);
}
public static void mergeSort(int[] array) {
mergeSortInternal(array,0,array.length-1);
}
public static void main(String[] args) {
int[] array = {2,1,53,4,5,10,9,36,23,6,19};
System.out.println("排序前:"+ Arrays.toString(array));
mergeSort(array);
System.out.println("排序后:"+ Arrays.toString(array));
}
}
3.7.4归并排序的非递归实现
package Sort;
import java.sql.SQLOutput;
import java.util.Arrays;
import java.util.Random;
import java.util.Stack;
public class TestSort {
private static void merge(int[] array,int low,int mid,int high) {
int s1 = low;
int e1 = mid;
int s2 = mid+1;
int e2 = high;
int[] tmpArr = new int[high-low+1];
int k = 0;//代表tmpArr的下标
//证明两个段都有数据
while (s1 <= e1 && s2 <= e2) {
if(array[s1] <= array[s2]) {
tmpArr[k++] = array[s1++];
}else {
tmpArr[k++] = array[s2++];
}
}
//剩余元素,全部挪过来
while (s1 <= e1) {
tmpArr[k++] = array[s1++];
}
//剩余元素,全部挪过来
while (s2 <= e2) {
tmpArr[k++] = array[s2++];
}
for (int i = 0; i < tmpArr.length; i++) {
array[i+low] = tmpArr[i];
}
}
/**
* \非递归的归并排序
* @param array
*/
public static void mergeSortNor(int [] array){
int gap = 1;
while(gap < array.length){
for (int i = 0; i < array.length; i+= 2*gap) {
int left = i;
int mid = left+gap-1;
if(mid >= array.length){
mid = array.length-1;
}
int right = mid+gap;
if(right >= array.length){
right = array.length-1;
}
merge(array,left,mid,right);
}
gap *= 2;
}
}
public static void main(String[] args) {
int[] array = {2,1,53,4,5,10,9,36,23,6,19};
System.out.println("排序前:"+ Arrays.toString(array));
mergeSortNor(array);
System.out.println("排序后:"+ Arrays.toString(array));
}
}
3.7.5海量数据的排序问题
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,归并排序是最常用的外部排序.
具体步骤:
- 先把文件切分成 200 份,每个 512 M;
- 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以;
- 进行二路归并,同时对 200 份有序文件做归并过程,最终结果就有序了.
图解:
3.7.6总结
- 时间复杂度:O(N*logN) (和数据是否有序没有关系)
- 空间复杂度:O(N),最多一次开辟N大小的空间.
- 稳定性:稳定
四:基于比较的排序总结
以上排序都是基于比较的排序.接下来,我会介绍三种非基于比较的排序.
五:非基于比较的排序
5.1计数排序
5.1.1基本思想
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
什么是鸽巢原理(又叫"抽屉原理")呢?简单来说就是,桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。这一现象就是我们所说的“抽屉原理”。 抽屉原理的一般含义为:“如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定有一个集合里至少有两个元素。” 它是组合数学中一个重要的原理.
5.1.2图解+步骤
基本步骤是:
- 统计相同元素出现次数;
- 根据统计的结果将序列回收到原来的序列中.
注意
- 在实际排序中,并不总是能遇到像0~9这么彰显人性光辉灿烂的题目.一种情况是,我们需要计数形如90-99之间的数据,这时候我们当前不会申请数组大小为100的空间,而是将90这个元素出现的次数记录在0下标位置,91这个元素出现的次数记录在1下标位置,依次类推即可.打印时只需要将相应的下标加上偏移量即可.
- 另外一种情况则更加普遍,例如我们需要计数的数据不是紧密排列的,而是毫无规律,如下图所示:
这种情况就需要先计算出数据的最大值和最小值,而所需数组大小就是[最大值-最小值+1].不难发现,此时有许多空间会被浪费,这也说明,计数排序的时间复杂度and空间复杂度是与所排序数据的范围密切相关的,我会在后面的总结中谈到这个问题.
5.1.3代码实现
package Sort;
import java.sql.SQLOutput;
import java.util.Arrays;
import java.util.Random;
import java.util.Stack;
public class TestSort {
/**
* 计数排序
* @param array
*/
public static void countSort(int [] array) {
int min = array[0];
int max = array[0];
for (int i = 1; i < array.length; i++) {
if(min > array[i]){
min = array[i];
}
if(max < array[i]){
max = array[i];
}
}
//此时min和max分别是数据中的最大值和最小值
int[] arr = new int[max-min+1];
for (int i = 0; i < array.length; i++) {
arr[array[i]-min]++;
}
int k = 0;
for (int i = 0; i < arr.length; i++) {
int count = arr[i];
while(count-- != 0){
array[k++] = (i+min);
}
}
}
public static void main(String[] args) {
int[] array = {2,1,53,4,5,10,9,36,23,6,19};
System.out.println("排序前:"+ Arrays.toString(array));
countSort(array);
System.out.println("排序后:"+ Arrays.toString(array));
}
}
5.1.4总结
- 时间复杂度:O(范围+N);
- 空间复杂度:O(范围);
- 稳定性:稳定
注意
虽然在上文中我所书写的代码是不能保证稳定性的,但是我们可以通过一些方法,使得计数排序的结果是稳定的.具体操作如下:
5.2基数排序
5.2.1基本思想
基数排序在计数排序的基础上进行了改进,将排序工作拆分为多个阶段,以排序整数数据为例,每一个阶段只根据一个数字进行排序,一共排序K轮(K为数据位数).
5.2.2图解+步骤
5.3桶排序
5.3.1基本思想
桶排序类似于基数排序,,不同之处在于,桶排序时是将数据划分为不同区间,在每个区间(桶)内的数据排好序之后,再依次出桶,放回原来的序列里.
5.3.2图解+步骤
步骤:
- 先根据排序数据的范围,确定需要的桶的个数;
- 将所有数据放入桶内,在桶内进行排序;
- 所有数据依次出桶,即可实现全部数据有序.
六:所有排序算法总结
以上就是所有的排序算法!
我们一定要熟练掌握7种比较排序的思想及代码,对于三种非比较排序,只需掌握其基本思路即可.重在理解,及时复盘,才能事半功倍!!