大家好,我是皮皮猫吖!
每文一言:请再悄悄加点油,无论如何都想听你说:“我终于成为不负众望的人了”。
本篇文章:
主要是关于java数据结构与算法的一些基本知识:常用排序算法:冒泡、选择、插入、希尔、快速、归并、基数、堆排序等【后续会补充堆排序】
正文如下:
一、排序算法
1)排序算法的介绍:
排序也称排序算法 (Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程。
2)排序的分类:
① 内部排序:
指将需要处理的所有数据都加载到内部存储器中进行排序。
② 外部排序法:
数据量过大,无法全部加载到内存中,需要借助外部存储进行
排序。
3) 常见的排序算法分类(见下图):
4)算法的时间复杂度:
度量一个程序(算法)执行时间的两种方法:
① 事后统计的方法
这种方法可行, 但是有两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素, 这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快。
② 事前估算的方法
通过分析某个算法的时间复杂度来判断哪个算法更优。
③ 时间频度
时间频度:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。[举例说明]
例子:计算1—100所有数字之和:
解法一:T(n) = n + 1
解法二:T(n) = 1
④ 忽略常数项:
结论:
1)2n+20 和 2n 随着n 变大,执行曲线无限接近, 20可以忽略
2)3n+10 和 3n 随着n 变大,执行曲线无限接近, 10可以忽略
⑤ 忽略低次项:
结论:
2)2n^2+3n+10 和 2n^2 随着n 变大, 执行曲线无限接近, 可以忽略 3n+10
1)n^2+5n+20 和 n^2 随着n 变大,执行曲线无限接近, 可以忽略 5n+20
⑥ 忽略系数
结论:
1)随着n值变大,5n^2+7n 和 3n^2 + 2n ,执行曲线重合, 说明 这种情况下, 5和3可以忽略。
2)而n^3+5n 和 6n^3+4n ,执行曲线分离,说明多少次方式关键
★★★⑦ 时间复杂度:
1)一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作 T(n)=O( f(n) ),称O( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度。
2)T(n) 不同,但时间复杂度可能相同。 如:T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的T(n) 不同,但时间复杂度相同,都为O(n²)。
3)计算时间复杂度的方法:
① 用常数1代替运行时间中的所有加法常数 T(n)=n²+7n+6 => T(n)=n²+7n+1
② 修改后的运行次数函数中,只保留最高阶项 T(n)=n²+7n+1 => T(n) = n²
③ 去除最高阶项的系数 T(n) = n² => T(n) = n² => O(n²)
★★★⑧ 常见的时间复杂度
1)常数阶O(1)
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)
上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。
2)对数阶O(log2n)
说明:
在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2n也就是说当循环 log2n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(log2n) 。 O(log2n) 的这个2 时间上是根据代码变化的,i = i * 3 ,则是 O(log3n) 。
上述时间复杂度:受到n和底数的影响,所以,时间复杂度为O(logan)。
3)线性阶O(n)
说明:
该段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度。
上述时间复杂度:只与n有关系,与其他都没有关系。所以,时间复杂度为O(n)。
4)线性对数阶O(nlog2n)
说明:线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(log2n)的代码循环N遍的话,那么它的时间复杂度就是 n * O(log2N),也就是了O(nlog2N)。
上述时间复杂度:外层与n有关系,内层与log2n有关系,所以,时间复杂度为O(nlog2n)。
5)平方阶O(n^2)
说明:平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n * n),即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(m * n)
上述时间复杂度:外程与n有关系,内层与m有关系,所以,时间复杂度为O(n^2)
6)立方阶O(n^3)
与上面的O(n²) 很相似,O(n³)相当于三层n循环
7)k次方阶O(n^k)
与上面的O(n²) 很相似,O(n^k)相当于k层n循环
8)指数阶O(2^n)
结论:
1)常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)< Ο(nk) <Ο(2n) ,随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
2)从图中可见,我们应该尽可能避免使用指数阶的算法。
⑨ 平均复杂度和最坏时间复杂度
1)平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。
2)最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。
3)平均时间复杂度和最坏时间复杂度是否一致,和算法有关(如下图):
⑩ 算法的空间复杂度
1)类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数。
2)空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况。
3)在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。 一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间。
5)冒泡排序:
① 冒泡排序的介绍:
冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
② 冒泡排序的优化:
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,在冒泡排序写好后,再进行优化)。
③ 冒泡排序的的过程分析:
举一个具体的案例来说明冒泡法。将五个无序的数:3, 9, -1, 10, -2 使用冒泡排序法将其排成一个从小到大的有序数列:
④ 冒泡排序的代码实现:
package com.data.structure.study6.sort;
import org.omg.CORBA.Current;
import java.util.Random;
/**
* 冒泡排序的代码分析
* @author imppm
* @create 2021-03-20-9:28
*/
public class Sort1BubbleDemo1 {
public static void main(String[] args) {
//测试普通冒泡排序
//需要遍历的数组
//int[] array = {3,9,-1,10,-2};
//冒泡排序数组
//bubbleSort1(array);
//遍历数组
//list(array);
//测试优化过后的冒泡排序
// int[] array = {3,9,-1,10,20};
// bubbleSort2(array);
//测试一下冒泡排序的时间复杂度
//创建一个80000数据的数组
int[] array = new int[80000];
Random random = new Random();
for (int i = 0; i < 80000; i++){
array[i] = random.nextInt();
}
//数组排序前的初始时间
long begin = System.currentTimeMillis();
bubbleSort(array);
//数组排序后的结束时间
long end = System.currentTimeMillis();
System.out.println("冒泡排序,排序80000个数据的数组,大概耗时"+(end-begin)+"ms。");
}
//冒泡排序的时间复杂度:O(n^2)
private static void bubbleSort1(int[] array){
//用作交换的变量
int temp;
//大循环:比较array.length-1次
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]){
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
System.out.printf("第%d趟的第%d次比较之后的数组:",i+1,j+1);
list(array);
}
System.out.println("------------------------------");
}
}
//冒泡排序的优化:在新的一轮中,如果没有发生一次交换的话,
// 在以后的交换判断中,是不会发生交换的
public static void bubbleSort2(int[] array){
//交换变量
int temp;
//标志位,判断在当前这一趟,是否发生交换
boolean flag = false;
//冒泡排序:需要比较array.length-1趟
for (int i = 0; i < array.length-1; i++){
//标志位置为false
flag = false;
for (int j = 0; j < array.length-1-i; j++){
//如果当前数据的值大于后面邻近位的值,就发生交换
//最后得到从大到小排序的数组
if(array[j]>array[j+1]){
//发生过交换,标志位就置为true
flag = true;
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
System.out.printf("第%d趟的第%d次比较之后的数组:",i+1,j+1);
list(array);
}
System.out.println("---------------------------------");
//如果flag = false,一次都没发生过交换,提前结束排序
if(!flag){
System.out.println("冒泡排序提前结束");
System.out.print("排序后的数组是:");
list(array);
return;
}
}
}
//冒泡排序的模板代码
//冒泡排序的时间复杂度:O(n^2)
private static void bubbleSort(int[] array){
//交换变量
int temp;
//标志位,判断在当前这一趟,是否发生交换
boolean flag = false;
//冒泡排序:需要比较array.length-1趟
for (int i = 0; i < array.length-1; i++){
//标志位置为false
flag = false;
for (int j = 0; j < array.length-1-i; j++){
//如果当前数据的值大于后面邻近位的值,就发生交换
//最后得到从大到小排序的数组
if(array[j]>array[j+1]){
//发生过交换,标志位就置为true
flag = true;
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
//如果flag = false,一次都没发生过交换,提前结束排序
if(!flag){
return;
}
}
}
//遍历排序后的数组
private static void list(int[] array){
for (int i:array){
System.out.print(i+" ");
}
System.out.println();
}
}
6)选择排序:
① 选择排序的介绍
选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
② 选择排序的思想
选择排序(selectsorting)也是一种简单的排序方法。它的基本思想是:第一次从arr[0] ~ arr[n-1]中选取最小值,与arr[0]交换,第二次从arr[1] ~ arr[n-1]中选取最小值,与arr[1]交换,第三次从arr[2] ~ arr[n-1]中选取最小值,与arr[2]交换,…,第i次从arr[i-1] ~ arr[n-1]中选取最小值,与arr[i-1]交换,…,第n-1次从arr[n-2] ~ arr[n-1]中选取最小值,与arr[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列。
③ 选择排序的的过程分析:
有一群牛, 颜值分别是101,34, 119, 1,使用选择排序从低到高进行排序[101, 34, 119, 1]
④ 选择排序的代码实现:
package com.data.structure.study6.sort;
import com.data.leetcode.day01.Array;
import java.util.Random;
/**
* 选择排序代码实现:
* 复杂的算法简单化,先简单实现,然后再综合实现
* @author imppm
* @create 2021-03-20-13:46
*/
public class Sort2SelectDemo1 {
public static void main(String[] args) {
//int[] array1 = {3,9,-1,10,-2};
// int[] array1 = {8,3,2,1,7,4,6,3};
// int[] array1 = {101,34,119,1};
// selectSort1(array1);
// list(array1);
//创建一个长度为80000的数组
int[] array1 = new int[80000];
//测试选择排序排序80000个数据的速度
Random random = new Random();
for (int i = 0; i < 80000; i++){
array1[i] = random.nextInt(80000);
}
long begin = System.currentTimeMillis();
selectSort1(array1);
long end = System.currentTimeMillis();
long result = end - begin;
System.out.println("选择排序,排序80000个数据的数组,耗时"+result+"ms!");
}
//选择排序算法模板
//选择排序的时间复杂度:O(n^2)
private static void selectSort1(int[] array){
//中间变量
int temp;
//记录最小值下标
int index;
//需要比较的次数:array.length-1
for (int i = 0; i < array.length-1; i++){
//每次都将下标初始化为已排序数据的下一位索引
index = i;
//从未排序的索引开始遍历:在剩下的数据中查找是否需要更换最小值索引
for (int j = i+1; j < array.length; j++){
//如果当前数组index索引下的值小于j索引下的值
//更换temp存储的值:更换为最小数据下标
//temp保存的永远都是最小值的索引
if(array[index] > array[j]){
index = j;
}
}
//如果没有发生交换,就不进行数据的设置
if(index!=i) {
temp = array[i];
array[i] = array[index];
array[index] = temp;
}
}
}
//遍历数组
private static void list(int[] array){
for (int i:array){
System.out.print(i+" ");
}
}
}
7)插入排序
① 插入排序的介绍
插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。
② 插入排序的思想:
插入排序(Insertion Sorting)的基本思想是:把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
③ 插入排序的过程分析:
④ 插入排序的代码实现:
有一群小牛, 考试成绩分别是101,34, 119, 1 请从小到大排序
package com.data.structure.study6.sort;
import java.util.Random;
/**
* 插入排序算法:
* @author imppm
* @create 2021-03-20-14:38
*/
public class Sort3InsertDemo1 {
public static void main(String[] args) {
// int[] array = {101, 34, 119, 1, -1, 3};
//
// insertSort1(array);
//创建一个长度为80000的数组
int[] array1 = new int[80000];
//测试插入排序排序80000个数据的速度
Random random = new Random();
for (int i = 0; i < 80000; i++){
array1[i] = random.nextInt(100000);
// array1[i] = (int) (Math.random()*80000);
}
long begin = System.currentTimeMillis();
insertSort(array1);
long end = System.currentTimeMillis();
long result = end - begin;
System.out.println("插入排序,排序80000个数据的数组,耗时"+result+"ms!");
list(array1);
}
//插入排序:
private static void insertSort1(int[] array){
//待插入的值
int insertVal;
//插入的索引
int insertIndex;
//从小到大进行排序
//总共需要比较array.length-1次
for (int i = 1; i < array.length; i++){
//获取到要插入的数据
insertVal = array[i];
//获取到插入数据索引的前一个
insertIndex = i - 1;
//要插入的数据是否在该位置
while (insertIndex>=0 && insertVal < array[insertIndex]){
//要插入的数据小于当前值
//已排序的数组向后移动一位
array[insertIndex+1] = array[insertIndex];
//插入的位置--
insertIndex--;
}
if(insertIndex+1!=i) {
//当前位置为要插入元素合适的位置
array[insertIndex + 1] = insertVal;
}
System.out.printf("第%d次排序后的数组:",i);
list(array);
}
}
//插入排序:模板代码
private static void insertSort(int[] array){
//待插入的值
int insertVal;
//插入的索引
int insertIndex;
//从小到大进行排序
//总共需要比较array.length-1次
for (int i = 1; i < array.length; i++){
//获取到要插入的数据
insertVal = array[i];
//获取到插入数据索引的前一个
insertIndex = i - 1;
//要插入的数据是否在该位置
while (insertIndex>=0 && insertVal < array[insertIndex]){
//要插入的数据小于当前值
//已排序的数组向后移动一位
array[insertIndex+1] = array[insertIndex];
//插入的位置--
insertIndex--;
}
//当前位置为要插入元素合适的位置
array[insertIndex+1] = insertVal;
}
}
private static void list(int[] array){
for (int i:array){
System.out.print(i+" ");
}
System.out.println();
}
}
8)希尔排序【移位法重点掌握】
① 简单插入排序存在的问题
数组 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}
结论:当需要插入的数是较小的数的时候,后移的次数明显增多,对效率有影响
② 希尔排序的介绍:
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
③ 希尔排序法基本思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止
④ 希尔排序的应用实例:
有一群小牛, 考试成绩分别是 {8,9,1,7,2,3,5,4,6,0} 请从小到大排序. 请分别使用
-
希尔排序时, 对有序序列在插入时采用交换法, 并测试排序速度.
-
希尔排序时, 对有序序列在插入时采用移动法, 并测试排序速度
package com.immort.dataStructure.sort;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
/**
* 希尔排序
* @author ppmy
* @create 2021-03-26 20:47
*/
public class Study2_ShellSort1 {
public static void main(String[] args) {
//创建要排序的数组
int[] array = {8,9,1,7,2,3,5,4,6,0};
//希尔排序
//exchangeShellSort(array);
//排序后的数组输出®
//list(array);
//test();
//shiftShellSort1(array);
//list(array);
test();
}
//测试希尔排序,排序80000个数据的时间
private static void test(){
//随机产生80000个随机数
int[] array = new int[800000];
Random random = new Random();
for (int i = 0; i < array.length; i++){
array[i] = random.nextInt(10000000);
}
//创建date对象
Date date = new Date();
//格式化对象
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//开始排序的毫秒数
long begin = System.currentTimeMillis();
//开始排序的时间
String beginStr = simpleDateFormat.format(date);
System.out.println("排序之前的时间为:"+beginStr);
//交换法进行希尔排序
//exchangeShellSort(array);
//移位法进行希尔排序
shiftShellSort1(array);
//排序完成之后的date
Date date1 = new Date();
//排序完成后的时间
String endStr = simpleDateFormat.format(date1);
//排序完成之后的毫秒数
long end = System.currentTimeMillis();
//排序消耗的毫秒数
long result = end - begin;
System.out.println("希尔排序随机排序80000数,时间为:"+result+"ms");
System.out.println("排序之后的时间为:"+endStr);
}
//希尔排序
private static void shellSort2(int[] array){
//中间交换变量
int temp;
//希尔排序分的组数
int groupCount;
//第一轮希尔排序的组数
groupCount = array.length / 2;
//第一轮排序:需要排序比较的次数
for (int i = groupCount; i < array.length; i++){
//第一轮排序的比较:
for (int j = i - groupCount; j >= 0; j-=groupCount){
//如果当前值大于同组中后面的其他值,就进行交换
if(array[j] > array[j+groupCount]){
temp = array[j];
array[j] = array[j+groupCount];
array[groupCount+j] = temp;
}
}
}
System.out.print("第一轮排序之后的数组:");
list(array);
//第二轮排序的组数
groupCount /= 2;
//第二轮排序
for (int i = groupCount; i < array.length; i++){
for (int j = i - groupCount; j >= 0;j -= groupCount){
if(array[j]>array[j+groupCount]){
temp = array[j];
array[j] = array[j+groupCount];
array[j+groupCount] = temp;
}
}
}
System.out.print("第二轮排序之后的数组:");
list(array);
//第三次排序的组数
groupCount /= 2;
//第三次排序
for (int i = groupCount; i < array.length; i++){
for (int j = i - groupCount; j >= 0; j -= groupCount){
if(array[j]>array[j+groupCount]){
temp = array[j];
array[j] = array[j+groupCount];
array[j+groupCount] = temp;
}
}
}
System.out.print("第三次排序之后的数组:");
list(array);
}
//希尔排序交换法模版代码【复杂】
//从小到大的希尔排序
private static void exchangeShellSort(int[] array){
//记录当前数组的组数
int groupCount = 0;
//作为中间交换的变量
int temp;
//设置组数的初值
groupCount = array.length;
while (true){
//如果当前的组数为1的时候,跳出该循环
if(groupCount == 1){
break;
}
//排序一次之后组数/2
groupCount /= 2;
//以该题目中的数组为例:8,9,1,7,2,3,5,4,6,0
//分为groupCount个组
//第一次分组之后进行排序:
//分为5个组:【8、3】、【9、5】、【1、4】、【7、6】、【2、0】只需要比较两位数就行
//第一次排序之后的数组为:3,5,1,6,0,8,9,4,7,2
//第二次分组之后进行排序:
//分为2个组:【3、1、0、9、7】、【5、6、8、4、2】需要比较5个数
//从每一组的最后一个数字开始向前比较,依次把最小的数字送到最前面
//第一组进行排序:
// 比较前两位:第一次内层for循环【1、3、0、9、7】
// 比较前三位:第二次内层for循环【0、1、3、9、7】
// 比较前四位:第三次内层for循环【0、1、3、9、7】
// 比较前五位:第四次内层for循环【0、1、3、7、9】
//第二组进行排序:
// 比较前两位:第一次内层for循环【5、6、8、4、2】
// 比较前三位:第二次内层for循环【5、6、8、4、2】
// 比较前四位:第三次内层for循环【4、5、6、8、2】
// 比较前五位:第四次内层for循环【2、4、5、6、8】
//第三次分组之后进行排序:与第二次分组类似
//分为1个组:需要比较10位数
//从当前组的最后一个数字开始向前比较,依次把最小的数字送到前面去
for (int i = groupCount; i < array.length; i++){
for (int j = i - groupCount; j >= 0; j -= groupCount){
if(array[j]>array[j+groupCount]){
temp = array[j];
array[j] = array[j+groupCount];
array[j+groupCount] = temp;
}
}
}
}
System.out.print("希尔排序之后的数组:");
}
//推荐使用该种排序算法
//移位法实现希尔排序【简单】
private static void shiftShellSort1(int[] array){
//记录组中的最小值
int group_min;
//记录最小值的索引值
int j;
//分组的次数
for (int groupCount = array.length/2;groupCount!=0;groupCount/=2){
//内层为插入排序
for (int i = groupCount; i < array.length; i++){
//假设当前位置为该组的最小值
group_min = array[i];
//取已排序数组的最后一个数据的索引
j = i - groupCount;
//比较当前位置与已排序数组的数据,找到要插入的位置
while (j >= 0 && array[j] > group_min){
//大的数据放到后面
array[j + groupCount] = array[j];
//最小值索引向前移动
j -= groupCount;
}
//最小值插入到适当的位置
array[j+groupCount] = group_min;
}
}
}
private static void list(int[] array){
for(int i :array){
System.out.print(i+" ");
}
System.out.println();
}
}
9)快速排序【经典算法】
① 快速排序的介绍:
快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
② 快速排序法的示意图:
③ 快速排序的应用实例:
要求: 对 [-9,78,0,23,-567,70] 进行从小到大的排序,要求使用快速排序法。【测试8w和800w】
package com.immort.dataStructure.sort;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
/**
* 快速排序的实现
* @author ppmy
* @create 2021-03-27 14:46
*/
public class Study3_QuickSortDemo1 {
public static void main(String[] args) {
int[] array = {-9,78,0,23,-567,70,-1,900,423};
quickSort(array, 0, array.length-1);
list(array);
//测试快速排序排序8 0000数据的时间
test();
}
//测试快速排序,排序8 0000个数据的时间
private static void test(){
//随机产生8 0000个随机数
int[] array = new int[80000];
Random random = new Random();
for (int i = 0; i < array.length; i++){
array[i] = random.nextInt(1000000);
}
//创建date对象
Date date = new Date();
//格式化对象
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//开始排序的毫秒数
long begin = System.currentTimeMillis();
//开始排序的时间
String beginStr = simpleDateFormat.format(date);
System.out.println("排序之前的时间为:"+beginStr);
//快速排序
quickSort(array,0,array.length-1);
//排序完成之后的date
Date date1 = new Date();
//排序完成后的时间
String endStr = simpleDateFormat.format(date1);
//排序完成之后的毫秒数
long end = System.currentTimeMillis();
//排序消耗的毫秒数
long result = end - begin;
System.out.println("快速排序随机排序80000数,时间为:"+result+"ms");
System.out.println("排序之后的时间为:"+endStr);
}
//快速排序:模板代码
private static void quickSort(int[] array, int left, int right){
//左指针:负责遍历中轴左边的数据
int l = left;
//右指针:负责遍历中轴右边的数据
int r = right;
//中轴索引上存储的数据
int middle = array[(l+r)/2];
//中间变量:交换时候使用
int temp = 0;
//左指针没有遇到右指针
//完成交换的操作:选定中轴,左边的数据小于中轴数据,右边的数据大于中轴数据
while (l < r){
//当遇到左边的值大于中轴数据的时候,左指针停止遍历
//左指针指向数据小于中轴值,循环遍历
while (array[l] < middle){
//左指针+1
l++;
}
//当遇到右指针的值小于中轴数据的时候,右指针停止遍历
//右指针指向数据大于中轴值,循环遍历
while (array[r] > middle){
//右指针-1
r--;
}
//如果l>=r,说明middle的左右两边的值,已经按照中轴左边小,右边大的数据进行排序
if(l >= r){
break;
}
//如果没有满足l<=r左右指针就停止了,就需要进行交换
temp = array[l];
array[l] = array[r];
array[r] = temp;
//交换完成,发现l==middle,说明右指针到达了中轴数据位置
//右指针需要向左移动一位
if(array[l] == middle){
r--;
}
//交换完毕,发现这个arr[r]==middle,表示左指针到达了中轴数据位置
//左指针需要向右移动一位
if(array[r] == middle){
l++;
}
}
//左指针和右指针都到达了中轴位置
if(l == r){
l++;
r--;
}
//右指针还没有到达左端:
// 传入数组:左边为left,右边为r,向左递归
if(left < r){
quickSort(array, left, r);
}
//左指针还没有到达右端
// 传入数据:左边为l,右边为fright,向有递归
if(right > l){
quickSort(array, l, right);
}
}
private static void list(int[] array){
for (int i:array){
System.out.print(i+" ");
}
System.out.println();
}
}
10)归并排序
① 归并排序的介绍:
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略( 分治法将问题 分(divide) 成一些小的问题然后递归求解,而治(conquer) 的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
② 归并排序的基本思想:
可以看到这种结构很像一棵完全二叉树,归并排序采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程。
③ 归并排序思想—合并相邻有序子序列
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤:
④ 归并排序的应用实例:
给定一个数组, val arr = Array(9,8,7,6,5,4,3,2,1), 请使用归并排序完成排序。
package com.immort.dataStructure.sort;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
/**
* @author ppmy
* @create 2021-03-28 10:24
*/
public class Study4_MergeSortDemo1 {
public static void main(String[] args) {
int[] array = {1,3,1,5,6,7,8,3,2,4,1,2,3,213};
int[] temp = new int[array.length];
mergeSort(array, 0, array.length-1, temp);
list(temp);
//测试归并排序:排序8 0000万个数据花费的时间
test();
}
//测试归并排序,排序8 0000个数据的时间
private static void test(){
//随机产生8 0000个随机数
int[] array = new int[80000];
Random random = new Random();
for (int i = 0; i < array.length; i++){
array[i] = random.nextInt(10000000);
}
//创建date对象
Date date = new Date();
//格式化对象
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//开始排序的毫秒数
long begin = System.currentTimeMillis();
//开始排序的时间
String beginStr = simpleDateFormat.format(date);
System.out.println("排序之前的时间为:"+beginStr);
int[] temp = new int[array.length];
//归并排序
mergeSort(array,0,array.length-1, temp);
//排序完成之后的date
Date date1 = new Date();
//排序完成后的时间
String endStr = simpleDateFormat.format(date1);
//排序完成之后的毫秒数
long end = System.currentTimeMillis();
//排序消耗的毫秒数
long result = end - begin;
System.out.println("归并排序随机排序80000数,时间为:"+result+"ms");
System.out.println("排序之后的时间为:"+endStr);
}
//分+和
private static void mergeSort(int[] array, int left, int right, int[] temp){
//将数组进行分解+合
//分解的结束条件是,分到只有一个数据
//分解到还有两个数据的时候,开始进行合并
if(left < right){
//获取到中间值
int mid = (left + right) / 2;
mergeSort(array, left, mid, temp);
mergeSort(array, mid+1, right, temp);
//进行合并
mergeArray(array,left,mid,right,temp);
}
}
/**
* 合并的方法
* @param array 要排序的数组
* @param left 左边有序序列的初始索引值
* @param middle 数组中间数据的索引【偶数:array.length/2-1;奇数:array.length/2】
* @param right 数组的长度
* @param temp 作为中间数组变量,归并好的数组
*/
private static void mergeArray(int[] array,int left, int middle, int right, int[] temp){
//左边有序序列的头指针
int array1_l = left;
//右边有序序列的头指针
int array2_l = middle+1;
//中间数组的初始索引值
int temp_index = 0;
//同时遍历两个有序序列
//左边有序序列的头指针不能超过middle的值
//右边有序序列的头指针不能超过right的值
while (array1_l <= middle && array2_l <= right){
//如果左边有序序列的数据大于右边有序序列的数据,将右边有序序列的数据填充到temp数组中,并将右边有序序列的头指针+1,temp+1
if(array[array1_l] > array[array2_l]){
temp[temp_index++] = array[array2_l++];
}
//如果左边有序序列的数据小于右边有序序列的值,将左边的有序序列的值填充到temp中,并将左边有序序列的头指针+1.temp的指针+1
else{
temp[temp_index++] = array[array1_l++];
}
}
//左边有序序列有剩余,将左边序列的值,都填充到temp数组中
while (array1_l <= middle){
temp[temp_index++] = array[array1_l++];
}
//右边有序序列有剩余,将右边序列的值,都填充到temp数组中
while (array2_l <= right){
temp[temp_index++] = array[array2_l++];
}
//将temp中的数据拷贝到array中
temp_index = 0;
//需要将数据拷贝到array中的初始位置
int tempLeft = left;
//将temp中的数据,放到array的tempLeft到right
while (tempLeft <= right){
array[tempLeft++] = temp[temp_index++];
}
}
private static void list(int[] array){
for (int i:array){
System.out.print(i+" ");
}
System.out.println();
}
}
11)基数排序【桶排序】
① 基数排序的介绍:
(1) 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用。
(2) 基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法。
(3) 基数排序(Radix Sort)是桶排序的扩展
(4) 基数排序是1887年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。
② 基数排序的基本思想:
(1) 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
(2) 图文解释,理解基数排序的步骤:
③ 基数排序的说明
(1) 基数排序是对传统桶排序的扩展,速度很快。
(2) 基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成 OutOfMemoryError 。
(3) 有负数的数组,一般不用基数排序进行排序。
④ 基数排序的应用实例:
将数组 {53, 3, 542, 748, 14, 214 } 使用基数排序, 进行升序排序:
package com.immort.dataStructure.sort;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
/**
* 基数排序:桶排序
* 获取数字的位数可以用一个巧妙的方法:
* (数字+"").length
* @author ppmy
* @create 2021-03-28 14:17
*/
public class Study5_RadixSortDemo1 {
public static void main(String[] args) {
int[] array = {53, 3, 542, 748, 14, 214};
radixSort(array);
list(array);
//测试桶排序:排序800 0000个数据
test();
}
//测试桶排序,排序800 0000个数据的时间
private static void test(){
//随机产生800 0000个随机数
int[] array = new int[8000000];
Random random = new Random();
for (int i = 0; i < array.length; i++){
array[i] = random.nextInt(1000000);
}
//创建date对象
Date date = new Date();
//格式化对象
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//开始排序的毫秒数
long begin = System.currentTimeMillis();
//开始排序的时间
String beginStr = simpleDateFormat.format(date);
System.out.println("排序之前的时间为:"+beginStr);
//桶排序
radixSort2(array);
//排序完成之后的date
Date date1 = new Date();
//排序完成后的时间
String endStr = simpleDateFormat.format(date1);
//排序完成之后的毫秒数
long end = System.currentTimeMillis();
//排序消耗的毫秒数
long result = end - begin;
System.out.println("桶排序随机排序800 0000个数据,时间为:"+result+"ms");
System.out.println("排序之后的时间为:"+endStr);
}
//基数排序的方法
private static void radixSort1(int[] array){
//定义一个二维数组,每一个二维数组就是一个桶
//1.二位数组包含10个一维数组
//2.为了防止桶中存储数据出现溢出的情况,将桶的大小都设置为array.length
//3,基数排序是典型的空间换时间的算法
//创建一个桶
int[][] buckets = new int[10][array.length];
//创建一个一维数组,用来存储每个桶中的数据个数
int[] bucketCount = new int[10];
int numberOfDigit;
//遍历数组中的所有的数据,向桶中添加数据
for (int i = 0; i < array.length; i++){
//获取到个数上的数字
numberOfDigit = array[i] % 10;
//把该数字加入到对应桶中
buckets[numberOfDigit][bucketCount[numberOfDigit]] = array[i];
bucketCount[numberOfDigit]++;
}
//记录当前遍历桶中数据的位置
int index;
//记录当前数组中的数据的位置
int begin = 0;
//遍历所有的桶,查看桶中存储的数据
for (int i = 0; i < buckets.length; i++){
index = 0;
//如果存储桶个数的数组对应的值不为0,表示有数据
if(bucketCount[i]>0){
while (index < bucketCount[i]){
array[begin++] = buckets[i][index];
index++;
}
bucketCount[i] = 0;
}
}
list(array);
}
//基数排序的模板代码1
private static void radixSort(int[] array){
//定义一个二维数组,每一个二维数组就是一个桶
//1.二位数组包含10个一维数组
//2.为了防止桶中存储数据出现溢出的情况,将桶的大小都设置为array.length
//3,基数排序是典型的空间换时间的算法
//创建一个桶:二维数组
int[][] buckets = new int[10][array.length];
//创建一个一维数组,用来存储每个桶中的当前数据个数
int[] bucketCount = new int[10];
//数字位数上的值即桶号
int numberOfDigit;
//记录桶排序是否已经排好序
//false:已排好序
//true:还需要排序
boolean flag = false;
//记录数据的位数
int count = 0;
while(true){
//获取当前的位数
count++;
//初始化桶标志位
flag = false;
//遍历数组中的所有数据,向桶中添加数据
for (int i = 0; i < array.length; i++){
//获取到数组中数据各个位数上的数字
numberOfDigit = getNumberOfDigits(count,array[i]);
//把该数字放到 该位数字 对应的桶中
//bucketCount[numberOfDigit]:获取到记录的numberOfDigit桶中的当前数据个数
//buckets[numberOfDigit][bucketCount[numberOfDigit]]:给numberOfDigit桶中继续向后添加数据
buckets[numberOfDigit][bucketCount[numberOfDigit]] = array[i];
//bucketCount[numberOfDigit]++:numberOfDigit桶中的数据个数+1
bucketCount[numberOfDigit]++;
//如果存在numberOfDigit不等于0,表示有非0桶放入数据,表示还未到达最大数的位数
if(numberOfDigit!=0){
flag = true;
}
}
//将array中的数据放入到桶中,全部放入到0桶中,表示排序已完成
if(!flag){
return;
}
//记录当前遍历桶中数据的位置
int index;
//记录当前数组中的数据的位置
int begin = 0;
//遍历所有的桶,将桶中存储的数据赋值到原数组中
for (int i = 0; i < buckets.length; i++){
index = 0;
//如果存储桶个数的数组对应的值不为0,表示该桶中有数据
//循环遍历,取出该桶中的所有数据
if(bucketCount[i]>0){
while (index < bucketCount[i]){
array[begin++] = buckets[i][index];
index++;
}
//当前桶中的数据已全部拿出,记录该桶的个数的变量需要置为0
bucketCount[i] = 0;
}
}
}
}
/**
*
* @param number 获取当前digit的第几位
* @param digit 数据
* @return 获取到的数据
*/
private static int getNumberOfDigits(int number, int digit){
int num = 0;
//获取到digit的第number位上的数字
for (int i = 0; i < number; i++){
num = digit%10;
digit/=10;
}
return num;
}
//基数排序模板代码2
public static void radixSort2(int[] arr){
//先找到原数组中的最大值
int max = arr[0];
for(int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
//获取到最大值数据的位数
int maxLength = (max + "").length();
//创建一个桶:二维数组
int[][] bucket = new int[10][arr.length];
//创建一个一维数组用来记录各个桶当前的数据个数
int[] bucketElementCounts = new int[10];
//遍历的次数:最大值数据的位数
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];
//记录桶中数据个数的数组,相应桶号下桶中数据个数+1
bucketElementCounts[digitOfElement]++;
}
//记录原数组的个数
int index = 0;
//遍历桶
for(int k = 0; k < bucketElementCounts.length; k++) {
//判断记录桶个数数组对应的桶号中的个数是否为0
//等于0:该桶中没有数据
//不等于0:该桶中存放了数据
if(bucketElementCounts[k] != 0) {
//遍历该桶:依次将桶中的数据放入到数组中
//放入的顺序是:桶中存放的顺序,从头开始放
for(int l = 0; l < bucketElementCounts[k]; l++) {
arr[index++] = bucket[k][l];
}
//每把桶中的数据放入到数组中,记录桶的数据个数的数组就需要清0一次
bucketElementCounts[k] = 0;
}
}
}
}
//遍历数组
private static void list(int[] array){
for (int i:array){
System.out.print(i+" ");
}
System.out.println();
}
}
12)排序的稳定性:
① 稳定性是什么?
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
13)基本排序算法的比较:
希望本篇文章对大家有所帮助,后续会继续分享java数据结构与算法相关学习知识…
如果文章内容有错误的地方,请在留言处留下你的见解,方便大家共同学习。谢谢!
如有侵权或其他任何问题请联系:QQ1370922071,本文主要用于学习交流,转载请声明!
作者:皮皮猫吖