排序算法
排序算法
- 排序也称排序算法,是将一组数据,按照指定的顺序进行排列的过程
1.排序的分类
1)内部排序
- 指将需要处理的所有数据都加载到内部存储器中进行排序
2)外部排序
- 数据量过大,无法全部加载到内存中,需要借助外部存储进行排序
3)常见的排序算法分类
2.算法的时间复杂度
- 度量一个程序(算法)执行时间的两种方法:
1.事后统计的方法:
这种方法可行,但是有2个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素,这种方式,要在同一台计算机的相同状态下运行,才能比较哪个算法速度更快。
2.事前估算的方法:
通过分析某个算法的时间复杂度来判断哪个算法更优
1)时间频度
-
一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n).
-
举例:计算1-100所有数字的和
int total=0;
int end=100;
for(int i=1;i<=end;i++){
total+=i;
}
//T(n)=n+1; for循环中的判断语句需要执行n+1次
//直接计算
total=(1+end)*end/2;
//T(n)=1;只需执行1次
- 时间频度的特点:随n的增大,可以
①忽略常数项 ②忽略低次项 ③忽略系数
2)时间复杂度
(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)=3n²+2n+2 => T(n)=3n²+2n+1
- 修改后的运行次数函数中,只保留最高阶项//T(n)=3n²+2n+2 => T(n)=3n²
- 去除最高阶项的系数//T(n)=3n² => T(n)=n² => O(n²)
3)常见的时间复杂度
①常数阶O(1)
②对数阶O(㏒₂n)
③线性阶O(n)
④线性对数阶O(n㏒₂n)
⑤平方阶O(n²)
⑥立方阶O(n³)
⑦k次方阶O(n^k)
⑧指数阶O(2^n) //尽可能避免使用
- 说明:常见的算法时间复杂度由小到大依次为:O(1)<O(㏒₂n)<O(n)<O(n㏒₂n)<O(n²)<O(n³)<O(nk)<O(2n)。随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
4)举例
①常数阶O(1) :
- 无论代码执行了多少行,只要没有循环等复杂结构,这个代码的时间复杂度就都是O(1)
int i=1;
int j=2;
++i;
j++;
int m=i+j;
- 上述代码在执行时,它消耗的时间并不随着某个变量的增长而增长,那么无论这类代码有多长,都可以用O(1)来表示它的时间复杂度。
②对数阶O(㏒₂n)
- 说明:如果N=a^x(a>0,a≠1),即a的x次方等于N,那么x叫做以a为底N的对数,记作x=㏒a N. 其中 a叫做对数的底数,N叫做真数,x叫做以a为底N的对数。
int i=1;
while(i<n){
i=i*2;
}
- 在while循环里面,每次都i * 2,乘完后,i距离n越来越近。假设循环x次后,n>2,此时该循环退出;也就是说2^x=n,那么x=㏒₂n,即当循环㏒₂n次以后,代码结束。因此这个代码的时间复杂度为O(㏒₂n)。O(㏒₂n)的这个2时间上是根据代码变化的,若i=i * 3,则为O(㏒₃n)
③线性阶O(n)
for(int i=1;i<=n;i++){
j=i;
j++;
}
- 这段代码的for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度
④线性对数阶O(n㏒₂n)
for(int m=1;m<n;m++){
i=1;
while(i<n){
i=i*2;
}
}
- 将时间复杂度为O(㏒₂n)的代码循环n遍的话,它的时间复杂度就是n*O(㏒₂n),即O(n㏒₂n)
⑤平方阶O(n²)
for(x=1;x<=n;x++){
for(i=1;i<=n;i++){
j=i;
j++;
}
}
- 把O(n)的代码再嵌套循环一边,它的时间复杂度就是阶O(n²)。这段代码其实是嵌套了2层n循环如果其中一层的n改成m,那么它的时间复杂度就是O(m*n)
⑥立方阶O(n³)、k次方阶O(n^k)
- O(n³)相当于三层n循环,O(n^k)相当于k层n循环
5)平均时间复杂度和最坏时间复杂度
(1) 平均时间复杂度是指所有可能的输入实例以等概率出现的情况下,该算法的运行时间。
(2) 最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。
(3) 平均时间复杂度和最坏时间复杂度是否一致,和算法有关
3.算法的空间复杂度
(1) 类似于时间复杂度的讨论,一个算法的空间复杂度定义为该算法所耗费的存储空间,它也是问题规模n的函数
(2) 空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况
(3) 在做算法分析时,主要讨论的是时间复杂度,从用户使用体验上看,更看重的是程序执行的速度。一些缓存产品和算法(基数排序)本质就是用空间换时间
4.排序算法
1)冒泡排序
(1)基本介绍:
- 冒泡排序的基本思想:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就像水底下的气泡一样逐渐向上冒
- 因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较
(2)排序图解:
- 小结:
- 一共进行 arr.length-1 次 大的循环
- 每一趟排序的次数在逐渐的减少
- 如果我们发现在某趟排序中,没有发生一次交换,可以提前结束冒泡排序(优化)
package com.sort;
/*
@author qw
@date 2020/8/17 - 17:04
**/
import java.util.Arrays;
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {3, 9, -1, 10, -2};
//冒泡排序的时间复杂度为O(n²)
int temp = 0;
boolean flag = false;//标识变量,表示是否进行过交换
for (int i = 0; i < arr.length; 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;
}
}
if (!flag) {
break;
} else {
flag = false;
}
}
System.out.println(Arrays.toString(arr));
}
}
2)选择排序
(1)基本介绍:
- 选择排序是从欲排序的数据中,按指定的规则选出来某一元素,再按照规定交换位置后达到排序的目的
- 排序思想:第一次从arr[0]arr[n-1]中选取最小值,与arr[0]交换,第二次从arr[1]arr[n-1]中选取最小值,与arr[1]交换,…,以此类推,总共通过n-1次,得到一个从小到大排列的有序序列
(2)排序图解:
- 说明:
- 选择排序一共有 arr.length-1 轮排序
- 每一轮排序,又是一个循环
- 先假定当前这个数是最小数,然后和后面的每个数进行比较,如果发现有比当前数更小的数,就重新确定最小数,并得到下标
- 当遍历到数组的最后时,得到本轮最小数和下标
- 将当前数和本轮最小数交换位置
(3)代码实现:
package com.sort;
/*
@author qw
@date 2020/8/17 - 18:02
**/
import java.util.Arrays;
public class SelectSort {
public static void main(String[] args) {
int[] arr = {101, 4, 98, 140};
selectSort(arr);
}
public static void selectSort(int[] arr) {
//选择排序的时间复杂度为O(n²)
for (int i = 0; i < arr.length; 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;
}
}
if (minIndex != i) {//若假设最小值下标的确不为最小值
arr[minIndex] = arr[i];
arr[i] = min;
}
System.out.println("第"+(i+1)+"轮排序后:");
System.out.println(Arrays.toString(arr));
}
}
}
3)插入排序
(1)基本介绍:
- 插入排序是对欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的
- 排序思想:把n个待排序的元素看成一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
(2)排序图解:
(3)代码实现:
package com.sort;
/*
@author qw
@date 2020/8/17 - 22:25
**/
import java.util.Arrays;
public class InsertSort {
public static void main(String[] args) {
int[] arr = {101, 4, -1, 68, 230, 5};
insertSort(arr);
}
public static void insertSort(int[] arr) {
int insertVal = 0;//需要插入的值
int insertIndex = 0; //被插入的位置下标
//i=1,即将第一位数当做一个有序数,从第二位开始插入
for (int i = 1; i < arr.length; i++) {
insertVal = arr[i];
insertIndex = i - 1;
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
//保证下标不越界,即到数组首位(下标为0)为止 或插入值不比前一位数值小时 就结束
arr[insertIndex + 1] = arr[insertIndex]; //被插入位置的值往后移
insertIndex--;//下标指针继续向左移
}
//退出while循环,表明插入位置已经找到
if(insertIndex+1!=i){//当插入的数恰好在适当的位置时,则不需要插入,留在原位
arr[insertIndex + 1] = insertVal;
//由于比较时是与前一位比较,不满足insertVal < arr[insertIndex],
// insertVal,即应排在arr[insertIndex]后面,所以insertIndex + 1;
}
System.out.println("第"+i+"轮插入");
System.out.println(Arrays.toString(arr));
}
}
}
4)希尔排序
(1)基本介绍:
- 希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序
- 排序思想:把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法终止
(2)排序图解:
(3)代码实现:
package com.sort;
/*
@author qw
@date 2020/8/18 - 14:02
**/
import java.util.Arrays;
public class ShellSort {
public static void main(String[] args) {
int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
shellSort1(arr);
shellSort2(arr);
}
//交换法
public static void shellSort1(int[] arr) {
int temp = 0;
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
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(Arrays.toString(arr));
}
//对交换式的希尔排序进行优化--->移位法
public static void shellSort2(int[] arr) {
int temp = 0;
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
//从第gap个元素,逐个对其所在组进行直接插入排序
for (int i = gap; i < arr.length; i++) {
int index = i;
temp = arr[index];
if (arr[index] < arr[index - gap]) {
while (index - gap >= 0 && temp < arr[index - gap]) {
arr[index] = arr[index - gap];
index -= gap;
}
//退出循环,即为temp找到了插入的位置
arr[index] = temp;
}
}
}
System.out.println(Arrays.toString(arr));
}
}
5)快速排序
(1)基本介绍:
- 快速排序是对冒泡排序的一种改进。
- 基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
(2)排序图解:
- 以第一个数为轴,利用双指针low,high 在剩余的数中,从头尾一起扫描,将比轴小的数放在轴的左边,将比轴大的数放在轴的右边;分成2部分后,再依次递归调用,以组内第一个数为轴。将所有数据按顺序排好后,结束算法。
(3)代码实现:
package com.sort;
/*
@author qw
@date 2020/8/18 - 16:36
**/
import java.util.Arrays;
public class QuickSort {
public static void main(String[] args) {
int[] arr = {-9, 78, 0, 23, -567, 65};
quickSort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
public static void quickSort(int [] arr,int low,int high) {
int l = low;
int h = high;
int pivot=arr[l];//第一个元素为轴
int pivotIndex = 0;
while(l<h) {
//从h开始向前扫描 默认右边为比轴大的值
while (arr[h] >= pivot && l < h) {
h--;//值比轴大则指针向左移动
}
arr[l]=arr[h]; //跳过或结束while循环即扫描的值比轴小,则将其放在左边
//从l开始向后扫面 默认左边为比轴小的值
while (arr[l] < pivot && l < h) {
l++;//值比轴小则指针向右移动
}
arr[h]=arr[l];//跳过或结束while循环即扫描的值比轴大,则将其放在右边
}
arr[l]=pivot; //跳出while循环,即l==h,则该位置为轴的位置
pivotIndex = l; //轴下标
if (low < high && low!=high-1) { //若low+1=high 说明low后只有1个数,不再进行递归
quickSort(arr,low,pivotIndex-1);//递归调用本身 将轴的左边部分继续排序
quickSort(arr, pivotIndex + 1, high); //递归调用本身 将轴的右边部分继续排序
}
}
}
6)归并排序
(1)基本介绍:
- 归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略(分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案“修补”在一起,即分而治之)。
(2)排序图解:
(3)代码实现:
package com.sort;
/*
@author qw
@date 2020/8/20 - 17:24
**/
import java.util.Arrays;
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);
}
}
/**
*
* @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;
int j = mid + 1;
int t = 0;
//(一)
//先把左右两边(有序)的数据按照规则填充到temp数组
//知道左右两边的有序序列,有一边处理完成
while (i <= mid && j <= right) {
//比较左右两边序列的元素,小的加入temp中
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
i++;
t++;
} else {
temp[t] = arr[j];
j++;
t++;
}
}
//(二)
//把有剩余数据的一边的数据依次全部填充到temp
while (i <= mid) {
//左边的有序序列还有剩余的元素,就全部填充到temp
temp[t] = arr[i];
t++;
i++;
}
while (j <= right) {
//右边的有序序列还有剩余的元素,就全部填充到temp
temp[t] = arr[j];
t++;
j++;
}
//(三)
//将temp数组的元素拷贝到arr
//注意,并不是每次都拷贝所有
t = 0;
int tempLeft = left;
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
t++;
tempLeft++;
}
}
}
7)基数排序
(1)基本介绍:
- 基数排序属于“分配式排序”,又称“桶子法”,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
- 基数排序是属于稳定性的排序,基数排序法是效率高的稳定性排序法
- 基数排序是桶排序的扩展
- 排序思想:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
(2)排序图解:
- 说明:
- 基数排序是对传统桶排序的扩展,速度很快
- 基数排序是经典空间换时间的方式,占用内存很大,当对海量数据排序时,容易造成OutOfMemeoryError
- 基数排序是稳定的。【假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的,否则称为不稳定的】
- 有负数的数组,不用基数排序来进行排序
(3)代码实现:
package com.sort;
/*
@author qw
@date 2020/8/20 - 20:18
**/
import java.util.Arrays;
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) {
int max=arr[0]; //找到数组中的最大数
for(int i=1;i<arr.length;i++) {
if(max<arr[i])
max=arr[i];
}
int maxLen=(max+"").length(); //获取最大数的长度
//定义一个二维数组,用于表示10个桶,并放入对应位数的数据
int[][] bucket=new int[10][arr.length];
//定义一个一维数组,用于存放每个桶放入的数据个数
int[] bcount=new int[10];
for(int i=0,x=1;i<maxLen;i++,x=x*10) {
for(int j=0;j<arr.length;j++) {
int digit=arr[j]/ x %10; //取出对应位数的值
//放入对应的桶中
bucket[digit][bcount[digit]]=arr[j];
bcount[digit]++;
}
int index=0; //原数组下标
//遍历每一个桶 把桶中的数据放回到原数组中
for(int k=0;k<bcount.length;k++) {
if(bcount[k]!=0) {
//循环第k个桶 把该桶内数据放回原数组
for(int l=0;l<bcount[k];l++) {
arr[index++]=bucket[k][l];
}
}
//循环结束 将每个桶对应个数置0
bcount[k]=0;
}
}
}
}
5.排序算法时间复杂度比较
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6b4rkuEQ-1597938879865)(D:\Typora\img\排序算法时间复杂度.jpg)]
相关术语解释:
1)稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面
2)不稳定:如果a原本在b前面,而a=b,排序之后a可能会出现在b的后面
3)内排序:所有排序操作都在内存中完成
4)外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行
5)时间复杂度:一个算法执行所耗费的时间
6)空间复杂度:运行完一个程序所需内存的大小
7)n:数据规模
8)k:“桶”的个数
9)In-place:不占用额外内存
10)Out-place:占用额外内存