高频排序算法

本文详细介绍了常见的排序算法,包括冒泡排序、快速排序、插入排序、希尔排序、简单选择排序、堆排序、归并排序以及基数排序。其中,冒泡排序和插入排序属于稳定排序,快速排序和堆排序属于不稳定排序。各种排序算法在不同场景下有不同的性能表现,如快速排序在序列元素随机分布时性能最佳,而插入排序在接近正序时表现最优。归并排序和堆排序的时间复杂度都是O(NlogN),但归并排序需要额外空间,而堆排序空间复杂度较低。基数排序则通过数位分配和收集实现排序,是稳定的排序算法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.冒泡排序

冒泡排序是一种交换排序(相邻元素两两比较)。
算法思想:冒泡排序重复地走访过要排序的数列,一次比较相邻的两个元素,如果它们的顺序不符合次序要求就交换它们的位置。走访数列的工作是重复地进行直到没有再需要交换的元素时,就说明该数列已经排序完成。

假设一个数组中有N个元素,要求按照升序排序。
(1)如果采用从后往前比较的方法,则最小值率先出现在数组的头角标位上。

//假设数组一共有N个元素,要求按照升序排序
 //冒泡排序1(如果是从后往前比较,则最小值率先出现在数组的头角标位上)
 public void bubbleSort1(int[] arr)
 {
  int tmp = 0;//定义一个临时变量
  //假设数组一共有N个元素,则最坏情况下需要进行N-1轮排序
  for(int i = 0;i < arr.length - 1;i++)
  {
   //因为是采用从后往前比较,则从最后一个元素开始比较
   //在第i轮排序时,前面的i-1个元素都已有序,只需要比较后面的N-(i-1)=N-i+1个元素即可,此时需要进行N-i+1-1=N-i次相邻元素两两比较
   for(int j = arr.length - 1;j > i;j--)
   {
    //相邻元素两两比较,如果不符合次序要求就交换两个元素的位置
    if(arr[j - 1] > arr[j])
    {
     tmp = arr[j - 1];
     arr[j - 1] = arr[j];
     arr[j] = tmp;
    }
   }
  }
 }

(2)如果采用从前往后比较的方法,则最大值率先出现在数组的尾角标位上。

//冒泡排序2(如果是从前往后比较,则最大值率先出现在数组的尾角标位上)
 public void bubbleSort2(int[] arr)
 {
  int tmp = 0;//临时变量
  //假设数组一共有N个元素,在最坏情况下需要进行N-1轮排序
  for(int i = 0;i < arr.length - 1;i++)
  {
   //因为是从前往后比较,则从数组的第一个元素开始比较
   //在第i轮排序时,前面的N-i+1个元素都还没有排序,需要进行N-i+1-1=N-i次比较
   for(int j = 0;j < arr.length - i - 1;j++)
   {
    //相邻元素两两比较,如果不符合次序要求则交换两个元素的位置
    if(arr[j] > arr[j + 1])
    {
     tmp = arr[j];
     arr[j] = arr[j + 1];
     arr[j + 1] = tmp;
    }
   }
  }
 }

(3)冒泡排序算法的优化:添加一个布尔型的标识位flag,用于标识在该轮排序中是否发生了数组元素的交换,如果没有发生则说明数组已经排好序了,就可以结束比较了。

//冒泡排序的优化
 public void bubbleSort3(int[] arr)
 {
  int tmp = 0;//临时变量
  //标识位,用来标识在该轮排序中是否发生了元素的交换,如果没有发生元素的交换则标识该数组已经排好序了,就不需要再进行比较了
  boolean flag = false;
  for(int i = 0;i < arr.length - 1;i++)
  {
   flag = false;
   //从后往前比较,最小值率先出现在数组的头角标位上
   //在第i轮排序时,前i-1个元素已经排好序了
   for(int j = arr.length - 1;j > i;j--)
   {
    if(arr[j - 1] > arr[j])
    {
     tmp = arr[j - 1];
     arr[j - 1] = arr[j];
     arr[j] = tmp;
     flag = true;
    }
   }
   if(!flag)
    break;//跳出循环
  }
 }

冒泡排序的特点:
(1)冒泡排序算法是通过相邻元素两两比较进行排序的,在排序过程中不会改变两个相同元素的相对位置,因此属于稳定排序。
(2)冒泡排序的最好时间复杂度是O(N),最坏时间复杂度是O(N ^ 2),因此平均实践复杂度是O(N ^ 2)。
(3)数组元素的顺序越接近正序的时候,冒泡排序算法的性能越好。

