ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

排序

2022-06-30 19:02:29  阅读:30  来源: 互联网

标签:arr int 复杂度 元素 数组 排序


1.1 概述

排序是将一组数据,依据指定的顺序进行排列的过程。

排序是算法中的一部分,也叫排序算法。算法处理数据,而数据的处理最好是要找到他们的规律,这个规律中有很大一部分就是要进行排序,所以需要有排序算法。

排序分为:内部排序和外部排序。

  • 内部排序:是将需要处理的所有数据加载到内存中进行排序;
  • 外部排序:当数据量过大,无法全部加载到内存中,需要借助外部存储(文件、磁盘等)进行排序。

常见的排序算法有:

  • 交换排序(冒泡排序、快速排序)
  • 选择排序(选择排序、堆排序)
  • 插入排序(插入排序、希尔排序)
  • 归并排序
  • 桶排序、 计数排序、基数排序

算法稳定性 -- 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面,则这个排序算法是稳定的。

学习排序,请参考动画演示:https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html

1.1.1 排序算法与复杂度归类

1、根据时间复杂度的不同,主流的排序算法可以分为3大类:

  1. 时间复杂度为O(n²)的排序算法:冒泡排序、选择排序、插入排序(冒选插)
  2. 时间复杂度为O(nlogn)的排序算法: 快速排序 、堆排序、希尔排序、归并排序 (快堆希归)
  3. 时间复杂度为线性O(n)的排序算法:桶排序、 计数排序、基数排序(桶计基)

各个排序算法比对表:

排序算法 时间复杂度 空间复杂度 是否稳定 比较次数
冒泡排序 平均、最差:O(n²) O(1) 稳定 未优化无序/有序:(n-1)²优化有序:n-1优化最差:n(n-1)/2
选择排序 平均、最差:O(n²) O(1) 不稳定 无序/有序固定:n(n-1)/2
插入排序 平均、最差:O(n²) O(1) 稳定 有序:n-1无序最差:n(n-1)/2
快速排序 平均、最差:O(nlogn) O(nlogn) 不稳定 属于交换排序
堆排序 平均、最差:O(nlogn) O(1) 不稳定 属于选择排序
希尔排序 平均:O(n^1.5)最好:O(nlogn) O(1) 不稳定 (n-1)^1.5nlogn
归并排序 平均、最差:O(nlogn) O(n) 稳定
桶排序 O(n) O(n) 稳定 适用于浮点数,适用于外部排序
计数排序 O(m+n) O(m) 0-10 m=10? 稳定 适合于连续的取值范围不大的数组
基数排序 平均、最差:O(d*n)d为位数 O(n) 稳定 处理海量数据排序时容易OutOfMemoryError

2、各种排序算法性能对比

  • 执行1万个随机数的排序,同一台机器相同条件下的测试结果:

    • 冒泡145ms、选择95ms、插入46ms
    • 快速2ms、堆2ms、希尔5ms、归并3ms
    • 计数1ms、基数2ms、桶17ms
    • Arrays.sort:4ms
    • Collections.sort:10ms
  • 执行5万个随机数的排序,同一台机器相同条件下的测试结果:

    • 冒泡4267ms、选择2282ms、插入1036ms
    • 快速9ms、堆9ms、希尔12ms、归并14ms
    • 计数3ms、基数7ms、桶38ms
    • Arrays.sort:11ms
    • Collections.sort:24ms
  • 执行10万个随机数的排序,同一台机器相同条件下的测试结果:

    • 冒泡16406ms、选择8633ms、插入4067ms
    • 快速16ms、堆16ms、希尔19ms、归并21ms
    • 计数6ms、基数11ms、桶49ms
    • Arrays.sort:19ms
    • Collections.sort:47ms
  • 排序效率排名:计数、基数 、快速、堆、希尔、归并、桶、插入、选择、冒泡

3、根据其稳定性,可以分为稳定排序和不稳定排序

  1. 稳定排序:值相同的元素在排序后仍然保持着排序前的顺序
  2. 不稳定排序:值相同的元素在排序后打乱了排序前的顺序。(选择、快速、堆、希尔)

1.1.2 如何分析一个“排序算法”?

1、算法的执行效率

  1. 最好、最坏、平均情况时间复杂度。
  2. 时间复杂度的系数、常数和低阶。
  3. 比较次数,交换(或移动)次数。

2、排序算法的稳定性

  1. 稳定性概念:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
  2. 稳定性重要性:可针对对象的多种属性进行有优先级的排序。
  3. 举例:给电商交易系统中的“订单”排序,按照金额大小对订单数据排序,对于相同金额的订单以下单时间早晚排序。用稳定排序算法可简洁地解决。先按照下单时间给订单排序,排序完成后用稳定排序算法按照订单金额重新排序。

3、排序算法的内存损耗

原地排序算法:特指空间复杂度是O(1)的排序算法。

4、常见的排序算法:冒泡排序、选择排序、插入排序、快速排序、堆排序、希尔排序、归并排序、桶排序、计数排序、基数排序。

1.2 冒泡排序(Bubble Sort)

1.2.1 概念

冒泡排序是最基础的排序算法。属于交换排序算法。

冒泡排序重复地遍历要排序的数组元素,一次比较两个元素,如果他们的顺序错误就把他们交换过来。重复地进行直到没有再需要交换,也就是说该数组已经排序完成。

这个算法的名字由来是因为越大的元素会经过交换慢慢“浮”到数列的顶端,故名“冒泡排序”。

按照冒泡排序的思想,把相邻的元素两两比较,当一个元素大于右侧相邻元素时,交换它们的位置;当一个元素小于或等于右侧相邻元素时,位置不变。

冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求,如果不满足就让它俩互换。

冒泡排序舞蹈视频:https://v.qq.com/x/page/e0379gl5jp1.html

1.2.2 冒泡排序的原理

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

0

经过第一轮后: 元素9作为数列中最大的元素,就像是汽水里的小气泡一样,“漂”到了最右侧。

0

每一轮结束都会有一个元素被移到最右侧。

0

1.2.2 冒泡排序的代码实现

package com.qf.sort;

import java.util.Arrays;

/**

* 冒泡排序

* 时间复杂度:O(n²)

*/

public class BubbleSort {

​ public static void main(String[] args) {

​ //定义一个整数类型数组,用于排序的原始数据

​ int[] arr = {5, 8, 6, 3, 9, 2, 1, 7};

​ System.out.println("冒泡排序:" + arr);

​ bubbleSort(arr);

​ //将排序之后的数组打印出来

​ System.out.println(Arrays.toString(arr));

​ }

​ /**

​ * 冒泡排序,未优化

​ */

​ public static void bubbleSort(int[] arr) {

​ for (int i = 0; i < arr.length - 1; i++) {

​ for (int j = 0; j < arr.length - 1; j++) {

​ //临时变量 用于交换

​ int tmp = 0;

​ if (arr[j] > arr[j + 1]) {

​ tmp = arr[j];

​ arr[j] = arr[j + 1];

​ arr[j + 1] = tmp;

​ }

​ }

​ }

​ }

}

