6、希尔排序

6.1、简单插入排序问题



我们看简单的插入排序可能存在的问题,数组 arr = { 2, 3, 4, 5, 6, 1 } 这时需要插入的数 1(最小),简单插入排序的过程如下



结论: 当需要插入的数是较小的数时, 后移的次数明显增多, 对效率有影响

{2,3,4,5,6,6}
{2,3,4,5,5,6}
{2,3,4,4,5,6}
{2,3,3,4,5,6}
{2,2,3,4,5,6}
{1,2,3,4,5,6}



6.2、希尔排序基本介绍

  • 希尔排序是希尔(Donald Shell) 于 1959 年提出的一种排序算法。 希尔排序也是一种插入排序, 它是简单插入排序经过改进之后的一个更高效的版本, 也称为缩小增量排序。

6.3、希尔排序基本思想

  • 希尔排序按照增量将数组进行分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止

6.4、希尔排序图解(交换法)

  • 第一次:gap = arr.length/5 = 5 , 将数组分为五组,每个数组元素的索引相差 5

    • 如何完成第一次的排序?

      • 仔细想想,我们需要用一次循环将每组中的元素排序
      • 总共有五组,我们又需要一次循环
      • 所以完成每次排序,需要两层循环

    • 程序代码如下,把 i ,j 都看作是辅助指针:

      • i 与 j 配合使用,可以将指针从数组第一个元素,移动至最后一个元素,目的:把数组遍历一遍
      • j 与 i 配合使用,每次都从数组索引 i 处往前遍历,每次向前移动 gap 个位置,然后进行交换(冒泡排序的意思):看看前面的元素有没有比我的值大,如果前面的元素比我的值大,我就要和他交换位置,跑到前面去


// 希尔排序的第1轮排序
// 因为第1轮排序,是将10个数据分成了 5组
for (int i = 5; i < arr.length; i++) {
// 遍历各组中所有的元素(共5组,每组有2个元素), 步长5
for (int j = i - 5; j >= 0; j -= 5) {
// 如果当前元素大于加上步长后的那个元素,说明交换
if (arr[j] > arr[j + 5]) {
temp = arr[j];
arr[j] = arr[j + 5];
arr[j + 5] = temp;
}
}
}
  • 第二次:gap = gap /2 = 2; , 将数组分为两组,每个数组元素的索引相差 2

    • 第一组:

      • i = 2 时,数组从索引 2 处往前遍历,间隔为 2 :将 arr[0]、arr[2] 排序
      • i = 4 时,数组从索引 4 处往前遍历,间隔为 2 :将 arr[0]、arr[2]、arr[4] 排序
      • i = 6 时,数组从索引 6 处往前遍历,间隔为 2 :将 arr[0]、arr[2]、arr[4]、arr[6] 排序
      • i = 8 时,数组从索引 8 处往前遍历,间隔为 2 :将 arr[0]、arr[2]、arr[4]、arr[6]、arr[8] 排序

    • 第二组:

      • i = 3 时,数组从索引 3 处往前遍历,间隔为 2 :将 arr[1]、arr[3] 排序
      • i = 5 时,数组从索引 5 处往前遍历,间隔为 2 :将 arr[1]、arr[3]、arr[5] 排序
      • i = 7 时,数组从索引 7 处往前遍历,间隔为 2 :将 arr[1]、arr[3]、arr[5]、arr[7] 排序
      • i = 9 时,数组从索引 9 处往前遍历,间隔为 2 :将 arr[1]、arr[3]、arr[5]、arr[7]、arr[9] 排序


// 希尔排序的第2轮排序
// 因为第2轮排序,是将10个数据分成了 5/2 = 2组
for (int i = 2; i < arr.length; i++) {
// 遍历各组中所有的元素(共5组,每组有2个元素), 步长5
for (int j = i - 2; j >= 0; j -= 2) {
// 如果当前元素大于加上步长后的那个元素,说明交换
if (arr[j] > arr[j + 2]) {
temp = arr[j];
arr[j] = arr[j + 2];
arr[j + 2] = temp;
}
}
}
System.out.println("希尔排序2轮后=" + Arrays.toString(arr));
  • 第三次:gap = gap /2 = 1; , 将数组分为一组,每个数组元素的索引相差 1 ,对于交换法而言,这就是异常冒泡排序

    • i = 1 时,数组从索引 1 处往前遍历,间隔为 1 :将 arr[0]、arr[1] 排序
    • i = 2 时,数组从索引 2 处往前遍历,间隔为 1 :将 arr[0]、arr[1]、arr[2] 排序
    • i = 3 时,数组从索引 3 处往前遍历,间隔为 1 :将 arr[0]、arr[1]、arr[2]、arr[3] 排序

