当前位置:   article > 正文

排序算法时间复杂度、空间复杂度、稳定性比较_排序算法的时间复杂度和空间复杂度

排序算法的时间复杂度和空间复杂度

排序算法分类 

这里写图片描述

  排序大的分类可以分为两种:内排序和外排序。

  放在内存的称为内排序,需要使用外存的称为外排序。

排序算法的时间复杂度和空间复杂度

排序算法

平均时间复杂度

最坏时间复杂度

最好时间复杂度

空间复杂度

稳定性

冒泡排序

O(n²)

O(n²)

O(n)

O(1)

稳定

直接选择排序

O(n²)

O(n²)

O(n)

O(1)

不稳定

直接插入排序

O(n²)

O(n²)

O(n)

O(1)

稳定

快速排序

O(nlogn)

O(n²)

O(nlogn)

O(nlogn)

不稳定

堆排序

O(nlogn)

O(nlogn)

O(nlogn)

O(1)

不稳定

希尔排序

O(nlogn)

O(ns)

O(n)

O(1)

不稳定

归并排序

O(nlogn)

O(nlogn)

O(nlogn)

O(n)

稳定

计数排序

O(n+k)

O(n+k)

O(n+k)

O(n+k)

稳定

基数排序

O(N*M) 

O(N*M)

O(N*M)

O(M)

稳定

1 归并排序可以通过手摇算法将空间复杂度降到O(1),但是时间复杂度会提高。

2 基数排序时间复杂度为O(N*M),其中N为数据个数,M为数据位数。

辅助记忆

  • 时间复杂度记忆- 
  1. 冒泡、选择、直接 排序需要两个for循环,每次只关注一个元素,平均时间复杂度为O(n²))(一遍找元素O(n),一遍找位置O(n))
  2. 快速、归并、希尔、堆基于二分思想,log以2为底,平均时间复杂度为O(nlogn)(一遍找元素O(n),一遍找位置O(logn))
  • 稳定性记忆-“快希选堆”(快牺牲稳定性) 
  1. 排序算法的稳定性:排序前后相同元素的相对位置不变,则称排序算法是稳定的;否则排序算法是不稳定的。
     

原理理解

1 冒泡排序

1.1 过程

冒泡排序从小到大排序:一开始交换的区间为0~N-1,将第1个数和第2个数进行比较,前面大于后面,交换两个数,否则不交换。再比较第2个数和第三个数,前面大于后面,交换两个数否则不交换。依次进行,最大的数会放在数组最后的位置。然后将范围变为0~N-2,数组第二大的数会放在数组倒数第二的位置。依次进行整个交换过程,最后范围只剩一个数时数组即为有序。

1.2 动图

1.3 核心代码(函数)

  1. //array[]为待排序数组,n为数组长度
  2. void BubbleSort(int array[], int n)
  3. {
  4. int i, j, k;
  5. for(i=0; i<n-1; i++)
  6. for(j=0; j<n-1-i; j++)
  7. {
  8. if(array[j]>array[j+1])
  9. {
  10. k=array[j];
  11. array[j]=array[j+1];
  12. array[j+1]=k;
  13. }
  14. }
  15. }

2 直接选择排序

2.1 过程

选择排序从小到大排序:一开始从0~n-1区间上选择一个最小值,将其放在位置0上,然后在1~n-1范围上选取最小值放在位置1上。重复过程直到剩下最后一个元素,数组即为有序。

2.2 动图

2.3 核心代码(函数)

  1. //array[]为待排序数组,n为数组长度
  2. void selectSort(int array[], int n)
  3. {
  4. int i, j ,min ,k;
  5. for( i=0; i<n-1; i++)
  6. {
  7. min=i; //每趟排序最小值先等于第一个数,遍历剩下的数
  8. for( j=i+1; j<n; j++) //从i下一个数开始检查
  9. {
  10. if(array[min]>array[j])
  11. {
  12. min=j;
  13. }
  14. }
  15. if(min!=i)
  16. {
  17. k=array[min];
  18. array[min]=array[i];
  19. array[i]=k;
  20. }
  21. }
  22. }

3 直接插入排序

3.1 过程

插入排序从小到大排序:首先位置1上的数和位置0上的数进行比较,如果位置1上的数大于位置0上的数,将位置0上的数向后移一位,将1插入到0位置,否则不处理。位置k上的数和之前的数依次进行比较,如果位置K上的数更大,将之前的数向后移位,最后将位置k上的数插入不满足条件点,反之不处理。