1.2.3 冒泡排序的优化代码

为什么需要优化?

针对不同的数列,需要循环的轮数是有不同的。

例如:54321需要4轮循环之后才能排好序。而12345,当执行第一轮循环后,所有相邻的两个数值都无需换位,那说明排序正常,无需排序。不用执行后续的循环。

0

0

0

1、外层循环优化

0

第6轮已经可以结束了,也就是如果不需要交换了,则说明已经排好序了 思路:在外层循环处,设置标志isSort,默认为排好,如果不交换则跳出本次循环。

2、内层循环优化 已经被移到右侧的元素不用再参与比较了

package com.qf.sort;

import java.util.Arrays;

/**

* 冒泡排序

* 时间复杂度:O(n²)

*/

public class BubbleSort {

​ public static void main(String[] args) {

​ //定义一个整数类型数组,用于排序的原始数据

​ int[] arr = {5, 8, 6, 3, 9, 2, 1, 7};

​ System.out.println("冒泡排序:" + arr);

​ bubbleSort2(arr);

​ //将排序之后的数组打印出来

​ System.out.println(Arrays.toString(arr) + arr);

​ }

​ /*

​ 1、数组排序(冒泡排序)

​ 冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。

​ 它重复地遍历要排序的数组元素,一次比较两个元素,如果他们的顺序错误就把他们交换过来。重复地进行直到没有再需要交换,也就是说该数组已经排序完成。

​ 这个算法的名字由来是因为越大的元素会经过交换慢慢“浮”到数列的顶端,故名“冒泡排序”。

​ 2、冒泡排序算法的原理如下

​ 1、比较相邻的元素。如果第一个比第二个大,就交换他们两个。

​ 2、对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。

​ 3、针对所有的元素重复以上的步骤(除了最后一个)。

​ 4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

​ 3、注意事項:

​ 1、内循环次数:j < arr.length - 1 - i

​ 2、两轮for循环中间设置boolean flag = true;

​ */

​ public static void bubbleSort2(int[] arr) {

​ for (int i = 0; i < arr.length - 1; i++) {

​ //默认排好了

​ boolean isSort = true;

​ for (int j = 0; j < arr.length - 1 - i; j++) {

​ //临时变量 用于交换

​ int tmp = 0;

​ if (arr[j] > arr[j + 1]) {

​ // isSort=false表示还没有排好序,需要交换位置

​ // x=xy;y=xy;x=x^y;

​ isSort = false;

​ /*arr[j] = arr[j] ^ arr[j + 1];

​ arr[j + 1] = arr[j] ^ arr[j + 1];

​ arr[j] = arr[j] ^ arr[j + 1];*/

​ //a = a + b – (b = a);

​ arr[j] = arr[j] + arr[j + 1] - (arr[j + 1] = arr[j]);

​ }

​ }

​ //排好了跳出循环

​ if (isSort) {

​ break;

​ }

​ }

​ }

}

1.2.4 冒泡排序算法的时间复杂度

冒泡排序的时间复杂度是O(n²)。

1、当数组本身是有序的,优化的情况下,需要比较n-1次,时间复杂度是O(n)。

2、当数组是无序的,未优化,最坏的情况下需要比较(n-1)²次,所以时间复杂度是O(n²)。

1.3 选择排序(Selection Sort)

1.3.1 概念

选择排序将数组分成已排序区间和未排序区间。初始已排序区间为空。每次从未排序区间中选出最小的元素放到已排序区间的末尾,直到未排序区间为空。

1、原理:

每一趟从待排序的记录中选出最小的元素,顺序放在已排好序的序列最后,直到全部记录排序完毕。

2、简单选择排序的基本思想:

给定数组:int[] arr={n个数据};

第1趟排序,在待排序数据arr[1]arr[n]中选出最小的数据,将它与arrr[1]交换;第2趟,在待排序数据arr[2]arr[n]中选出最小的数据,将它与arr[2]交换;以此类推,第i趟在待排序数据arr[i]~arr[n]中选出最小的数据,将它与r[i]交换,直到全部排序完成。

1.3.2 选择排序算法逻辑

选择排序是一种最简单的排序算法。其排序的逻辑如下:

  1. 有一个待排序的数组A
  2. 从A中找出最小的元素。
  3. 将找到的最小元素跟数组A中第一个元素交换位置(如果最小元素就是第一个元素,则自己跟自己交换位置)。如下图:

0

(如上图,⻓方形高低代表数字的大小,找到最小的数字,跟第一个位置的数据进行交换)

交换之后,结果如下图所示:

0

4、然后,在剩下的4个数字中再找到最小的那个数字,跟第2个位置的数字交换。如下图:

0

交换之后的结果如下如:

0

5、再在剩下的三个数字中,找到最小的那个数字跟第3个位置的数字交换位置。上图中剩下的三个数字中最小的就是第3个位置的数字,所以,它自己跟自己交换位置,就是不变。同理第 四个数字也是不变,第5个数字也是不变。(上图中例子第3、4、5个元素正好就是对应的排 序,所以不变。如果不是对应的最小数字,同理交换位置就行。) 以上就是选择排序的算法逻辑。

1.3.3 选择排序算法的实现步骤

  1. 找出最小的数字
  2. 将最小的数字放到第一个位置
  3. 将第一个位置的数字,放到原本是最小数字的位置。
  4. 重复上面3个步骤

1.3.4 选择排序算法的代码实现

package com.qf.sort;

import java.util.Arrays;

/**

* 选择排序

* 时间复杂度:O(n²)

*/

public class SelectionSort {

​ public static void main(String[] args) {

​ //定义一个整数类型数组,用于排序的原始数据

​ int[] arr = {5, 8, 6, 3, 9, 2, 1, 7};

​ System.out.println("选择排序:");

​ selectionSort(arr);

​ System.out.println(Arrays.toString(arr));

​ }

​ /**

​ * 选择排序总共循环了所少次?

​ * n+(n-1)+(n-2)+(n-3)+...+1

​ * 高斯算法就是(n+1)n/2=(n^2+n)/ 2=1/2n2+n/2。当n->∞时,利用极限思维1/2*n2+n/2可以等于n²,记作O(n²)。

​ */

​ public static void selectionSort(int[] arr) {

​ //获取数组的大小

​ int length = arr.length;

​ //第一个循环用来遍历数组中的所有数字

​ for (int i = 0; i < length; i++) {

​ //初始化一个变量,用来记录最小数字的下标。初始默认假设第一个数字就是最小数字

​ int minIndex = i;

​ //第二个循环,通过比较获取数组中最小的数字的下标

​ for (int j = i + 1; j < length; j++) {

​ //如果找到更小的数字

​ if (arr[minIndex] >= arr[j]) {

​ //将minIndex变量的值修改为新的最小数字的下标

​ minIndex = j;

​ }

​ }

​ //所有数字一个个比较结束之后,就能确认那个数字最小了。

​ //将最小的数字替换到第一个位置,将第一个位置的数字放到最小数字原来的位置,就是一次交换。

​ arr[i] = arr[i] + arr[minIndex] - (arr[minIndex] = arr[i]);

​ }

​ }

}

