目录
一、堆排序
1、工作原理
堆排序(Heap Sort)是一种基于堆数据结构的比较排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或大于)它的父节点。堆排序利用堆这种数据结构所设计的一种排序算法。堆排序可以分为两个主要的过程:
- 建堆:将无序序列构造成一个大顶堆(或小顶堆)。
- 排序:反复将堆顶元素(即最大值或最小值)与末尾元素交换,然后将剩余部分重新调整为大顶堆(或小顶堆),重复执行直到整个序列有序。
首先,将数组中的元素重新排列,使之成为一个堆。这一步是从最后一个非叶子节点开始,逐步向前进行堆化操作。
然后,反复将堆顶元素(即当前最大值)与末尾元素交换,缩小堆的范围(不包括已经排好序的末尾元素),并对新的堆顶元素进行堆化操作,以确保剩余的元素仍然满足堆的性质。这个过程一直重复,直到堆的范围缩小到只剩下一个元素为止,此时数组已经有序。
通过这个过程,堆排序实现了从无序到有序的转换,且时间复杂度为 O(nlogn),这使得堆排序在处理大数据集时非常有效。
通过这个过程,堆排序实现了从无序到有序的转换,且时间复杂度为 O(nlogn),这使得堆排序在处理大数据集时非常有效。
2、时间复杂度
- 建堆:对于包含 n 个元素的数组,建堆的时间复杂度是 O(n)。
- 排序:对于每个元素,都需要将堆顶元素(即当前最大值)与末尾元素交换,然后对剩余元素重新进行堆化。由于堆化操作的时间复杂度是 O(logn),而总共需要进行 n−1 次这样的操作,所以排序的时间复杂度是 O(nlogn)。
因此,堆排序的总体时间复杂度是 O(nlogn)
3、示例代码
#include <iostream>
#include <vector>
void heapify(std::vector<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) {
std::swap(arr[i], arr[largest]);
// 递归地对受影响的子树进行堆化
heapify(arr, n, largest);
}
}
void heapSort(std::vector<int>& arr) {
int n = arr.size();
// 建堆(重新排列数组)
for (int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);
// 一个一个从堆中取出元素
for (int i = n - 1; i > 0; i--) {
// 移动当前根到数组末尾
std::swap(arr[0], arr[i]);
// 调用max heapify来维护堆的性质
heapify(arr, i, 0);
}
}
void printArray(const std::vector<int>& arr) {
for (int val : arr)
std::cout << val << " ";
std::cout << std::endl;
}
int main() {
std::vector<int> arr = {12, 11, 13, 5, 6, 7};
std::cout << "Unsorted array: ";
printArray(arr);
heapSort(arr);
std::cout << "Sorted array: ";
printArray(arr);
return 0;
}
二、计数排序
1、工作原理
计数排序(Counting Sort)是一种非比较型排序算法,适用于一定范围内的整数排序。它的工作原理是统计每个元素出现的次数,然后根据这些统计信息将元素放到正确的位置上。
- 确定范围:首先,找到输入数组中的最大值和最小值,以确定计数数组的大小(即范围)。
- 计数:然后,创建一个计数数组,并初始化所有元素为0。遍历输入数组,对每个元素进行计数,即将其对应的计数数组中的值加一。
- 排序:最后,根据计数数组中的信息,将元素放回原数组中。具体来说,遍历计数数组,对于每个非零元素,将其对应的值(通过加上最小值来还原原始值)放回原数组中相应数量的位置,并将计数减一,直到计数为零。
通过这个过程,计数排序能够高效地对一定范围内的整数进行排序。然而,它不适用于范围非常大的整数或浮点数排序,因为这将导致计数数组的大小变得不可接受。
2、时间复杂度
-
空间复杂度:计数排序需要额外的空间来存储计数数组,其大小为最大值与最小值之差加一(
max_val - min_val + 1
)。因此,空间复杂度为 O(k),其中 k 是输入数据的范围。 -
时间复杂度:
- 遍历输入数组以找到最大值和最小值的时间复杂度为 O(n)。
- 填充计数数组的时间复杂度为 O(n),因为需要遍历输入数组一次。
- 根据计数数组重新排列原数组的时间复杂度为 O(n+k),其中 k 是计数数组的大小(即输入数据的范围)。然而,在大多数情况下,我们可以认为 k 是一个常数或者与 n 成线性关系(例如,当数据是有界的整数时),因此这部分的时间复杂度可以近似为 O(n)。
综合以上分析,计数排序的总体时间复杂度在最优情况下为 O(n+k),但在实际应用中通常可以认为是 O(n)(假设 k 是常数或与 n 成线性关系)。然而,需要注意的是,当输入数据的范围非常大时,计数排序的空间复杂度可能会成为问题。
3、示例代码
#include <iostream>
#include <vector>
#include <algorithm> // 用于std::fill
#include <climits> // 用于INT_MAX
void countingSort(std::vector<int>& arr) {
if (arr.empty()) return;
// 找到数组中的最大值和最小值
int max_val = *std::max_element(arr.begin(), arr.end());
int min_val = *std::min_element(arr.begin(), arr.end());
// 创建计数数组并初始化为0
int range = max_val - min_val + 1;
std::vector<int> count(range, 0);
// 统计每个元素出现的次数
for (int num : arr) {
count[num - min_val]++;
}
// 根据计数数组重新排列原数组
int index = 0;
for (int i = 0; i < range; ++i) {
while (count[i] > 0) {
arr[index++] = i + min_val;
count[i]--;
}
}
}
void printArray(const std::vector<int>& arr) {
for (int val : arr) {
std::cout << val << " ";
}
std::cout << std::endl;
}
int main() {
std::vector<int> arr = {4, 2, 2, 8, 3, 3, 1};
std::cout << "Unsorted array: ";
printArray(arr);
countingSort(arr);
std::cout << "Sorted array: ";
printArray(arr);
return 0;
}
三、桶排序
1、工作原理
桶排序(Bucket Sort)是一种基于分布的排序算法,它将数组元素分散到有限数量的桶中,然后对每个桶分别进行排序(通常使用其他排序算法,如插入排序),最后将所有桶中的元素合并起来形成最终的排序结果。
- 创建桶:首先,根据输入数据的数量和分布情况确定桶的数量,并创建相应数量的桶(这里使用
std::vector<std::vector<float>>
来表示桶)。 - 分配元素到桶中:然后,遍历输入数组,将每个元素根据其值分配到相应的桶中。分配的依据可以是元素的范围(如使用线性函数将元素值映射到桶索引)或某种哈希函数。
- 桶内排序:对每个桶中的元素进行排序。这里可以使用任何有效的排序算法,如插入排序、快速排序或归并排序等。
- 合并桶:最后,将所有桶中的元素合并起来形成最终的排序结果。这可以通过遍历所有桶并将桶中的元素依次添加到输出数组中来实现。
桶排序是一种非常灵活的排序算法,其性能取决于桶的数量、桶内排序算法以及输入数据的分布。在实际应用中,需要根据具体情况调整桶的数量和桶内排序算法以获得最佳性能。
2、时间复杂度
桶排序的时间复杂度取决于几个因素:
- 桶的数量:如果桶的数量太多,则每个桶中的元素很少,但合并桶的成本增加;如果桶的数量太少,则桶中的元素太多,对每个桶进行排序的成本增加。
- 桶内排序算法:桶内排序算法的选择也会影响整体时间复杂度。如果桶内使用插入排序,最坏情况下时间复杂度为 O(n2)(当桶内元素很多时)。如果桶内使用更高效的排序算法(如快速排序或归并排序),则整体时间复杂度可以降低。
- 输入数据的分布:如果输入数据均匀分布,则桶排序效率较高;如果数据分布不均,则可能导致某些桶中元素过多,影响性能。
在理想情况下(即桶的数量适当且输入数据均匀分布),桶排序的时间复杂度为 O(n+k),其中 n 是输入数据的数量,k 是桶的数量。然而,在实际应用中,由于桶内排序和合并桶的成本,桶排序的时间复杂度通常更高。如果桶内使用插入排序,并且每个桶中的元素数量最多为 M(假设 M 是一个常数或与 n 成线性关系),则桶排序的时间复杂度为 O(nlogM)(假设合并桶的成本是线性的)。在最坏情况下(即所有数据都落入同一个桶中),桶排序的时间复杂度会退化到 O(n2)。
3、示例代码
#include <iostream>
#include <vector>
#include <algorithm> // 用于std::sort
#include <cmath> // 用于std::sqrt等数学函数
void bucketSort(std::vector<float>& arr) {
if (arr.empty()) return;
// 1. 创建桶
int bucketCount = static_cast<int>(std::sqrt(arr.size())); // 桶的数量可以是数组大小的平方根
std::vector<std::vector<float>> buckets(bucketCount);
// 2. 将元素分配到桶中
float minValue = *std::min_element(arr.begin(), arr.end());
float maxValue = *std::max_element(arr.begin(), arr.end());
float range = maxValue - minValue;
for (float num : arr) {
int bucketIndex = static_cast<int>((num - minValue) / range * bucketCount);
if (bucketIndex >= bucketCount) {
bucketIndex = bucketCount - 1; // 防止索引越界
}
buckets[bucketIndex].push_back(num);
}
// 3. 对每个桶进行排序
for (auto& bucket : buckets) {
std::sort(bucket.begin(), bucket.end());
}
// 4. 合并所有桶中的元素
arr.clear();
for (const auto& bucket : buckets) {
arr.insert(arr.end(), bucket.begin(), bucket.end());
}
}
void printArray(const std::vector<float>& arr) {
for (float val : arr) {
std::cout << val << " ";
}
std::cout << std::endl;
}
int main() {
std::vector<float> arr = {0.42, 0.32, 0.33, 0.52, 0.37, 0.47, 0.51, 0.46, 0.50, 0.12};
std::cout << "Unsorted array: ";
printArray(arr);
bucketSort(arr);
std::cout << "Sorted array: ";
printArray(arr);
return 0;
}
四、基数排序
1、工作原理
基数排序(Radix Sort)是一种非比较型整数排序算法,它按照数字的每一位(从最低有效位到最高有效位,或从最高有效位到最低有效位)进行排序。基数排序通常使用计数排序或桶排序作为其子过程来排序每个位上的数字。
- 找到最大数字的位数:首先,找到输入数组中的最大数字,以确定需要排序的位数。
- 从最低有效位到最高有效位进行排序:然后,从最低有效位开始,依次对每个位上的数字进行排序。这可以通过使用计数排序作为子过程来实现。
- 计数排序:对于每个位上的数字,使用计数排序进行排序。计数排序会根据数字在该位上的值将其分配到不同的桶中,然后按照桶的顺序将数字重新组合起来。
- 重复排序:对每个位重复上述过程,直到最高有效位为止。
- 得到排序结果:经过对所有位的排序后,输入数组将按照从小到大的顺序排列。
基数排序是一种稳定的排序算法,适用于处理大量整数数据。由于它使用计数排序作为子过程,因此当输入数据中的数字范围较小时,基数排序的效率非常高。然而,当数字范围非常大时,计数排序的桶数量将增加,从而影响基数排序的性能。
2、时间复杂度
基数排序的时间复杂度主要取决于两个因素:
- 数字的位数:基数排序需要按照数字的每一位进行排序,因此时间复杂度与数字的位数成正比。
- 计数排序的时间复杂度:基数排序使用计数排序作为其子过程来排序每个位上的数字。如果数字的范围是 k(在十进制中 k=10),则计数排序的时间复杂度为 O(n+k),其中 n 是输入数组的大小。
综合以上因素,基数排序的总体时间复杂度为 O(d⋅(n+k)),其中 d 是数字的位数,n 是输入数组的大小,k 是数字的基数(在十进制中 k=10)。由于 d 和 k 都是常数(或可以看作是常数,因为数字的位数和基数在大多数情况下都是有限的),因此基数排序的时间复杂度可以近似为 O(n)。
3、示例代码
#include <iostream>
#include <vector>
#include <algorithm> // 用于std::max_element
#include <cmath> // 用于计算幂
#include <array> // 用于固定大小的数组
// 获取数组中数字在指定位置(从右往左数,从0开始)上的数值
int getDigit(int num, int digitPosition) {
return (num / static_cast<int>(std::pow(10, digitPosition))) % 10;
}
// 使用计数排序对数组中指定位置上的数字进行排序
void countingSortByDigit(std::vector<int>& arr, int digitPosition) {
const int base = 10; // 基数为10,因为我们处理的是十进制数字
std::vector<int> output(arr.size()); // 输出数组
std::array<int, base> count = {0}; // 计数数组,大小为基数
// 统计每个桶中的元素数量
for (int num : arr) {
int digit = getDigit(num, digitPosition);
count[digit]++;
}
// 计算每个桶的起始位置
for (int i = 1; i < base; ++i) {
count[i] += count[i - 1];
}
// 构建输出数组
for (int i = arr.size() - 1; i >= 0; --i) {
int digit = getDigit(arr[i], digitPosition);
output[count[digit] - 1] = arr[i];
count[digit]--;
}
// 将排序结果复制回原数组
for (int i = 0; i < arr.size(); ++i) {
arr[i] = output[i];
}
}
// 基数排序主函数
void radixSort(std::vector<int>& arr) {
if (arr.empty()) return;
// 找到数组中的最大值,以确定需要排序的位数
int maxVal = *std::max_element(arr.begin(), arr.end());
int maxDigits = 0;
while (maxVal > 0) {
maxVal /= 10;
maxDigits++;
}
// 从最低有效位到最高有效位进行排序
for (int digitPosition = 0; digitPosition < maxDigits; ++digitPosition) {
countingSortByDigit(arr, digitPosition);
}
}
void printArray(const std::vector<int>& arr) {
for (int val : arr) {
std::cout << val << " ";
}
std::cout << std::endl;
}
int main() {
std::vector<int> arr = {170, 45, 75, 90, 802, 24, 2, 66};
std::cout << "Unsorted array: ";
printArray(arr);
radixSort(arr);
std::cout << "Sorted array: ";
printArray(arr);
return 0;
}