3.2 动图


3.3 核心代码(函数)

  1. //array[]为待排序数组,n为数组长度
  2. void insertSort(int array[], int n)
  3. {
  4. int i,j,temp;
  5. for( i=1;i<n;i++)
  6. {
  7. if(array[i]<array[i-1])
  8. {
  9. temp=array[i];
  10. for( j=i;array[j-1]>temp;j--)
  11. {
  12. array[j]=array[j-1];
  13. }
  14. array[j]=temp;
  15. }
  16. }
  17. }

4 快速排序

 4.1 过程

 快速排序从小到大排序:在数组中随机选一个数(默认数组首个元素),数组中小于等于此数的放在左边部分,大于此数的放在右边部分,这个操作确保了这个数是处于正确位置的,再对左边部分数组和右边部分数组递归调用快速排序,重复这个过程。

 4.2 动图

 

 4.3 核心代码(函数)

  1. void quicksort(int a[], int left, int right) {
  2. int i, j, t, privotkey;
  3. if (left > right) //(递归过程先写结束条件)
  4. return;
  5. privotkey = a[left]; //temp中存的就是基准数(枢轴)
  6. i = left;
  7. j = right;
  8. while (i < j) {
  9. //顺序很重要,要先从右边开始找(最后交换基准时换过去的数要保证比基准小,因为基准选取数组第一个数)
  10. while (a[j] >= privotkey && i < j) {
  11. j--;
  12. }
  13. a[i] = a[j];
  14. //再找左边的
  15. while (a[i] <= privotkey && i < j) {
  16. i++;
  17. }
  18. a[j] = a[i];
  19. }
  20. //最终将基准数归位
  21. a[i] = privotkey;
  22. quicksort(a, left, i - 1);//继续处理左边的,这里是一个递归的过程
  23. quicksort(a, i + 1, right);//继续处理右边的 ,这里是一个递归的过程
  24. }

5 堆排序

 5.1 过程

 堆排序从小到大排序:首先将数组元素建成大小为n的大顶堆,堆顶(数组第一个元素)是所有元素中的最大值,将堆顶元素和数组最后一个元素进行交换,再将除了最后一个数的n-1个元素   建立成大顶堆,再将最大元素和数组倒数第二个元素进行交换,重复直至堆大小减为1。

  • 注:完全二叉树 
    假设二叉树深度为n,除了第n层外,n-1层节点都有两个孩子,第n层节点连续从左到右。如下图 
    这里写图片描述

  • 注:大顶堆 
    大顶堆是具有以下性质的完全二叉树:每个节点的值都大于或等于其左右孩子节点的值。 
    即,根节点是堆中最大的值,按照层序遍历给节点从1开始编号,则节点之间满足如下关系: 
    这里写图片描述 (1<=i<=n/2)

 5.2 动图

 
 

 5.3 核心代码(函数)

这里写图片描述

  1. void heapSort(int array[], int n)
  2. {
  3. int i;
  4. for (i=n/2;i>0;i--)
  5. {
  6. HeapAdjust(array,i,n);//从下向上,从右向左调整
  7. }
  8. for( i=n;i>1;i--)
  9. {
  10. swap(array, 1, i);
  11. HeapAdjust(array, 1, i-1);//从上到下,从左向右调整
  12. }
  13. }
  14. void HeapAdjust(int array[], int s, int n )
  15. {
  16. int i,temp;
  17. temp = array[s];
  18. for(i=2*s;i<=n;i*=2)
  19. {
  20. if(i<n&&array[i]<array[i+1])
  21. {
  22. i++;
  23. }
  24. if(temp>=array[i])
  25. {
  26. break;
  27. }
  28. array[s]=array[i];
  29. s=i;
  30. }
  31. array[s]=temp;
  32. }
  33. void swap(int array[], int i, int j)
  34. {
  35. int temp;
  36. temp=array[i];
  37. array[i]=array[j];
  38. array[j]=temp;
  39. }

6 希尔排序

 6.1 过程

 希尔排序是插入排序改良的算法,希尔排序步长从大到小调整,第一次循环后面元素逐个和前面元素按间隔步长进行比较并交换,直至步长为1,步长选择是关键。

 6.2 动图