1.3.5 选择排序算法的时间复杂度

对于选择排序算法,无论是排好序或未排序,都需要循环比较n(n-1)/2次。当n->∞时,等于n²,所以选择排序算法的时间复杂度为O(n²)。

1.3.6 选择排序算法的总结

选择排序是一种简单的排序算法,适用于数据量较小的情况,因为根据时间复杂度分析,数据量越大,选择排序所花费的时间按照平方倍数增⻓,会非常慢。

但是选择排序也有它的优势,选择排序的优势就是思维逻辑简单。

选择排序还有个特点,就是不论数组的顺序是排好序或者是乱序的,选择排序都需要花费一样的时间来计算。比如,利用选择排序对数组{1,2,3,4,5}和数组{3,1,4,2,5}排序所花费的时间是一样的。

1.4 插入排序(Insertion Sort)

1.4.1 概念

插入排序将数组数据分成已排序区间和未排序区间(有序区间和无序区间)。

初始已排序区间只有一个元素,即数组第一个元素。从未排序区间取出一个元素插入到已排序区间,新插入的元素要与已排序区间的数据一一比较大小,直到该元素找到合适的位置。陆续从未排序区间取出数据插入到已排序区间,直到未排序区间为空。

实际操作过程中,未排序区间的第一个元素与已排序区间的最后一个元素比较大小,如果大于最后一个数据则不换位置,直接加到已排序区间的末尾。如果数据小于已排序区间的最后一个数据,则需要换位,并且该数据要与已排序区间前面的数据一一比较大小,直到找到合适的位置。

1.4.2 插入排序的实现步骤

0

有一个待排序的数组:5、2、4、6、1、3。插入排序步骤如下:

1、初始时,有序区间中只有5,无序区间中有2、4、6、1、3。

将无序区间的2插入到有序区间,先比较2和5的大小。2比5小则交换位置。

比较后,有序区间中是2、5,无序区间中有4、6、1、3。

2、再将4加入到有序区间,比较4和5的大小。4比5小,则交换位置。此时有序区间是2、4、5,顺序正确。

如果顺序不正确,4还要与2比较,必须保证有序区间一定是排好序的。此时无序区间中有6、1、3。

3、再将6加入到有序区间,比较6和5的大小。6比5大,则无需交换位置,直接将6放在有序区间的末尾。此时有序区间是2、4、5、6,此时无序区间中有1、3。

4、再将1加入到有序区间,比较1和6的大小。1比6小,则交换位置。1与6交换位置后,有序区间的顺序还是不正确,需要继续调整,1再与之前的数据一一比较,直到找到合适的位置。此时有序区间是1、2、4、5、6,此时无序区间中有3。

5、再将3加入到有序区间,比较3和6的大小。3比6小,则交换位置。3与6交换位置后,有序区间的顺序还是不正确,需要继续调整,3再与之前的数据一一比较,直到找到合适的位置。此时有序区间是1、2、3、4、5、6,此时无序区间有空。

6、当无序区间为空,也就意味着排序结束。最终排序结果为:1、2、3、4、5、6。

1.4.3 插入排序和选择排序的区别

比如就上面这个例子而言,插入排序是将0从索引为4的位置移动到索引3、2、1、0,最终才算结束。而选择排序是找到最小的值0,直接跟1进行交换,0到1的位置,1到0的位置。

1.4.4 插入排序的代码实现

/**

* 插入排序

* 时间复杂度:O(n²)

* 插入排序的时间复杂度有两种:

* 1、当数组本身是有序的,则采用插入排序的时间复杂度是O(n)。

* 原因:如果数组本身是有序,插入排序需要每两个挨着的数字进行比较一次,总共比较n-1次, 所以时间复杂度是O(n)。

* 2、当数组是无序的,最坏的情况下需要比较n(n-1)/2次,所以时间复杂度是O(n²)。

*/

public static void insertionSort(int[] arr) {

​ int count = 0;

​ //对数组进行遍历

​ for (int i = 0; i < arr.length; i++) {

​ //第二个循环仅仅是将当前数据跟自己左边的数字进行比较,如果小于左边数字则交换位置,否则位置不变。

​ for (int j = i; j > 0; j--) {

​ count++;

​ if (arr[j] > arr[j - 1]) break;

​ // 前后两个数据交换位置

​ arr[j] = arr[j] + arr[j - 1] - (arr[j - 1] = arr[j]);

​ }

​ }

​ System.out.println("需要循环比较的次数:" + count);

}

以上插入排序的java代码实现,代码中的第二个for循环是重点,第二个for循环是只比较当前数据左边的值,如果比左边的值小则交换位置,否则位置不变。

1.4.5 插入排序的的时间复杂度

插入排序的时间复杂度有两种:

1、当数组本身是有序的,插入排序的时间复杂度是O(n)。

原因:如果数组本身是有序,插入排序需要每两个挨着的数字进行比较一次,总共比较n-1次, 所以时间复杂度是O(n)。

2、当数组是无序的,最坏的情况下需要比较n(n-1)/2次,所以时间复杂度是O(n²)。

1.4.6 插入排序的的总结

根据插入排序的时间复杂度来看,插入排序适合如下类型的数组:

1、数组中的每一个元素距离其最终的位置都不远。比如{1,0,2,3,4,5},这个数组中0最终位置 应该是第一个位置,0此时的位置距离第一个位置不远。

2、一个有序的大数组中融入一个小数组。比如有序大数组{1,2,3,4,5,6},融入一个小数组 {0,1}。

3、数组中只有几个元素的位置不正确。 上述这三种情况的数组适合使用插入排序算法。打过麻将的同学想想,打麻将过程中不停地摸牌、打牌、整理牌的过程是不是就是一次插入排序呢! 排序是算法的基础,排序的用途很多。

1.6 快速排序(Quick Sort)

1.6.1 概念

快速排序(Quick Sort)是对冒泡排序的一种改进。由霍尔(C. A. R. Hoare)在1962年提出。

同冒泡排序一样,快速排序也属于交换排序算法,通过元素之间的比较和交换位置来达到排序的目的。

1.6.2 算法思想

通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

快速排序与冒泡排序不同的是,冒泡排序在每一轮中只把1个元素冒泡到数列的一端。而快速排序每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样每次交换的时候就不会像冒泡排序一样只能在相邻的数之间进行交换,交换的距离就得到提升。

快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。这样总的比较和交换次数就少了,速度自然就提高了。

快速排序在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(n²),它的平均时间复杂度为O(nlogn)。

快速排序这种思路就就是分治法。

0

1.6.3 快速排序的实现步骤

