【数据结构】一篇文章带你学会八大排序

在这里插入图片描述

  • 一、排序的概念
    • 1. 排序的使用:
    • 2. 稳定性:
    • 3. 内部排序:
    • 4. 外部排序︰
    • 5. 排序的用途:
  • 二、排序的原理及实现
    • 1. 插入排序
      • 1.1 直接插入排序
        • 1.1.1 直接插入排序在现实中的应用
        • 1.1.2 直接插入排序的思想及个人理解
        • 1.1.3 直接插入排序的排序过程及代码实现
        • 1.1.4 直接插入排序的复杂度计算
        • 1.1.5 直接插入排序的总结
      • 1.2 希尔排序(缩小增量排序)
        • 1.2.1 希尔排序的由来
        • 1.2.2 希尔排序的排序思想
        • 1.2.3 希尔排序的排序过程及代码实现
        • 1.2.4 希尔排序的复杂度
        • 1.2.5 希尔排序的总结
    • 2. 选择排序
      • 2.1 直接选择排序
        • 2.1.1 直接选择排序的基本思想
        • 2.1.2 直接选择排序过程及代码实现
        • 2.1.3 直接选择排序的时间复杂度
        • 2.1.4 直接选择排序的总结
      • 2.2 堆排序
    • 3. 交换排序
      • 3.1 *冒泡排序*
        • 3.1.1 冒泡排序的基本思想
        • 3.1.2 冒泡排序的排序过程及代码实现
        • 3.1.3 冒泡排序的时间复杂度
        • 3.1.4 冒泡排序的总结
      • 3.2 快速排序
        • 3.2.1 快速排序的基本思想
        • 3.2.2 快速排序的排序过程及代码实现(三种版本+非递归版本)
          • 3.2.2.1 hoare版本
          • 3.2.2.2 挖坑法
          • 3.2.2.3 前后指针法
          • 3.2.2.4 快速排序的优化(三数取中)
          • 3.2.2.5 非递归版本
        • 3.2.3 快速排序的时间复杂度
        • 3.2.4 快速排序的总结
    • 4. 归并排序
      • 4.1 归并排序的基本思想:
      • 4.2 归并排序的代码实现
        • 4.2.1 归并排序的递归版本实现
        • 4.2.2 归并排序的非递归版本实现
      • 4.3 归并排序的总结:
    • 5. 计数排序(非比较函数)
      • 5.1 计数排序的思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
      • 5.2 计数排序的代码实现
      • 5.3 计数排序的总结:
  • 三、排序算法复杂度及稳定性分析
  • 结尾

一、排序的概念

1. 排序的使用:

所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

2. 稳定性:

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
简而言之,两个相同的数,例如下图,排完序后黑9仍然相对于在红9的前面,则算稳定。
在这里插入图片描述

3. 内部排序:

数据元素全部放在内存中的排序。

4. 外部排序︰

数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

5. 排序的用途:

排序作为数据结构中很重要的一环,在生活中被普遍的使用。
5.1 例如某东在售卖商品时,都会有按综合排序、按销量排序等都会有排序的身影
在这里插入图片描述
5.2 例如在壁纸软件某paper中也有最热门、评分最高等中也有排序的身影

二、排序的原理及实现

1. 插入排序

1.1 直接插入排序

1.1.1 直接插入排序在现实中的应用

在这里插入图片描述
相信大家都玩过牌,在摸牌阶段,每摸一张牌都从后往前依次比较,若摸起来的这张牌比比较的这张牌小,则继续向前比较,直到遇到比它小的那张牌,则将摸起来的那张牌放在它的后面,若没有比摸起来的这张牌小的,则将这张牌放在最前面。而直接插入排序与摸牌的思想相同。


1.1.2 直接插入排序的思想及个人理解

直接插入排序的思想:当插入第i(i>=1)个元素时,前面的array[0],array[1]…array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2]…的排序码顺序进行比较,找到插入位置即将array[i]插入.原来位置上的元素顺序后移。

我个人理解为是:将待排序的数据依次与前面有序的数组比较,直到形成一个新的有序数组。


1.1.3 直接插入排序的排序过程及代码实现

a. 静态排序过程
在这里插入图片描述
b. 动态排序过程

在这里插入图片描述
c. 代码实现

