本篇博客中总结了以下经典排序算法:
冒泡排序、快速排序、简单插入排序、希尔排序、简单选择排序、堆排序、二路归并排序、计数排序、桶排序、基数排序。
经典的排序算法分为下面两大类:
非线性时间比较类排序:通过比较来决定元素间的相对次序。时间复杂度不能突破O(nlogn)而得此名称。
线性时间非比较类排序:不能通过比较来决定元素间的相对次序。它可以突破比较排序的时间下限,以线性时间运行而得此称。
目录
一、交换排序
1.1、冒泡排序
在深入学习更多排序算法后和在实际使用情况中,冒泡排序的使用还是极少的。它适合数据规模很小的时候,而且它的效率也比较低,但是作为入门的排序算法,还是值得学习的。
冒泡排序是一种简单的排序算法。它重复地走访要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法步骤描述:
①比较两个相邻的元素。如果第一个比第二个大,就交换它们两个;
②对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
③针对所有的元素重复以上的步骤,除了最后一个;
④重复步骤①~③,直到排序完成。
代码实现:
package com.review08.sort;
public class BubbleSort {
public static void bubblesort(int arr[]) {
int temp = 0;
for(int i=0; i<arr.length-1; i++) { //外层循环控制排序的趟数
for(int j=0; j<arr.length-1-i; j++) { //内层循环控制比较次数
if(arr[j]>arr[j+1]) { //相邻两个数比较,前面的数大就交换
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
System.out.println("\n第"+(i+1)+"趟:");
printarr(arr);
}
}
public static void printarr(int [] arr) {
for (int i=0; i<arr.length; i++) {
System.out.print(arr[i] +" ");
}
}
public static void main(String[] args) {
int[] arr = {9,8,7,6,5,4,3,2,1};
System.out.println("排序前:");
printarr(arr);
bubblesort(arr);
}
}
选择排序方法时,冒泡排序显然不够理想,看看下面改良后的快速排序。
1.2、快速排序
所谓快速排序:基于分治的思想,是冒泡排序的改进型。首先在数组中选择一个基准点并把基准点放于序列的开头,这里就基准点就选取数组的第一个元素; 然后分别从数组的两端扫描数组,设两个指示标志(i 指向起始位置,j 指向末尾),首先从后半部分开始,如果发现有元素比该基准点的值小,就交换 i 和 j 位置的值,然后从前半部分开始扫秒,发现有元素大于基准点的值,就交换 i 和 j 位置的值,如此往复循环,直到 i >= j ,然后把基准点的值放到 i 这个位置,一次排序就完成了;之后再采用递归的方式分别对前半部分和后半部分排序,当前半部分和后半部分均有序时该数组自然也就有序了。
看下面的例子:给5,8,3,6,7,9,2用快速排序法排序
第一趟:
快速排序代码实现(用到了递归哦!):
package com.review08.sort;
import java.lang.reflect.Array;
public class QuickSort {
/**寻找所标记数的每一趟排序后的最终位置(数组下标)
* @param arr 要排序的数组
* @param s 开始位置
* @param e 结束位置
* @return i 中间位置
*/
public static int sort(int arr[], int s, int e) { //寻找中间位置
int mark = arr[s];
int i = s;
int j = e;
while(i<j) { //直到i、j相遇
while (i < j) { //j负责找小的,扔给i
if(arr[j] < mark) {
arr[i] = arr[j];
break; //扔给j后,退出里面的while循环,让i走
}
j--; //所在位置比mark的值大,就继续向前走
}
while (i < j) {
if(arr[i] >= mark) { //i负责找大的,扔给j
arr[j] = arr[i];
break; //扔给i后,退出里面的while循环,让i走
}
i++; //所在位置比mark的值小,就继续向前走
}
}
arr[i] = mark; //把标记的那个数放在i位置上
return i;//最后i所在位置就是中间位置(i所在位置的前面的数都比它小,后面的都比它大)
}
public static void quicksort(int arr[], int s, int e) { //这里用到了递归算法!
if(s<e) {
int index = sort(arr, s, e); //中间那个位置
quicksort(arr, s, index-1); //中间位置的左边
quicksort(arr, index+1, e); //中间位置的右边
}
}
public static void printarr(int arr[]) { //打印数组
for (int i=0; i<arr.length; i++) {
System.out.print(arr[i]+" ");
}
}
public static void main(String[] args) {
int[] arr = {6,7,5,4,8,9,1,2};
sort(arr, 0, arr.length-1);
System.out.println("第一趟排序结果:");
printarr(arr);
quicksort(arr, 0, arr.length-1);
System.out.println("\n最终排序结果:");
printarr(arr);
}
}
二、插入排序
2.1简单插入排序
把表分成两部分,前半部分已排序,后半部分未排序,后半部分元素依次一个一个插入前半部分适当的位置。
假设线性中前 j-1元素已经有序,现在要将线性表中第 j 个元素插入到前面的有序子表中,插入过程如下:
将第 j 个元素放到一个变量T中,然后从有序子表的最后一个元素(即线性表中第 j-1个元素)开始,往前逐个与T进行比较,将大于T的元素均依次向后移动一个位置,直到发现一个元素不大于T为止,此时就将T(即原线性表中的第 j 个元素)插入到刚移出的空位置上,有序子表的长度就变为 j 了。效率与冒泡排序法相同。
例:15,45,84,48,79,10,35,34,88
(15) | 45 84 48 79 10 35 34 88
(15 45) | 84 48 79 10 35 34 88
(15 45 84) | 48 79 10 35 34 88
(15 45 48 84) | 79 10 35 34 88
(15 45 48 79 84) | 10 35 34 88
(10 15 45 48 79 84) | 35 34 88
(10 15 35 45 48 79 84) | 34 88
(10 15 34 35 45 48 79 84) | 88
(10 15 34 35 45 48 79 84 88)
代码实现:
package com.review08.sort;
public class SimpleInsertSort {
public static void simpleinsert(int[] arr) {
if(arr.length<1) {
return;
}
for(int i=1; i<arr.length; i++) {//注意,i从1开始,我们是从arr[1]开始与前面的元素比较的
//因为0~i-1是有序的,如果arr[i]>arr[i-1],则0~i也是有序的
if(arr[i]<arr[i-1]) {
int temp = arr[i];//先保存i位置元素的值
int j = i-1;//从i-1开始向前找,一直找到比i位置元素小的位置,然后插入
for(; j>=0&&arr[j]>temp; j--) {
arr[j+1] = arr[j];//没有找到就将此位置的元素向后移一位,腾出位置
}
arr[j+1] = temp;//将i位置元素放在腾出的位置上面
}
}
}
public static void main(String[] args) {
int[] arr = {41,12,8,65,32,75,11,29};
System.out.println("简单插入排序前:");
printArr(arr);
simpleinsert(arr);
System.out.println("\n简单插入排序后:");
printArr(arr);
}
public static void printArr(int[] arr) {
for(int i=0; i<arr.length; i++) {
System.out.print(arr[i]+" ");
}
}
}
2.2、希尔(shell)排序
希尔排序严格来说是基于插入排序的思想,又被称为缩小增量排序。
具体流程如下:
1、将包含n个元素的数组,分成d=n/2个数组序列,第一个数据和第n/2+1个数据为一对...
2、对每对数据进行比较和交换,排好顺序;
3、然后分成d=n/4个数组序列,再次排序;
4、不断重复以上过程,随着d减少并直至为1,排序完成。
下面举个例:
原始序列:18,35,94,47,61,28,17,98,共8个数
代码实现
public class ShellSort {
//法一:
public static void shellSort1(int[] arr) {
int i,j,r,temp;
r = arr.length/2;
for( ; r>=1; r=r/2) { //划分组,间隔为r
for(i=r;i<arr.length;i++) {
temp = arr[i];
j = i - r;
//一轮排序
while(j >=0 && temp <arr[j]) {
arr[j+r] = arr[j];
j -= r;
}
arr[j+r] = temp;
}
}
}
//法二:
public static void shellSort2(int[] arr) {
if(arr == null && arr.length <= 1) {
return;
}
int increment = arr.length/2;//增量
while(increment >= 1) {
for(int i=0; i<arr.length-increment; i++) {
for(int j=i; j<arr.length-increment; j=j+increment) {
if(arr[j]>arr[j+increment]) {
int temp = arr[j];
arr[j] = arr[j+increment];
arr[j+increment] = temp;
}
}
}
increment = increment/2; //设置新的增量
}
}
}
三、选择排序
3.1、简单选择排序
基本思想(数组长度为n):
第一次遍历n-1个数,找到最小的数值与第一个元素交换;
第二次遍历n-2个数,找到最小的数值与第二个元素交换;
...
第n-1次遍历,找到最小的数值与第n-1个元素交换,排序完成
看下面的例子:
代码实现简单交换排序
思路:第一个层循环遍历数组,第二层循环找到剩余元素中最小值的索引,内层循环结束,交换数据。 内层循环每结束一次,排好一位数据。两层循环结束,数据排好有序。
public static void simpleswap(int[] arr) {
for(int i=0; i<arr.length; i++) { //负责遍历索引
int index =i ;//index负责找最小的索引
for(int j=i+1; j<arr.length; j++) { //负责找剩余元素中的最小值
if(arr[j]<arr[index]) {
index = j;
}
}
//每找到一次就与前面的交换一次
if(i != index){
int temp = arr[index];
arr[index] = arr[i];
arr[i] = temp;
}
}
}
3.2、堆排序
下一篇文章二叉树里讲的是小根堆实现的堆排序,大根堆实现原理也差不多!
堆是一棵顺序存储的完全二叉树。完全二叉树中所有非终端节点的值均不大于(或不小于)其左、右孩子节点的值。
其中每个结点的值小于等于其左、右孩子的值,这样的堆称为小根堆; 其中每个结点的值大于等于其左、右孩子的值,这样的堆称为大根堆。
堆排序的大概步骤如下:
- 构建大根堆;
- 选择顶,并与第0位置元素交换;
- 由于步骤2的的交换可能破环了大根堆的性质,第0不再是最大元素,需要调用maxHeap调整堆(沉降法),如果需要重复步骤2。
堆排序中最重要的算法就是maxHeap,该函数假设一个元素的两个子结点都满足最大堆的性质(左右子树都是最大堆),只有跟元素可能违反最大堆性质,那么把该元素以及左右子结点的最大元素找出来,如果该元素已经最大,那么整棵树都是最大堆,程序退出,否则交换跟元素与最大元素的位置,继续调用maxHeap原最大元素所在的子树。该算法是分治法的典型应用。
四、归并排序
归并排序 (merge sort) 是一类与插入排序、交换排序、选择排序不同的另一种排序方法。归并的含义是将两个或两个以上的有序表合并成一个新的有序表。归并排序有多路归并排序、两路归并排序 , 可用于内排序,也可以用于外排序。这里仅仅写到内排序的归并方法!
4.1、二路归并排序
代码实现二路归并排序:
package com.review08.sort;
public class TwoWaysMergeSort {
private static int number = 0;
public static void main(String[] args) {
int[] arr = {10,15,33,8,48,11,95,87,38};
printArr("排序前:",arr);
twomergesort(arr);
printArr("\n排序后:",arr);
}
public static void printArr(String pre, int[] a) {
System.out.print(pre);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
private static void twomergesort(int[] arr) {
System.out.println("\n**************开始排序**************");
sort(arr, 0, arr.length-1);
}
private static void sort(int[] arr, int left, int right) {
if (left >= right) {
return;
}
int middle = (left + right) / 2;
//二路归并里面有两个sort,多路归并里面有多个sort
sort(arr, left, middle);
sort(arr, middle + 1, right);
merge(arr, left, middle, right);
}
private static void merge(int[] arr, int left, int middle, int right) {
int[] temp = new int[arr.length];
int r1 = middle + 1;
int left1 = left;
int left2 = left;
//逐个归并
while (left <= middle && r1 <= right) {
if (arr[left] <= arr[r1]) {
temp[left1++] = arr[left++];
} else {
temp[left1++] = arr[r1++];
}
}
//将左边剩余的归并
while (left <= middle) {
temp[left1++] = arr[left++];
}
//将右边剩余的归并
while (r1 <= right) {
temp[left1++] = arr[r1++];
}
System.out.print("第"+(++number)+"趟排序:");
//从临时数组拷贝到原数组
while(left2 <= right) {
arr[left2] = temp[left2];
//输出中间归并排序结果
System.out.print(arr[left2]+"\t ");
left2++;
}
System.out.println();
}
}
4.2、多路归并排序
多路归并排序涉及到的情况较为复杂,这里略过!
————————————————————————————————————————————————
常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。
计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置。非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。
——————————————————————————————————————————————————
五、线性时间非比较类排序
5.1、计数排序
计数排序需要占用大量的空间,仅仅只适用要比较的数据范围很集中的情况,比如0~1000,10000~20000,...这样在一定范围的数据。
需要三个数组:
待排序数组 int[] before= new int[]{4,3,6,3,5,1};
辅助计数数组 int[] arr= new int[max - min + 1]; //该数组大小为待排序数组中的最大值减最小值+1
输出数组 int[] res = new int[before.length];
1.求出待排序数组before的最大值max=6, 最小值min=1
2.实例化辅助计数数组arr,arr数组中每个下标对应before中的一个元素,arr用来记录每个元素出现的次数
3.计算 before中每个元素在arr中的位置 position = before[i] - min,此时 arr= [1,0,2,1,1,1]; (3出现了两次,2未出现)
4.根据 arr数组求得排序后的数组,此时 res = [1,3,3,4,5,6]
例:
原始数列:105 110 105 102 111 107 108 104 110 105
Java代码实现计数排序:
package com.review08.sort;
public class CountSort {
public static int[] countSort(int[] before) {
//找出数组中的最大值和最小值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int i=0; i<before.length; i++) {
max = Math.max(max,before[i]);
min = Math.min(min,before[i]);
}
int[] arr = new int[max-min+1]; //记录出现次数的数组
//找出每个数字出现的次数
for (int i=0; i<before.length; i++) {
int index = before[i]-min;
arr[index] ++;
}
//计算每个数字在排序后数组中应该出现的位置
for(int i=1; i<arr.length; i++) {
arr[i] = arr[i] +arr[i-1];
}
//根据arr数组排序
int res[] = new int[before.length];
for(int i=0; i<before.length; i++) {
int index = --arr[before[i]-min];
res[index] = before[i];
}
return res;
}
//测试
public static void main(String[] args) {
int [] before = {105,110,105,102,111,107,108,104,110,105,105};
printArr(countSort(before));
}
//打印数组元素
public static void printArr(int[]arr) {
for(int i=0; i<arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
}
5.2、桶排序
桶排序与计数排序恰恰相反,桶排序可用于最大值与最小值相差较大的情况,比如[106,1902,47867,83556,102456]。不过计数排序是桶排序的一种特殊情况,可以把计数排序当成每个桶里只有一个元素的情况。
桶排序要求数据的分布必须均匀,否则可能导致数据都集中到一个桶中。比如[114,170,127,132,20000], 这种数据会导致前4个数都集中到同一个桶中。导致桶排序失效。
桶排序步骤:
1.找出待排序数组中的最大值max、最小值min
2.我们使用动态数组ArrayList 作为桶,桶里放的元素也用 ArrayList 存储。桶的数量为(max-min)/arr.length+1
3.遍历数组 arr,计算每个元素 arr[i] 放的桶
4.每个桶各自排序
5.遍历桶数组,把排序好的元素放进输出数组
简言之,就是把数组 arr 划分为n个大小相同子区间(桶),每个子区间各自排序,最后合并。
代码后期更新!
5.3、基数排序
思想:把待排序的整数按位分,分为个位,十位.....从小到大依次将位数进行排序。实际上分为两个过程:分配和收集。 分配就是:从个位开始,按位数从小到大把数据排好,分别放进0--9这10个桶中; 收集就是:依次将0-9桶中的数据放进数组中,重复这两个过程直到最高位。
例:
代码实现:
package com.review08.sort;
import java.util.Arrays;
public class RadixSort {
public static void main(String[] args) {
int a[] = {400,52,61,84,96,195,26};
radixSort(a,10,3);
for(int i=0; i<a.length; i++) {
System.out.print(a[i] + " ");
}
}
/**
* 基数排序
* @param arr 待排序数组
* @param radix 基数(10,桶的个数)
* @param distance 待排序中,最大的位数
*/
public static void radixSort(int[] arr, int radix, int distance) {
int length = arr.length;
int[] temp = new int[length]; //用于暂存元素
int[] count = new int[radix]; //用于计数排序的桶
int divide = 1;//除数,用于取个位数数字、十位数数字、百位数数字...
for(int i=0; i<distance; i++) {
System.arraycopy(arr,0,temp,0,length);
Arrays.fill(count,0); //桶清空
for(int j=0; j<length; j++) { //这个循环用于吧每个数的个十百千分开,并使相对应号数的桶的个数增加1
//divide:1 10 100 radix:10
int tempKey = (temp[j]/divide) % radix;
count[tempKey] ++;
}
for (int j=1; j<radix; j++) {
count[j] = count[j] + count[j-1];
}
for(int j=length-1; j>=0; j--) {
int tempKey = (temp[j]/divide) % radix;
count[tempKey] --;
arr[count[tempKey]] = temp[j];
}
divide = divide * radix; //1 10 100
}
}
}
补充
最后附上最近看马士兵老师讲的视频中的对排序写的一首宋词。
画红圈圈的四种排序算法是一定要掌握的!时间复杂度、空间复杂度和稳定性(参看马士兵老师的宋词)也是要掌握的!