[关闭]
@Sakura-W 2016-09-02T08:28:18.000000Z 字数 8481 阅读 1327

排序算法

数据结构与算法


一、概述

1.时间复杂度:
一般而言,对于一个排序,比较好的性能是O(NlogN),坏的性能是O(n^2),最理想的性能是O(n)。仅使用一个抽象关键比较运算的排序算法总平均上至少需要O(NlogN)。

2.稳定性:
稳定排序算法会让原本有相等键值的记录维持相对次序。也就是说,如果一个排序算法是稳定的,当有两个相等键值的记录和R和S,且在原本的列表中R出现在S之前,在排序过后的列表中R依然在S前面。

3.分类:

1)根据排序的数据是否全部在内存中:内部排序和外部排序。
2)根据排序方法的稳定性:稳定排序和不稳定排序
3)根据排序的方法:插入(插入排序、希尔排序)、选择(选择排序、堆排序)、交换(冒泡排序、快速排序)、合并(归并排序)等

4.内排序算法分类:

1)简单排序算法,时间复杂度为O(n^2):冒泡排序、插入排序和简单选择排序。性能比较:直接插入排序 > 简单选择排序 > 冒泡排序
2)改进算法,时间复杂度为O(nlogn):堆排序、希尔排序、归并排序和快速排序
3)非基于比较的算法,时间复杂度为O(n):计数排序、桶式排序和基数排序

image_1amgvaabh180t1o691paa1kjtah99.png-92.5kB

二、内排序

1.冒泡排序

1)说明:对当前还未排好序的数,自上而下对相邻两个数依次比较和调整,让较大的数往下沉,较小的数往上冒(故称为冒泡)。

2)实现:

  1. public static void bubbleSort(int[] a){
  2. int temp;//中间数
  3. for(int i = 0; i < a.length - 1; i++){//i表示交换的次数,总共交换a.length - 1次就足够了
  4. for(int j = 0; j < a.length - 1 - i; j++){
  5. if(a[j] > a[j+1]){//相邻两数交换,使得最大值往最后走
  6. temp = a[j];
  7. a[j] = a[j+1];
  8. a[j+1] = temp;
  9. }
  10. }
  11. }
  12. }

3)改进:设置标记,如果某趟排序过程中未发生任何交换,说明数据已经是有序的了,就不用再进行排序了

  1. public static void bubbleSort(int[] a){
  2. boolean flag = true;//设置标记
  3. int temp;
  4. for(int i = 0; i < a.length - 1 && flag; i++){
  5. flag = false;
  6. for(int j = 0; j < a.length - 1 - i; j++){
  7. if(a[j] > a[j+1]){
  8. temp = a[j];
  9. a[j] = a[j+1];
  10. a[j+1] = temp;
  11. flag = true;
  12. }
  13. }
  14. }
  15. }

2.简单选择排序

1)说明:选择排序的基本思想是每一趟在n-i+1个记录中选择关键字最小的记录作为序列的第i个记录。
简单选择排序比较的次数比较多,但移动数据的次数比较少,性能优于冒泡排序

2)实现:

  1. public static void selectSort(int[] a){
  2. int min, temp;//min表示最小记录的位置,temp为了暂存值
  3. for(int i = 0; i < a.length; i++){
  4. min = i;
  5. for(int j = i + 1; j < a.length; j++){
  6. if(a[min] > a[j]){
  7. min = j;
  8. }
  9. }
  10. if(min != i){
  11. temp = a[min];
  12. a[min] = a[i];
  13. a[i] = temp;
  14. }
  15. }
  16. }

3.直接插入排序

1)说明:基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数加1的有序表。性能优于冒泡排序和简单选择排序。

2)实现:

  1. public static void insertSort(int[] a){
  2. int temp, i, j;
  3. for(i = 1; i < a.length; i++){
  4. if(a[i] < a[i - 1]){//说明顺序不对,需要排序
  5. temp = a[i];
  6. for(j = i - 1; j >= 0 && a[j] > temp; j--){
  7. a[j + 1] = a[j];//逐个往后腾出位置
  8. }
  9. a[j+1] = temp;
  10. }
  11. }
  12. }

4.希尔排序

1)说明:也是利用插入进行排序,与直接插入排序不同的是:将关键字较小的记录跳跃式地往前移动,从而使得每次完成一轮循环后,整个序列就变得有序一些。希尔排序的关键并不是随便分组后各自排序,而是将相隔某个增量的记录组成一个子序列,实现跳跃式的移动,使得时间复杂度小于O(n^2)

2)实现:

  1. public static void shellSort(int[] a){
  2. int i, j, temp;
  3. int increment = a.length;
  4. do{
  5. increment = increment/3 + 1;//确定增量的大小
  6. for(i = increment; i < a.length; i++){
  7. if(a[i] < a[i - increment]){//这部分与插入排序的思想一致,只是插入排序的增量总是1
  8. temp = a[i];
  9. for(j = i - increment; j >= 0 && temp < a[j]; j -= increment){
  10. a[j+increment] = a[j];
  11. }
  12. a[j+increment] = temp;
  13. }
  14. }
  15. }while(increment > 1);
  16. }

5.堆排序

1)说明:堆是一种完全二叉树,其根结点一定是堆中所有结点最大值或者最小值。堆排序就是利用堆进行排序的方法,其基本思想是:将待排序的序列构造成一个大顶堆,此时整个序列的最大值就是堆顶的根结点,将它移走(与末尾元素交换,此时末尾元素就是最大值了),然后将剩余的n-1个元素重新构造成一个堆,如此循环下去,便能得到一个有序序列了。

2)实现:

  1. public class HeapSort {
  2. private static int[] sort = new int[]{1,0,10,20,3,5,6,4,9,8,12,17,34,11};
  3. public static void main(String[] args){
  4. System.out.println("Before Build: " + Arrays.toString(sort));
  5. buildMaxHeap(sort, sort.length);
  6. System.out.println("After Build: " + Arrays.toString(sort));
  7. heapSort(sort, sort.length);
  8. System.out.println("After Sort: " + Arrays.toString(sort));
  9. }
  10. public static void heapSort(int[] arr, int size){
  11. //构建最大堆
  12. buildMaxHeap(arr, size);
  13. //交换元素
  14. for(int i = size - 1; i > 0; i--){
  15. //将堆顶元素与数组最后一个元素交换
  16. swap(arr, 0, i);
  17. //重新构建大顶堆
  18. buildMaxHeap(arr, i);//这里不能是i - 1,与buildMaxHeap有关
  19. }
  20. }
  21. /**
  22. * 创建最大堆
  23. * @param arr
  24. * @param size
  25. */
  26. public static void buildMaxHeap(int[] arr, int size){
  27. for(int i = size / 2; i >= 0; i--){
  28. maxHeapify(arr, size, i);
  29. }
  30. }
  31. /**
  32. * 调整无序区(使其部分成为二叉堆)
  33. * @param arr
  34. * @param size
  35. * @param index
  36. */
  37. public static void maxHeapify(int[] arr, int size, int index){
  38. int leftSonIndex = 2*index + 1;
  39. int rightSonIndex = 2*index + 2;
  40. int temp = index;
  41. if(index <= size / 2){
  42. if(leftSonIndex < size && arr[temp] < arr[leftSonIndex]){
  43. temp = leftSonIndex;
  44. }
  45. if(rightSonIndex < size && arr[temp] < arr[rightSonIndex]){
  46. temp = rightSonIndex;
  47. }
  48. if(temp != index){
  49. swap(arr, index, temp);
  50. maxHeapify(arr, size, temp);
  51. }
  52. }
  53. }
  54. /**
  55. * 交换数组中两数的值
  56. * @param arr
  57. * @param i
  58. * @param j
  59. */
  60. public static void swap(int[] arr, int i, int j){
  61. int temp = arr[i];
  62. arr[i] = arr[j];
  63. arr[j] = temp;
  64. }
  65. }

3)评价:运行时间主要是消耗在初始构建堆和重建堆时的反复筛选上。时间复杂度为O(nlogn)。不适用于排序序列个数较少的情况

6.归并排序

1)说明:归并是指将两个或两个以上的有序表组合成一个有序表。归并排序利用归并的思想将无序序列中的元素两两排序后再合并,最终获得一个有序序列。归并排序利用了分治算法的思想。

2)实现:

  1. public class MergeSort {
  2. public static void main(String[] args) {
  3. int[] arr = {26, 5, 98, 108, 28, 99, 34, 56, 64, 1};
  4. mergeSort(arr);
  5. //打印排序好的数组
  6. for (int i : arr) {
  7. System.out.print(i + " ");
  8. }
  9. }
  10. /**
  11. * 对外接口:最终的排序方法
  12. * @param a
  13. */
  14. public static void mergeSort(int[] a){
  15. System.out.println("开始排序");
  16. sort(a, 0, a.length - 1);
  17. }
  18. /**
  19. * 递归地将数组分成小的片段
  20. * @param a
  21. * @param left
  22. * @param right
  23. */
  24. private static void sort(int[] a, int left, int right){
  25. if(left >= right){
  26. return;
  27. }
  28. int mid = (left + right) / 2;
  29. sort(a , left, mid);
  30. sort(a, mid + 1, right);
  31. merge(a, left, mid, right);
  32. }
  33. /**
  34. * 合并两个已经排序好的序列
  35. * @param a
  36. * @param left
  37. * @param mid
  38. * @param right
  39. */
  40. private static void merge(int[] a, int left, int mid, int right){
  41. //中间数组
  42. int[] temp = new int[a.length];
  43. int r1 = mid + 1;
  44. int tIndex = left;
  45. int cIndex = left;
  46. //把数组中一部分数据排序好
  47. while(left <= mid && r1 <= right){
  48. if(a[left] <= a[r1]){
  49. temp[tIndex] = a[left];
  50. tIndex++;
  51. left++;
  52. }else{
  53. temp[tIndex] = a[r1];
  54. tIndex++;
  55. r1++;
  56. }
  57. }
  58. //把左边或者右边还剩下的数据拷贝到新的数组中
  59. while(left <= mid){
  60. temp[tIndex] = a[left];
  61. tIndex++;
  62. left++;
  63. }
  64. while(r1 <= right){
  65. temp[tIndex] = a[r1];
  66. tIndex++;
  67. r1++;
  68. }
  69. //将排序好的数组拷贝到原来的数组
  70. while(cIndex <= right){
  71. a[cIndex] = temp[cIndex];
  72. cIndex++;
  73. }
  74. }
  75. }

3)评价:归并排序是一种比较占用内存,但效率高且稳定的算法。其时间复杂度为O(nlogn),空间复杂度为O(n + logn)

7.快速排序

1)说明:通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分的记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整体有序的目的。