#include <stdio.h>void InsertSort(int* arr , int n)
{for (int i = 0; i < n - 1; i++){int end = i;int tmp = arr[end + 1];      //tmp 记录需要插入的值while (end >= 0){if (arr[end] > tmp)      //如果arr[end] > tmp 则 arr[end] 向后移动一位{arr[end + 1] = arr[end];end--;}else					 //如果arr[end] <= tmp ,则将tmp放到 arr[end] 的后面{break;}}							 //如果需要插入的数是最小的时候,是特殊情况需要放在第一位arr[end + 1] = tmp;}
}

注意
这里有一个很巧妙的设计,上述代码只分了两种情况,而一般情况下的想法是将比较分为三种情况:
待排序数从后往前与有序数列比较时较小,被比较数向后移动,待排序数向前继续比较
待排序数从后往前与有序数列比较时较大或者相等时,待排序数放在比较数后面
待排序数比有序数列中的任何一个数据都要小时,无法继续比较(待排序数已经到了数组首元素位置,继续比较的数无意义且已经越界),放在有序数列的第一个位置上

而这里则将( 2 )( 3 )两种情况合并为一种,详细来说就是,( 2 )( 3 )两种情况都是需要赋值的,而按照第一种情况的相反方向列为赋值情况,那么待排序数就到了正确的位置,赋值并跳出循环,但循环有两种停止情况,不满足循环条件(待排序是比有序数组中的每一位都小) 待排序数放在正确的位置,而我们并不能确定是上面两种情况的哪一种,所以要判断待排序数是否是不满足循环条件,将其赋值到有序数组的首元素位置。上述情况则为下面的代码。

两种代码都可行,并无本质上的区别。

void InsertSort(int* a, int n)
{for (int i = 0; i < n - 1; i++){int end = i;int tmp = a[end + 1];for (end = i; end >= 0; end--){if (a[end] > tmp){ a[end + 1] = a[end];}else{a[end + 1] = tmp;break;}}if (end + 1 == 0){a[end + 1] = tmp;}}
}
1.1.4 直接插入排序的复杂度计算

在这里插入图片描述

1.1.5 直接插入排序的总结
  1. 元素集合越接近有序,直接插入排序算法的时间效率越高
  2. 时间复杂度:O(n^2)
  3. 空间复杂度:O(1),它是一种稳定的排序算法
  4. 稳定性:稳定

1.2 希尔排序(缩小增量排序)

1.2.1 希尔排序的由来

希尔排序的定义是:希尔排序(Shell Sort)也称为缩小增量排序,是插入排序的一种。它是英国数学家唐纳德·希尔(Donald Shell) 在1959 年提出的。

1.2.2 希尔排序的排序思想

希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件刚好被分成一组,算法便终止。

简单来说,希尔排序的工作原理是:

  1. 按照一个增量 gap ,将记录分组。所有距离为 gap 的倍数的记录放在同一组。
  2. 对每组使用直接插入排序算法对该组进行排序。
  3. 持续减少增量 gap 的值,重复第一和第二步,直到 gap = 1 时,整个序列变为一个组,算法终止。

所以,希尔排序是一种基于插入排序的分组排序算法,它通过增量的逐步缩小来达到平均趋近 O(n*logn) 的效率。

1.2.3 希尔排序的排序过程及代码实现

在这里插入图片描述

void ShellSort(int* a, int n)
{int gap = n;while (gap > 1){gap = gap / 2;for (int i = 0; i < n - gap; i++){int end = i;int tmp = a[end + gap];while (end >= 0){if (a[end] > tmp){a[end + gap] = a[end];end -= gap;}else{break;}}a[end + gap] = tmp;}}
}
1.2.4 希尔排序的复杂度
  • 《数据结构(C语言版)》— 严蔚敏
    在这里插入图片描述
  • 《数据结构-用面相对象方法与C++描述》— 殷人昆
    在这里插入图片描述

1.2.5 希尔排序的总结
  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就
    会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定。
  4. 稳定性:不稳定

2. 选择排序

2.1 直接选择排序

2.1.1 直接选择排序的基本思想

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

2.1.2 直接选择排序过程及代码实现

在这里插入图片描述
上面的动态图显示的是一次选择一个最小/最大的值进行交换,下面我写的的代码是直接选择排序的优化版本,一次挑出最大和最小的分别与最右边与最左边进行交换。

// 选择排序
void SelectSort(int* a, int n)
{int left = 0;int right = n - 1;while (left <= right){// 记录最大值和最小值的位置int maxi = left;int mini = left;for (int i = left; i <= right; i++){if (a[i] > a[maxi]){maxi = i;}if (a[i] < a[mini]){mini = i;}}// 将最小的换到左边,最大的换到右边Swap(&a[left], &a[mini]);// 这里是为了解决最大的值在最左边时,左边与最小值交换// 导致最大值的位置被改变,下面交换交换时会出现问题// 下面的图中展示可能出现的情况if (left == maxi)maxi = mini;Swap(&a[maxi], &a[right]);left++;right--;}
}

在这里插入图片描述

2.1.3 直接选择排序的时间复杂度

![在这里插入图片描述](https://img-blog.csdnimg.cn/12daa8ad15964efe9efbaa00d8d0a5d1.png

2.1.4 直接选择排序的总结

直接选择排序的特性总结:

  1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
  2. 时间复杂度:O(n^2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

2.2 堆排序

堆排序这里不过多解释了,详细讲解在我写的这篇文章有详细的讲解。【数据结构】非线性结构之树结构(含堆)


3. 交换排序

3.1 冒泡排序

3.1.1 冒泡排序的基本思想
  1. 比较相邻的元素,如果第一个比第二个大,就交换它们两个
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素会是最大的数
  3. 针对所有的元素重复以上的步骤,除了最后一个元素之外的所有元素都已被排序
  4. 继续采用相同的方法对剩余未排序元素重复排序,直到所有元素有序为止

注意: 冒泡排序能够优化,定义一个变量,用来记录排序过程是否有数据交换操作,如果没有交换操作,那么这次排序就已经完成。

3.1.2 冒泡排序的排序过程及代码实现

在这里插入图片描述

void BubbleSort(int* arr, int N)
{int i = 0;for (i = 0; i < N - 1; i++){for (int j = 0; j < N - i - 1; j++){if (arr[j] > arr[j + 1]){Swap(&arr[j], &arr[j + 1]);}}}
}
3.1.3 冒泡排序的时间复杂度

在这里插入图片描述

3.1.4 冒泡排序的总结
  1. 冒泡排序是—种非常容易理解的排序
  2. 时间复杂度:O(n^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定

3.2 快速排序

3.2.1 快速排序的基本思想

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

3.2.2 快速排序的排序过程及代码实现(三种版本+非递归版本)
多种快速排序的实现共用Swap()函数
void Swap(int* e1, int* e2)
{int tmp = *e1;*e1 = *e2;*e2 = tmp;
}
3.2.2.1 hoare版本

在这里插入图片描述
hoare版本的思想是在待排序的数组中选取一边的元素作为关键字,例如这里使用左边的元素作为关键字,使用一个变量keyi记录关键字的下标,再使用两个下标分别记录关键字左边的一个元素(left)和数组的最后一个元素(right),left从左向右寻找比关键字大的元素,right从右向左寻找比关键字小的元素,找到后让两个下标对应位置上的两个元素交换,然后两个下标继续上面的操作继续寻找,直到两个下标相等后,keyi指向的元素(关键字)与当前下标上的元素交换,则完成了当前关键字左边的元素比关键字小,关键字右边的元素比关键字大,那么则完成了第一次排序。

记录交换后关键字的下标,将关键字左边和右边分割成为两个待排序区间,重复上面第一次排序的操作继续将剩下的待排序区间进行排序,直到区间不存在或者是区间内只有一个元素 ,那么当前区间不可继续分割,则这次排序才算结束。当所有的区待排序区间都不存在或是只有一个元素时,那么所有的元素就到了它应该在的位置,则排序完成。

int PartSort1(int* a, int left, int right)
{int keyi = left;while (left < right){while (left < right && a[right] >= a[keyi]){right--;}while (left < right && a[left] <= a[keyi]){left++;}Swap(&a[left], &a[right]);}Swap(&a[left], &a[keyi]);return right;
}void QuickSort(int* a, int left, int right)
{int keyi = PartSort1(a, left, right);//[left , keyi - 1] [keyi] [keyi + 1 , right]if (keyi - 1 > left){QuickSort(a, left, keyi - 1);}if (right > keyi + 1){QuickSort(a, keyi + 1, right);}
}
3.2.2.2 挖坑法

在这里插入图片描述

挖坑法的思想是在待排序数组中的选取一边的下标作为坑位且坑位指向的元素为关键字,例如这里使用左边的元素作为关键字,使用一个变量keyi记录坑位的下标,再使用两个下标分别记录关键字左边的一个元素(left)和数组的最后一个元素(right),先使right从右向左寻找比关键字小的元素,找到后将right指向的元素赋值给keyi(坑位)指向的元素,将当前right值赋值给keyi形成新坑位,再使left从左向右寻找比关键字大的元素,找到后将当left指向的元素赋值给keyi(坑位)指向的元素,将当前right值赋值给keyi形成新坑位。重复以上操作,直到两个下标相遇后,将keyi指向的元素(关键字)赋值给最后的坑位,则完成了当前关键字左边的元素比关键字小,关键字右边的元素比关键字大,那么则完成了第一次排序。

记录交换后关键字的下标,将关键字左边和右边分割成为两个待排序区间,重复上面第一次排序的操作继续将剩下的待排序区间进行排序,直到区间不存在或者是区间内只有一个元素 ,那么当前区间不可继续分割,则这次排序才算结束。当所有的区待排序区间都不存在或是只有一个元素时,那么所有的元素就到了它应该在的位置,则排序完成。

int PartSort2(int* a, int left, int right)
{int keyi = left;int key = a[left];while (left < right){while (left < right && a[right] >= key){right--;}a[left] = a[right];while (left < right && a[left] <= key){left++;}a[right] = a[left];}a[right] = key;return left;
}void QuickSort(int* a, int left, int right)
{int keyi = PartSort2(a, left, right);//[left , keyi - 1] [keyi] [keyi + 1 , right]if (keyi - 1 > left){QuickSort(a, left, keyi - 1);}if (right > keyi + 1){QuickSort(a, keyi + 1, right);}
}
3.2.2.3 前后指针法

在这里插入图片描述
前后指针法的思想是选取待排序数组的左边作为关键字,使用一个变量keyi记录关键字的下标,再使用两个下标分别记录关键字的下标(prev)和关键字左边的一个元素(cur),先使cur从左向右遍历,若cur指向的元素比关键字大,那么cur向后继续遍历。若cur指向的元素比关键字小,那么prev先向后移动,curprev指向的元素交换,然后cur向后继续遍历,重复上面的步骤,直到cur越界后,keyi指向的元素(关键字)与prev上指向的元素交换,则完成了当前关键字左边的元素比关键字小,关键字右边的元素比关键字大,那么则完成了第一次排序。

记录交换后关键字的下标,将关键字左边和右边分割成为两个待排序区间,重复上面第一次排序的操作继续将剩下的待排序区间进行排序,直到区间不存在或者是区间内只有一个元素 ,那么当前区间不可继续分割,则这次排序才算结束。当所有的区待排序区间都不存在或是只有一个元素时,那么所有的元素就到了它应该在的位置,则排序完成。

int PartSort3(int* a, int left, int right)
{int prev = left;int cur = left + 1;int keyi =  left;while (cur <= right){if (a[cur] < a[keyi] && cur != prev){prev++;Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[keyi], &a[prev]);return prev;
}void QuickSort(int* a, int left, int right)
{int keyi = PartSort3(a, left, right);//[left , keyi - 1] [keyi] [keyi + 1 , right]if (keyi - 1 > left){QuickSort(a, left, keyi - 1);}if (right > keyi + 1){QuickSort(a, keyi + 1, right);}
}
3.2.2.4 快速排序的优化(三数取中)

前面未优化的快速排序有一个致命缺点,就是当待排序数组有序或接近有序时,选取关键字并排序后需要将待排序数组分割为两个区间,而有序或接近有序的数字分割时,并不能实现相对的平分,使得一个区间占据了一两个元素,而另外一个区间占据相对很多的数据,导致每一次排序几乎都是O(n的递减),最终快速排序的时间复杂度变为O(n^2)

三数取中的实现是取区间最左边、最右边和中间的三个元素中中间大小的元素,将该元素与待排序数组最左边元素交换,使得所取关键字的大小不是最大或最小,使得分割区间时,两个区间元素数量不是最差的情况(即一个区间占一个元素,而另外一个区间占其他元素),能有效的降低待排序数组为有序或接近有序时,快速排序的时间复杂度。

下面实现了三数取中的代码,并且在hoare版本中的使用,三数取中在上面三个方法使用的方法相同,在下面非递归版本中也会使用到。
在这里插入图片描述

在这里插入图片描述

int GetMidIndex(int* a, int left, int right)
{int mid = (left + right) / 2;if (a[left] < a[mid]){if (a[mid] < a[right]){return mid;}else if (a[left] > a[right]){return left;}else //(a[left] <= a[right]){return right;}}else //(a[left] > a[mid]){if (a[mid] > a[right]){return mid;}else if (a[left] > a[right]){return right;}else{return left;}}
}// 快速排序hoare版本三数取中优化
int PartSort1(int* a, int left, int right)
{int keyi = GetMidIndex(a, left, right);Swap(&a[keyi], &a[left]);keyi = left;while (left < right){while (left < right && a[right] >= a[keyi]){right--;}while (left < right && a[left] <= a[keyi]){left++;}Swap(&a[left], &a[right]);}Swap(&a[left], &a[keyi]);return right;
}
3.2.2.5 非递归版本

快速排序的非递归版本需要使用栈,而C语言的库中并没有关于栈的数据结构,那么这里需要自己实现栈。递归和非递归版本的内部逻辑相同,都是使用上面三个排序思路。

递归版本在向下传递参数时,每一层都有单独的两个变量存储待排序区间的范围,而快速排序非递归版本没有这个能力,那么非递归版本需要通过栈来存储待排序数组的左右两边的下标,将待排序数组排序后,记录关键字的下班,将待排序数组分割为两个区间,并将两个区间范围的下标入栈,形成两个新的待排序数组,重复上面的操作,取出待排序数组的范围进行下一次排序,直到栈不为空。若栈不为空,则说明还有区间未被排序,取出栈中待排序数组的范围继续排序。栈为空时,说明所有区间都排序结束,则快速排序完成。

QuickSortNonR.c 文件的实现
#include <stdio.h>
#include "Stack.h"// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{Stack st;StackInit(&st);int end = right;int begin = left;StackPush(&st, end);StackPush(&st, begin);while (!StackEmpty(&st)){begin = StackTop(&st);StackPop(&st);end = StackTop(&st);StackPop(&st);int keyi = PartSort1(a, begin, end);//[begin , keyi - 1] [keyi] [keyi + 1 , end]if (end > keyi + 1){StackPush(&st, end);StackPush(&st, keyi + 1);}if (keyi - 1 > begin){StackPush(&st, keyi - 1);StackPush(&st, begin);}}
}
stack.h 文件的实现
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>// 支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{STDataType* a;int top;		// 栈顶int capacity;  // 容量 
}Stack;
// 初始化栈 
void StackInit(Stack* ps);
// 入栈 
void StackPush(Stack* ps, STDataType data);
// 出栈 
void StackPop(Stack* ps);
// 获取栈顶元素 
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数 
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps);
// 销毁栈 
void StackDestroy(Stack* ps);
stack.c 文件的实现
#include "Stack.h"
// 初始化栈 
void StackInit(Stack* ps)
{assert(ps);ps->capacity = 0;ps->top = 0;     //top指向栈顶的后面一个元素ps->a = NULL;
}void StackFull(Stack* ps)
{assert(ps);int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;STDataType* tmp = (STDataType*)realloc(ps->a, sizeof(STDataType) * newcapacity);if (tmp == NULL){perror("realloc");return;}ps->a = tmp;ps->capacity = newcapacity;
}// 入栈 
void StackPush(Stack* ps, STDataType data)
{assert(ps);if (ps->capacity == ps->top)StackFull(ps);ps->a[ps->top] = data;ps->top++;
}// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps)
{assert(ps);return ps->top == 0;
}// 出栈 
void StackPop(Stack* ps)
{assert(ps);assert(!StackEmpty(ps));//栈为空,则不能继续出栈ps->top--;
}// 获取栈顶元素 
STDataType StackTop(Stack* ps)
{assert(ps);assert(!StackEmpty(ps));//栈为空,则无栈顶元素return ps->a[ps->top - 1];
}// 获取栈中有效元素个数 
int StackSize(Stack* ps)
{assert(ps);return ps->top;   //由于top是指向栈顶元素的下一个位置		//而元素个数正好是下标 + 1 ,也就是top
}// 销毁栈 
void StackDestroy(Stack* ps)
{assert(ps);free(ps->a);ps->a = NULL;ps->capacity = 0;ps->top = 0;
}
3.2.3 快速排序的时间复杂度

快速排序的时间复杂度如果每次都平分两个待排序数组时为O(n*logn --> n * 以二为底n的对数) ,而在最坏的情况下时间复杂度为O(n^2),但是在三数取中的优化下,快速排序不会变为最坏的情况,一般时间复杂度为O(n*logn)
在这里插入图片描述

3.2.4 快速排序的总结
  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度:O(n*logn)
  3. 空间复杂度:O(logn)
  4. 稳定性:不稳定

4. 归并排序

4.1 归并排序的基本思想:

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
在这里插入图片描述

在这里插入图片描述

4.2 归并排序的代码实现

需要注意的是归并排序的边界问题,归并排序每次将两个有序子序列合并为一个有序序列,但是这两个子序列并不一定都存在或是两个子序列都存在,但是两个子序列的元素个数并不相同。使用变量begin1end1记录第一个子序列的区间范围,使用变量begin2end2记录第二个子序列的区间范围,那么越界情况可以分为下面三种情况:

  1. end2 越界
    在这里插入图片描述

  2. begin2和end2越界
    在这里插入图片描述

  3. end1、begin2和end2越界
    在这里插入图片描述

所以实现归并排序需要很好的处理好边界问题。

4.2.1 归并排序的递归版本实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>// 归并排序递归实现
void _MergeSort(int* a, int left, int right, int* tmp)
{int mid = (left + right) / 2;//[left , mid] [mid + 1 , right]if (mid > left)_MergeSort(a, left, mid, tmp);if (right > mid + 1)_MergeSort(a, mid + 1, right, tmp);int i = left;int begin1 = left, end1 = mid;int begin2 = mid + 1, end2 = right;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2]){tmp[i++] = a[begin1++];}else{tmp[i++] = a[begin2++];}}while (begin1 <= end1){tmp[i++] = a[begin1++];}while (begin2 <= end2){tmp[i++] = a[begin2++];}memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}void MergeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);_MergeSort(a, 0, n - 1, tmp);free(tmp);
}
4.2.2 归并排序的非递归版本实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>//归并排序非递归实现
void MergeSortNonR(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc");return;}int gap = 1;while (gap < n){int j = 0;for (int i = 0; i < n; i += 2 * gap){int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;//三种越界情况//end1 begin2 end3 都越界//begin2 end3 越界//end3 越界if (end1 >= n || begin2 >= n){break;}if (end2 >= n){end2 = n - 1;}while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2]){tmp[j++] = a[begin1++];}else{tmp[j++] = a[begin2++];}}while (begin1 <= end1){tmp[j++] = a[begin1++];}while (begin2 <= end2){tmp[j++] = a[begin2++];}//这里需要归并一段,拷贝一段,而不是整段拷贝//整段拷贝会使原数组中后面没有归并的元素被覆盖memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));}gap *= 2;}free(tmp);
}

4.3 归并排序的总结:

  1. 归并的缺点在于需要O(n)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  2. 时间复杂度:O(n*logn)
  3. 空间复杂度:O(n)
  4. 稳定性:稳定

5. 计数排序(非比较函数)

5.1 计数排序的思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:

  1. 统计相同元素出现次数
  2. 根据统计的结果将序列回收到原来的序列中

在这里插入图片描述

5.2 计数排序的代码实现

#include <stdio.h>
#include <stdlib.h>// 计数排序
void CountSort(int* a, int n)
{int max = a[0];int min = a[0];for (int i = 0; i < n; i++){if (max < a[i]){max = a[i];}if (min > a[i]){min = a[i];}}int range = max - min + 1;int* CountArr = (int*)calloc(range, sizeof(int));if (CountArr == NULL){perror("malloc");return;}for (int i = 0; i < n; i++){CountArr[a[i] - min]++;}int num = 0;for (int j = 0; j < range; j++){while (CountArr[j]--){a[num++] = j + min;}}
}

5.3 计数排序的总结:

  1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
  2. 时间复杂度:O(MAX(n,范围))
  3. 空间复杂度:O(范围)

三、排序算法复杂度及稳定性分析

在这里插入图片描述

在这里插入图片描述

结尾

如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
如果这篇文章对你有用的话,希望大家给一个三连支持一下!!🌹🌹
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/2774940.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

如何有效避免交易贵金属爆仓的一些建议

在交易贵金属的市场中&#xff0c;爆仓是一个令人恐惧的场景。许多交易者在面临巨额亏损时无法承受并被迫平仓&#xff0c;导致资金损失甚至破产。为了帮助您规避这种风险&#xff0c;下面将提供一些有效的建议&#xff0c;帮助您在交易贵金属时避免爆仓。 第一&#xff0c;了解…

C++力扣题目377--组合求和VI 爬楼梯进阶版 322--零钱兑换 279完全平方数

377. 组合总和 Ⅳ 力扣题目链接(opens new window) 难度&#xff1a;中等 给定一个由正整数组成且不存在重复数字的数组&#xff0c;找出和为给定目标正整数的组合的个数。 示例: nums [1, 2, 3]target 4 所有可能的组合为&#xff1a; (1, 1, 1, 1) (1, 1, 2) (1, 2, …

计算机网络基础 第三章——数据链路层层 知识点(持续更新)

3.1差错产生的原因及差错控制方法 1.差错产生的原因及差错控制方法 (1)当数据信号从发送端出发经过物理线路时,由于物理线路存在着噪声,因此数据信号通 过物理线路传输到接收端时,接收信号必然是数据信号与噪声信号电平的叠加。在接收端 接收电路在取样时对叠加后的信号进行判…

JavaScript相关(一)——作用域

本篇将从JS的执行上下文开始&#xff0c;去理解&#xff1a;变量提升、 栈式调用、作用域和闭包。 参考&#xff1a; 浏览器工作原理与实践 JS执行上下文 执行上下文是 JavaScript 执行一段代码时的运行环境&#xff0c;比如调用一个函数&#xff0c;就会生成这个函数的执行…

发廊理发店微信小程序展示下单前端静态模板源码

模板描述&#xff1a;剪发小程序前端源码&#xff0c;一共五个页面&#xff0c;包括店铺、理发师、订单、我的等页面 注&#xff1a;该源码是前端静态模板源码&#xff0c;没有后台和API接口

个人博客说明

本人博客主要发布平台为博客园 https://www.cnblogs.com/carmi 更多详细&#xff0c;完整图片的文章还请师傅们动动小手到博客园去看吧。

STM32/C51开发环境搭建(KeilV5安装)

Keil C51是美国Keil Software公司出品的51系列兼容单片机C语言软件开发系统&#xff0c;与汇编相比&#xff0c;C语言在功能上、结构性、可读性、可维护性上有明显的优势&#xff0c;因而易学易用。Keil提供了包括C编译器、宏汇编、链接器、库管理和一个功能强大的仿真调试器等…

降准是什么意思?降准对股市有哪些影响?

降准是什么意思 降准&#xff0c;全称为“中央银行调低法定存款准备率”&#xff0c;是指中央银行降低法定存款准备率&#xff0c;以增加银行的可用资金&#xff0c;从而增加市场的流动性。 具体来说&#xff0c;存款准备金是商业银行为了应对储户取款和清算时准备的资金&…

java异常类

目录 异常 编译时异常 运行时异常 异常的抛出&#xff1a;throw 异常的声明&#xff1a;throws try-catch捕获并处理&#xff1a; finally 自定义异常类 异常处理流程总结 异常 当程序出现异常之后&#xff0c;将不会执行异常之后的代码 1. Throwable&#xff1a;是异…

Java基础常见面试题总结-并发(二)

volatile底层原理 volatile是轻量级的同步机制&#xff0c;volatile保证变量对所有线程的可见性&#xff0c;不保证原子性。 当对volatile变量进行写操作的时候&#xff0c;JVM会向处理器发送一条LOCK前缀的指令&#xff0c;将该变量所在缓存行的数据写回系统内存。由于缓存一…

【Jave EE】----SpringBoot配置文件

1.配置文件的作用 数据库的连接信息&#xff08;包含⽤户名和密码的设置&#xff09;项⽬的启动端⼝ 第三⽅系统的调⽤秘钥等信息 ⽤于发现和定位问题的普通⽇志和异常⽇志 2.SpringBoot的配置文件分类 系统使用的配置文件&#xff0c;如端口号的设置&#xff0c;连接数据库的配…

Java项目:18 基于SpringBoot的学生成绩管理系统

作者主页&#xff1a;舒克日记 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 基于springboot的学生成绩管理系统主要功能 分为两个端&#xff0c;教师和学生 教师的主要功能&#xff1a;学生信息、成绩信息的增删改查 学生的主要…

[职场] 服务行业个人简历 #笔记#笔记

服务行业个人简历 服务员个人简历范文1 姓名: XXX国籍:中国 目前所在地:天河区民族:汉族 户口所在地:阳江身材: 160cm43kg 婚姻状况:未婚年龄: 21岁 培训认证:诚信徽章: 求职意向及工作经历 人才类型:普通求职 应聘职位: 工作年限:职称:初级 求职类型:全职可到职日期:随时 月薪…

bpmn.js一个基于Bpmn 2.0的前端工作流展示和绘制工具

bpmn.js是由开源工作流引擎camunda内部组织BPMN.IO组织开发的一款基于BPMN 2.0的工作流展示、编辑的web端工具库。由于工作流引擎activiti、flowable、camunda属于同宗分流&#xff0c;其工作流定义格式大致相同&#xff0c;所以我们可以使用bpmn.js完美融合其中任一工作流引擎…

无广告iOS获取设备UDID 简单方便快捷

ps&#xff1a; 为啥不用蒲公英了&#xff0c;就是因为有广告了&#xff0c;获取个UDID还安装游戏&#xff0c;真恶心?&#xff0c;所以找了新的获取UDID都方法&#xff0c;网页直接获取就可以&#xff0c;不会安装软件。 UDID 是一种 iOS 设备的特殊识别码。除序号之外&…

Redis中内存淘汰算法实现

Redis中内存淘汰算法实现 Redis的maxmemory支持的内存淘汰机制使得其成为一种有效的缓存方案&#xff0c;成为memcached的有效替代方案。 当内存达到maxmemory后&#xff0c;Redis会按照maxmemory-policy启动淘汰策略。 Redis 3.0中已有淘汰机制&#xff1a; noevictionall…

LLM之RAG实战(二十二)| LlamaIndex高级检索(一)构建完整基本RAG框架(包括RAG评估)

在RAG&#xff08;retrieval Augmented Generation&#xff0c;检索增强生成&#xff09;系统中&#xff0c;检索到文本的质量对大型语言模型生成响应的质量是非常重要的。检索到的与回答用户查询相关的文本质量越高&#xff0c;你的答案就越有根据和相关性&#xff0c;也更容易…

一文读懂:MybatisPlus从入门到进阶

快速入门 简介 在项目开发中&#xff0c;Mybatis已经为我们简化了代码编写。 但是我们仍需要编写很多单表CURD语句&#xff0c;MybatisPlus可以进一步简化Mybatis。 MybatisPlus官方文档&#xff1a;https://www.baomidou.com/&#xff0c;感谢苞米豆和黑马程序员。 Mybat…

io三个练习:

练习一&#xff1a; 使用 四种方式拷贝文件&#xff0c;并统计各自用时 1字节流的基本流&#xff1a;一次读写一个字节 2字节流的基本流&#xff1a;一次读写一个字节数组 3字节缓冲流&#xff1a;一次读写一个字节 4字节缓冲流&#xff1a;一次读写一个字节数组 public clas…

小区创业项目推荐:小投资大回报的店铺类型

作为一位拥有5年鲜奶吧创业经验的自媒体博主&#xff0c;我深知在小区内寻找一个既小投资又能带来大回报的创业项目是多么重要。今天&#xff0c;我要为大家推荐的&#xff0c;正是这样一个项目——鲜奶吧。 一、鲜奶吧&#xff1a;小区内的健康食品新宠 随着健康饮食观念的深…