1. 排序算法的介绍
- 排序也叫排序算法(Sort Algorithm),排序是将一组数据,按照指定的顺序进行排列的过程
- 分类:内部排序,将需要处理的所有数据都加载到内部存储器中进行排序
- 外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序
2. 算法的复杂度
2.1 算法的时间复杂度
度量一个算法执行时间的两种方法
1.事后统计
这个方法可行,但又两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机硬件等环境因素,这种方式要在同一台计算机的相同状态下运行,才能比较
2.事前估算
通过分析某个算法的时间复杂度来判断
2.2 时间频度
- 一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,他花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度,记为T(n)
2.3 时间复杂度
2.3.1 常见的时间复杂度
- 常数阶O(1)
- 对数阶O(log2 n)
- 线性阶O(n)
- 线性对数阶O(nlog2 n)
- 平方阶O(n^2)
- 立方阶O(n^3)
- k次方阶O(n^k)
- 指数阶O(2^n)
2.3.2 常数阶
- 无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)
- 代码在执行的时候,它消耗的时间并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。
2.3.3 对数阶
2.3.4 线性阶
- for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度
2.3.5 线性对数阶
- 线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)
2.3.6 平方阶
- 如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(nn),即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(mn)
3. 冒泡排序
3.1 基本介绍
- 冒泡排序(Bubble Sorting)通过对代排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后,像水底的气泡一样逐渐向上冒
- 优化:在排序过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换,从而减少不必要的比较
3.2 图解和思想
3.3 代码实现
public class BubbleSort {
public static void main(String[] args) {
int arr[] = {3, 9, -1, 10, 20};
//int arr[] = {1, 2, 3, 4, 5, 6};
int temp = 0;//临时变量
boolean flag = false;//标识变量,表示是否进行过交换
for (int i = 0; i < arr.length - 1; i++) {
//第一趟排序,将最大的数排在最后
for (int j = 0; j < arr.length - 1 - i; j++) {
//如果前面的数比后面的数大,则交换
if (arr[j] > arr[j + 1]) {
flag = true;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第" + (i + 1) + "趟排序后的数组");
System.out.println(Arrays.toString(arr));
if (!flag) {//在一趟排序中,没有发生过交换
break;
} else {
flag = false;//重置flag,进行下次判断
}
}
}
// //第二趟排序,将第二大的数排在倒二
// for (int j = 0; j < arr.length - 1 - 1; j++) {
// //如果前面的数比二后面的数大,则交换
// if (arr[j] > arr[j + 1]) {
// temp = arr[j];
// arr[j] = arr[j + 1];
// arr[j + 1] = temp;
// }
// }
// System.out.println("第二趟排序后的数组");
// System.out.println(Arrays.toString(arr));
// //第3趟排序,将第3大的数排在倒3
// for (int j = 0; j < arr.length - 1 - 2; j++) {
// //如果前面的数比二后面的数大,则交换
// if (arr[j] > arr[j + 1]) {
// temp = arr[j];
// arr[j] = arr[j + 1];
// arr[j + 1] = temp;
// }
// }
// System.out.println("第3趟排序后的数组");
// System.out.println(Arrays.toString(arr));
//
// //第4趟排序,将第二大的数排在倒4
// for (int j = 0; j < arr.length - 1 - 3; j++) {
// //如果前面的数比二后面的数大,则交换
// if (arr[j] > arr[j + 1]) {
// temp = arr[j];
// arr[j] = arr[j + 1];
// arr[j + 1] = temp;
// }
// }
// System.out.println("第4趟排序后的数组");
// System.out.println(Arrays.toString(arr));
}
4. 选择排序
4.1 基本介绍
- 是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的
4.2 图解和思想
选择排序(select sorting)基本思想是
- 第一次从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次,得到一个按排序从小到大排序的有序序列
4.3 代码实现
public class SelectSort {
public static void main(String[] args) {
//int[] arr = {101, 34, 119, 1};
int[] arr = new int[80000];
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 80000);
}
Date start = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss");
String date1 = simpleDateFormat.format(start);
System.out.println(date1);
selectSort(arr);
Date end = new Date();
String date2 = simpleDateFormat.format(end);
System.out.println(date2);
//System.out.println(Arrays.toString(arr));
}
//选择排序
public static void selectSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
//第一轮排序
int minIndex = i;
int min = arr[i];
for (int j = i + 1; j < arr.length; j++) {
if (min > arr[j]) {//说明假定的最小值不是最小的
min = arr[j];
minIndex = j;
}
}
//将最小值和arr[0]交换
if (minIndex != i) {
arr[minIndex] = arr[i];
arr[i] = min;
}
//System.out.println("第" + (i + 1) + "轮后");
//System.out.println(Arrays.toString(arr));
}
// //第2轮排序
// minIndex = 1;
// min = arr[1];
// for (int j = 1 + 1; j < arr.length; j++) {
// if (min > arr[j]) {//说明假定的最小值不是最小的
// min = arr[j];
// minIndex = j;
// }
// }
// //将最小值和arr[0]交换
// if (minIndex != 1) {
// arr[minIndex] = arr[1];
// arr[1] = min;
// }
// System.out.println("第2轮后");
// System.out.println(Arrays.toString(arr));
// //第2轮排序
// minIndex = 2;
// min = arr[2];
// for (int j = 2 + 1; j < arr.length; j++) {
// if (min > arr[j]) {//说明假定的最小值不是最小的
// min = arr[j];
// minIndex = j;
// }
// }
// //将最小值和arr[0]交换
// if (minIndex != 2) {
// arr[minIndex] = arr[2];
// arr[2] = min;
// }
// System.out.println("第3轮后");
// System.out.println(Arrays.toString(arr));
}
}
5. 插入排序
5.1 基本介绍
-是对欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的
5.2 图解和思想
- 插入排序(Insertion Sorting)基本思想:把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含n-1个元素,排序过程中每次从无序表中取出第一个元素,把他的排序码依次与有序表的排序码进行比较,将他插入到有序表的适当位置,使之成为新的有序表
5.3 代码实现
public class InsertSort {
public static void main(String[] args) {
int[] arr = new int[80000];
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 80000);
}
Date start = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss");
String date1 = simpleDateFormat.format(start);
System.out.println(date1);
insertSort(arr);
Date end = new Date();
String date2 = simpleDateFormat.format(end);
System.out.println(date2);
//int[] arr = {101, 34, 119, 1, -1, 89};
}
//插入排序
public static void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
//第1轮
//定义待插入的数
int insertVal = arr[i];
int insertIndex = i - 1;//arr[1]前面这个数的下标
//给insertVal找插入位置时,不越界
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
//给insertVal找到插入的位置
//将arr[insertIndex]后移
arr[insertIndex + 1] = arr[insertIndex];
insertIndex--;
}
//退出while时,说明插入的位置找到,insertIndex+1
arr[insertIndex + 1] = insertVal;
//System.out.println("第" + i + "轮后");
//System.out.println(Arrays.toString(arr));
}
// //第二轮
// insertVal = arr[2];
// insertIndex = 2 - 1;//arr[1]前面这个数的下标
//
// //给insertVal找插入位置时,不越界
// while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
// //给insertVal找到插入的位置
// //将arr[insertIndex]后移
// arr[insertIndex + 1] = arr[insertIndex];
// insertIndex--;
// }
// //退出while时,说明插入的位置找到,insertIndex+1
// arr[insertIndex + 1] = insertVal;
// System.out.println("第2轮后");
// System.out.println(Arrays.toString(arr));
//
// //第3轮
// insertVal = arr[3];
// insertIndex = 3 - 1;//arr[1]前面这个数的下标
//
// //给insertVal找插入位置时,不越界
// while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
// //给insertVal找到插入的位置
// //将arr[insertIndex]后移
// arr[insertIndex + 1] = arr[insertIndex];
// insertIndex--;
// }
// //退出while时,说明插入的位置找到,insertIndex+1
// arr[insertIndex + 1] = insertVal;
// System.out.println("第3轮后");
// System.out.println(Arrays.toString(arr));
}
}
6. 希尔排序
6.1 基本介绍
- 简单插入排序存在的问题,数组arr={1,2,3,4,5,6,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)也是一种插入排序,是简单插入排序经过改进后的一个更高效的版本,成为缩小增量排序
6.2 图解和思想
- 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序,随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法终止
6.3 代码实现
public class ShellSort {
public static void main(String[] args) {
int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
shellSort2(arr);
System.out.println(Arrays.toString(arr));
}
public static void shellSort(int[] arr) {
int temp;
int count = 0;
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
//第一轮,将10个数据分成5组
for (int i = gap; i < arr.length; i++) {
//遍历各组中所有元素(共gap组,每组有2个元素),步长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));
}
// //第2轮,将10个数据分成5/2=2组
// for (int i = 2; i < arr.length; i++) {
// 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轮,将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));
}
//对交换式的希尔排序进行优化->移位法
public static void shellSort2(int[] arr) {
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;
}
}
}
}
}
7. 快速排序
7.1 基本介绍
- 快速排序(QuickSort)是对冒泡排序的一种改进,通过一趟排序将要排序的数据分割成独立的两部分,期中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
7.2 图解和思想
7.3 代码实现
public class QuickSort {
public static void main(String[] args) {
int[] arr = {-9, 78, 0, 23, -567, 70};
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
public static void quickSort(int[] arr, int left, int right) {
int l = left;//左下标
int r = right;//右下标
int pivot = arr[(left + right) / 2];//中间值
int temp = 0;//临时变量,交换时使用
//while循环的目的,让比pivot小的值放到左边,比pivot大的值放右边
while (l < r) {
//在pivot左边一直找,找到大于等于pivot值才退出
while (arr[l] < pivot) {
l += 1;
}
//在pivot右边一直找,找到小于等于pivot值才退出
while (arr[r] > pivot) {
r -= 1;
}
//如果l>=r说明pivot左右两边的值,已经按照左边都是小,右边都是大的
if (l >= r) {
break;
}
//交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//交换完后,发现这个arr[l]==pivot值,r--前移
if (arr[l] == pivot) {
r -= 1;
}
//交换完后,发现这个arr[r]==pivot值,l++后移
if (arr[r] == pivot) {
l += 1;
}
}
//如果l==r,必须l++,r--否则栈溢出
if (l == r) {
l += 1;
r -= 1;
}
//左递归
if (left < r) {
quickSort(arr, left, r);
}
//右递归
if (right > l) {
quickSort(arr, l, right);
}
}
}
8. 归并排序
8.1 基本介绍
- 归并排序(MergeSort)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的“修补”在一起,即分而治之)
8.2 图解和思想
8.3 代码实现
public class MergeSort {
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);
}
}
/*合并的方法
* arr 排序的原始数组
* left左边有序序列的初始索引
* mid中间索引
* right右边索引
* 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数组的当前索引
//1.先把所有两边的数据按照规则填充到temp数组,直到左右两边的有序序列有一边处理完毕为止
while (i <= mid && j <= right) {
//如果左边有序序列的当前元素小于等于右边有序序列的当前元素
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
t += 1;
i += 1;
} else {
temp[t] = arr[j];
t += 1;
j += 1;
}
}
//2.把有剩余数据的一边的数据依次全部填充到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;
}
//3.将temp数组的元素拷贝到arr
t = 0;
int tempLeft = left;
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
t += 1;
tempLeft += 1;
}
}
}
9. 基数排序
9.1 基本介绍
- 基数排序(RadixSort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort),顾名思义,是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
- 基数排序是属于稳定性的排序,是桶排序的扩展
- 速度很快,空间换时间的方式,占用内存很大,容易造成OOM
9.2 思想和图解
- 将所有待比较数值统一位同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行依次排序,这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列
9.3 代码实现
public class RadixSort {
public static void main(String[] args) {
int[] arr = {53, 3, 542, 748, 14, 214};
sortRadix(arr);
}
//基数排序方法
public static void sortRadix(int[] arr) {
//得到数组中最大的数
int max = arr[0];//假设第一个数是最大数
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
//最大数是几位数
int maxLength = (max + "").length();
//定义一个二维数组,表示10个桶,每个桶就是一个一维数组
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];
bucketElementCounts[digitOfElement]++;
}
//按照桶的顺序,依次取出数据放入原来数组
int index = 0;
//遍历每一个桶,并将桶中的数据放入到原数组
for (int k = 0; k < bucketElementCounts.length; k++) {
//如果桶中有数据放到原数组
if (bucketElementCounts[k] != 0) {
//循环该桶放入数据
for (int l = 0; l < bucketElementCounts[k]; l++) {
//取出元素放入到arr
arr[index++] = bucket[k][l];
}
}
//第一轮处理后需要将每个bucketElementCounts[k]=0
bucketElementCounts[k] = 0;
}
System.out.println("第" + (i + 1) + "轮" + Arrays.toString(arr));
}
// //第2轮(针对每个元素的十位进行排序)
// for (int j = 0; j < arr.length; j++) {
// //取出元素的十位
// int digitOfElement = arr[j] / 10 % 10;
// //放入到对应的桶中
// bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
// bucketElementCounts[digitOfElement]++;
// }
// //按照桶的顺序,依次取出数据放入原来数组
// index = 0;
// //遍历每一个桶,并将桶中的数据放入到原数组
// for (int k = 0; k < bucketElementCounts.length; k++) {
// //如果桶中有数据放到原数组
// if (bucketElementCounts[k] != 0) {
// //循环该桶放入数据
// for (int l = 0; l < bucketElementCounts[k]; l++) {
// //取出元素放入到arr
// arr[index++] = bucket[k][l];
// }
// }
// bucketElementCounts[k] = 0;
// }
// System.out.println("第2轮" + Arrays.toString(arr));
//
// //第3轮(针对每个元素的百位进行排序)
// for (int j = 0; j < arr.length; j++) {
// //取出元素的百位
// int digitOfElement = arr[j] / 100 % 10;
// //放入到对应的桶中
// bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
// bucketElementCounts[digitOfElement]++;
// }
// //按照桶的顺序,依次取出数据放入原来数组
// index = 0;
// //遍历每一个桶,并将桶中的数据放入到原数组
// for (int k = 0; k < bucketElementCounts.length; k++) {
// //如果桶中有数据放到原数组
// if (bucketElementCounts[k] != 0) {
// //循环该桶放入数据
// for (int l = 0; l < bucketElementCounts[k]; l++) {
// //取出元素放入到arr
// arr[index++] = bucket[k][l];
// }
// }
// bucketElementCounts[k] = 0;
// }
// System.out.println("第3轮" + Arrays.toString(arr));
}
}