2)实现:

  1. public class QuickSort {
  2. public static void main(String[] args) {
  3. int[] arr = {55, 5, 98, 108, 28, 99, 34, 56, 64, 1};
  4. qSort(arr, 0, 9);
  5. for (int i : arr) {
  6. System.out.print(i + " ");
  7. }
  8. }
  9. /**
  10. * 对外接口
  11. * @param arr
  12. */
  13. public static void quickSort(int[] arr){
  14. qSort(arr, 0, arr.length - 1);
  15. }
  16. /**
  17. * 分治、递归地对数组进行处理
  18. * @param arr
  19. * @param low
  20. * @param high
  21. */
  22. private static void qSort(int[] arr, int low, int high){
  23. if(low < high){
  24. //确定基准值
  25. int pivot = partition(arr, low, high);
  26. //递归排序左右数组
  27. qSort(arr, low, pivot - 1);
  28. qSort(arr, pivot + 1, high);
  29. }
  30. }
  31. /**
  32. * 依据基准值给数组大致划分为两部分,左边的数都小于基准值,右边的数都大于基准值
  33. * @param arr
  34. * @param low
  35. * @param high
  36. * @return
  37. */
  38. private static int partition(int[] arr, int low, int high){
  39. //把数组的第一个数当做基准值
  40. int pivot = arr[low];
  41. //将数组中大于和小于等于基准值的数据分开
  42. while(low < high){
  43. //从数组右侧开始,如果某数大于基准值,则high减小一位,如果数组小于或等于基准值,则跳过该步骤
  44. while(low < high && arr[high] > pivot){
  45. high--;
  46. }
  47. //如果数组右侧某值小于基准值,则将数值的值赋值给左侧相应位置
  48. if(low < high){
  49. arr[low] = arr[high];
  50. low++;
  51. }
  52. //从数组左侧开始,如果某数小于基准值,则low增加一位,如果大于基准值,则跳过该步骤
  53. while(low < high && arr[low] <= pivot){
  54. low++;
  55. }
  56. //如果数组右侧某值大于基准值,则将数值的值赋值给右侧相应位置
  57. if(low < high){
  58. arr[high] = arr[low];
  59. high--;
  60. }
  61. }
  62. //扫描完成,基准值到位
  63. arr[low] = pivot;
  64. //返回基准值的位置
  65. return low;
  66. }
  67. }

8.计数排序

1)说明:计数排序使用一个额外的数组C,其中第i个元素的值是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。
image_1ar4tfunn5sl2291f7p5kq174c9.png-35.1kB

2)实现:

  1. public static int[] countSort(int[] a, int k) {//k表示数组中最大值max+1
  2. //创建辅助数组
  3. int c[] = new int[k];
  4. //c[i]表示i的个数
  5. for (int i = 0; i < a.length; i++){
  6. c[a[i]]++;
  7. }
  8. //c[i]表示i的位置(从后面开始数,第一个i的位置)
  9. for (int i = 1; i < k; i++){
  10. c[i] += c[i-1];
  11. }
  12. //创建输出数组
  13. int b[] = new int[a.length];
  14. //扫描原始数组a,并将a[i]放到相应的位置
  15. for (int i = a.length-1; i >= 0; i--){
  16. //c[a[i]]表示从右边开始,a[i]所处的第一个位置,每次赋值后,c[a[i]]要减一
  17. b[--c[a[i]]] = a[i];
  18. }
  19. return b;
  20. }

改进:

  1. public static int[] countSort(int[] a){
  2. //最后排序后返回的数组
  3. int[] b = new int[a.length];
  4. //确定待排序的数组中的最大值和最小值,确定中间数组C的大小
  5. int max = a[0];
  6. int min = a[0];
  7. for (int i : a) {
  8. if(i > max){
  9. max = i;
  10. }else if(i < min){
  11. min = i;
  12. }
  13. }
  14. //创建中间数组的大小
  15. int k = max - min + 1;
  16. int[] c = new int[k];
  17. //确定C数组中某个位置的值的大小
  18. for(int i = 0; i < a.length; i++){
  19. c[a[i] - min] += 1;//改进的地方,不用创建很大的中间数组了
  20. }
  21. //确定C中某项的值(包括小于和等于其小标的所有值)
  22. for(int i = 1; i < c.length; i++){
  23. c[i] = c[i] + c[i - 1];
  24. }
  25. //按存取方式取出c中的元素,放入b数组中
  26. for(int i = a.length - 1; i >= 0; i--){
  27. b[--c[a[i] - min]] = a[i];
  28. }
  29. return b;
  30. }

3)评价:计数排序是用来排序0到100之间的数字的最好的算法,但它不适合按字母顺序排序人名。计数排序对于数据范围很大的数组,需要大量时间和内存,并不适用。

9.桶式排序

