一、概览
排序算法基本分为:选择排序、冒泡排序、插入排序、希尔排序、快速排序、堆排序、归并排序(合并排序)、计数排序、桶排序、基数排序。
其中,冒泡排序和快速排序属于交换排序,插入排序和希尔排序属于插入排序,选择排序(简单选择排序)和堆排序属于选择排序。
二、排序算法
2.1 冒泡排序
基本思想:从左到右进行两两对比,若为逆序则交换位置,进而将最值元素上浮到右侧。
图解如下:
可以看到每次进行两两对比,逆序就交换位置,将最大值上浮。
Java代码如下:
public static int[] bubbleSort(int[] array){
for(int i=array.length-1;i>0;i--){
for(int j=0;j<i;j++){
if(array[j]>array[j+1]){
int temp = array[j];
array[j]=array[j+1];
array[j+1]=temp;
}
}
}
return array;
}
优化思路:每次小遍历都判断下是否交换过元素,若没有交换过元素,则说明序列已有序,可以提前结束排序。
2.2 快速排序
基本思想:选择一个基准数,扫描数列,将比基准数小的元素放到它的左边,大于或等于基准数的元素放到右边,得到左右两个区间,再对两个区间重复进行快速排序,直到各区间少于两个元素。
这里举例子说明快速排序。比如有如下数列:
现在进行第一次快排,将44作为基准数,L和R分别指向左右两端。
这里先挖出基准数44,然后右指针开始向左遍历,把比44小的数填到原基准数的坑(索引为0),这里明显会把19填入第一个坑,得到下图。
这里填好19后,接着左指针开始向右运动,将比44大的数填入原19的坑位。很明显会把47填到坑位,得到下图。
紧接着,右指针往左找比44小的数填坑,左指针往右找大于或等于44的填坑,直到两个指针索引相等,这样就把44(基准数)填入坑位。
这样得到第一轮排序后的结果,如下:
接着,第二轮,对19到2之间的数列进行快排,将19做为基准数,左指针指向19,右指针指向2,重复上面的步骤,直到区间元素小于2,左边递归结束,接着走右边递归,直至最后结束。
Java代码如下:
public static void quickSort(int[] array,int low,int high){
if(low > high || array.length == 1){
return;
}
int key = array[low];
int min = low;
int max = high;
while(low<high){
while(array[high]>=key && low<high){
high--;
}
array[low] = array[high];
while(array[low]<key && low<high){
low++;
}
array[high] = array[low];
}
array[low] = key;
quickSort(array,min,low-1);
quickSort(array,low+1,max);
}
优化思路:基准数取中间数,尽可能减少递归深度。结合插入排序,比如元素少于10的区间,直接用插入排序,效率更高。
2.3 插入排序(直接插入排序)
基本思想:构建有序序列,对于未排序的数据,在已排序的序列中找到相应位置将未排序的数据插入。
图解如下:
可以看到这里将第一个元素视为有序,第二个元素开始视为无序,然后依次和有序数列的元素对比偏移位置,直到有序数列元素小于或等于无序元素。
Java代码如下:
public static int[] insertSort(int[] array){
for(int i=1;i<array.length;i++){
int insert = array[i];
int index = i;
for(int j=i-1;j>=0;j--){
if(array[j]>insert){
array[j+1] = array[j];
index = j;
}else{
break;
}
}
array[index] = insert;
}
return array;
}
优化思路:可以用二分法查找已排序的数列的元素与未排序元素对比,减少对比次数 。可以一次对比多个未排序元素,减少总循环次数。可以将数据链表化,进而大大减少移动次数。可以使用直接插入排序的升级版即希尔排序。
2.4 希尔排序
基本思想:将排序数列分成多个组,对每个组进行插入排序,接着多次调整分组,使数列更加有序,最后使用一次插入排序,将整个数列有序。
这里举例子说明快速排序。比如有如下数列:
这里对数列分组,一般每组间隔元素(步长)为原长度的一半,这里第一次步长为:15/2向下取整数为7。因此索引为0、7、14的数为一组,索引为1、8的数据为一组,以此类推因此,不同颜色标记分组如下:
接着分别对[44,26,48]、[3,27]、[38,46]等分组进行插入排序,完成第一轮,排序结果如下:
第二轮步长为第一次步长的一半,即7/2向下取整,为3,因此分组有3组,具体如下:
对各组[26,5,36,38,19],[3,4,44,46,50],[2,15,27,47,48]进行插入排序,完成第二轮排序,结果如下:
第三轮步长为3/2向下取整为1,因此只有1组,即整个数列进行插入排序。结果如下:
这里问题来了,最后还是要对整个数列进行插入排序,那前面的分组排岂不是无意义!答案是否定的,由于分组排序后,整个序列是大致有序的,最后一次插排的查找和移动元素的次数会小很多!就拿[2,3,4,5]这几个数来讲,最后一次排序不会碰到小数字还在倒数几位要进行大量查找对比和移动元素的情况!总体的遍历元素和移动元素的次数是降低的!
Java代码如下:
public static int[] shellSort(int[] array){
for(int gap=array.length/2; gap>0; gap=gap/2){
//对每组进行排序
for(int i=0;i<gap;i++){
//这里进行快速排序
for(int j=i+gap;j<array.length;j=j+gap){
int temp = array[j];
int index = j;
for(int h = j-gap;h>=0;h=h-gap){
if(array[h]>temp){
array[h+gap] = array[h];
index = h;
}else{
break;
}
}
array[index] = temp;
}
}
}
return array;
}
优化思路:选择更合理的步长。
2.5 选择排序
基本思想:从待排序列中找到最小的元素与第一个元素对换,接着从剩下的元素中继续用这种选择和交换方式进行处理,最后得到一个有序序列。
图解如下:
可以看出先找待排序列的最小值,然后和第一个元素交换。
Java代码如下:
public static int[] selectSort(int[] array){
for(int i=0;i<array.length;i++){
int minIndex = i;
for(int y=i;y<array.length;y++){
if(array[y]<array[minIndex]){
minIndex = y;
}
}
int temp = array[i];
array[i] = array[minIndex];
array[minIndex] = temp;
}
return array;
}
优化思路:每次循环遍历找出最小值和最大值,最小值放左边,最大值放右边,这样循环次数可减少一半。
2.6 堆排序
基本思想:将无序数列构造最大堆,再与最后一个节点交换,然后再将少去最后一个元素的数列构造成最大堆,再交换,这样循环直到数列有序。
数列转化成堆(完全二叉树)的规律如下图所示:
我们用索引为1的节点来举例,其父节点索引为:(1-1)/2=0,左子节点为(2×1)+1=3,右子节点为(2×1)+2=4。其它节点同理。
这里举例子说明堆排序。比如有如下数列:
从上到下,从左到右转化成堆后如下:
接着构造大顶堆(父节点值需要大于两个子节点的值),从最后的父节点46开始往前进行比对交换,依次比对交换46、65、35、13、96、60、91,将子节点比父节点大的值进行交换,得到如下大顶堆:
接着将堆顶96与最后元素22进行交换,堆排除掉96,得到新堆和有序数列[96],如下:
可以发现堆顶为22,不是大顶堆,需要对22进行降级,由于一开始是大顶堆,所以最大值在22的两个子节点(65,91)中,对比可以得出91比65大,因此需要将91和22交换位置,得到如下堆:
这里22还是小于子节点,需要将更大的子节点(81)与22交换位置,同理继续用46与22交换位置,得到如下大顶堆:
这里继续将22与91交换,得到新堆与有序数列[91,96],如下:
重复上面交换堆顶和大子节点步骤最终就可以把堆的所有最大值一一放到有序数列中完成排序。
可以看出,除了第一次构造大顶堆需要对所有父节点遍历对比交换,后续的构造大顶堆只要堆顶依次交换大子节点即可。
Java代码如下:
public static int[] heapSort(int[] array){
//第一次构建大顶堆,从最后一个元素开始找父节点进行堆调整,其中父节点为(i-1)/2,左子节点为2i+1,右子节点为2i+2。
for(int i=array.length-1;i>=0;i=i-2){
int parent = (i-1)/2;
int left = 2*parent+1;
int right = 2*parent+2;
if(parent>=0){
if(right<array.length){
if(array[left]>array[right]){
if(array[left]>array[parent]){
int temp = array[left];
array[left] = array[parent];
array[parent] = temp;
}
}else{
if(array[right]>array[parent]){
int temp = array[right];
array[right] = array[parent];
array[parent] = temp;
}
}
}else{
if(array[left]>array[parent]){
int temp = array[left];
array[left] = array[parent];
array[parent] = temp;
}
}
}
}
// 交换顶部最大值和最后一位
int temp = array[array.length-1];
array[array.length-1] = array[0];
array[0] = temp;
// 循环进行堆调整和交换位置
for(int i=array.length-2;i>0;i--){
int parent=0;
while (parent<=i){
int left = parent*2+1;
int right = parent*2+2;
boolean isChanged = false;
if(right<=i){
if(array[left]>array[right]){
if(array[left]>array[parent]){
temp = array[left];
array[left] = array[parent];
array[parent] = temp;
parent = left;
isChanged = true;
}
}else{
if(array[right]>array[parent]){
temp = array[right];
array[right] = array[parent];
array[parent] = temp;
parent = right;
isChanged = true;
}
}
}else if(left<=i && array[left]>array[parent]){
temp = array[left];
array[left] = array[parent];
array[parent] = temp;
parent = left;
isChanged = true;
}
if(!isChanged){
break;
}
}
temp = array[i];
array[i] = array[0];
array[0] = temp;
}
return array;
}
2.7 合并排序
基本思想:将已经有序的子数列合并得到另一个有序数列。即待排数列分解为多个长度为1的有序数列,然后将有序数列两两合并最后合并成一个有序序列为止。
这里举例子图解说明,如下图:
数列先分成长度为1的子数列,后续两两合并,最后得到最终的序列。这里以最后一步举例说明两两合并:
这里两两合并有序一次遍历即可,设置两个指针分别指向两数列起始位置,对比后将小的放前面即可。
比如这里S1指向3,用3和5对比,则将3作为新数列第一个,接着S1指向38,用38和5对比,结果5比38小,则接下来第二个数是5,接着S2指向15和38对比,比38小,则第三个数是15,这样依次类推(S1和S2相互各移动一段数列复制到新数列)。
Java代码如下:
public static void mergeSort(int[] array,int left,int right,int[] temp){
if(right<=left){
return;
}
int middle = left+(right-left)/2;
mergeSort(array,left,middle,temp);
mergeSort(array,middle+1,right,temp);
//合并,先排序将元素复制到临时数组中(将左右两部分数组遍历,小的放前面),再将临时数组copy到原数组中
int s1 = left;
int s2 = middle+1;
int count = left;
while(s1<=middle || s2<=right){
while(s1<=middle && ((s2<=right &&array[s1]<=array[s2]) || s2>right)){
temp[count] = array[s1];
s1++;
count++;
}
while(s2<=right && ((s1<=middle && array[s2]<array[s1]) || s1>middle)){
temp[count] = array[s2];
s2++;
count++;
}
}
for(int i=left;i<=right;i++){
array[i] = temp[i];
}
}
2.8 计数排序
基本思想:通过对特定值域范围内待排序数组元素数量进行统计来完成数组排序。即统计待排数列各元素出现的次数,然后复原出有序数列。
这里举例子说明计数排序。比如有如下数列:
该数列最大值为9,最小值为1,因此准备一个长度为10(最大值+1)的临时数组,则该临时数组的索引为0、1、2...9(最大值),其中1到9索引上的值分别保存待排数列1到9范围内各元素的出现次数。得到如下临时数组:
可以看到1到9索引上的值分别为:2、7、2...2。意味着待排数列有2个1、7个2...2个9。只需遍历临时数组,按出现次数复原数列即可。
可以看出计数排序的前提有两个:需要排序的元素必须是整数,排序元素在一定范围内,越集中越好。
Java代码如下:
public static int[] countSort(int[] array){
if(array==null || array.length==0){
return array;
}
int max = array[0];
for(int i=0;i<array.length;i++){
if(array[i]>max){
max = array[i];
}
}
int[] tempArr = new int[max+1];
for(int i=0;i<array.length;i++){
tempArr[array[i]]++;
}
int count = 0;
for(int i=0;i<tempArr.length;i++){
if(tempArr[i]>0){
for(int j=0;j<tempArr[i];j++){
array[count] = i;
count++;
}
}
}
return array;
}
优化思路:临时数组长度改成(最大值-最小值+1),这样可以减少[0,最小值)的空间占用。
2.9 桶排序
基本思想:把数据分到有限数量的桶里,然后对每个桶排序,最后把全部桶的数据合并。
这里举例子图解说明,如下图:
待排数列范围是3到48,这里初始化5个桶,其中0号桶装入[0,10)的数据,1号桶装入[10,20)的数据,以此类推,4号桶装入[40,50)的数据。遍历一次各桶装完数据如下图:
接着对各桶进行排序(可以是插入、冒泡等排序),最后按桶顺序进行合并就得到最终的有序数列了。
Java代码如下:
public static int[] basketSort(int[] array) {
if(array==null || array.length==0){
return array;
}
int max = array[0];
int min = array[0];
for(int i=0;i<array.length;i++){
if(array[i]>max){
max = array[i];
}
if(array[i]<min){
min = array[i];
}
}
int[][] basketArr = new int[10][array.length];
int[] indexArr = new int[10];
int size = (max-min)/10+2;
for(int i=0;i<array.length;i++){
int index = array[i]/size;
basketArr[index][indexArr[index]] = array[i];
indexArr[index]++;
}
for(int i=0;i<10;i++){
insertSort(basketArr[i]);
}
int count = 0;
for(int i=0;i<10;i++){
for(int j=0;j<basketArr[i].length;j++){
if(basketArr[i][j]>0){
array[count]=basketArr[i][j];
count++;
}
}
}
return array;
}
2.10 基数排序
基本思想:由基数低位到高位分别放到不同的桶里,最后合并。注意,这里基数排序虽然用到了桶思想,但和基本的桶排序不一样,算是桶排序的扩展。
这里举例子图解说明,如下图:
这里初始化10个桶,待排数列先按个位大小分配到相应的桶里。接着按每个桶元素先进先出原则收集元素得到如下数列:
接着将收集的数列按十位数大小装桶,如下:
接着再按桶顺序以及每个桶先进先出收集元素对数列排序得到如下:
接着按百位大小分配以及再收集就神奇地完成了排序!如下所示:
Java代码如下:
public static int[] radixSort(int[] array){
if(array==null || array.length==0){
return array;
}
int max = array[0];
for(int i=0;i<array.length;i++){
if(array[i]>max){
max = array[i];
}
}
int digit = 0;
while(max>0){
max /=10;
digit++;
}
int base = 1;
for(int i=1;i<=digit;i++){
int[][] basketArr = new int[10][array.length];
int[] indexArr = new int[10];
for(int j=0;j<array.length;j++){
int index = array[j]/base%10;
basketArr[index][indexArr[index]] = array[j];
indexArr[index]++;
}
int count = 0;
for(int j=0;j<10;j++){
for(int k=0;k<basketArr[j].length;k++){
if(basketArr[j][k]>0){
array[count]=basketArr[j][k];
count++;
}
}
}
base *=10;
}
return array;
}
三、时间和空间复杂度
具体时间空间复杂度如下: