💖💖 大家好呀,欢迎来看我的文章😍,不求别的,希望各位大哥哥,小姐姐们能够给我一点点支持,让我这个萌新有前进的动力,太感谢啦,爱你们😙,哈哈哈💖
八大排序算法总结
一. 前言
我们通常所说的八大排序算法指的是:冒泡排序,插入排序,选择排序,希尔排序,快速排序,基数排序(桶排序),堆排序,归并排序
这么多的排序算法,究竟学习哪一种才是最好的呢,我们说,算法没有好坏之分,只有什么样的算法适合用在什么样的地方,如果你没有将一种算法用在它擅长的地方,那即使它是最好的算法也不能凸显出它的优势,所以这几种算法是我们每一个学习编程的同学都应该掌握的,至于哪一种算法适合用在什么地方,在下面的文章中我将会详细介绍。
二. 如何评价一个算法的好坏
评价一个算法的好坏主要从:时间复杂度,空间复杂度,正确性,可读性,健壮性来评价
2.1 时间负复杂度
时间复杂的是评价一个算法好坏最主要的因素,在计算机处理大量计算任务的时候,代码的执行效率成为了评判一个算法是否是一个优秀算法的关键,我们不可能花费大量的时间来等一个算法去执行吧,就像你买计算机不可能买那种运算素的特别慢的那种吧。
2.2 空间复杂度
算法的空间复杂度是指算法需要消耗的内存空间。其计算和表示方法与时间复杂度类似,一般都用复杂度的渐近性来表示。同时间复杂度相比,空间复杂度的分析要简单得多,利用空间换时间的操作在大多数优秀算法都有存在,这种方法可以在利用部分内存空间的时候降低程序运行的时间(基数排序),不过我们一般可以将空间的花费忽略不计,因为在当今这个年代应该没有多少人买不起内存了吧,就连买一个手机都要买128gb的更别说电脑了。
2.3 正确性
算法的正确性是评价一个算法优劣的最重要的标准,不正确的算法何谈其他
2.4 可读性
如果你对一些代码比较复杂的算法感到反感的话也可以将代码复杂的放在评价一个算法的好坏上,不过我们并不推荐,因为程序主要是给计算机看的,不过依然要在保证其他项的同时保证程序的可读性。
2.5 健壮性
健壮性是指一个算法对不合理数据输入的反应能力和处理能力,也称为容错性,一个好的算法,他也将会有良好的健壮性。
2.6 八大排序算法的比较
三. 排序的基本概念
3.1 内部排序和外部排序
-
内部排序
指在排序期间将待排序的数据都读入内存当中进行排序的一种方法,不和外部储存进行数据的交换
-
外部排序
和外部排序差不多,只不过在排序期间和外存之间会进行数据的交换,通常是排序的数据量特别大的 时候,通过对一部分数据先进行排序,完了之后存储在外存中,待全部数据都进行排序之后再次将所有的数据都读入到内存当中
3.2 什么是稳定排序
-
稳定排序:假定待排序的数组中存在A和B这两个数据,并且A和B都相等,假设等于0,并且A在B的前面,如果排序之后A任然在B的前面A和B未因为排序而交换位置(A和B交换位置是可能发生的,因为A和B相等)说明次排序是稳定排序,反之为不稳定排序
-
不稳定排序:快速排序、希尔排序、堆排序
-
稳定排序:冒泡排序,直接插入排序、归并排序、基数排序
-
不确定:简单选择排序(插入版稳定,交换版不稳定)
四. 八大排序算法详解
4.1 冒泡排序
-
基本介绍:冒泡排序是排序算法当中最为基本的一种排序方式,为什么会叫冒泡排序呢?因为数组中的每一个数据都能够想泡泡一样逐步找到自己对应的位置(逐渐变大,或变小),并且冒泡排序每一轮只能确定一个数的位置
-
基本思想:每一趟只能确定将一个数归位。即第一趟只能确定将末位上的数归位,第二趟只能将倒数第 2 位上的数归位,依次类推下去。如果有 n 个数进行排序,只需将 n-1 个数归位,也就是要进行 n-1 趟操作。而 “每一趟 ” 都需要从第一位开始进行相邻的两个数的比较,将较大的数放后面,比较完毕之后向后挪一位继续比较下面两个相邻的两个数大小关系,重复此步骤,直到最后一个还没归位的数。
-
实现图解:
-
代码实现:
public void bubbleSort(int[] array){
for(int i=0;i<array.length-1;i++){//控制比较轮次,一共 n-1 趟
for(int j=0;j<array.length-1-i;j++){//控制两个挨着的元素进行比较
if(array[j] > array[j+1]){
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
}
- 算法优化:上面的代码其实还可以被优化,因为如果数据本身就不是全部无序,那么我们就可以只进行无序的排列,有序的数据就可以不用进行排列,有序的排列了就是在做无用功
public static int[] bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return arr;
}
for (int i = 0; i < arr.length - 1; i++) {
boolean isSorted = true;//有序标记,每一轮的初始是true
for (int j = 0; j < arr.length -i - 1; j++) {
if (arr[j + 1] < arr[j]) {
isSorted = false;//有元素交换,所以不是有序,标记变为false
int t = arr[j];
arr[j] = arr[j+1];
arr[j+1] = t;
}
}
//一趟下来是否发生位置交换,如果没有交换直接跳出大循环
if(isSorted )
break;
}
return arr;
}
-
时间复杂的分析
很显然,这就是一个等差数列,那么根据时间复杂度的计算方法就可以计算出算法所用的时间为:O(n^2)
4.2 插入排序
- 基本介绍:每一步将一个待排序的数据插入到前面已经排好序的有序序列中,直到插完所有元素为止。
- 基本思想:第一次排序从数组中的第二个数开始,依次和前面的有序数组(已经排列好)进行比较,直到找到一个能插入这个数的位置,对于一个无序数组array{4,5,7,3,2,8},而言,进行第一次排序的时候我们知道的就是第一个元素array[0] = 4是有序的,那进行第一次排序比较的时候就应该用第二个元素array[1] = 5和前面有序的数组进行比较,然而第一次比较之后可以发现,数组中的数并不需要交换位置,进行第二轮比较也不用交换位置,直到第三次交换之后才发现3比前面的数据小 ,这显然是不符合的,就应该将3交换位置,从上面的理论可以看出3应该排在第一位,进行三轮比较之后,得到的结果应该是这样的array{3,4,5,7,2,8},按照这个比较方法依次进行比较最终得出结果,如图所示:
- 实现图解:
- 代码实现:
public class InsertSortTest {
public static void InsertSort(int[] source) {
int i, j;
int insertNode;// 要插入的数据
// 从数组的第二个元素开始循环将数组中的元素插入
for (i = 1; i < source.length; i++) {
// 设置数组中的第2个元素为第一次循环要插入的数据
insertNode = source[i];
j = i - 1;
// 如果要插入的元素小于第j个元素,就将第j个元素向后移
while ((j >= 0) && insertNode < source[j]) {
source[j + 1] = source[j];
j--;
}
// 直到要插入的元素不小于第j个元素,将insertNote插入到数组中
source[j + 1] = insertNode;
System.out.print("第" + i + "趟排序:");
printArray(source);
}
}
private static void printArray(int[] source) {
for (int i = 0; i < source.length; i++) {
System.out.print("\t" + source[i]);
}
System.out.println();
}
public static void main(String[] args) {
int source[] = new int[] { 53, 27, 36, 15, 69, 42 };
System.out.print("初始关键字:");
printArray(source);
System.out.println("");
InsertSort(source);
System.out.print("\n\n排序后结果:");
printArray(source);
}
}
- 时间复杂度分析
在排序过程中将后面的元素与前面的元素进行比较,总共进行了n-1次,每一次又分为两步
- 和前一元素进行比较
- 将前一元素进行往后挪
通过分析可知,该算法的时间复杂度为:O(n^2)
4.3 选择排序
- 基本介绍:每一趟从待排序的数据元素中选择最小(或最大)的一个元素作为首元素,直到所有元素排完为止。
- 基本思路:每一趟通过不断地比较交换来使得首元素为当前最小,交换是一个比较耗时间的操作,我们可以通过设置一个值来记录较小元素的下标,循环结束后存储的就是当前最小元素的下标,这时再进行交换就可以了。对于数组一个无序数组{4,6,8,5,9}来说,我们以min来记录较小元素的小标,i和j结合来遍历数组,初始的时候min和i都指向数组的首元素,j指向下一个元素,j开始从右向左进行遍历数组元素,若有元素比min元素更小则进行交换,然后min为更小元素的小标,i再向右走,这样循环到i走到最后一个元素就完成了排序,过程如下图所示:
- 实现图解:
- 代码实现
public static void selectionSort(int[] nums) {
if (nums == null || nums.length < 2) {
return;
}
for(int i = 0; i < nums.length - 1; i++) {
for(int j = i + 1; j < nums.length; j++) {
if(nums[i] > nums[j]) {
swap(nums, i, j);
}
}
}
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
- 算法优化
上面的算法的缺点:在每趟比较过程中,只要发现比第一位要小的数据就交换他们,其实这是没必要 进行的操作,白白增加了交换的次数,降低了算法的效率。
优化:其实我们只要找到每一趟中最小的那个数据将它和第一位数进行交换即可,可以定义一个新的索引,指向最小的那个数。
public static void selectionSort(int[] nums) {
if (nums == null || nums.length < 2) {
return;
}
for(int i = 0; i < nums.length - 1; i++) {
int minIndex = i;
for(int j = i + 1; j < nums.length; j++) {
if(nums[i] > nums[j]) {
minIndex = nums[j] < nums[minIndex] ? j : minIndex;
}
}
swap(nums, i, minIndex);
}
}
- 时间复杂度分析
因为也是进行了n-1次的比较,所以经过分析可以直到次算法的时间复杂度为:O(n^2)。
4.4 希尔排序
- 基本介绍:希尔排序实际上是对插入排序进行的一次优化假设arr = {2,3,4,5,6,1} 这时需要插入的数 1(最小), 这样的过程是:
{2,3,4,5,6,6}
{2,3,4,5,5,6}
{2,3,4,4,5,6}
{2,3,3,4,5,6}
{2,2,3,4,5,6}
{1,2,3,4,5,6}
可以看出当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响。所以就发明了希尔排序。 - 基本思想:希尔排序需要定义一个增量,这里选择增量为gap=length/2,缩小增量以gap=gap/2的方式,这个增量可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列,这个增量是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。对于一个无序序列{8,9,1,7,2,3,5,4,6,0}来说,我们初始增量为gap=length/2=5,所以这个序列要被分为5组,分别是{8,3},{9,5},{1,4},{7,6},{2,0},对这5组分别进行直接插入排序,则小的元素就被调换到了前面,然后再缩小增量gap=gap/2=2。
- 实现图解:
第一次分为8列进行比较
第二次分为4列进行比较
第三次分为2列进行比较
第四次分为1列进行比较
通过这几次的比较最终可以得到有序数组
- 代码实现
package sortdemo;
import java.util.Arrays;
/**
* Created by chengxiao on 2016/11/24.
*/
public class ShellSort {
public static void main(String []args){
int []arr ={1,4,2,7,9,8,3,6};
sort(arr);
System.out.println(Arrays.toString(arr));
int []arr1 ={1,4,2,7,9,8,3,6};
sort1(arr1);
System.out.println(Arrays.toString(arr1));
}
/**
* 希尔排序 针对有序序列在插入时采用交换法
* @param arr
*/
public static void sort(int []arr){
//增量gap,并逐步缩小增量
for(int gap=arr.length/2;gap>0;gap/=2){
//从第gap个元素,逐个对其所在组进行直接插入排序操作
for(int i=gap;i<arr.length;i++){
int j = i;
while(j-gap>=0 && arr[j]<arr[j-gap]){
//插入排序采用交换法
swap(arr,j,j-gap);
j-=gap;
}
}
}
}
/**
* 希尔排序 针对有序序列在插入时采用移动法。
* @param arr
*/
public static void sort1(int []arr){
//增量gap,并逐步缩小增量
for(int gap=arr.length/2;gap>0;gap/=2){
//从第gap个元素,逐个对其所在组进行直接插入排序操作
for(int i=gap;i<arr.length;i++){
int j = i;
int temp = arr[j];
if(arr[j]<arr[j-gap]){
while(j-gap>=0 && temp<arr[j-gap]){
//移动法
arr[j] = arr[j-gap];
j-=gap;
}
arr[j] = temp;
}
}
}
}
/**
* 交换数组元素
* @param arr
* @param a
* @param b
*/
public static void swap(int []arr,int a,int b){
arr[a] = arr[a]+arr[b];
arr[b] = arr[a]-arr[b];
arr[a] = arr[a]-arr[b];
}
}
- 时间复杂度分析:
通过分析可以得出时间复杂度为:O(n^2)。
4.5 快速排序
-
基本介绍:
快速排序是(Quick sort)是对冒泡排序的一种改进,是非常重要且应用比较广泛的一种高效率排序算法,之所以称为快速排序就是因为快了嘛
-
基本思路:
其实实现的方法类似于二分查找,都是二分,不过,快速排序是将每一次二分之后的数据进行比较,使得一边的数据比另一边的数据小,之后再一次进行二分,使用递归的方法,进行持续的二分比较,最后得到排序之后的数据。进行二分的时候首先需要设置一个值,也叫中间值,然后利用这个中间值进行二分,递归
-
实现图解
-
代码实现:
public class QuickSort {
public static void quickSort(int [] arr,int left,int right) {
int pivot=0;
if(left<right) {
pivot=partition(arr,left,right);
quickSort(arr,left,pivot-1);
quickSort(arr,pivot+1,right);
}
}
private static int partition(int[] arr,int left,int right) {
int key=arr[left];
while(left<right) {
while(left<right && arr[right]>=key) {
right--;
}
arr[left]=arr[right];
while(left<right && arr[left]<=key) {
left++;
}
arr[right]=arr[left];
}
arr[left]=key;
return left;
}
public static void main(String[] args) {
int arr[]= {65,58,95,10,57,62,13,106,78,23,85};
System.out.println("排序前:"+Arrays.toString(arr));
quickSort(arr,0,arr.length-1);
System.out.println("排序后:"+Arrays.toString(arr));
}
}
-
时间复杂度分析:
快速排序在每次分割的过程中,需要 1 个空间存储基准值。而快速排序的大概需要 logN次的分割处理,所以占用空间也是 logN 个,经过分析可知,算法的时间复杂度为:O(log2^n)。
4.6 基数排序
-
基本介绍:
基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。具体做法是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
-
基本思路:
创建一个二位数组来存储每一遍历之后的数据,这个二维数组的行必须是10,列的话是数据的大小(待排序数组的长度),然后分别进行个为十位…的比较,将比较的值放正桶里面,每一次比较之后更新原始数组,然后再一次进行比较,最后代所有位都比较完毕之后得到的数据就是排序好的数据。
-
实现图解
- 代码实现
package com.radixSort; import java.util.Arrays; /** * @author Mr.sun */ public class radixSort { public static void main(String[] args) { //创建一个待排序数组 int[] array ={548,123,678,3,10,78,}; //这只是第一轮循环,循环的次数受到待排序中最高位数的影响,如果一轮排序结束之后发现所有数据mod上一个10之后的数都是0 //说明排序结束,还要一种方法,每一次mod数的时候检查是否为0,如果不是0就在变量上加一,最后如果变量为1则说明循环还剩余一次 //最后循环一次之后就可以结束这个程序 int[] radixSortTest = radixSortTest(array); System.out.println(Arrays.toString(radixSortTest)); } public static int[] radixSortTest(int[] array){ //创建一个二维数组来表示桶 int length = array.length; int[][] buckets= new int [10][length]; int[] arrayElementsCount = new int[10]; int temp = 0; int index = 0; int k = 0; //遍历每一个待排序数组里面的数据将数据利用循环将第一轮的数据存储在桶里面 do{ temp = 0; for(int i = 0;i<length;i++){ int digit = (array[i]/(int)(Math.pow(10,k)))%10; if(array[i]/(int)(Math.pow(10,k))!=0){ temp++; } //将数据存储到桶里面 buckets[digit][arrayElementsCount[digit]] = array[i]; arrayElementsCount[digit]++; } //将数据从桶里面取出来 for(int j = 0;j<10;j++){ if(arrayElementsCount[j] !=0){ for(int l = 0;l<arrayElementsCount[j];l++){ array[index++] = buckets[j][l]; } arrayElementsCount[j] =0; } } index = 0; k++; }while(temp-1>=0); return array; } }
- 时间复杂度分析
这个时间复杂度跟数的位数有关。这个数位可以理解为常数级。但是每一次又需要对数进行排序。而基数排序通常搭配的是计数排序。而计数排序的时间复杂度是 O(n)的线性的。忽略常数项,因此基数排序的时间复杂度是 O(n)的
4.7 堆排序
-
基本介绍:
要理解堆排序首先就要理解堆,堆不是一种算法,而是一种数据结构,一种叫做完全二叉树的数据结构。虽然我之前学习过堆,但是对堆的理解还不够透彻,这里就先不向大家介绍堆了,到后面我深入学习有关堆的知识之后再向大家介绍堆相关的内容。
4.8 归并排序
-
基本介绍:
归并排序,顾名思义就是归,但在归的前提是要分,这里主要是利用到分治的思想,将大问题都按照一定的规则分为若干小问题,然后再解决这些小问题,最后大问题得到求解
-
基本实现思路:把数组从中间划分成两个子数组;一直递归地把子数组划分成更小的子数组,直到子数组里面只有一个元素,依次按照递归的返回顺序,不断地合并排好序的子数组,直到最后把整个数组的顺序排好。看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现的详细步骤
-
实现图解:
- 代码实现:
package sortdemo;
import java.util.Arrays;
/**
* Created by chengxiao on 2016/12/8.
*/
public class MergeSort {
public static void main(String []args){
int []arr = {9,8,7,6,5,4,3,2,1};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int []arr){
int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
sort(arr,0,arr.length-1,temp);
}
private static void sort(int[] arr,int left,int right,int []temp){
if(left<right){
int mid = (left+right)/2;
sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
}
}
private static void merge(int[] arr,int left,int mid,int right,int[] temp){
int i = left;//左序列指针
int j = mid+1;//右序列指针
int t = 0;//临时数组指针
while (i<=mid && j<=right){
if(arr[i]<=arr[j]){
temp[t++] = arr[i++];
}else {
temp[t++] = arr[j++];
}
}
while(i<=mid){//将左边剩余元素填充进temp中
temp[t++] = arr[i++];
}
while(j<=right){//将右序列剩余元素填充进temp中
temp[t++] = arr[j++];
}
t = 0;
//将temp中的元素全部拷贝到原数组中
while(left <= right){
arr[left++] = temp[t++];
}
}
}
-
时间复杂度分析
归并排序的时间复杂度是O(NlgN)。
假设被排序的数列中有N个数。遍历一趟的时间复杂度是O(N),需要遍历多少次呢?
归并排序的形式就是一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的可以得出它的时间复杂度是O(NlgN)。
这就是我目前能总结的关于排序算法的知识啦,希望各位大佬能给我指出问题,谢谢啦,我会继续努力的。
部分图片来源于网络,侵权删。