【第七节】C/C++排序算法

目录

前言

一、冒泡排序

二、选择排序

三、插入排序

四、希尔排序

五、归并排序

六、快速排序

七、 堆排序

八、计数排序

九、桶排序

十、基数排序


前言

        排序算法可以大致分为两大类:比较类排序和非比较类排序。以下是这两大类中一些常见的排序算法示例:

比较类排序(非线性时间比较类排序)

  1. 冒泡排序(Bubble Sort):通过重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

  2. 选择排序(Selection Sort):首先在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

  3. 插入排序(Insertion Sort):将数组分为已排序和未排序两部分,初始时,已排序部分只包含一个元素,其余的元素都在未排序部分。然后,每次从未排序部分取出第一个元素,在已排序部分找到相应的位置并插入,直到所有元素均排序完毕。

  4. 希尔排序(Shell Sort):是插入排序的一种又称“缩小增量排序”,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。

  5. 归并排序(Merge Sort):将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

  6. 堆排序(Heap Sort):是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。

  7. 快速排序(Quick Sort):通过一次排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

  8. 计数排序(Counting Sort)(虽然它通常被认为是非比较类排序,但在某些情况下,如当输入数据范围较小时,它可以作为比较类排序的替代):不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

非比较类排序(线性时间非比较类排序)

  1. 基数排序(Radix Sort):按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

  2. 桶排序(Bucket Sort):是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:首先,要使得数据分散得尽可能均匀;其次,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

  3. 计数排序(Counting Sort)(如上面提到的,虽然有时作为比较类排序的替代,但在更严格的分类中它属于非比较类排序):适用于待排序的值域不大且可以预估的情况。它的基本原理是将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

        请注意,计数排序在上述分类中被提及了两次,因为它既可以作为比较类排序(当输入数据范围较大时),也可以作为非比较类排序(当输入数据范围较小时)。但在更严格的分类中,它通常被视为非比较类排序。

他们各自算法复杂度如下:

算法其中相关概念说明:

稳定与不稳定

        在排序算法中,稳定性是一个重要的属性,它描述了在排序过程中具有相同值的元素之间的相对位置是否保持不变。

  • 稳定:如果待排序的序列中存在两个或两个以上关键字相等的元素,排序后这些元素的相对次序保持不变,则称该排序方法是稳定的。换句话说,如果a原本在b的前面,并且a和b的值相等(即a=b),那么经过稳定的排序算法处理后,a仍然会在b的前面。

  • 不稳定:如果待排序的序列中存在两个或两个以上关键字相等的元素,经过排序后,这些元素的相对次序发生变化,则称该排序方法是不稳定的。也就是说,如果a原本在b的前面,并且a和b的值相等(即a=b),那么经过不稳定的排序算法处理后,a可能会出现在b的后面。

时间复杂度

        时间复杂度是评估算法执行效率的一个重要指标,它描述的是当数据规模增大时,算法所需时间的增长趋势。通常用O()表示,并基于算法中基本操作(如比较、交换等)的执行次数来估算。

  • 常见的时间复杂度
    • O(1):常数时间复杂度,表示算法的执行时间不受数据规模n的影响。
    • O(logn):对数时间复杂度,表示算法的执行时间与log(n)成正比。
    • O(n):线性时间复杂度,表示算法的执行时间与数据规模n成正比。
    • O(n^2):平方时间复杂度,表示算法的执行时间与n的平方成正比。
    • O(nlogn):介于线性与平方之间的一种时间复杂度,常见于一些高效的排序算法。

空间复杂度

        空间复杂度是评估算法所需额外存储空间的一个指标,它描述的是算法在计算机内执行时所需存储空间的度量,并通常以数据规模n的函数形式来表示。

  • 常见的空间复杂度
    • O(1):常数空间复杂度,表示算法所需额外存储空间不随数据规模n的变化而变化。
    • O(n):线性空间复杂度,表示算法所需额外存储空间与数据规模n成正比。
    • O(n^2):平方空间复杂度,表示算法所需额外存储空间与n的平方成正比。
    • 空间复杂度还可能涉及对数空间、指数空间等,但在实际应用中相对较少见。

        需要注意的是,空间复杂度和时间复杂度在评估算法时都很重要,但在实际应用中往往需要根据具体问题和需求来权衡两者的关系。

