一. 排序算法简介
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析。
常见的排序算法有:
(1)冒泡排序;
(2)选择排序;
(3)插入排序;
(4)希尔排序;
(5)归并排序;
(6)快速排序;
(7)基数排序;
(8)堆排序;
(9)计数排序;
(10)桶排序。
二. 时间复杂度和空间复杂度
稳定性是一个特别重要的评估标准。稳定的算法在排序的过程中不会改变元素彼此的位置的相对次序,反之不稳定的排序算法经常会改变这个次序,这是我们不愿意看到的。我们在使用排序算法或者选择排序算法时,更希望这个次序不会改变,更加稳定,所以排序算法的稳定性,是一个特别重要的参数衡量指标依据。就如同空间复杂度和时间复杂度一样,有时候甚至比时间复杂度、空间复杂度更重要一些。所以往往评价一个排序算法的好坏往往可以从下边几个方面入手:
(1)时间复杂度:即从序列的初始状态到经过排序算法的变换移位等操作变到最终排序好的结果状态的过程所花费的时间度量。
(2)空间复杂度:就是从序列的初始状态经过排序移位变换的过程一直到最终的状态所花费的空间开销。
(3)使用场景:排序算法有很多,不同种类的排序算法适合不同种类的情景,可能有时候需要节省空间对时间要求没那么多,反之,有时候则是希望多考虑一些时间,对空间要求没那么高,总之一般都会必须从某一方面做出抉择。
(4)稳定性:稳定性是不管考虑时间和空间必须要考虑的问题,往往也是非常重要的影响选择的因素。
算法 | 时间复杂度 | 空间复杂度 | 是否稳定 | 最好时间 | 最坏时间 |
冒泡排序 | 是 | ||||
选择排序 | 否 | ||||
插入排序 | 是 | ||||
快速排序 | 否 | ||||
归并排序 | 是 | ||||
希尔排序 | 否 | ||||
基数排序 | 是 | ||||
堆排序 | 否 | ||||
计数排序 | 是 | ||||
桶排序 | 是 |
三. 具体解析及其Java实现
<零> 交换swap()函数
在排序时,经常遇到数组两个变量要交换的情况,那么经常使用的交换的方法有三种:
中间变量交换
public void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
加减交换
public void swap(int[] arr, int i, int j) {
if(i == j) return; // 保证调用时满足 i != j,则需有此句,否则i == j时此数将变为0
arr[i] = arr[i] + arr[j]; // a = a + b
arr[j] = arr[i] - arr[j]; // b = a - b
arr[i] = arr[i] - arr[j]; // a = a - b
}
异或交换
public void swap(int[] arr, int i, int j) {
if(i == j) return; // 保证被调用时满足 i != j,则需有此句,否则i == j时此数将变为0
arr[i] = arr[i] ^ arr[j]; // a = a ^ b
arr[j] = arr[i] ^ arr[j]; // b = (a ^ b) ^ b = a ^ (b ^ b) = a ^ 0 = a
arr[i] = arr[i] ^ arr[j]; // a = (a ^ b) ^ a = (a ^ a) ^ b = 0 ^ b = b
}
<一> 冒泡排序,选择排序,插入排序
冒泡排序提前结束:
public int[] BubbleSort(int[] arr){
if(arr == null || arr.length < 2) return arr;
for(int i = 0; i < arr.length-1; i++){
boolean flag = true;
for(int j = 1; j < arr.length-i; j++){
if(arr[j-1] > arr[j]){
swap(arr,j-1,j);
flag = false;
}
}
if(flag) break;
}
return arr;
}
public void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
冒泡排序是相邻交换,每一轮只确定一个数字的位置,flag用来提前结束循环。
选择排序:
选择排序,选择的是最值,那么以选择最小值为例,在每一轮当中,选择当前轮的最小值,然后与目标值(每一轮的最左边位置)交换
public int[] SelectSort(int[] arr){
if(arr == null || arr.length < 2) return arr;
for(int i = 0; i < arr.length-1; i++){
int minindex = i;
for(int j = i+1; j < arr.length; j++){
if(arr[minindex] > arr[j]){
minindex = j;
}
}
swap(arr,i,minindex);
}
return arr;
}
public void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
例:7 7 2,第一轮交换第一个7和2,则两个7位置关系改变,所以是不稳定排序。
插入排序:
public int[] InsertSort(int[] arr){
if(arr == null || arr.length < 2) return arr;
for(int i = 1; i < arr.length; i++){
for(int j = i-1; j >=0 && arr[j] > arr[j+1]; j--){
swap(arr,j,j+1);
}
}
return arr;
}
public void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
插入排序,每次在一段范围内保持有序,开始是0-0,然后是0-1,然后是0-2,插入的意思是将目标插入到已经排好序的范围的适当位置,对于将要处理的目标数,其前i-1位置都是有序的。
<二> 快速排序
快速排序的总体思想:取基准数,小于放左边,大于放右边,那么我们的中间的基准数是排好了,然后根据分治的思想,再排左右两边。
快排初始版本:分成两部分,大于基准数和小于等于基准数,取首或取尾作为基准数
快排改进版本:分成三部分,大于基准数,小于基准数,等于基准数,取首或者尾为基准数
快排再改进:分成三部分,大于基准数,小于基准数,等于基准数,取随机数为基准数
public void QuickSort(int[] arr, int left, int right){
if(left < right){
int pivot = left + (int)Math.random()*(right-left+1);
swap(arr,pivot,right);//把最后一个数和随机数交换
int[] p = partition(arr,left,right);
QuickSort(arr, left, p[0]-1);
QuickSort(arr, p[1]+1, right);
}
}
public int[] partition(int[] arr, int left, int right) {
int pivot = right;
int i = left;
left--;
while(i < right){
if(arr[i] < arr[pivot]){
left++;
swap(arr,i,left);
i++;
}
else if(arr[i] > arr[pivot]){
right--;
swap(arr,right,i);
}
else{
i++;
}
}
swap(arr,right,pivot);
return new int[]{left+1,right};
}
public void swap(int[] arr, int i , int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
以{4,3,2,3,5,1}为例:
要点注意:(1) pivot = left + (int)Math.random()*(right-left+1)注意基数
(2)left--刚开始的位置为虚位,
(3)在大于小于的情况中都是先++或者先--
(4)arr[i] > arr[pivot]的情况,i不需要++
(5)最后返回left+1,right, p[0]-1 ,p[1]+1
扩展1:荷兰国旗问题
扩展2:快排初始版本*和快排改进版本*
<三> 归并排序
归并排序最核心的思想在于分治,即数组的两个部分是有序的,接下来变成了合并两个有序数组的问题,那么根据外排序的方法即可获得最后的数组。
那么对于一个数组arr={8,4,5,7,1,3,6,2}来说,它的归并排序过程是这样的:
public void MergeSort(int[] arr, int left, int right){
if(left == right) return;
int middle = left + ((right-left)>>1);
MergeSort(arr,left,middle);
MergeSort(arr,middle+1,right);
merge(arr,left,right,middle);
}
public void merge(int[] arr, int left, int right, int middle) {
int indexleft = left;
int indexright = middle+1;
int[] temp = new int[right-left+1];
int i = 0;
while(indexleft <= middle && indexright <= right){
temp[i++] = arr[indexleft] <= arr[indexright] ? arr[indexleft++] : arr[indexright++];
}
while(indexleft <= middle){
temp[i++] = arr[indexleft++];
}
while(indexright <= right){
temp[i++] = arr[indexright++];
}
for(int j = 0; j < temp.length; j++){
arr[j+left] = temp[j];
}
}
要点注意:(1) middle = left + ((right-left)>>1)防溢出
(2)MergeSort一定将左右两边分开成互补的两部分left,middle和middle+1,right
(3)arr[indexleft] <= arr[indexright]保证稳定排序
(4)temp的长度是right-left+1
挖坑后填
扩展1:自底向上非原地归并排序,自底向上原地归并排序,自顶向下原地归并排序
扩展2:小和问题,逆序对
<四> 堆排序
待更新
<五> 希尔排序
待更新
<六>计数排序
待更新
<七>桶排序
待更新
<八>基数排序
待更新
参考来源:
【1】leetcode yukiyama 十大排序从入门到入赘
【2】b站 左程云
【3】百度百科 排序算法
【4】菜鸟教程 十大经典排序算法