1.排序算法简介
将杂乱无章的数据元素,通过一定的方法按关键字顺序排列的过程叫做排序。假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。
比较类非线性时间排序:交换类排序(快速排序和冒泡排序)、插入类排序(简单插入排序和希尔排序)、选择类排序(简单选择排序和堆排序)、归并排序(二路归并排序和多路归并排序)。
非比较类线性时间排序:计数排序、基数排序、桶排序。
2.快速排序
该算法是不稳定算法,由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
算法步骤:
- 从数列中挑出一个元素,称为 “基准”(pivot),
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
算法的空间复杂度为:该算法的空间复杂度为O(logn),快速排序是递归进行的,需要栈的辅助,因此需要的辅助空间比前面几类排序方法要多。快速排序的效率和选取的“枢轴”有关,选取的枢轴越接近中间值,算法效率就越高,因此为了提高算法效率,可以在第一次选取“枢轴”时做文章,如在数据堆中随机选取3个值,取3个值的平均值作为“枢轴”,就如抽样一般。关于具体如何提高快速排序算法的效率
public static <T extends Comparable<? super T>> void quickSort(List<T> list) { quickSort(list, 0, list.size() - 1); } private static <T extends Comparable<? super T>> void quickSort( List<T> list, int left, int right) { if (left < right) { int pivot = partition(list, left, right); quickSort(list, left, pivot - 1); quickSort(list, pivot + 1, right); } } private static <T extends Comparable<? super T>> int partition( List<T> list, int left, int right) { T pivot = list.get(right); int i = left - 1; for (int j = left; j <= right - 1; j++) { if (list.get(j).compareTo(pivot) <= 0) { i++; Collections.swap(list, i, j); } } Collections.swap(list, i + 1, right); return i + 1; }
3.冒泡排序
该算法是一个稳定排序,算法思想:通过一系列的“交换”动作完成的,首先第一个记录与第二个记录比较,如果第一个大,则二者交换,否则不交换;然后第二个记录和第三个记录比较,如果第二个大,则二者交换,否则不交换,以此类推,最终最大的那个记录被交换到了最后,一趟冒泡排序完成。在这个过程中,大的记录就像一块石头一样沉底,小的记录逐渐向上浮动。冒泡排序算法结束的条件是一趟排序没有发生元素交换。
算法性能:最内层循环的元素交换操作是算法的基本操作。最坏情况,待排序列逆序,则基本操作的总执行次数为(n-1+1)*(n-1)/2=n(n-1)/2,其时间复杂度为O(n*n);最好情况,待排序列有序,则时间复杂度为O(n),因此平均情况下的时间复杂度为O(n*n)。算法的额外辅助空间只有一个用于交换的temp,所以空间复杂度为O(1)。
public static <T extends Comparable<? super T>> void bubbleSort(List<T> list) { for (int i = 0; i < list.size() - 1; i++) { for (int j = 1; j < list.size() - i; j++) { if (list.get(j - 1).compareTo(list.get(j)) > 0) { Collections.swap(list, j - 1, j); } } } }
4.插入排序
该算法是一个稳定排序,插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法步骤:
1)将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
2)从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。
算法性能:考虑最坏情况,即整个序列是逆序的,则其基本操作总的执行次数为n*(n-1)/2,其时间复杂度为O(n*n)。考虑最好情况,即整个序列已经有序,则循环内的操作均为常量级,其时间复杂度为O(n)。因此本算法平均时间复杂度为O(n*n)。算法所需的额外空间只有一个value,因此空间复杂度为O(1)。
public static <T extends Comparable<? super T>> void insertionSort(List<T> list) { T key; for(int j = 1; j < list.size(); j++) { key = list.get(j); int i = j - 1; while (i >= 0 && (list.get(i).compareTo(key) > 0)) { list.set(i + 1, list.get(i)); i--; list.set(i + 1, key); } } }
5.希尔排序
希尔排序(Shell Sort)是插入排序的一种,是针对直接插入排序算法的改进,是将整个无序列分割成若干小的子序列分别进行插入排序,希尔排序并不稳定。该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。
public static void shellSort(int[] data) { int j = 0; int temp = 0; // 每次将步长缩短为原来的一半 for (int increment = data.length >> 1; increment > 0; increment >>= 1) { for (int i = increment; i < data.length; i++) { temp = data[i]; for (j = i; j >= increment; j -= increment) { if (temp > data[j - increment])// 如想从小到大排只需修改这里 { data[j] = data[j - increment]; } else { break; } } data[j] = temp; } } }
6.选择排序
该算法是不稳定排序,算法思想:该算法的主要动作就是“选择”,采用简单的选择方式,从头至尾顺序扫描序列,找出最小的一个记录,和第一个记录交换,接着从剩下的记录中继续这种选择和交换,最终使序列有序。
算法性能:将最内层循环中的比较视为基本操作,其执行次数为(n-1+1)*(n-1)/2=n(n-1)/2,其时间复杂度为O(n*n),本算法的额外空间只有一个temp,因此空间复杂度为O(1)。
public static <T extends Comparable<? super T>> void selectionSort(List<T> list) { int currentSmallestIndex; for (int i = 0; i < list.size() - 1; i++) { currentSmallestIndex = i; for (int j = i + 1; j < list.size(); j++) { if (list.get(j).compareTo(list.get(currentSmallestIndex)) < 0) { currentSmallestIndex = j; } } Collections.swap(list, i, currentSmallestIndex); } }
7.堆排序
该算法是不稳定算法,算法思想:堆是一种数据结构,最好的理解堆的方式就是把堆看成一棵完全二叉树,这个完全二叉树满足任何一个非叶节点的值,都不大于(或不小于)其左右孩子节点的值。若父亲大孩子小,则这样的堆叫做大顶堆;若父亲小孩子大,这样的堆叫做小顶堆。根据堆的定义,其根节点的值是最大(或最小),因此将一个无序序列调整为一个堆,就可以找出这个序列的最大(或最小)值,然后将找出的这个值交换到序列的最后(或最前),这样有序序列元素增加1个,无序序列中元素减少1个,对新的无序序列重复这样的操作,就实现了序列排序。堆排序中最关键的操作是将序列调整为堆,整个排序的过程就是通过不断调整使得不符合堆定义的完全二叉树变为符合堆定义的完全二叉树的过程。
堆排序执行过程(大顶堆):
(1)从无序序列所确定的完全二叉树的第一个非叶子节点开始,从右至左,从下至上,对每个节点进行调整,最终将得到一个大顶堆。将当前节点(a)的值与其孩子节点进行比较,如果存在大于a值的孩子节点,则从中选出最大的一个与a交换。当a来到下一层的时候重复上述过程,直到a的孩子节点值都小于a的值为止。
(2)将当前无序序列中第一个元素,在树中是根节点(a)与无序序列中最后一个元素(b)交换。a进入有序序列,到达最终位置,无序序列中元素减少1个,有序序列中元素增加1个,此时只有节点b可能不满足堆的定义,对其进行调整。
(3)重复过程2,直到无序序列中的元素剩下1个时排序结束。
算法性能:完全二叉树的高度为[log(n+1)],即对每个节点调整的时间复杂度为O(logn),基本操作总次数是两个并列循环中基本操作次数相加,则整个算法时间复杂度为O(logn)*n/2+O(logn)*(n-1),即O(nlogn)。额外空间只有一个temp,因此空间复杂度为O(1)。
堆排序的优点是适合记录数很多的场景,如从1000000个记录中选出前10个最小的,这种情况用堆排序最好,如果记录数较少,则不提倡使用堆排序。另外,Hash表+堆排序是处理海量数据的绝佳组合,关于海量数据处理会在之后的博文中介绍到。
public static <T extends Comparable<? super T>> void heapSort(List<T> list) { buildMaxHeap(list); int heapSize = list.size(); for(int i = list.size() - 1; i >= 1; i--) { Collections.swap(list, i, 0); heapSize--; maxHeapify(list, 0, heapSize); } } private static <T extends Comparable<? super T>> void maxHeapify (List<T> list, int index, int heapSize) { int largest; //stores the index containing the largest element in list int leftChildIndex = 2 * index + 1; int rightChildIndex = 2 * index + 2; if (leftChildIndex < heapSize && list.get(leftChildIndex).compareTo(list.get(index)) > 0) { largest = leftChildIndex; } else { largest = index; } if (rightChildIndex < heapSize && list.get(rightChildIndex).compareTo(list.get(largest)) > 0) { largest = rightChildIndex; } if (largest != index) { Collections.swap(list, largest, index); maxHeapify(list, largest, heapSize); } } private static <T extends Comparable<? super T>> void buildMaxHeap(List<T> list) { for (int i = list.size() / 2; i >= 0; i--) { maxHeapify(list, i, list.size()); } }
8.归并排序
该算法是一个稳定排序,算法思想:其核心就是“两两归并”,首先将原始序列看成每个只含有单独1个元素的子序列,两两归并,形成若干有序二元组,则第一趟归并排序结束,再将这个序列看成若干个二元组子序列,继续两两归并,形成若干有序四元组,则第二趟归并排序结束,以此类推,最后只有两个子序列,再进行一次归并,即完成整个归并排序。
算法性能:可以选取“归并操作”作为基本操作,“归并操作”即为将待归并表中元素复制到一个存储归并结果的表中的过程,其次数为要归并的两个子序列中元素个数之和。算法总共需要进行logn趟排序,每趟排序执行n次基本操作,因此整个归并排序中总的基本操作执行次数为nlogn,即时间复杂度为O(nlogn),说明归并排序时间复杂度和初始序列无关。由于归并排序需要转存整个待排序列,因此空间复杂度为O(n)。
public static <T extends Comparable<? super T>> void mergeSort(List<T> list) { mergeSort(list, 0, list.size() != 0 ? list.size() - 1 : 0); } private static <T extends Comparable<? super T>> void mergeSort( List<T> list, int left, int right) { if (left == right) { return; } int mid = (right + left) / 2; mergeSort(list, left, mid); mergeSort(list, mid + 1, right); merge(list, left, mid, right); } private static <T extends Comparable<? super T>> void merge(List<T> list, int left, int middle, int right) { int leftLength = middle - left + 1; int rightLength = right - middle; List<T> leftList = new ArrayList<T>(); List<T> rightList = new ArrayList<T>(); for (int i = 0; i < leftLength; i++) { leftList.add(list.get(left + i)); } for (int j = 0; j < rightLength; j++) { rightList.add(list.get(middle + j + 1)); } int i = 0; int j = 0; for (int k = 0; k < right - left + 1; k++) { if (i >= leftList.size()) { list.set(left + k, rightList.get(j)); j++; } else if (j >= rightList.size()) { list.set(left + k, leftList.get(i)); i++; } else if (leftList.get(i).compareTo(rightList.get(j)) <= 0) { list.set(left + k, leftList.get(i)); i++; } else { list.set(left + k, rightList.get(j)); j++; } } }
9.计数排序
该算法是一个稳定算法,计数排序的时间按复杂度为O(n+k),因此k=O(n),即k远小于数据量n时,复杂度为O(n),用计数排序能够得到一个很好的效果。
算法的步骤如下:
- 找出待排序的数组中最大和最小的元素
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
public static List<Integer> countingSort(List<Integer> list) { return countingSort(list, LinearMaximum.findMax(list)); } private static List<Integer> countingSort(List<Integer> list, Integer maximum) { List<Integer> valueCounts = new ArrayList<Integer>(); List<Integer> sortedList = Arrays.asList(new Integer[list.size()]); for (int i = 0; i <= maximum; i++ ) { valueCounts.add(0); } for (int j = 0; j < list.size(); j++) { valueCounts.set(list.get(j), valueCounts.get(list.get(j)) + 1); } for (int i = 1; i <= maximum; i++) { valueCounts.set(i, valueCounts.get(i) + valueCounts.get(i - 1)); } for (int j = list.size() - 1; j >= 0; j--) { sortedList.set(valueCounts.get(list.get(j)) - 1, list.get(j)); valueCounts.set(list.get(j), valueCounts.get(list.get(j)) - 1); } return sortedList; }
10.桶排序
该算法是一个稳定排序,先定义桶,桶为一个数据容器,每个桶存储一个区间内的数。依然有一个待排序的整数序列A,元素的最小值不小于0,最大值不超过K。假设我们有M个桶,第i个桶Bucket[i]存储i*K/M至(i+1)*K/M之间的数。桶排序步骤如下:
- 扫描序列A,根据每个元素的值所属的区间,放入指定的桶中(顺序放置)。
- 对每个桶中的元素进行排序,什么排序算法都可以,例如插入排序。
- 依次收集每个桶中的元素,顺序放置到输出序列中。
public static void bucketSort(List<Integer> list) { try { bucketSort(list, LinearMaximum.findMax(list)); } catch (IllegalStateException e) {} } private static void bucketSort(List<Integer> list, Integer maximum) { List<List<Integer>> buckets = new ArrayList<List<Integer>>(); for (int i = 0; i < list.size(); i++) { buckets.add(new LinkedList<Integer>()); } for (Integer element : list) { int key = getBucket(element, list.size(), maximum); buckets.get(key).add(element); } for (List<Integer> bucket : buckets) { InsertionSort.insertionSort(bucket); } int index = 0; for (List<Integer> bucket : buckets) { while (bucket.isEmpty() == false) { list.set(index, bucket.remove(0)); index++; } } } private static int getBucket(int element, int listSize, int maxElement) { if (maxElement > listSize) { return (int)((10 * element * (double)listSize / maxElement) - 1) / 10; } else { return element!= 0 ? element - 1 : element; } }
11.基数排序
该算法是稳定排序,基数排序(radix sorting)将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。 然后 从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。具体过程可以参考动画演示。
假设我们有一些二元组(a,b),要对它们进行以a为首要关键字,b的次要关键字的排序。我们可以先把它们先按照首要关键字排序,分成首要关键字相同的若干堆。然后,在按照次要关键值分别对每一堆进行单独排序。最后再把这些堆串连到一起,使首要关键字较小的一堆排在上面。按这种方式的基数排序称为MSD(Most Significant Dight)排序。第二种方式是从最低有效关键字开始排序,称为LSD(Least Significant Dight)排序。首先对所有的数据按照次要关键字排序,然后对所有的数据按照首要关键字排序。要注意的是,使用的排序算法必须是稳定的,否则就会取消前一次排序的结果。由于不需要分堆对每堆单独排序,LSD方法往往比MSD简单而开销小。下文介绍的方法全部是基于LSD的。
基数排序的简单描述就是将数字拆分为个位十位百位,每个位依次排序。因为这对算法稳定要求高,所以我们对数位排序用到上一个排序方法计数排序。因为基数排序要经过d (数据长度)次排序, 每次使用计数排序, 计数排序的复杂度为 On), d 相当于常量和N无关,所以基数排序也是 O(n)。基数排序虽然是线性复杂度, 即对n个数字处理了n次,但是每一次代价都比较高, 而且使用计数排序的基数排序不能进行原地排序,需要更多的内存, 并且快速排序可能更好地利用硬件的缓存, 所以比较起来,像快速排序这些原地排序算法更可取。对于一个位数有限的十进制数,我们可以把它看作一个多元组,从高位到低位关键字重要程度依次递减。可以使用基数排序对一些位数有限的十进制数排序。
public class RadixSort { private static void radixSort(int[] array,int d) { int n=1;//代表位数对应的数:1,10,100... int k=0;//保存每一位排序后的结果用于下一位的排序输入 int length=array.length; int[][] bucket=new int[10][length];//排序桶用于保存每次排序后的结果,这一位上排序结果相同的数字放在同一个桶里 int[] order=new int[length];//用于保存每个桶里有多少个数字 while(n<d) { for(int num:array) //将数组array里的每个数字放在相应的桶里 { int digit=(num/n)%10; bucket[digit][order[digit]]=num; order[digit]++; } for(int i=0;i<length;i++)//将前一个循环生成的桶里的数据覆盖到原数组中用于保存这一位的排序结果 { if(order[i]!=0)//这个桶里有数据,从上到下遍历这个桶并将数据保存到原数组中 { for(int j=0;j<order[i];j++) { array[k]=bucket[i][j]; k++; } } order[i]=0;//将桶里计数器置0,用于下一次位排序 } n*=10; k=0;//将k置0,用于下一轮保存位排序结果 } } public static void main(String[] args) { int[] A=new int[]{73,22, 93, 43, 55, 14, 28, 65, 39, 81}; radixSort(A, 100); for(int num:A) { System.out.println(num); } }