// 希尔排序的第3轮排序
// 因为第3轮排序,是将10个数据分成了 2/2 = 1组
for (int i = 1; i < arr.length; i++) {
// 遍历各组中所有的元素(共5组,每组有2个元素), 步长5
for (int j = i - 1; j >= 0; j -= 1) {
// 如果当前元素大于加上步长后的那个元素,说明交换
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
System.out.println("希尔排序3轮后=" + Arrays.toString(arr));
  • 总结:每次使用循环改变 gap 的值(初始值:数组大小/2 ,之后:gap = gap/2),然后在改变 gap 的循环中嵌套上面的双层 for 循环


改变 gap :for (int gap = arr.length / 2; gap > 0; gap /= 2) {



内层循环:实现对每组数组的排序

for (int i = gap; i < arr.length; i++) {
// 遍历各组中所有的元素(共gap组,每组有?个元素), 步长gap
for (int j = i - gap; j >= 0; j -= gap) {



希尔排序伪代码



for (int gap = arr.length / 2; gap > 0; gap /= 2) { for (int i = gap; i < arr.length; i++) { // 遍历各组中所有的元素(共gap组,每组有?个元素), 步长gap for (int j = i - gap; j >= 0; j -= gap) { // 对每组进行冒泡排序 } }}

尚硅谷算法与数据结构学习笔记07 -- 排序算法2_数据

尚硅谷算法与数据结构学习笔记07 -- 排序算法2_数组_02

6.5、代码实现

6.5.1、理解希尔排序(交换法)

  • 理解基于交换法的希尔排序
public class ShellSort {

public static void main(String[] args) {

int[] arr = { 8, 9, 1, 7, 2, 3, 5, 4, 6, 0 };
shellSort(arr);

}

// 使用逐步推导的方式来编写希尔排序
// 希尔排序时, 对有序序列在插入时采用交换法,
// 思路(算法) ===> 代码
public static void shellSort(int[] arr) {

int temp = 0;

// 希尔排序的第1轮排序
// 因为第1轮排序,是将10个数据分成了 5组
for (int i = 5; i < arr.length; i++) {
// 遍历各组中所有的元素(共5组,每组有2个元素), 步长5
for (int j = i - 5; j >= 0; j -= 5) {
// 如果当前元素大于加上步长后的那个元素,说明交换
if (arr[j] > arr[j + 5]) {
temp = arr[j];
arr[j] = arr[j + 5];
arr[j + 5] = temp;
}
}
}
System.out.println("希尔排序1轮后=" + Arrays.toString(arr));

// 希尔排序的第2轮排序
// 因为第2轮排序,是将10个数据分成了 5/2 = 2组
for (int i = 2; i < arr.length; i++) {
// 遍历各组中所有的元素(共5组,每组有2个元素), 步长5
for (int j = i - 2; j >= 0; j -= 2) {
// 如果当前元素大于加上步长后的那个元素,说明交换
if (arr[j] > arr[j + 2]) {
temp = arr[j];
arr[j] = arr[j + 2];
arr[j + 2] = temp;
}
}
}
System.out.println("希尔排序2轮后=" + Arrays.toString(arr));

// 希尔排序的第3轮排序
// 因为第3轮排序,是将10个数据分成了 2/2 = 1组
for (int i = 1; i < arr.length; i++) {
// 遍历各组中所有的元素(共5组,每组有2个元素), 步长5
for (int j = i - 1; j >= 0; j -= 1) {
// 如果当前元素大于加上步长后的那个元素,说明交换
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
System.out.println("希尔排序3轮后=" + Arrays.toString(arr));

}
}
  • 程序运行结果
希尔排序1轮后=[3, 5, 1, 6, 0, 8, 9, 4, 7, 2]
希尔排序2轮后=[0, 2, 1, 4, 3, 5, 7, 6, 9, 8]
希尔排序3轮后=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

6.5.2、编写希尔排序(交换法)

  • 编写基于交换法的希尔排序算法
public class ShellSort {

public static void main(String[] args) {

int[] arr = { 8, 9, 1, 7, 2, 3, 5, 4, 6, 0 };
shellSort(arr);

}

// 使用逐步推导的方式来编写希尔排序
// 希尔排序时, 对有序序列在插入时采用交换法,
// 思路(算法) ===> 代码
public static void shellSort(int[] arr) {

int temp = 0;
int count = 0;
// 根据前面的逐步分析,使用循环处理
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
// 遍历各组中所有的元素(共gap组,每组有?个元素), 步长gap
for (int j = i - gap; j >= 0; j -= gap) {
// 如果当前元素大于加上步长后的那个元素,说明交换
if (arr[j] > arr[j + gap]) {
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
System.out.println("希尔排序第" + (++count) + "轮 =" + Arrays.toString(arr));
}

}
  • 程序运行结果
希尔排序第1轮 =[3, 5, 1, 6, 0, 8, 9, 4, 7, 2]
希尔排序第2轮 =[0, 2, 1, 4, 3, 5, 7, 6, 9, 8]
希尔排序第3轮 =[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

6.5.3、测试希尔排序(交换法)性能

  • 测试基于交换法的希尔排序算法性能
public class ShellSort {

public static void main(String[] args) {

// 创建要给80000个的随机的数组
int[] arr = new int[80000];
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数
}

System.out.println("排序前");
Date date1 = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(date1);
System.out.println("排序前的时间是=" + date1Str);

shellSort(arr); // 交换式

Date data2 = new Date();
String date2Str = simpleDateFormat.format(data2);
System.out.println("排序前的时间是=" + date2Str);

}

// 使用逐步推导的方式来编写希尔排序
// 希尔排序时, 对有序序列在插入时采用交换法,
// 思路(算法) ===> 代码
public static void shellSort(int[] arr) {

int temp = 0;
int count = 0;
// 根据前面的逐步分析,使用循环处理
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
// 遍历各组中所有的元素(共gap组,每组有?个元素), 步长gap
for (int j = i - gap; j >= 0; j -= gap) {
// 如果当前元素大于加上步长后的那个元素,说明交换
if (arr[j] > arr[j + gap]) {
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
}
}

}
  • 程序运行结果
排序前
排序前的时间是=2020-07-16 10:22:27
排序前的时间是=2020-07-16 10:22:33
  • 分析:由于使用交换法实现希尔排序算法,所以基于交换法的希尔排序算法比简单选择排序算法更慢,所以我们一定要编写基于插入法的希尔排序算法

6.5.4、编写希尔排序(插入法)

  • 编写基于插入法的希尔排序算法:

    • 记录当前位置的元素值 int temp = arr[j]; ,从当前元素前一个位置开始,往前寻找,每次移动 gap 个距离

      • 如果 temp < arr[j - gap] :

        • 将数组元素后移,腾出插入空间:arr[j] = arr[j - gap];
        • 然后继续往前找:j -= gap;

      • 如果 temp > arr[j - gap] ,找到插入位置,执行插入 arr[j] = temp; ,因为在上一步已经腾出了插入空间,并且将指针 j 前移,所以可直接插入
      • 如果 找到数组最前面还是没有找到插入位置:j - gap < 0 ,则证明 temp 需要插入在数组最前面

    • 仅仅就是将之前交换法的冒泡操作替换成了插入操作

public class ShellSort {

public static void main(String[] args) {

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

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

shellSort(arr);

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

}

// 对交换式的希尔排序进行优化->移位法
public static void shellSort(int[] arr) {
// 增量gap, 并逐步的缩小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
// 从第gap个元素,逐个对其所在的组进行直接插入排序
for (int i = gap; i < arr.length; i++) {
int j = i;
int temp = arr[j];
if (arr[j] < arr[j - gap]) {
while (j - gap >= 0 && temp < arr[j - gap]) {
// 移动
arr[j] = arr[j - gap];
j -= gap;
}
// temp 比 arr[j - gap] 大,所以需要插入在 j 的位置
arr[j] = temp;
}

}
}
}

}
  • 程序运行结果
排序前
[8, 9, 1, 7, 2, 3, 5, 4, 6, 0]
排序前
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

6.5.5、测试希尔排序(插入法)性能

  • 测试基于插入法的希尔排序算法性能
public class ShellSort {

public static void main(String[] args) {

// 创建要给80000个的随机的数组
int[] arr = new int[80000];
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数
}

System.out.println("排序前");
Date date1 = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(date1);
System.out.println("排序前的时间是=" + date1Str);

shellSort(arr); // 交换式

Date data2 = new Date();
String date2Str = simpleDateFormat.format(data2);
System.out.println("排序前的时间是=" + date2Str);

}

// 对交换式的希尔排序进行优化->移位法
public static void shellSort(int[] arr) {
// 增量gap, 并逐步的缩小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
// 从第gap个元素,逐个对其所在的组进行直接插入排序
for (int i = gap; i < arr.length; i++) {
int j = i;
int temp = arr[j];
if (arr[j] < arr[j - gap]) {
while (j - gap >= 0 && temp < arr[j - gap]) {
// 移动
arr[j] = arr[j - gap];
j -= gap;
}
// 当退出while后,就给temp找到插入的位置
arr[j] = temp;
}

}
}
}

}
  • 程序运行结果:1s 都不到,果然快啊
排序前
排序前的时间是=2020-07-16 11:02:20
排序后的时间是=2020-07-16 11:02:20
  • 八百万个数据的测试结果
排序前
排序前的时间是=2020-07-16 14:38:55
排序前的时间是=2020-07-16 14:38:57

7、快速排序

7.1、快排简介


  1. 快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
  2. 快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
  3. 快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
  4. 快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。
  5. 虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:
  6. 快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

7.2、代码思路


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

尚硅谷算法与数据结构学习笔记07 -- 排序算法2_排序算法_03


快排流程分析

以 {25, 84, 21, 47, 15, 27, 68, 35, 20} 数列为例(下面的流程和上面的动图其实不太一样,不过大体思想是一样的)


  1. 第一趟:val = 25; 先取出来保存着

    • {20, 84, 21, 47, 15, 27, 68, 35, 20}
    • {20, 84, 21, 47, 15, 27, 68, 35, 84}
    • {20, 15, 21, 47, 15, 27, 68, 35, 84}
    • {20, 15, 21, 47, 47, 27, 68, 35, 84}
    • {20, 15, 21, 25, 47, 27, 68, 35, 84}

  2. 第二趟:val = 20; 先取出来保存着

    • {15, 15, 21}
    • {15, 20, 21}

  3. 以此类推 …

7.3、代码实现

7.3.1、编写快排算法

  • 快排代码
private static void quickSort(int[] arr, int left, int right) {
if (left < right) {
int partitionIndex = partition(arr, left, right);
quickSort(arr, left, partitionIndex - 1);
quickSort(arr, partitionIndex + 1, right);
}
}

private static int partition(int[] arr, int left, int right) {
int pivot = arr[left];
//终止while循环以后left和right一定相等的
while (left < right) {
while (left < right && arr[right] >= pivot) {
--right;
}
arr[left] = arr[right];
while (left < right && arr[left] <= pivot) {
++left;
}
arr[right] = arr[left];
}
arr[left] = pivot;
//right可以改为left
return left;
}
  • 测试代码
public static void main(String[] args) {
int[] arr = {25, 84, 21, 47, 15, 27, 68, 35, 20};
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
  • 程序输出
arr=[15, 20, 21, 25, 27, 35, 47, 68, 84]

7.3.2、测试快速排序性能

  • 编测试快速排序算法性能
public class QuickSort {

public static void main(String[] args) {

// 创建要给80000个的随机的数组
int[] arr = new int[80000];
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数
}

System.out.println("排序前");
Date date1 = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(date1);
System.out.println("排序前的时间是=" + date1Str);

quickSort(arr, 0, arr.length - 1); // 交换式

Date data2 = new Date();
String date2Str = simpleDateFormat.format(data2);
System.out.println("排序前的时间是=" + date2Str);

}

private static void quickSort(int[] arr, int left, int right) {
if (left < right) {
int partitionIndex = partition(arr, left, right);
quickSort(arr, left, partitionIndex - 1);
quickSort(arr, partitionIndex + 1, right);
}
}

private static int partition(int[] arr, int left, int right) {
int pivot = arr[left];
// 终止while循环以后left和right一定相等的
while (left < right) {
while (left < right && arr[right] >= pivot) {
--right;
}
arr[left] = arr[right];
while (left < right && arr[left] <= pivot) {
++left;
}
arr[right] = arr[left];
}
arr[left] = pivot;
// right可以改为left
return left;
}
}
  • 程序运行结果:卧槽,八百个数据只需要 1s ,甚至可能还不到。。。
排序前
排序前的时间是=2020-08-06 18:43:44
排序前的时间是=2020-08-06 18:43:44

8、归并排序

8.1、归并排序基本介绍


  • 归并排序(MERGE-SORT) 是利用归并的思想实现的排序方法, 该算法采用经典的分治(divide-and-conquer)策略
  • 分治法将问题分(divide)成一些小的问题然后递归求解, 而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起, 即分而治之

8.2、归并排序思想

  • 分 --> 治

尚硅谷算法与数据结构学习笔记07 -- 排序算法2_数组_04

8.3、归并排序代码思路


  • 合并时,其实是拿着原数组(arr)中两个相邻的子数组(arr1、arr2)进行合并,我们使用三个指针,来表示两个子数组在原数组中的位置

    • arr[left] ~ arr[mid] 为 arr1
    • arr[mid + 1] ~ arr[right] 为 arr2

  • 如何合并?

    • 首先,需要一个临时的 temp 数组,其大小与原数组 arr 一样
    • 定义辅助指针 i 遍历 arr1 ,定义辅助指针 j 遍历 arr2 ,原则就是,把 arr1 和 arr2 中的数往 temp 中放,使得 temp[left] ~ temp[right] 是有序数组
    • 最后把 temp 临时数组中的数据拷贝回原数组中(个人认为,最后一下次再拷贝回去就行。。。)

  • 如何分?

    • 向左递归拆分:mergeSort(arr, left, mid, temp);
    • 向右递归拆分:mergeSort(arr, mid + 1, right, temp);


尚硅谷算法与数据结构学习笔记07 -- 排序算法2_排序算法_05

8.4、代码实现

8.4.1、编写归并排序算法

  • 归并排序算法实现代码
public class MergetSort {

public static void main(String[] args) {

int arr[] = { 8, 4, 5, 7, 1, 3, 6, 2 };
int temp[] = new int[arr.length]; // 归并排序需要一个额外空间
mergeSort(arr, 0, arr.length - 1, temp);
System.out.println("归并排序后=" + Arrays.toString(arr));

}

// 分+合方法
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2; // 中间索引
// 向左递归进行分解
mergeSort(arr, left, mid, temp);
// 向右递归进行分解
mergeSort(arr, mid + 1, right, temp);
// 合并
merge(arr, left, mid, right, temp);
}
}

// 合并的方法
/**
*
* @param arr 排序的原始数组
* @param left 左边有序序列的初始索引
* @param mid 中间索引
* @param right 右边索引
* @param temp 做中转的数组
*/
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {

int i = left; // 初始化i, 左边有序序列的初始索引
int j = mid + 1; // 初始化j, 右边有序序列的初始索引
int t = 0; // 指向temp数组的当前索引

// (一)
// 先把左右两边(有序)的数据按照规则填充到temp数组
// 直到左右两边的有序序列,有一边处理完毕为止
while (i <= mid && j <= right) {// 继续
// 如果左边的有序序列的当前元素,小于等于右边有序序列的当前元素
// 即将左边的当前元素,填充到 temp数组
// 然后 t++, i++
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
t += 1;
i += 1;
} else { // 反之,将右边有序序列的当前元素,填充到temp数组
temp[t] = arr[j];
t += 1;
j += 1;
}
}

// (二)
// 把有剩余数据的一边的数据依次全部填充到temp
while (i <= mid) { // 左边的有序序列还有剩余的元素,就全部填充到temp
temp[t] = arr[i];
t += 1;
i += 1;
}

while (j <= right) { // 右边的有序序列还有剩余的元素,就全部填充到temp
temp[t] = arr[j];
t += 1;
j += 1;
}

// (三)
// 将temp数组的元素拷贝到arr
// 注意,并不是每次都拷贝所有
t = 0;
int tempLeft = left; //
// 第一次合并 tempLeft = 0 , right = 1 //第二次: tempLeft = 2 right = 3 //第三次: tL=0 ri=3
// 最后一次 tempLeft = 0 right = 7
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
t += 1;
tempLeft += 1;
}

}

}
  • 程序运行结果
    归并排序后=[1, 2, 3, 4, 5, 6, 7, 8]

8.4.2、测试归并排序性能

  • 测试归并排序算法的性能
public class MergetSort {

public static void main(String[] args) {

// 测试快排的执行速度
// 创建要给80000个的随机的数组
int[] arr = new int[8000000];
for (int i = 0; i < 8000000; i++) {
arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数
}
System.out.println("排序前");
Date data1 = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(data1);
System.out.println("排序前的时间是=" + date1Str);

int temp[] = new int[arr.length]; // 归并排序需要一个额外空间
mergeSort(arr, 0, arr.length - 1, temp);

Date data2 = new Date();
String date2Str = simpleDateFormat.format(data2);
System.out.println("排序前的时间是=" + date2Str);

// System.out.println("归并排序后=" + Arrays.toString(arr));
}

// 分+合方法
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2; // 中间索引
// 向左递归进行分解
mergeSort(arr, left, mid, temp);
// 向右递归进行分解
mergeSort(arr, mid + 1, right, temp);
// 合并
merge(arr, left, mid, right, temp);
}
}

// 合并的方法
/**
*
* @param arr 排序的原始数组
* @param left 左边有序序列的初始索引
* @param mid 中间索引
* @param right 右边索引
* @param temp 做中转的数组
*/
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {

int i = left; // 初始化i, 左边有序序列的初始索引
int j = mid + 1; // 初始化j, 右边有序序列的初始索引
int t = 0; // 指向temp数组的当前索引

// (一)
// 先把左右两边(有序)的数据按照规则填充到temp数组
// 直到左右两边的有序序列,有一边处理完毕为止
while (i <= mid && j <= right) {// 继续
// 如果左边的有序序列的当前元素,小于等于右边有序序列的当前元素
// 即将左边的当前元素,填充到 temp数组
// 然后 t++, i++
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
t += 1;
i += 1;
} else { // 反之,将右边有序序列的当前元素,填充到temp数组
temp[t] = arr[j];
t += 1;
j += 1;
}
}

// (二)
// 把有剩余数据的一边的数据依次全部填充到temp
while (i <= mid) { // 左边的有序序列还有剩余的元素,就全部填充到temp
temp[t] = arr[i];
t += 1;
i += 1;
}

while (j <= right) { // 右边的有序序列还有剩余的元素,就全部填充到temp
temp[t] = arr[j];
t += 1;
j += 1;
}

// (三)
// 将temp数组的元素拷贝到arr
// 注意,并不是每次都拷贝所有
t = 0;
int tempLeft = left; //
// 第一次合并 tempLeft = 0 , right = 1 //第二次: tempLeft = 2 right = 3 //第三次: tL=0 ri=3
// 最后一次 tempLeft = 0 right = 7
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
t += 1;
tempLeft += 1;
}

}

}
  • 程序运行结果:八百万数据用了 1s ,也挺快
排序前
排序前的时间是=2020-07-16 16:18:32
排序前的时间是=2020-07-16 16:18:33

8.5、总结


  • 先将数组分为左右两半,先执行左半边递归:

    • 首先执行左递归到最深层,条件 if (left < right) 不满足,开始执行合并,合并 { 8, 4 } 到临时数组 temp 中,变为有序数组 { 4, 8 } ,再拷贝回原数组 arr 中
    • 然后执行最深层的右递归,条件 if (left < right) 不满足,开始执行合并,合并 { 5, 7 } 到临时数组 temp 中,变为有序数组 { 2, 7 } ,再拷贝回原数组 arr 中
    • 合并完后,递归回溯至上一节,开始执行合并,合并 { 4, 5, 7, 8 } 到临时数组 temp 中,变为有序数组 { 4, 5, 7, 8 } ,再拷贝回原数组 arr 中

  • 右左半边的递归也是同样的道理

尚硅谷算法与数据结构学习笔记07 -- 排序算法2_算法_06

9、基数排序

9.1、基数排序基本介绍


  • 基数排序(radix sort) 属于“分配式排序” (distribution sort) , 又称“桶子法” (bucket sort) 或 bin sort, 顾名思义, 它是通过键值的各个位的值, 将要排序的元素分配至某些“桶” 中, 达到排序的作用
  • 基数排序法是属于稳定性的排序, 基数排序法的是效率高的稳定性排序法
  • 基数排序(Radix Sort)是桶排序的扩展
  • 基数排序是 1887 年赫尔曼· 何乐礼发明的。 它是这样实现的: 将整数按位数切割成不同的数字, 然后按每个位数分别比较。

9.2、基数排序思想


  • 将所有待比较数值统一为同样的数位长度, 数位较短的数前面补零。
  • 然后, 从最低位开始, 依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

9.3、基数排序图解



有 10 个桶,对应编号为 0~9



步骤


  • 第一步:根据原数组 arr 中每个元素的个位数,将其依次放入 0~9 号桶中(每个桶从前往后放),放置完毕后,再将桶中的数据依次取出(每个桶从前往后取),放回原数组 arr 中,这样原数组 arr 中个位数的元素就已经按照顺序排好了
  • 第二步:根据原数组 arr 中每个元素的十位数,将其依次放入 0~9 号桶中(每个桶从前往后放),放置完毕后,再将桶中的数据依次取出(每个桶从前往后取),放回原数组 arr 中,这样原数组 arr 中十位数 + 个位数的元素就已经按照顺序排好了
  • 第三步:根据原数组 arr 中每个元素的百位数,将其依次放入 0~9 号桶中(每个桶从前往后放),放置完毕后,再将桶中的数据依次取出(每个桶从前往后取),放回原数组 arr 中,这样原数组 arr 中百位数 + 十位数 + 个位数的元素就已经按照顺序排好了



何时排序完毕?当数组中最长位数的元素处理完毕,排序完成



桶的容量如何确定?假设数组每个元素位数相同,那么单个桶最大容量即为数组容量,我们用一个二维数组来表示桶:​​int[][] bucket = new int[10][arr.length];​



我们如何知道每桶中装了几个元素?这也需要记录,用一个一维数组来记录:

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



总结:



假设数组中元素的最长位数为 maxLength ,则处理完 maxLength 位数后,数组排序完毕:*for(int i = 0 , n = 1; i < maxLength; i++, n = 10) {



使用一个 for 循环处理原一维数组 arr ,将其放入桶中

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



使用两层 for 循环,处理 10 个 桶,将其中的元素放回原一维数组中

for (int k = 0; k < bucketElementCounts.length; k++) {
if (bucketElementCounts[k] != 0) {
for (int l = 0; l < bucketElementCounts[k]; l++) {





尚硅谷算法与数据结构学习笔记07 -- 排序算法2_算法_07

9.4、代码实现

9.4.1、理解基数排序

  • 逐步分解,理解基数排序算法
public class RadixSort {

public static void main(String[] args) {

int arr[] = { 53, 3, 542, 748, 14, 214};
radixSort(arr);
System.out.println("基数排序后 " + Arrays.toString(arr));

}

//基数排序方法
public static void radixSort(int[] arr) {

// //根据前面的推导过程,我们可以得到最终的基数排序代码

//1. 得到数组中最大的数的位数
int max = arr[0]; //假设第一数就是最大数
for(int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}

//定义一个二维数组,表示10个桶, 每个桶就是一个一维数组
//说明
//1. 二维数组包含10个一维数组
//2. 为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为arr.length
//3. 名明确,基数排序是使用空间换时间的经典算法
int[][] bucket = new int[10][arr.length];

//为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数
//可以这里理解
//比如:bucketElementCounts[0] , 记录的就是 bucket[0] 桶的放入数据个数
int[] bucketElementCounts = new int[10];


//第1轮(针对每个元素的个位进行排序处理)
for(int j = 0; j < arr.length; j++) {
//取出每个元素的个位的值
int digitOfElement = arr[j] / 1 % 10;
//放入到对应的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
bucketElementCounts[digitOfElement]++;
}
//按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
int index = 0;
//遍历每一桶,并将桶中是数据,放入到原数组
for(int k = 0; k < bucketElementCounts.length; k++) {
//如果桶中,有数据,我们才放入到原数组
if(bucketElementCounts[k] != 0) {
//循环该桶即第k个桶(即第k个一维数组), 放入
for(int l = 0; l < bucketElementCounts[k]; l++) {
//取出元素放入到arr
arr[index++] = bucket[k][l];
}
}
//第l轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
bucketElementCounts[k] = 0;
}
System.out.println("第1轮,对个位的排序处理 arr =" + Arrays.toString(arr));


//第2轮(针对每个元素的十位进行排序处理)
for (int j = 0; j < arr.length; j++) {
// 取出每个元素的十位的值
int digitOfElement = arr[j] / 10 % 10; //748 / 10 => 74 % 10 => 4
// 放入到对应的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
bucketElementCounts[digitOfElement]++;
}
// 按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
index = 0;
// 遍历每一桶,并将桶中是数据,放入到原数组
for (int k = 0; k < bucketElementCounts.length; k++) {
// 如果桶中,有数据,我们才放入到原数组
if (bucketElementCounts[k] != 0) {
// 循环该桶即第k个桶(即第k个一维数组), 放入
for (int l = 0; l < bucketElementCounts[k]; l++) {
// 取出元素放入到arr
arr[index++] = bucket[k][l];
}
}
//第2轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
bucketElementCounts[k] = 0;
}
System.out.println("第2轮,对个位的排序处理 arr =" + Arrays.toString(arr));


//第3轮(针对每个元素的百位进行排序处理)
for (int j = 0; j < arr.length; j++) {
// 取出每个元素的百位的值
int digitOfElement = arr[j] / 100 % 10; // 748 / 100 => 7 % 10 = 7
// 放入到对应的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
bucketElementCounts[digitOfElement]++;
}
// 按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
index = 0;
// 遍历每一桶,并将桶中是数据,放入到原数组
for (int k = 0; k < bucketElementCounts.length; k++) {
// 如果桶中,有数据,我们才放入到原数组
if (bucketElementCounts[k] != 0) {
// 循环该桶即第k个桶(即第k个一维数组), 放入
for (int l = 0; l < bucketElementCounts[k]; l++) {
// 取出元素放入到arr
arr[index++] = bucket[k][l];
}
}
//第3轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
bucketElementCounts[k] = 0;
}
System.out.println("第3轮,对个位的排序处理 arr =" + Arrays.toString(arr));
}

}
  • 程序运行结果
第1轮,对个位的排序处理 arr =[542, 53, 3, 14, 214, 748]
第2轮,对个位的排序处理 arr =[3, 14, 214, 542, 748, 53]
第3轮,对个位的排序处理 arr =[3, 14, 53, 214, 542, 748]
基数排序后 [3, 14, 53, 214, 542, 748]

9.4.2、编写基数排序

  • 编写基数排序算法
public class RadixSort {

public static void main(String[] args) {

int arr[] = { 53, 3, 542, 748, 14, 214 };
radixSort(arr);
System.out.println("基数排序后 " + Arrays.toString(arr));

}

// 基数排序方法
public static void radixSort(int[] arr) {

//根据前面的推导过程,我们可以得到最终的基数排序代码
//1. 得到数组中最大的数的位数
int max = arr[0]; //假设第一数就是最大数
for(int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
//得到最大数是几位数
int maxLength = (max + "").length();

//定义一个二维数组,表示10个桶, 每个桶就是一个一维数组
//说明
//1. 二维数组包含10个一维数组
//2. 为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为arr.length
//3. 名明确,基数排序是使用空间换时间的经典算法
int[][] bucket = new int[10][arr.length];

//为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数
//可以这里理解
//比如:bucketElementCounts[0] , 记录的就是 bucket[0] 桶的放入数据个数
int[] bucketElementCounts = new int[10];


// n=1 表示处理个位,n=10表示处理十位,n=100表示处理百位 ......
for(int i = 0 , n = 1; i < maxLength; i++, n *= 10) {
//(针对每个元素的对应位进行排序处理), 第一次是个位,第二次是十位,第三次是百位..
for(int j = 0; j < arr.length; j++) {
//取出每个元素的对应位的值
int digitOfElement = arr[j] / n % 10;
//放入到对应的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
bucketElementCounts[digitOfElement]++;
}
//按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
int index = 0;
//遍历每一桶,并将桶中的数据,放入到原数组
for(int k = 0; k < bucketElementCounts.length; k++) {
//如果桶中,有数据,我们才放入到原数组
// 遍历第k个桶(即第k个一维数组), 将桶中的数据放回原数组中
for (int l = 0; l < bucketElementCounts[k]; l++) {
// 取出元素放入到arr
arr[index++] = bucket[k][l];
}
//第i+1轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
bucketElementCounts[k] = 0;
}
System.out.println("第"+(i+1)+"轮,对个位的排序处理 arr =" + Arrays.toString(arr));
}
}

}
  • 程序运行结果

第1轮,对个位的排序处理 arr =[542, 53, 3, 14, 214, 748]

第2轮,对个位的排序处理 arr =[3, 14, 214, 542, 748, 53]

第3轮,对个位的排序处理 arr =[3, 14, 53, 214, 542, 748]

基数排序后 [3, 14, 53, 214, 542, 748]


9.4.3、测试基数排序性能

  • 测试基数排序算法的性能
public class RadixSort {

public static void main(String[] args) {

// 80000000 * 11 * 4 / 1024 / 1024 / 1024 =3.3G
int[] arr = new int[8000000];
for (int i = 0; i < 8000000; i++) {
arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数
}
System.out.println("排序前");
Date data1 = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(data1);
System.out.println("排序前的时间是=" + date1Str);

radixSort(arr);

Date data2 = new Date();
String date2Str = simpleDateFormat.format(data2);
System.out.println("排序前的时间是=" + date2Str);


}

// 基数排序方法
public static void radixSort(int[] arr) {

//根据前面的推导过程,我们可以得到最终的基数排序代码
//1. 得到数组中最大的数的位数
int max = arr[0]; //假设第一数就是最大数
for(int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
//得到最大数是几位数
int maxLength = (max + "").length();

//定义一个二维数组,表示10个桶, 每个桶就是一个一维数组
//说明
//1. 二维数组包含10个一维数组
//2. 为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为arr.length
//3. 名明确,基数排序是使用空间换时间的经典算法
int[][] bucket = new int[10][arr.length];

//为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数
//可以这里理解
//比如:bucketElementCounts[0] , 记录的就是 bucket[0] 桶的放入数据个数
int[] bucketElementCounts = new int[10];


// n=1 表示处理个位,n=10表示处理十位,n=100表示处理百位 ......
for(int i = 0 , n = 1; i < maxLength; i++, n *= 10) {
//(针对每个元素的对应位进行排序处理), 第一次是个位,第二次是十位,第三次是百位..
for(int j = 0; j < arr.length; j++) {
//取出每个元素的对应位的值
int digitOfElement = arr[j] / n % 10;
//放入到对应的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
bucketElementCounts[digitOfElement]++;
}
//按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
int index = 0;
//遍历每一桶,并将桶中的数据,放入到原数组
for(int k = 0; k < bucketElementCounts.length; k++) {
//如果桶中,有数据,我们才放入到原数组
// 遍历第k个桶(即第k个一维数组), 将桶中的数据放回原数组中
for (int l = 0; l < bucketElementCounts[k]; l++) {
// 取出元素放入到arr
arr[index++] = bucket[k][l];
}
//第i+1轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
bucketElementCounts[k] = 0;
}
System.out.println("第"+(i+1)+"轮,对个位的排序处理 arr =" + Arrays.toString(arr));
}
}

}
  • 程序运行结果:可以啊,八百万数据 1s 就排好了,但是太占空间了

排序前

排序前的时间是=2020-07-16 18:16:21

排序前的时间是=2020-07-16 18:16:22


9.5、基数排序的说明


  • 基数排序是对传统桶排序的扩展, 速度很快
  • 基数排序是经典的空间换时间的方式, 占用内存很大,当对海量数据排序时, 容易造成 OutOfMemoryError 。
  • 基数排序时稳定的。 [注:假定在待排序的记录序列中, 存在多个具有相同的关键字的记录, 若经过排序, 这些记录的相对次序保持不变, 即在原序列中, r[i]=r[j], 且 r[i]在 r[j]之前, 而在排序后的序列中, r[i]仍在 r[j]之前,则称这种排序算法是稳定的; 否则称为不稳定的]
  • 有负数的数组, 我们不用基数排序来进行排序, 如果要支持负数, 参考: https://code.i-harness.com/zh-CN/q/e98fa9

10、常用排序算法总结和对比

10.1、排序算法的比较图

尚硅谷算法与数据结构学习笔记07 -- 排序算法2_数组_08

10.2、相关术语解释


  • 稳定:如果 a 原本在 b 前面, 而 a=b, 排序之后 a 仍然在 b 的前面;
  • 不稳定:如果 a 原本在 b 的前面, 而 a=b, 排序之后 a 可能会出现在 b 的后面;
  • 内排序: 所有排序操作都在内存中完成;
  • 外排序: 由于数据太大, 因此把数据放在磁盘中, 而排序通过磁盘和内存的数据传输才能进行;
  • 时间复杂度: 一个算法执行所耗费的时间。
  • 空间复杂度: 运行完一个程序所需内存的大小。
  • n: 数据规模
  • k: “桶” 的个数
  • In-place:不占用额外内存
  • Out-place:占用额外内存