一、冒泡排序

        冒泡排序是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误(即前一个元素比后一个元素大,在升序排序中)就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

        这个算法的名字“冒泡排序”非常形象地描述了算法的过程:越小的元素会经由交换慢慢“浮”到数列的顶端(或越大的元素“沉”到底部,这取决于排序是升序还是降序)。

1.1 算法描述

1)比较相邻的元素。如果第一个比第二个大,就交换它们两个;
2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
3)针对所有的元素重复以上的步骤,除了最后一个;
4)重复步骤1~3,直到排序完成。

1.2 动图演示

1.3 C语言代码实现

#include <stdio.h>// 冒泡排序函数
void bubbleSort(int arr[], int n) {int i, j, temp;for (i = 0; i < n - 1; i++) { // 外层循环控制需要排序的轮数for (j = 0; j < n - i - 1; j++) { // 内层循环控制每一轮的比较次数if (arr[j] > arr[j + 1]) { // 如果前一个元素大于后一个元素,则交换temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}}}
}// 打印数组
void printArray(int arr[], int size) {int i;for (i = 0; i < size; i++)printf("%d ", arr[i]);printf("\n");
}int main() {int arr[] = {64, 34, 25, 12, 22, 11, 90};int n = sizeof(arr) / sizeof(arr[0]);printf("原始数组: ");printArray(arr, n);bubbleSort(arr, n);printf("排序后的数组: ");printArray(arr, n);return 0;
}

二、选择排序

2.1 算法描述

        选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理如下:

  1. 从未排序的序列中找出最小(或最大)的元素,存放到排序序列的起始位置。
  2. 从剩余未排序的元素中继续寻找最小(或最大)的元素,然后将其放到已排序序列的末尾。
  3. 重复步骤2,直到所有元素都排序完毕。

        这种排序方法通过不断选择剩余未排序元素中的最小(或最大)元素,逐步构建有序序列,直到整个序列有序。

2.2 动图演示

2.3 代码演示

#include <stdio.h>// 选择排序函数
void selectionSort(int arr[], int n) {int i, j, min_idx;for (i = 0; i < n - 1; i++) {// 假设当前位置是最小值min_idx = i;for (j = i + 1; j < n; j++) {// 如果找到更小的值,则更新最小值的索引if (arr[j] < arr[min_idx]) {min_idx = j;}}// 将找到的最小值与当前位置的值交换if (min_idx != i) {int temp = arr[min_idx];arr[min_idx] = arr[i];arr[i] = temp;}}
}// 打印数组
void printArray(int arr[], int size) {int i;for (i = 0; i < size; i++)printf("%d ", arr[i]);printf("\n");
}int main() {int arr[] = {64, 34, 25, 12, 22, 11, 90};int n = sizeof(arr) / sizeof(arr[0]);printf("原始数组: ");printArray(arr, n);selectionSort(arr, n);printf("排序后的数组: ");printArray(arr, n);return 0;
}

        选择排序(Selection Sort)是一种简单直观的排序算法,其稳定性表现在其性能不受输入数据分布的影响,始终保持O(n^2)的时间复杂度。这意味着无论输入数据是已经排序、逆序还是随机分布,选择排序都需要进行相同数量的比较和交换操作。因此,在实际应用中,选择排序更适合处理小规模数据集,因为随着数据规模的增大,其效率会显著下降。

        选择排序的主要优点是它的空间复杂度为O(1),即它不需要额外的存储空间来存储临时数据,所有的操作都在原始数组上进行,这使得它在内存受限的环境中成为一个可行的选择。

        从理论上讲,选择排序因其简单性,可能是非专业人士在需要手动排序时最容易想到的方法之一。它的基本思想是:在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置,然后再从剩余未排序元素中继续寻找最小(或最大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

        总结来说,选择排序的稳定性和简单性是其主要特点,尽管它在处理大规模数据时效率不高,但在小规模数据或内存受限的情况下,它是一个实用的选择。

三、插入排序

        插入排序(Insertion-Sort)的算法描述简洁明了,它是一种简单直观的排序方法。其工作原理基于构建有序序列的思想,对于每一个未排序的元素,它会在已排序的序列中从后向前进行扫描,直到找到该元素应该插入的位置,并将其插入。这个过程会持续进行,直到所有的元素都被插入到有序序列中,从而实现整个数组的排序。

3.1 算法描述

        插入排序是一种通常采用in-place方式在数组上实现的排序算法。以下是其具体算法描述:

1)首先,我们假设数组的第一个元素是已排序的。

2)接着,我们取出数组中的下一个元素,并开始在已排序的元素序列中从后向前进行扫描。

3)在扫描过程中,如果当前已排序的元素大于新取出的元素,我们就将该已排序的元素向后移动一个位置,以便为新元素腾出空间。

