简述
都说十大排序算法对于程序员是一个必备的技能点,因为对十大排序认识自己还是很迷茫,所以这篇文章将是我对排序的进一步认识和理解,下面我会对十大排序进行介绍,并附上java代码实现,第一次写文章有不足地方还请谅解。
目录
- 术语解释
- 稳定性
(1) 稳定排序
(2) 非稳定排序 - 时间复杂度
- 空间复杂度
- 内部排序
- 外部排序
- 稳定性
- 排序算法总结(图解)
- 冒泡排序
- 选择排序
- 插入排序
- 希尔排序
- 归并排序
- 非递归式
- 递归式
- 快速排序
- 堆排序
- 计数排序
- 桶排序
- 基数排序
术语解释
排序中所用到的术语是用来描述排序有哪些优缺点以便在不同场合使用排序算法达到更好效果。
1. 稳定性
(1) 稳定排序:在序列中如果a在b的前面,并且a==b,经过排序交换后,a还在b的前面,则称为稳定排序。
(2) 非稳定排序:在序列中如果a在b的前面,并且a==b,经过排序交换后,a有可能不在b前面,则称为非稳定排序。
2. 时间复杂度
一个算法执行需要消耗的时间。
3. 空间复杂度
运行完一个程序所需内存的大小。
4. 内部排序
是指在排序过程中不占用额外的存储空间,只利用原来存储待排数据的存储空间进行比较和交换的数据排序。
5. 外部排序
因为数据量大,需要占用额外的存储空间的数据排序。
排序算法总结(图解)
这是我在网上找到的一张图片,里面对十大排序各种性质进行了罗列,以方便自己的理解。
图片注释:
O(1) : 就是y=1,是一个常量,不管x怎么变,y不变,一条与x轴平行的线。
n : 数据的规模。
k : 表示临时数组的大小、桶的个数。
In-place:表示占用常数内存或不占用额外内存。
Out-place:占用额外的内存。
1. 冒泡排序
冒泡排序一种较为简单的排序算法。进行冒泡排序最大需要n趟(n取决于数据的大小),而每一趟都会进行序列的相邻数据的大小比较并交换位置(谁在前面需自己决定,下面是以小数据的排在前面为例)
性质:时间复杂度O(n^2),空间复杂度O(1),属于内部排序、稳定排序。
代码实现:
public class BubbleSort {
public static void main(String[] args) {
//这里我用从小到大排序作为例子
int[] a = {5,7,2,8,10,3,6,1,9};
for(int i = 0 ; i < a.length ; i++) {
boolean flag = true;
for(int j = 0 ; j < a.length-1-i; j++ ) {
if(a[j] > a[j+1]) { //这里可以根据自己决定大的在前或者小的在前
flag = false;
int tem = a[j]; //两两交换
a[j] = a[j+1];
a[j+1] = tem;
}
}
if(flag){ //判断是否排完,提前结束
break;
}
}
for(int i=0 ; i < a.length ; i++) {
System.out.print(a[i] + " ");
}
}
}
2. 选择排序
在n个数据的序列中,需要经过n-1趟的排序。选择排序的原理就是每次在序列中找到最小(大)值,首先每一次找最值需要定一个标准,把未排序列的第一个当作最小(大)值这么一标准,然后在剩下的序列中再找比这个最值小或者大的值来代替它,最终和未排序列的第一个交换(如果找不到那就是自己和自己交换),重复以上步骤,直到排序完成。下面我以从小到大排序为例子。
性质:时间复杂度O(n^2),空间复杂度O(1),属于内部排序、非稳定排序。
代码实现:
public class SelectionSort {
public static void main(String[] args) {
//这里我用从小到大排序作为例子
int[] a = {5,7,2,8,10,3,6,1,9};
for(int i = 0 ; i < a.length-1 ; i++) {
int minIndex = i; //先假设最小的索引是序列的第一个
for(int j = i+1 ; j < a.length; j++) {
if(a[minIndex] > a[j]) {
minIndex = j; //更新索引值
}
}
int tem = a[minIndex]; //找到最小值和未排列序列第一个数字交换
a[minIndex] = a[i];
a[i] = tem;
}
for(int i=0 ; i < a.length ; i++) {
System.out.print(a[i] + " ");
}
}
3. 插入排序
工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
执行步骤(以从小到大排序为例):
- 从第一个元素开始,可以认为是已经排序
- 取第二个元素,在已排的序列中从后向前扫描
- 如果已排的元素大于该元素,那么已排元素向后移动一位
- 重复第三步,直到找到已排元素小于或等于该元素时为止
- 将该元素插入最后停止的位置
性质:时间复杂度O(n^2),空间复杂度O(1),属于内部排序、稳定排序。
代码实现:
public class InsertionSort {
public static void main(String[] args) {
//这里我用从小到大排序作为例子
int[] a = {5,7,2,8,10,3,6,1,9};
for(int i = 1 ; i < a.length; i++ ) {
int insert = a[i]; //取出要插入的元素
int ageIndex = i-1;
for(; ageIndex >= 0 && a[ageIndex] > insert ; ageIndex--) {//这里需把ageIndex >= 0写在前面,不然会报错
a[ageIndex+1] = a[ageIndex]; //把大于或小于insert的元素向后移
}
a[ageIndex+1] = insert; //插入ageIndex+1个位置上,因为上面的循环中最后多ageIndex多减了一次
}
for(int i=0 ; i < a.length ; i++) {
System.out.print(a[i] + " ");
}
}
}
4. 希尔排序
希尔排序可以说是插入排序改进版,在插入排序中,比较数据是针对相邻之间是元素,而希尔排序可以进行远距离比较。
执行步骤:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的 子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
性质:时间复杂度O(n log n),空间复杂度O(1),属于内部排序、非稳定排序。
代码实现:
public class ShellSort {
public static void main(String[] args) {
//这里我用从小到大排序作为例子
int[] a = {5,7,2,8,10,3,6,1,9,4};
int gap = a.length/2; //分组进行,各组之间交替进行
for(; gap > 0 ; gap = gap/2) { //进行分组,每次都是缩小一半
for(int i = gap ; i < a.length ; i++) { //这里就是纯粹插入排序
int insert = a[i];
int ageIndex = i-gap;
for(;ageIndex >=0 && a[ageIndex] > insert ; ageIndex = ageIndex - gap) {
a[ageIndex + gap] = a[ageIndex];
}
a[ageIndex + gap] = insert;
}
}
for(int i=0 ; i < a.length ; i++) {
System.out.print(a[i] + " ");
}
}
}
5. 归并排序
归并排序是将一个数组分成左右两部分,然后进行比较排序,最终再合并成一个完全有序的一种算法。归并排序在进行分组的过程中,需把序列分到不能再分的情况下(也就是说只剩1个元素的时候),依次进行左右两部分元素比较并合并(使子序列变得有序),最终得到整个序列有序。
执行步骤:
- 把长度为n的输入序列分成两个长度为n/2的子序列
- 对这两个子序列分别采用归并排序
- 将两个排序好的子序列合并成一个最终的排序序列
性质:时间复杂度O(n log n) ,空间复杂度O(n) ,属于外部排序、稳定排序。
代码实现:
1. 非递归式
public class MergeSort2 {
public static void main(String[] args) {
//这里我用从小到大排序作为例子
int[] a = {5,7,2,8,10,3,6,1,9,4};
mergeSort(a);
for(int i=0 ; i < a.length ; i++) {
System.out.print(a[i] + " ");
}
}
public static int[] mergeSort(int[] a) {
if(a == null || a.lenght < 2)
return;
//归并排序(非递归式)
//从数组大小为1的时候开始合并,接着是2、4、8....
for(int i=1 ; i < a.length; i += i) {
//对数组进行划分
int left = 0;
int mid = left + i - 1;
int right = mid + i;
//进行合并
while(right < a.length) {
merge(a, left, mid, right);
left = right + 1;
mid = left + i - 1;
right = mid + i;
}
//对漏掉的数组进行合并
if(left < a.length && mid < a.length) {
merge(a, left, mid, a.length - 1);
}
}
return a;
}
public static void merge(int[] a, int left, int mid, int right) {
//创建一个能存储左半部分和右半部分的临时数组
int[] arr = new int[right - left + 1];
int i = left;
int j = mid + 1;
int index = 0;
//合并数组
while(i <= mid && j <= right) {
if(a[i] < a[j]) {
arr[index++] = a[i++];
}else {
arr[index++] = a[j++];
}
}
//下面这两个while循环是用来添加漏掉的剩余元素
while(i <= mid) {
arr[index++] = a[i++];
}
while(j <= right) {
arr[index++] = a[j++];
}
//把临时数组给复制进原来数组
for(i = 0; i < index; i++) {
a[left++] = arr[i];
}
}
}
2. 递归式
public class MergeSort {
public static void main(String[] args) {
//这里我用从小到大排序作为例子
int[] a = {5,7,2,8,10,3,6,1,9,4};
//归并排序(递归式)
mergeSort(a, 0, a.length-1);
for(int i=0 ; i < a.length ; i++) {
System.out.print(a[i] + " ");
}
}
public static void mergeSort(int[] a, int left, int right) {
//将传进来的数组分成两个数组
if(left
6. 快速排序
快速排序是通过每一趟设置一个“基准”来把序列分成两部分进行排序,其中一部分均小于这个“基准”,另一部分则均大于这个“基准”,再把小于这个“基准”的部分再设定一个“基准”,再次分成两部分进行排序,大于这个“基准”的部分也是同样,以此类推,分到不能再分为止,此时整个序列就达到了有序。(这和归并排序有些类似)
执行步骤:
- 在序列中设定一个“基准”
- 排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面
- 对比基准值小的元素和比基准值大的元素重复1、2步
性质:时间复杂度O(n log n),空间复杂度O(log n),属于内部排序、非稳定排序。
代码实现:
public class QuickSort {
public static void main(String[] args) {
//这里我用从小到大排序作为例子
int[] a = {5,7,2,8,10,3,6,1,9,4};
quicksort(a, 0, a.length - 1);
for(int i=0 ; i < a.length ; i++) {
System.out.print(a[i] + " ");
}
}
public static void quicksort(int[] a, int start, int end) {
int i, j;
if(start > end) {
return;
}
i = start;
j = end;
int jz = a[start]; //先设定序列第一个元素为基准
while(i < j) {
while(jz <= a[j] && i < j) { //这里必须先从右向左找小于基准的第一个元素
j--; //否则会报索引越界异常
}
while(jz >= a[i] && i < j) {
i++;
}
if(i < j) { //把小于基准的和大于基准的元素交换
int tem = a[i];
a[i] = a[j];
a[j] = tem;
}
}
a[start]=a[j]; //到这里证明这一趟排完
a[j]=jz; //更换基准的位置,使得左边都是比基准小的,右边都是比基准大的
quicksort(a, start, j-1); //然后再对这左右两部分进行快速排序(不包含基准)
quicksort(a, j+1, end);
}
}
7. 堆排序
堆排序就是把未排序列看成是一个二叉堆,每次通过堆顶和最后一个元素进行交换,再进行堆顶的调整操作,重复操作来达到有序数列。
执行步骤:
- 创建二叉堆(可以是大顶堆,也可以是小顶堆),此时都为无序
- 把堆顶的元素和无序状态的最后一个元素交换,交换后的堆的最后一个元素位置为有序
- 由于交换后的堆的特性有可能被破坏,需要对堆进行调整,调整为新的二叉堆
- 重复2、3步,直到排序完成
解释:
1.大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列。
2.小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列。
性质:时间复杂度O(n log n),空间复杂度O(1),属于内部排序、非稳定排序。
代码实现:
public class HeapSort {
public static void main(String[] args) {
//这里我用从小到大排序作为例子
int[] a = {5,7,2,8,10,3,6,1,9,4};
heapsort(a);
for(int i=0 ; i < a.length ; i++) {
System.out.print(a[i] + " ");
}
}
public static void heapsort(int[] a) {
//构建二叉堆(大顶堆)
for(int i = (a.length - 2) / 2 ; i >= 0 ; i--) {
downAdjust(a, i, a.length);
}
//堆排序
for(int i = a.length - 1; i >= 1; i--) {
//把堆顶的元素与最后一个元素交换
int tem = a[i];
a[i] = a[0];
a[0] = tem;
//为了保持有序,交换后还行进行下沉操作
downAdjust(a, 0, i);
}
}
//调整操作(父节点下沉,子节点上浮)
public static void downAdjust(int[] a, int parent, int n) {
//临时保存要下沉的元素
int tem = a[parent];
//定位左孩子节点的位置
int child = 2 * parent + 1;
//开始下沉
while(child < n) {
//如果右孩子大于左孩子,则定位到右孩子
if(child + 1 < n && a[child + 1] > a[child]) {
child = child + 1;
}
//如果孩子节点小于或等于父节点,则下沉结束
if(a[child] <= tem) {
break;
}
//父节点进行下沉
a[parent] = a[child];
parent = child;
child = 2 * parent + 1;
}
a[parent] = tem;
}
}
8. 计数排序
之前所介绍的排序都是基于比较的排序,那么计数排序就不一样了。计数排序是用一个临时数组来储存原来数组中的元素出现的次数,然后再把统计的元素从小到大或是从大到小依次罗列出来。适合在数据元素较为集中时使用。
执行步骤:
- 找出待排序的数组中最大和最小的元素
- 建立一个临时数组tem,数组大小是最大值和最小值的差值
- 统计原来数组里的元素出现的次数,存放进tem数组,tem数组的下标就是原来数组里的元素,如tem[i] = n,表示元素i出现了n次。
- 创建两个循环嵌套,第一个循环遍历tem数组,第二个循环遍历元素出现的次数,保存进原来数组里面(也就是说元素出现几次就添加几次这个元素)
性质:时间复杂度O(n+k),空间复杂度O(n+k),属于外部排序、稳定排序。其中为k临时数组的大小。
代码实现:
public class CountingSort {
public static void main(String[] args) {
//这里我用从小到大排序作为例子
int[] a = {5,7,2,8,10,3,6,1,9,4};
countingsort(a);
for(int i=0 ; i < a.length ; i++) {
System.out.print(a[i] + " ");
}
}
public static void countingsort(int[] a) {
if(a == null || a.length < 2)
return;
int max = a[0];
int min = a[0];
//寻找数组中的最大值和最小值
for(int i = 0 ; i < a.length ; i++) {
if(max < a[i]) {
max = a[i];
}
if(min > a[i]) {
min = a[i];
}
}
//创建一个临时的数组,为了防止创建多余的空间,
//需要定义一个区间的大小,这个区间大小由最大和最小值的差值决定
int[] tem = new int[max - min + 1];
//统计元素出现的次数,元素的值当做tem数组的下标
for(int i = 0 ; i < a.length; i++) {
tem[a[i] - min]++;
}
int index = 0;
//把统计好的数据还原到原来数组中
for(int i = 0 ; i < max - min + 1; i++) {
for(int j = tem[i] ; j > 0; j--) {
a[index++] = i + min;
}
}
}
}
9. 桶排序
桶排序是计数排序的一个更高级版本,桶排序和计数排序的不同就是桶排序把最大值和最小值之间的这些数据进行瓜分,放到不同的桶中,对每个桶进行排序(这里可以使用其他排序,比如快速排序、插入排序等看情况而定),最终再把每个桶里面的序列拼接起来使整个序列有序。
执行步骤:
- 同样地,找出原数组中的最大值和最小值
- 设置桶的数量,定义每一个桶所装的元素范围、桶的数量
- 创建桶并初始化
- 遍历原数组并把数据放到对应的桶里
- 对每个桶进行排序
- 把每个桶排好的数据进行拼接合并
性质:时间复杂度O(n+k),空间复杂度O(n+k),属于外部排序、稳定排序。其中的k是代表桶的个数。
代码实现:
import java.util.ArrayList;
public class BucketSort {
public static void main(String[] args) {
//这里我用从小到大排序作为例子
int[] a = {5,7,2,8,10,3,6,1,9,4};
bucketsort(a);
for(int i=0 ; i < a.length ; i++) {
System.out.print(a[i] + " ");
}
}
public static void bucketsort(int[] a) {
if(a == null || a.length < 2)
return;
int max = a[0];
int min = a[0];
//寻找数组中的最大值和最小值
for(int i = 0 ; i < a.length ; i++) {
if(max < a[i]) {
max = a[i];
}
if(min > a[i]) {
min = a[i];
}
}
//计算差值
int d = max - min;
//设置桶存放元素的范围,这里5就是0~4,5~9,10~14...
int bucketSize = 5;
//桶的数量
int bucketNum = d/5 + 1;
//创建桶
ArrayList> bucketList = new ArrayList<>();
//初始化桶
for(int i = 0 ; i < bucketNum; i++) {
bucketList.add(new ArrayList());
}
//遍历原数组,将每个元素放入桶中
for(int i = 0 ; i < a.length ; i++) {
//计算应该存放在哪个桶
Integer I = (a[i] - min) / bucketSize;
bucketList.get(I).add(a[i] - min);
}
//对每个桶进行排序,这里使用插入排序
for(int i = 0 ; i < bucketNum ; i++) {
insertionsort(bucketList.get(i));
}
//把每个排序好的数据进行合并汇总放回原数组
int index = 0;
for(int i = 0 ; i < bucketNum; i++) {
for(Integer j : bucketList.get(i)) {
a[index++] = j + min;
}
}
}
public static void insertionsort(ArrayList a) {
for(int i = 1 ; i < a.size(); i++ ) {
int insert = a.get(i); //取出要插入的元素
int ageIndex = i-1;
for(; ageIndex >= 0 && a.get(ageIndex) > insert ; ageIndex--) {//这里需把ageIndex >= 0写在前面,不然会报错
a.set(ageIndex + 1, a.get(ageIndex)); //把大于或小于insert的元素向后移
}
a.set(ageIndex+1, insert); //插入ageIndex+1个位置上,因为上面的循环中最后多ageIndex多减了一次
}
}
}
10. 基数排序
基数排序是把元素先按个位数(低位)放进十个桶里(0-9),把具有相同位数的放在同一个桶里,按顺序0~9取出;接着按十位数又放进十个桶里,按顺序取出;接着按百位数来.....以此类推。最终达到整个序列有序。反之,也可以从高位进行。
执行步骤(以从低位开始排序为例):
- 找出原数组里元素的最大值、获取最大值是几位数
- 创建十个桶,并初始化桶
- 遍历原数组元素,从个位数开始找到对应的桶并放进去
- 每当一个位数全部装进桶里之后,按顺序取出里面的元素
- 重复3、4步,直到大于最大值的位数
性质:时间复杂度O(n*k),空间复杂度O(n+k),属于外部排序、稳定排序。其中的k是代表桶的个数。
代码实现:
import java.util.ArrayList;
public class RadioSort {
public static void main(String[] args) {
//这里我用从小到大排序作为例子
int[] a = {5,7,2,8,10,3,6,1,9,4};
rediosort(a);
for(int i=0 ; i < a.length ; i++) {
System.out.print(a[i] + " ");
}
}
public static void rediosort(int[] a) {
if(a == null || a.length < 2)
return;
int max = a[0];
//找出最大值
for(int i = 1 ; i < a.length; i++) {
if(max < a[i]) {
max = a[i];
}
}
//计算最大值是几位数
int num = 1;
while(max / 10 > 0) {
num++;
max = max / 10;
}
//创建10个桶
ArrayList> bucketList = new ArrayList<>();
//初始化桶
for(int i = 0 ; i < 10 ; i++) {
bucketList.add(new ArrayList());
}
//进行排序,从个位数开始排序
for(int i = 1 ; i <= num; i++) {
for(int j = 0 ; j < a.length ; j++) {
//获取每个数第i个位数的数字
int I = (a[j] / (int)Math.pow(10, i-1)) % 10;
//放入对应的桶里
bucketList.get(I).add(a[j]);
}
//合并放回原数组
int index = 0;
for(int j = 0 ; j < 10; j++) {
for(Integer k : bucketList.get(j)) {
a[index++] = k;
}
//放回原数组后把桶里数据清空,因为每排完一个位后还需要用到这10个桶
bucketList.get(j).clear();
}
}
}
}