排序算法:排序大的分类可以分为两种:内排序和外排序。
在排序过程中,全部记录存放在内存,则称为内排序,如果排序过程中需要使用外存,则称为外排序。下面讲的排序都是属于内排序。 内排序可以分为以下几类:
-
- 插入排序:直接插入排序(稳定)、二分法插入排序(稳定)、希尔排序(不稳定)。
- 选择排序:简单选择排序(不稳定)、堆排序(不稳定)。
- 交换排序:冒泡排序(稳定)、快速排序(不稳定)。
- 归并排序(稳定)
- 基数排序(稳定)
插入排序
-
- 思想:每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的字序列的合适位置,直到全部插入排序为止。
- 直接插入排序(从后向前找到合适位置后插入)
-
- 直接插入排序是稳定的排序。关于各种算法的稳定性分析可以参考http://www.cnblogs.com/Braveliu/archive/2013/01/15/2861201.html
- 文件初态不同时,直接插入排序所耗费的时间有很大差异。
- 若文件初态为正序,则每个待插入的记录只需要比较一次就能够找到合适的位置插入,故算法的时间复杂度为O(n),这是最好的情况。
- 若初态为反序,则第i个待插入记录需要比较i+1次才能找到合适位置插入,故时间复杂度为O(n^2),这是最坏的情况。
- 直接插入排序的平均时间复杂度为O(n^2)。
// 插入排序
public static void insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int j = i;
int temp = arr[i];
for (; j > 0 && temp < arr[j - 1]; j--) {
arr[j] = arr[j - 1];
}
arr[j] = temp;
}
}
- 二分法插入排序(按二分法找到合适位置插入)
-
- 二分插入稳定。二分插入排序的比较次数与待排序记录的初始状态无关,仅依赖于记录的个数。
- 当n较大时,比直接插入排序的最大比较次数少得多。但大于直接插入排序的最小比较次数。
- 算法的移动次数与直接插入排序算法的相同,最坏的情况为n^2/2,最好的情况为n,平均移动次数为O(n^2)。
- 希尔排序
-
- 基本思想:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。该方法实质上是一种分组插入方法。
- 希尔排序不稳定
- 希尔排序时间性能优于直接插入排序,希尔排序的平均时间复杂度为O(nlogn)。
- 希尔排序就是对直接插入排序的一个优化。比如有这么一种情况:对一个无序数组进行从小到大的排序,但是数组的最后一个位置的数是最小的,我们要把它挪到第一个位置,其他位置的都要往后移动,要是这个数组非常大,那么直接插入排序的开销就非常大。
// 希尔排序
public static void shellSort(int[] arr) {
int step = arr.length / 2;
for (; step > 0; step /= 2) {
for (int i = step; i < arr.length; i++) {
int j = i;
int temp = arr[i];
for (; j >= step && temp < arr[j - step]; j -= step) {
arr[j] = arr[j - step];
}
arr[j] = temp;
}
}
}
选择排序(简单选择排序、堆排序)
-
- 思想:每趟从待排序的 记录序列中选择关键字最小的记录放置到已排序表的最前位置,直到全部排完。
- 简单选择排序:,
-
- 基本思想:在要排序的一组数中,选出最小的一个数与第一个位置的数交换;然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。
- 简单选择排序不稳定。
- 时间复杂度:T(n)=O(n^2)。
//选择排序
public static void selectionSort(int[]arr){
for(int i=0;i<arr.length;i++){
int min=i;
for(int j=i+1;j<arr.length;j++){
if(arr[min]>arr[j]){
min=j;
}
}
if(i!=min){
int temp=arr[min];
arr[min]=arr[i];
arr[i]=temp;
}
}
}
- 堆排序
-
- 基本思想:堆排序是一种树形选择排序,是对直接选择排序的有效改进。 初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储序,使之成为一个堆,这时堆的根节点的数最大。然后将根节点与堆的最后一个节点交换。然后对前面(n-1)个数重新调整使之成为堆。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。
- 堆排序不稳定。
- 堆排序优于简单选择排序的原因:
-
- 直接选择排序中,为了从R[1..n]中选出关键字最小的记录,必须进行n-1次比较,然后在R[2..n]中选出关键字最小的记录,又需要做n-2次比较。事实上,后面的n-2次比较中,有许多比较可能在前面的n-1次比较中已经做过,但由于前一趟排序时未保留这些比较结果,所以后一趟排序时又重复执行了这些比较操作。
- 堆排序可通过树形结构保存部分比较结果,可减少比较次数。
- 堆排序的最坏时间复杂度为O(nlogn)。堆序的平均性能较接近于最坏性能。由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。
//堆排序
public static void heapSort(int[] arr) {
//第一步建堆
buildHeap(arr);
// 要注意每一轮的len是会-1的,heapify要把len传进去,不能用arr.length
for (int len = arr.length - 1; len >= 0; len--) {
int temp = arr[0];
arr[0] = arr[len];
arr[len] = temp;
heapify(arr, 0, len);
}
}
public static void buildHeap(int[] arr) {
int len = arr.length;
//建堆从最后一个有子节点的节点开始调整,一直到根节点
for (int i = len / 2 - 1; i >= 0; i--) {
heapify(arr, i, len);
}
}
public static void heapify(int[] arr, int i, int len) {
int temp = arr[i];
// i的左孩子等于2i+1. 左孩子+1是右孩子,选出较大的,再跟arr[i]比较,如果孩子比arr[i]大,就替换掉arr[i]
for (int child = getLeftChild(i); child < len; i = child, child = getLeftChild(i)) {
if (child + 1 != len && arr[child + 1] > arr[child]) {
child++;
}
if (arr[child] > temp) {
arr[i] = arr[child];
}
else {
break;
}
}
arr[i] = temp;
}
交换排序
-
- 冒泡排序
-
- 基本思想:在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
- 冒泡排序是一种稳定的排序方法。
-
- •若文件初状为正序,则一趟起泡就可完成排序,排序码的比较次数为n-1,且没有记录移动,时间复杂度是O(n)
- •若文件初态为逆序,则需要n-1趟起泡,每趟进行n-i次排序码的比较,且每次比较都移动三次,比较和移动次数均达到最大值∶O(n^2)
- •冒泡排序平均时间复杂度为O(n^2)
// 冒泡
public static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j + 1] < arr[j]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
- 快速排序
-
- 基本思想:(改进冒泡排序;分治法)选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟扫描,将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。
-
- 快速排序不稳定。
- 快速排序的时间复杂度为O(nlogn)。
- 当n较大时使用快排比较好,当序列基本有序时用快排反而不好。
// 快排 public static void quickSort(int[] arr) { quickSort(arr, 0, arr.length - 1); } public static void quickSort(int[] arr, int low, int high) { if (low >= high) return; int pivot = arr[low]; int i = low; int j = high; while (i < j) { while (i < j && arr[j] > pivot) { j--; } if (i < j) { arr[i] = arr[j]; i++; } while (i < j && arr[i] < pivot) { i++; } if (i < j) { arr[j] = arr[i]; j--; } } arr[i] = pivot; quickSort(arr, low, i - 1); quickSort(arr, i + 1, high); }
单链表实现快排:left、mid、right。递归left、right.最后合并起来
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ public class Solution { public ListNode sortList(ListNode head) { if(head == null || head.next == null) return head; ListNode left = new ListNode(0), leftHead = left; ListNode right = new ListNode(0), rightHead = right; ListNode mid = new ListNode(0), midHead = mid; int val = head.val; while(head != null) { if(head.val < val) { left.next = head; left = head; } else if(head.val > val) { right.next = head; right = head; } else { mid.next = head; mid = head; } head = head.next; } left.next = null; right.next = null; mid.next = null; return merge(sortList(leftHead.next), midHead.next, sortList(rightHead.next)); } public ListNode merge(ListNode left, ListNode mid, ListNode right) { ListNode leftTail = getTail(left); ListNode midTail = getTail(mid); midTail.next = right; if(leftTail != null) { leftTail.next = mid; return left; } else { return mid; } } public ListNode getTail(ListNode head) { if(head == null) return head; while(head.next != null) { head = head.next; } return head; } }
归并排序
-
- 基本思想:归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
- 归并排序是稳定的排序方法。
- 归并排序的时间复杂度为O(nlogn)。
- 速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。
public static void mergeSort(int[] arr) { mergeSort(arr, 0, arr.length - 1); } public static void mergeSort(int[] arr, int low, int high) { int mid = (low + high) / 2; if (low < high) { mergeSort(arr, low, mid); mergeSort(arr, mid + 1, high); mergeSortCore(arr, low, mid, high); } } public static void mergeSortCore(int[] arr, int low, int mid, int high) { int len = high - low + 1; int[] temp = new int[len]; int i = low; int j = mid + 1; int k = 0; while (i <= mid && j <= high) { if (arr[i] < arr[j]) { temp[k++] = arr[i++]; } else { temp[k++] = arr[j++]; } } while (i <= mid) { temp[k++] = arr[i++]; } while (j <= high) { temp[k++] = arr[j++]; } for (int p = 0; p < temp.length; p++) { arr[low + p] = temp[p]; } }
基数排序
-
- 基本思想:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
- 基数排序是稳定的排序算法。
- 基数排序的时间复杂度为O(d(n+r)),d为位数,r为基数。
桶排序
关于桶排序先做几点说明:
1)桶排序是稳定的;
2)桶排序是常见排序算法中最快的一种,大多数情况下比快排和归并排序还要快
3)桶排序非常快但是也非常消耗空间,典型的以空间换时间,基本上是最耗内存的一种排序算法。
桶排序中:无序数组有个要求,就是成员隶属于固定(有限的)的区间,如范围为0-9
例如待排数字[6 2 4 1 5 9]
准备10个空桶,最大数个空桶
public static void bucketSort(int[] arr){
int [] bucket=new int[10];
for(int i=0;i<arr.length;i++){
bucket[arr[i]]=arr[i];
}
int cur=0;
for(int i=0;i<bucket.length;i++){
if(bucket[i]!=0){
arr[cur++]=bucket[i];
}
}
}
一、稳定性:
-
- 稳定:冒泡排序、插入排序、归并排序和基数排序
- 不稳定:选择排序、快速排序、希尔排序、堆排序
二、平均时间复杂度
-
- O(n^2):直接插入排序,简单选择排序,冒泡排序。
- 在数据规模较小时(9W内),直接插入排序,简单选择排序差不多。当数据较大时,冒泡排序算法的时间代价最高。性能为O(n^2)的算法基本上是相邻元素进行比较,基本上都是稳定的。
- O(nlogn):快速排序,归并排序,希尔排序,堆排序。
- 其中,快排是最好的, 其次是归并和希尔,堆排序在数据量很大时效果明显。
三、排序算法的选择
-
- 1.数据规模较小
- (1)待排序列基本有序的情况下,直接插入排序;
- (2)对稳定性不作要求宜用简单选择排序,对稳定性有要求宜用插入或冒泡
- 2.数据规模不是很大
- (1)完全可以用内存空间,序列杂乱无序,对稳定性没有要求,快速排序,此时要付出log(N)的额外空间。
- (2)序列本身可能有序,对稳定性有要求,空间允许下,宜用归并排序
- 3.数据规模很大
- (1)对稳定性有要求,则可考虑归并排序。
- (2)对稳定性没要求,宜用堆排序
- 4.序列初始基本有序(正序),宜用直接插入,冒泡