一、排序算法介绍:
排序也称排序算法(Sort Algorithm),排序是将一组数据依指定的顺序进行排列的过程。
排序的分类:
(1)内部排序:指将需要处理的所有数据都加载到内部存储器中进行排序,包括插入排序(直接插入排序、希尔排序)、选择排序(简单选择排序、堆排序)、交换排序(冒泡排序、快速排序)、归并排序和基数排序
(2)外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。
二、算法的时间复杂度:
一、度量一个程序(算法)执行时间的两种方法:
(1)事后统计的方法:这种方法可行,但是有两个问题。一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素。这种方式要在同一台计算机的相同状态下运行,才能比较算法速度。
(2)事前估算的方法:通过分析某个算法的时间复杂度来判断哪个算法更优
二、时间频度:
一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度,记为T(n)
三、时间复杂度:
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)不同但时间复杂度可能相同。
3.计算时间复杂度的方法:
a.用常数1代替运行时间中的所有加法常数
b.修改后的运行次数函数中,只保留最高阶项
c.去除最高阶项的系数
4.常见的算法复杂度:
(1).常数阶时间复杂度:无论代码执行多少行,没有循环等复杂结构,这个代码的时间复杂度都是O(1);
(2).对数阶时间复杂度:
int i = 1;
while(i<=n){
i *= 2;
}
3.线性阶时间复杂度:
for(int i =1;i<=n;i++){
System.out.println(i);
}
(3).线性对数阶时间复杂度:
for(int i =1;i<=n;i++){
int j = 1;
while(j<=n){
j *= 2;
}
}
(4).平方阶:
for(int i =1;i<=n;i++){
for(int i =1;j<=n;j++){
System.out.println(j);
}
}
5.平均时间复杂度和最坏时间复杂度
(1)平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下该算法的运行时间;
(2)最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。这样做的原因是最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。
三、空间复杂度:
基本介绍:
(1)类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数。
(2)空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况
(3)在做算法分析时主要讨论的是时间复杂度。从用户使用体验上看更看重的程序执行的速度。
四、冒泡排序算法:
1.思路介绍:
冒泡排序(Bubble Sorting)的基本思想是通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就像水底下的气泡一样逐渐向上冒。
因为排序的过程中各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列是有序的,因此要在排序过程中设置一个标志flag判断元素是否进行过交换,从而减少不必要的比较。
2.规则总结:
(1)一共进行数组大小-1次排序
(2)每一次排序的次数逐渐减少
(3)如果发现在某趟排序中没有发生一次交换,可以提前结束冒泡排序,也就是进行优化
3.代码实现:先找最大值,从后往前
int[] arr = {3,9,-1,10,-2};
for(int j = 0;j < arr.length - 1;j++){
一共排序“数组大小-1”趟
每趟需要排序的只是前面的数据,因此每趟排序的比较次数逐渐减1
for(int i = 0; i < arr.length - j - 1;i++){
if(arr[i] > arr[i+1]){
int temp = arr[i];
arr[i] = arr[i+1];
arr[i+1] = temp;
}
}
}
4.代码优化
int[] arr = {3,9,-1,10,-2};
boolean flag = false;
for(int j = 0;j < arr.length - 1;j++){
一共排序“数组大小-1”趟
每趟需要排序的只是前面的数据,因此每趟排序的比较次数逐渐减1
for(int i = 0; i < arr.length - j - 1;i++){
if(arr[i] > arr[i+1]){
flag = true;
int temp = arr[i];
arr[i] = arr[i+1];
arr[i+1] = temp;
}
}
if(!flag){
break;
}else{
flag = false;
}
}
五、选择排序算法:
选择式排序也属于内部排序法,是从待排序的数据中按指定的规则选出某元素,再依规定交换位置后达到排序的目的。
1.思路分析:
(1)第一次从arr[0]~arr[n-1]中选取最小值并与arr[0]交换;
(2)第二次从arr[1]~arr[n-1]中选取最小值并与arr[1]交换;
(3)第n-1次从arr[n-2]~arr[n-1]中选取最小值并与arr[n-2]交换,总共通过n-1次循环得到一个从小到大排列的有序序列;
2.规则总结:
(1)选择排序一共有数组大小-1趟排序
(2)每轮排序又是一个循环,先假定当前的这个数是最小的,然后和后面的每个数进行比较,如果发现有比当前数更小的数就重新确定最小数并得到下标;
(3)当遍历到数组的最后时就得到本轮最小数和下标,并进行交换
3.代码实现:先找最小值,从前往后的顺序
int [] arr = {1,5,2,4,7};
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;
}
}
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
六、插入排序:
1.插入排序算法介绍:
插入式排序属于内部排序法,是对于待排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。
2.算法思想:
把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
3.代码实现:
int[] arr = {101,34,119,1};
int[] arr = {34,101,119,1};
int[] arr = {34,101,119,1};
int[] arr = {1,34,101,119};
public static void insertSort(int[] arr){
for(int i = 1;i<arr.length;i++){
int insertIndex = i - 1;//前i-1个元素是有序的
int insertVal = arr[i];//获取此次待插入的值
//insertIndex>=0确保插入位置不越界
while(insertIndex>=0&&insertVal<arr[insertIndex]){
arr[insertIndex + 1] = arr[insertIndex];
//往后移动有序表,直到找到待插入元素合适的位置或到达数组头为止
insertIndex--;
}
if(insertIndex+1!=i){
arr[insertIndex+1] = insertVal;
}
}
}
七、希尔排序
1.插入排序算法介绍:
希尔排序是希尔(Donald shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
2.算法思想:
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时整个文件恰被分成一组,算法便终止;
3.代码实现:
int[] arr = {8,9,1,7,2,3,5,4,6,0};
public static void shell(int[]arr){
for(int gap = arr.length/2;gap>0;gap/=2){
int temp = 0;
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;
}
}
}
}
}
public static void shell2(int[]arr){
for(int gap = arr.length/2;gap>0;gap/=2){
for(int i = gap;i<arr.length;i++){
int j = i;
int temp = arr[j];
if(arr[j]<arr[j-temp]){
while(j-gap>=0&&temp<arr[j-gap]){
arr[j] = arr[j-gap];
j -= gap;
}
arr[j] = temp;
}
}
}
}
}
八、快速排序:
1.算法介绍:
快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
2.代码实现:
int[] arr = {-9,78,0,23,-567,70};
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(l<r){
在pivot左边一直找,找到大于等于pivot的值才退出
while(arr[l]<pivot){
l += 1;
}
在pivot右边一直找,找到小于等于pivot的值才退出
while(arr[r]>pivot){
r -= 1;
}
如果l>=r说明已经满足左边都是小于等于pivot的值以及右边都是大于等于pivot的值
if(l>=r){
break;
}
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
如果交换完毕后发现arr[l] == pivot,前移
if(arr[l] == pivot){
r--;
}
反之同理
if(arr[r] == pivot){
l++;
}
}
if(l==r){
l++;
r--;
}
if(left<r){
quickSort(arr,left,r);
}
if(right>l){
quickSort(arr,l,right);
}
}
九、归并排序:
1.算法介绍:
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
2.代码实现:
int[] arr = {8,4,5,7,1,3,6,2};
合并方法
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数组的当前索引
先把左右两边有序的数据按照规则填充到temp数组中
直到左右两边的有序序列有一边处理完毕为止
while(i<=mid&&j<=right){
如果左边的有序序列的当前元素小于等于右边的有序序列的当前元素
则将左边的当前元素拷贝到temp数组
if(arr[i]<=arr[j]){
temp[t] = arr[i];
t++;
i++;
}else{
反之亦然
temp[t] = arr[j];
t++;
j++;
}
}
把有剩余数据的一边的数据依次全部填充到temp中
while(i<=mid){
temp[t] = arr[i];
t++;
i++;
}
while(j<=right){
temp[t] = arr[j];
t++;
j++;
}
将temp数组的元素拷贝到arr中
t = 0;
int tempLeft = left;
while(tempLeft<=right){
arr[tempLeft] = temp[t];
t++;
tempLeft++;
}
}
分解+合并算法
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);
}
}
十、基数排序:
1.算法介绍:
基数排序(radixsort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort),顾名思义它是通过键值的各个位的值将要排序的元素分配至某些“桶”中,达到排序的作用;
基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法
基数排序(Radix Sort)是桶排序的扩展;
基数排序将整数按位数切割成不同的数字,然后按每个位数分别比较。
2.基本思想:
基数排序将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成后数列就变成一个有序序列。
3.代码实现:
public static void radixSort(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();
for(int i = 0,n=1;i<maxLength;i++,n*=10){
定义一个二维数组,表示10个桶,每个桶就是一个一维数组
二维数组包含10个一维数组
防止在放入数据的过程中出现数据溢出,每个一维数组的长度必须是原数组的长度
int[][] bucket = new int[10][arr.length];
记录每个桶中实际存储了多少数据,定义一个一维数组记录各个桶每次放入的数据个数
int[] buckElementCounts = new int[10];
for(int j = 0;j<arr.length;j++){
取出每个元素的个位
int digitOfElement = arr[j] / n % 10;
bucket[digitOfElement][bucketElementCounts[digitOfElement]++] = arr[j];
}
按照桶的顺序依次取出数据放入原来的数组
int index = 0;
每遍历一个桶就将其中的数据放入到原数组中
for(int k = 0;k < bucketElementCounts.length; k++){
if(bucketElementsCounts[k]!=0){
如果桶中有数据才放入到原数组中
for(int l = 0;l<bucketElementCounts[k];l++){
arr[index++] = bucket[k][l];
}
}
}
}
}
4.说明:
(1)基数排序是对传统桶排序的扩展,速度很快;
(2)基数排序是经典的空间换时间的方式,占用内存很大,当对海量数据排序时容易造成OutOfMemoryEror
(3)基数排序是稳定的。[注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序后这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的]
十一、常用的排序算法的总结对比
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定性 |
冒泡排序 | In-Place | 稳定 | ||||
选择排序 | In-Place | 不稳定 | ||||
插入排序 | In-Place | 稳定 | ||||
希尔排序 | In-Place | 不稳定 | ||||
归并排序 | Out-Place | 稳定 | ||||
快速排序 | In-Place | 不稳定 | ||||
堆排序 | In-Place | 不稳定 | ||||
计数排序 | Out-Place | 稳定 | ||||
桶排序 | Out-Place | 稳定 | ||||
基数排序 | Out-Place | 稳定 |
内排序:所有排序操作都在内存中完成
外排序:由于数据太大因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行
N:数据的规模
K:桶的个数
In-Place:不占用额外的内存
Out-Place:占用额外的内存