zoukankan      html  css  js  c++  java
  • 排序

    排序

      排序是将一个无序的序列按照一定顺序排列成一个有序序列的操作。其中,升序排序是指序列按从小到大排序,降序排序是指序列按从大到小排序。

      若一个序列中存在相同的两个元素,在排序后:

        a. 若这两个元素先后顺序不变,则称该排序算法是稳定的。

        b. 若这两个元素先后顺序变化,则称该排序算法是不稳定的。

      例如:对于序列(56, 34, 47, 23, 66, 18, 82, 47)中有两个47,在第一个47下划线。

        若排序结果为(18, 23, 34, 47, 47, 56, 66, 82),即两个47的先后顺序不变,则该排序算法是稳定的;

        若排序结果为(18, 23, 34, 47, 47, 56, 66, 82),即两个47的先后顺序变化,则该排序算法是不稳定的。

      常见的排序算法有8种:冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、堆排序和基数排序。

      平均时间复杂度 最好情况 最坏情况 额外内存 稳定性
    冒泡排序 O(n2) O(n) O(n2) × √ 
    选择排序 O(n2) O(n2) O(n2) × ×
    插入排序 O(n2) O(n) O(n2) × √ 
    希尔排序 O(n log n) O(n log n) O(n log n) ×  × 
    快速排序 O(n log n) O(n log n) O(n2) ×  ×
    归并排序 O(n log n) O(n log n) O(n log n)
    堆排序 O(n log n) O(n log n) O(n log n) × ×
    基数排序 O(n) O(n) O(n)

    冒泡排序

      冒泡排序的思想是:将序列中相邻两个元素进行比较,若为逆序,则交换位置。所以,一趟冒泡排序可以将序列中最大(或最小)的元素排在序列最末端。

      若一个序列有n个元素,则需要进行(n - 1)趟冒泡排序。其中,第一趟冒泡排序需要比较(n - 1)次,第二趟冒泡排序需要比较(n - 2)次......也就是说,(n - 1)趟冒泡排序需要比较n * (n - 1) / 2次,最坏情况下需要交换n * (n - 1) / 2次。

      为了防止冒泡排序趟数过多,可以对冒泡排序进行优化。若一趟冒泡排序中没有进行过元素的交换,则表示该序列已经有序,无需再进行排序。所以可以设置一个标识位,用于标识当前冒泡排序是否交换过元素,若没有交换过,直接退出排序。

      冒泡排序的代码如下:

     1 public static void bubbleSort(int[] arr) {
     2     // 标识位
     3     boolean flag;
     4     for (int i = arr.length - 1; i >= 1; i--) {
     5         // 标识位初值置为false
     6         flag = false;
     7         for (int j = 0; j < i; j++) {
     8             // 相邻两个数进行比较
     9             if (arr[j] > arr[j + 1]) {   // 降序则为arr[j] < arr[j + 1]
    10                 int temp = arr[j];
    11                 arr[j] = arr[j + 1];
    12                 arr[j + 1] = temp;
    13                 // 若交换过元素,则标识位置为true
    14                 flag = true;
    15             }
    16         }
    17         // 如果标识位为false,表示当前冒泡排序没有交换过元素,即序列已经有序,直接退出
    18         if (! flag) break;
    19     }
    20 }
    bubbleSort

      例如:对序列(5, 8, 3, 4, 9, 2, 7, 6, 1)进行升序排序。

      第一趟冒泡排序的过程为:

      

      每一趟冒泡排序的结果为:

      

    选择排序

      选择排序的思想是:每次从序列中找到最大(或最小)的元素与最末端的元素交换位置。所以,一趟选择排序可以将序列中最大(或最小)的元素排在序列最末端。

      若一个序列有n个元素,则需要进行(n - 1)趟选择排序。其中,第一趟选择排序需要比较(n - 1)次,第二趟选择排序需要比较(n - 2)次......也就是说,(n - 1)趟选择排序需要比较n * (n - 1) / 2次。最坏情况下每一趟选择排序需要进行一次交换,(n - 1)趟选择排序需要进行(n - 1)次交换。

      选择排序的代码如下:

     1 public static void selectSort(int[] arr) {
     2     for (int i = arr.length - 1; i > 0; i--) {
     3         // 遍历序列找到最小元素所在的位标
     4         int index = i;
     5         for (int j = 0; j < i; j++) {
     6             if (arr[index] < arr[j]) index = j;   // 降序则为arr[index] > arr[j]
     7         }
     8         // index == i表示最小元素所在的位标刚好在最末端,无需交换
     9         if (index == i) continue;
    10         int temp = arr[index];
    11         arr[index] = arr[i];
    12         arr[i] = temp;
    13     }
    14 }
    selectSort

      例如:对序列(5, 8, 3, 4, 9, 2, 7, 6, 1)进行升序排序。

      第一趟选择排序的过程为:

      

      每一趟选择排序的结果为:

      

    插入排序

      插入排序的思想是:在序列前面的部分找到合适的位置插入元素,使得序列前面的部分变得有序。

      若一个序列有n个元素,则需要进行(n - 1)趟插入排序。在最坏的情况下,第一趟插入排序需要比较1次,移位1次,第二趟插入排序需要比较2次,移位2次......也就是说,在最坏的情况下,(n - 1)趟插入排序需要比较n * (n - 1) / 2次,移位n * (n - 1) / 2次。

      插入排序的代码如下:

     1 public static void insertSort(int[] arr) {
     2     for (int i = 1; i < arr.length; i++) {
     3         // 将当前元素作为哨兵
     4         int temp = arr[i];
     5         int index = i;
     6         while (index >= 1 && temp < arr[index - 1]) {   // 降序则为temp > arr[index - 1]
     7             // 若前一个当前元素比哨兵大,则后移
     8             arr[index] = arr[index - 1];
     9             index--;
    10         }
    11         // 找到合适的位置,将哨兵插入
    12         arr[index] = temp;
    13     }
    14 }
    insertSort

      例如:对序列(5, 8, 3, 4, 9, 2, 7, 6, 1)进行升序排序。

      第一趟插入排序的过程为:

      

      每一趟插入排序的结果为:

      

    希尔排序

      希尔排序是对插入排序的一种改进,其思想是:将序列划分按照增量d(初始值为序列长度的一半)划分成d个子序列,并对每个子序列进行插入排序,之后对增量d不断减半,重复这一过程直到d减少到1,对整个序列进行一次插入排序。

      希尔排序与插入排序的不同之处在于,插入排序每次对相邻元素进行比较,元素最多只移动了一个位置,而希尔排序每次对相隔较远距离(即增量d)的元素进行比较,使得元素移动时能跨过多个元素,实现宏观上的调整。当增量减小到1时,序列已基本有序,最后一趟希尔排序可以说是接近最好情况的直接插入排序。一般情况下,可将前面各趟希尔排序的调整看作是最后一趟希尔排序的预处理,看似多做了几趟插入排序,实际上比只做一次插入排序效率更高。

      希尔排序的代码如下:

     1 public static void shellSort(int[] arr) {
     2     // 增量d的初始值为序列长度的一半,之后每次增量d都减半
     3     for (int d = arr.length / 2; d > 0; d /= 2) {
     4         for (int i = d; i < arr.length; i++) {
     5             // 对每一个子序列进行插入排序
     6             int temp = arr[i];
     7             int j = i;
     8             while (j >= d && temp < arr[j - d]) {   // 降序则为temp > arr[j - d]
     9                 arr[j] = arr[j - d];
    10                 j -= d;
    11             }
    12             arr[j] = temp;
    13         }
    14     }
    15 }
    shellSort

      例如:对序列(5, 8, 3, 4, 9, 2, 7, 6, 1)进行升序排序。

      第一趟希尔排序的过程为:

      

      每一趟希尔排序的结果为:

      

    快速排序

      快速排序是对冒泡排序的一种改进,其思想是:从序列中选定一个元素作为枢轴,将其他元素与枢轴进行比较,分为比枢轴小和比枢轴大两个子序列,之后对每个子序列做同样的操作。

      快速排序的最好情况是,每一趟快速排序都能划分出同等大小的两个子序列;最坏情况是,每一趟快速排序取到的枢轴都是最值,此时快速排序就退化成冒泡排序。

      以第一个(或者最后一个)元素作为枢轴,当原序列已经有序时,每一趟快速排序取到的枢轴就都是序列中的最值,此时快速排序就退化成冒泡排序。

      快速排序的代码如下(取序列第一个元素作为枢轴):

     1 /**
     2  * @param l   序列第一个元素的位标
     3  * @param h  序列最后一个元素的位标
     4  */
     5 public static void quickSort(int[] arr, int l, int h) {
     6     // l < h,即h + 1 - l > 1,表示划分的子序列中至少有2个元素
     7     if (l < h) {
     8         // 定义low和high分别指向子序列的两端
     9         int low = l, high = h;
    10         // 取子序列的第一个元素作为枢轴
    11         int pivot = arr[low];
    12         // low == high时循环结束
    13         while (low < high) {
    14             // 先从high开始往前找,找到第一个比枢轴小的元素放在arr[low]处
    15             while (low < high && arr[high] >= pivot) high--;   // 降序则arr[high] <= pivot
    16             arr[low] = arr[high];
    17             // 之后从low开始往后找,找到第一个比枢轴大的元素放在arr[high]处
    18             while (low < high && arr[low] <= pivot) low++;   // 降序则arr[low] >= pivot
    19             arr[high] = arr[low];
    20         }
    21         // low和high相遇的位置即为枢轴应该放入的位置
    22         arr[low] = pivot;
    23         // 此时将序列划分为(l, low - 1)和(low + 1, h)两个子序列,再分别对两个子序列递归划分
    24         quickSort(arr, l, low - 1);
    25         quickSort(arr, low + 1, h);
    26     }
    27 }
    quickSort

      例如:对序列(5, 8, 3, 4, 9, 2, 7, 6, 1)进行升序排序。

      第一趟快速排序的过程为:

      

      每一趟快速排序的结果为:

      

      可以看到,在这个例子中,除了第一趟快速排序外,之后的每一趟排序都无法划分出两个子序列。

    归并排序

      归并排序的思想是:指序列递归分解成若干个长度大致相等的子序列,然后将各个子序列合并,并在合并时进行排序,最终合并为整体有序序列的过程。

      归并排序的代码如下:

     1 public static void mergeSort(int[] arr, int low, int high, int[] temp) {
     2     // low < high,即high + 1 - low > 1,表示序列中元素至少有2个元素
     3     if (low < high) {
     4         // 分组:将一个序列分为同等大小的两个子序列,并对子序列进行同样的操作
     5         int middle = (low + high + 1) / 2;
     6         mergeSort(arr, low, middle - 1, temp);
     7         mergeSort(arr, middle, high, temp);
     8         // 合并:将两个有序序列合并为一个有序序列
     9         int i = low, j = middle, k = 0;
    10         while (i < middle && j <= high) {
    11             if (arr[i] <= arr[j]) temp[k++] = arr[i++];   // 降序则arr[i] >= arr[j]
    12             else temp[k++] = arr[j++];
    13         }
    14         while (i < middle) temp[k++] = arr[i++];
    15         while (j <= high) temp[k++] = arr[j++];
    16         System.arraycopy(temp, 0, arr, low, k);
    17     }
    18 }
    mergeSort

      例如:对序列(5, 8, 3, 4, 9, 2, 7, 6, 1)进行升序排序。

      归并排序的过程为:

      

      

    堆排序

      堆排序是利用堆特性的一种选择排序,其思想是:先将原序列调整成堆(升序为大顶堆,降序为小顶堆),之后每一次将根结点与最后一个结点交换,将最后一个结点“取出”,剩余的结点再调整成堆。

      堆排序的代码如下:

     1 public static void adjust(int[] arr, int len, int i) {
     2     int temp = arr[i];
     3     while (i <= len / 2 - 1) {
     4         int j = i * 2 + 1;
     5         if (j + 1 < len && arr[j] < arr[j + 1]) j++;   // 降序则为arr[j] > arr[j + 1]
     6         if (temp >= arr[j]) break;   // 降序则为temp <= arr[j]
     7         arr[i] = arr[j];
     8         i = j;
     9     }
    10     arr[i] = temp;
    11 }
    adjust
     1 public static void heapSort(int[] arr) {
     2     // 将整个无序序列调整成堆,需要对所有非叶子结点进行调整,其中arr.length / 2 - 1为最后一个非叶子结点
     3     for (int i = arr.length / 2 - 1; i >= 0; i--) {
     4         adjust(arr, arr.length, i);
     5     }
     6     for (int i = arr.length - 1; i > 0; i--) {
     7         int temp = arr[i];
     8         arr[i] = arr[0];
     9         arr[0] = temp;
    10         adjust(arr, i, 0);
    11     }
    12 }
    heapSort

      例如:对序列(5, 8, 3, 4, 9, 2, 7, 6, 1)进行升序排序。

      将无序序列调整为大顶堆:

      

      第一趟堆排序的过程为:

      

      每一趟堆排序的结果为:

      

    基数排序

      基数排序的思路是:将元素看成由多个关键字组成,将所有元素按从低位关键字开始的每一位关键字进行排序。以数字为例,数字可以看作由个位数、十位数、百位数等组成,而每一位都是0-9的数字。先将序列中所有数字按个位数排序,再按十位数排序,......,最后按最高位排序。

      基数排序的趟数与序列中元素的个数无关,而与最大元素中关键字的个数有关。例如,一个数字序列中最大的数字有k位数,则对该序列排序时需要进行k趟基数排序。

      在一趟基数排序中,首先需要遍历所有元素,统计每个关键字中元素的个数,然后计算每个关键字的起始下标,最后再一次遍历所有元素,按照记录的起始下标将元素放入数组中。

      基数排序(数字序列)的代码如下:

     1 public static void radixSort(int[] arr) {
     2     // 找到最大的元素
     3     int max = arr[0];
     4     for (int i = 1; i < arr.length; i++) {
     5         if (max < arr[i]) max = arr[i];
     6     }
     7     // 获取排序的趟数
     8     int len = (max + "").length();
     9     // 创建辅助数组
    10     int[] temp = new int[arr.length];
    11     for (int k = 1, count = 1; count <= len; k *= 10, count++) {
    12         int[] pos = new int[10];
    13         // 记录每个关键字的元素个数
    14         for (int i : arr) pos[i / k % 10]++;
    15         // 计算每个关键字的起始位标
    16         pos[9] = arr.length - pos[9];   // 降序则为pos[0] = arr.length - pos[0];
    17         for (int i = 8; i >= 0; i--) pos[i] = pos[i + 1] - pos[i];   // 降序则为for (int i = 1; i <= 9; i++) pos[i] = pos[i - 1] - pos[i];
    18         // 将每个元素按起始位标插入序列中
    19         for (int i : arr) temp[pos[i / k % 10]++] = i;
    20         System.arraycopy(temp, 0, arr, 0, arr.length);
    21     }
    22 }
    radixSort

      例如,对(337, 332, 132, 267, 262, 164, 260, 167)进行升序排序。

      第一趟基数排序的过程为:

        a. 统计个位数中每个值的元素个数:个位数为0的有1个,个位数为2的有3个,个位数为4的有1个,个位数为7的有3个。

    0 1 2 3 4 5 6 7 8 9
    1 0 3 0 1 0 0 3 0 0

        b. 计算每个值的起始下标:个位数为0的从下标0开始,个位数为2的从下标1开始,个位数为4的从下标4开始,个位数为7的从下标5开始。

    0 1 2 3 4 5 6 7 8 9
    0 1 1 4 4 5 5 5 8 8

        c. 再次遍历序列,将所有元素插入到辅助数组中:元素插入后,对应位标加1。

          

      每一趟基数排序的结果为:

      

    排序算法效率测试

      随机生成一个有100000个元素的序列,其中元素的取值范围为[0, 100000),统计八种算法对该序列进行升序排序耗费的时间。

              

      大致上可以看出,对同一个序列进行升序排序,冒泡排序耗费的时间最大,而归并排序、堆排序、基数排序耗费的时间最短。

  • 相关阅读:
    git archive
    查看库的详细版本号,
    locks
    jquery中的DOM操作集锦
    Eclipse调试Bug的七种常用技巧
    eclipse如何修改dynamic web module version
    Eclipse导入到web项目没有run on server
    深入理解JavaScript系列
    10个好用的JQuery代码片段收集
    实现table样式的设定
  • 原文地址:https://www.cnblogs.com/lqkStudy/p/11300106.html
Copyright © 2011-2022 走看看