快速排序一般基于递归实现。其步骤是这样的:

  • 1.选定一个合适的值(理想情况中值最好,但实现中一般使用数组第一个值),称为“基准元素”(pivot)。
  • 2.基于基准元素,将数组分为两部分,较小的分在左边,较大的分在右边。
  • 3.第一轮下来,这个基准元素的位置一定在最终位置上。
  • 4.对两个子数组分别重复上述过程,直到每个数组只有一个元素。
  • 5.排序完成。
  • 总结:快速排序,其实就是给基准元素找其正确索引位置的过程。

1、基准元素的选择

基准元素,英文是pivot,在分治过程中,以基准元素为中心,把其他元素移动到它的左右两边。

对于数组4、7、6、5、3、2、8、1来说,选择第一个元素4作为基准元素。

2、元素的交换

选定了基准元素以后,要做的就是把其他元素中小于基准元素的都交换到基准元素一边,大于基准元素的都交换到基准元素另一边。

快速排序算法有:双边循环法和单边循环法。

A、双边循环法

1、首先,选定基准元素pivot,并且设置两个指针left和right,指向数列的最左和最右两个元素。

0

  • 从right指针开始,让指针所指向的元素和基准元素做比较。
  • right指向的数据如果小于pivot,则right指针停止移动,切换到left指针。否则right指针向左移动。
  • 轮到left指针行动,让指针所指向的元素和基准元素做比较。
  • left指向的数据如果大于pivot,则left指针停止移动。否则left指针继续向右移动。
  • 左右指针指向的元素交换位置。

right指针当前指向的数据1小于pivot,right指针停止,轮到left指针移动。

由于left开始指向的是基准元素,所以left右移1位。

0

left右移一位是7,由于7>4,所以left指针停下。这时,left和right指针所指向的元素进行交换。

0

2、接下来重新切换到right指针,向左移动。right指针先移动到8,8>4,继续左移,指向到2。由于2<4,停止在2的位置。

0

第一轮下来数组为:3、1、2、4、5、6、8、7。本轮下来,本轮的基准元素4的位置就是最终排序完成应该放置的位置。

  1. 接下来,采用递归的方式分别对4之前的前半部分排序,再对4后面的后半部分排序。
  2. 前半部分:3、1、2,以3为基准元素进行排序。结果为:2、1、3。

本轮下来数组为:2、1、3、4、5、6、8、7。本轮的基准元素3的位置就是其最终索引位置。

  1. 元素2、1,以2为基准元素排序,结果为1、2。

本轮下来数组为:1、2、3、4、5、6、8、7。本轮的基准元素2的位置就是其最终索引位置。

  1. 左侧只剩下元素1,1就是自己的基准元素。

本轮下来数组为:1、2、3、4、5、6、8、7。本轮的基准元素1的位置就是其最终索引位置。

  1. 前半部分:5、6、8、7,以5为基准元素进行排序。结果为:5、6、8、7。

本轮下来数组为:1、2、3、4、5、6、8、7。本轮的基准元素5的位置就是其最终索引位置。

  1. 元素6、8、7,以6为基准元素进行排序。结果为:6、8、7。

本轮下来数组为:1、2、3、4、5、6、8、7。本轮的基准元素6的位置就是其最终索引位置。

  1. 元素8、7,以8为基准进行排序。结果为:7、8。

本轮下来数组为:1、2、3、4、5、6、7、8。本轮的基准元素8的位置就是其最终索引位置。

  1. 元素7,以7自身作为基准进行排序。

本轮下来数组为:1、2、3、4、5、6、7、8。本轮的基准元素7的位置就是其最终索引位置。

\9. 所有基准元素都找到其正确的索引位置,排序结束。

快速排序,其实就是给基准元素找其正确索引位置的过程。

每轮循环都是以基准元素找到合适位置结束。

0

B、单边循环法 单边循环法只从数组的一边对元素进行遍历和交换。

1、开始和双边循环法相似,首先选定基准元素pivot。同时,设置一个mark指针指向数列起始位置, 这个mark指针代表小于基准元素的区域边界。

0

接下来,从基准元素的下一个位置开始遍历数组。

如果遍历到的元素大于基准元素,就继续往后遍历

如果遍历到的元素小于基准元素,则需要做两件事:

第一,把mark指针右移1位,因为小于pivot的区域边界增大了1;

第二,让最新遍历到的元素和mark指针所在位置的元素交换位置,因为最新遍历的元素归属于小 于pivot的区域

首先遍历到元素7,7>4,所以继续遍历。

0

接下来遍历到的元素是3,3<4,所以mark指针右移1位。

0

随后,让元素3和mark指针所在位置的元素交换,因为元素3归属于小于pivot的区域。

0

按照这个思路,继续遍历,后续步骤如图所示:

0

1.6.4 快速排序的代码实现

/**

* 快速排序:双边循环法

*/

public class QuickSort {

​ public static void main(String[] args) {

​ int[] arr = new int[]{4, 7, 3, 5, 6, 2, 8, 1};

​ quickSort(arr, 0, arr.length - 1);

​ System.out.println(Arrays.toString(arr));

​ }

​ public static void quickSort(int[] arr, int startIndex, int endIndex) {

​ if (startIndex >= endIndex) {

​ return;

​ }

​ int pivotIndex = partition(arr, startIndex, endIndex);

​ // 根据基准元素,分成两部分进行递归排序

​ quickSort(arr, startIndex, pivotIndex - 1);

​ quickSort(arr, pivotIndex + 1, endIndex);

​ }

​ /**

​ * 分治(双边循环法)

​ *

​ * @param arr 待交换的数组

​ * @param startIndex 起始下标

​ * @param endIndex 结束下标

​ * @return

​ */

​ private static int partition(int[] arr, int startIndex, int endIndex) {

​ // 取第1个位置(也可以选择随机位置)的元素作为基准元素

​ int pivot = arr[startIndex];

​ int left = startIndex;

​ int right = endIndex;

​ while (left != right) {

​ while (left < right && arr[right] > pivot) {

​ right--;

​ }

​ //控制left指针比较并右移

​ while (left < right && arr[left] <= pivot) {

​ left++;

​ }

​ //交换left和right 指针所指向的元素

​ if (left < right) {

​ int p = arr[left];

​ arr[left] = arr[right];

​ arr[right] = p;

​ }

​ }

​ //pivot 和指针重合点交换

​ arr[startIndex] = arr[left];

​ arr[left] = pivot;

​ return left;

​ }

}

/**

* 快速排序:单边循环法

*/

