🎗️ 新人博客首写,希望大家一起加油进步ヾ(•ω•`)o
🎗️ 乾坤未定,你我皆黑马
目录
一、排序以及分类
排序: 排序就是使一串记录的数据,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
排序的稳定性: 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
通俗来讲就是相同数据在排序前后的相对位置不发生变化。示意图如下:
二、插入排序
1.直接插入排序
-
基本思想
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。实际中我们玩扑克牌时,就用了插入排序的思想。 -
排序过程
遍历数组 下标 i 是从第二个数开始,下标 j 是从 i 的前面往前进行遍历,依次找到比 i 下标大的数字往后移,下标 i 的数插入到最后一个比 i 大的数前面(从下标 i 往前面数),移的过程前面就是已经有序的了,见下面动图更容易理解
-
代码实现
/**
* 直接插入排序算法
* 时间复杂度:1+2+...n = O(n*n)
* 最好的情况是O(n):即当数据趋于有序的时候,时间复杂度越低,
* 所以当数据基本有序的时候,建议使用直接插入排序
* 空间复杂度:O(1)
* 稳定性:稳定
* 如果一个排序是稳定的,可以改为不稳定的
* 如果一个排序是不稳定的,改不成稳定的
* @param array
*/
public static void insertSort2(int[] array) {
//i从一个开始,表示只有一个元素时自身有序,整体思路:将小数据移到前面位置
for (int i = 1; i < array.length; i++) {
int tmp = array[i]; //把值给先存起来
int j = i - 1;
for (; j >= 0; j--) {
if(array[j] > tmp) {
array[j + 1] = array[j];
} else {
array[j + 1] = tmp; //这个可以不写,因为跳出循环后也要执行
break; //表示走到这前面的数据已经有序,不需要再进入小循环内了
}
}
//走到这说明j不满足循环条件了,即是j = -1 或者是提前跳出循环表示前面的已经有序,此时把tmp的值给到j = 0
array[j + 1] = tmp;
}
}
- 直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高,所以当数据趋近于有序时更推荐直接插入排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|
O(N^2) | O(1) | 稳定 |
2.希尔排序
- 基本思想
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成多个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。
即是:假如有10个元素需要排序,首先将这10个元素为5组,对每组分别进行直接插入排序,再分为2组,并对每组分别进行直接插入排序,最后分为1组,进行直接插入排序; - 代码实现
/**
* 希尔排序 : 直接插入排序算法的一种改进
* 时间复杂度:O(N^1.3) 是个平均值
* 空间复杂度:O(1)
* 稳定性:不稳定的排序
* @param array
*/
public static void shellSort(int[] array){
int gap = array.length;
while(gap > 1) {
shell(array,gap);
gap /= 2; //以gap进行分组来进行直接插入排序
}
//整体进行一次希尔排序
shell(array,1);
}
//希尔排序的具体实现过程
public static void shell(int[] array,int gap) {
for (int i = gap; i < array.length; i++) {
int tmp = array[i];
int j = i - gap;
for (; j >= 0 ; j -= gap) {
if(array[j] > tmp) {
array[j+gap] = array[j];
} else {
//array[j + 1 ] = tmp;
break;
}
}
array[j+gap] = tmp;
}
}
- 希尔排序的特性总结:
- 可以看作是直接插入排序的一种优化,分组越往后,数据也就越有序,则时间效率也就越高
- 时间复杂度:O(N^1.3),是一个平均值,不是具体值
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
- 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排
序的时间复杂度都不固定:
时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|
O(N^1.3) | O(1) | 不稳定 |
三、选择排序
1.直接选择排序
-
基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。 -
实现过程:
-
在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素
-
若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
-
在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
见下动图更容易理解:
-
代码实现
/**
* 3.选择排序算法
* 时间复杂度:O(N^2)
* 空间复杂度:O(1)
* 稳定性:不稳定
* @param array
*/
public static void selectSort(int[] array) {
for (int i = 0; i < array.length; i++) {
int minIndex = i;
int j = i + 1;
//找到最小值的下标
for (; j < array.length; j++) {
if(array[j] < array[minIndex]) {
minIndex = j;
}
}
//进行交换
swap(array,i,minIndex);
}
}
//交换函数 进行封装起来
private static void swap(int[] array,int i,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
- 直接选择排序的特性总结
- 记忆方法:
i 从零下标开始遍历,j 从 i+ 1开始往后遍历,找到后面最小的值与 i 下标的值进行交换,所以 i 前面的数值就是已经有序的,从小到大
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|
O(N^2) | O(1) | 不稳定 |
2.堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
举例理解: 即是以升序为例,首先要建立一个大根堆,大根堆堆顶存放的是最大元素,每次将最大元素与end出元素交换,再调整剩下end-1部分的堆,这样就能依次把最大的元素放在后面 ; 同样的降序排列使用小根堆即可。
动图理解
- 代码实现
/**
* 5.堆排序:
* 时间复杂度:O(N*logn)
* 空间复杂度:O(1)
* 稳定性:不稳定的排序
* @param array
*/
public static void heapSort(int[] array) {
createBigHeap(array);
int end = array.length-1;
while (end > 0) {
swap(array,0,end);
shiftDown(array,0,end);
end--;
}
}
private static void createBigHeap(int[] array) {
for (int parent = (array.length-1-1)/2; parent >= 0 ; parent--) {
shiftDown(array,parent,array.length);
}
}
private static void shiftDown(int[] array,int parent,int len) {
int child = 2*parent+1;
while (child < len) {
if(child+1 < len && array[child] < array[child+1]) {
child++;
}
if(array[child] > array[parent]) {
swap(array,child,parent);
parent = child;
child = 2*parent+1;
}else {
break;
}
}
}
//交换函数 进行封装起来
private static void swap(int[] array,int i,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
- 堆排序的特性总结:
堆排序使用堆来选数,效率就高了很多。
时间复杂度:O(N*logN)
空间复杂度:O(1)
稳定性:不稳定
四、交换排序
1.冒泡排序
- 排序思想
每次都从第一个元素与后一个元素进行比较,如果后面的元素小,则进行交换,此时完成的就是升序排序。如果后面的元素大进行交换,就是降序排序,相当于气泡大的往上面跑。我们拿前者举例,每一趟比较完之后,都会在最后的位置确定一个该趟的最小值,见下动图更容易理解:
- 代码
/**
* 冒泡排序:
* 时间复杂度:(不要考虑优化)O(N^2)
* 空间复杂度:O(1)
* 稳定性:稳定的
* @param array
*/
public static void bubbleSort(int[] array) {
for (int i = 0; i < array.length-1; i++) {
boolean flg = false;
for (int j = 0; j < array.length-1-i; j++) {
if(array[j] > array[j+1]) {
swap(array,j,j+1);
flg = true;
}
}
if(flg == false) {
return;
}
}
}
- 冒泡排序的特性总结
- 记忆方法:
从一开始进行两两比较,最小的值依次放在最后面
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|
O(N^2) | O(1) | 稳定 |
2.快速排序
- 基本思想
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
按照基准划分为左右两个子序列的不同方式,又可分为两个方法:
- Hoare法
- 挖坑法
2.1 Hoare 法
-
思路:
-
先将最左边的值作为基准
-
先让 right 从数组最右边开始向左遍历,遇到比基准小的就停下。再让 left 从数组左边开始向右遍历,遇到比基准大的就停下
-
交换 right 和 left 所存储的值,重复第二,三个步骤
-
直到 left 和 right 相遇,此时 left 和 right 相同的这个值和基准进行交换,交换结果就是基准跑到中间位置,基准左边都是比基准小的,右边都是比基准大的,之后在重新选取左半子序列和右半子序列的基准,重复上述操作即可。具体实现过程可见下面的动图更容易理解:
-
代码实现:
/**
* 6.快排法
*时间复杂度:N*logN
* 最好情况 :N*logN
* 最坏情况:N^2 有序(只有单树的情况,树的高度为n)、逆序的时候,时间复杂度会高 优化!!!
*空间复杂度:
* 最好情况:logN
* 最坏情况:N
* 稳定性:不稳定
* @param array
*/
public static void quickSort1(int[] array) {
quick(array,0,array.length - 1); //给下标
}
public static void quick(int[] array,int start,int end) {
if(start >= end) { //注意此处是大于等于 而不是=号,大于的时候代表左边为空了没有值,即是start=0,end = -1
return;
}
//进行优化,三数取中法
int index = midThree(array,start,end);
swap(array,index,start);
int pivot = partition(array,start,end); //进行划分
quick(array,start,pivot - 1); //左边进行排序
quick(array,pivot + 1,end); //右边进行排序
}
//交换函数 进行封装起来
private static void swap(int[] array,int i,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
//找到基准值的改进方法:三数取中
private static int midThree(int[] array,int left,int right) {
int mid = (left+right) / 2;
//6 8
if(array[left] < array[right]) {
if(array[mid] < array[left]) {
return left;
}else if(array[mid] > array[right]) {
return right;
}else {
return mid;
}
}else {
//array[left] > array[right]
if(array[mid] < array[right]) {
return right;
}else if(array[mid] > array[left]) {
return left;
}else {
return mid;
}
}
}
//hoare法 (这个感觉容易理解点)
public static int partition3(int[] array,int left,int right) {
int tmp = array[left];
int ret = left;
while (left < right) {
//左边做基准,要先走右边,不然最后相遇的地方会出错,会相遇到比基准大的地方
//此时在进行交换,就会发生错误,导致比基准大的跑到最前面了,应该是小于基准的跑到前面去
//右边right找到比基准小的停下来
while(left < right && array[right] >= tmp) {
right--;
}
//左边left找到比基准大的停下来
while(left < right && array[left] <= tmp) {
left++;
}
swap(array,left,right);
}
//走到这说明相遇了,left的值和tmp进行交换
swap(array,ret,left);
return left;
}
- 注意事项:
当我们选取到左边为基准时,要先让右边走,再让左边走,这样最后相遇的才保证是比基准小的值,进行交换的话就把小的值交换到左边。
若是左边先走,则是相遇到比基准大的值,进行交换,最左边的值就会比中间基准大,不符合我们之前分析的:让基准左边都是比基准小的。
2.2 挖坑法
- 思路:
与hoare法步骤相差不大,细节略有不同。
hoare法:右边找到小的值,左边找到大的值,进行交换。
挖坑法:
-
以左边基准值为坑,右边先走,遇到比基准小的值,将 right 值给到左边(填坑),此时 right 位置处是一个新坑,
-
之后是左边走,遇到比基准大的值,将 left 位置处值给到 right 处
-
重复上述过程直至相遇,此处是新坑,把原来先存储起来的基准值放到这个新坑处,就保证了基准左边是小值,右边是比基准大的值。
具体实现过程可见下面的动图更容易理解:
-
代码实现:
/**
* 6.快排法
*时间复杂度:N*logN
* 最好情况 :N*logN
* 最坏情况:N^2 有序(只有单树的情况,树的高度为n)、逆序的时候,时间复杂度会高 优化!!!
*空间复杂度:
* 最好情况:logN
* 最坏情况:N
* 稳定性:不稳定
* @param array
*/
public static void quickSort1(int[] array) {
quick(array,0,array.length - 1); //给下标
}
public static void quick(int[] array,int start,int end) {
if(start >= end) { //注意此处是大于等于 而不是=号,大于的时候代表左边为空了没有值,即是start=0,end = -1
return;
}
//进行优化,三数取中法
int index = midThree(array,start,end);
swap(array,index,start);
int pivot = partition(array,start,end); //进行划分
quick(array,start,pivot - 1); //左边进行排序
quick(array,pivot + 1,end); //右边进行排序
}
//交换函数 进行封装起来
private static void swap(int[] array,int i,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
//找到基准值的改进方法:三数取中
private static int midThree(int[] array,int left,int right) {
int mid = (left+right) / 2;
//6 8
if(array[left] < array[right]) {
if(array[mid] < array[left]) {
return left;
}else if(array[mid] > array[right]) {
return right;
}else {
return mid;
}
}else {
//array[left] > array[right]
if(array[mid] < array[right]) {
return right;
}else if(array[mid] > array[left]) {
return left;
}else {
return mid;
}
}
}
//划分的方法 挖坑法
public static int partition(int[] array,int left,int right) {
int tmp = array[left];
while(left < right) {
//左边做基准,要先走右边,不然最后相遇的地方会出错,会相遇到比基准大的地方
//因为基准在左边,所以先从右边进行找小的值,把小的值转移到左边
while( left < right && array[right] >= tmp) { //注意此处必须取等,不然会陷入循环 如:6 2 1 6
right--;
}
//走到这说明right下标的值就是右边第一个小于tmp的数,给到最左边
array[left] = array[right];
while( left < right && array[left] <= tmp) { //注意此处必须取等,不然会陷入循环 如:6 2 1 6
left++;
}
//走到这说明left下标的值就是左边第一个大于tmp的数,给到最右边
array[right] = array[left];
}
//循环走完说明相遇了,把tmp的值给到left或者right
array[left] = tmp;
return left; //返回下标
}
-
注意事项:
同Hoare法,左边为基准值,先让右边走 -
冒泡排序的特性总结
-
记忆方法:
Hoare法:从右边先走,遇到比基准值小停下,此时左边大值停下,进行交换,直至相遇
挖坑法:从右边先走,遇到比基准值小停下,右边值给到左边,之后左边走,遇到大值给右边,直至相遇
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|
O(N^logN) | O(logN) | 不稳定 |
五、归并排序
- 思路:
像二叉树一样,将数据分为左边组和右边组,一直分到一组只有一个元素,单个元素肯定是有序的,之后再进行合并有序即可。整体即是左边组有序,右边有序,之后再合并在一起进行排序。
见下面的动图助于理解:
- 代码:
/**
* 7.归并排序
* 时间复杂度:N*logN
* 空间复杂度:N
* 稳定性:稳定的排序
* 插入排序 冒泡排序 归并排序
* @param array
*/
//递归实现的归并排序
public static void mergeSort1(int[] array) {
mergeSortFunc(array,0,array.length-1);
}
//分解的代码
private static void mergeSortFunc(int[] array,int left,int right) {
if(left >= right) {
return;
}
int mid = (left+right) / 2;
mergeSortFunc(array,left,mid);
mergeSortFunc(array,mid+1,right);
merge(array,left,right,mid);
}
//主要实现的代码
//合并的代码
private static void merge(int[] array,int start,int end,int mid) {
int s1 = start;
int e1 = mid;
int s2 = mid + 1;
int e2 = end;
int[] tmp = new int[end - s1 + 1];
int k = 0; //记录新数组的下标
while(s1 <= mid && s2 <= end) {
if(array[s1] > array[s2]) { //s2小,把小的放进新数组里
tmp[k++] = array[s2++];
// k++;
// s2++;
} else {
tmp[k++] = array[s1++];
}
}
while(s1 <= mid) { //说明s1所在数组内还有数据
tmp[k++] = array[s1++];
}
while(s2 <= end) { //说明s2所在数组内还有数据
tmp[k++] = array[s2++]; //把s2所在数组的数据添加到新数组内
}
//再拷贝到原来数组中去
for (k = 0; k < tmp.length; k++) {
array[k + start] = tmp[k];
}
}
- 归并排序总结
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|
O(N^logN) | O(N) | 稳定 |
六、七大排序比较总结
🎗️🎗️🎗️ 好啦,到这里我们的排序分享就没了,如果感觉做的还不错的可以点个赞,关注一下,你的支持就是我继续下去的动力,蟹蟹大家了,我们下期再见,拜拜~ ☆*: .。. o(≧▽≦)o .。.:*☆