2.快速排序

算法思想:通过一趟排序将要排序的数据分割成独立的两部分,使得分割点左侧都是比它小的数据,分割点右侧都是比它大的数据。然后,再按照此方法对这两部分数据进行分别的快速排序。整个排序过程可以递归进行,以此达到使整个数据变成有序序列。

快速排序的处理过程:
假设初始状态为一组无序的数组:2 4 5 1 3
1)首先定义两个指针left和right分别指向序列的第一个元素(2)和最后一个元素(3),然后以第一个元素(序列最左边的元素2)作为基准,并将基准元素的值使用一个临时变量base存储起来。
2)从右向左扫描,移动偏移指针right,寻找第一个比基准元素base小的元素(K[right]<base),并将其值赋值给指针left所指的位置。
3)从左向右扫描,移动偏移指针left,寻找第一个比基准元素base大的元素(K[left] > base),并将其值赋值给指针right所指的位置。
4)不断重复二、三步骤,直到指针left和right重合,这样所有的元素就都被扫描了一遍了。此时,将基准元素base的值赋值给重合位置。此时,已经完成了一次排序。基准数base左边都是比他小的数,右边都是比它大的数。
5)以基准数base作为分割点,再对其左右两边的数分别按照步骤一、二、三、四去排序。经过递归的过程,最后排序结束。

//快速排序法
 public void quickSort(int[] arr)
 {
  quickSort(arr, 0, arr.length - 1);
 }
 public void quickSort(int[] arr, int left, int right)
 {
  if(left >= right)//防止堆栈溢出
   return;
  
  int i = left;
  int j = right;
  int base = arr[i];//以序列中的第一个元素(最左边的元素)作为基准
  
  while(i < j)
  {
   while(i < j && arr[j] >= base)//从右向左找到第一个小于base的元素K[j],使得K[j]<base,并将其移到指针i所指的位置
   {
    j--;
   }
   if(i < j)
    arr[i++] = arr[j];
   while(i < j && arr[i] <= base)//从左到右找到第一个大于base的元素K[i],使得K[i]>base,并将其移到指针j所指的位置
   {
    i++;
   }
   if(i < j)
    arr[j--] = arr[i];
  }
  //当指针重合时,表示此时序列中的所有元素都被遍历过一遍了
  //将base的值赋值给此时指针重合处
  arr[i] = base;
  //此时,在base左侧都是比它小的数,在base右侧都是比它大的数
  //对base左右两侧的数据分别递归进行上述步骤,直到所有元素都排好序
  quickSort(arr, left, i - 1);
  quickSort(arr,i + 1,right);
 }

快速排序的特点:
(1)快速排序算法属于交换排序,且是不稳定的排序算法(相等元素可能会因为分区而交换顺序)。
(2)快速排序算法的时间复杂度:最好情况和平均情况的时间复杂度是O(NlogN),最差情况的时间复杂度是O(N ^ 2)。
(3)快速排序算法的空间复杂度是O(logN)。
(4)序列元素随机分布时,快速排序算法的性能最好;序列元素越接近有序时,快速排序算法的性能越差。

3.插入排序

插入排序(每次将一个新的数据插入到有序序列的合适位置里)的处理过程:
假设初始有一组无序序列R0,R1,R2,…,RN-1
(1)首先将这个序列中下标为0的元素R0视为一个只包含有1个元素的有序序列,然后我们要依次把元素R1,R2,…,RN-1插入到这个有序序列中去。所以需要一个外部循环,从下标1开始扫描到下标N-1。
(2)假设要将元素Ri插入到前面有序的序列中,可知此时前面i-1个元素都已有序,元素Ri需要依次与前面的i-1个元素进行比较来确定插入位置。所以需要一个内部循环,从后往前,即从下标i-1扫描到下标0。
(3)在插入元素时,在插入位置处的元素及其之后的元素都需要向后顺次移动一个位置。

//直接插入排序
 public void insertSort(int[] arr)
 {
  int tmp = 0;//用于临时存储要插入的元素的值
  //因为我们将下标为0的元素R0看作是一个初始只包含1个元素的有序序列,然后再依次将元素R1,R2,...,RN-1插入到这个有序序列中
  for(int i = 1;i < arr.length;i++)
  {
   tmp = arr[i];
   int j = 0;//定义并初始化该局部变量
   //当插入下标为i的元素Ri时,前面i-1个元素已经有序了,从后往前,Ri依次与前面的元素进行比较来确定合适的插入位置。
   for(j = i - 1;j >= 0;j--)
   {
    if(arr[j] > tmp)
     arr[j + 1] = arr[j];//依次向后移动一个位置
    else
     break;//跳出该层循环
   }
   arr[j + 1] = tmp;
  }
 }

