程序员必备十大排序算法

  • 常见排序算法
    • 基本概念
    • 插入排序
      • 直接插入排序
        • 排序思路
        • 排序过程
        • 代码实现
        • 算法分析
      • 折半插入排序
        • 排序思路
        • 排序过程
        • 代码实现
        • 算法分析
      • 希尔排序
        • 排序思路
        • 排序过程
        • 代码实现
        • 算法分析
    • 交换排序
      • 冒泡排序
        • 排序思路
        • 排序过程
        • 代码实现
        • 算法分析
      • 快速排序
        • 排序思路
        • 排序过程
        • 代码实现
        • 算法分析
    • 选择排序
      • 简单选择排序
        • 排序思路
        • 排序过程
        • 代码实现
        • 算法分析
      • 堆排序
        • 排序思路
        • 排序过程
        • 代码实现
        • 算法分析
    • 归并排序
      • 排序思路
      • 排序过程
      • 代码实现
      • 算法分析
    • 基数排序
      • 排序思路
      • 排序过程
      • 代码实现
      • 算法分析
    • 计数排序
      • 排序思路
      • 排序过程
      • 代码实现
      • 算法分析
    • 桶排序
      • 排序思路
      • 排序过程
      • 代码实现
      • 算法分析
    • 各种内排序方法的比较和选择

常见排序算法

基本概念

排序:就是整理表中的元素,是之按关键字递增或者递减有序排列。

输入:n个元素, R 0 R_0 R0​, R 1 R_1 R1​,…, R n − 1 R_{n-1} Rn−1​,其相应的关键字分别为 k 0 k_0 k0​, k 1 k_1 k1​,…, k n − 1 k_{n-1} kn−1​。

输出: R r 0 R_{r_0} Rr0​​, R i 1 R_{i_1} Ri1​​,…, R i n − 1 R_{i_{n-1}} Rin−1​​,使得 k r 0 k_{r_0} kr0​​<= k i 1 k_{i_1} ki1​​<=…<= k i n − 1 k_{i_{n-1}} kin−1​​

稳定性:如果待排序的表中存在有多个关键字相同的元素,经过排序后这些具有相同关键字的元素之间的相对次序保持不变,则称这种排序方法时稳定的;若具有相同关键字的元素之间的相对次序发生变化,则称这种排序方法是不稳定的。在所有可能的输入实例中,只要有一个实例使得算法不满足稳定性要求,则该排序算法就是不稳定的

内排序:在排序过程中,整个表都放在内存中处理,排序时不涉及数据的内、外存交换,则称之为内排序。

外排序:在排序过程中要进行数据的内、外存交换,则称之为外排序。

内排序适用于元素个数不是很多的小表,外排序则适用于元素个数很多,不能一次将其全部元素放入内存的大表。内排序是外排序的基础

按所用的策略不同,内排序方法可以分为需要关键字比较和不需要关键字比较两类。需要关键字比较的排序方法有插入排序、选择排序、交换排序和归并排序等;不需要关键字比较的排序方法有基数排序等。

基于比较的排序算法的性能

在基于比较的排序算法中主要涉及比较和移动基本操作:

  • 比较:关键字之间的比较
  • 移动:元素从一个位置移动到另一个位置

若待排序元素的关键字顺序正好和排序顺序相同,称此表中元素为正序;反之,若待排序元素的关键字顺序正好和排序顺序相反,称此表中元素为反序

基于比较的方法对任意的n个元素排序,最好的平均时间复杂度为O(n l o g 2 log_2 log2​n)。

插入排序

基本思想:每次将一个待排序的元素按其关键字大小插入到前面已经排好序的子表中的适当位置,直到全部元素插入完成为止

直接插入排序

排序思路

假设待排序的元素放在数组R[0…n-1]中,在排序过程中的某一中间时刻,R被划分为两个子区间R[0…i-1]和R[i…n-1],其中前一个子区间是已排好序的有序区,后一个子区间则是当前未排序的部分,不妨称其为无序区,初始时i=1,有序区只有R[0]一个元素。

排序过程

直接插入排序的一趟操作时将当前无序区的开头元素R[i] (1<=i<=n-1)插入到有序区R[0…i-1]中的适当位置,使R[0…i]变为新的有序区。这种方法通常称为增量法,因为它每次使有序区增加一个元素。

一趟直接插入排序:在有序区中插入R[i]的过程

对于第i趟排序,其过程如下

举例

直接插入排序和打扑克牌时,从牌桌上逐一拿起扑克牌,在手上排序的过程相同。

Input: {5 2 4 6 1 3}。

首先拿起第一张牌, 手上有 {5}。

拿起第二张牌 2, 把 2 insert 到手上的牌 {5}, 得到 {2 5}。

拿起第三张牌 4, 把 4 insert 到手上的牌 {2 5}, 得到 {2 4 5}。

  • 先将待插入元素R[i]暂存到temp中
  • j在有序区中从后往前找(初值为i-1即R[i]的前一个位置)
  • 凡是关键字大于R[i]的记录均往后移一个位置
  • 若找到R[j]<temp,则将temp放在R[j]的后面,即置R[j+1]=temp

说明直接插入排序每趟产生的有序区并不一定是全局有序区,也就是说有序区中的元素并不一定放在最终位置上。当一个元素在整个排序结束前就已经放在其最终位置上称为归位

代码实现