public class QuickSort2 {

​ public static void main(String[] args) {

​ int[] arr = new int[]{4, 7, 3, 5, 6, 2, 8, 1};

​ quickSort(arr, 0, arr.length - 1);

​ System.out.println(Arrays.toString(arr));

​ }

​ public static void quickSort(int[] arr, int startIndex, int endIndex) {

​ // 递归结束条件:startIndex大于或等于endIndex时

​ if (startIndex >= endIndex) {

​ return;

​ }

​ // 得到基准元素位置

​ int pivotIndex = partition(arr, startIndex, endIndex);

​ // 根据基准元素,分成两部分进行递归排序

​ quickSort(arr, startIndex, pivotIndex - 1);

​ quickSort(arr, pivotIndex + 1, endIndex);

​ }

​ /**

​ * 分治(单边循环法)

​ *

​ * @param arr 待交换的数组

​ * @param startIndex 起始下标

​ * @param endIndex 结束下标

​ * @return

​ */

​ private static int partition(int[] arr, int startIndex, int endIndex) {

​ // 取第1个位置(也可以选择随机位置)的元素作为基准元素

​ int pivot = arr[startIndex];

​ int mark = startIndex;

​ for (int i = startIndex + 1; i <= endIndex; i++) {

​ if (arr[i] < pivot) {

​ mark++;

​ int p = arr[mark];

​ arr[mark] = arr[i];

​ arr[i] = p;

​ }

​ }

​ arr[startIndex] = arr[mark];

​ arr[mark] = pivot;

​ return mark;

​ }

}

1.6.5 快速排序的时间复杂度

快速排序的时间复杂度是:O(nlogn)

1.7 堆排序(Heap Sort)

1.7.1 概念

堆排序(Heap sort)是指利用堆这种数据结构所设计的一种排序算法。属于选择排序算法的一种。

1.7.2 堆的特性

堆是具有以下性质的完全二叉树

大顶堆:每个节点的值都大于或等于其左右孩子节点的值。

0

小顶堆:每个结点的值都小于或等于其左右孩子结点的值。

0

对堆中的结点按层进行编号,将这种逻辑结构映射到数组中:

0

该数组从逻辑上讲就是一个堆结构,用简单的公式来描述一下堆的定义就是:

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]

小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

1.7.3 堆排序的基本思想

将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。 将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得 到n个元素的次小值。如此反复执行,便能得到一个有序序列了。

1、构造初始堆

将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。

0

