八大排序算法:
- 冒泡排序
- 选择排序
- 插入排序
- 希尔排序
- 归并排序
- 快速排序
- 堆排序
- 基数排序
0、排序算法说明
- 0.1 排序的定义
对一序列对象根据某个关键字进行排序。- 0.2 术语说明
- 稳定 :如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
- 不稳定 :如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
- 内排序 :所有排序操作都在内存中完成;
- 外排序 :由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
- 时间复杂度 : 一个算法执行所耗费的时间。
- 空间复杂度 :运行完一个程序所需内存的大小。
- 0.3 算法总结
- 图片名词解释:
- n: 数据规模
- k: “桶”的个数
- In-place: 占用常数内存,不占用额外内存
- Out-place: 占用额外内存
- 0.5 算法分类
- 0.6 比较和非比较的区别
常见的快速排序、归并排序、堆排序、冒泡排序 等属于比较排序 。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置 。
在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。
比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。
计数排序、基数排序、桶排序则属于非比较排序 。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置 。
非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。
非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。
1、冒泡排序(Bubble Sort)
冒泡排序 是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
1.3 代码实现
- 1.1 算法描述
- 步骤1: 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 步骤2: 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 步骤3: 针对所有的元素重复以上的步骤,除了最后一个;
- 步骤4: 重复步骤1~3,直到排序完成。
- 1.2 动图演示
package com.ggqq.sort; import java.util.Arrays; public class MaoPao { public static void main(String[] args){ int[] arr ={2,33,2,12,43,656,76,2,1}; 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]){//交换位置 int temp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = temp; } } } System.out.println(Arrays.toString(arr)); } }
- 1.4 算法分析
- 最佳情况:T(n) = O(n)
- 最差情况:T(n) = O(n2)
- 平均情况:T(n) = O(n2)
2、选择排序(Selection Sort)
选择排序 是表现最稳定的排序算法之一 ,因为无论什么数据进去都是O(n2)的时间复杂度 ,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。
选择排序(Selection-sort) 是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。2.3 代码实现
- 2.1 算法描述
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
- 步骤1:初始状态:无序区为R[1…n],有序区为空;
- 步骤2:第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
- 步骤3:n-1趟结束,数组有序化了。
- 2.2 动图演示(找最小)
package com.ggqq; import java.util.Arrays; public class XuanZe { public static void main(String[] args) { //选择排序 //原理:从0索引处开始,依次和后面的元素进行比较,小的元素往前放,经过一轮比较后,最小的元素就出现在了最小的索引处 int[] arr = {3,44,38,5,47,15,36,26,27,3,46,4,19,50,48}; for(int index = 0 ;index<arr.length-1;index++){ for (int i = index+1;i<arr.length;i++){ if(arr[index]>arr[i]){ int t = arr[index]; arr[index] = arr[i]; arr[i] = t; } } } //遍历数组 System.out.println(Arrays.toString(arr)); } }
2.4 算法分析
- 最佳情况:T(n) = O(n2)
- 最差情况:T(n) = O(n2)
- 平均情况:T(n) = O(n2)
3、直接插入排序(Insertion Sort)
插入排序(Insertion-Sort) 的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
- 3.1 算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 步骤1: 从第一个元素开始,该元素可以认为已经被排序;
- 步骤2: 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 步骤3: 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 步骤4: 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 步骤5: 将新元素插入到该位置后;
- 步骤6: 重复步骤2~5。
- 3.2 动图演示
- 3.3 代码实现
package com.ggqq; import java.util.Arrays; public class ChaRu { public static void main(String[] args) { //直接插入排序 //原理:直接插入排序,从1索引处开始,将后面的元素,插入之前的有序排列中使之仍保持有序 int[] arr = {3,44,38,5,47,15,36,26,27,3,46,4,19,50,48}; //方法一:两层for for(int i = 1; i<arr.length;i++){ for(int j = i; j>0; j--){ if(arr[j]<arr[j-1]){ int t = arr[j]; arr[j] = arr[j-1]; arr[j-1] = t; } } } //方法二: for(int i = 1; i<arr.length;i++){ int j = i; while(j>0 && arr[j]<arr[j-1]){ int t = arr[j]; arr[j] = arr[j-1]; arr[j-1] = t; j--; } } //遍历数组 System.out.println(Arrays.toString(arr)); } }
3.4 算法分析
- 最佳情况:T(n) = O(n)
- 最坏情况:T(n) = O(n2)
- 平均情况:T(n) = O(n2)
4、希尔排序(Shell Sort)(直接插入排序的优化)(直接插入排序是增量为1的希尔排序)
希尔排序是希尔(Donald Shell) 于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
- 4.1 算法描述
我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的,可以试试克努特序列 h= 3 * h+ 1。此处我们做示例使用希尔增量。
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 步骤1:选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 步骤2:按增量序列个数k,对序列进行k 趟排序;
- 步骤3:每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
- 4.2 过程演示
4.3 代码实现
package com.ggqq; import java.util.Arrays; public class XiEr { public static void main(String[] args) { //希尔排序 //原理:是对直接插入排序的一个优化,核心的思想是合理的选取增量,经过一轮排序后,就会让序列大致有序 //然后不断缩小增量,进行插入排序,直到增量为1,整个排序结束 int[] arr = {3,44,38,5,47,15,36,26,27,3,46,4,19,50,48}; //法一:合理的选取增量,第一次这个增量选取为数组长度的一半,然后不断的减半 for(int h = arr.length/2; h>0; h/=2){ for(int i = h; i<arr.length;i++){ for(int j = i; j>h-1; j-=h){ if(arr[j]<arr[j-h]){ int t = arr[j]; arr[j] = arr[j-h]; arr[j-h] = t; } } } } //法二:合理的选取增量,这次这个增量选取克努特序列 /** * int h = 1; * h = h*3 + 1; 1,4,13,40,121,364....... * */ int jiange = 1; while (jiange<=arr.length/3){ jiange = jiange*3 +1; } for(int h = jiange; h>0; h=(h-1)/3){ for(int i = h; i<arr.length;i++){ for(int j = i; j>h-1; j-=h){ if(arr[j]<arr[j-h]){ int t = arr[j]; arr[j] = arr[j-h]; arr[j-h] = t; } } } } //遍历数组 System.out.println(Arrays.toString(arr)); } }
4.4 算法分析
- 最佳情况:T(n) = O(nlog2 n)
- 最坏情况:T(n) = O(nlog2 n)
- 平均情况:T(n) =O(nlog2n)
5、归并排序(Merge Sort)
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。
归并排序 是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
- 5.1 算法描述
- 步骤1:把长度为n的输入序列分成两个长度为n/2的子序列;
- 步骤2:对这两个子序列分别采用归并排序;
- 步骤3:将两个排序好的子序列合并成一个最终的排序序列。
- 5.2 演示
- 5.3 代码实现
package com.ggqq; import java.util.Arrays; public class GuiBing { public static void main(String[] args) { //归并排序 //原始待排序数组 int[] arr = {3,44,38,5,47,15,36,26,27,3,46,4,19,50,48,-8,101000}; //我们先给一个左右两边是有序的一个数组,先来进行归并操作 //int[] arr1 = {4,5,7,8,1,2,3,6}; //----------------拆分---------------- chaiFen(arr,0,arr.length-1); //----------------归并---------------- guiBing(arr,0,arr.length/2,arr.length-1); //遍历原数组 System.out.println(Arrays.toString(arr)); } private static void chaiFen(int[] arr, int startIndex, int endIndex) { //计算中间索引 int centerIndex = (startIndex+endIndex)/2; if(startIndex<endIndex){ chaiFen(arr,startIndex,centerIndex);//左边拆分 chaiFen(arr,centerIndex+1,endIndex);//右边拆分 guiBing(arr,startIndex,centerIndex,endIndex); } } private static void guiBing(int[] arr1, int startIndex, int centerIndex, int endIndex) { //定义一个临时数组 int[] tempArr = new int[endIndex-startIndex+1]; //定义左边数组的起始索引 int i= startIndex; //定义右边数组的起始索引 int j= centerIndex+1; //定义临时数组的起始索引 int index = 0; //比较左右两个数组的元素大小,往临时数组中放 while(i<=centerIndex && j<=endIndex){ if(arr1[i]<=arr1[j]){ tempArr[index]=arr1[i]; i++; }else { tempArr[index] = arr1[j]; j++; } index++; } //处理可能剩余的元素(左边剩余) while(i<=centerIndex){ tempArr[index]=arr1[i]; i++; index++; } //处理可能剩余的元素(右边剩余) while(j<=endIndex){ tempArr[index] = arr1[j]; j++; index++; } //将临时数组中的元素取到原数组中 for (int k = 0; k < tempArr.length; k++) { arr1[k+startIndex]=tempArr[k]; } } }
5.4 算法分析
- 最佳情况:T(n) = O(n)
- 最差情况:T(n) = O(nlogn)
- 平均情况:T(n) = O(nlogn)
6、快速排序(Quick Sort)非常重要!!!(笔试简答遇到过)
快速排序 的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
- 6.1 算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 步骤1:从数列中挑出一个元素,称为 “基准”(pivot );
- 步骤2:分区:重新排序数列,找所有元素比基准值小的摆放在基准前面,找所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 步骤3:递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。
不同的是,冒泡排序在每一轮只把一个元素冒泡到数列的一端,而快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。
这种思路就叫做分治法。
每次把数列分成两部分,究竟有什么好处呢?
假如给定8个元素的数列,一般情况下冒泡排序需要比较8轮,每轮把一个元素移动到数列一端,时间复杂度是O(n^2)。
而快速排序的流程是什么样子呢?
如图所示,在分治法的思想下,原数列在每一轮被拆分成两部分,每一部分在下一轮又分别被拆分成两部分,直到不可再分为止。
这样一共需要多少轮呢?平均情况下需要logn轮,因此快速排序算法的平均时间复杂度是 O(nlogn)。
基准元素的选择
基准元素,英文pivot,用于在分治过程中以此为中心,把其他元素移动到基准元素的左右两边。
那么基准元素如何选择呢?
最简单的方式是选择数列的第一个元素:
这种选择在绝大多数情况是没有问题的。但是,假如有一个原本逆序的数列,期望排序成顺序数列,那么会出现什么情况呢?
..........
我们该怎么避免这种情况发生呢?
其实很简单,我们可以不选择数列的第一个元素,而是随机选择一个元素作为基准元素。
这样一来,即使在数列完全逆序的情况下,也可以有效地将数列分成两部分。
当然,即使是随机选择基准元素,每一次也有极小的几率选到数列的最大值或最小值,同样会影响到分治的效果。
所以,快速排序的平均时间复杂度是 O(nlogn),最坏情况下的时间复杂度是 O(n^2)。
元素的移动
选定了基准元素以后,我们要做的就是把其他元素当中小于基准元素的都移动到基准元素一边,大于基准元素的都移动到基准元素另一边。
具体如何实现呢?有两种方法:
1.挖坑法
2.指针交换法
何谓挖坑法?我们来看一看详细过程。(非常重要,多益网络笔试简答让把每一步都推出来)
给定原始数列如下,要求从小到大排序:
首先,我们选定基准元素Pivot,并记住这个位置index,这个位置相当于一个“坑”。并且设置两个指针left和right,指向数列的最左和最右两个元素:
接下来,从right指针开始,把指针所指向的元素和基准元素做比较。如果比pivot大,则right指针向左移动;如果比pivot小,则把right所指向的元素填入坑中。
在当前数列中,1<4,所以把1填入基准元素所在位置,也就是坑的位置。这时候,元素1本来所在的位置成为了新的坑。同时,left向右移动一位。
此时,left左边绿色的区域代表着小于基准元素的区域。
接下来,我们切换到left指针进行比较。如果left指向的元素小于pivot,则left指针向右移动;如果元素大于pivot,则把left指向的元素填入坑中。
在当前数列中,7>4,所以把7填入index的位置。这时候元素7本来的位置成为了新的坑。同时,right向左移动一位。
此时,right右边橙色的区域代表着大于基准元素的区域。
下面按照刚才的思路继续排序:
8>4,元素位置不变,right左移
2<4,用2来填坑,left右移,切换到left。
6>4,用6来填坑,right左移,切换到right。
3<4,用3来填坑,left右移,切换到left。
5>4,用5来填坑,right右移。这时候left和right重合在了同一位置。
这时候,把之前的pivot元素,也就是4放到index的位置。此时数列左边的元素都小于4,数列右边的元素都大于4,这一轮交换终告结束。
- 6.3 代码实现
package com.ggqq.sort; import java.util.Arrays; public class Quick { public static void main(String[] args) { int[] arr = {3,44,38,5,47,15,36,26,27,3,46,4,19,50,48,-8,101000}; quickSort(arr,0,arr.length-1); System.out.println(Arrays.toString(arr)); } public static void quickSort(int[] arr,int start,int end){ //分治 if(start<end){ int index = getIndex(arr,start,end); //基准元素 quickSort(arr,start,index-1);//左区 quickSort(arr,index+1,end);//右区 } } //获取基准元素 private static int getIndex(int[] arr,int start,int end) { int i = start; int j = end; int x = arr[i]; //由后向前找比他小的数,直到找到暂停 while(i<j){ while(i<j && arr[j]>x){ j--; } if(i<j){ arr[i] = arr[j];//将找到的数挖出来填到前一个坑中 i++; } //由前向后找比他大的数,直到找到暂停 while(i<j && arr[i]<x){ i++; } if(i<j){ arr[j] = arr[i];//将找到的数挖出来填到前一个坑中 j--; } } //将基准数填到最后的坑中(此时i和j相等) arr[i] = x; return i; } }
力扣:https://leetcode-cn.com/problems/sort-an-array/submissions/
指针交换法
何谓指针交换法?我们来看一看详细过程。
给定原始数列如下,要求从小到大排序:
开局和挖坑法相似,我们首先选定基准元素Pivot,并且设置两个指针left和right,指向数列的最左和最右两个元素:
接下来是第一次循环,从right指针开始,把指针所指向的元素和基准元素做比较。如果大于等于pivot,则指针向左移动;如果小于pivot,则right指针停止移动,切换到left指针。
在当前数列中,1<4,所以right直接停止移动,换到left指针,进行下一步行动。
轮到left指针行动,把指针所指向的元素和基准元素做比较。如果小于等于pivot,则指针向右移动;如果大于pivot,则left指针停止移动。
由于left一开始指向的是基准元素,判断肯定相等,所以left右移一位。
由于7 > 4,left指针在元素7的位置停下。这时候,我们让left和right指向的元素进行交换。
接下来,我们进入第二次循环,重新切换到right向左移动。right先移动到8,8>4,继续左移。由于2<4,停止在2的位置。
切换到left,6>4,停止在6的位置。
元素6和2交换。
进入第三次循环,right移动到元素3停止,left移动到元素5停止。
元素5和3交换。
进入第四次循环,right移动到元素3停止,这时候请注意,left和right指针已经重合在了一起。
当left和right指针重合之时,我们让pivot元素和left与right重合点的元素进行交换。此时数列左边的元素都小于4,数列右边的元素都大于4,这一轮交换终告结束。
下面的方法在力扣上超时;
package com.ggqq; import java.util.Arrays; public class Quick2 { public static void main(String[] args) { //快速排序(指针交换法) int[] arr = {3,44,38,5,47,15,36,26,27,3,46,4,19,50,48,-8,101000}; //调用工具类,进行快速排序,传入数组,起始位置,结束位置 quickSort(arr,0,arr.length-1); //遍历数组 System.out.println(Arrays.toString(arr)); } private static void quickSort(int[] arr, int start, int end) { //找出左右两区的索引位置,然后对左右两区进行递归调用 if(start<end){ int index = getIndex(arr,start,end); quickSort(arr,start,index-1);//左区 quickSort(arr,index+1,end); //右区 } } //找索引 private static int getIndex(int[] arr, int start, int end) { int i= start; int j = end; int x = arr[i];//将该数组的第一个元素设为比较元素 while(i<j){ //由后向前找比他小的数 while (i<j && arr[j] >= x){ j--; } //由前向后找比他大的数 while (i<j && arr[i] < x){ i++; } if(i!=j){ swap(arr,i,j);//将大数与小数交换 } } //将基准数和 left与right重合点的元素进行交换 swap(arr,i,start); return i;//返回比较元素的位置 } //交换函数 public static void swap(int arr[],int i,int j){ int t = arr[i]; arr[i] = arr[j]; arr[j] = t; } }
6.4 算法分析
- 最佳情况:T(n) = O(nlogn)
- 最差情况:T(n) = O(n2)
- 平均情况:T(n) = O(nlogn)
7、堆排序(Heap Sort)
堆排序(Heapsort) 是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
- 7.1 算法描述
- 步骤1:将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 步骤2:将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 步骤3:由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新的大顶堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
- 7.2 动图演示
- 7.3 代码实现
package com.ggqq; import java.util.Arrays; public class Dui { public static void main(String[] args) { //定义一个数组 int[] arr = {1,0,6,7,2,3,4,3,231,313}; //定义开始调整的位置 //从最后一个非叶子节点开始向上构造最大堆 int startIndex = (arr.length-1)/2;//最后一个非叶子节点的索引(百度解答:因为它是最后一个节点的parent) //循环开始调 for(int i = startIndex;i>=0;i--){ toMaxHeap(arr,arr.length,i); } //经过上面的操作后,已经把数组变成了一个大顶堆,把根元素和最后一个元素进行交换 for(int i = arr.length-1;i>0;i--){ //进行调换 int t = arr[0]; arr[0] = arr[i]; arr[i] = t ; //换完之后,再把剩余元素调成大顶堆 toMaxHeap(arr,i,0); } System.out.println(Arrays.toString(arr)); } //调整成大顶堆的方法 private static void toMaxHeap(int[] arr, int size, int index) {//arr:要调整的数组,size:调整的元素个数大小 index:从哪里开始调整 //获取左右子节点的索引 int leftNodeIndex = index * 2 + 1; int rightNodeIndex = index * 2 + 2; //查找最大节点所对应大的索引 int maxIndex = index; if(leftNodeIndex<size && arr[leftNodeIndex]>arr[maxIndex]){ maxIndex = leftNodeIndex; } if(rightNodeIndex<size && arr[rightNodeIndex]>arr[maxIndex]){ maxIndex = rightNodeIndex; } //调换位置 if(maxIndex != index){ int t = arr[maxIndex]; arr[maxIndex] = arr[index]; arr[index] = t; //调换完之后,可能会影响到下面的子树不是大顶堆,我们还需要再次调换 toMaxHeap(arr,size,maxIndex); } } }
7.4 算法分析
- 最佳情况:T(n) = O(nlogn)
- 最差情况:T(n) = O(nlogn)
- 平均情况:T(n) = O(nlogn)
8、基数排序(Radix Sort)
基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数(个,十,百位...);
基数排序是按照 低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
基数排序基于分别排序,分别收集,所以是稳定的。
- 8.1 算法描述
- 步骤1:取得数组中的最大数,并取得位数;
- 步骤2:arr为原始数组,从最低位开始取每个位组成radix数组;
- 步骤3:对radix进行计数排序(利用计数排序适用于小范围数的特点);
- 8.2 动图演示
第一轮根据个位数字进行取,第二轮根据十位数字进行取(比如数字7,没有十位,可以看做十位是0,所以放到0号桶)...- 8.3 代码实现(不适合负数)
package com.ggqq;
import java.util.Arrays;
public class JiShu {
public static void main(String[] args) {
//基数排序:通过分配,在再收集的方式进行排序 (本代码不适合负数)
//原始待排序数组
int[] arr = {3,44,38,5,47,15,36,26,27,3,46,4,19,50,48,8,0,101000};
sortArray(arr);
//遍历原数组
System.out.println(Arrays.toString(arr));
}
private static void sortArray(int[] arr) {
//确定排序轮次(几位数)
//获取数组中的最大值
int max = getMax(arr);
int len = String.valueOf(max).length();//获取最大位数
//定义二维数组,放10个桶
int[][] tempArr = new int[10][arr.length];//(极端情况下,会将所有的数字都放在一个桶中)
//定义统计数组
int[] counts = new int[10];
//循环轮次
for(int i = 0, n = 1 ;i<len;i++,n*=10){
//获取每个位上的数字
for(int j = 0 ;j<arr.length;j++){
int yuShu = arr[j]/n%10;
tempArr[yuShu][counts[yuShu]++] = arr[j];
}
//取出桶中的元素
int index = 0;
for (int k = 0;k<counts.length;k++){
if(counts[k] != 0){
for(int h = 0;h<counts[k];h++){
//从桶中取出元素放回原数组
arr[index] = tempArr[k][h];
index++;
}
counts[k] = 0;//清除上一次统计的个数
}
}
}
}
private static int getMax(int[] arr) {
int max = arr[0];
for(int i = 1;i<arr.length;i++){
if(arr[i]>max){
max=arr[i];
}
}
return max;
}
}
8.4 算法分析
- 最佳情况:T(n) = O(n * k)
- 最差情况:T(n) = O(n * k)
- 平均情况:T(n) = O(n * k)
8.5 基数排序有两种方法:
- MSD 从高位开始进行排序
- LSD 从低位开始进行排序
基数排序 vs 计数排序 vs 桶排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序: 根据键值的每位数字来分配桶
- 计数排序: 每个桶只存储单一键值
- 桶排序: 每个桶存储一定范围的数值