插入排序算法的特点:
(1)当序列中的元素已经是正序排列时,只需要从前往后遍历一遍所有元素,但无需移动元素,因此最好情况下时间复杂度为O(N);当序列中的元素全部是反序排列时,除了需要从前往后遍历一遍所有元素以外,每次都需要移动元素,因此最坏情况下时间复杂度为O(N ^ 2),所以平均时间复杂度为O(N ^ 2)。因此,序列越接近于正序,插入排序算法的性能就越好。
(2)由于在排序过程中,我们只定义了一个临时变量tmp来存储将要插入的元素,因此空间复杂度为O(1)。
(3)插入排序算法是一种稳定的排序算法,因为在直接插入排序的过程中,不需要改变相等元素的相对位置。

4.希尔排序

算法思想:把序列中的所有记录按照步长gap分组,对每组记录按照直接插入排序方法进行排序。随着步长gap的减少,所分成的组中包含的记录会越来越多。当步长gap减少为1时,整个数据会合成为一组,构成一组有序记录,则完成排序。
假设初始时有一个包含N个记录的无序序列
(1)在第一趟排序中,可以将步长gap1设置为gap1=N/2(取整),即相隔距离为gap1的记录组成一组,可以分为gap1组,接下来按照直接插入排序的方法分别对每组元素进行排序。
(2)在第二趟排序中,可以将步长gap2设置为gap2=gap1/2(取整),这样相隔距离为gap2的元素便可以组成一组,分为gap2组,接下来直接按照直接插入排序的方法分别对每组元素进行排序。
(3)以此类推,直到步长gap为1时,这时所有元素组成一组,对这组元素进行直接插入排序排序后,就可以完成排序了。

//希尔排序
 public void shellSort(int[] arr)
 {
  int gap = arr.length / 2;//初始步长
  //按照步长gap进行分组,对每组元素采用直接插入排序方法进行排序
  while(gap >= 1)//gap等于1时所有元素组成一组
  {
   for(int i = gap; i < arr.length;i++)
   {
    int tmp = arr[i];
    int j = 0;//定义并初始化局部变量
    for(j = i - gap;j >= 0 && arr[j] > tmp;j = j - gap)//从后往前
    {
     arr[j + gap] = arr[j];
    }
    arr[j + gap] = tmp;
   }
   gap /= 2;//gap=agp/2取整
  }
  
 }

希尔排序的特点:
(1)最好情况下时间复杂度是O(N),平均时间复杂度为O(NlogN)。
(2)希尔排序是不稳定的排序算法。

直接插入排序和希尔排序的比较
(1)直接插入排序是稳定的排序算法(因为在排序过程中不会改变相等的两个元素的相对位置),而希尔排序是不稳定的排序算法。
(2)直接插入排序更适合于原始序列中的元素基本有序的集合,原始序列越接近于正序,直接插入排序的性能越好(最好情况下的时间复杂度为O(N)),原始序列越接近于反序,直接插入排序的性能越差(最坏情况下的时间复杂度为O(N ^ 2)),平均时间复杂度为O(N ^ 2),空间复杂度为O(1)(因为只使用了一个临时变量来存储将要插入的元素的值)。
(3)希尔排序的比较次数和移动次数都比直接插入排序的要少,N越大时效果越明显,希尔排序算法的最好情况下时间复杂度为O(N),平均情况下时间复杂度为O(NlogN),空间复杂度为O(1)(因为只使用了一个临时变量来存储将要插入的元素的值)。
(4)在希尔排序中,增量gap的取值必须满足:最后一次排序的步长为1。
(5)直接插入排序适用于链式结构,希尔排序不适用于链式结构。

5.简单选择排序

算法思想:每一趟都从待排序的记录中选出关键字最小的记录,顺序放到已经排好序的记录序列的末尾,直到全部排序结束为止。
假设原始序列中一共有N个记录
(1)从待排序的序列中,找到关键字最小的元素。如果这个关键字最小的元素不是待排序序列的第一个元素,则交换该元素与第一个元素的位置;
(2)从余下的N-1个元素中,再找出一个关键字最小的元素,重复上述步骤,直到排序结束。
(3)简单选择排序的最小值率先出现在数组的头角标位上,在第i趟排序过程中,前面的i-1个元素已经排好序了,此时将当前第i小的元素放在位置i上。