1)说明:扫描序列A,根据每个元素的值所在的区间,放入指定的桶中(顺序放置),然后对每个桶中的元素进行排序(什么排序算法都可以,比如插入排序),最后依次收集每个桶中的元素,顺序放置到输出序列中。

2)实现:

  1. public static void bucketSort(int[] arr){
  2. //确定数组中最大值和最小值
  3. int max = arr[0];
  4. int min = arr[0];
  5. for(int i = 0; i < arr.length; i++){
  6. if(max < arr[i]){
  7. max = arr[i];
  8. }else if(min > arr[i]){
  9. min = arr[i];
  10. }
  11. }
  12. //初始化桶(这里简单地用二维数组表示桶的集合,每个一维数组表示一个桶,实际上应该用链表来表示桶,因为元素的个数并不确定)
  13. int bucketCount = (max - min) / arr.length + 1;
  14. int[][] buckets = new int[bucketCount][arr.length];
  15. //每个桶的当前存储位置
  16. int[] order = new int[bucketCount];
  17. //把原始数组中的元素分配到各个桶中
  18. for(int i = 0; i < arr.length; i++){
  19. //k表示该数被分配到第几个桶
  20. int k = arr[i] / arr.length;
  21. buckets[k][order[k]] = arr[i];
  22. order[k]++;
  23. }
  24. //将各个桶中的数据排序
  25. for(int i = 0; i < bucketCount; i++){
  26. Arrays.sort(buckets[i]);
  27. }
  28. //按顺序,将一个一个桶中的元素打印出来
  29. for(int i = 0; i < bucketCount; i++){
  30. for(int j = 0; j < buckets[i].length; j++){
  31. if(buckets[i][j] != 0){
  32. System.out.print(buckets[i][j] + " ");
  33. }
  34. }
  35. }
  36. }

这个实现方法采用数组来表示桶,并不是太合适,最好能够使用一个链表表示一个桶,这样比较灵活。这个方法采用Java的Arrays类的静态搜索方法Arrays.sort()来对每个桶中的序列排序,主要为了方便.

10.基数排序

1)说明:桶排序都只是在研究一个关键字的排序,而基数排序能够对多个关键字进行排序。基数排序分为:MSD排序和LSD排序。MSD排序:先以主关键字排序,将序列分成主关键字相同的若干堆,再按照次关键字对每一堆单独排序;LSD排序:首先按照次关键字排序,然后按照首要关键字排序,要求是排序的算法必须是稳定的,否则会取消前一次排序的结果。

2)实现:

  1. public static void radixSort(int[] arr, int d){
  2. int n = 1;//表示位数,开始的时候是个位
  3. int k = 0;//下标,用于保存每一位的排序结果,用于下一位排序的输入
  4. //创建桶
  5. int[][] buckets = new int[10][arr.length];
  6. //创order数组表示当前桶的位置
  7. int[] order = new int[arr.length];
  8. while(n < d){
  9. //将待排序列分配到各个桶里
  10. for (int num : arr) {
  11. //表示该数应该存在第几个桶中
  12. int digit = (num / n) % 10;
  13. //order[digit]初始值为0
  14. buckets[digit][order[digit]] = num;
  15. order[digit]++;
  16. }
  17. //按顺序将桶中的序列赋值到原数组
  18. for(int i = 0; i < arr.length; i++){
  19. if(order[i] != 0){
  20. for(int j = 0; j < order[i]; j++){
  21. arr[k] = buckets[i][j];
  22. k++;
  23. }
  24. }
  25. //还原,便于下一次排序
  26. order[i] = 0;
  27. }
  28. //还原,便于下一次排序
  29. n *= 10;
  30. k = 0;
  31. }
  32. }

3)评价:计数排序、桶式排序和基数排序都是非基于比较的排序算法,而其时间复杂度依赖于数据的范围,桶排序还依赖空间的开销和数据的分布。基数排序是一种对多元组排序的有效方法,具体实现要用到计数排序或桶排序。

参考文献:
三种线性排序算法 计数排序、桶排序与基数排序
排序算法-维基百科

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注