这里写图片描述 

 6.3 核心程序(函数)

  1. //下面是插入排序
  2. void InsertSort( int array[], int n)
  3. {
  4. int i,j,temp;
  5. for( i=0;i<n;i++ )
  6. {
  7. if(array[i]<array[i-1])
  8. {
  9. temp=array[i];
  10. for( j=i-1;array[j]>temp;j--)
  11. {
  12. array[j+1]=array[j];
  13. }
  14. array[j+1]=temp;
  15. }
  16. }
  17. }
  18. //在插入排序基础上修改得到希尔排序
  19. void SheelSort( int array[], int n)
  20. {
  21. int i,j,temp;
  22. int gap=n; //~~~~~~~~~~~~~~~~~~~~~
  23. do{
  24. gap=gap/3+1; //~~~~~~~~~~~~~~~~~~
  25. for( i=gap;i<n;i++ )
  26. {
  27. if(array[i]<array[i-gap])
  28. {
  29. temp=array[i];
  30. for( j=i-gap;array[j]>temp;j-=gap)
  31. {
  32. array[j+gap]=array[j];
  33. }
  34. array[j+gap]=temp;
  35. }
  36. }
  37. }while(gap>1); //~~~~~~~~~~~~~~~~~~~~~~
  38. }

7 归并排序

 7.1 过程

 归并排序从小到大排序:首先让数组中的每一个数单独成为长度为1的区间,然后两两一组有序合并,得到长度为2的有序区间,依次进行,直到合成整个区间。

 7.2 动图

 7.3 核心代码(函数)

递归实现

  1. 实现归并,并把数据都放在list1里面
  2. void merging(int *list1, int list1_size, int *list2, int list2_size)
  3. {
  4. int i=0, j=0, k=0, m=0;
  5. int temp[MAXSIZE];
  6. while(i < list1_size && j < list2_size)
  7. {
  8. if(list1[i]<list2[j])
  9. {
  10. temp[k++] = list1[i++];
  11. }
  12. else
  13. {
  14. temp[k++] = list2[j++];
  15. }
  16. }
  17. while(i<list1_size)
  18. {
  19. temp[k++] = list1[i++];
  20. }
  21. while(j<list2_size)
  22. {
  23. temp[k++] = list2[j++];
  24. }
  25. for(m=0; m < (list1_size+list2_size); m++)
  26. {
  27. list1[m]=temp[m];
  28. }
  29. }
  30. //如果有剩下的,那么说明就是它是比前面的数组都大的,直接加入就可以了
  31. void mergeSort(int array[], int n)
  32. {
  33. if(n>1)
  34. {
  35. int *list1 = array;
  36. int list1_size = n/2;
  37. int *list2 = array + n/2;
  38. int list2_size = n-list1_size;
  39. mergeSort(list1, list1_size);
  40. mergeSort(list2, list2_size);
  41. merging(list1, list1_size, list2, list2_size);
  42. }
  43. }
  44. //归并排序复杂度分析:一趟归并需要将待排序列中的所有记录
  45. //扫描一遍,因此耗费时间为O(n),而由完全二叉树的深度可知,
  46. //整个归并排序需要进行[log2n],因此,总的时间复杂度为
  47. //O(nlogn),而且这是归并排序算法中平均的时间性能
  48. //空间复杂度:由于归并过程中需要与原始记录序列同样数量级的
  49. //存储空间去存放归并结果及递归深度为log2N的栈空间,因此空间
  50. //复杂度为O(n+logN)
  51. //也就是说,归并排序是一种比较占内存,但却效率高且稳定的算法

