别再说你不懂排序算法了,汇总篇

531 阅读9分钟

这是我参与8月更文挑战的第31天,活动详情查看:8月更文挑战

一、冒泡排序

1.1 概述

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

1.2 算法描述

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  • 针对所有的元素重复以上的步骤,除了最后一个;
  • 重复步骤1~3,直到排序完成。

1.3 算法分析

  • 时间复杂度:T(n) = O(n2) 最好情况O(n) 最坏情况O(n2)
  • 空间复杂度:O(1)

稳定性:稳定

举例

对 {6,9,1,4,5,8,7,0,2,3}进行排序

第一趟:先比较6和9,9更大,则不移动,在比较9和1,9更大,9和1交换位置,到最好后得到第一趟排序后的数列:

 {6,1,4,5,8,7,0,2,3}

第二趟:继续上述操作,得到

 {1,4,5,6,7,0,2,8,9}

可以看出,每经过一次排序后,都会有一个最终确认的值在最后一个。

1.4 代码实现

 public class bubbleSort {
     public static void sort(int[] arr){
         if(arr==null||arr.length==1) return;
         //循环的次数
         for(int i=0;i<arr.length-1;i++){
          //j表示要比较元素的第一个.arr.length-1-i是无序数组的第一个
             boolean isSorted=true;//假设已经有序了
             for(int j=0;j<arr.length-1-i;j++){
                 if(arr[j]>arr[j+1]){
                     int temp=arr[j];
                     arr[j]=arr[j+1];
                     arr[j+1]=temp;
                     isSorted=false;
                 }
             }
             if(isSorted==true) return;
             print(arr);
         }
     }
     public static void print(int[] arr){
         if(arr==null) return;
         for(int i:arr){
             System.out.print(i+" ");
         }
         System.out.println();
     }
     public static void main(String[] args) {
         int[]arr={6,9,1,4,5,8,7,0,2,3};
         System.out.println("排序前:");
         print(arr);
         System.out.println("排序后:");
         sort(arr);
         print(arr);
     }
 }

排序的过程及结果如下所示:

img

二、选择排序

2.1 概述

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。但是不稳定的地方在于它可能会打乱相同数字的顺序。

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

2.2 算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  • 将第一个值看成最小值
  • 然后和后续的比较找出最小值和下标
  • 交换本次遍历的起始值和最小值

每次遍历的时候,将前面找出的最小值,看成一个有序的列表,后面的看成无序的列表,然后每次遍历无序列表找出最小值。

2.3 算法分析

  • 时间复杂度:平均情况 O(n^2) 最好情况 O(n^2) 最坏情况 O(n^2)
  • 空间复杂度: O(1)
  • 稳定性:不稳定

总结:虽然符合人的思想,但是不稳定和时间复杂度差

2.4 代码实现

 public class SelectionSort {
     public static void sort(int[]arr){
         if(arr==null||arr.length==1) return;
         //循环的次数
         for(int i=0;i<arr.length-1;i++){
             //i是无序区的第一个元素
             int minIndex=i;
             for (int j = i+1; j <arr.length ; j++) {
                 if (arr[j] < arr[minIndex]) {
                     minIndex = j;//找到最小元素的索引值,
                 }
             }//交换元素
                 int temp=arr[i];
                 arr[i]=arr[minIndex];
                 arr[minIndex]=temp;
                 print(arr);
         }
     }
     public static void print(int[] arr){
         if(arr==null) return;
         for(int i:arr){
             System.out.print(i+" ");
         }
         System.out.println();
     }
     public static void main(String[] args) {
         int[]arr={6,9,1,4,5,8,7,0,2,3};
         System.out.println("排序前:");
         print(arr);
         sort(arr);
         System.out.println("排序后:");
         print(arr);
     }
 }

三、插入排序

3.1 概述

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

3.2 算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  • 默认从第二个数据开始比较
  • 如果第二个数据比第一个小,则交换。然后在用第三个数据比较,如果比前面小,则插入。否则退出循环。

3.3 算法分析

  • 时间复杂度:平均情况:O(n2) 最好情况 O(n) 最坏情况 O(n^2)
  • 空间复杂度: O(1)
  • 稳定性:稳定

3.4 代码实现

 public class InsertSort {
     public static void sort(int []arr){
         if(arr==null||arr.length<=1)return;
         for(int i=0;i<arr.length-1;i++){
             //i代表有序区的最后一个元素
             int value=arr[i+1];//value代表要插入的元素
             int j=i;
             for(;j>=0&&arr[j]>value;j--){
                 //如果有序队列的最后一个元素比value大,则向后挪动,覆盖后面的元素
                 arr[j+1]=arr[j];
             }
             //这里有两种情况,如果遇到比value小的时候,退出循环的话,则在该数后面插入vaule,
             // 如果因为小于0退出循环的话,此时i=-1,所以加1表示在第一个位置插入value。
             arr[j+1]=value;
             print(arr);//打印数组的排序过程
         }
     }
     
     public static void print(int[] arr){
         if(arr==null) return;
         for(int i:arr){
             System.out.print(i+" ");
         }
         System.out.println();
     }
     public static void main(String[] args) {
         int[]arr={6,9,1,4,5,8,7,0,2,3};
         System.out.println("排序前:");
         print(arr);
         System.out.println("排序后:");
         sort(arr);
         print(arr);
     }
 }

