八大排序方法及对Arrays类的排序实现探讨
1.插入排序—直接插入排序(Straight Insertion Sort)
基本思想:
将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。
要点:设立哨兵,作为临时存储和判断数组边界之用。
如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
效率:
时间复杂度:O(n^2).
其他的插入排序有二分插入排序,2-路插入排序。
代码如下
2. 插入排序—希尔排序(Shell`s Sort)
基本思想:
-
算法先将要排序的一组数按某个增量d(n/2,n为要排序数的个数)分成若干组,每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。当增量减到1时,进行直接插入排序后,排序完成。
-
希尔排序法(缩小增量法) 属于插入类排序,是将整个无序列分割成若干小的子序列分别进行插入排序的方法。
- 希尔排序方法是一个不稳定的排序方法。
-
代码如下
-
3. 选择排序—简单选择排序(Simple Selection Sort)
-
基本思想:
在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。
-
代码如下
- 简单选择排序,每趟循环只能确定一个元素排序后的定位。我们可以考虑改进为每趟循环确定两个元素(当前趟最大和最小记录)的位置,从而减少排序所需的循环次数。改进后对n个数据进行排序,最多只需进行[n/2]趟循环即可。
-
4.选择排序—堆排序(Heap Sort)
- 堆排序是一种树形选择排序,是对直接选择排序的有效改进。
基本思想:
堆的定义如下:具有n个元素的序列(k1,k2,...,kn),当且仅当满足
时称之为堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最小项(小顶堆)。
若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。如:(a)大顶堆序列:(96, 83,27,38,11,09)
(b) 小顶堆序列:(12,36,24,85,47,30,53,91)
初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。
因此,实现堆排序需解决两个问题:
1. 如何将n 个待排序的数建成堆;
2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。
首先讨论第二个问题:输出堆顶元素后,对剩余n-1元素重新建成堆的调整过程。
调整小顶堆的方法:1)设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶((最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。
2)将根结点与左、右子树中较小元素的进行交换。
3)若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复方法 (2).
4)若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复方法 (2).
5)继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。
称这个自根结点到叶子结点的调整过程为筛选。如图:
再讨论对n 个元素初始建堆的过程。
建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。1)n 个结点的完全二叉树,则最后一个结点是第个结点的子树。
2)筛选从第个结点为根的子树开始,该子树成为堆。
3)之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。
如图建堆初始过程:无序序列:(49,38,65,97,76,13,27,49)
-
代码如下
-
package sort; /** * 作者 super_xueyi * 日期:2017年8月31日下午6:42:27 * 描述:HeapSort */ public class HeapSort { public static int[] heapSort(int[] A, int n) { //堆排序算法 int i; //先把A[]数组构建成一个大顶堆。 //从完全二叉树的最下层最右边的非终端结点开始构建。 for(i=n/2-1;i>=0;i--){ HeapAdjust(A,i,n); } //开始遍历 for(i=n-1;i>0;i--){ swap(A,0,i); //每交换一次得到一个最大值然后丢弃 HeapAdjust(A,0,i); } return A; } //A[i]代表的是下标为i的根结点 private static void HeapAdjust(int[] A,int i,int n){ //【注意】这里下标从0开始 int temp; //存储根结点 temp = A[i]; //沿根结点的左右孩子中较大的往下遍历,由于完全二叉树特性 i的左子节点2i+1 i的右子节点2i+2 for(int j=2*i+1;j<n;j=j*2+1){ if(j<n-1&&A[j]<A[j+1]){ ++j; } if(temp>=A[j]){ break; } //将子节点赋值给根结点 A[i] = A[j]; //将子节点下标赋给i i = j; } //将存储的根结点的值赋给子节点 A[i] = temp; } private static void swap(int[] A,int i,int j){ int temp = A[i]; A[i]=A[j]; A[j] = temp; } }
5. 归并排序(Merge Sort)
- 基本思想
-
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并操作的工作原理如下:第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置重复步骤3直到某一指针超出序列尾将另一序列剩下的所有元素直接复制到合并序列尾
-
-
代码如下
-
package sort; /** * 作者 super_xueyi * 日期:2017年8月31日下午6:48:49 * 描述:MergeSort */ public class MergeSort { public static int[] mergeSort(int[] A, int n) { //归并排序,递归做法,分而治之 mSort(A,0,n-1); return A; } private static void mSort(int[] A,int left,int right){ //分而治之,递归常用的思想,跳出递归的条件 if(left>=right){ return; } //中点 int mid = (left+right)/2; //有点类似后序遍历! mSort(A,left,mid); mSort(A,mid+1,right); merge(A,left,mid,right); } //将左右俩组的按序子序列排列成按序序列 private static void merge(int[] A,int left,int mid,int rightEnd){ //充当tem数组的下标 int record = left; //最后复制数组时使用 int record2 = left; //右子序列的开始下标 int m =mid+1; int[] tem = new int[A.length]; //只要left>mid或是m>rightEnd,就跳出循环 while(left<=mid&&m<=rightEnd){ if(A[left]<=A[m]){ tem[record++]=A[left++]; }else{ tem[record++]=A[m++]; } } while(left<=mid){ tem[record++]=A[left++]; } while(m<=rightEnd){ tem[record++]=A[m++]; } //复制数组 for( ;record2<=rightEnd;record2++){ A[record2] = tem[record2]; } } }
6. 交换排序—冒泡排序(Bubble Sort)
- 基本思想
- 冒泡排序,顾名思义,从下往上遍历,每次遍历往上固定一个最小值
- 加一个标志位,当某一趟冒泡排序没有元素交换时,则冒泡结束,元素已经有序,可以有效的减少冒泡次数。
-
代码如下
7. 交换排序—快速排序(Quick Sort)
基本思想
快速排序(Quicksort)是对冒泡排序的一种改进,使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。
- 从数列中挑出一个元素,称为”枢轴”(pivot)。
- 重新排序数列,所有元素比枢轴值小的摆放在基准前面,所有元素比枢轴值大的摆在枢轴的后面(相同的数可以到任一边)。在这个分区结束之后,该枢轴就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于枢轴值元素的子数列和大于枢轴值元素的子数列排序。
-
-
代码如下
-
package sort; /** * 作者 super_xueyi * 日期:2017年8月31日下午6:56:03 * 描述:QuickSort */ public class QuickSort { public static int[] quickSort(int[] A, int n) { //快速排序 qSort(A,0,n-1); return A; } public static void qSort(int[] A,int left,int right){ //枢轴 int pivot; if(left<right){ pivot = partition(A,left,right); qSort(A,left,pivot-1); qSort(A,pivot+1,right); } } //优化选取一个枢轴,想尽办法把它放到一个位置,使它左边的值都比它小,右边的值都比它大 public static int partition(int[] A,int left,int right){ //优化选取枢轴,采用三数取中的方法 int pivotKey = median3(A,left,right); //从表的俩边交替向中间扫描 //枢轴用pivotKey给备份了 while(left<right){ while(left<right&&A[right]>=pivotKey){ right--; } //用替换方式,因为枢轴给备份了,多出一个存储空间 A[left]=A[right]; while(left<right&&A[left]<=pivotKey){ left++; } A[right]=A[left]; } //把枢轴放到它真正的地方 A[left]=pivotKey; return left; } //三数取中 public static int median3(int[] A,int left,int right){ int mid=(right-left)/2; if(A[left]>A[right]){ swap(A,left,right); } if(A[mid]>A[left]){ swap(A,mid,left); } if(A[mid]>A[right]){ swap(A,mid,right); } return A[left]; } public static void swap(int[] A,int i,int j){ int temp =A[i]; A[i]=A[j]; A[j]=temp; } }
8. 桶排序/基数排序(Radix Sort)
-
基本思想
- 设置一个定量的数组当作空桶子。
- 寻访序列,并且把项目一个一个放到对应的桶子去。
- 对每个不是空的桶子进行排序。
- 从不是空的桶子里把项目再放回原来的序列中
-
代码如下
-
-
通过Arrays类的源代码可以发现其调用的DualPivotQuicksort类的sort方法
查看DualPivotQuicksort类的源代码发现在带排序数组长度小于一个值时使用插入排序,查看常量INSERTION_SORT_THRESHOLD,
发现其为47。
同样的方法查看快速排序的最佳数组长度为286
并且发现当数组基本有序时不使用归并排序
在数组长度太大是使用
计数排序Counting sort,长度为3200以上时