4)我们持续重复上述步骤,直到找到已排序的元素序列中小于或等于新元素的位置。

5)一旦找到这样的位置,我们就将新元素插入到该位置之后。

        然后,我们继续取出数组中的下一个元素,并重复上述步骤2到步骤5,直到数组中的所有元素都完成排序。

3.2 动图演示

3.3 代码演示

#include <stdio.h>// 插入排序函数
void insertionSort(int arr[], int n) {int i, key, j;for (i = 1; i < n; i++) {key = arr[i]; // 将当前元素设为keyj = i - 1;// 将大于key的元素向后移动while (j >= 0 && arr[j] > key) {arr[j + 1] = arr[j];j = j - 1;}arr[j + 1] = key; // 插入key到正确的位置}
}// 打印数组
void printArray(int arr[], int size) {int i;for (i = 0; i < size; i++)printf("%d ", arr[i]);printf("\n");
}int main() {int arr[] = {12, 11, 13, 5, 6};int n = sizeof(arr) / sizeof(arr[0]);printf("原始数组: ");printArray(arr, n);insertionSort(arr, n);printf("排序后的数组: ");printArray(arr, n);return 0;
}

        插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

四、希尔排序

        希尔排序(Shell Sort)是由Donald Shell在1959年提出的一种排序算法,它是简单插入排序的一种改进版,也是第一个在平均时间复杂度上突破O(n^2)的排序算法。与插入排序的主要区别在于,希尔排序在比较元素时不是逐个地比较,而是采用增量序列,使每个子序列尽可能接近有序,然后逐渐缩小增量,直至增量为1,此时整个序列已经基本有序,再进行一次直接插入排序即可。由于这种排序算法在比较时优先比较距离较远的元素,因此有时也被称为缩小增量排序。

4.1 算法描述

        先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  • 按增量序列个数k,对序列进行k 趟排序;
  • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

4.2 动图演示

4.3 代码演示

#include <stdio.h>// 交换两个元素
void swap(int* a, int* b) {int t = *a;*a = *b;*b = t;
}// 希尔排序
void shellSort(int arr[], int n) {int gap, i, j, temp;for (gap = n / 2; gap > 0; gap /= 2) { // 增量序列可以是 n/2, n/4, ... , 1for (i = gap; i < n; i++) { // 对每个子序列进行插入排序temp = arr[i];for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {arr[j] = arr[j - gap];}arr[j] = temp;}}
}// 打印数组
void printArray(int arr[], int size) {int i;for (i = 0; i < size; i++)printf("%d ", arr[i]);printf("\n");
}int main() {int arr[] = {12, 34, 54, 2, 3};int n = sizeof(arr) / sizeof(arr[0]);printf("原始数组: ");printArray(arr, n);shellSort(arr, n);printf("排序后的数组: ");printArray(arr, n);return 0;
}

        希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。

五、归并排序

        归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

5.1 算法描述

  • 把长度为n的输入序列分成两个长度为n/2的子序列;
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列。

5.2 动图演示

5.3 代码实现

#include <stdio.h>// 合并两个已排序的子数组
void merge(int arr[], int left, int mid, int right) {int n1 = mid - left + 1;int n2 = right - mid;/* 创建临时数组 */int L[n1], R[n2];/* 拷贝数据到临时数组 L[] 和 R[] */for (int i = 0; i < n1; i++)L[i] = arr[left + i];for (int j = 0; j < n2; j++)R[j] = arr[mid + 1 + j];/* 合并临时数组到 arr[left..right] */int i = 0, j = 0, k = left;while (i < n1 && j < n2) {if (L[i] <= R[j]) {arr[k] = L[i];i++;} else {arr[k] = R[j];j++;}k++;}/* 拷贝 L[] 的剩余元素到 arr */while (i < n1) {arr[k] = L[i];i++;k++;}/* 拷贝 R[] 的剩余元素到 arr */while (j < n2) {arr[k] = R[j];j++;k++;}
}// 归并排序函数
void mergeSort(int arr[], int left, int right) {if (left < right) {// 找到中间索引int mid = left + (right - left) / 2;// 对左半部分进行归并排序mergeSort(arr, left, mid);// 对右半部分进行归并排序mergeSort(arr, mid + 1, right);// 合并两个已排序的子数组merge(arr, left, mid, right);}
}// 测试归并排序
int main() {int arr[] = {12, 11, 13, 5, 6, 7};int arr_size = sizeof(arr) / sizeof(arr[0]);printf("原始数组: \n");for (int i = 0; i < arr_size; i++)printf("%d ", arr[i]);printf("\n");// 调用归并排序函数mergeSort(arr, 0, arr_size - 1);printf("排序后的数组: \n");for (int i = 0; i < arr_size; i++)printf("%d ", arr[i]);printf("\n");return 0;
}

        归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。

六、快速排序

        快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

6.1 算法描述:

        快速排序是一种基于分治策略的排序算法,它通过将待排序的列表划分为两个子列表来实现。算法的主要步骤如下:

  1. 选取一个元素作为“基准”(pivot)。
  2. 通过一趟排序,将待排序的列表划分为两部分:所有比基准小的元素都移到基准的左边,所有比基准大的元素都移到基准的右边(与基准相等的元素可以放在任一边)。这个过程称为“分区”(partition)操作,完成后基准元素处于其最终排序位置。
  3. 递归地对基准左边和右边的两个子列表进行快速排序。

6.2 动图演示

6.3 代码实现

#include <stdio.h>// 交换数组中两个元素的位置
void swap(int* a, int* b) {int t = *a;*a = *b;*b = t;
}// 快速排序的分区函数
int partition(int arr[], int low, int high) {int pivot = arr[high];    // 选择最右边的元素作为基准int i = (low - 1);  // 小于基准的元素的索引for (int j = low; j <= high - 1; j++) {// 如果当前元素小于或等于基准if (arr[j] <= pivot) {i++;    // 递增索引swap(&arr[i], &arr[j]);}}swap(&arr[i + 1], &arr[high]);return (i + 1);
}// 快速排序函数
void quickSort(int arr[], int low, int high) {if (low < high) {/* pi 是分区后基准元素的索引 */int pi = partition(arr, low, high);// 分别对基准元素左右两侧的子数组进行递归排序quickSort(arr, low, pi - 1);quickSort(arr, pi + 1, high);}
}// 测试快速排序函数
int main() {int arr[] = {10, 7, 8, 9, 1, 5};int n = sizeof(arr) / sizeof(arr[0]);quickSort(arr, 0, n - 1);printf("Sorted array: \n");for (int i = 0; i < n; i++)printf("%d ", arr[i]);printf("\n");return 0;
}

七、 堆排序

        堆排序(Heapsort)是一种基于堆数据结构的排序算法。堆是一种近似完全二叉树的结构,其特点在于它满足堆的性质:对于任意节点,其值总是大于或等于(最大堆)或小于或等于(最小堆)其子节点的值。通过构建这种特殊的二叉树,并在排序过程中保持堆的性质,堆排序能高效地实现数组的排序。

7.1 算法描述

  • 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

7.2 动图演示

7.3 代码实现

#include <stdio.h>// 交换两个元素
void swap(int* a, int* b) {int t = *a;*a = *b;*b = t;
}// 调整堆
void heapify(int arr[], int n, int i) {int largest = i; // 初始化largest为根int left = 2 * i + 1; // 左子节点int right = 2 * i + 2; // 右子节点// 如果左子节点大于根if (left < n && arr[left] > arr[largest])largest = left;// 如果右子节点大于目前已知的最大if (right < n && arr[right] > arr[largest])largest = right;// 如果最大元素不是根if (largest != i) {swap(&arr[i], &arr[largest]);// 递归地调整受影响的子堆heapify(arr, n, largest);}
}// 堆排序函数
void heapSort(int arr[], int n) {// 构建大顶堆for (int i = n / 2 - 1; i >= 0; i--)heapify(arr, n, i);// 一个个从堆顶取出元素for (int i = n - 1; i >= 0; i--) {// 将当前最大的元素arr[0]和arr[i]交换swap(&arr[0], &arr[i]);// 重新调整剩余未排序元素为最大堆heapify(arr, i, 0);}
}// 测试堆排序
int main() {int arr[] = {12, 11, 13, 5, 6, 7};int n = sizeof(arr) / sizeof(arr[0]);heapSort(arr, n);printf("Sorted array is \n");for (int i = 0; i < n; ++i)printf("%d ", arr[i]);printf("\n");return 0;
}

八、计数排序

        计数排序(Counting Sort)是一种非基于比较的排序算法,它通过统计输入数据集中每个元素出现的次数,然后按照这些次数将元素放到输出序列的正确位置上。其核心思想是将输入的数据值转化为键(通常是整数),并将这些键作为索引来在额外开辟的数组空间中存储计数值。最后,根据这些计数值来确定原数组中每个元素在排序后数组中的位置。

        计数排序是一种线性时间复杂度的排序算法(在数据值范围不是很大的情况下),它的时间复杂度为O(n+k),其中n是待排序数组的长度,k是输入数据的范围大小。然而,计数排序要求输入的数据必须是有确定范围的整数,并且该范围不宜过大,否则将消耗大量的额外空间。

        在实际应用中,如果数据范围很大或者数据不是整数,计数排序可能不是最佳选择。但对于一些具有固定范围的小整数数组,计数排序可以非常高效地完成任务。

8.1 算法描述

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

8.2 动图演示

8.3 代码演示

#include <stdio.h>
#include <stdlib.h>void countingSort(int arr[], int n, int max) {int *count = (int *)malloc((max + 1) * sizeof(int));int *output = (int *)malloc(n * sizeof(int));// 初始化计数数组for (int i = 0; i <= max; i++) {count[i] = 0;}// 统计每个元素的出现次数for (int i = 0; i < n; i++) {count[arr[i]]++;}// 计算每个元素在排序后数组中的位置for (int i = 1; i <= max; i++) {count[i] += count[i - 1];}// 构建输出数组for (int i = n - 1; i >= 0; i--) {output[count[arr[i]] - 1] = arr[i];count[arr[i]]--;}// 将输出数组复制回原数组for (int i = 0; i < n; i++) {arr[i] = output[i];}// 释放动态分配的内存free(count);free(output);
}int main() {int arr[] = {4, 2, 2, 8, 3, 3, 1};int n = sizeof(arr) / sizeof(arr[0]);int max = 8; // 假设已知最大值为8countingSort(arr, n, max);printf("Sorted array: \n");for (int i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");return 0;
}

        计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。

九、桶排序

        桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

9.1 算法描述

  • 设置一个定量的数组当作空桶;
  • 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  • 对每个不是空的桶进行排序;
  • 从不是空的桶里把排好序的数据拼接起来。

9.2 动图演示

9.3 代码实现

#include <stdio.h>
#include <stdlib.h>// 插入排序,用于桶内排序
void insertionSort(int arr[], int n) {for (int i = 1; i < n; i++) {int key = arr[i];int j = i - 1;while (j >= 0 && arr[j] > key) {arr[j + 1] = arr[j];j = j - 1;}arr[j + 1] = key;}
}// 桶排序
void bucketSort(int arr[], int n) {// 找到数组中的最大值和最小值int minValue = arr[0];int maxValue = arr[0];for (int i = 1; i < n; i++) {if (arr[i] < minValue)minValue = arr[i];if (arr[i] > maxValue)maxValue = arr[i];}// 桶的个数int bucketSize = (maxValue - minValue) / n + 1;if (bucketSize == 0)bucketSize = 1;// 创建桶int bucketCount = (maxValue - minValue) / bucketSize + 1;int *buckets[bucketCount];for (int i = 0; i < bucketCount; i++) {buckets[i] = (int *)malloc(sizeof(int) * n);buckets[i][0] = 0; // 桶内元素计数器}// 将数据放到各个桶中for (int i = 0; i < n; i++) {int index = (arr[i] - minValue) / bucketSize;buckets[index][buckets[index][0]++] = arr[i];}// 对每个桶进行排序for (int i = 0; i < bucketCount; i++) {insertionSort(buckets[i] + 1, buckets[i][0]);}// 将桶中的数据放回原数组int index = 0;for (int i = 0; i < bucketCount; i++) {for (int j = 1; j <= buckets[i][0]; j++) {arr[index++] = buckets[i][j];}free(buckets[i]); // 释放桶的内存}
}// 测试桶排序
int main() {int arr[] = {0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68};int n = sizeof(arr) / sizeof(arr[0]);bucketSort(arr, n);printf("Sorted array is \n");for (int i = 0; i < n; i++)printf("%.2f ", arr[i]);printf("\n");return 0;
}// 注意:由于示例代码中的arr是浮点数,但桶排序更适合整数,所以这里假设我们实际上是对整数进行排序
// 如果要排序浮点数,需要更复杂的策略来确定桶的大小和数量

        桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。 

十、基数排序

        基数排序是一种分步进行的排序算法,它首先根据数据的最低有效位进行排序,然后进行收集;接着根据次低有效位进行排序,再次收集;这一过程持续进行,直到处理完最高有效位。在某些情况下,数据的属性具有不同的优先级,基数排序会先按照较低优先级的属性进行排序,然后按照较高优先级的属性进行排序。最终的排序结果是,高优先级的数据项排在前面,如果高优先级相同,则低优先级较高的数据项排在前面。

10.1 算法描述

  • 取得数组中的最大数,并取得位数;
  • arr为原始数组,从最低位开始取每个位组成radix数组;
  • 对radix进行计数排序(利用计数排序适用于小范围数的特点);

10.2 动图演示

10.3 代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>// 获取数组中最大数的位数
int getMaxDigit(int arr[], int n) {int maxVal = arr[0];for (int i = 1; i < n; i++) {if (arr[i] > maxVal) {maxVal = arr[i];}}int digit = 0;while (maxVal) {maxVal /= 10;digit++;}return digit;
}// 基数排序函数
void radixSort(int arr[], int n) {int maxDigit = getMaxDigit(arr, n);int *output = (int *)malloc(n * sizeof(int));// 从最低位到最高位进行排序for (int exp = 1; exp <= maxDigit; exp *= 10) {int outputIndex = 0; // 用于指向output数组的索引// 使用计数排序按当前位数排序int counts[10] = {0}; // 10个桶for (int i = 0; i < n; i++) {int digit = (arr[i] / exp) % 10; // 获取当前位的数字counts[digit]++; // 计数}// 修改计数数组,以便将元素放入正确的位置for (int i = 1; i < 10; i++) {counts[i] += counts[i - 1];}// 构建输出数组for (int i = n - 1; i >= 0; i--) {int digit = (arr[i] / exp) % 10;output[counts[digit] - 1] = arr[i];counts[digit]--;}// 复制排序后的数组回原数组for (int i = 0; i < n; i++) {arr[i] = output[i];}}free(output); // 释放临时数组
}// 测试基数排序
int main() {int arr[] = {170, 45, 75, 90, 802, 24, 2, 66};int n = sizeof(arr) / sizeof(arr[0]);radixSort(arr, n);printf("Sorted array: \n");for (int i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");return 0;
}

        基数排序通过依次对数据的不同位进行排序和收集,确保了排序的稳定性。尽管如此,与桶排序相比,基数排序的效率稍低。每次对关键字进行桶分配需要O(n)的时间,而分配后形成新的关键字序列同样需要O(n)的时间。如果数据可以被划分为d个关键字,那么基数排序的总时间复杂度大约是O(d*2n),尽管d通常远小于n,这使得基数排序的时间复杂度接近线性。

        在空间使用方面,基数排序需要O(n+k)的额外空间,其中k代表桶的数目。由于通常情况下n远大于k,因此实际所需的额外空间大约为n个单位。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ChatGPT-4o医学应用、论文撰写、数据分析与可视化、机器学习建模、病例自动化处理、病情分析与诊断支持

2022年11月30日&#xff0c;可能将成为一个改变人类历史的日子——美国人工智能开发机构OpenAI推出了聊天机器人ChatGPT-3.5&#xff0c;将人工智能的发展推向了一个新的高度。2023年11月7日&#xff0c;OpenAI首届开发者大会被称为“科技界的春晚”&#xff0c;吸引了全球广大…

WPF布局控件

目录 Grid StackPanel WrapPanel DockPanel UniformGrid Canvas&InkCanvas Canvas InkCanvas Border Grid 属性 ShowGridLines&#xff1a;显示边线 ColumnDefinitions 列集合 表示有几列下面就写几个ColumnDefinition Width 宽&#xff1a;如果写具体数字则表…

科普文:一文搞懂jvm实战(一)Runtime实时监控jvm

概叙 Java Runtime 类是 Java 标准库中的关键类之一。它提供了对当前Java虚拟机&#xff08;JVM&#xff09;实例的访问和控制&#xff0c;允许程序动态地修改和管理运行时环境。 Java Runtime 是Java虚拟机&#xff08;JVM&#xff09;的一个实例&#xff0c;代表了正在执行Ja…

Spring Boot 实现 AOP 动态热插拔功能并附DEMO源码

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

python中的包和模块

目录 一、包与模块 二、第三方包的安装 2.1 pip install 2.2使用 curl 管道 2.3其他安装方法 三、导入单元的构成 3.1pip的使用 四、模块的缓存 一、包与模块 Python 中除了函数库以外&#xff0c;还有非常多且优秀的第三方库、包、模块。 模块Module&#xff1a;以…

【linux网络(七)】数据链路层详解

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:Linux从入门到精通⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学更多操作系统知识   &#x1f51d;&#x1f51d; Linux网络 1. 前言2. 认识MAC…

【计算机毕业设计】061互助学习微信小程序

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

北京网站建设怎么开始做

北京作为中国的首都&#xff0c;拥有众多的企业和机构&#xff0c;网站建设不仅是一种宣传和推广的手段&#xff0c;更是企业发展的必备工具。但是对于很多企业来说&#xff0c;网站建设是一个相对陌生的领域&#xff0c;不知道从哪里开始。今天我们就来谈一谈北京网站建设的步…

Dockerhub无法拉取镜像配置阿里镜像加速器

打开阿里镜像加速地址&#xff1a; https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors 根据平台类型按照对应方式进行配置&#xff1a;Dokcer Desktop是在右上角点开配置 找到Docker Engine 进行设置JSON结构&#xff1a; 记得要重启Docker服务才会生效&#xff01…

深度学习笔记: 最详尽解释预测系统的分类指标(精确率、召回率和 F1 值)

欢迎收藏Star我的Machine Learning Blog:https://github.com/purepisces/Wenqing-Machine_Learning_Blog。如果收藏star, 有问题可以随时与我交流, 谢谢大家&#xff01; 预测系统的分类指标(精确率、召回率和 F1 值) 简介 让我们来谈谈预测系统的分类指标以及对精确率、召回…

SpringSecurity6 | 基于数据库实现登录认证

SpringSecurity6 | 基于数据库认证 ✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: 循序渐进学SpringSecurity6 ✨特色专栏: MySQL学习 🥭本文内容: SpringSecurity6 | 基于数据库实现登…

数据资产的价值变现及管理规划(AMT企源)

从数据资源到数据资产之数据资产的价值变现及管理规划 题记 本文旨在探讨数据资产发展途径、数据产权及价值变现的服务流程和路径&#xff0c;并对数据资产管理平台框架、数据资产管理实施规划做出初步解读&#xff0c;以期为数据资产管理提供有益的思路和方案。 本次推出《从…

09 - Python图形用户界面和游戏开发

图形用户界面和游戏开发 基于tkinter模块的GUI GUI是图形用户界面的缩写&#xff0c;图形化的用户界面对使用过计算机的人来说应该都不陌生&#xff0c;在此也无需进行赘述。Python默认的GUI开发模块是tkinter&#xff08;在Python 3以前的版本中名为Tkinter&#xff09;&…

【linux】虚拟机安装 BCLinux-R8-U4-Server-x86_64

目录 一、概述 1.1移动云Linux系统订阅服务 CLS 1.2 大云天元操作系统BC-Linux 二、安装 一、概述 1.1移动云Linux系统订阅服务 CLS 移动云Linux系统订阅服务 CLS &#xff08;Cloud Linux Service&#xff09;为使用BC-Linux操作系统的用户提供标准维保服务以及高级技术支…

mysql-5.6.26-winx64免安装版本

mysql为什么要使用免安装 MySQL 提供免安装版本主要有以下几个原因和优势&#xff1a; 便捷性&#xff1a;用户无需经历安装过程&#xff0c;直接解压即可使用。这对于需要快速部署环境或者在不支持安装权限的系统上使用MySQL非常有用。灵活性&#xff1a;免安装版允许用户将…

Optional类方法

Optional类 简介方法empty()方法of(T value)ofNullable(T value)filter(Predicate<? super T> predicate)get()ifPresent(Consumer<? super T> consumer)isPresent()map(Function<? super T,? extends U> mapper)orElse(T other)orElseGet(Supplier<?…

LeetCode 子集

原题链接78. 子集 - 力扣&#xff08;LeetCode&#xff09; 这是一道暴力搜索问题参考大佬们的题解&#xff0c;对这类题目做出一下总结 1.确定递归参数变量 2.递归结束条件 3.做出选择&#xff0c;递归调用进入下一层 4.回溯&#xff0c;返回到递归前的状态 要完成前面这…

最新扣子(Coze)实战案例:图像流工具之创建一个精美的LOGO,完全免费教程

&#x1f9d9;‍♂️ 大家好&#xff0c;我是斜杠君&#xff0c;手把手教你搭建扣子AI应用。 &#x1f4dc; 本教程是《AI应用开发系列教程之扣子(Coze)实战教程》&#xff0c;完全免费学习。 &#x1f440; 关注斜杠君&#xff0c;可获取完整版教程。&#x1f44d;&#x1f3f…

商家团购app微信小程序模板

手机微信商家团购小程序页面&#xff0c;商家订餐外卖小程序前端模板下载。包含&#xff1a;团购主页、购物车订餐页面、我的订单、个人主页等。 商家团购app微信小程序模板

linux-内存映射MMAP-lseek-dup-fifo-通信-IO多路复用

1、内存映射MMap&#xff1a; DMA&#xff1a; 可以用*/[]取代read和write&#xff1b; 限制&#xff1a; 1、文件大小固定不能改变&#xff1b;&#xff08;ftruncate&#xff09; 2、只能是磁盘文件&#xff1b; 3、建立映射之前先open mmap函数&#xff1a; mmap第一个…