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

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) {
                // 对每组进行冒泡排序
            }
        }
    }
    

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)把小于基准值元素的子数列和大于基准值元素的子数列排序;


快排流程分析

以 {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、归并排序思想

  • 分 --> 治

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);

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 中
  • 右左半边的递归也是同样的道理

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++) {

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、排序算法的比较图

10.2、相关术语解释

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

exodus3

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值