四、希尔排序

4.1 概述

希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。

希尔排序是把元素按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个数组被分成一组,算法便终止。

4.2 算法描述

我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;

按增量序列个数k,对序列进行k 趟排序;

每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

4.3 算法分析

  • 时间复杂度:O(nlogn) {性能略优于O(n^2)但又不及O(nlogn)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

4.4 代码实现

 public class ShellSort {
     public static void sort(int[]arr){
         if(arr==null||arr.length<=1) return;//如果数组元素为空或只有一个,退出
         int gap=arr.length/2;//偏移量
         //当偏移量为0时,退出循环
         while (gap>=1){
             //每组进行直接插入排序
         for(int i=gap;i<arr.length;i++){
             //i代表要插入的元素,
             int value=arr[i];
             int j= i-gap;      //i的上一个元素
             for ( ; j>=0&&arr[j]>value ; j-=gap) {
                 arr[j+gap]=arr[j];
             }
             arr[j+gap]=value;
         }
         print(arr);
         gap=gap/2;
         }
     }
  
     public static void print(int[] arr){
         if(arr==null) return;
         for(int i:arr){
             System.out.print(i+" ");
         }
         System.out.println();
     }
     public static void main(String[] args) {
         int[]arr={6,9,1,4,5,8,7,0,2,3};
         System.out.println("排序前:");
         print(arr);
         System.out.println("排序后:");
         sort(arr);
         print(arr);
  
     }
 }

五、归并排序

5.1 概述

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。

归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

5.2 算法描述

  • 把长度为n的输入序列分成两个长度为n/2的子序列;
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列。

5.3 算法分析

  • 最佳情况:T(n) = O(nlogn)
  • 空间复杂度: O(n)
  • 稳定性: 稳定

5.4 代码实现

 public class MergeSort {
     public static void sort(int[] arr) {
         if (arr == null || arr.length <= 1) return;
         int[] temp = new int[arr.length];
         mergeSort(arr, temp, 0, arr.length - 1);
     }
  
     private static void mergeSort(int[] arr, int[] temp, int low, int high) {
         if (low >= high) return;
         // int mid = (low + high) / 2;
         int mid = low + ((high - low) >> 1);
         // 对左边排序
         mergeSort(arr, temp, low, mid);
         // 对右边排序
         mergeSort(arr, temp,mid + 1, high);
         // 归并两个有序的子序列
         merge(arr, temp, low, mid, high);
         print(arr);
     }
  
 private static void merge(int[] arr, int[] temp, int low, int mid, int high) {
         // int[] temp = new int[high - low + 1];
         int left = low;
         int right = mid + 1;
         int index = low;
         while (left <= mid && right <= high) {
             if (arr[left] <= arr[right]) {
                 temp[index++] = arr[left++];
             } else {
                 temp[index++] = arr[right++];
             }
         }
         while (left <= mid) {
             temp[index++] = arr[left++];
         }
  
         while (right <= high) {
             temp[index++] = arr[right++];
         }
         // 重新赋值给arr对应的区间
         for (int i = low; i <= high ; i++) {
             arr[i] = temp[i];
         }
     }
  
     public static void print(int[] arr) {
         if (arr == null) return ;
         for(int i : arr) {
             System.out.print(i + " ");
         }
         System.out.println();
     }
     public static void main(String[] args) {
         int[] arr = {6, 9, 1, 4, 5, 8, 7, 0, 2, 3};
         System.out.print("排序前:");
         print(arr);
         sort(arr);
         System.out.print("排序后:");
         print(arr);
     }
  }

六、快速排序详解

6.1 快速排序的基本思想

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

分治法: 快速排序也属于交换排序,每一轮挑选一个基准元素,并让其他比它大的元素移动到数列的一边,比它小的元素移动到数列的另一边,从而把数列拆成了两个部分。

  • a、确认列表第一个数据为中间值,第一个值看成空缺;
  • b、然后在剩下的队列中,看成有左右两个指针(高低)
  • c、开始高指针向左移动,如果遇到小于中间值的数据,则将这个数据赋值到低指针空缺,并且将高指针的数据看成空缺位;然后向右移动下低指针,并且切换低指针移动;
  • d、当低指针移动到大于中间值的时候,赋值到高指针空缺的地方,然后高指针向左移动
  • e、直到高指针和低指针相等时退出,并且将中间值赋值给对应指针位置
  • f、然后将中间值的左右看成行的列表,再进行快速排序操作。

image.png

6.2 算法描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • 从数列中挑出一个元素,称为 “基准”(pivot);(基准值特别影响性能)我们可以随机选择出一个基准元素与首字母交换
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

6.3 算法分析

  • 时间复杂度:平均情况:T(n) = O(nlogn) 

    • 在分治法的思想下,原数列在每一轮都被拆分成两部分,每一部分在下一轮又分别拆分成两部分,直到不可再分为止。每一轮的比较和交换,需要把数组全部元素都遍历一遍,时间复杂度是O(n),平均情况下遍历logn轮,因此时间复杂度是O(nlong)
  • 空间复杂度: O(logn) (栈占用的空间)

  • 稳定性:不稳定

6.4 代码实现

6.4.1 双边交换法

选定left指针和right指针分别指向队头和队尾,注意点:从right指针开始,让指针所指向的元素和基准元素做比较,如果大于或等于pivot,则指针向左移动,如果小于pivot,则right指针停止移动,切换到left指针移动,如果left指针大于pivot,则交换left和right的位置,否则,left指针向右移动。

代码:

 public class QuickSort {
     public static void sort(int[] arr) {
         if (arr == null || arr.length <= 1) return;
         // 包左不包右
         quickSort(arr, 0, arr.length);
     }
  
     private static void quickSort(int[] arr, int low, int high) {
         if (high - low <= 1) return;
         // 快速排序的主要操作
         int partition = partition(arr, low, high);//得到基准元素位置
         //根据基准元素,分成两部分递归
         quickSort(arr, low, partition);
         quickSort(arr, partition + 1, high);
         // print(arr);
     }
 //双边循环法
     private static int partition(int[] arr, int low, int high) {
         int pivot = arr[low];//取第一个位置作为基准元素
         int left = low;
         int right = high-1;
         while (left < right) {
             while (left < right && arr[right] >= pivot) {
                 right--;
             }
             arr[left] = arr[right];
             while (left < right && arr[left] <= pivot) {
                 left++;
             }
             arr[right] = arr[left];
         }
         //privot赋值到指针重合点
         arr[left] = pivot;
         print(arr);
         return left;//返回指针重合点处
     }
  
     public static void print(int[] arr) {
         if (arr == null) return ;
         for(int i : arr) {
             System.out.print(i + " ");
         }
         System.out.println();
     }
     public static void main(String[] args) {
         int[] arr = {6, 9, 1, 4, 5, 8,8 ,7, 0, 2, 3,10};
         System.out.print("排序前:");
         print(arr);
         sort(arr);
         System.out.print("排序后:");
         print(arr);
     }
 }
6.4.2 单边交换法

与双边交换法比较,虽然双边更直观,但是代码实现比较繁琐,而单边法则简单多了,只从数组的一边进行遍历和交换。

思路:首先选定基准元素pivot。同时,设置一个mark指针指向数列起始位置,这个mark指针代表小于基准元素的区域边界。

img

代码:

 public class QuickSort {
     public static void sort(int[] arr) {
         if (arr == null || arr.length <= 1) return;
         // 包左不包右
         quickSort(arr, 0, arr.length);
     }
  
     private static void quickSort(int[] arr, int low, int high) {
         if (high - low <= 1) return;
         // 快速排序的主要操作
         int partition = partition(arr, low, high);//得到基准元素位置
         //根据基准元素,分成两部分递归
         quickSort(arr, low, partition);
         quickSort(arr, partition + 1, high);
         // print(arr);
     }
  
     private static int partition(int[] arr, int low, int high) {
         int pivot = arr[low];//取第一个位置作为基准元素
         int mark=low;
         for(int i=low+1;i<high;i++){
             if(arr[i]<pivot){
                 //第一步:mark右移
                 mark++;
                 //第二步:交换元素
                 int temp=arr[mark];
                 arr[mark]=arr[i];
                 arr[i]=temp;
             }
         }
         //mark处与第一个元素交换位置
         arr[low]=arr[mark];
         arr[mark]=pivot;
         return mark;
     }
     //单边排序法
  
     public static void print(int[] arr) {
         if (arr == null) return ;
         for(int i : arr) {
             System.out.print(i + " ");
         }
         System.out.println();
     }
     public static void main(String[] args) {
         int[] arr = {6, 9, 1, 4, 5, 8,8 ,7, 0, 2, 3,10};
         System.out.print("排序前:");
         print(arr);
         sort(arr);
         System.out.print("排序后:");
         print(arr);
     }
 }

总结

这些是之前学算法的时候做的笔记,在此将这样的排序算法进行总结,利于算法刷题和学习回顾。

本文参考了 《漫画算法:小灰的算法之旅》,其他的都比较常见。