面试相关-七大排序算法:图解+动图+最直观的代码分析_性能比较
Posted 尚墨1111
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试相关-七大排序算法:图解+动图+最直观的代码分析_性能比较相关的知识,希望对你有一定的参考价值。
1 排序算法:
1.1 面试
面试中最常考的是快速排序和归并排序,经常有面试官要求现场写出这两种排序的代码。
其他排序可能会要求比较各自的优劣、各种算法的思想及其使用场景。分析算法的时间和空间复杂度。
1.2 区分
简单的排序:冒泡排序、选择排序、插入排序
简单排序的变种:快速排序、堆排序、希尔排序,比较高效的排序。
基于分治递归思想:归并排序
线性排序:计数排序、桶排序、基数排序三种
排序算法大体可分为两种:
- 比较排序:时间复杂度最少可达到
O(nlogn)
,主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。 - 非比较排序:时间复杂度可以达到
O(n)
,主要有:计数排序,基数排序,桶排序等。
1.3 性能比较
1.3.1 时间性能:
从平均时间来看,快速排序效率最高的,但快速排序在最坏情况下的时间性能不如堆排序和归并排序。而后者相比较的结果是,在n较大时归并排序使用时间较少,但使用辅助空间较多。(用时间换空间)
序列基本有序或者n较小时,直接插入排序是好的方法,因此常将它和其他的排序方法,如快速排序、归并排序等结合在一起使用。
基数排序的时间复杂度也可以写成O(d*n)。因此它最使用于n值很大而关键字较小的的序列。若关键字也很大,而序列中大多数记录的最高关键字均不同,则亦可先按最高关键字不同,将序列分成若干小的子序列,而后进行直接插入排序。
1.3.2 稳定性比较:
排序算法稳定性
- 稳定的:如果存在多个具有相同排序码的记录,经过排序后,这些记录的相对次序仍然保持不变,则这种排序算法称为稳定的。
插入排序、冒泡排序、归并排序、非比较排序(基数、计数、桶式)都是稳定的排序算法。 - 不稳定的:直接选择排序、堆排序、希尔排序、快速排序。
附:基于比较排序算法时间下限为O(nlogn)的证明:
基于比较排序下限的证明是通过决策树证明的,决策树的高度Ω(nlgn),这样就得出了比较排序的下限。
1.3.3 使用场景
若n较小(如n≤50),可采用直接插入或直接选择排序。当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
若文件初始状态基本有序(指正序),则应选用直接插入、冒泡或随机的快速排序为宜;
若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序需要的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
若要求排序稳定,则可选用归并排序。通常可以将它和直接插入排序结合在一起使用:先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定 的,所以改进后的归并排序仍是稳定的。
2 七大排序算法
2.1 选择排序
选择排序,不断的选择剩余元素中最小的,和相应的位置交换。
2.1.1 原理图解
2.1.2 动图分析
2.1.3 代码分析
public void selectSort(int[] arr){
// 从第一个元素开始交换
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;
}
}
// 找完了后面所有的元素,已经确定了最小的元素极其位置,下一步进行交换
if(minIndex!=i){
// 注意这里的min值已经替换成最小的了,相当于临时变量
arr[minIndex]=arr[i];
arr[i] = min;
}
}
}
2.2 插入排序
分析过程见代码
2.2.1 原理分析
2.2.2 动图分析
2.2.3 代码
public void insertSort2(int[] arr){
for (int i = 0; i < arr.length; i++) {
// 从这个值开始,比较前面所有的元素,找合适的位置坐下
for (int j = i; j >0 && arr[j]<arr[j-1]; j--) {
// 每次替换都要定义一个temp赋值需要插入的数,这样会造成不必要的浪费,方法一是改进版
// 把插入位置之后的每一位元素都往后移一位就相当于交换当前的两个位置
// 因为前面是有序的,所以不可能出现前面的一个不需要换,而前面的前面需要交换的情况
// 所以这样交换区别于选择排序,而是所有元素后移一位的操作。
int temp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
}
}
}
//下面是优化思路(首选)
public void insertSort(int[] arr){
for (int i = 0; i < arr.length; i++) {
// 把数组分成两部分,前面有序表+后面无序表
// 拿在手里的牌是当前的遍历值,要插入的初始位置就是它前面那个
int insertVal = arr[i];
int j ;
for (j=i; j >0 && insertVal<arr[j-1] ; j--) {
arr[j] = arr[j-1];//一直移一直移
}
arr[j] = insertVal;//最后才插入
}
}
2.3 希尔排序
2.3.1 原理分析
2.3.2 动图分析
2.3.3 代码
/ while和for循环可以相互转化使用,但是觉得下面这种方法会更好理解一些
public void shellSort(int[] arr){
// 循环缩小步伐
for (int step = arr.length/2; step >=1; step/=2) {
// 每个步伐走一遍全体成员,第一个元素是下标step,比如步子为1时就是从2开始排序
// (理解上)分成了很多组,但是并不是一个组一个组来拍,而是顺序往后,会经过所有组,都来一遍
for (int i = step; i < arr.length; i++) {
// 里面就是对每一组的成员进行插入排序了
int j;//如果从step开始的话会数组越界
int temp = arr[i];
// 这里的临界值需要注意,从当前的i 前面一位开始比较j-step
// 如果j不设置j>=step的话,-=step之后就会在后面arr[j]中出现数组越界的情况
for (j=i; j >= step && temp<arr[j-step] ; j-=step) {
// j-step 的值赋给 j 就是后移呀,思考一下
arr[j] = arr[j-step];//对应后移一位,这里后移 step
}
// 移完所有的元素,最后就把自己插进入合适的位置
arr[j] = temp;
}
}
}
// 一样的只是边界换了一种表示而已
public void shell(int[] arr){
for (int step = arr.length/2; step >0 ; step/=2) {
for (int i = step; i <arr.length ; i++) {
int temp = arr[i];
int j=i-step;
for (; j >0 && temp<arr[j] ; j-=step) {
arr[j+step] = arr[j];
}
arr[j+step] = temp;
}
}
}
2.4 归并排序
2.4.1 原理分析
2.4.2 动图分析
2.4.3 代码分析
public void mergeSort(int[] arr,int left,int right){
if(left<right){
int mid = left+(right-left)/2;
mergeSort(arr,left,mid);
mergeSort(arr,mid+1,right);
merge(arr,left,mid+1,right);
}
}
public void merge(int[] arr,int left,int mid,int right){
// 首先把拆分的左数组、右数组构造出来
int[] leftArr = new int[mid-left];
for (int i = left; i < mid; i++) {
leftArr[i-left] = arr[i];
}
int[] rightArr = new int[right-mid+1];
for (int i = mid; i <= right; i++) {
rightArr[i-mid] = arr[i];
}
// 双指针,比较两个数组对应位置的大小,填充
int i=0,j=0;
// 原地更新,节省内存空间
// 考虑java的数组是不可变的,这样复制的操作会导致低效,可以考虑使用ArrayList,
// 使用Arrays.copyOfRange;
int k = left;
while(i<leftArr.length && j<rightArr.length){
if(leftArr[i]<rightArr[j]){
arr[k] = leftArr[i];
i++;
}else{
arr[k] = rightArr[j];
j++;
}
k++;
}
// 一个数组的指针已经到头了,剩下的直接加上去就好了
while(i<leftArr.length){
arr[k] = leftArr[i];
i++;
k++;
}
while(j<rightArr.length){
arr[k]=rightArr[j];
j++;
k++;
}
}
2.5 冒泡排序
2.5.1 原理分析
2.5.2 动图分析
2.5.3 代码分析
public void bubbleSort(int[] arr){
//对整个数组从头到尾,公平起见,每个元素都要来一遍,外循环
for (int i = 0; i < arr.length - 1; i++) {
// 每一个元素的遍历交换操作,从头到尾,依次,大就交换、大就交换
for (int j = 0; j < arr.length - 1 - i; j++) {
if(arr[j]>arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
// 优化,如果已经有序了,就不必再遍历到最后一个元素了,有序的数组,同样会执行n(n-1)/2
public void bubble(int[] arr){
boolean flag = false;
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length-i-1; j++) {
if(arr[j]>arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
flag = true;
}
}
if(!flag){
break;
}else{
flag=false;
}
}
}
2.6 快速排序
2.6.1 原理分析
这篇文章讲的非常清楚:快排原理
2.6.2 动图分析
2.6.3 代码分析
public void quickSort(int[] arr,int left,int right){
if(left>=right){//对于小数组,插入排序算法性能更优,此时可以修改为当 left+M>=right 时调用其他插入算法
return;
}else{
int base = arr[left];
int i = left;
int j = right;
while(i<j){//循环将所有的数都交换完毕
while(i<j && arr[j]>=base){//从后往前找比基准值小的数
j--;
}
while(i<j && arr[i]<=base){//从前往后找比基准值大的数
i++;
}
swap(arr,i,j);//将这两个数交换
}
swap(arr,left,j);//把基准位置换到中间去,实现 小 + 基准 + 大
//递归
quickSort(arr,left,j-1);
quickSort(arr,j+1,right);
}
}
public void swap(int[] arr,int a,int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
2.7 线性排序
待完善
参考
写文章是,参考了很多文章,没有一 一列出
java代码是个人理解,觉得比较直观的,不一定是写法最好的。
感谢:
- 《算法4》
- 尚硅谷《数据结构与算法》
- anAngryAnt/LearningNotes
- 排序算法 - 面试中的排序算法总结
- 五分钟学算法
以上是关于面试相关-七大排序算法:图解+动图+最直观的代码分析_性能比较的主要内容,如果未能解决你的问题,请参考以下文章
七大排序算法详解,动图展示 +代码实现,老奶奶看了都直呼内行