迭代实现

  1. void MergeSort(int k[],int n)
  2. {
  3. int i,next,left_min,left_max,right_min,right_max;
  4. //动态申请一个与原来数组一样大小的空间用来存储
  5. int *temp = (int *)malloc(n * sizeof(int));
  6. //逐级上升,第一次比较2个,第二次比较4个,第三次比较8个。。。
  7. for(i=1; i<n; i*=2)
  8. {
  9. //每次都从0开始,数组的头元素开始
  10. for(left_min=0; left_min<n-i; left_min = right_max)
  11. {
  12. right_min = left_max = left_min + i;
  13. right_max = left_max + i;
  14. //右边的下标最大值只能为n
  15. if(right_max>n)
  16. {
  17. right_max = n;
  18. }
  19. //next是用来标志temp数组下标的,由于每次数据都有返回到K,
  20. //故每次开始得重新置零
  21. next = 0;
  22. //如果左边的数据还没达到分割线且右边的数组没到达分割线,开始循环
  23. while(left_min<left_max&&right_min<right_max)
  24. {
  25. if(k[left_min] < k[right_min])
  26. {
  27. temp[next++] = k[left_min++];
  28. }
  29. else
  30. {
  31. temp[next++] = k[right_min++];
  32. }
  33. }
  34. //上面循环结束的条件有两个,如果是左边的游标尚未到达,那么需要把
  35. //数组接回去,可能会有疑问,那如果右边的没到达呢,其实模拟一下就可以
  36. //知道,如果右边没到达,那么说明右边的数据比较大,这时也就不用移动位置了
  37. while(left_min < left_max)
  38. {
  39. //如果left_min小于left_max,说明现在左边的数据比较大
  40. //直接把它们接到数组的min之前就行
  41. k[--right_min] = k[--left_max];
  42. }
  43. while(next>0)
  44. {
  45. //把排好序的那部分数组返回该k
  46. k[--right_min] = temp[--next];
  47. }
  48. }
  49. }
  50. }
  51. //非递归的方法,避免了递归时深度为log2N的栈空间,
  52. //空间只是用到归并临时申请的跟原来数组一样大小的空间,并且在时间性能上也有一定的提升,
  53. //因此,使用归并排序是,尽量考虑用非递归的方法。

8 桶排序(基数排序和基数排序的思想)

 8.1 过程

 桶排序是计数排序的变种,把计数排序中相邻的m个”小桶”放到一个”大桶”中,在分完桶后,对每个桶进行排序(一般用快排),然后合并成最后的结果。

 8.2 图解

 8.3 核心程序

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int a[11],i,j,t;
  5. for(i=0;i<=10;i++)
  6. a[i]=0; //初始化为0
  7. for(i=1;i<=5;i++) //循环读入5个数
  8. {
  9. scanf("%d",&t); //把每一个数读到变量t中
  10. a[t]++; //进行计数(核心行)
  11. }
  12. for(i=0;i<=10;i++) //依次判断a[0]~a[10]
  13. for(j=1;j<=a[i];j++) //出现了几次就打印几次
  14. printf("%d ",i);
  15. getchar();getchar();
  16. //这里的getchar();用来暂停程序,以便查看程序输出的内容
  17. //也可以用system("pause");等来代替
  18. return 0;
  19. }

9 计数排序

 9.1 过程

 算法的步骤如下: 
 - 找出待排序的数组中最大和最小的元素 
 - 统计数组中每个值为i的元素出现的次数,存入数组C的第i项 
 - 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加) 
 - 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

 9.2 图解

