写在前面
- 代码实测
所有的代码,都经过了LeetCode 912的测试,有一部分时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)的代码超时,超时的算法有:冒泡排序和选择排序,两者实现简单。
附图如下
-
文章目的
最近一直在准备秋招笔试,日常被各种算法题暴打,最近发现自己连一些排序最基础的概念都模糊不清,特地参考教材,总结了各个排序算法。
《数据结构教程(第五版)》李春葆
- 如果有看不懂的代码,或注释,可以手动举几个样例,手算一遍会使思路更清晰,请原谅我这个菜鸡。。。
排序汇总
直接插入排序
-
原理:
将数组划分为有序区和无序区,每一次将当前无序区开头元素插入到有序区适当位置(增量法:每一次有序区增加一个元素)
-
特点:
就地排序、稳定排序、 O ( n 2 ) O(n^2) O(n2)的时间复杂度
public void InsortSort(int[] nums,int n){
/**
对n个数进行直接插入排序
例如 5 2 3 1,num数组中值的变化
i=1:由于2和5逆序, 2 5 3 1
i=2:2 5 5 1 -> 2 3 5 1
i=3: 2 3 5 5->2 3 3 5 -> 2 2 3 5->插入1, 1 2 3 5
*/
int temp,j;//temp暂存nums[i],j记录需要后移的位置
for(int i=1;i<n;i++){
if(nums[i]<nums[i-1]){
//只有当逆序时,才交换
//寻找位置
temp=nums[i];
j=i-1;
do{
nums[j+1]=nums[j];//后移
j--;
}while(j>=0&&nums[j]>temp);//直到nums[j]<=temp时,停止移动
nums[j+1]=temp;//填入nums[i],此时nums[j]处在正确位置nums[j+1]也向后移动了一位
}
}
}
折半插入排序
-
原理:
- 在直接插入排序中,在有序区通过二分的方法找到无序区第一个元素应该存放位置index,然后统一将有序区index后元素后移一位
-
特点:
- 和直接插入排序相比性能没有改善, O ( n 2 ) 的 时 间 复 杂 度 O(n^2)的时间复杂度 O(n2)的时间复杂度,仅仅是减少了关键字比较次数
- 稳定的排序
public void BinInsortSort(int[] nums,int n){
/**
对n个数进行折半插入排序
*/
int temp;
int lower,high;
for(int i=1;i<n;i++){
if(nums[i]<nums[i-1]){
//逆序时,排序
temp=nums[i];
lower=0;
high=i-1;//在[lower,high]做二分查找第一个大于等于num[i]的位置(j)
while(lower<high){
int mid=(lower+high)>>1;//此种二分要注意溢出问题,更普遍的方法是:lower+(high-lower)/2;
if(nums[mid]<temp){
lower=mid+1;//此种二分每次都能缩小区间,所以不可能出现死循环
}else{
high=mid;
}
}
//此时lower=high,第一个大于num[i]的位置j
for(int j=i-1;j>=lower;j--){
//有序区lower后面的元素全部后移一位
nums[j+1]=nums[j];
}
//将lower位置赋值num[i]
nums[lower]=temp;
}
}
}
希尔排序
-
一种分组插入排序
-
选取一个小于数组长度n的整数 d 1 , 一 般 为 n / 2 d_1,一般为 n/2 d1,一般为n/2,将所有距离为d1倍数的元素放在同一个组
-
例如:1 2 3 4 5 6 7 8 9 10
-
分为5组:
1 6
2 7
3 8
4 9
5 10
分别做直接插入排序,然后再分为2组、1组
-
特点:
- 希尔排序实现较复杂,时间复杂度难以计算,一般认为 O ( n 1.3 ) O(n^{1.3}) O(n1.3),
- 是一种不稳定的排序
-
public void ShellSort(int[] nums,int n){
/**
希尔排序
例子:5 1 1 2 0 0 n=6
分3组:
5 2 直接插入排序: 2 5
1 0 0 1
1 0 0 1
此时数组: 2 0 0 5 1 1
分2组:
2 0 1 直接插入排序: 0 1 2
0 5 1 0 1 5
此时数组: 0 0 1 1 5 2 //大部分已经有序
分1组也就是数组本事,只有一个逆序,做一次插入排序即可
*/
int temp;
int d=n/2;//分组数量
while(d>0){
for(int i=d;i<n;i++){
//对多组进行直接插入排序
temp=nums[i];
int j=i-d;//记录后移的位置
while(j>=0&&temp<nums[j]){
//逆序就后移
nums[j+d]=nums[j];
j-=d;
}
//在正确位置上插入
nums[j+d]=temp;
}
d/=2;
}
}
冒泡排序(超时)
-
原理:
- 在无序区中比较相邻的两个元素,逆序则交换
- 注意:(冒泡排序元素的交换次数,就等于数组中的逆序对数,逆序对数 快速的求解方法为归并排序),这也是字节、微软考过的面试题,如果想了解此算法可以通过下面链接,在此就不贴代码了.
逆序对数
public void BubbleSort(int[] nums,int n){
/**
超时!!!
冒泡排序实现起来简单,但性能较低
*/
if(n==1)
return;
boolean exchange=false;
for(int i=0;i<n-1;i++){//n个元素只要找到前n-1大元素,就可以完成排序
exchange=false;
for(int j=n-1;j>i;j--){
//每次把无序区最小值放到有序区的末尾
if(nums[j]<nums[j-1]){
int temp=nums[j];//交换的临时变量
nums[j]=nums[j-1];
nums[j-1]=temp;
exchange=true;
}
}
if(!exchange){
//如果遍历一次无序区,没有元素交换,则可以直接退出
return;
}
}
}
快速排序
-
注意快速排序是一个不稳定的排序算法,面试很可能会被问到如何实现稳定的快排
- 首先快排的思想就是选取一个基准元素A,将所有小于A的元素放在一侧,大于A的元素放在另一侧,但由于传统快排实现中,只要小于或大于的基准元素就交换,很可能不满足稳定性
稳定性: 大小相同的两个值在排序之前和排序之后的先后顺序不变
- 稳定的快排需要借助一个辅助数组,遍历一遍数组,将小于基准元素加入数组中,再遍历一遍,将大于的加入数组中,最后将辅助数组转移到原数组中,稳定的快速排序因为需要多次遍历整个数组,因此效率会低很多。
-
还要注意的是,快速排序空间复杂度: O ( l o g n ) O(logn) O(logn),因为递归树的高度: O ( l o g n ) O(logn) O(logn)所以需要需要栈空间 O ( l o g n ) O(logn) O(logn)
-
快排常考第K大问题,快排思想可以实现 O ( n ) O(n) O(n)的时间复杂度
- 题目地址: 数组中第K大个元素
-
传统快排算法
public void quickSort(int[] nums,int lower,int high){ //[lower,high)
/**
* 快速排序:
* 从数组中取出一个数(通常第一个)
* 将比这个数大的全放在右边,小的放在左边(得到某个位置,左边数都比这个数小,右边数都比它大)
* 递归第二步得到的两个子数组
*
*/
if(lower<high){//结束条件
int i=partition(nums,lower,high);
quickSort(nums,lower,i);//[lower,high)右开
quickSort(nums,i+1,high);
}
}
public int partition(int[] nums,int lower,int high){
int base=nums[lower];
int i=lower;
int j=high-1; //[lower,high)右开
while(i<j){
while(i<j&&nums[j]>=base)
j--;
nums[i]=nums[j];
// nums[j]=base;
while(i<j&&base>=nums[i])
i++;
nums[j]=nums[i];
}
nums[i]=base;
return i;
}
- 稳定的快速排序实现
- 虽然理论上时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),但是超时,没有通过样例
public void quickSort(int[] nums,int lower,int high,int[] temp){ //[lower,high)
/**
* 快速排序:
* 从数组中取出一个数(通常第一个)
* 将比这个数大的全放在右边,小的放在左边(得到某个位置,左边数都比这个数小,右边数都比它大)
* 递归第二步得到的两个子数组
*
*/
if(lower+1<high){//结束条件
int i=partition(nums,lower,high,temp);
quickSort(nums,lower,i,temp);//[lower,high)右开
quickSort(nums,i+1,high,temp);
}
}
public int partition(int[] nums,int lower,int high,int[] temp){
//[lower,high)排序
int index;
int base=nums[lower];
int j=lower;//存储写入temp数组的索引
for(int i=lower+1;i<high;i++){
if(nums[i]<base){
temp[j++]=nums[i];
}
}
index=j;
temp[j++]=base;
for(int i=lower+1;i<high;i++){
if(nums[i]>=base){//对于和base相等的元素,由于基准元素索引位lower,所以相等元素base放后面即可保证稳定性
temp[j++]=nums[i];
}
}
//将temp全部转移到nums即可
for(int i=lower;i<high;i++){
nums[i]=temp[i];
}
return index;
}
选择排序(超时)
-
思想:
- 每一趟从待排序的元素中选出关键字最小的元素,放到已排好序的元素最后
- 适合从大量元素中选择一部分排序元素,例如从10000个元素选择前10位(但是利用堆可以更快找到前K大元素)
public void SelectSort(int[] nums,int n){
/**
超时!!!
选择排序:一次选择无序区最小值和无序区第一个交换
*/
int index;//保存无序区的最小值索引
for(int i=0;i<n-1;i++){
index=i;
for(int j=i+1;j<n;j++){
if(nums[j]<nums[index]){
index=j;
}
}
if(index!=i){
//交换nums[i],nums[k]
int temp=nums[index];
nums[index]=nums[i];
nums[i]=temp;
}
}
}
堆排序
-
由于建立初始堆所需比较次数较多,所以堆排序不适合元素数较少的排序表
-
不稳定排序方法
-
注意堆在优先队列中的应用
-
注意:堆排序和选择排序一样,和初始序列顺序无关
-
注意:
- 以0为开始下标的两个子节点分别是:2i+1、2i+2
- 以1为开始下标的两个子节点分别是:2i、2i+1
public void SelectSort(int[] nums,int n){
/**
堆排序:
shit()方法:如果左右子树都是大根堆,插入当前结点后形成的树,继续保持大根堆的特性
因此,只要从最后一个分支结点向前遍历,就可以建立一棵大根堆,这是因为当遍历到前面的一个结点i,以i为根节点的两颗子树2*i,2*i+1,都符合大根堆的特性!
*/
for(int i=(n-2)/2;i>=0;i--){
//注意数组下标从1开始
shit(nums,i,n);
}
for(int i=n-1;i>0;i--){
//将堆顶,也就是堆中最大元素,放到数组后面
//交换
int temp=nums[0];
nums[0]=nums[i];
nums[i]=temp;
//堆顶元素变了,维护大根堆
shit(nums,0,i);
}
}
public void shit(int[] nums,int lower,int high){//维护[lower,high)的大根堆
//以lower为根节点,维护大根堆特性
int i=lower;//指向当前结点
int j=2*lower+1;//当前节点的最大孩子节点
int temp=nums[i];//
while(j<high){
if(j<high-1&&nums[j]<nums[j+1]){
j=j+1;
}
if(temp<nums[j]){//如果根节点小于最大的孩子
//交换
nums[i]=nums[j];
i=j;//递归到孩子节点
j=2*i+1;
}else{
break;
}
}
nums[i]=temp;
}
归并排序
-
思想:分治法
-
应用:逆序对数
public void mergeSort(int[] nums,int lower,int high,int[] temp){ //排序[lower,high)
/**
* 归并排序:
* 利用分治的思想,将数组分成两个小数组,直到数组长度为1(此时长度为1的数组本身就时排好序的)
* 合并:将两个排好序的数组,合并成一个排序数组,时间复杂度 O(n)
* 注意:因为排序数组为[lower,high) 所以 high-lower=1 时数组长度为1,因此high-lower>1 才需要分治
*/
if(high-lower>1){
int mid=lower+(high-lower+1)/2;
mergeSort(nums,lower,mid,temp);
mergeSort(nums,mid,high,temp);
Merge(nums,lower,mid,high,temp);
}
}
public void Merge(int[] num,int lower,int mid,int high,int[] temp){//[lower,high)
//[lower,mid)和[mid,high)两个有序数组合并
for(int i=lower;i<high;i++){
temp[i]=num[i];
}
int i=lower;
int j=mid;
for(int k=lower;k<high;k++){
if(i==mid){
//如果[lower,mid)全部放入nums中
num[k]=temp[j++];
}else if(j==high){
//如果[mid,high)全部放入nums中
num[k]=temp[i++];
}else if(temp[i]<temp[j]){
num[k]=temp[i++];
}else{
num[k]=temp[j++];
}
}
}
基数排序、桶排序
都可以实现 O ( n ) O(n) O(n)的时间复杂度,也是面试的常考点,这里没有实现,之后如果有例题啥的会补上。