//简单选择排序
 public void selectSort(int[] arr)
 {
  for(int i = 0;i < arr.length - 1;i++)
  {
   int tmp = arr[i];
   int index = i;
   for(int j = i + 1;j < arr.length;j++)//从待排序的序列中找到关键字最小的元素
   {
    if(arr[j] < tmp)
    {
     tmp = arr[j];
     index = j;
    }
   }
   arr[index] = arr[i];
   arr[i] = tmp;
  }
 }

简单选择排序的特点:
(1)简单选择排序是一种不稳定的排序算法。
(2)简单选择排序的比较次数与序列的初始排序无关,假设待排序的序列一共有N个元素,都需要比较(N-1)+(N-2)+…+1=(N-1+1) * (N-1)/2=N * (N-1)/2次;而简单选择排序的移动次数与序列的初始排序有关,在最好情况下即序列初始正序时,移动次数为0;最坏情况下即序列初始反序时,移动次数最多。所以简单排序的最好情况下、最坏情况下和平均情况下的时间复杂度均为O(N ^ 2)。
(3)简单选择排序的空间复杂度为O(1)(因为只使用了一个临时变量tmp来保存关键字最小的元素)。

6.堆排序

堆是一棵顺序存储的完全二叉树。
在这里插入图片描述

其中,每个节点的关键字都不大于(<=)其孩子节点的关键字,这样的堆称为小根堆;
每个节点的关键字都不小于(>=)其孩子节点的关键字,这样的堆称为大根堆。
对于n个元素的序列{R0,R1,…,Rn-1},当且仅当满足下列关系之一时,称之为堆:
R[i]<=R[2i+1]且R[i]<=R[2i+2](小根堆)
R[i]>=R[2i+1]且R[i]>=R[2i+2](大根堆)
其中i=0,1,…,(n-3)/2。
算法思想:
(1)构建堆,根据初始数组去构造初始堆,构建一个完全二叉树,保证所有父结点的值均不小于其左右孩子结点的值(大根堆)。
(2)交换堆顶元素和最后一个元素的位置,每次交换第一个元素和最后一个元素,输出最后一个元素(最大值),然后把剩下的元素在调整为一个大根堆。
(3)当输出完最后一个元素后,这个数组已经是按照从小到大的顺序排列了。

//堆排序(大顶堆:升序)
 public void heapAdjust(int[] arr, int pos, int len)//大根堆(父结点的值>=其左右孩子结点的值)
 {
  int tmp = 0;
  int child = 0;
  for(tmp = arr[pos];2 * pos + 1 <= len;pos = child)
  {
   child = 2 * pos + 1;//父结点的左子节点
   if(child + 1 <= len && arr[child] < arr[child + 1])
    child++;
   if(arr[child] < tmp)
    break;//跳出循环
   else
    arr[pos] = arr[child];
  }
  arr[pos] = tmp;
 }
 public void heapSort(int[] arr)
 {
  //1.构造堆
  for(int i = arr.length / 2 - 1;i >= 0;i--)
  {
   heapAdjust(arr, i, arr.length - 1);
  }
  for(int i = arr.length - 1; i > 0;i--)
  {
   //2.交换堆顶元素(堆顶元素必为最大值)和最后一个元素的位置
   int tmp = arr[0];
   arr[0] = arr[i];
   arr[i] = tmp;
   //去掉最后一个元素(交换后最后一个元素是最大值)后,再将剩余元素调整成一个大顶堆
   heapAdjust(arr, 0, i - 1);
  }
 }

特点:
(1)堆排序的最好情况、平均情况和最坏情况的时间复杂度均为O(NlogN),空间复杂度为O(1)。
(2)堆排序是不稳定的排序算法。

7.归并排序

归并排序的算法思想:将待排序序列R[0,1,…,n-1]先看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2的有序序列;将这些有序序列再次归并,得到n/4个长度为4的有序序列;入此反复进行下去,最后得到一个长度为n的有序序列。
综上可知:归并排序其实要做两件事情:
“分解”——将序列每次折半拆分
“合并”——将划分后的序列段两两排序后合并

