基于比较的排序算法
基于比较的排序算法有三种设计思路,分别为插入,交换和选择。
对于插入排序,主要有直接插入排序,希尔排序;
对于交换排序,主要有冒泡排序,快速排序;
对于选择排序,主要有简单选择排序,堆排序;
其它排序:归并排序。
不稳定排序:简单选择排序,堆排序,快速排序
1、插入排序
(1) 直接插入排序
思想:将所有待排序数据分成两个序列,一个是有序序列S,另一个是待排序序列U,初始时,S为空,U为所有数据组成的数列,然后依次将U中的数据插到有序序列S中,直到U变为空。
【示例】:
[初始关键字] [49] 38 65 97 76 13 27 49
J=2(38) [38 49] 65 97 76 13 27 49
J=3(65) [38 49 65] 97 76 13 27 49
J=4(97) [38 49 65 97] 76 13 27 49
J=5(76) [38 49 65 76 97] 13 27 49
J=6(13) [13 38 49 65 76 97] 27 49
J=7(27) [13 27 38 49 65 76 97] 49
J=8(49) [13 27 38 49 49 65 76 97]
/** 插入排序<br/>
*/
public static void insertSort(int[] arr) {
int len = arr.length;
int temp;
int j;
//外循环是未排序的序列,从第二个数开始
for (int i = 1; i < len; i++) {
temp = arr[i];//将未排序的第一个数赋值temp
j=i;
if(arr[j-1]>temp){//与排好序的最后一位进行比较
while(j>=1 && arr[j-1]>temp){//找到第一个比temp小的数
arr[j] = arr[j-1];//顺序后移
j--;
}
}
arr[j]=temp;//插入
}
}
特点:稳定排序,原地排序
复杂度分析:时间复杂度O(N*N)
最好情况:顺序的,123456,移动次数为0,比较次数为n-1次,时间复杂度为O(N)
最坏情况:逆序的,654321,移动次数2+3+4+…n+1,比较次数2+3+4+…n,时间复杂度为O(N*N)
平均情况:概率相同原则,比较次数与移动次数均为N* N/4,时间复杂度O(N*N),因此性能比冒泡和简单选择排序要好一些。
适用场景:当数据已经基本有序时,采用插入排序可以明显减少数据交换和数据移动次数,进而提升排序效率。
(2)、希尔排序
特点:非稳定排序,原地排序,时间复杂度O(n^lamda)(1 < lamda < 2), lamda和每次步长选择有关。
思想:增量缩小排序。先将序列按增量划分为元素个数近似的若干组,使用直接插入排序法对每组进行排序,然后不断缩小增量直至为1,最后使用直接插入排序完成排序。
适用场景:因为增量初始值不容易选择,所以该算法不常用。
2、交换排序
(1)冒泡排序
思想:将整个序列分为无序和有序两个子序列,不断通过交换较大元素至无序子序列首完成排序。
/**
* 冒泡法排序
*/
public static void bubbleSort(int[] arr){
int temp = 0 ;
int len = arr.length;
for (int i = 0; i < len-1; i++) {
for (int j = len-1; j > i; j--) {//从后往前
if(arr[j]<arr[j-1]){
temp = arr[j];
arr[j]=arr[j-1];
arr[j-1]= temp;
}
}
}
}
特点:稳定排序,原地排序,时间复杂度O(N*N)
使用场景:最好的情况下,就是本身就是有序的,比较n-1次,时间复杂度O(N)
最坏情况下,逆序,比较1+2+…+n-1次,时间复杂度为O(N^2)
(2)快速排序
思想:不断寻找一个序列的枢轴点,然后分别把小于和大于枢轴点的数据移到枢轴点两边,然后在两边数列中继续这样的操作,直至全部序列排序完成。
【示例】:
初始关键字 [49 38 65 97 76 13 27 49]
一趟排序之后 [27 38 13] 49 [76 97 65 49]
二趟排序之后 [13] 27 [38] 49 [49 65]76 [97]
三趟排序之后 13 27 38 49 49 [65]76 97
最后的排序结果 13 27 38 49 49 65 76 97
/**
* 快速排序
*/
public static void quickSort(int[] arr,int start,int end){
if(start > end)
return;
int i = start;
int j = end;
int index = arr[i];//确定中轴
while(i<j){
//右边一个指针,如果右边数比中轴大,向左走
while(i<j && arr[j]>index)
j--;
if(i<j){//右边的数比中轴小,把右边的数给左边i的位置
arr[i] = arr[j];
i++;
}
//然后从左往右,小于中轴就向右走,如果大于,赋值给右边j的位置
while(i<j && arr[i]<index)
i++;
if(i<j){
arr[j]=arr[i];
j--;
}
}
arr[i]=index;//此时i=j,把中轴的值赋给i
quickSort(arr, 0, i-1);
quickSort(arr, i+1, end);
}
特点:不稳定排序,原地排序
复杂度分析:时间复杂度O(N*lg N)
适用场景:当待排序的关键字是随机分布时,快速排序的平均时间最短;
优化快速排序:
1、优化选取枢轴
固定选择第一个作为枢轴是不合理的,排序快慢取决于枢轴在整个序列的位置
改进:三数取中:取三个关键字,将中间数作为枢轴
2、优化排序方案
如果数据较大时,快速排序较好,如果只是几个数字,还不如用直接插入排序好,此时可以增加一个判断,如果数据个数大于某个值,就用快速排序,小于某个值,就用直接插入排序
3、选择排序
(1)简单选择排序
思想:将序列划分为无序和有序两个子序列,寻找无序序列中的最小(大)值和无序序列的首元素交换,有序区扩大一个,循环下去,最终完成全部排序。
排序过程:
【示例】:
初始关键字 [49 38 65 97 76 13 27 49]
第一趟排序后 13 [38 65 97 76 49 27 49]
第二趟排序后 13 27 [65 97 76 49 38 49]
第三趟排序后 13 27 38 [97 76 49 65 49]
第四趟排序后 13 27 38 49 [49 97 65 76]
第五趟排序后 13 27 38 49 49 [97 97 76]
第六趟排序后 13 27 38 49 49 76 [76 97]
第七趟排序后 13 27 38 49 49 76 76 [ 97]
最后排序结果 13 27 38 49 49 76 76 97
/** 简单选择排序<br/>
* <li>在未排序序列中找到最小元素,存放到排序序列的起始位置</li>
* <li>再从剩余未排序元素中继续寻找最小元素,然后放到排序序列末尾。</li>
* <li>以此类推,直到所有元素均排序完毕。</li>
*/
public static void selectSort(int[] arr) {
int len =arr.length;
int temp ;
int flag; //记录最小值下标
for (int i = 0; i < len; i++) {
temp = arr[i];
flag = i;
for (int j = i+1; j < len; j++) {
if(arr[j]<temp){
temp = arr[j];
flag = j;
}
}
if(i!=flag){
arr[flag]=arr[i];
arr[i] = temp;
}
}
}
特点:不稳定排序(比如对3 3 2三个数进行排序,第一个3会与2交换)
复杂度分析:其特点是交换次数少,比较次数多。时间复杂度O(N*N),无论最好最坏情况,比较的次数都是一样多为1+2+3+…+n-1
对于交换次数而言:最好情况是顺序,交换次数为0,最差情况是逆序,交换次数为n-1次
使用场景:当n较小,元素分布有序,如果不要求稳定性,选择直接选择排序,性能要略优于冒泡。
(2) 堆排序
1、 基本思想:
/**
* 堆排序,大顶堆,从小到大排序
* 注意位置是index的,值为arr[index-1]
*/
public static void maxHeapSort(int[] arr){
//根据输入构建大顶堆,从最后一个跟节点往前来
for(int i = arr.length/2;i>0;i--){
maxHeapify(arr, i, arr.length);
}
//循环,每次把根节点和最后一个节点调换位置
for(int i=arr.length;i>0;i--){
int temp = arr[0];
arr[0] = arr[i-1];
arr[i-1]=temp;
//堆得长度减少1,排除掉最后一个节点
maxHeapify(arr,1,i-1);
}
}
/*
* 难点:堆调整,使其变为最大堆
*/
private static void maxHeapify(int[] arr, int parentIndex, int heapSize) {
int temp = arr[parentIndex-1];
int childIndex ;
//把最大根节点赋值为temp,注意temp是每次for循环都要赋值的
for(childIndex = parentIndex*2;childIndex <=heapSize;childIndex *= 2){
if(childIndex<heapSize && arr[childIndex-1]<arr[childIndex]){
childIndex++;//子节点最大的那个
}
if(temp > arr[childIndex-1])
break;//如果根节点比子节点大,什么也不做
arr[parentIndex-1]=arr[childIndex-1];//否则把子节点值赋给根节点,注意,此时还要执行
parentIndex = childIndex;
}
arr[parentIndex-1]=temp;//插入
}
特点:非稳定排序,原地排序
复杂度分析:无论最好最坏平均都是时间复杂度O(N*lg N)
整个构建堆得过程中,因为是从最下层最右边的非叶子节点开始,将他与孩子进行比较和交换最多就2次,因此时间复杂度为O(N)
重建堆:从最上到最下O(lgN),一共需要n-1次堆顶记录,因此是O(N*lg N)
适用场景:对原始记录排序不敏感,无论最好最坏平均复杂度均为O(N*lg N),要好于冒泡,简单选择,直接插入。不如快排广泛
4、其它排序
(1) 归并排序
思想:首先,将整个序列(共N个元素)看成N个有序子序列,然后依次合并相邻的两个子序列,这样一直下去,直至变成一个整体有序的序列。
/**
* 归并排序,先分再合
*/
public static void mergeSort(int[] arr,int left,int right){
if(left<right){
int mid = (left+right)/2;
mergeSort(arr, left, mid);
mergeSort(arr, mid+1, right);
merge(arr, left, mid, right);
}
}
/*
* 合并
*/
public static void merge(int[] arr,int left , int mid, int right){
int i,j,k;
int leftLength = mid-left+1;//左数组长度
int rightLength = right-mid;//右数组长度
int[] L = new int[leftLength];
int[] R = new int[rightLength];
//左右数组赋值
for (i = 0,k=left; i < leftLength; i++,k++) {
L[i]=arr[k];
}
for (i = 0,k=mid+1; i < rightLength; i++,k++) {
R[i]=arr[k];
}
//从小到大合并
for (k=left,i=0,j=0;i<leftLength && j<rightLength;k++) {
if(L[i]>R[j]){
arr[k]=R[j];
j++;
}else{
arr[k]=L[i];
i++;
}
}
//剩下的加入末尾
if(i<leftLength){
for (int m = i; m < leftLength; m++,k++) {
arr[k]=L[m];
}
}
if(j<rightLength){
for (int m = j; m < rightLength; m++,k++) {
arr[k]=R[m];
}
}
}
特点:稳定排序,非原地排序
复杂度分析:最好最坏平均时间复杂度都是O(N*lgN)
空间复杂度:由于在归并过程中需要与原始记录序列相同数量的存储结构存放归并结果,以及递归时需要深度为lgN的栈空间,空间复杂度为O(N+lgN)
适用场景: 当n较大,内存空间允许,因为比较占内存,且要求稳定性
非基于比较的排序算法
非基于比较的排序算法主要有三种,分别为:基数排序,桶排序和计数排序。这些算法均是针对特殊数据的,不如要求数据分布均匀,数据偏差不会太大。采用的思想均是内存换时间,因而全是非原地排序。
1、基数排序
特点:稳定排序,非原地排序,时间复杂度O(N)
思想:把每个数据看成d个属性组成,依次按照d个属性对数据排序(每轮排序可采用计数排序),复杂度为O(d*N)
适用场景:数据明显有几个关键字或者几个属性组成
2、桶排序
特点:稳定排序,非原地排序,时间复杂度O(N)
思想:将数据按大小分到若干个桶(比如链表)里面,每个桶内部采用简单排序算法进行排序。
适用场景:0
3、计数排序
特点:稳定排序,非原地排序,时间复杂度O(N)
思想:对每个数据出现次数进行技术(用hash方法计数,最简单的hash是数组!),然后从大到小或者从小到大输出每个数据。
使用场景:比基数排序和桶排序广泛得多。
总结
对于基于比较的排序算法,大部分简单排序(直接插入排序,选择排序和冒泡排序)都是稳定排序,选择排序除外;大部分高级排序(除简单排序以外的)都是不稳定排序,归并排序除外,但归并排序需要额外的存储空间。对于非基于比较的排序算法,它们都对数据规律有特殊要求 ,且采用了内存换时间的思想。排序算法如此之多,往往需要根据实际应用选择最适合的排序算法。