目录
基本排序算法(Basic Sorting Algorithm)
前言
上一章,我们认识了算法的概念,分类,这一次我们真正进入算法海洋之一的排序算法。
友情提示:本文中的代码由Java实现
先了解一下专业术语:
空间复杂度和时间复杂度:见文章算法(一):算法复杂度之时间复杂度和空间复杂度
稳定和不稳定:假如一组数据中存在两个相同的元素A=B,A在前B在后,排序后AB的前后顺序保持不变称之为稳定,反之则称之为不稳定,换句话说就是能避免不必要的位置交换
内排序和外排序:所有的排序操作都在内存中执行叫内排序,因数据较大把数据放在磁盘中进行排序的叫外排序
再来看一眼结构图:
最后看一下表格:
排序方式 | 时间复杂度(最佳) | 时间复杂度(最坏) | 时间复杂度(平均) | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
直接选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
直接插入排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
希尔排序 | O(nlogn) | O(n²) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(n²) | O(nlogn) | O(nlogn) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
基本排序算法(Basic Sorting Algorithm)
冒泡排序(Bubble Sort)
描述:从第一个元素开始和下一个元素比较,如果第一个比第二个大,那么交换两个元素的位置,继续和下一个元素比较;如果第一个比第二个小,那就拿第二个元素和下一个元素比较,直到最大的元素就会出现在最后一个位置,没有元素可以继续比较,算是完成一轮比较,然后继续按照上述规则,继续进行比较,除了最后面已经找出的最大元素,以此类推。
动态图展示:
代码:
/**
* 冒泡排序
* @param n
* @return
*/
public static void sort(int[] n) {
// 每一轮冒泡出开一个元素 外层循环次数就表示数组总共需要冒泡几轮
for(int i=0;i<n.length;i++) {
// 临时变量 用于冒泡时数据交换的备份
int temp;
// 内层循环表示该轮进行两两比较的次数
for(int j = 0;j<n.length-1-i;j++) {
//判断两个元素 如果前一个大于后一个 那么准备进行位置交换 反之不做任何操作
if(n[j]>n[j+1]) {
//把大的元素赋值到临时变量上
temp = n[j];
//把小的元素赋值到大的元素所在的位置
n[j]=n[j+1];
//把临时变量中的大元素值赋值到小元素所在的位置
n[j+1]=temp;
// 完成交换 继续循环
}
}
}
}
看完之后是不是内心想着原来如此,酱紫啊什么的。别高兴太早,其实冒泡排序还是可以进行优化的,接下往下看:
冒泡排序过程中最后面的都是有序的,每次排序都是只对前面的元素进行排序,大家想过没,有这么一种情况,假如前面的一部分元素已经是有序的或者说整体已经是有序的,但是由于不清楚是否有序,而进行冒泡排序。实际上只是做了无用功,没有发生任何交换,岂不是白白的浪费了时间,那该怎么进行改进呢?
其实很简单,我们知道外层循环是是冒泡的轮数,内层循环是进行比较找出最大的元素,只要后面的元素比前面的元素大,就会发生位置交换,换言之,如果没有发生交换,则表示一组元素中每两个相邻的元素都是前面的小于后面的,也就是说本身已经是有序的,既然数据有序就可以终止排序了,总结一句话:如果没有发生交换,则表示元素已经有序。
优化:
/**
* 冒泡排序优化版
* @param n
* @return
*/
public static void sort(int[] n) {
// 每一轮冒泡出开一个元素 外层循环次数就表示数组总共需要冒泡几轮
for(int i=0;i<n.length;i++) {
// 临时变量 用于冒泡时数据交换的备份
int temp;
// 布尔值 纪录是否经过交换 每一轮开始前重置状态为false 表示为交换
boolean flag=false;
// 内层循环表示该轮进行两两比较的次数
for(int j = 0;j<n.length-1-i;j++) {
// 判断两个元素 如果前一个大于后一个 那么准备进行位置交换 反之不做任何操作
if(n[j]>n[j+1]) {
// 把大的元素赋值到临时变量上
temp = n[j];
// 把小的元素赋值到大的元素所在的位置
n[j]=n[j+1];
// 把临时变量中的大元素值赋值到小元素所在的位置
n[j+1]=temp;
// 发生了交换 纪录状态
flag=true;
}
}
// 内层循环完毕后 查看flag状态是否发生了交换
if(!flag){
// 如果flag没有变化,则表示这一轮没有发生交换 当前就是有序的排列 终止程序
return;
}
//如果执行到这里,证明发生了交换 继续下一轮的比较
}
}
复杂度分析:
时间复杂度:冒泡排序的程序总运行次数为n(n+1)/2,即n²/2+n/2;只保留最高次幂,去除系数,得到时间复杂度为n²;即T(n)=O(n²);这是最差情况下的,同时也是平均情况下的时间复杂度;而最佳的情况下则是已经有序的一组数据,使用优化后的冒泡排序,只需要进行一轮比较就可以了,总次数为n,时间复杂度T(n)=O(n)。
空间复杂度:除去程序本身的大小,额外定义了几个辅助变量,与n的大小无关,所以空间复杂度S(n)=O(1)。
稳定性分析:
冒泡排序两个相同的元素经过两两互换称为相邻的元素时,只有判断为前方元素大于后方元素才进行互换,所以相等的时候不会互换,相对位置关系在排序后不会变好。根据稳定性原则,冒泡排序时稳定的。
直接选择排序(Selection Sort)
选择排序(Selection-sort) 是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,然后和首位元素进行位置互换,然后再从剩余未排序元素中继续寻找最小(大)元素,然后和未排序元素的首位进行位置互换。以此类推,直到所有元素均排序完毕。
动态图如下:
代码如下:
/**
* 直接选择排序
* @param array
* @return
*/
public static int[] selectionSort(int[] array) {
// 外层循环表示选择次数 每次目标是选择出一个最小的元素
for (int i = 0; i < array.length; i++) {
// 局部变量 纪录最小元素的索引 默认为未排序元素的首位元素索引
int minIndex = i;
// 内层循环排序 遍历未排序元素
for (int j = i; j < array.length; j++) {
// 如果未排序元素小于最小元素
if (array[j] < array[minIndex]){
// 以该元素的的索引覆盖默认索引
minIndex = j;
}
}
// 进行位置互换
int temp = array[minIndex];
array[minIndex] = array[i];
array[i] = temp;
}
return array;
}
最初始的选择排序其实还是有优化的余地的,比如:最后进行位置互换的时候,如果未排序的首位元素本身就是最小的,一轮比较下来没有比它更小的,那最后的互换操作就有点多余了。另外如果每次只找出最小的元素排在前面,那么完全可以做到同时找到最大的元素放在后面,最终形成前端有序,后端有序,中间部分无序的情况,每次排序都排出一个最大和最小,加快一半的速度完成
优化后的代码(程序为了方便理解,多写了几行if else语句,可以合并):
/**
* 直接选择排序优化版
* @param array
* @return
*/
public static int[] selectionSort(int[] array) {
// 数组长度
int len = array.size();
// 外层循环表示选择次数 进行选择的数组范围是left~right 没次选出一个最大值和最小值
for (int left = 0, right = len - 1; left < right; left++, right--) {
// 纪录最小和最大元素的索引 默认为待排序元素的首位和末位元素的索引
int min = left;
int max = right;
// 内层循环排序 遍历待排序元素
for (int i = left; i <= right; i++) {
// 如果待排序中的某元素小于指定的最小元素
if (array[i] < array[min]){
// 则该元素的的索引就是最小值索引
min = i;
}
// 如果待排序中的某元素大于指定的最大元素
if (array[i] > array[max]){
// 则该元素的的索引就是最大值索引
max = i;
}
}
// 如果待排序元素首位索引left和最小元素的索引min相等
if(left == min){
//表示待排序元素首位就是最小值,不需要进行交换
}else{
// 首位不是最小值,交换首位和最小值的位置
swap(array, left, min);
}
// 如果待排序元素首位的索引left和最大元素的索引max相等
if(left == max){
// 如果首位是最大值,上面一步已经最大值和最小值进行了交换,首位索引left对应的是最小值,最大值对应的索引是min,所以此时最大值的的索引max应该改为min
max = min;
}
// 如果待排序元素末位索引right和最大元素的索引max相等
if(right == max){
//表示待排序元素末位就是最大值,不需要进行交换
}else{
//末位不是最大值,交换末位和最大值的位置
swap(array,right,max);
}
}
return array;
}
// 位置互换
public static void swap(int[] array,int a,int b) {
int temp = array[a];
array[a] = array[b];
array[b] = temp;
}
复杂度分析:
时间复杂度:直接选择排序的程序总运行次数为n(n+1)/2,即n²/2+n/2;只保留最高次幂,去除系数,得到时间复杂度为n²;即T(n)=O(n²);
空间复杂度:除去程序本身的大小,额外定义了几个辅助变量,与n的大小无关,所以空间复杂度S(n)=O(1)。
稳定性分析:
直接选择排序是不稳定的。举个例子{2²,2³,1}(数字2右上角只是做标记区分排序前后的两个相同元素2的位置关系,不要看成平方和立方),第一次排序把1排到前面,其他位置不变,一次排列就有序了,实际上原来处于第二和第三位置的两个元素2,均向后移动了一位,变成了{1,2²,2³},根据稳定性规则,此排序方法不稳定。
直接插入排序(Insertion Sort)
直接插入排序(Insertion Sort),就是把一个个元素插入到有序元素中对应的位置上。举个例子:一套书,共有十部,被随意的排在一起,使用插入排序,把后面的书序号拿出来,往前放找对位置插进去,最终把书本排列成有序从第一部到第十部的样子。
动态图:
静态图(temp临时储存的元素值 ):
代码:
/**
* 直接插入排序
* @param array
* @return
*/
public static int[] insertionSort(int[] array) {
// 外层循环为插入的次数 从第二位元素开始算次数
for (int i = 0; i < array.length - 1; i++) {
// 把准备插入的元素记为临时变量 array[0]不参与排序,从第二位array[1]开始算 i+1就是要插入的元素的索引
int temp = array[i + 1];
// 首次比较的是要插入元素和该元素的前一位,i就是前一位元素的的索引
int preIndex = i;
// 比较要插入的元素和前一位元素,如果准备插入的元素小于前一位的元素
while (preIndex >= 0 && temp < array[preIndex]) {
// 前一位元素向后移动一位 索引变成要插入元素原来的位置
array[preIndex + 1] = array[preIndex];
// 索引自减1,继续循环向更前一位比较
preIndex--;
}
// 一直比较到要插入元素大于某一个元素或者索引preIndex为负数,没有元素可以比较,把要插入元素放在要比较元素索引后面,要比较的元素索引为preIndex,所以插入元素最终的位置就是preIndex+1
array[preIndex + 1] = temp;
}
return array;
}
复杂度分析:
时间复杂度:直接插入排序的程序总运行次数最差为n(n+1)/2,即n²/2+n/2(数据本身从大到小的顺序,需要经历比较和位置变化);最佳为n次(数据本身已经是从小到大的顺序,只需要比较不需要更换元素位置),只保留最高次幂,去除系数,得到时间复杂度为即最坏和平均:T(n)=O(n²),最佳:T(n)=O(n);
空间复杂度:除去程序本身的大小,额外定义了几个辅助变量,与n的大小无关,所以空间复杂度S(n)=O(1)。
稳定性分析:
直接插入排序是稳定的。和冒泡排序类似,每次都是两两比较,并且只有小于前一位元素才会继续比较,大于等于前一位元素都会放在该元素后面,所以远离相同的两个元素在排序后相对位置关系不会变化,根据稳定性规则,此排序方法稳定。
好了,暂时告一段落了,排序算法的队伍比较大,本章就只介绍基本排序算法,后续文章将会介绍几个高效排序算法。
古德拜!😁😁😁😁😁😁