这里写图片描述

 9.3 核心程序(函数)

  1. 程序1
  2. #define NUM_RANGE (100) //预定义数据范围上限,即K的值
  3. void counting_sort(int *ini_arr, int *sorted_arr, int n) //所需空间为 2*n+k
  4. {
  5. int *count_arr = (int *)malloc(sizeof(int) * NUM_RANGE);
  6. int i, j, k;
  7. //初始化统计数组元素为值为零
  8. for(k=0; k<NUM_RANGE; k++){
  9. count_arr[k] = 0;
  10. }
  11. //统计数组中,每个元素出现的次数
  12. for(i=0; i<n; i++){
  13. count_arr[ini_arr[i]]++;
  14. }
  15. //统计数组计数,每项存前N项和,这实质为排序过程
  16. for(k=1; k<NUM_RANGE; k++){
  17. count_arr[k] += count_arr[k-1];
  18. }
  19. //将计数排序结果转化为数组元素的真实排序结果
  20. for(j=n-1 ; j>=0; j--){
  21. int elem = ini_arr[j]; //取待排序元素
  22. int index = count_arr[elem]-1; //待排序元素在有序数组中的序号
  23. sorted_arr[index] = elem; //将待排序元素存入结果数组中
  24. count_arr[elem]--; //修正排序结果,其实是针对算得元素的修正
  25. }
  26. free(count_arr);
  27. }
  28. 程序2:C++(最大最小压缩桶数)
  29. public static void countSort(int[] arr) {
  30. if (arr == null || arr.length < 2) {
  31. return;
  32. }
  33. int min = arr[0];
  34. int max = arr[0];
  35. for (int i = 1; i < arr.length; i++) {
  36. min = Math.min(arr[i], min);
  37. max = Math.max(arr[i], max);
  38. }
  39. int[] countArr = new int[max - min + 1];
  40. for (int i = 0; i < arr.length; i++) {
  41. countArr[arr[i] - min]++;
  42. }
  43. int index = 0;
  44. for (int i = 0; i < countArr.length; i++) {
  45. while (countArr[i]-- > 0) {
  46. arr[index++] = i + min;
  47. }
  48. }

10 基数排序

 10.1 过程

 基数排序是基于数据位数的一种排序算法。 
 它有两种算法 
 ①LSD–Least Significant Digit first 从低位(个位)向高位排。 
 ②MSD– Most Significant Digit first 从高位向低位(个位)排。 
 时间复杂度O(N*最大位数)。 
 空间复杂度O(N)。

 10.2 图解

这里写图片描述 
 对a[n]按照个位0~9进行桶排序: 
这里写图片描述 
 对b[n]进行累加得到c[n],用于b[n]中重复元素计数 
 !!!b[n]中的元素为temp中的位置!!!跳跃的用++补上: 
这里写图片描述 
 temp数组为排序后的数组,写回a[n]。temp为按顺序倒出桶中的数据(联合b[n],c[n],a[n]得到),重复元素按顺序输出: 
这里写图片描述

 10.3 核心程序

  1. //基数排序
  2. //LSD 先以低位排,再以高位排
  3. //MSD 先以高位排,再以低位排
  4. void LSDSort(int *a, int n)
  5. {
  6. assert(a); //判断a是否为空,也可以a为空||n<2返回
  7. int digit = 0; //最大位数初始化
  8. for (int i = 0; i < n; ++i)
  9. { //求最大位数
  10. while (a[i] > (pow(10,digit))) //pow函数要包含头文件math.h,pow(10,digit)=10^digit
  11. {
  12. digit++;
  13. }
  14. }
  15. int flag = 1; //位数
  16. for (int j = 1; j <= digit; ++j)
  17. {
  18. //建立数组统计每个位出现数据次数(Digit[n]为桶排序b[n])
  19. int Digit[10] = { 0 };
  20. for (int i = 0; i < n; ++i)
  21. {
  22. Digit[(a[i] / flag)%10]++; //flag=1时为按个位桶排序
  23. }
  24. //建立数组统计起始下标(BeginIndex[n]为个数累加c[n],用于记录重复元素位置
  25. //flag=1时,下标代表个位数值,数值代表位置,跳跃代表重复)
  26. int BeginIndex[10] = { 0 };
  27. for (int i = 1; i < 10; ++i)
  28. {
  29. //累加个数
  30. BeginIndex[i] = BeginIndex[i - 1] + Digit[i - 1];
  31. }
  32. //建立辅助空间进行排序
  33. //下面两条可以用calloc函数实现
  34. int *tmp = new int[n];
  35. memset(tmp, 0, sizeof(int)*n);//初始化
  36. //联合各数组求排序后的位置存在temp中
  37. for (int i = 0; i < n; ++i)
  38. {
  39. int index = (a[i] / flag)%10; //桶排序和位置数组中的下标
  40. //计算temp相应位置对应a[i]中的元素,++为BeginIndex数组数值加1
  41. //跳跃间隔用++来补,先用再++
  42. tmp[BeginIndex[index]++] = a[i];
  43. }
  44. //将数据重新写回原空间
  45. for (int i = 0; i < n; ++i)
  46. {
  47. a[i] = tmp[i];
  48. }
  49. flag = flag * 10;
  50. delete[] tmp;
  51. }
  52. }

推荐一个非常好的算法可视化演示的网站:https://visualgo.net/zh

快速排序、归并排序、堆排序三种算法性能比较

上文指出三种算法的性能差异:在数据量小的时候快速排序当属第一,堆排序最差,但随着数据的不断增大归并排序的性能会逐步赶上并超过快速排序,性能成为三种算法之首。可能在数据量大到一定数量时,快速排序的堆栈开销比较大,所以在性能上大打折扣,甚至堆排序的性能也能好过它,但总体上来说快速排序表现的还是比较优秀的。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/501333
推荐阅读
相关标签
  

闽ICP备14008679号