/*** 直接插入排序算法,对array[0...n-1]按递增有序进行直接插入排序** @param array 待排序数组* @param n     数组元素个数* @return 排序好的数组*/
public static int[] insertSort(int[] array, int n) {int temp = 0;int i = 0;int j = 0;for (i = 1; i < n; i++) {//数组反序时if (array[i - 1] > array[i]) {// temp暂存待插入array[i]的值temp = array[i];// 找array[i]的插入位置,从array[i]的前一个数开始查找即i-1// 当j<0或者找到array[j]小于temp时j = i - 1;do {// 将关键字大于array[i]的记录往后移array[j + 1] = array[j];j--;} while (j >= 0 && array[j] > temp);// 在array[j]的后面插入array[i]array[j + 1] = temp;}}return array;
}

算法分析

最好情况是表初态为正序,此时算法的时间复杂度为O(n);最坏情况是初表态为反序,相应的时间复杂度为O( n 2 n^2 n2)。算法的平均时间复杂度也是O( n 2 n^2 n2)

直接插入排序算法中只使用i、j和temp这三个辅助变量,与问题规模无关,故辅助空间复杂度为O(1),也就是说,它是一个就地排序算法。当i>j且R[i]=R[j]时,本算法将R[i]插入到R[j]的后面,使R[i]和R[j]的相对位置保持不变,所以直接插入排序是一种稳定的排序方法

折半插入排序

排序思路

直接排序插入中将无序区的开头元素R[i] (1<=i<=n-1)插入到有序区R[0…i-1]是采用顺序比较的方法。

由于有序区的元素是有序的,可以采用折半查找方法先在R[0…i-1]中找到插入的位置,再通过移动元素进行插入,这样的插入排序称为折半插入排序二分插入排序

采用折半查找在有序区找到插入的位置

排序过程

  1. 第i趟在R[low…high](初始时low=0,high=i-1)中采用折半查找方法查找插入R[i]的位置为R[high+1]
  2. 再将R[high+1…i-1]元素后移一个位置
  3. 置R[high]=R[i]

利用二分查找找到high,将待插入值放到R[high]的后面

代码实现

/*** 折半插入排序算法* @param array 待排序数组* @param n 数组中元素个数* @return 排序号好的数组*/
public static int[] binInsertSort(int[] array, int n) {int i = 0;int j = 0;int temp = 0;int low = 0, mid = 0, high = 0;for (i = 1; i < n; i++) {// 反序时if (array[i-1] > array[i]) {temp = array[i];low = 0;high = i-1;// 在array[low...high]中查找插入位置// 一直找,循环结束的时候,array[high]<temp,即在array[high]的后面插入a[i]while (low <= high) {// 取中间位置mid = (low + high) / 2;if (array[mid] > temp) {// 插入点在左半区high = mid - 1;} else {// 插入点在右半区low = mid + 1;}}// 找位置high后面的元素即high+1,然后对high+1到i-1之间的元素集中进行后移for (j = i - 1; j >= high+1; j--) {array[j + 1] = array[j];}// 在high后面插入temparray[high + 1] = temp;}}return array;
}

算法分析

折半插入排序的元素移动次数与直接插入排序相同,不同的仅是变分散移动为集中移动,平均时间复杂度为O( n 2 n^2 n2)。折半插入排序和直接插入排序相比移动元素的性能没有改善,仅仅减少了关键字的比较次数。就平均性能而言,由于折半查找优于顺序查找,所以折半插入排序也优于直接插入排序。折半插入排序的空间复杂度为O(1),也是一种稳定的排序算法

和直接插入排序一样,折半插入排序每趟产生的有序区并不一定是全局有序区

希尔排序

排序思路

希尔排序也是一种插入排序方法,实际上是一种分组插入方法。**其基本思想先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。**先取一个小于n的整数 d 1 d_1 d1​作为第一个增量,把表的全部元素分成 d 1 d_1 d1​个组,将所有距离为 d 1 d_1 d1​的倍数的元素放在同一个组中。在各组内进行直接插入排序;然后取第2个增量 d 2 d_2 d2​(< d 1 d_1 d1​ ),重复上述的分组和排序,直到所取的增量 d 1 d_1 d1​=1( d t d_t dt​< d t − 1 d_{t-1} dt−1​<…< d 2 d_2 d2​< d 1 d_1 d1​ ),即所有元素放在同一组中进行直接插入排序为止。所以希尔排序也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。

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

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;

排序过程

  1. d=n/2

  2. 将排序序列分为d个组,在各组内进行直接插入排序

  3. 递减d=d/2,重复步骤2,直至d=1

    算法最后一趟对所有数据进行了直接插入排序,所以结果一定是正确的。

一趟希尔排序过程

将记录序列分成若干个子序列,分别对每个子序列进行直接插入排序。

例如:将n个记录分成d个子序列:

例如:n=10

每一趟希尔排序从元素R[d]开始起,采用直接插入排序,直到元素R[n-1]为止。每个元素的比较和插入都在同组内部进行,对于元素R[i],同组的前面的元素有{R[i]|j=i-d>=0}。

希尔排序每趟并不产生有序区,在最后一趟排序结束前,所有元素并不一定归位了,但是在希尔排序每趟完成后数据越来越接近有序

代码实现

取 d 1 d_1 d1​=n/2, d i + 1 d_{i+1} di+1​=[ d i d_i di​/2]时的希尔排序算法如下:

/*** 希尔排序* @param array 待排序数组* @param n 数组大小* @return 排序好的数组*/
public static int[] shellSort(int[] array, int n) {int j = 0, i = 0, d = 0,temp=0;// 增量置初值d = n / 2;while (d > 0) {// 对所有组采用直接插入排序for (i = d; i < n; i++) {// 对相隔d个位置一组采用直接插入排序temp = array[i];j = i - d;while (j >= 0 && temp < array[j]) {array[j + d] = array[j];j = j - d;}array[j + d] = temp;}// 减小增量d = d / 2;}return array;
}

算法分析

希尔排序的时间复杂度约为O( n 1.3 n^{1.3} n1.3)

为什么希尔排序要比直接插入排序好?

例如:有10个元素要排序。

为什么希尔排序是一种不稳定的排序算法?

例子如下:

交换排序

冒泡排序

排序思路

冒泡排序也称为气泡排序,是一种典型的交换排序方法,其基本思想是通过无序区中相邻元素关键字间的比较和位置的交换字使最小的元素如气泡一般逐渐往上“漂浮”直至“水面”

排序过程

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

说明:冒泡排序每趟产生的有序区一定是全局有序区,也就是说每趟产生的有序区中的所有元素都归位了

代码实现

使用双循环进行排序。外部循环控制所有的回合,内部循环实现每一轮的冒泡处理,先进行元素比较,再进行元素交换

/*** 冒泡排序算法** @param array 待排序数组* @return 排序好的数组*/
public static int[] bubbleSort(int[] array) {for (int i = 0; i < array.length - 1; i++) {for (int j = 0; j < array.length - i - 1; j++) {// 相邻两个元素之间,当一个元素大于右侧相邻元素时,交换它们的位置if (array[j] > array[j + 1]) {int temp = array[j];array[j] = array[j + 1];array[j + 1] = temp;}}}return array;
}

改进算法:如果能判断出数列已经有序,并做出标记,那么剩下的几轮排序就不必执行了,可以提前结束工作。利用布尔变量isSorted作为标记。如果在本轮排序中,元素有交换,则说明数列无序;如果没有元素交换,则说明数列已然有序,然后直接跳出大循环。

/*** 改进的冒泡排序算法1** @param array 待排序数组* @return 排序好的数组*/
public static int[] bubbleSort1(int[] array) {boolean isSorted;for (int i = 0; i < array.length - 1; i++) {isSorted = false;for (int j = 0; j < array.length - i - 1; j++) {// 相邻两个元素之间,当一个元素大于右侧相邻元素时,交换它们的位置if (array[j] > array[j + 1]) {int temp = array[j];array[j] = array[j + 1];array[j + 1] = temp;isSorted = true;}}if (!isSorted) {return array;}}return array;
}

算法再改进:按照现有的逻辑,有序区的长度和排序的轮数是相等的。例如第1轮排序过后的有序区长度是1,第2轮排序过后的有序区长度是2 ……实际上,数列真正的有序区可能会大于这个长度,如上述例子中在第2轮排序时,后面的5个元素实际上都已经属于有序区了。因此后面的多次元素比较是没有意义的。那么,该如何避免这种情况呢?我们可以在每一轮排序后,记录下来最后一次元素交换的位置,该位置即为无序数列的边界,再往后就是有序区了。

/*** 改进的冒泡排序算法2** @param array 待排序数组* @return 排序好的数组*/
public static int[] bubbleSort2(int[] array) {// 记录最后一次交换的位置int lastExchangeIndex = 0;// 无序数列的边界,每次比较只需要比到这里为止int sortBorder = array.length - 1;for (int i = 0; i < array.length - 1; i++) {// 有序标记,每一轮的初始值都是trueboolean isSorted = true;for (int j = 0; j < sortBorder; j++) {int temp = 0;if (array[j] > array[j + 1]) {int temp = array[j];array[j] = array[j + 1];array[j + 1] = temp;// 因为有进行元素交换,所以不是有序的,标记为falseisSorted = false;// 更新最后一次交换元素的位置lastExchangeIndex = j;}}sortBorder = lastExchangeIndex;if (isSorted) {break;}}return array;
}

鸡尾酒排序(冒泡排序改进算法):冒泡算法的每一轮都是从左到右来比较元素,进行单向的位置交换的。鸡尾酒排序过程就像钟摆一样,第1轮从左到右,第2轮从 右到左,第3轮再从左到右……

  • 优点:是能够在特定条件下,减少排序的回合数;
  • 缺点:代码量几乎增加了1倍。至于它能发挥出优势的场景,是大部分元素已经有序的情况。
/*** 鸡尾酒排序,改进的冒泡排序算法** @param array 待排序数组* @return 排序好的数组*/
public static int[] bubbleSort3(int[] array) {int temp = 0;for (int i = 0; i < array.length / 2; i++) {// 有序标记,每一轮的初始值都是trueboolean isSorted = true;// 奇数轮,从左往右比较和交换for (int j = i; j < array.length - i - 1; j++) {if (array[j] > array[j + 1]) {int temp = array[j];array[j] = array[j + 1];array[j + 1] = temp;//有元素交换,所以不是有序的,标记为falseisSorted = false;}}if (isSorted) {break;}// 在偶数轮之前,将isSorted重新标记为trueisSorted = true;// 偶数轮,从右往左比较和交换for (int j = array.length - i - 1; j > i; j--) {if (array[j] < array[j - 1]) {int temp = array[j];array[j] = array[j - 1];array[j - 1] = temp;// 有元素交换,所以不是有序的,标记为falseisSorted = false;}}if (isSorted) {break;}}return array;
}

算法分析

最好情况时表初态为正序,此时算法的时间复杂度为O(n);最坏情况时表初态为反序,相应的时间复杂度为O( n 2 n^2 n2)。算法的平均时间复杂度为O( n 2 n^2 n2)

在冒泡排序算法中只使用i、j、temp这3个辅助变量,与问题规模n无关,故辅助空间复杂度为O(1),也就是说它是一个就地排序

当R[i]=R[j]且i>j时,两者没有逆序,不会发生交换,也就是说使R[i]和R[j]的相对位置保持不变,所以冒泡排序也是一种稳定的排序算法

快速排序

排序思路

快速排序(quick sort)是由冒泡排序算法改进而得的,它的基本思想是在待排序的n个元素中任取一个元素(通常取第一个元素)作为基准,把该元素放入适当位置后,数据序列被此元素划分成两部分。所有关键字比该元素关键字小的元素放置在前一部分,所有比它大的元素放在后一部分,并把该元素排在这两部分的中间(称为该元素归位),这个过程称为一趟快速排序,即一趟划分。

之后对产生的两个部分分别重复上述过程,直至每部分内只有一个元素或空为止。简而言之,每趟使表的第一个元素放入适当位置,将表一分为二,对子表按递归方式继续这种划分,直至划分的子表的长为1或0。

排序过程

  • 设两个指示器i和j,它们的初始值分别为指向无序区中的第一个和最后一个元素。
  • 假设无序区中的元素为R[start],R[start+1],… ,R[end],则i的初始值为start,j的初始值为end。
  • 首先将R[start]移至变量pivot中作为基准,令j自位置end起向左扫描直至R[j]<pivot时将R[j]移至位置i
  • 然后让i向右扫描直至R[i]>pivot时将R[i]移至位置j
  • 依此重复直至i=j,此时所有R[k](k=start,start+1,… ,i-1)的关键字都小于pivot,R[k](k=i+1,i+2,… ,end)的关键字必大于pivot
  • 此时再将pivot中的元素移至位置i,它将无序区中的元素分割成R[start…i-1]和R[i+1…end]

  1. 从数列中挑出一个元素,称为 “基准”(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

快速排序每趟仅将一个元素归位

快速排序是一个递归过程,其递归模型如下:

f(R,s,t) = 不做任何事情 当R[s…t]中没有元素或者只有一个元素时

f(R,s,t) = i=partition(R,s,t); 其他情况

​ f(R,s,i-1) ;

​ f(R,i+1,t) ;

代码实现

  • 递归实现、分治(双边循环法)
    /*** 快速排序算法(递归实现),对array[start...end]的元素进行快速排序** @param array 待排序数组* @param start 排序起始位置* @param end   排序终点位置*/public static void quickSort(int[] array, int start, int end) {if (start >= end) {return;}int left = start;int right = end;// 取第一个位置(也可以选择随机位置)的元素作为基准元素int pivot = array[start];// 从两端向中间交替扫描,直到left=right为止while (left < right) {// 从右向做开始扫描,找到第一个小于pivot的array[left]while (left < right && array[right] >= pivot) {right--;}// 找到这样的array[right],放入array[left]处array[left] = array[right];// 从左向右扫描,找到一个大于pivot的array[left]while (left < right && array[left] <= pivot) {left++;}// 找到这样的array[left],放入array[right]处array[right] = array[left];}// 当left = right时,一趟排序结束,此时将基准元素放入left的位置// 此时基准元素的左边小于array[left],右边大于array[left]array[left] = pivot;// 对基准元素剩余左边部分进行快速排序quickSort(array, start, left - 1);// 对基准元素剩余右边部分进行快速排序quickSort(array, left + 1, end);}

或者将一趟划分提取为一个方法

    /*** 快速排序算法(递归实现),对array[start...end]的元素进行快速排序** @param array 待排序数组* @param start 排序起始位置* @param end   排序终点位置*/public static void quickSort1(int[] array, int start, int end) {//基准元素位置int pivot;//递归结束条件:start大于等于endif (start >= end) {return;//区间至少存在两个元素的情况} else {//得到基准元素位置pivot = partition1(array, start, end);//根据基准元素,分成两部分进行递归排序//对左区间递归排序quickSort1(array, start, pivot - 1);//对右区间递归排序quickSort1(array, pivot + 1, end);}}/*** 一趟划分,查找基准元素位置,分治(双边循环法)** @param array 待排序数组* @param start 起始位置* @param end   终点位置* @return 基准元素位置*/public static int partition1(int[] array, int start, int end) {int left = start;int right = end;//取第一个位置(也可以选择随机位置)的元素作为基准元素int pivot = array[start];//从两端向中间交替扫描,直至left=right为止while (left < right) {//从右向左扫描,找到一个小于pivot的array[right]while (left < right && array[right] >= pivot) {right--;}//找到这样的array[right],放入array[left]处array[left] = array[right];//从左向右扫描,找到一个大于pivot的array[left]while (left < right && array[left] <= pivot) {left++;}//找到这样的array[left],放入array[right]处array[right] = array[left];}//当left = right时,一趟排序结束,此时将基准元素放入left的位置//此时基准元素的左边小于array[left],右边大于array[left]array[left] = pivot;//返回当前找到的基准元素return left;}
  • 递归实现、分治(单边循环法)
    /*** 快速排序算法(递归实现),对array[start...end]的元素进行快速排序** @param array 待排序数组* @param start 排序起始位置* @param end   排序终点位置*/public static void quickSort1(int[] array, int start, int end) {//基准元素位置int pivot;//递归结束条件:start大于等于endif (start >= end) {return;//区间至少存在两个元素的情况} else {//得到基准元素位置pivot = partition1(array, start, end);//根据基准元素,分成两部分进行递归排序//对左区间递归排序quickSort1(array, start, pivot - 1);//对右区间递归排序quickSort1(array, pivot + 1, end);}}/*** 一趟划分,查找基准元素位置,分治(单边循环法)** @param array 待排序数组* @param start 起始位置* @param end   终点位置* @return 基准元素位置*/public static int partition2(int[] array, int start, int end) {//取第一个位置(也可以选择随机位置)的元素作为基准元素int pivot = array[start];//指针left指向待排序数组首部int left = start;//从左边开始扫描for (int i = start + 1; i <= end; i++) {//如果找到一个小于基准值的元素if (array[i] < pivot) {//指针左移一位left++;//交换这个元素的值int temp = array[left];array[left] = array[i];array[i] = temp;}}//将left位置的元素放到数组首部array[start] = array[left];//将基准元素放入left的位置array[left] = pivot;return left;}
  • 非递归实现、分治(单边循环法)
/*** 一趟划分,查找基准元素位置,分治(单边循环法)** @param array 待排序数组* @param start 起始位置* @param end   终点位置* @return 基准元素位置*/
public static int partition2(int[] array, int start, int end) {//取第一个位置(也可以选择随机位置)的元素作为基准元素int pivot = array[start];int mark = start;for (int i = start + 1; i <= end; i++) {if (array[i] < pivot) {mark++;int p = array[mark];array[mark] = array[i];array[i] = p;}}array[start] = array[mark];array[mark] = pivot;return mark;
}/*** 快速排序算法(非递归实现),对array[start...end]的元素进行快速排序* 代码中一层一层的方法调用,本身 就使用了一个方法调用栈。每次进入一个新方法,就相当于入栈;* 每次有方法返回,就相当于出栈。* 所以,可以把原本的递归实现转化成一个栈的实现,在栈中存储每一次方法调用的参数。* 该方法引入了一个存储Map类型元素的栈,用于存储每一次交换时的起始下标和结束下标。* 每一次循环,都会让栈顶元素出栈,通过partition方法进行分治,并且按照 基准元素的* 位置分成左右两部分,左右两部分再分别入栈。当栈为空时,说明排序已经完毕,退出循环。** @param array 待排序数组* @param start 排序起始位置* @param end   排序终点位置*/
public static void quickSort2(int[] array, int start, int end) {//用一个集合栈来代替递归的函数栈Stack<Map<String, Integer>> quickSortStack = new Stack<>();//整个数列的起止下标,以哈希的形式入栈Map rootParam = new HashMap();rootParam.put("start", start);rootParam.put("end", end);quickSortStack.push(rootParam);//循环结束条件:栈为空时while (!quickSortStack.isEmpty()) {//栈顶元素出栈,得到起始下标Map<String, Integer> param = quickSortStack.pop();//得到基准元素位置int pivot = partition2(array, param.get("start"), param.get("end"));//根据基准元素分成两部分,把每一个部分的起止下标入栈if (param.get("start") < pivot - 1) {Map<String, Integer> leftParam = new HashMap<>();leftParam.put("start", param.get("start"));leftParam.put("end", pivot - 1);quickSortStack.push(leftParam);}if (pivot + 1 < param.get("end")) {Map<String, Integer> rightParam = new HashMap<>();rightParam.put("start", pivot + 1);rightParam.put("end", param.get("end"));quickSortStack.push(rightParam);}}
}

算法分析

快速排序算法最好的情况是每一次划分都将n个元素划分为两个长度差不多相同的子区间,也就是说,每次划分所取的基准都是当前无序区的“中值元素”,划分的结果是基准的左、右两个无序子区间的长度大致相等。这样的递归树高度为O( l o g 2 log_2 log2​n),而每一层划分的是时间为O(n),所以此时算法的时间复杂度为O(n l o g 2 log_2 log2​n)、空间复杂度为O( l o g 2 log_2 log2​n)。

最好情况

快速排序算法最坏的情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的元素,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中元素的数目仅比划分前的无序区中的元素个数减少一个。**这样的递归树高度为n,需要做n-1次划分,此时算法的时间复杂度为O( n 2 n^2 n2)、空间复杂度为O(n)。**当初始数据数列为正序或者反序时,显然呈现最坏的情况。如果初始数列是随机的,每次可以划分为两个长度差不多相同的子区间,会呈现出最好的情况。

最坏情况

快速排序算法是一种不稳定的排序算法

实际上,在快速排序中可以任意一个元素为基准(更好的选择方法是从数序中随机选择一个元素作为基准),以下算法以当前区间的中间位置为基准,同样可以达到快速排序的目的:

 /*** 对array[start...end]以中间元素为基准进行快速排序* @param array 待排序数组* @param start 起始位置* @param end   终止位置*/public static void quickSort3(int[] array, int start, int end) {int i, pivot;// 用区间中间元素作为基准pivot = (start + end) / 2;//区间内至少存在两个元素的情况if (start < end) {// 若基准不是区间中的第一个元素,将其与第一个元素交换if (pivot != start) {int temp = array[pivot];array[pivot] = array[start];array[start] = temp;}//划分i = partition1(array, start, end);//对左区间递归排序quickSort1(array, start, i - 1);//对右区间递归排序quickSort1(array, i + 1, end);}}

例题

选择排序

基本概念

​ 选择排序的基本思想是每一趟从排序的元素中选出关键字最小的元素,顺序放在已排好序的子表的最后,直到全部元素排序完毕。由于选择排序方法中每一趟总是从无序区中选出全局最小(或最大)的关键字,所以适合于从大量的元素中选择一部分排序元素,例如从10000个元素中选择出关键字大小为前10位的元素就适合采用选择排序算法。

简单选择排序

排序思路

简单选择排序(simple selection sort)的基本思想是第i趟排序开始时,当前有序区和无序区分别位R[0…i-1]和R[i…n-1](0<=i<n-1),该趟排序是从当前无序区中选出关键字最小的元素R[k],将它与无序区的第1个元素R[i]交换,是R[0…i]和R[i+1…n-1]分别变为新的有序区和新的无序区。

简单选择排序每趟产生的有序区一定是全局有序区,也就是说,每趟产生的有序区中所有元素都归位了

排序过程

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。

  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

  3. 重复第二步,直到所有元素均排序完毕。

动画演示如下图所示:

代码实现

/*** 简单选择排序算法** @param array 待排序数组* @return 排好序的数组*/public static int[] selectSort(int[] array) {int i, j, k;//总共要经过array.length - 1次比较for (i = 0; i < array.length - 1; i++) {//做第i趟排序k = i;//在当前无序区R[i...n-1]中选值最小的array[k]for (j = i + 1; j < array.length; j++) {if (array[j] < array[k]) {//k记下目前找到的最小值在数组中所在的位置k = j;}}//array[i]和当前这一趟排序找到的最小值交换if (k != i) {int temp = array[k];array[k] = array[i];array[i] = temp;}}return array;}

算法分析

无论初始数据序列的状态如何,在第i趟排序中选出最小关键字的元素,内for循环需做n-1-(i+1)+1=n-i-1次比较,因此总的比较次数为:

至于元素的移动次数,当初始表为正序时,移动次数为0;当表初态为反序时,每趟排序均要执行交换操作,所以总的移动次数为最大值3(n-1)。然而,无论元素的初始序列如何排列,所需进行的关键字比较相同,因此总的平均时间复杂度为O( n 2 n^2 n2)

在简单选择排序算法中只使用i、j、k和temp这4个辅助变量,与问题规模无关,故辅助空间复杂度位O(1),也就是说它是一个就地排序

简单选择排序算法是一个不稳定的排序方法

原地操作几乎是选择排序的唯一优点,当空间复杂度要求较高时,可以考虑选择排序;实际适用的场合非常罕见。

堆排序

排序思路

**堆排序(heap sort)**是一种树形选择排序方法,它的特点是将R[1…n](R[i]的关键字为 k i k_i ki​)看成是一棵完全二叉树的顺序存储结构。利用完全二叉树中双亲结点和孩子结点之间的位置关系在无序区中选择关键字最大(或最小)的元素。

堆的定义

​ 堆的定义是R[1…n]中的n个关键字序列 k 1 k_1 k1​, k 2 k_2 k2​,… , k n k_n kn​称为堆,当且仅当该序列满足如下性质(简称为堆性质):

(1) k i k_i ki​<= k 2 i k_{2_i} k2i​​且 k i k_i ki​<= k 2 i + 1 k_{2i+1} k2i+1​ 满足这种情况的称为小根堆树中分支任何结点的关键字都小于其孩子节点的关键字

(2) k i k_i ki​>= k 2 i k_{2_i} k2i​​且 k i k_i ki​>= k 2 i + 1 k_{2i+1} k2i+1​(1<=i<=[n/2]) 满足这种情况的称为大根堆树中任何分支结点的关键字大于等于其孩子结点的关键字

堆排序每趟产生的有序区一定是全局有序区,也就是说每趟排序产生的有序区中的所有元素都归位了

排序过程

假如完全二叉树的根节点是R[i],它的左、右子树已是大根堆,将其两个孩子的关键字R[2i]、R[2i+1]的最大者与R[i]比较,若R[i]较小,将其与最大孩子进行交换,这有可能破坏下一级的堆。继续采用上述方法构造下一级的堆,直到这棵完全二叉树变成一个大根堆为止。

  1. 把待排序的序列构建成二叉堆。需要从小到大排序,则构建成最大堆;需要从大到小排序,则构建成最小堆。
  2. 此时,整个序列的最大值就是堆顶的根节点。将它移走(就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值)。
  3. 然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次大值。
  4. 如此反复执行,便能得到一个有序序列了。

代码实现

    /*** “下沉”调整** @param array       当前待调整的堆对应的数组* @param parentIndex 当前待调整的堆的父结点* @param length      当前待调整的堆的大小,即结点个数,也就是对应数组的长度*/public static void downAdjust(int[] array, int parentIndex, int length) {// 先取出待调整的堆的父结点parentIndexint temp = array[parentIndex];// 从parentIndex结点的左孩子结点开始,也就是2*parentIndex+1处开始int i = 2 * parentIndex + 1;while (i < length) {// 如果当前parentIndex结点含有右孩子结点并且其左孩子结点的值小于右孩子结点的值// 指向右孩子(比较大的孩子)if (i + 1 < length && array[i] < array[i + 1]) {i++;}// 如果孩子结点的值大于父结点的值,则将子结点的值赋给父结点(不用进行交换)if (array[i] > temp) {// 将子结点的值赋给父结点(不用进行交换)array[parentIndex] = array[i];// 修改parentIndex和i的值,以便继续向下筛选// 继续下沉,当前孩子结点为新的父结点parentIndex = i;// 当前孩子结点的左孩子结点为新的孩子结点i = 2 * i + 1;} else {// 如果父结点比孩子结点都大,则跳出循环,不再调整break;}}// 将待调整的堆的父结点parentIndex放到最终的位置,注意上面的parentIndex=k// 此时parentIndex的值已经改变array[parentIndex] = temp;}

构建初始堆R[1…n]的过程是:对于一棵完全二叉树,从i=[n/2]~1,即从最后一个分支结点开始,反复利用上述筛选方法建堆。大者**“上浮”,小者被”筛选“**下去。即:

for( i = n / 2; i>=1;i–)

​ sift(array, i, n);

在初始堆R[1…n]构造好以后,根结点R[1]一定是最大关键字结点,将其放到排序序列的最后,也就是将堆中的根与最后一个叶子交换。由于最大元素已归位,整个待排序的元素的个数减少一个。由于根结点的改变,这n-1个结点R[1…n-1]不一定为堆,但其左子树和右子树均为堆,再调用一次sift算法将这n-1个结点R[1…n-1]调整成堆,其根结点为次大的元素,将它放到排序序列的倒数第2个位置,即将堆中的根与最后一个叶子交换,待排序的元素个数变为n-2个,即R[1…n-2],再调整,再将根结点归位,如此这样,直到完全二叉树只剩一个根为止。实现堆排序的算法如下:

/*** 堆排序算法(升序)** @param array 待调整的堆*/public static void heapSort(int[] array) {//1、把无序数组构建成一个最大堆for (int i = (array.length - 2) / 2; i >= 0; i--) {//从第一个非叶子结点(或者最后一个分支结点)从下至上,从右至左调整结构downAdjust(array, i, array.length);}//2、循环删除堆顶元素,移到集合尾部,调整堆产生新的堆顶for (int i = array.length - 1; i > 0; i--) {//将堆顶元素与末尾元素进行交换int temp = array[i];array[i] = array[0];array[0] = temp;//重新对堆进行调整即对剩下(未归位)的元素进行排序downAdjust(array, 0, i);}}

算法分析

堆排序算法和简单选择排序算法一样,其时间性能与初始序列的顺序无关,也就是说,堆排序的算法最好、最坏和平均时间复杂度都是O(n l o g 2 log_2 log2​n)

由于建初始堆所需的比较次数比较多,所以堆排序不适合元素较少的排序表

堆排序只使用i、j、temp等辅助变量,其辅助空间复杂度为O(1)

另外,在进行筛选时可能把后面向相同关键字的元素调整到前面,所以堆排序是一种不稳定的排序方法

归并排序

排序思路

归并排序是多次将两个或两个以上的有序表合并成一个新的有序表。最简单的归并是将两个有序的子表合并成一个有序的表,即二路归并

二路归并排序(2-way merge sort)的基本思路是将R[0…n-1]看成是n个长度为1的有序序列,然后进行两两归并,得到[n/2]个长度为2(最后一个有序序列的长度可能为2)的有序序列,再进行两两归并,得到[n/4](最后一个有序序列的长度可能小于4)的有序序列,… ,直到得到一个长度为n的有序序列。

归并排序每趟排序产生的有序区只是局部有序的,也就是说在最后一趟排序结束前所有元素并不一定归位了

排序过程

设两个有序表存放在同一数组中的相邻位置上,即R[low…mid],R[mid+1…high],先将它们合并到一个局部的暂存数组R1中,待合并完成后将R1复制到R中。

R[low…mid]为第1段,R[mid+1…high]为第2段。每次从两个段中取出一个元素进行关键字的比较,将较小者放入R1中,最后将各段中余下的部分直接复制到R1中。这样R1是一个有序表,再将其复制到R中。对应的过程如下:

代码实现

public static void sort(int[] array) {//在排序前,先建立一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间导致栈溢出int[] temp = new int[array.length];sort(array, 0, array.length - 1, temp);}/*** @param array 待排序数组* @param start 起始位置* @param end   终点位置* @param temp  临时数组(用于保存排序结果)*/public static void sort(int[] array, int start, int end, int[] temp) {if (start < end) {//把待排序数组分成左右两部分int middle = (start + end) / 2;//对左边部分进行归并排序,使得左边的数组有序sort(array, 0, middle, temp);//对右边部分进行归并排序,使得右边的数组有序sort(array, middle + 1, end, temp);//将左右两个有序数组进行合并操作merge(array, start, middle, end, temp);}}public static void merge(int[] array, int start, int middle, int end, int[] temp) {//左序列的指针int i = start;//右序列的指针int j = middle + 1;//临时数组的指针int k = 0;while (i <= middle && j <= end) {// 比较左右两个数组的头部,谁的头部比较小就放入temp数组// 指向该头部的指针以及指向temp数组头部的指针往后移动一位,// 然后进入下一次比较,直到左右两个数组其中有一个数组遍历完成// 另外一个没有遍历完成的数组使用一个while循环来将其全部填充进temp中if (array[i] <= array[j]) {temp[k++] = array[i++];} else {temp[k++] = array[j++];}}//将左边剩余元素填充进temp中while (i <= middle) {temp[k++] = array[i++];}//将右边剩余元素填充进temp中while (j <= end) {temp[k++] = array[j++];}k = 0;//将temp中的元素全部拷贝到原数组中while (start <= end) {array[start++] = temp[k++];}}

算法分析

对于长度为n的排序表,二路归并需要进行[ l o g 2 log_2 log2​n]趟,每趟归并时间为O(n),故其时间复杂度无论是在最好还是最坏情况下均是O(n l o g 2 log_2 log2​n),显然平均时间复杂度也是O(n l o g 2 log_2 log2​n)

在二路归并排序过程中,每次二路归并都需要使用一个辅助数组来暂存两个有序子表归并的结果,而每次二路归并后都会释放其空间,但最后一趟需要所有元素参与归并,所以总的辅助空间复杂度为O(n)

在一次二路归并排序中,如果第1段元素R[i]和第2段元素R[j]的关键字相同,总是将R[i]放在前面、R[j]放在后面,相对次序不会发生改变,所以二路归并排序是一种稳定的排序算法

归并排序可以是多路的,如三路归并排序等。以三路归并排序为例,排序的趟数是[ l o g 3 log_3 log3​n],每一趟的时间为O(n),对应的执行时间为O(n l o g 3 log_3 log3​n),但 l o g 3 n log_3n log3​n= l o g 2 n log_2n log2​n/$log_2 3 , 所 以 时 间 复 杂 度 仍 为 O ( n 3,所以时间复杂度仍为O(n 3,所以时间复杂度仍为O(nlog_2$n),不过三路归并排序算法的实现远比二路归并排序算法复杂。

例题

设待排序表有10个记录,其关键字分别为{18,2,20,34,12,32,6,16,1,5}。说明采用归并排序方法进行排序的过程:

基数排序

排序思路

基数排序是通过**“分配”和“收集”**过程来实现排序,不需要进行关键字间的比较,是一种借助于多关键字排序的思想对单关键字排序的方法。它是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

一般情况下,元素R[i]的关键字由d位数字(或字符)组成,即 k d − 1 k^{d-1} kd−1 k d − 2 k^{d-2} kd−2… k 1 k^1 k1 k 0 k^0 k0,每一个数字表示关键字的一位,其中 k d − 1 k^{d-1} kd−1为最高位、 k 0 k^0 k0是最低位,每一位的值都在0 ≤ \leq ≤ k i k^i ki<r范围内,其中r称为基数(radix)。例如,对于二进制数r为2,对于十进制数r为10。

基数排序有两种,即最低位优先(least significant digit first,LSD)和最高位优先(most significant digit first,MSD),其原理是相同的。

基数排序每趟并不产生有序区,也就是说在最后一趟排序结束前所有元素并不一定归位了。

以下三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  • 基数排序:根据键值的每位数字来分配桶;
  • 计数排序:每个桶只存储单一键值;
  • 桶排序:每个桶存储一定范围的数值;

排序过程

最低位优先的过程是先按最低位的值进行元素排序,在此基础上再按次低位进行排序,依此类推。有低位向高位,每趟排序都是根据关键字的一位并在前一趟的基础上对所有元素进行排序,直至最高位,则完成了基数排序的整个过程。

在对一个数据序列排序时是采用最低位优先还是最高位优先排序方法是由数据序列的特点确定的。例如对整数序列递增排序,由于个位数的重要性低于十位数,十位数的重要性低于百位数,一般越重要的位越放在后面排序,个数为属于最低位,所以对整数序列递增排序时应该采用最低位优先排序方法。

利用Java实现的LSD基数的基本思路大致如下:

  1. 先求出待排序数组中的最大值
  2. 根据最大值求出最大位数,以此来决定需要多少趟基数排序,比如待排序的数组中的最大值为1234,其位数为4,则当前待排序数组需要经过4趟基数排序能使数组有序,可以通过将int类型的数转换成String对象并调用其方法来计算长度int maxLength = (max + "").length();
  3. 基数排序需要10个“桶”,分别代表数字0~9,比如第一个桶代表数字0,在进行一趟基数排序时,该“桶”保存了待排序数组中那些个位数为0的元素;
  4. 定义一个一维数组和一个二维数组来代表这10个“桶”,一维数组bucketElemtCounts代表每个桶中含有的元素个数,bucketElemtCounts[i]的值表示桶中存放个位数为i的元素个数,二维数组bucket[i][j]的表示某元素放在存放个位数为i中的桶中的第j个位置。
  5. 先进行一趟基数排序的“分配”过程,对待排序数组进行遍历,依次求出其个位数、十位数…直至最高位数,将他们放到对应的桶中,往桶中放入元素时,桶所含元素的数量值要加1
  6. 然后进行一趟基数排序的“收集”过程,依次对10个桶进行遍历,判断桶是否为空,如果不为空,将桶中的元素取出,重新赋值给待排序数组并将桶所含元素的数量值置为0,以便下一趟排序使用该桶。
  7. 重复第5、6个步骤,直至循环结束。

LSD 基数排序动图演示

代码实现

public static int[] radixSort(int[] array) {//获取待排序数组中的最大值int max = array[0];for (int num : array) {if (num > max) {max = num;}}/** 计算待排序数组的最大值的位数,比如1234的位数为4,* 通过转换成String对象并调用其方法来计算长度*/int maxLength = (max + "").length();// bucket二维数组代表10个桶,bucket[0...9],分别对应数字0~9,// bucket[i][x]代表第i个桶中各元素的位置,每个桶中可以存放的元素个数最多为array.length个// 即将待排序数组中的所有元素放到某一个桶中int[][] bucket = new int[10][array.length];// bucketElemtCounts[i]代表第i个桶中的元素个数int[] bucketElemtCounts = new int[10];//进行基数排序,最大位数有多少位,就需要多少次基数排序for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {//基数排序“分配”过程for (int j = 0; j < array.length; j++) {//算出待排序数组array中第j个元素的个位数,digitOfElemt代表个位数(取值范围[0,9])int digitOfElemt = array[j] / n % 10;//根据digitOfElemt代表的数字将其放到对应的桶中bucket[digitOfElemt][bucketElemtCounts[digitOfElemt]] = array[j];//第digitOfElemt个桶中放入了一个元素,该桶所含的元素数量+1bucketElemtCounts[digitOfElemt]++;}//基数排序“收集”过程int index = 0;//对10个桶进行遍历,收集10个桶中的元素到待排序数组中,完成第i趟排序for (int k = 0; k < bucketElemtCounts.length; k++) {//如果bucketElemtCounts[k]不为0,说明这个代表数字k的桶中含有元素//那么就将里面的元素取出来,重新放到待排序数组中if (bucketElemtCounts[k] != 0) {for (int q = 0; q < bucketElemtCounts[k]; q++) {array[index++] = bucket[k][q];}}//该桶中的元素已经放到待排序的数组中,即完成了一趟排序//将其赋值为0,进行下一趟排序bucketElemtCounts[k] = 0;}}return array;}

算法分析

在基数排序过程中共进行了d趟的分配和收集。每一趟中分配过程需要扫描所有结点,而收集过程是按队列进行的,所以一趟的执行时间为O(n+r),因此基数排序的时间复杂度为O(d(n+r))

在基数排序中第一趟排序需要的辅助存储空间为r(创建r个队列),但以后的各趟排序中重复使用这些队列,所以总的辅助空间复杂度为O®

在基数排序中使用的是队列,排在后面的元素只能排在前面相同关键字元素的后面,相对位置不会发生改变,它是一种稳定的排序方法

例题

设待排序的表有10个记录,其关键字分别为{75,23,98,44,57,12,29,64,38,82}。说明采用基数排序方法进行排序的过程。

计数排序

排序思路

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

计数排序的特征

当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。

由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。

通俗地理解,例如有 10 个年龄不同的人,统计出有 8 个人的年龄比 A 小,那 A 的年龄就排在第 9 位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1 的原因。

排序过程

  1. 找出待排序的数组中最大和最小的元素
  2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
  3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
  4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

代码实现

public static int[] countingSort(int[] array) {//求出待排序数组的最大值int max = array[0];for (int num : array) {if (num>max){max = num;}}//定义一个max+1个桶,将值为i的元素放到第i个桶中int bucketLen = max + 1;//bucket[i]的值代表这个存储了数值为i的元素的数量int[] bucket = new int[bucketLen];// 将待排序数组放到对应的桶中// 比如数值10就要放到第10个桶中,对应bucket[10],// 放入时bucket[10]要+1,代表放入了一个元素for (int value : array) {bucket[value]++;}int sortedIndex = 0;// 遍历每一个桶for (int j = 0; j < bucketLen; j++) {// 如果桶不为空,取出桶中的元素放到待排序数组中while (bucket[j] > 0) {array[sortedIndex++] = j;bucket[j]--;}}return array;}

算法分析

计数排序的时间复杂度为 O(N+K)。因为算法过程中需要申请一个额外空间和一个与待排序集合大小相同的已排序空间,所以空间复杂度为 O(N+K)。由此可知,**计数排序只适用于元素值较为集中的情况,若集合中存在最大最小元素值相差甚远的情况,则计数排序开销较大、性能较差。**通过额外空间的作用方式可知,额外空间存储元素信息是通过计算元素与最小元素值的差值作为下标来完成的,若待排序集合中存在元素值为浮点数形式或其他形式,则需要对元素值或元素差值做变换,以保证所有差值都为一个非负整数形式。

桶排序

排序思路

**桶排序(Bucket sort)**或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间O(n)。但桶排序并不是比较排序,他不受到O(nlogn)下限的影响。

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

  1. 在额外空间充足的情况下,尽量增大桶的数量
  2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

什么时候最快:当输入的数据可以均匀的分配到每一个桶中。

什么时候最慢:当输入的数据被分配到了同一个桶中。

排序过程

桶排序以下列程序进行:

  1. 设置一个定量的数组当作空桶子。

  2. 遍历序列,并且把数组中的每个元素一个一个放到对应的桶子去。

  3. 对每个不是空的桶子进行排序。

  4. 从不是空的桶子里把元素取出来再放回原来的数组中。

代码实现

public static int[] bucketSort(int[] array, int bucketSize) {//求出待排序数组中的最小值和最大值int min = array[0];int max = array[0];for (int value : array) {if (min > value) {min = value;} else if (value > max) {max = value;}}//求出桶的数量int bucketCount = (int) Math.floor(max - min) / bucketSize + 1;//定义一个二维数组代表桶int[][] buckets = new int[bucketCount][0];//利用映射函数将数据分配到各个桶中for (int i = 0; i < array.length; i++) {//计算待排序数组的第i个元素应该放在哪个桶中int index = (int) Math.floor((array[i] - min) / bucketSize);//先将第index个桶的容量加1,用来存放第i个元素buckets[index] = Arrays.copyOf(buckets[index], buckets[index].length + 1);//然后将待排序数组的第i个元素放到对应的第index个桶中buckets[index][buckets[index].length - 1] = array[i];}int arrayIndex = 0;for (int[] bucket : buckets) {if (bucket.length <= 0) {continue;}//对每个桶进行排序,这里使用了插入排序bucket = InsertSort.insertSort(bucket);for (int value : bucket) {array[arrayIndex++] = value;}}return array;}

算法分析

时间复杂度:

  1. 将数据装入桶,需要N次循环

  2. 之后排序,需要M次循环

  3. 假设使用比较先进的排序算法,需要时间复杂度为O(N*logN)

  4. 平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)

最小时间复杂度O(N),此时 M=N
空间复杂度:M个桶的额外空间,以及N个元素的额外空间,O(N+M)
**稳定性:**一般来说桶排序是稳定的

算法优化:

  1. 桶排序的优化需要从排序上下功夫,尽量做到如下两点

  2. N个数都符合均匀分布,每一个桶中有N/M个数据

  3. 尽量的增大桶的数量,极限情况下每个桶只能得到一个数据,但是会增加空间复杂度

特点总结:

  1. 桶排序是稳定的
  2. 桶排序是常见排序里最快的一种,比快排还要快…(大多数情况下)
  3. 桶排序非常快,但是同时也非常耗空间,(基本上是最耗空间的一种排序算法)
    注意事项
    桶排序是一个简单快速的排序,需要新建一个 大范围的数组,即Buckets,因此桶排序有其局限性,适合元素值集合并不大的情况。

各种内排序方法的比较和选择

平均时间复杂度将排序方法分为下面3类:

  • 平方阶排序 O( n 2 n^2 n2) 排序:一般称为简单排序方法,例如直接插入排序、简单选择排序和冒泡排序。
  • 线性对数阶O(n l o g 2 log_2 log2​n)排序:如快速排序、堆排序和归并排序。
  • 线性阶排序O(n):如基数排序(假定数据的位数d和进制r为常量时)。

空间复杂度将排序方法分为下面3类:

  • O(n)归并排序、基数排序为O®
  • O( l o g 2 log_2 log2​n)快速排序
  • O(1)其他排序方法

稳定性将排序方法分为下面3类:

  • 不稳定的希尔排序、快速排序、堆排序、简单选择排序。
  • 稳定的其他排序方法

各种排序方法的性能

名词解释:

  • n:数据规模
  • k:"桶"的个数
  • In-place:占用常数内存,不占用额外内存
  • Out-place:占用额外内存
  • 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同

因为不同的排序方法适应不同的应用环境和要求,所以选择合适的排序方法应综合考虑以下因素:

  • 待排序的元素数目(问题规模)
  • 元素的大小(每个元素的规模)
  • 关键字的结构及其初始状态
  • 对稳定性的要求
  • 语言工具的条件
  • 排序数据的存储结构
  • 时间和辅助空间复杂度

选择排序算法时可参考以下结论:

  • 首先考虑排序对稳定性的要求,若要求稳定,则只能在稳定方法中选取,否则可以在所有方法中选取;其次要考虑待排序元素个数n的大小,若n较大,则可在改进方法中选取,否则在简单方法中选取;然后再考虑其他因素。

  • n较小(如n$\leq$50),可直接采用直接插入简单选择排序。一般地,直接插入排序较好,但简单选择排序移动的元素少于直接插入排序。

  • 文件初始状态基本有序(指正序),则选用直接插入冒泡排序为宜。

  • n较大,应采用时间复杂度为O(n l o g 2 log_2 log2​n)的排序方法,例如快速排序、堆排序或二路归并排序。快速排序是目前基于比较的内排序中被认为是较好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最少;但堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的坏情况。这两种排序都是不稳定的,若要求排序稳定,则可选用二路归并排序

  • 若需要将两个有序表合并成一个新的有序表,最好采用二路归并排序方法。

  • 基数排序可能在O(n)时间内完成对n个元素的排序。但遗憾的是,基数排序只适用于像字符串和整数这类有明显结构特征的关键字,而当关键字的取值范围属于某个无穷集合(例如实数型关键字)时无法使用基数排序,这时只有借助于“比较”的方法来排序。由此可知,若n很大,元素的关键字位数较少且可以分解时采用基数排序较好。

程序员必备十大排序算法相关推荐

  1. 凉哥核心圈程序员必备十大图书推荐(一)

    写在前面 凉哥核心圈程序员必备十大图书推荐(一),各位伙伴应该一目了然了哈,没错凉哥准备出一系列图书推荐的文章,其实很多朋友在私下问凉哥除了大学的课程外自己要不要读一些技术类的书籍呢,答案当时要的,但 ...

  2. 程序员必备十大技术网站推荐

    题外话 到今天上午为止,学完<Qt实战一二三>博主@一去丶二三里中,基于Widget的用户界面.布局管理.QPainter这几部分内容.也跟着实现了一些电子时钟,时钟绘制等效果.Qt给自己 ...

  3. 程序员必备十大学习网站,你真的都了解吗?

    一.开源中国 开源中国成立于2008年8月,是目前国内最大的开源技术社区,拥有超过200万会员,形成了由开源软件库.代码分享.资讯.协作翻译.码云.众包.招聘等几大模块内容,为IT开发者提供了一个发现 ...

  4. 「干货总结」程序员必知必会的十大排序算法

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:硬刚一周,3W字总结,一年的经验告诉你如何准备校招! 个人原创100W+访问量博客:点击前往,查看更多 绪论 身 ...

  5. 「归纳|总结」程序员必知必会的十大排序算法

    微信搜一搜「bigsai」关注这个有趣的程序员 新人原创公众号,求支持一下!你的点赞三连肯定对我至关重要! 文章已收录在 我的Github bigsai-algorithm 欢迎star 本文目录 绪 ...

  6. 程序员必知必会的十大排序算法

    绪论 身为程序员,十大排序是是所有合格程序员所必备和掌握的,并且热门的算法比如快排.归并排序还可能问的比较细致,对算法性能和复杂度的掌握有要求.bigsai作为一个负责任的Java和数据结构与算法方向 ...

  7. mysql外部排序算法_「干货总结」程序员必知必会的十大排序算法

    绪论 身为程序员,十大排序是是所有合格程序员所必备和掌握的,并且热门的算法比如快排.归并排序还可能问的比较细致,对算法性能和复杂度的掌握有要求.bigsai作为一个负责任的Java和数据结构与算法方向 ...

  8. 这或许是东半球分析十大排序算法最好的一篇文章

    作者 | 不该相遇在秋天 转载自五分钟学算法(ID:CXYxiaowu) 前言 本文全长 14237 字,配有 70 张图片和动画,和你一起一步步看懂排序算法的运行过程. 预计阅读时间 47 分钟,强 ...

  9. 八十八、Python | 十大排序算法系列(下篇)

    @Author:Runsen @Date:2020/7/10 人生最重要的不是所站的位置,而是内心所朝的方向.只要我在每篇博文中写得自己体会,修炼身心:在每天的不断重复学习中,耐住寂寞,练就真功,不畏 ...

最新文章

  1. 深度学习的五个能力级别
  2. 浅谈最近发布的金融行业多方安全计算的技术标准
  3. cookie和session的讲解
  4. mysql 优化设计库_MySQL 数据库最优化设计原则
  5. c# 访问修饰符的访问权限
  6. 数组-slice、indexOf
  7. Dashboard集群
  8. c++ n次方函数_高中数学必修一二次函数与幂函数试题及答案
  9. 搜索引擎的那些事(开篇)
  10. 机器学习、统计分析、数据挖掘、神经网络、人工智能、模式识别,
  11. iphone手机可不可以运行java_如何在不启动Xcode的情况下运行iPhone模拟器?
  12. User-Agent for Chrome浏览器模拟微信功能
  13. win10偶尔打不开开始菜单(按win键和点击开始菜单都没反应)
  14. 【hadoop生态之ZooKeeper】第二章Zookeeper安装【笔记+代码】
  15. win10,win11 下部署Vicuna-7B,Vicuna-13B模型,gpu cpu运行
  16. 赛力斯华为智选SF5入驻华为旗舰店,将通过华为零售渠道销售
  17. 软件项目管理——人力资源管理
  18. 应用程序无法正常启动(0xc000007b) 问题解决
  19. 杨辉三角c语言程序for循环,如何用C语言循环输出杨辉三角?
  20. 数控计算机键面英语怎么认,设备按钮面板 数控机床操作面板按键详解

热门文章

  1. 面向监狱编程 - 石胖子写网游外挂 (1) 请求是乱码
  2. B题 2010年上海世博会影响力的定量评估---数据曲线拟合
  3. 计算机毕业设计Java智能化管理的仓库管理(源码+系统+mysql数据库+lw文档)
  4. 凯撒密码(java python)
  5. vue项目的停止_基于Vue项目开发中遇到的坑及终结
  6. 无线图传发射模块静电浪涌测试
  7. Web前端开发技术————期末编程例题
  8. Elasticsearch插件开发之自定义搜索语法
  9. 高端商业建筑设计-锦沧文华广场中河建设
  10. 前端可视化——SVG矢量图技术