以下总结了常见的八大排序算法,对于编码比较复杂的堆排序、归并排序和基数排序,则只给出了排序的思想,没有附上代码
八大排序算法的时间复杂度和空间复杂度:
八大排序算法的稳定性总结:
①稳定:冒泡排序、插入排序、归并排序、基数排序
②不稳定:选择排序、快速排序、希尔排序、堆排序
常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)<Ο(2^n) <Ο(n!)
一 冒泡排序
public void bubbleSort(int[] a){
//外层循环:需要n-1趟排序
for(int i=0;i<a.length-1;i++){
/**
*对冒泡排序的优化:设置一个标志,如果在某趟排序中并没有交换数据
*则表示已经是有序的了,不需要进行排序,可以直接退出循环*/
boolean flag=false;//需要注意该标志位应该放置在第一层循环内
//内层循环:每趟排序需要比较的次数
for(int j=0;j<a.length-1-i;j++){
if(a[j]>a[j+1]){
int temp=a[j];
a[j]=a[j+1];
a[j+1]=temp;
flag=true;
}
}
if(!flag){
break;
}
}
}
二 选择排序
public void selectSort(int[] a){
// 外层循环,n-1趟排序
for(int i=0;i<a.length-1;i++){
int min=i;//最小值的下标
for(int j=i+1;j<a.length;j++){
if(a[j]<a[min]){
min=j;
}
}
if(min!=i){
int temp=a[i];
a[i]=a[min];
a[min]=temp;
}
}
}
三 快速排序
public void sort(int[] arr, int left, int right) {
// 递归结束的条件
if (left > right) {
return;
}
// 因为left和right后面还需要用到这两个值,所以不应该用它们来遍历,而是用两个新的变量i,j
int i = left;
int j = right;
// 将数组的第一个值当做基准值,用一个变量存起来
int temp = arr[left];
while (i != j) {
// 从右往左走,找到第一个小于基准值的下标(必须加上i<j的限制,避免数组越界异常)
while (arr[j] >= temp && i < j) {
j--;
}
// 将找到的这个小于基准值的的数赋值给下标为i的位置,并且i++
if (i < j) {
arr[i] = arr[j];
i++;
}
// 从左往右走,找到第一个大于基准值的下标(必须加上i<j的限制,避免数组越界异常)
while (arr[i] <= temp && i < j) {
i++;
}
// 将找到的这个大于基准值的的数赋值给下标为j的位置,并且j--
if (i < j) {
arr[j] = arr[i];
j--;
}
}
// 一趟循环结束后,将保存起来的基准值temp赋给下标为i或者为j的位置
arr[i] = temp;
// 递归对基准值左右两边的数据进行快速排序
sort(arr,left, i-1);
sort(arr,i+1, right);
}
四 插入排序
public void insertSort(int[] a) {
// 总共需要n-1轮排序,因为第一个数是看作有序的
for (int i = 1; i < a.length; i++) {
int insertValue = a[i];// 需要把待插入的数给保存起来,避免在移动的过程中被覆盖
int insertIndex = i - 1;// 待插入数的前一个数的下标
// 待插入数小于前一个数,说明还没找到插入的位置,需要继续向前比较
while (insertIndex >= 0 && insertValue < a[insertIndex]) {
// 前一个数往后移一个位置
a[insertIndex + 1] = a[insertIndex];
insertIndex--;
}
// 退出循环,说明找到待插入位置了
a[insertIndex + 1] = insertValue;
}
}
五 希尔排序
直接插入排序存在的问题是如果待插入的数很小,并且在最后面,那么需要移动的次数很多,导致效率很低;希尔排序是插入排序的改进版,可以解决直接插入排序存在的问题;希尔排序也叫缩小增量排序
排序图解(图片来自尚硅谷):
public static void shellSort(int []a){
for(int gap=a.length/2;gap>0;gap=gap/2){
for(int i=gap;i<a.length;i++){
int insertValue=a[i];//需要把待插入的数给保存起来
int insertIndex=i-gap;//待插入数的前一个数的下标
//待插入数小于前一个数,说明还没找到插入的位置,需要继续向前比较
while(insertIndex>=0&&insertValue<a[insertIndex]){
//前一个数往后移一个位置
a[insertIndex+gap]=a[insertIndex];
insertIndex=insertIndex-gap;
}
//退出循环,说明找到待插入位置了
a[insertIndex+gap]=insertValue;
}
}
}
六 堆排序
堆排序是利用堆这种数据结构而设计的一种排序算法;堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆, 注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
使用堆排序进行升序排序的基本思想是:
① 将待排序序列构造成一个大顶堆(从最后一个非叶子节点进行调整,如果该节点的值小于左右子节点任一方的值,那么将该节点与比他大的那个节点进行交换,如果该节点的值都比左右子节点的值小,那么将该节点与值较大的那个子节点进行交换;再依次对倒数第二个非叶子节点执行同样的操作,直至根节点,如果在交换的过程中又影响了刚刚交换过的节点,那么被影响的节点需要再次进行调整)
② 此时,整个序列的最大值就是堆顶的根节点,将其与末尾元素进行交换,此时末尾就为最大值
③ 然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了
注意:升序排序使用大顶堆,降序排序使用小顶堆;这句话的意思是对于升序排序,在整个排序过程中,我们都是构建大顶堆,然后每次将大顶堆的根节点与最末尾节点进行交换,最终的结果就变成一颗小顶堆了;而对于降序排序,在整个排序过程中,我们都是构建小顶堆,然后每次将小顶堆的根节点与最末尾节点进行交换,最终的结果就变成一颗大顶堆了
七 归并排序
归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略(分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)
排序思路图解(图片来自尚硅谷):
归并排序的基本思想是——合并两个有序的子序列
例如:给定一个数组(3,8,2,9,5,1,4,6),使用归并排序,每一轮排序的结果为:
第一轮:(3,8,2,9,1,5,4,6)
第二轮:(2,3,8,9,1,4,5,6)
第三轮:(1,2,3,4,5,6,8,9)
public void mergeSort(int []nums){
if (nums == null || nums.length == 0) {
return new int[0];
}
divide(nums, 0, nums.length - 1);
}
/**
* 分治思想的分:也就是将一个大的数组每次拆分成两部分,直到拆分成每部分只有一个元素为止,然后再依次进行合并
* @param nums
* @param l
* @param r
*/
public void divide(int[] nums, int l, int r) {
// 递归出口
if (l >= r) {
return;
}
int mid = (l + r) / 2;
divide(nums, l, mid);
divide(nums,mid + 1, r);
merge(nums, l, mid, r);
}
/**
* 分治算法思想的治:合并两个有序数组,在本题中是将数组从下标mid切分成两部分,并合并[l,mid]、[mid+1,r]这两部分
* @param nums
* @param l
* @param mid
* @param r
*/
public void merge(int[] nums, int l, int mid, int r) {
int[] temp = new int[r - l + 1];
int i = l;
int j = mid + 1;
int cur = 0;
while (i <= mid && j <= r) {
if (nums[i] < nums[j]) {
temp[cur] = nums[i];
i++;
} else {
temp[cur] = nums[j];
j++;
}
cur++;
}
while (i <= mid) {
temp[cur] = nums[i];
i++;
cur++;
}
while (j <= r) {
temp[cur] = nums[j];
j++;
cur++;
}
// 将排好序的临时数组重新赋值给原数组,注意是下标从l-r
for (int x = 0; x < temp.length; x++) {
nums[l++] = temp[x];
}
}
八 基数排序
基数排序是桶排序的扩展,基数排序需要进行几轮排序取决于待排序的数字中最大的那个数是几位数,如果是3位,则需要进行三轮排序基数排序,以三轮排序为例,基数排序的步骤如下:
- 创建十个桶,下标分别为0-9
- 先处理个位的,将数组中每个数根据个位的值放到对应下标的桶,例如23,应该放到下标为3的桶
- 当所有的数都放到对应的桶之后,将桶中的数据按顺序输出,即为第一轮排序的结果
- 再处理十位的,将数组中每个数根据十位的值依次放到对应下标的桶(要用第一轮排序的结果),例如23,应该放到下标为2的桶
- 当所有的数都放到对应的桶之后,将桶中的数据按顺序输出,即为第二轮排序的结果
- 最后处理百位的,将数组中每个数根据百位的值依次放到对应下标的桶(要用第二轮排序的结果),例如23,应该放到下标为0的桶
- 当所有的数都放到对应的桶之后,将桶中的数据按顺序输出,即为第三轮排序的结果
例如:给定一个数组(17,9,243,14,47,8,260),使用基数排序,每一轮排序的结果为:
第一轮:(260,243,14,17,47,8,9)
第二轮:(8,9,14,17,243,47,260)
第三轮:(8,9,14,17,47,243,260)