2、此时从最后一个非叶子节点开始(叶子节点自然不用调整,第一个非叶子节点 arr.length/2- 1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。

0

3、找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换

0

4、这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。

0

此时,我们就将一个无序序列构造成了一个大顶堆。

5、将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。将堆顶元素9和末尾元素4进行交换

0

重新调整结构,使其继续满足大顶堆

0

再将堆顶元素8与末尾元素5进行交换,得到第二大元素8

0

继续进行调整成大顶堆,顶部的6与右子树的4交换,最终使得整个序列有序。

1.7.3 堆排序的代码实现

/*

* 堆排序

* */

public class HeapSort {

​ public static void main(String[] args) {

​ System.out.println("排序前:" + Arrays.toString(arr));

​ //执行排序

heapSort(arr);

​ System.out.println("排序后:" + Arrays.toString(arr));

​ }

​ public static void heapSort(int[] arr) {

​ // 1. 把无序数组构建成大顶堆

​ for (int i = arr.length / 2 - 1; i >= 0; i--) {

​ adjustHeap(arr, i, arr.length);

​ }

//System.out.println("---" + Arrays.toString(arr));

​ // 2. 调整堆结构+交换堆顶元素与末尾元素,调整堆产生新的堆顶

​ for (int i = arr.length - 1; i > 0; i--) {

​ // 最后1个元素和第1个元素进行交换

​ arr[0] = arr[0] + arr[i] - (arr[i] = arr[0]);

​ // “下沉”调整成大顶堆

​ adjustHeap(arr, 0, i);

​ }

//System.out.println("===" + Arrays.toString(arr));

}

public static void adjustHeap(int[] arr, int parentIndex, int length) {

​ // curIndex 保存父节点值,用于最后的赋值

​ int curIndex = arr[parentIndex];

​ int childIndex = 2 * parentIndex + 1;

​ while (childIndex < length) {

​ // 如果有右孩子,且右孩子大于左孩子的值,则定位到右孩子

​ if (childIndex + 1 < length && arr[childIndex + 1] > arr[childIndex]) {

​ childIndex++;

​ }

​ // 如果父节点大于任何一个孩子的值,则直接跳出

​ if (curIndex >= arr[childIndex]) {

​ break;

​ }

​ //无须真正交换,单向赋值即可

​ arr[parentIndex] = arr[childIndex];

​ parentIndex = childIndex;

​ //下一个左孩子

​ childIndex = 2 * childIndex + 1;

​ }

​ arr[parentIndex] = curIndex;

}

}

1.7.4 堆排序的时间复杂度

堆排序的时间复杂度,最好、最坏、平均都是:O(nlogn)

堆排序是不稳定排序。

1.7 希尔排序(Shell's Sort)

1.7.1 概念

希尔排序(Shell's Sort)是插入排序的一种,又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序本身还不够高效,插入排序每次只能将数据移动一位。当有大量数据需要排序时,会需要大量的移位操作。
  • 但是插入排序在对几乎已经排好序的数据操作时,效率很高,几乎可以达到线性排序的效率。

所以,如果能对数据进行初步排列后,再用插入排序,那么就会大大提高效率。

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;当增量减至 1 时,整个文件恰被分成一组,算法便终止。

由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

1.7.2 希尔排序的基本思想

把元素按步长gap分组,对每组元素采用直接插入排序方法进行排序;

随着步长逐渐减小,所分成的组包含的元素越来越多;

当步长值减小到1时,整个数据合成一组,构成一组有序记录,完成排序;

0

1.7.3 希尔排序的代码实现

//希尔排序的交换法。交换法好理解,但是性能略低。

public static void shellSort(int[] arr) {

​ int loop = 0;

​ //gap:步长

​ for (int gap = arr.length / 2; gap > 0; gap /= 2) {

​ //对一个步长区间进行比较 [gap,arr.length)

​ for (int i = gap; i < arr.length; i++) {

​ //对步长区间中具体的元素进行比较

​ for (int j = i - gap; j >= 0; j -= gap) {

​ if (arr[j] > arr[j + gap]) {

​ //换位

​ arr[j] = arr[j] + arr[j + gap] - (arr[j + gap] = arr[j]);

​ }

​ }

​ }

​ System.out.println("希尔排序-第" + (++loop) + "轮结果为:" + Arrays.toString(arr));

​ }

}

1.7.4 希尔排序的优劣

不需要大量的辅助空间,和归并排序一样容易实现。

希尔排序是基于插入排序的一种算法, 在此算法基础之上增加了一个新的特性,提高了效率。

希尔排序的时间的时间复杂度为O(n^1.5),希尔排序时间复杂度的下界是n*logn。

希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O(n²)复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。 此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,而快速排序在最坏的情况下执行的效率会变差。几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法。

本质上讲,希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。

原因是,当n值很大时数据项每一趟排序需要移动的个数很少,但数据项的距离很长。当n值减小时每一趟需要移动的数据增多,此时已经接近于它们排序后的最终位置。 正是这两种情况的结合才使希尔排序效率比插入排序高很多。

Shell算法的性能与所选取的分组长度序列有很大关系。

1.7.5 希尔排序的时间复杂度

希尔排序的时间复杂度与增量(即步长gap)的选取有关。例如,当增量为1时,希尔排序退化成了直接插入排序,此时的时间复杂度为O(n²),而具有增量的希尔排序的时间复杂度为O(n^3/2)。

  • 平均:O(n^1.5)
  • 下限:O(nlogn)

1.7.6 希尔排序稳定性

希尔排序是不稳定的算法,它满足稳定算法的定义。对于相同的两个数,可能由于分在不同的组中而导致它们的顺序发生变化。

算法稳定性 -- 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!

1.8 归并排序(Merge Sort)

1.8.1 概念

归并排序是一类与插入排序、交换排序、选择排序不同的另一种排序方法。

归并排序是采用分治法的一个非常典型的应用。归并排序的思想就是先递归分解数组,再合并数组。

将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。

归并排序有多路归并排序、两路归并排序 , 可用于内排序,也可以用于外排序。

1.8.2 两路归并排序算法的思路

分而治之(divide - conquer)。

每个递归过程涉及三个步骤:

  1. 第一, 分解: 把待排序的 n 个元素的序列分解成两个子序列, 每个子序列包括 n/2 个元素;
  2. 第二, 治理: 对每个子序列分别调用归并排序MergeSort,进行递归操作;
  3. 第三, 合并: 合并两个排好序的子序列,生成排序结果。

0

归并排序图片演示:

https://www.icode9.com/i/?i=20180607002713740

1.8.2 归并排序的代码实现

/**

* 归并排序

*/

public class MergeSort {

​ public static void main(String[] args) {

​ int[] arr = new int[]{4, 7, 3, 5, 6, 2, 8, 1};

​ mergeSort(arr, 0, arr.length - 1);

​ System.out.println(Arrays.toString(arr));

​ }

​ public static int[] mergeSort(int[] a, int low, int high) {

​ int mid = (low + high) / 2;

​ if (low < high) {

​ mergeSort(a, low, mid);

​ mergeSort(a, mid + 1, high);

​ //左右归并

​ merge(a, low, mid, high);

​ }

​ return a;

​ }

​ public static void merge(int[] a, int low, int mid, int high) {

​ int[] temp = new int[high - low + 1];

​ int i = low;

​ int j = mid + 1;

​ int k = 0;

​ // 把较小的数先移到新数组中

​ while (i <= mid && j <= high) {

​ if (a[i] < a[j]) {

​ temp[k++] = a[i++];

​ } else {

​ temp[k++] = a[j++];

​ }

​ }

​ // 把左边剩余的数移入数组

​ while (i <= mid) {

​ temp[k++] = a[i++];

​ }

​ // 把右边边剩余的数移入数组

​ while (j <= high) {

​ temp[k++] = a[j++];

​ }

​ // 把新数组中的数覆盖nums数组

​ for (int x = 0; x < temp.length; x++) {

​ a[x + low] = temp[x];

​ }

​ }

}

1.8.3 归并排序的时间复杂度

归并排序的时间复杂度是:O(nlogn)

1.8.4 归并排序总结

  1. 稳定性:归并排序是一种稳定的排序。
  2. 存储结构要求:可用顺序存储结构,也易于在链表上实现。
  3. 时间复杂度:对长度为n的文件,需进行二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlogn)。
  4. 空间复杂度:需要一个辅助向量来暂存两有序子文件归并的结果,故其辅助空间复杂度为O(n),显然它不是就地排序。

1.9 桶排序(Bucket Sort)

1.9.1 概念

桶排序,顾名思义会用到“桶" 。核心思想是将要排序的数据分到几个有序的桶里,每个桶里的数据单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。

桶排序同样是一种线性时间的排序算法。桶排序需要创建若干个桶来协助排序,每一个桶(bucket)代表一个区间范围,里面可以承载一个或多个元素。

除了对一个桶内的元素做链表存储,也有可能对每个桶中的元素继续使用其他排序算法进行排序,所以更多时候,桶排序会结合其他排序算法一起使用。

桶排序对排序的数据要求苛刻:

1、要排序的数据需要很容易就能划分成m个桶,并且桶与桶之间有着天然的大小顺序;

2、数据在各个桶之间的分布是比较均匀的;

3、桶排序比较适合用在外部排序中。

所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。

1.9.2 桶排序的步骤

1、桶排序的第1步,就是创建这些桶,并确定每一个桶的区间范围具体需要建立多少个桶,如何确定桶的区间范围,有很多种不同的方式。我们这里创建的桶数量等于原始数列的元素数量,除最后一个桶只包含数列最大值外, 前面各个桶的区间按照比例来确定。

区间跨度 = (最大值-最小值)/ (桶的数量 - 1)

假设有一个非整数数列如下:4.5、0.84、3.25、2.18、0.5

桶的个数为5,计算出区间跨度为1。

    • 序列:4.5、0.84、3.25、2.18、0.5
    • 区间跨度 = (最大值-最小值)/(桶的数量-1)
    • 区间跨度 = 1.0
    • 0.5-1.5 (0号桶中:0.84, 0.5)
    • 1.5-2.5 (1号桶中:2.18)
    • 2.5-3.5 (2号桶中:3.25)
    • 3.5-4.5 (3号桶中:空)
    • 4.5-5.5 (4号桶中:4.5)

如何计算当前数据属于某个区间,或者说在哪个桶里?

    • 桶编号 = (int) ((元素数值 - 最小值) / 区间跨度);

0

2、第2步,遍历原始数列,把元素对号入座放入各个桶中。

0

3、第3步,对每个桶内部的元素分别进行排序(显然,只有第1个桶需要排序)

0

4、第4步,遍历所有的桶,输出所有元素

0.5、0.84、2.18、3.25、4.5

1.9.3 桶排序的代码实现

/**

* 序列:4.5、0.84、3.25、2.18、0.5

* 区间跨度 = (最大值-最小值)/(桶的数量-1)

* 区间跨度 = 1.0

* 0.5-1.5 (0号桶中:0.84, 0.5)

* 1.5-2.5 (1号桶中:2.18)

* 2.5-3.5 (2号桶中:3.25)

* 3.5-4.5 (3号桶中:空)

* 4.5-5.5 (4号桶中:4.5)

* 判断当前数据在某个区域的公式:

* 桶编号 = (int) ((元素数值 - 最小值) / 区间跨度);

*/

public static void bucketSort(double[] arr) {

​ double max = arr[0];

​ double min = arr[0];

​ //获得最大值和最小值之间的差

​ for (int i = 0; i < arr.length; i++) {

​ if (arr[i] > max) max = arr[i];

​ if (arr[i] < min) min = arr[i];

​ }

​ double diff = max - min;

​ //桶初始化

​ int bucketCount = arr.length;

​ ArrayList<LinkedList> buckets = new ArrayList<>(bucketCount);

​ for (int i = 0; i < bucketCount; i++) {

​ buckets.add(new LinkedList<>());

​ }

​ // 将每个元素放入桶中

​ // 区间跨度 = (最大值-最小值)/(桶的数量-1)

​ double span = diff / (bucketCount - 1);

//System.out.println("----区间跨度为:" + span);

​ for (int i = 0; i < arr.length; i++) {

​ // 获取当前数据应该放在哪个区间内,也就是获取桶的编号

​ // 桶编号 = (int) ((元素数值 - 最小值) / 区间跨度);

​ int num = (int) ((arr[i] - min) / span);

//System.out.println("---桶编号:" + num);

​ buckets.get(num).add(arr[i]);

​ }

​ System.out.println("===" + buckets);

​ //对每个桶内部进行排序

​ for (int i = 0; i < buckets.size(); i++) {

​ //对链表进行排序

​ Collections.sort(buckets.get(i));

​ }

​ // 将数据回填到原数组中

​ int index = 0;

​ for (LinkedList list : buckets) {

​ for (double element : list) {

​ arr[index++] = element;

​ }

​ }

}

//测试Collections.sort排序

public static void test() {

​ LinkedList list = new LinkedList();

​ list.add(0.98);

​ list.add(0.5);

​ list.add(0.23);

​ list.add(3.0);

​ list.add(0.03);

​ list.add(2.05);

​ Collections.sort(list);

​ System.out.println(list);

}

1.9.4 桶排序的时间复杂度

平均时间复杂度:O(n + k)

最佳时间复杂度:O(n + k)

最差时间复杂度:O(n ^ 2)

空间复杂度:O(n * k)

稳定性:稳定

桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。

桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

1.10 计数排序(Count Sort)

1.10.1 概念

计数排序,这种排序算法是利用数组下标来确定元素的正确位置的。

计数排序是基本的桶排序。

定义n个桶,每个桶一个编号,数据放到相应编号的桶中。定义一个数组,数组索引表示桶的编号,索引值就是存放的数值。如果该值为1说明只出现一次,如果大于1,说明重复多次出现。

计数排序是典型的空间换时间的算法。

1、假设数组中有10个整数,取值范围为0~10,要求用最快的速度把这10个整数从小到大进行排序。 可以根据这有限的范围,建立一个长度为10的数组。数组下标从0到9,元素初始值全为0。

0

假设数组数据为:7 3 2 1 9 6 5 4 3 8

下面就开始遍历这个无序的随机数列,每一个整数按照其值对号入座,同时,对应数组下标的元素进行加1操作。

最终,当数列遍历完毕时,数组的状态如下:

0

该数组中每一个下标位置所对应的值,其实就是数列中对应整数出现的次数。

直接遍历数组,输出数组元素的下标值,元素的值就是输出的次数。0不输出。

输出: 1 2 3 3 4 5 6 7 8 9

2、如果起始数不是从0开始,比如以下数列:

95,94,91,98,99,90,99,93,91,92 ,数组起始数为90。此时可以采用偏移量的方式来排序。

0

数组遍历完毕,数组的状态如下:

0

原本输出:0 1 1 2 3 4 5 8 9 9

增加上偏移量90后,实际应该顺序输出为:90 91 91 92 93 94 95 98 99 99

1.10.2 计数排序的应用场景

  1. 计数排序适合于连续的取值范围不大的数组。不连续和取值范围过大会造成数组过大。
  2. 计数排序只能用在数据范围不大的场景中,如果数据范围k比要排序的数据n大很多,就不适合用计数排序了。
  3. 计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。

1.10.3 计数排序的代码实现

/**

* 计数排序:

* 是最基本的桶排序。

* 计数排序适合于连续的取值范围不大的数组。不连续和取值范围过大会造成数组过大。

* 计数排序只能用在数据范围不大的场景中,如果数据范围k比要排序的数据n大很多,就不适合用计数排序了。

* 计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。

* 计数排序的时间复杂度是O(n+m)

* n: 数据个数

* m: 数据范围

*/

public class CountSort {

​ public static void main(String[] args) {

​ //获取成绩

​ int[] arr = makeRandomArr(10, 100, 60);

​ System.out.println("原始数据:" + Arrays.toString(arr));

​ countSort2(arr);

​ System.out.println("排序后:" + Arrays.toString(arr));

​ }

​ /**

​ * 全班50个人,最低成绩60分,最高成绩100分。将这些成绩进行排序。

​ * 随机生成50个成绩。

​ */

​ public static int[] makeRandomArr(int count, int max, int min) {

​ int[] arr = new int[count];

​ for (int i = 0; i < arr.length; i++) {

​ //(int) (Math.random()*(max-min)+min);

​ arr[i] = (int) (Math.random() * (max - min) + min);

​ }

​ return arr;

​ }

​ // 无偏移量的排序

​ public static void countSort(int[] arr, int n) {

​ //定义n个桶,每个桶一个编号,数据放到相应编号的桶中。

​ //数组索引表示桶的编号,索引值就是数据的值。

​ // 如果该值为1说明只出现一次,如果大于1,说明重复多次出现。

​ int[] buckets = new int[n];

​ for (int i = 0; i < arr.length; i++) {

​ ++buckets[arr[i]];

​ }

​ System.out.println("桶数据为:" + Arrays.toString(buckets));

​ // 将数据回填到原数组中

​ int index = 0;

​ for (int i = 0; i < buckets.length; i++) {

​ if (buckets[i] > 0) {

​ for (int j = 0; j < buckets[i]; j++) {

​ arr[index++] = i;

​ }

​ }

​ }

​ }

​ /**

​ * 带有偏移量的排序

​ * 偏移量就是待排序数列的最小值。桶个数就是最大值和最小值的差值 + 1.

​ * 计数排序,这种排序算法是利用数组下标来确定元素的正确位置的。

​ * 定义n个桶,每个桶一个编号,数据放到相应编号的桶中。

​ * 定义一个数组,数组索引表示桶的编号,索引值就是存放的数值。

​ * 如果该值为1说明只出现一次,如果大于1,说明重复多次出现。

​ */

​ public static void countSort2(int[] arr) {

​ //求取最大值和最小值的差值,差值+1就是桶的个数

​ int max = arr[0];

​ int min = arr[0];

​ for (int i = 1; i < arr.length; i++) {

​ if (arr[i] > max) max = arr[i];

​ if (arr[i] < min) min = arr[i];

​ }

​ // 桶的个数 = 差值+1

​ int[] buckets = new int[max - min + 1];

​ System.out.println(buckets.length);

​ for (int i = 0; i < arr.length; i++) {

​ //桶编号 = 实际数值 - 最小数值

​ int n = arr[i] - min;

​ ++buckets[n];

​ }

​ System.out.println("桶数据为:" + Arrays.toString(buckets));

​ int[] nums2 = new int[arr.length];

​ // 将数据回填到原数组中

​ int index = 0;

​ for (int i = 0; i < buckets.length; i++) {

​ if (buckets[i] > 0) {

​ for (int j = 0; j < buckets[i]; j++) {

​ // 实际数值 = 桶编号 + 最小数值

​ arr[index++] = i + min;

​ }

​ }

​ }

​ }

}

1.10.3 计数排序的时间复杂度

计数排序的时间复杂度是O(n+m)

  • n: 数据个数
  • m: 数据范围

1.11 基数排序(Radix Sort)

1.11.1 概念

基数排序是桶排序的扩展。是1887年赫尔曼.何乐礼发明的。基数排序的基本实现原理是:将整数按位切割成不同的数字,然后按位数进行比较。

基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。

基数排序是经典的空间换时间的算法。占用内存很大,当对海量数据排序时,容易造成OutOfMemoryError。

1.11.2 基本解法

第一步:有一串数值如下所示:64, 32, 90,76, 11,93, 85, 44, 18, 21, 65, 89, 57,11

首先根据个位数的数值,将它们分配至编号0到9的桶子中:

0

第二步:将这些桶子中的数值重新串接起来,成为的数列为:90, 11, 21, 11, 32, 93, 64, 44, 85, 65, 76, 57, 18, 89

接着再进行一次分配,这次是根据十位数来分配:

0

第三步:将这些桶子中的数值重新串接起来,成为的数列为:11, 11, 18, 21, 32, 44, 57, 64, 65, 76, 85, 89, 90, 93

这时候整个数列已经排序完毕。

如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。

1.11.3 基数排序的两种实现方式

基数排序的实现,有两种方式。

  • 低位优先法,适用于位数较小的数排序,简称LSD。
  • 高位优先法,适用于位数较多的情况,简称MSD。

LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好。

MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。在进行完最低位数的分配后再合并回单一的数组中。

1.11.4 基数排序的代码实现

/**

* 基数排序

* 基数排序的实现,有两种方式,低位优先法,适用于位数较小的数排序 简称LSD(最低有效位Least Significant Digit)

* 高位优先法,适用于位数较多的情况,简称MSD(最高有效位most significant digit)

* 这里实现低位优先法

*/

public static void radixSort(int[] arr) {

​ //求当前数组中的最大数,其位数就是需要重复操作的轮数

​ int max = arr[0];

​ for (int i = 1; i < arr.length; i++) {

​ if (arr[i] > max)

​ max = arr[i];

​ }

​ // 存贮最大的数的位数,用来判断需要进行几轮基数排序

​ int maxLen = (max + "").length();

​ //定义10个桶容器。

​ // 数组的第一维表示0-9,二维下标按照最大可能arr.length来计算。

​ // 虽然额外浪费了内存空间。基数排序就是用空间换时间的算法。

​ int[][] bucket = new int[10][arr.length];

​ // 定义桶的计数器

​ // 记录每个桶中放置数据的个数。数组元素共10个,表示10个桶。

​ // 数组索引表示桶的编号,索引对应的数值是该桶中的数据个数

​ int[] elementCount = new int[10];

​ //times是记录重复操作轮数的计数器。重复次数取决了最大数值的位数

​ //循环中定义变量n,用来表示位数。1表示个位,10表示十位,100表示百位。目的是获取数字每个位上的值。

​ for (int times = 1, n = 1; times <= maxLen; times++, n *= 10) {

​ //遍历数值,放到桶中

​ for (int i = 0; i < arr.length; i++) {

​ //获取元素个位、十位、百位上的数字。就是桶编号

​ int lsd = ((arr[i] / n) % 10);

​ //将数值放入桶中

​ bucket[lsd][elementCount[lsd]] = arr[i];

​ // 计数器增加

​ elementCount[lsd]++;

​ }

//System.out.println("-------------------------------");

​ //数组索引下标。每轮结束都要形成新的数列,数组下标重新记录。

​ int index = 0;

​ // 从10个桶中取出数据,形成新的数列

​ for (int i = 0; i < 10; i++) {

​ //从有数据的桶中遍历数据

​ if (elementCount[i] > 0) {

​ for (int j = 0; j < elementCount[i]; j++) {

​ arr[index++] = bucket[i][j];

​ }

​ }

​ //遍历完数据,将计数器清空,下次重新计数

​ elementCount[i] = 0;

​ }

//System.out.println("第" + times + "轮后:" + Arrays.toString(arr));

​ }

}

1.11.4 基数排序的复杂度和稳定性

  1. 在基数排序中,r为基数,d为位数。则基数排序的时间复杂度为O(d(n+r))。
  2. 基数排序的效率和初始序列是否有序没有关联。
  3. 空间复杂度:对于任何位数上的基数进行“装桶”操作时,都需要n+r个临时空间。
  4. 算法稳定性:稳定。在基数排序过程中,每次都是将当前位数上相同数值的元素统一“装桶”,并不需要交换位置。所以基数排序是稳定的算法。
  5. 基数排序是效率高的稳定性排序算法。

1.12 Arrays.sort()(DualPivot Quicksort)

1.12.1 概念

Arrays.sort排序算法是双基准元素快速排序DualPivotQuicksort。

由弗拉基米尔·雅罗斯拉夫斯基Vladimir Yaroslavskiy,乔恩·本特利Jon Bentley和约书亚·布洛克Josh Bloch提供该算法。

在许多数据集上表现出O(nlogn)的时间复杂度,比传统的(单基准元素)快速排序算法性能更快。

1.12.2 排序原理

1、在微小数组上使用插入排序(Use insertion sort on tiny arrays)

int INSERTION_SORT_THRESHOLD = 47;

如果要排序的数组长度小于INSERTION_SORT_THRESHOLD这个常量,则插入排序优先于快速排序。

2、对小数组使用快速排序(Use Quicksort on small arrays)

int QUICKSORT_THRESHOLD = 286;

如果要排序的数组长度小于QUICKSORT_THRESHOLD这个常量,则快速排序优先于归并排序。

3、数组接近排序,使用归并排序(Check if the array is nearly sorted)

int MAX_RUN_COUNT = 67; //归并排序的最大运行次数。

int MAX_RUN_LENGTH = 33; //归并排序运行的最大长度。

数组不是高度结构化的,使用快速排序代替归并排序。

4、对大数组使用计数排序(Use counting sort on large arrays)

int COUNTING_SORT_THRESHOLD_FOR_BYTE = 29;

如果要排序的字节数组的长度大于COUNTING_SORT_THRESHOLD_FOR_BYTE该常量,则优先使用计数排序而不是插入排序。

5、对大数组使用计数排序(Use counting sort on large arrays)

int COUNTING_SORT_THRESHOLD_FOR_SHORT_OR_CHAR = 3200;

如果要排序的短数组或char数组的长度大于COUNTING_SORT_THRESHOLD_FOR_SHORT_OR_CHAR该常量,则优先使用计数排序而不是快速排序。

1.12.3 源码截图

1、在微小数组上使用插入排序(Use insertion sort on tiny arrays)

0

2、对小数组使用快速排序(Use Quicksort on small arrays)

0

3、数组接近排序,使用归并排序(Check if the array is nearly sorted)

0

4、对大数组使用计数排序(Use counting sort on large arrays)

0

5、对大数组使用计数排序(Use counting sort on large arrays)

0

标签:arr,int,复杂度,元素,数组,排序
来源: https://www.cnblogs.com/liang5683/p/16428074.html

专注分享技术,共同学习,共同进步。侵权联系[[email protected]]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有