文章目录
- 一、前言
- 二、对比
- 1.排序算法相关概念
- 1.1 时间复杂度
- 1.2 空间复杂度
- 1.3 排序方式
- 1.4 稳定度
- 2.表格比较
- 3.算法推荐
- 3.1 小规模数据
- 3.2 中等规模数据
- 3.3 大规模数据
- 3.4 特殊需求
- 三、排序算法
- 1.冒泡排序(Bubble Sort)
- 1.1 简介
- 1.2 示例代码:
- 1.3 示例结果
- 2.选择排序(Selection Sort)
- 2.1 简介
- 2.2 示例代码:
- 2.3 示例结果
- 3.插入排序(Insertion Sort)
- 3.1 简介
- 3.2 示例代码:
- 3.3 示例结果
- 4.希尔排序(Hill sort)
- 4.1 简介
- 4.2 示例代码:
- 4.3 示例结果
- 5.归并排序(Merge Sort)
- 5.1 简介
- 5.2 示例代码:
- 5.3 示例结果
- 6.快速排序(Quick Sort)
- 6.1 简介
- 6.2 示例代码:
- 6.3 示例结果
- 7.堆排序(Heap Sort)
- 7.1 简介
- 7.2 示例代码:
- 7.3 示例结果
- 8.计数排序(Counting Sort)
- 8.1 简介
- 8.2 示例代码:
- 8.3 示例结果
- 9. 桶排序(Bucket Sort)
- 9.1 简介
- 9.2 示例代码:
- 9.3 示例结果
- 10.基数排序(Radix Sort)
- 10.1 简介
- 10.2 示例代码:
- 10.3 示例结果
- 四、总结
同系列的相关文章
十大经典排序算法之冒泡排序
十大经典排序算法之选择排序
十大经典排序算法之插入排序
十大经典排序算法之希尔排序
十大经典排序算法之归并排序
十大经典排序算法之快速排序
十大经典排序算法之堆排序
十大经典排序算法之计数排序
十大经典排序算法之桶排序
十大经典排序算法之基数排序
一、前言
大家好,最近我在菜鸟教程上发现了一篇非常详细介绍排序算法的文章,它对各种经典的排序算法进行了深入讲解,包括冒泡排序、插入排序、选择排序、快速排序等等。阅读这篇文章不仅能够帮助初学者更好地理解排序算法的原理和实现方法,也能够帮助有经验的开发者选择适合实际应用的排序算法。
- 冒泡排序(Bubble Sort)
- 选择排序(Selection Sort)
- 插入排序(Insertion Sort)
- 希尔排序(Hill sort)
- 归并排序(Merge Sort)
- 快速排序(Quick Sort)
- 堆排序(Heap Sort)
- 计数排序(Counting Sort)
- 桶排序(Bucket Sort)
- 基数排序(Radix Sort)
每个排序算法都有其独特的特点和适用场景,通过学习和理解这些算法,我们可以在不同的应用场景中选择最合适的排序算法。希望大家能够从这些排序算法中受益,并在编程实践中灵活运用它们。
二、对比
1.排序算法相关概念
1.1 时间复杂度
排序算法的时间复杂度是指用于衡量算法所需执行基本操作次数的一个函数。这个函数通常用大O记号来表示,它给出了最差情况下算法运行时间的上界。
- 平均情况:指在所有可能的输入数据分布情况下,排序算法所需要的时间。
- 最好情况:指在输入数据已经有序的情况下,排序算法所需要的时间。
- 最坏情况:指在输入数据完全逆序的情况下,排序算法所需要的时间。
1.2 空间复杂度
排序算法的空间复杂度是指在算法执行过程中所需要的额外的存储空间。这个额外的存储空间通常用来存储算法执行过程中的中间结果或辅助数据结构。
在排序算法中,空间复杂度是一个重要的性能指标。如果算法需要的额外存储空间很大,那么它可能会导致程序的运行速度变慢,或者在处理大规模数据时出现内存不足的情况。因此,对于排序算法来说,除了时间复杂度之外,空间复杂度也是需要考虑的因素。
具体来说,排序算法的空间复杂度可以分为原地排序和非原地排序两种情况:
- 原地排序:指算法执行过程中不需要额外的存储空间(或者只需要常数级别的额外存储空间)。例如,快速排序、堆排序和冒泡排序都是原地排序算法。
- 非原地排序:指算法执行过程中需要额外的存储空间,这个存储空间的大小通常与输入数据的规模有关。例如,归并排序和计数排序都是非原地排序算法。
需要注意的是,空间复杂度只考虑算法本身使用的额外存储空间,不包括输入数据本身的存储空间。因此,如果需要对输入数据进行复制或移动等操作,这些额外的存储空间也需要计入算法的空间复杂度中。
总的来说,空间复杂度是一个反映算法内存占用情况的指标,对于不同的排序算法,我们可以根据具体情况选择适合的算法和数据结构,以达到更好的性能。
1.3 排序方式
- In-place 排序:是指排序过程中只使用原始输入序列的固定内存空间来进行排序,不需要额外的辅助空间。这意味着排序算法能够在原地修改输入数据,而不需要额外的存储空间。
- Out-place 排序:是指排序过程中需要使用额外的存储空间来存储中间结果或者最终结果。这意味着排序算法会创建一个新的数据结构来存储排序后的结果,而不是直接在原始输入数据上进行修改
具体选择使用 in-place 还是 out-place 排序取决于实际应用的需求和约束条件。In-place 排序节省了额外的空间,但可能牺牲了一些时间性能。Out-of-place 排序可以保留原始数据的不变性,但需要额外的空间。
总的来说,in-place 和 out-of-place 是描述排序算法是否需要额外存储空间的概念。选择合适的排序方式取决于问题的约束条件和性能要求。
1.4 稳定度
在排序算法中,"稳定性"是一个重要的概念,用来描述排序算法在排序过程中,对于相等元素的相对位置是否保持不变。
- 稳定:如果排序算法对相等元素的顺序没有改变,那么我们称这个排序算法是稳定的;
- 不稳定:如果排序算法可能改变相等元素的顺序,那么我们称这个排序算法是不稳定的。
举个例子来说,假设有一组学生成绩数据,每个学生的成绩都是整数。现在我们按照成绩从小到大进行排序,但是如果有多个学生的成绩相同,我们希望他们在排序后的结果中仍然保持原来的相对顺序。这种情况下,我们就需要使用稳定的排序算法。
因此,在选择排序算法时,稳定性也是需要考虑的一个重要因素。
2.表格比较
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定度 |
---|---|---|---|---|---|---|
冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | In-place | 稳定 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | In-place | 不稳定 |
插入排序 | O(n²) | O(n) | O(n²) | O(1) | In-place | 稳定 |
希尔排序 | O(n log n) | O(n log² n) | O(n log² n) | O(1) | In-place | 不稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | Out-place | 稳定 |
快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) | In-place | 不稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | In-place | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | Out-place | 稳定 |
桶排序 | O(n+k) | O(n+k) | O(n²) | O(n+k) | Out-place | 稳定 |
基数排序 | O(n x k) | O(n x k) | O(n x k) | O(n+k) | Out-place | 稳定 |
3.算法推荐
3.1 小规模数据
(例如,数据量在100以下)
冒泡排序:尽管冒泡排序的时间复杂度为O(n²),但对于非常小的数据集,其实它的性能还是可以接受的。特别是当数据集中的元素大部分都接近于有序时,冒泡排序能够达到线性的时间复杂度。
3.2 中等规模数据
(例如,数据量在100到10000之间)
- 插入排序:插入排序在小规模数据上与冒泡排序有相似的效率,但随着数据量的增加,其效率逐渐超越冒泡排序。当数据量适中,且数据部分有序时,插入排序是一个很好的选择。
- 选择排序:选择排序的时间复杂度也是O(n²),但在某些特定情况下,例如当数据已经部分有序时,它的效率可能会超过插入排序。
3.3 大规模数据
(例如,数据量在10000以上)
- 快速排序:快速排序的平均时间复杂度为O(n log n),并且在实际应用中通常表现出良好的性能。它尤其适用于数据分布不均的情况。
- 归并排序:归并排序的时间复杂度也是O(n log n),并且它是稳定的排序算法。对于需要稳定排序的应用,如比较排序等,归并排序是一个理想的选择。
3.4 特殊需求
计数排序、桶排序和基数排序:这三种算法都是针对特定类型的输入数据(如整数)设计的。当需要排序的数据都是整数,并且范围较小(例如,在一定范围内的整数)时,这些算法是高效的。
堆排序:堆排序适用于任何可以表示为堆的数据结构。堆排序对于某些特定的问题(如堆化问题)特别有效。
选择合适的排序算法需要考虑到数据的规模、是否部分有序、是否为特定类型的数据(如整数)、以及是否有特殊的需求(如稳定排序)。在选择时,了解这些因素可以帮助你做出最佳的选择。
三、排序算法
1.冒泡排序(Bubble Sort)
1.1 简介
冒泡排序是一种简单的排序算法,它重复地遍历要排序的列表,一次比较两个元素,如果它们的顺序错误就把它们交换过来。重复地进行直到没有需要交换的元素,这时排序完成。
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端,就如同冒泡一样,故名“冒泡排序”。
1.2 示例代码:
代码中的外层循环控制冒泡的轮数,内层循环用于比较相邻的元素并进行交换。最终,通过多轮冒泡操作,将数组中的元素按照升序排列。
在主函数中,首先定义了一个待排序的数组arr,然后计算数组的长度,接下来调用bubble_sort函数对数组进行排序,最后通过循环输出排序后的数组元素。
#include <stdio.h>void bubble_sort(int arr[], int len)
{int i, j, temp;for (i = 0; i < len - 1; i++) // 外层循环控制冒泡的轮数,共需进行len-1轮冒泡{for (j = 0; j < len - 1 - i; j++) // 内层循环用于比较相邻元素并交换位置{if (arr[j] > arr[j + 1]) // 如果前一个元素大于后一个元素,则进行交换{temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}}}
}int main()
{int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };int len = sizeof(arr) / sizeof(arr[0]); // 计算数组长度bubble_sort(arr, len); // 调用冒泡排序函数对数组进行排序int i;for (i = 0; i < len; i++) // 循环输出排序后的数组元素{printf("%d ", arr[i]);}return 0;
}
1.3 示例结果
2.选择排序(Selection Sort)
2.1 简介
选择排序是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
选择排序之所以被这样命名,是因为在每一次排序过程中,它都需要从待排序的数据中选出最小(或最大)的一个元素。这个选择的过程是排序算法的关键步骤,也是它被称为“选择排序”的原因。
2.2 示例代码:
代码中的选择排序函数selection_sort使用了两层循环,外层循环控制每一轮的比较,内层循环遍历未排序的元素,寻找最小值的索引。一旦找到最小值,就将其与当前位置进行交换。
在主函数中,首先定义了一个待排序的数组arr,然后计算数组的长度,接下来调用selection_sort函数对数组进行排序,最后通过循环输出排序后的数组元素。
#include <stdio.h>void swap(int *a, int *b) //定义交换函数
{int temp = *a;*a = *b;*b = temp;
}void selection_sort(int arr[], int len) //定义选择排序函数
{int i, j;for (i = 0; i < len - 1; i++) //外层循环控制每一轮的比较{int min = i; //假设当前位置为最小值for (j = i + 1; j < len; j++) //内层循环遍历未排序的元素,寻找最小值{if (arr[j] < arr[min]) //如果找到比假设最小值还小的元素,更新最小值索引{min = j;}}swap(&arr[min], &arr[i]); //将最小值与当前位置交换}
}int main()
{int arr[] = {11, 25, 8, 5, 56, 78, 99, 53, 52, 6, 46, 50, 91, 17};int len = sizeof(arr) / sizeof(arr[0]); //计算数组长度selection_sort(arr, len); //调用选择排序函数进行排序int i;for (i = 0; i < len; i++) //输出排序后的数组{printf("%d ", arr[i]);}return 0;
}
2.3 示例结果
3.插入排序(Insertion Sort)
3.1 简介
插入排序是一种简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序之所以被这样命名,是因为它的工作原理是将一个记录插入到已经排好序的有序表中,从而形成一个新的、记录数增1的有序表。这个插入的过程是排序算法的关键步骤,也是它被称为“插入排序”的原因。
3.2 示例代码:
代码中的插入排序函数 insertion_sort 使用了两层循环,外层循环从第二个元素开始,依次将其插入到已排序的序列中。内层循环将大于 key 的元素依次向后移动,为 key 腾出插入位置。再将 key 插入到正确的位置上。
在主函数中,首先定义了一个待排序的数组 arr,然后计算数组的长度,接下来调用 insertion_sort 函数对数组进行排序,最后通过循环输出排序后的数组元素。
#include <stdio.h>void insertion_sort(int arr[], int len)
{int i, j, key;for (i = 1; i < len; i++) // 外层循环从第二个元素开始,依次将其插入到已排序的序列中{key = arr[i]; // 将当前待插入元素保存在key中j = i - 1; // j指向已排序序列的最后一个元素while ((j >= 0) && (arr[j] > key)) // 内层循环将大于key的元素依次向后移动{arr[j + 1] = arr[j];j--;}arr[j + 1] = key; // 将待插入元素插入到正确的位置}
}int main()
{int arr[] = {31, 22, 13, 44, 55, 66, 77, 88, 99, 10, 20, 30, 40, 50};int len = sizeof(arr) / sizeof(arr[0]); // 计算数组长度insertion_sort(arr, len); // 调用插入排序函数进行排序int i;for (i = 0; i < len; i++) // 输出排序后的数组{printf("%d ", arr[i]);}return 0;
}
3.3 示例结果
4.希尔排序(Hill sort)
4.1 简介
希尔排序是插入排序的一种更高效的改进版本,也被称为缩小增量排序。该方法的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
4.2 示例代码:
代码中的希尔排序函数 shell_sort 使用了两层循环,外层循环使用希尔增量序列,每次将增量除以2。内层循环对每个增量间隔进行插入排序,将大于 temp 的元素依次向后移动,并将 temp 插入到正确的位置上。
在主函数中,首先定义了一个待排序的数组 arr,然后计算数组的长度,接下来调用 shell_sort 函数对数组进行排序,最后通过循环输出排序后的数组元素。
#include <stdio.h>void shell_sort(int arr[], int len)
{int gap, i, j;int temp;for (gap = len >> 1; gap > 0; gap >>= 1) // 使用希尔增量序列,每次将增量除以2{for (i = gap; i < len; i++) // 对每个增量间隔进行插入排序{temp = arr[i]; // 将当前待插入元素保存在temp中for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) // 内层循环将大于temp的元素依次向后移动{arr[j + gap] = arr[j];}arr[j + gap] = temp; // 将待插入元素插入到正确的位置}}
}int main()
{int arr[] = {29, 33, 15, 41, 82, 49, 67, 53, 91, 11, 23, 12, 72, 38};int len = sizeof(arr) / sizeof(arr[0]); // 计算数组长度shell_sort(arr, len); // 调用希尔排序函数进行排序int i;for (i = 0; i < len; i++) // 输出排序后的数组{printf("%d ", arr[i]);}return 0;
}
4.3 示例结果
5.归并排序(Merge Sort)
5.1 简介
归并排序是一种创建在归并操作上的一种有效的排序算法。它采用分治法(Divide and Conquer)的思想,将原问题分解为若干子问题,然后递归地解决这些子问题,最终将子问题的解合并得到原问题的解。
归并排序之所以被这样命名,是因为它的核心操作是归并,即将两个或两个以上的有序表合并成一个新的有序表。
5.2 示例代码:
代码中的归并排序函数 merge_sort 使用了两层循环,外层循环表示归并的步长,每次翻倍;内层循环将待排序数组分成若干个子数组,并将相邻的子数组进行排序和合并。在子数组进行归并时,使用了一个辅助数组 b,将左、右子数组中的元素依次取出,进行比较并放入辅助数组中,最后将归并后的结果复制回原始待排序数组。如果经过奇数次归并,最终排序结果保存在辅助数组 b 中,需要将其拷贝回原始待排序数组中。
在主函数中,首先定义了一个待排序的数组 arr,然后计算数组的长度,接下来调用 merge_sort 函数对数组进行排序,最后通过循环输出排序后的数组元素。
#include <stdio.h>
#include <stdlib.h>// 用于返回两个数中的最小值
int min(int x, int y)
{return x < y ? x : y;
}void merge_sort(int arr[], int len) {int *a = arr; // a指向待排序数组int *b = (int *) malloc(len * sizeof(int)); // b作为辅助数组,用于存储归并后的结果int seg, start;for (seg = 1; seg < len; seg += seg) { // seg表示归并的步长,每次翻倍for (start = 0; start < len; start += seg * 2) { // 对于每个步长,将待排序数组分成若干个子数组,每个子数组长度为seg*2int low = start, mid = min(start + seg, len), high = min(start + seg * 2, len); // 计算左、右子数组的起点和终点int k = low; // k指向当前位于辅助数组中的位置int start1 = low, end1 = mid; // 左子数组的起点和终点int start2 = mid, end2 = high; // 右子数组的起点和终点while (start1 < end1 && start2 < end2) // 归并左、右子数组b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];while (start1 < end1) // 如果左子数组还有剩余元素,将其直接复制到辅助数组中b[k++] = a[start1++];while (start2 < end2) // 如果右子数组还有剩余元素,将其直接复制到辅助数组中b[k++] = a[start2++];}int *temp = a; // 交换a和b指针a = b;b = temp;}if (a != arr) { // 如果a不等于原始待排序数组,说明经过了奇数次归并,此时最终排序结果保存在b中int i;for (i = 0; i < len; i++)b[i] = a[i]; // 将b中的元素拷贝到原始待排序数组中b = a;}free(b); // 释放动态分配的辅助数组空间
}int main()
{int arr[] = {59, 42, 73, 14, 25, 36, 47, 68, 89, 19, 29, 39, 49, 69};int len = sizeof(arr) / sizeof(arr[0]); // 计算数组长度merge_sort(arr, len); // 调用归并排序函数进行排序int i;for (i = 0; i < len; i++) // 输出排序后的数组{printf("%d ", arr[i]);}return 0;
}
5.3 示例结果
6.快速排序(Quick Sort)
6.1 简介
快速排序是一种基于分治思想的排序算法,通过选择一个基准元素,将待排序序列划分为两个子序列,一个子序列的所有元素都比基准元素小,另一个子序列的所有元素都比基准元素大,然后对这两个子序列递归进行快速排序,最终实现整个序列的有序。
快速排序被这样命名的原因主要是,该算法是所有排序算法中速度最快的一种。虽然它的最坏情况时间复杂度是O(n²),但在实际应用中,当数据量较大且分布不均时,快速排序通常表现出优于其他排序算法的性能。
6.2 示例代码:
代码首先定义了一个 Range 结构体,表示待排序数组的某个区间 [start, end],并定义了一个 new_Range 函数,用于创建 Range 对象。接下来,定义了一个 swap 函数,用于交换两个变量的值。然后,定义了 quick_sort 函数,该函数中使用了一个 Range 类型的数组 r,模拟了一个栈的结构,用于存储待排序的区间。该函数首先将整个待排序数组作为初始区间加入到 r 数组中,然后进入一个 while 循环。在 while 循环中,取出 r 数组中的最后一个区间,以该区间中间位置的元素作为基准值,从左到右扫描数组,找到第一个大于等于基准值的元素,从右到左扫描数组,找到第一个小于等于基准值的元素,如果左指针小于等于右指针,则交换左右指针所指向的元素,左指针右移,右指针左移。重复执行该过程直到区间被分成两个部分。随后,将左、右区间加入到 r 数组中,接着继续循环,直到 r 数组为空。
在主函数中,首先定义了一个待排序的数组 arr,然后计算数组的长度,接下来调用 quick_sort 函数对数组进行排序,最后通过循环输出排序后的数组元素。
#include <stdio.h>// 定义结构体Range,表示待排序数组的某个区间[start, end]
typedef struct _Range
{int start, end;
} Range;// 定义函数new_Range,用于创建Range类型的对象
Range new_Range(int s, int e)
{Range r;r.start = s;r.end = e;return r;
}// 定义函数swap,用于交换两个变量的值
void swap(int *x, int *y)
{int t = *x;*x = *y;*y = t;
}// 定义函数quick_sort,用于进行快速排序
void quick_sort(int arr[], const int len) {if (len <= 0)return; // 避免len等於負值時引發段錯誤(Segment Fault)Range r[len]; // 定义一个Range类型的数组r,用于存储待排序的区间int p = 0; // 定义一个指针p,用于指向下一个待排序区间r[p++] = new_Range(0, len - 1); // 将整个待排序数组作为初始区间加入到r数组中while (p) { // 当r数组不为空时,循环执行以下操作Range range = r[--p]; // 取出r数组中的最后一个区间if (range.start >= range.end)continue; // 如果区间长度为0或负数,则跳过本次循环int mid = arr[(range.start + range.end) / 2]; // 取区间中间位置的元素作为基准值int left = range.start, right = range.end; // 定义左、右指针do {while (arr[left] < mid) ++left; // 从左到右扫描,找到第一个大于等于mid的元素while (arr[right] > mid) --right; // 从右到左扫描,找到第一个小于等于mid的元素if (left <= right) { // 如果左指针小于等于右指针swap(&arr[left], &arr[right]); // 交换左右指针所指向的元素left++; // 左指针右移right--; // 右指针左移}} while (left <= right); // 当左指针小于等于右指针时,继续扫描if (range.start < right) r[p++] = new_Range(range.start, right); // 将左区间加入到r数组中if (range.end > left) r[p++] = new_Range(left, range.end); // 将右区间加入到r数组中}
}int main()
{int arr[] = {17, 28, 39, 42, 53, 64, 75, 86, 97, 11, 22, 33, 44, 55};int len = sizeof(arr) / sizeof(arr[0]); // 计算数组长度quick_sort(arr, len); // 调用快速排序函数进行排序int i;for (i = 0; i < len; i++) // 输出排序后的数组{printf("%d ", arr[i]);}return 0;
}
6.3 示例结果
7.堆排序(Heap Sort)
7.1 简介
堆排序是一种利用堆这种数据结构所设计的一种排序算法。它通过将待排序序列构造成一个大顶堆(或小顶堆),然后将堆顶元素(最大值或最小值)与堆尾互换,之后将剩余元素重新调整为大顶堆(或小顶堆),以此类推,直到整个序列有序。
堆排序之所以被这样命名,是因为它利用了堆这种数据结构。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序正是利用了堆的这种特性进行排序,因此得名。
7.2 示例代码:
这里定义了一个交换函数 swap,用于交换两个变量的值。
接下来定义了 max_heapify 函数,用于维护最大堆性质。在这个函数中,以 dad 为根节点的子树将会被调整为最大堆。
然后是 heap_sort 函数,这是堆排序的主要实现部分。它首先将待排序的数组转换成最大堆,然后依次取出根节点(最大值),与堆中最后一个元素交换并调整堆,直到所有元素都被取出。
最后在 main 函数中,定义了一个待排序的数组 arr,然后计算了数组的长度,并调用了 heap_sort 函数对数组进行排序。最后通过循环输出排序后的数组元素。
这段代码通过堆排序算法对数组进行排序,并输出排序后的结果。
#include <stdio.h>
#include <stdlib.h>void swap(int *a, int *b)
{int temp = *b;*b = *a;*a = temp;
}void max_heapify(int arr[], int start, int end)
{// 建立父節點指標和子節點指標int dad = start;int son = dad * 2 + 1;while (son <= end) { // 若子節點指標在範圍內才做比較if (son + 1 <= end && arr[son] < arr[son + 1]) // 先比較兩個子節點大小,選擇最大的son++;if (arr[dad] > arr[son]) //如果父節點大於子節點代表調整完畢,直接跳出函數return;else { // 否則交換父子內容再繼續子節點和孫節點比較swap(&arr[dad], &arr[son]);dad = son;son = dad * 2 + 1;}}
}void heap_sort(int arr[], int len)
{int i;// 初始化,i從最後一個父節點開始調整for (i = len / 2 - 1; i >= 0; i--)max_heapify(arr, i, len - 1);// 先將第一個元素和已排好元素前一位做交換,再重新調整,直到排序完畢for (i = len - 1; i > 0; i--) {swap(&arr[0], &arr[i]);max_heapify(arr, 0, i - 1);}
}int main()
{int arr[] = {99, 88, 77, 66, 55, 44, 33, 22, 11, 10, 20, 30, 40, 50};int len = sizeof(arr) / sizeof(arr[0]); // 计算数组长度heap_sort(arr, len); // 调用插入排序函数进行排序int i;for (i = 0; i < len; i++) // 输出排序后的数组{printf("%d ", arr[i]);}return 0;
}
7.3 示例结果
8.计数排序(Counting Sort)
8.1 简介
计数排序是一种非比较排序算法,适用于小范围整数排序。它的工作原理是将输入的数据值转化为键存储在额外开辟的数组空间中,数组下标代表数值,而数组的值则代表该数值出现的次数。
计数排序被这样命名的原因是,它通过计数每个元素出现的次数,然后根据计数结果进行排序。这种算法主要适用于整数,且整数值范围较小的情况。
8.2 示例代码:
计数排序函数 counting_sort 的实现。这里传入了一个原始数组 ini_arr、一个用于存放排序结果的数组 sorted_arr 和数组的长度 n。在函数中,首先动态分配了一个大小为 100 的计数数组 count_arr,用于统计原始数组中每个元素的出现次数。然后遍历原始数组,将每个元素出现的次数记录在计数数组中。接下来通过累加计算,将计数数组中的每个元素更新为小于等于它的元素个数之和。这一步的目的是确定每个元素在排序后的数组中的位置。最后,倒序遍历原始数组,根据计数数组确定元素在排序后数组中的位置,并将元素存放到对应位置上。
最后在 main 函数中,定义了一个长度为 10 的数组 arr 和一个用于存放排序结果的数组 sorted_arr。通过 srand 函数和 rand 函数生成随机数来填充原始数组。然后调用 counting_sort 函数对原始数组进行计数排序,并将结果存放在 sorted_arr 中。最后通过调用 print_arr 函数打印排序后的数组,并释放动态分配的内存。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>void print_arr(int *arr, int n) {int i;printf("%d", arr[0]);for (i = 1; i < n; i++)printf(" %d", arr[i]);printf("\n");
}void counting_sort(int *ini_arr, int *sorted_arr, int n) {int *count_arr = (int *) malloc(sizeof(int) * 100);int i, j, k;for (k = 0; k < 100; k++)count_arr[k] = 0;for (i = 0; i < n; i++)count_arr[ini_arr[i]]++;for (k = 1; k < 100; k++)count_arr[k] += count_arr[k - 1];for (j = n; j > 0; j--)sorted_arr[--count_arr[ini_arr[j - 1]]] = ini_arr[j - 1];free(count_arr);
}int main(int argc, char **argv) {int n = 10;int i;int *arr = (int *) malloc(sizeof(int) * n);int *sorted_arr = (int *) malloc(sizeof(int) * n);srand(time(0));for (i = 0; i < n; i++)arr[i] = rand() % 100;counting_sort(arr, sorted_arr, n);print_arr(sorted_arr, n);free(arr);free(sorted_arr);return 0;
}
8.3 示例结果
9. 桶排序(Bucket Sort)
9.1 简介
桶排序(Bucket Sort)是一种排序算法,它的工作原理是将待排序的元素分到有限数量的桶子里,对每个桶里的元素进行排序,然后再对所有桶中的元素进行排序。
这个名称来源于该算法的核心思想,即将数据分布到有限数量的桶子里。每个桶子里的数据再单独进行排序,最终再将所有桶中的数据合并起来。这种将数据分类到桶子里的操作,使得“桶排序”这一名称成为该算法的恰当描述。
9.2 示例代码:
首先,定义了两个结构体:Node和BucketManager。Node结构体表示链表中的节点,包含一个整数元素elem和指向下一个节点的指针list_next。BucketManager结构体用于管理桶,包含一个表示桶数量的整数nums和存储桶的指针数组buckets。
然后,定义了一个辅助结构体BucketSpaceManager,用于管理分配给桶的节点的内存空间。它包含一个表示当前可用节点索引的整数index和指向节点空间的指针nodes_space。
接下来,定义了一系列函数用于初始化桶空间、释放桶空间、释放桶、查找数组中的最大值和最小值、插入节点到桶中,并实现了桶排序函数bucket_sort。
在主函数main中,创建了一个整数数组arr并初始化,然后调用bucket_sort函数对数组进行排序。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define BUCKET_SIZE (5) /**< 假定均匀分布的情况下平均每个桶放几个元素*/typedef struct Node
{int elem;struct Node* list_next;
} Node;typedef struct BucketManager
{int nums;Node** buckets;
} BucketManager;typedef struct BucketSpaceManager
{int index;Node* nodes_space;
} BucketSpaceManager;BucketSpaceManager* init_bucket_space(int size)
{BucketSpaceManager* space_mgr = (BucketSpaceManager*)malloc(sizeof(BucketSpaceManager));if (!space_mgr){printf("out of memory,File:%s, Func:%s, Line:%d\n", __FILE__, __func__, __LINE__);goto exit_1;}space_mgr->index = 0;space_mgr->nodes_space = (Node*)malloc(size * sizeof(Node));if (!space_mgr->nodes_space){printf("out of memory,File:%s, Func:%s, Line:%d\n", __FILE__, __func__, __LINE__);goto exit_2;}return space_mgr;exit_2:free(space_mgr);
exit_1:return NULL;
}BucketManager* init_buckets(int bucket_nums)
{BucketManager* bucket_mgr = (BucketManager*)malloc(sizeof(BucketManager));if (!bucket_mgr){printf("out of memory,File:%s, Func:%s, Line:%d\n", __FILE__, __func__, __LINE__);goto exit_1;}bucket_mgr->nums = bucket_nums;bucket_mgr->buckets = (Node**)calloc(bucket_mgr->nums, sizeof(Node*));if (!bucket_mgr->buckets){printf("out of memory,File:%s, Func:%s, Line:%d\n", __FILE__, __func__, __LINE__);goto exit_2;}return bucket_mgr;
exit_2:free(bucket_mgr);
exit_1:return NULL;
}Node* get_bucket_space(BucketSpaceManager* space_mgr)
{if (space_mgr){return &space_mgr->nodes_space[space_mgr->index++];}else{return NULL;}
}void release_bucket_space(BucketSpaceManager* space_mgr)
{if (space_mgr){if (space_mgr->nodes_space){free(space_mgr->nodes_space);}free(space_mgr);}
}void release_buckets(BucketManager* buckets_mgr)
{if (buckets_mgr){if (buckets_mgr->buckets){free(buckets_mgr->buckets);}free(buckets_mgr);}
}int find_max_min(int* arr, int size, int* p_max, int* p_min)
{if (size <= 0){return -1;}*p_max = arr[0];*p_min = arr[0];int i;for (i = 1; i < size; ++i){if (arr[i] > *p_max){*p_max = arr[i];}if (arr[i] < *p_min){*p_min = arr[i];}}return 0;
}int insert_bucket(BucketManager* bucket_mgr, int index, Node* new_node)
{Node* cur, *pre;if (!bucket_mgr->buckets[index]){bucket_mgr->buckets[index] = new_node;}else{/** 桶内使用插入排序 */cur = bucket_mgr->buckets[index];pre = cur;while (cur->list_next && new_node->elem > cur->elem){pre = cur;cur = cur->list_next;}if (new_node->elem <= cur->elem){if (pre == cur){new_node->list_next = cur;bucket_mgr->buckets[index] = new_node;}else{new_node->list_next = cur;pre->list_next = new_node;}}else{cur->list_next = new_node;}}return 0;
}void bucket_sort(int* arr, int size)
{int max, min;int ret = find_max_min(arr, size, &max, &min);if (ret < 0){return;}BucketSpaceManager* space_mgr = init_bucket_space(size);if (!space_mgr){printf("out of memory,File:%s, Func:%s, Line:%d\n", __FILE__, __func__, __LINE__);goto exit_1;}int bucket_nums = (max - min) / BUCKET_SIZE + 1;BucketManager* bucket_mgr = init_buckets(bucket_nums);if (!bucket_mgr){goto exit_2;}int i;for (i = 0; i < size; ++i){int index = (arr[i] - min) / BUCKET_SIZE;Node* new_node = get_bucket_space(space_mgr);if (!new_node){goto exit_3;}new_node->elem = arr[i];new_node->list_next = NULL;insert_bucket(bucket_mgr, index, new_node);}for (i = 0; i < bucket_mgr->nums; ++i){Node* node = bucket_mgr->buckets[i];while(node){printf("%d ", node->elem);node = node->list_next;}}printf("\n");
exit_3:release_buckets(bucket_mgr);
exit_2:release_bucket_space(space_mgr);
exit_1:return;
}int main()
{int arr[] = { 5, 22, 1, 48, 73, 66, 29, 81, 94, 16, 27, 39, 47, 51 };int size = sizeof(arr) / sizeof(arr[0]);bucket_sort(arr, size);return 0;
}
9.3 示例结果
10.基数排序(Radix Sort)
10.1 简介
基数排序是一种非比较整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。
“基数排序”这个名字的由来是因为这种排序算法的工作原理是基于每个数的“基数”(即每一位数字)来进行排序的。
10.2 示例代码:
首先定义了基数排序函数radixsort。它接受一个整型数组和数组的长度作为参数。在开始排序前,首先找到数组中的最大值m。使用一个循环,直到最大值除以当前位数的基数不再大于0。在每次循环中,创建一个大小为基数的桶数组bucket,用于计数每个桶中元素的个数。在每次循环中,根据当前位数的值,将数组元素放入对应的桶中。然后,按桶的顺序将元素复制回原始数组。最后,将当前位数的基数乘以基数,以便处理下一位数。
最后是程序的主函数。它定义了一个整型数组arr并初始化了一些元素。
然后,通过使用sizeof操作符计算数组的长度,将数组的长度存储在变量n中。接下来,调用radixsort函数对数组进行排序。最后,使用print函数打印排序后的数组。
#include<stdio.h>#define MAX 20
#define BASE 10void print(int *a, int n) {int i;for (i = 0; i < n; i++) {printf("%d\t", a[i]);}
}void radixsort(int *a, int n) {int i, b[MAX], m = a[0], exp = 1;for (i = 1; i < n; i++) {if (a[i] > m) {m = a[i];}}while (m / exp > 0) {int bucket[BASE] = { 0 };for (i = 0; i < n; i++) {bucket[(a[i] / exp) % BASE]++;}for (i = 1; i < BASE; i++) {bucket[i] += bucket[i - 1];}for (i = n - 1; i >= 0; i--) {b[--bucket[(a[i] / exp) % BASE]] = a[i];}for (i = 0; i < n; i++) {a[i] = b[i];}exp *= BASE;#ifdef SHOWPASSprintf("\nPASS : ");print(a, n);
#endif}
}int main() {int arr[] = {99, 88, 77, 66, 55, 44, 33, 22, 11, 10, 20, 30, 40, 50};int n = sizeof(arr) / sizeof(arr[0]);radixsort(arr, n);print(arr, n);printf("\n");return 0;
}
10.3 示例结果
四、总结
感谢你的观看,谢谢!