文章大纲:
1.总体排序算法对比图
2.9种排序算法介绍
冒泡排序
算法描述
冒泡排序是一个平均时间复杂度为O(n^2)的排序算法,它的算法思想是假设有一个数组长度为N,然后在[0...N-1]的范围内,假设i=0。先把第i个数和它后面的数i+1比较,如果大则交换,否则不变。然后继续向后遍历,直到将最大的数交换至数组末尾,然后下一次在[0..N-2]范围内从i=0开始。直到范围缩小到[0..1]结束
代码实现
import java.util.*;
public class BubbleSort {
public int[] bubbleSort(int[] A, int n) {
int temp;
for(int i=1;i<n;i++){
for (int j=0;j<n-i;j++){//0到j的范围 j又跟随i每次减小
if (A[j]>A[j+1]){//前后两个数进行比较
temp=A[j];
A[j]=A[j+1];
A[j+1]=temp;
}
}
}
return A;
}
}
复杂度
1.平均时间复杂度:O(N^2)
2.最坏时间复杂度:O(N^2) 出现情况:任何情况下
3.最好时间复杂度:O(N^2) 出现情况:任何情况下
4.空间复杂度:O(1)
稳定性
是稳定的排序算法
使用场景
当文件或者数组基本有序的情况下使用冒泡会好(当冒泡算法改进时有效,否则还是机械的每一次都会去比较)
算法改进
设置一个标志变量,当数组发生交换操作时更新交换的下标位置。然后下一次排序就将范围缩小到[0...标志变量的位置]
/**
* 设置一标志性变量pos,用于记录每趟排序中最后一次进行交换的位置。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可
* @param A
* @param n
* @return
*/
public int [] bubbleSort_1( int A[], int n) {
int i= n -1; //初始时,最后位置保持不变
while ( i> 0) {
int pos= 0; //每趟开始时,无记录交换
for (int j= 0; j< i; j++){
if (A[j]> A[j+1]) {
pos= j; //记录交换的位置
int tmp = A[j]; A[j]=A[j+1];A[j+1]=tmp;
}
}
i= pos; //为下一趟排序作准备
}
return A;
}
插入排序
算法描述
插入排序是一个平均时间复杂度为O(N^2)的排序算法,它的算法思想是假设有一个数组长度为N,然后在[0..N-1]的范围内,从i=1开始,和i-1的数进行比较,如果小则交换,并继续向前一个数比较,直到大则停止,i++。重复遍历直到i=N-1。
算法实现
import java.util.*;
public class InsertionSort {
public int[] insertionSort(int[] A, int n) {
for (int i=1;i<n;i++){
for (int j=i;j>0;j--){
if (A[j]<A[j-1]){
int temp=A[j];
A[j]=A[j-1];
A[j-1]=temp;
}
}
}
return A;
}
}
复杂度
1.平均时间复杂度:O(N^2)
2.最坏时间复杂度:O(N^2) 出现情况:整个数组为倒序的情况
3.最好时间复杂度:O(NK) 出现情况:当每个元素只需要移动不超过K个位置即可排好序
4.空间复杂度:O(1)
稳定性
是稳定的排序算法
使用场景
文件或者数组基本有序的情况下使用较好,或者在出现最好的时间复杂度的情况下,也就是元素移动的距离很小即可排好序时
算法改进
要想当算法遇到最好情况时能够达到O(NK)的时间复杂度,那么算法就需要不能机械的每一次都去比较,当不用交换时就结束遍历
public int[] insertionSort_1(int[] A,int n){
int count=0;
for (int i=1;i<n;i++){
int j=i;
while (j>0&&A[j]<A[j-1]){
count++;
int temp=A[j];
A[j]=A[j-1];
A[j-1]=temp;
j--;
}
}
System.out.println("times "+count);
return A;
}
选择排序
算法描述
选择排序是一个平均时间复杂度为O(N^2)的排序算法,它的算法思想是假设有一个长度为N的数组,在[0...N-1]的范围上,从i=0开始,遍历整个数组,记录最小值下标。一趟结束后和第一个位置交换,然后从i=1开始直到i=N-1结束
算法实现
public int[] selectionSort_1(int[] A, int n) {
int smallest_index;
int temp;
for (int i=0;i<n;i++){
smallest_index=i;
for (int j=i;j<n;j++){
if (A[j]<A[smallest_index]){
smallest_index=j;
}
}
if (smallest_index!=i){
temp=A[i];
A[i]=A[smallest_index];
A[smallest_index]=temp;
}
}
return A;
}
复杂度
1.平均时间复杂度:O(N^2)
2.最坏时间复杂度:O(N^2) 出现情况:任何情况下
3.最好时间复杂度:O(N^2) 出现情况:任何情况下
4.空间复杂度:O(1)
稳定性
是不稳定的排序算法
例子:[3,3,3,2] 第一次交换改变了3个3的位置顺序
使用场景
在记录规模比较小或者数组基本有序的情况下(交换次数少)使用选择排序较好
算法改进
二元选择排序,在一趟排序中从原本只记录一个最小值,改为同时记录最大值和最小值,使得排序的次数减少为N/2。
public int [] SelectSort(int A[],int n) {
int i ,j , min ,max, tmp;
for (i=1 ;i <= n/2;i++) {
// 做不超过n/2趟选择排序
min = i; max = i ; //分别记录最大和最小关键字记录位置
for (j= i+1; j<= n-i; j++) {
if (A[j] > A[max]) {
max = j ;
continue ;
}
if (A[j]< A[min]) {
min = j ;
}
}
if (min!=i-1){
tmp = A[i-1]; A[i-1] = A[min]; A[min] = tmp;
}
if (max!=n-i){
tmp = A[n-i]; A[n-i] = A[max]; A[max] = tmp;
}
}
return A;
}
归并排序
算法描述
归并排序是一个平均时间复杂度为O(N*logN)的排序算法,它的算法思想有两种一种是递归的思想,一种是迭代的思想。
递归思想:
假设有一个长度为N的数组,在[0...N-1]的范围内对数组进行递归拆分。每次从当前数组的中间位置拆分成2部分,然后递归进行。第一次拆分后,分为[0...N/2] 和[N/2+1....N-1]两部分,每部分包含N/2个元素。直到数组上界<数组下界停止递归。然后将这两个不能拆分的部分进行排序。
①开启辅助空间大小为两个元素个数之和
②然后设置两个指针指向两个部分的起点
③选择相对小的元素放入到辅助空间,并移动指针到下一位置
④重复步骤③直到某一指针到达序列尾
⑤将另一序列剩下的所有元素直接复制到合并序列尾
算法实现(递归思想)
/**
* 递归分解数组 归并排序
* @param A
* @param n
* @return
*/
public int[] mergeSort1(int[] A, int n) {
mergePass1(A,0,n-1);
return A;
}
public void mergePass1(int [] A,int left,int right){
if (right>left){
int middle=(left+right)/2;
mergePass1(A,left,middle);
mergePass1(A,middle+1,right);
merge1(A,left,right,middle);
}
}
public void merge1(int [] A,int low,int high,int mid){
int k=0;
int [] temp=new int[high-low+1];
int i=low,j=mid+1;
while (i<=mid&&j<=high){
if (A[i]>A[j]){
temp[k]=A[j];
j++;
k++;
}else {
temp[k]=A[i];
i++;
k++;
}
}
while (i<=mid){
temp[k]=A[i];
i++;
k++;
}
while (j<=high){
temp[k]=A[j];
j++;
k++;
}
for (k=0,i=low;i<=high;i++,k++){
A[i]=temp[k];
}
}
迭代思想:
假设有一个长度为N的数组,间隔gap为2^i次方(i=1,2,3...).然后以gap为中间位置,形成前后两个部分,前部分为[j...j+gap-1] 后部分为[j+gap...j+gap*2-1] 遍历j将整个数组进行拆分。
①开启辅助空间大小为两个元素个数之和
②然后设置两个指针指向两个部分的起点
③选择相对小的元素放入到辅助空间,并移动指针到下一位置
④重复步骤③直到某一指针到达序列尾
⑤将另一序列剩下的所有元素直接复制到合并序列尾
调整gap变大,增加间隔为原来的2倍。这样就将前后两部分的序列空间增加了。再次进行排序。
算法实现
/**
* 循环分解数组 归并排序
* @param A
* @param n
* @return
*/
public int[] mergeSort(int[] A, int n) {
for (int i=1;i<n;i=i*2){
mergePass(A,i);
}
return A;
}
public void mergePass(int [] A,int gap){
int i=0;
for (;i+gap*2-1<A.length;i=gap*2+i){
merge(A,i,i+gap*2-1,i+gap);
}
if (i+gap-1<A.length){
merge(A,i,A.length-1,i+gap);
}
}
public void merge(int [] A,int low,int high,int mid){
int k=0;
int [] temp=new int[high-low+1];
int i=low,j=mid;
while (i<mid&&j<=high){
if (A[i]>A[j]){
temp[k]=A[j];
j++;
k++;
}else {
temp[k]=A[i];
i++;
k++;
}
}
while (i<mid){
temp[k]=A[i];
i++;
k++;
}
while (j<=high){
temp[k]=A[j];
j++;
k++;
}
for (k=0,i=low;i<=high;i++,k++){
A[i]=temp[k];
}
}
复杂度
1.平均时间复杂度:O(NlogN)
2.最坏时间复杂度:O(NlogN) 出现情况:任何情况下
3.最好时间复杂度:O(NlogN) 出现情况:任何情况下
4.空间复杂度:O(N)
稳定性
是一种稳定的排序算法
使用场景
数量较大的数组排序,或者要求稳定的一些排序场景
算法改进
把递归的归并排序进行了改进。
1.当序列的长度小于一定值时,改用插入排序。
假设整个序列长度为n,当子序列长度为k时,采取插入排序策略,这样一共有n/k个子序列。子序列完成排序复杂度:最坏情况下,n/k个子序列完成排序的时间复杂度为O(nk)。证明:每个子序列完成插入排序复杂度为O(k^2),一共n/k个子序列,故为O(nk)。
子序列完成合并复杂度:最坏情况下,合并两个子序列的时间复杂度为O(n),一共有n/k个子序列,两两合并,共需要lg(n/k)步的合并,所以这些子序列完成合并的复杂度为O(nlg(n/k))。
改进后的插入归并排序的最坏情况的复杂度为O(nk+nlg(n/k)),这里k的最大取值不能超过lgn,显然如果k大于lgn,复杂度就不是与归并一个级别了,也就是说假设一个1000长度的数组,采用插入策略排序子序列时,子序列的最大长度不能超过10。
2.比较两个待合并的子序列,如果后者的最小值大于前者最大值就可以直接返回了,已经是有序的。
public void mergePass1(int [] A,int left,int right){
if (right>left){
if (right-left<=10){//当数组长度小于10时 就改用插入排序 因为插入排序在n比较小的情况下运行更快一些
insertionSort_for_merge(A,left,right);
return;
}
int middle=(left+right)/2;
mergePass1(A,left,middle);
mergePass1(A,middle+1,right);
if (A[middle]<=A[middle+1]){//当两个子序列已经有序的情况下就不用进行合并了,直接返回。
return;
}
merge1(A,left,right,middle);
}
}
快速排序
算法描述
快速排序是一个平均时间复杂度为O(NlogN)的排序算法,它的算法思想是,在一个长度为N的数组上,首先在[0...N-1]的范围内随机选取一个位置作为基准值,然后把这个位置的数和最后一个数交换。之后设置一个记录下标j,j表示[0...j]的范围内的数都比基准值小。然后遍历整个数组和最后一位的基准值作比较。发现小了,则交换当前位置的数和j位置的数。然后整个数组变成以基准值为中间值的两个部分,之后对生成的两个部分递归调用上述方法即可。
算法实现
class QuickSort {
public int[] quickSort(int[] A, int n) {
quickSort_1(A,0,n-1);
return A;
}
public int Partition(int []A,int low,int high){
if (A==null||low<0||high>=A.length){
return -1;
}
int random_index=(low+high)/2;//随机生成一个候选值
int temp;
//把候选值和最后一个数进行交换
temp=A[random_index];
A[random_index]=A[high];
A[high]=temp;
//初始化一个记录小于等于候选值的下标: 当遍历数组时 如果当前元素小于候选值 则和j位置元素替换
//j的初始值为数组的起点 low 当遍历完数组后 把候选值和当前j位置的元素交换
int j=low;
for (int i=low;i<high;i++){
if (A[i]<A[high]){
temp=A[j];
A[j]=A[i];
A[i]=temp;
j++;
}
if (i==high-1){
temp=A[j];
A[j]=A[high];
A[high]=temp;
}
}
//此时的数组小于候选值的数在前面,大于的则在后面
return j;//返回候选值的下标位置
}
public void quickSort_1(int []A,int low,int high){
if (low==high){//如果相等则递归结束
return;
}
int partition = Partition(A, low, high);//以候选值为分界点递归调用函数进行快速排序
if (partition>low){//候选值的前半部分递归
quickSort_1(A,low,partition-1);
}
if (partition<high){//候选值的后半部分递归
quickSort_1(A,partition+1,high);
}
}
public void print(int [] A,int n){
for (int i=0;i<n;i++){
System.out.print(A[i]);
}
}
}
复杂度
1.平均时间复杂度:O(NlogN)
2.最坏时间复杂度:O(N^2) 出现情况:在数组为正序或者逆序的情况下,每次划分成2个部分,只得到一个部分。退化成冒泡排序
3.最好时间复杂度:O(NlogN) 出现情况:任何情况下
4.平均空间复杂度:O(logN)
5.最坏空间复杂度:O(N)
稳定性
快速排序是一个不稳定的排序算法
使用场景
记录规模较大并且记录分布比较随机时快速排序会较快,一般基于内部的比较排序快速排序都是效率很高的。
算法改进
1.优化选取标准值的方法,使用三数取中法
2.优化子序列的排序算法,当子序列长度小于一定值时使用插入排序效率更高
3.过滤掉相同元素,把和标准值相同的元素去除掉,不参与下一次的递归,减少递归子序列的长度,提高效率
public int[] quickSort(int[] A, int n) {
quickSort_2(A,0,n-1);
return A;
}
/**
* 优化的快速排序 单次排序过程。 把重复的元素都集中在数组的中间部分,然后下次递归时的量子序列将不再包含它们
* 这样可以更好的减少子序列进行递归的长度
* @param A
* @param low
* @param high
*/
public void quickSort_2(int []A,int low,int high){
if (low==high){//如果相等则递归结束
return;
}
if (high-low+1<2){//当子序列的长度小于一定值时(这里假设为10)使用插入排序会效率更高 优化2
insertionSort_for_quick(A,low,high);
return;
}
int []result = Partition2(A, low, high);//以候选值为分界点递归调用函数进行快速排序
int s=result[1];
int partition=result[0];
if (partition>low){//候选值的前半部分递归
quickSort_2(A,low,partition-1);
}
if (partition<high){//候选值的后半部分递归
quickSort_2(A,partition+s+1,high);//跳过相同元素,减少递归的子序列长度
}
}
/**
* 在遍历的过程中设置了一个s下标用于记录和标准值相同的元素 并让这些元素和j位置的元素进行交换。
* @param A
* @param low
* @param high
* @return
*/
public int[] Partition2(int []A,int low,int high){
int [] result=new int[2];
if (A==null||low<0||high>=A.length){
result[0]=-1;
return result;
}
int random_index=SelectMedainOfThree(A,low,high);//三数取中法 优化1
int temp;
//把候选值和最后一个数进行交换
temp=A[random_index];
A[random_index]=A[high];
A[high]=temp;
//初始化一个记录小于等于候选值的下标: 当遍历数组时 如果当前元素小于候选值 则和j位置元素替换
//j的初始值为数组的起点 low 当遍历完数组后 把候选值和当前j位置的元素交换
int j=low;int s=low;//s代表出现重复元素进行交换的下标位置
int count_same=0;//代表和标准值重复的元素个数
for (int i=low;i<high;i++){
if (A[i]<A[high]){
temp=A[j];
A[j]=A[i];
A[i]=temp;
j++;
s=j;//每次交换一个小的元素当j位置,都会重置s 为了在比标准值小的元素位置后开始进行相等元素的交换
continue;
}
if (A[i]==A[high]){//当元素相等时,i和s位置进行交换
temp=A[s];
A[s]=A[i];
A[i]=temp;
s++;
count_same++;
}
}
//把最后的标准值还原到s位置
temp=A[s];
A[s]=A[high];
A[high]=temp;
//此时的数组小于候选值的数在前面,大于的则在后面
result[0]=j;//返回候选值的下标位置
result[1]=count_same;//返回相同元素的个数
return result;
}
public int SelectMedainOfThree(int [] A,int low,int high){
int middle=(low+high)/2;
return A[middle]>=A[low]?(A[middle]>=A[high]?high:middle):(A[low]>=A[high]?high:low);
}
private void insertionSort_for_quick(int[] A,int low,int high){
for (int i=low+1;i<=high;i++){
int j=i;
while (j>low&&A[j]<A[j-1]){
int temp=A[j];
A[j]=A[j-1];
A[j-1]=temp;
j--;
}
}
}
堆排序
算法描述
堆排序是一个平均时间复杂度为O(NlogN)的排序算法,它的思想是以二叉树为基本的数据结构。将数组元素放入一个二叉树中进行调整。这个二叉树满足的条件是任一父节点元素都大于任一子节点元素(大顶堆),或者反过来(小顶堆)。然后建立好初始堆后。每次将堆顶元素和和最后的元素交换,之后堆的元素个数减一遍历进行。直到堆元素只剩一个。
算法实现
class HeadSort{
public void HeadAdjust(int [] A,int parent,int length){
int temp=A[parent];//保存当前父节点
int child=parent*2+1;//先取左孩子
while (child<length){
if (child+1<length&&A[child]<A[child+1]){//如果右孩子存在并且大于左孩子 则选取右孩子
child++;
}
if (temp>=A[child]){//如果保存的父节点比孩子大 则循环结束 说明 父节点最大
break;
}
A[parent]=A[child];//让父节点等于孩子节点
parent=child;
child=child*2+1;//选取孩子节点的左节点继续向下筛选
}
A[parent]=temp;//把最后的孩子节点赋值为最初的父节点 相当于节点交换
}
/**
* 可以看到建立初始堆和在进行真正堆排序时的循环条件是不一样的,
* 建立初始堆时 需要把每一个父子节点都进行堆的调整 得到一个堆 从整个数组的一半开始,保证了能遍历到每一个父子节点
* 当建立好初始堆之后,就能利用一次堆调整来调整好堆。而无序数组第一次建立初始堆时则不行。这是因为堆的特点是任意
* 父子节点都大于等于(如果为小根堆小于等于)它的两个孩子节点 所以在调整堆函数时只需要比较父子节点和两个孩子节点的大小关系
* 就能把三者的大小顺序调整正确,然后再往子节点向下进行比较 即可 而无序数组不具备堆的特点需要多次循环把每一个父子节点都调整正确后才行
* @param A
* @return
*/
public int [] headSort(int []A){
for (int i=A.length/2;i>=0;i--){//先循环建立初始堆
HeadAdjust(A,i,A.length-1);
}
for (int i=A.length-1;i>0;i--){//循环n-1次完成堆排序 每次把堆顶元素和最后一个元素替换,然后将剩下的元素进行堆排序
int temp=A[i];
A[i]=A[0];
A[0]=temp;
HeadAdjust(A,0,i);
}
return A;
}
public void print(int [] A,int n){
for (int i=0;i<n;i++){
System.out.print(A[i]);
}
}
}
复杂度
1.平均时间复杂度:O(NlogN)
2.最坏时间复杂度:O(NlogN) 出现情况:任何情况
3.最好时间复杂度:O(NlogN) 出现情况:
4.平均空间复杂度:O(1)
使用场景
记录比较大或者对于辅助空间要求比较小的时候使用堆排序会比较好。比如一些题目说一个基本有序的序列,每个元素移到到排序好的位置不超过k,这类问题用堆排序的时间复杂度会低。还有大数据量,找几百万个数中最小的100个数也可以使用堆排序。判断数组中是否有重复值,要求空间复杂度为O(1)等。
计数排序
算法描述
计算排序是一个平均时间复杂度为O(N)的排序算法,它的算法思想不是基于比较的算法思想,假设给定一个长度为N的数组,然后根据数组中的最大值和最小值之差建立相应的桶的个数。之后依次把数组中的元素放入桶中。然后再按照桶的顺序依次倒出完成排序
算法实现
/**
* 计数排序
*/
class CountSort{
public int [] CountSort(int [] A,int n){
int min=A[0];
int max=A[0];
for (int i=0;i<A.length;i++){
min=Math.min(min,A[i]);
max=Math.max(max,A[i]);
}
int []temp=new int[max-min+1];
for (int i=0;i<A.length;i++){
temp[A[i]-min]++;
}
int k=0;
for (int i=0;i<temp.length;i++){
while (temp[i]!=0){
A[k]=i+min;
k++;
temp[i]--;
}
}
return A;
}
public void print(int [] A,int n){
for (int i=0;i<n;i++){
System.out.print(A[i]);
}
}
}
复杂度
1.平均时间复杂度:O(N)
2.最坏时间复杂度:O(N) 出现情况:任何情况
3.最好时间复杂度:O(N) 出现情况:任何情况
4.平均空间复杂度:O(M)
稳定性
计数排序是一个稳定的排序算法
使用场景
对一些记录的范围比较集中的情况下使用计数排序会好一点
基数排序
算法描述
基数排序是一个评价时间复杂度为O(KN)的排序算法,它的算法思想是假设一个长度为N的数组,创建一个关键字为0-9的二维数组。然后每一次比较数组中每一个元素的个位,放入二维数组中。然后依次倒出。再比较十位。一直重复即可
算法实现
/**
* 基数排序
*/
class RadixSort {
public int[] radixSort(int[] A, int n) {
radix_sort(A,n,findMaxNumber_digits(A));
return A;
}
/**
* 找到这个数组中最大的数有多少位
* 例如:数组中最大的为123,则返回3
* @param A 数组
* @return 位数
*/
public int findMaxNumber_digits(int [] A){
int max=A[0];
for(int i=0;i<A.length;i++){
max=Math.max(A[i],max);
}
return String.valueOf(max).length();
}
/**
* 获取数字的第n位数
* @param num 待获取的数字
* @param n 第几位数
* @return 位数
*/
public int getNumberBydigit(int num,int n){
int result=0;
while (n>0){
result=num%10;
num=num/10;
n--;
}
return result;
}
public int [] radix_sort(int [] A,int n,int radix){
int [][] temp=new int[10][A.length];
int [] count=new int[10];
for (int i=1;i<=radix;i++){
for (int j=0;j<A.length;j++){
int numberBydigit = getNumberBydigit(A[j], i);
temp[numberBydigit][count[numberBydigit]++]=A[j];
}
int k=0,m=0;
for (int j=0;j<count.length;j++){
while (count[j]!=0){
A[k++]=temp[j][m];
count[j]--;
m++;
}
m=0;
}
}
return A;
}
public void print(int [] A,int n){
for (int i=0;i<n;i++){
System.out.print(A[i]);
}
}
}
复杂度
1.平均时间复杂度:O(KN)
2.最坏时间复杂度:O(KN) 出现情况:任何情况
3.最好时间复杂度:O(KN) 出现情况:任何情况
4.平均空间复杂度:O(N)
稳定性
基数排序是一个稳定的排序算法
使用场景
适用于多关键字的排序
希尔排序
算法描述
希尔排序是一个平均时间复杂度为O(Nlog2N)的排序算法,它的算法思想是,假设给定一个长度为N的数组。然后设置步长gap值(假设步长gap每次为数组长度的一半),然后比较间隔gap的相邻两个元素的值,如果后者小,前者大则交换,也就是执行插入排序。遍历缩小gap,直到gap=1。
算法实现
class ShellSort{
public int [] shellSort(int [] A,int n){
int gap=n/2;//分隔的间隔长度 每次都取一半
while (gap>=1){
for (int i=gap;i<A.length;i++){
int temp=A[i];
int j;
for (j=i-gap;j>=0&&temp<A[j];j=j-gap){
A[j+gap]=A[j];
}
A[j+gap]=temp;
}
gap=gap/2;
}
return A;
}
public void print(int [] A,int n){
for (int i=0;i<n;i++){
System.out.print(A[i]);
}
}
}
复杂度
1.平均时间复杂度:O(Nlog2N)
2.最坏时间复杂度:O(N^2) 出现情况:逆序的情况
3.最好时间复杂度:O(Nlog2N)
4.平均空间复杂度:O(1)
稳定性
希尔排序是一个不稳定的排序算法
参考文献:
九大基础排序总结与对比
八大排序算法