//归并排序
 //合并
 public void merge(int[] arr, int low, int mid, int high)
 {
  int len1 = mid - low + 1;
  int[] L = new int[len1];
  int len2 = high - mid;
  int[] R = new int[len2];
  
  int i, j, k;
  for(i = 0, k = low;i < len1;i++, k++)
  {
   L[i] = arr[k];
  }
  for(j = 0, k = mid + 1; j < len2; j++, k++)
  {
   R[j] = arr[k];
  }
  
  i = 0;
  j = 0;
  k = low;
  while(i < len1 && j < len2)
  {
   if(L[i] <= R[j])//考虑到归并排序是稳定的排序算法,即在排序过程中不会改变两个相同元素的相对位置关系。
   {
    arr[k] = L[i];
    i++;
    k++;
   }
   else
   {
    arr[k] = R[j];
    j++;
    k++;
   }
  }
  while(i < len1)
  {
   arr[k++] = L[i++];
  }
  while(j < len2)
  {
   arr[k++] = R[j++];
  }
 }
 //拆分
 public void mergeSort(int[] arr, int l, int r)
 {
  if(l < r)
  {
   int mid = (r + l) / 2;
   mergeSort(arr, l, mid);
   mergeSort(arr, mid + 1, r);
   merge(arr, l, mid, r);
  }
 }

特点:
(1)归并排序的形式本质上是一棵二叉树,而且是一颗完全二叉树,因此它需要遍历的次数就是完全二叉树的深度,所以归并排序的最好情况、平均情况和最差情况的时间复杂度都是O(NlogN)。
(2)因为在算法过程中我们有定义临时的数组L和R来存放元素,所以空间复杂度为O(N)。
(3)归并排序法是稳定的排序方法。

归并排序法和堆排序法、快速排序法的比较
(1)由于归并排序法形式上本质还是一棵二叉树,而且是一棵完全二叉树,其遍历的次数就是该完全二叉树的深度,因此归并排序法的最好情况、平均情况和最坏情况的时间复杂度都是O(NlogN);而堆排序法是一棵顺序存储的完全二叉树,因此堆排序法的最好情况、平均情况和最坏情况的时间复杂度都是O(NlogN);而快速排序法最好情况和平均情况的时间复杂度为O(NlogN),但是最坏情况的时间复杂度为O(N ^ 2)。因此,如果从时间复杂度的角度来看,首选归并排序和堆排序。
(2)由于在合并过程中我们定义了两个数组L和R来临时存放元素,因此归并排序法的空间复杂度为O(N);堆排序的空间复杂度为O(1);快速排序法由于是递归完成的,而递归需要将数据暂时存放在栈空间中,所以快速排序法的空间复杂度为O(logN)。因此,如果从空间复杂度上来看,首选堆排序,次选快速排序,最后选归并排序。
(3)由于归并排序法是稳定的排序方法,而堆排序和快速排序都是不稳定的排序方法。因此,如果从稳定性的角度来看,应选归并排序。
(4)若从平均情况的排序速度来看,应选快速排序(快速排序法的“快速”可不是浪得虚名~)。

8.基数排序

算法思想:
基数排序与上述7种排序算法都不一样,因为它并不需要比较关键字的大小,它是根据关键字中各个数位上的值,通过对排序的N个元素进行若干趟的“分配”和“收集”来实现排序的。
例如,我们现在有一个待排序的序列R{50,123,543,187,49,30,0,2,11,100},我们知道,任何一个阿拉伯数字,其各个数位上的值都是以基数0 ~ 9来表示的,因此我们可以将0 ~ 9视为10个编号分别为0 ~ 9的桶。
(1)首先根据各个元素的个位上的值将所有元素分别分配到这10个桶中,例如R[0]=50,它就被分配到编号为0的桶中,如下图所示。
在这里插入图片描述

(2)分类后,我们再从编号为0 ~ 9的这10个桶中依次将元素收集起来,便得到了一组按照个位数升序排序的序列R{50,30,0,100,11,2,123,543,187,49}的序列。
(3)可以继续按照十位数对这个序列中的元素进行分配,就得到了按照十位数升序排序的序列R{0,100,2,11,123,30,543,49,50,187}。
(4)最后按照百位数对这个序列中的元素进行分配,就得到了按照百位数升序排序的序列R{0,2,11,30,49,50,100,123,187,543},这样就完成了对原始序列的升序排序了。

特点:基数排序法是稳定的排序方法。

参考:面试时写不出排序算法?看这篇就够了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值