在计算机科学领域,排序算法是最基础也是最关键的部分之一。它们不仅在理论上具有重要意义,也在实际应用中发挥着至关重要的作用。从经典的冒泡排序到更高效的快速排序,每种算法都有其独特之处和适用场景。本博客旨在深入探讨各种排序算法的内在原理、适用场景,并提供它们的C++实现。
计算机算法中,排序是一个基础且重要的领域。以下是一些关键的排序方法,每种方法都有其独特的特点和适用场景:
冒泡排序(Bubble Sort):通过重复交换相邻逆序的元素,使较大的元素“冒泡”到数组的一端。尽管简单,但效率较低,通常不用于大规模数据集。
选择排序(Selection Sort):重复地选择剩余元素中的最小者,放到已排序序列的末尾。这种方法也是效率较低的,适合小规模数据。
插入排序(Insertion Sort):构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。适用于部分已排序的数据集。
快速排序(Quick Sort):采用分治法策略,通过选取一个“基准”元素,将数组分为两部分,一部分小于基准,一部分大于基准,然后递归排序这两部分。适用于大规模数据集,是一种高效的排序方法。
归并排序(Merge Sort):也是一种分治法策略的应用,将数组分成若干个小块,先递归地对每个小块排序,然后将这些小块合并成一个大的有序数组。它是稳定的排序方法,适用于大规模数据集。
堆排序(Heap Sort):利用堆这种数据结构所设计的一种排序算法。它将数组转换成一个最大堆,然后将堆顶的最大数取出,将剩余的堆继续调整为最大堆。适用于大规模数据集,效率较高。
希尔排序(Shell Sort):是插入排序的一种更高效的改进版本。它通过比较距离一定间隔的元素来工作,间隔逐渐减少直到为1,使数组变为基本有序。
计数排序(Counting Sort):适用于一定范围内的整数排序。通过计算每个元素的出现次数来确定每个元素的位置。
基数排序(Radix Sort):按照低位先排序,然后收集;再按照高位排序,然后再收集,以此类推,直到最高位。适用于整数或某些类型的字符串排序。
桶排序(Bucket Sort):将元素分布到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
不同的排序算法有其特定的应用场景和优势。选择合适的排序方法取决于数据的特性(如数据大小、是否已部分排序等)和应用的需求(如对稳定性、时间复杂度、空间复杂度的考虑)。在实际应用中,选择最合适的排序算法对于优化性能至关重要。
从STL的代码中,我们可以学习快速排序的优化方法,主要有以下几点:
// 递归快速排序
template <typename T>
void quickSort(vector<T>& arr, int start, int end) {
if (start >= end) return ;
int l = start, r = end;
T pivot = arr[(start + end) / 2];
while (l <= r) {
while (l <= r && arr[l] < pivot) l++;
while (l <= r && arr[r] > pivot) r--;
if (l <= r) swap(arr[l++], arr[r--]);
}
quickSort(arr, start, r);
quickSort(arr, l, end);
}
在快速排序中,“三点取中法”(Median-of-Three)是一种选择枢轴(pivot)的策略,旨在提高快速排序算法的效率,特别是在处理具有特定模式或几乎已排序的数组时。此方法通过考虑数组的首部、中部和尾部三个元素,并从这三个元素中选择中间值作为枢轴,从而提高枢轴选择的效果。
实现步骤
1. 选取三个点:从数组中选取三个点:起始位置(start),结束位置(end)和中间位置((start + end) / 2)。
1. 比较并排序这三个点:比较这三个位置的元素,并通过交换使得 arr[start] <= arr[mid] <= arr[end]。
1. 选择中间元素作为枢轴:选择中间位置的元素作为枢轴,并且可能将其与数组的某个位置(如 end-1)交换,从而在分区函数中使用。
1. 进行快速排序:以此枢轴进行常规的快速排序分区操作。
template <typename T>
int medianOfThree(std::vector<T>& arr, int start, int end) {
int mid = start + (end - start) / 2;
// 排序 start, mid, end
if (arr[start] > arr[mid])
std::swap(arr[start], arr[mid]);
if (arr[start] > arr[end])
std::swap(arr[start], arr[end]);
if (arr[mid] > arr[end])
std::swap(arr[mid], arr[end]);
// 将中间值放在 end - 1
std::swap(arr[mid], arr[end - 1]);
return arr[end - 1];
}
#if 1
template <typename T>
int partition(std::vector<T>& arr, int start, int end) {
T pivot = medianOfThree(arr, start, end);
int i = start, j = end - 1;
while (true) {
while (arr[++i] < pivot) {}
while (arr[--j] > pivot) {}
if (i < j)
std::swap(arr[i], arr[j]);
else
break;
}
std::swap(arr[i], arr[end - 1]); // 将枢轴放到正确的位置
return i;
}
#else
template <typename T>
int partition(vector<T>& arr, int low, int high) {
int pivotValue = arr[high];
swap(arr, high, low);
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] < pivotValue) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, high);
return i + 1;
}
#endif
template <typename T>
void quickSort(std::vector<T>& arr, int start, int end) {
if (start < end) {
int pivotIndex = partition(arr, start, end);
quickSort(arr, start, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, end);
}
}
备注:
- 这里的partition代码和快速排序的代码与我认为的标准快速排序不大一样,但我感觉有一番新意,放在这里用作参考。
- 三点取中法一般用于解决极端特殊情况,在一般情况下,取中的过程计算量还是有的,会减慢排序的速度。
在单边递归版本中,我们只递归地对数组的一部分进行排序,而另一部分通过迭代完成。这样可以减少递归调用的深度,从而节省栈空间。
// 单边递归快速排序
template <typename T>
void quickSortSingleRecursion(vector<T>& arr, int start, int end) {
while (start < end) {
int l = start, r = end, pivot = arr[(start + end) / 2];
while (l <= r) {
while (l <= r && arr[l] < pivot) l++;
while (l <=r && arr[r] > pivot) r--;
if (l <= r) {
swap(arr[l], arr[r]);
l++; r--;
}
}
quickSortSingleRecursion(arr, l, end);
end = r;
}
}
无监督快速排序(也称为Hoare分区方案的快速排序)不使用额外的检查来减少比较操作。它可能比上述版本稍快,但它的实现较为复杂,且对于初学者来说不那么直观。
// 无监督快速排序
template <typename T>
void quickSortUnwatched(vector<T>& arr, int start, int end) {
while (start < end) {
int l = start, r = end, pivot = arr[(start + end) / 2];
do {
while (arr[l] < pivot) l++;
while (arr[r] > pivot) r--;
if (l <= r) {
swap(arr[l], arr[r]);
l++; r--;
}
}while (l <= r);
quickSortUnwatched(arr, l, end);
end = r;
}
}
注意:无监督版本的快速排序在处理等于枢纽值的元素时可能表现不如标准版本。在实际应用中,选择哪种快速排序版本取决于具体的应用场景和性能要求。
C++ 标准库中的 std::sort 算法实现通常是一种混合排序算法,它结合了快速排序、插入排序和堆排序的特点。这种混合排序算法被设计为在大多数情况下都能提供良好的性能。特别地,std::sort 在某些情况下会从快速排序切换到堆排序,以避免快速排序的最坏情况性能。
快速排序转换为堆排序的典型情况是当快速排序的递归深度过深时。这通常发生在以下情况:
基于以上原理,我设计实现一个结合了快速排序、堆排序和插入排序的混合排序算法
// 插入排序
template <typename T>
void insertionSort(vector<T>& arr, int left, int right) {
for (int i = left + 1, j = i; i <= right; i++, j = i)
while (j > left && arr[j] < arr[j - 1]) swap(arr[j], arr[j--]);
}
// 堆排序
// 堆排序的辅助函数
template <typename T>
void heapify(std::vector<T>& arr, int n, int i) {
int largest = i; // 初始化最大元素为当前节点
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);
}
}
// 堆排序
template <typename T>
void heapSort(std::vector<T>& arr, int start, int end) {
int n = end - start + 1; // 计算堆的大小
// 构建最大堆
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]); // 将当前最大元素移到数组末尾
heapify(arr, i, 0); // 调整剩余数组,使其成为最大堆
}
}
// 快速排序
// 分区函数
template <typename T>
int partition(std::vector<T>& arr, int start, int end) {
T pivot = arr[(start + end) / 2]; // 选择中间的元素作为枢轴
int l = start, r = end;
while (l <= r) {
while (arr[l] < pivot) l++; // 从左侧找到第一个不小于枢轴的元素
while (arr[r] > pivot) r--; // 从右侧找到第一个不大于枢轴的元素
if (l <= r) {
std::swap(arr[l], arr[r]); // 交换这两个元素
l++;
r--;
}
}
return l; // 返回分区后的分界点
}
// 快速排序
template <typename T>
void quickSort(std::vector<T>& arr, int start, int end, int depthLimit) {
if (start < end) {
if (end - start < 16) { // 小数组使用插入排序
insertionSort(arr, start, end);
} else if (depthLimit == 0) { // 深度限制达到时使用堆排序
heapSort(arr, start, end);
} else {
int l = partition(arr, start, end); // 快速排序的分区操作
quickSort(arr, start, l - 1, depthLimit - 1);
quickSort(arr, l, end, depthLimit - 1);
}
}
}
// 组合排序
template <typename T>
void hybridSort(std::vector<T>& arr) {
int maxDepth = log(arr.size()) * 2;
quickSort(arr, 0, arr.size() - 1, maxDepth);
}
int main() {
// 生成随机数
srand(static_cast<unsigned int>(time(0))); // 初始化随机数生成器
std::vector<int> testArray;
for (int i = 0; i < 20; ++i) {
testArray.push_back(rand() % 100); // 生成0到99之间的随机数
}
// 打印未排序的数组
std::cout << "Original array:\n";
for (int i : testArray) {
std::cout << i << " ";
}
std::cout << "\n";
// 使用混合排序
hybridSort(testArray);
// 打印排序后的数组
std::cout << "Sorted array:\n";
for (int i : testArray) {
std::cout << i << " ";
}
std::cout << "\n";
return 0;
}
这段代码首先生成一个包含20个随机整数的 vector,然后调用 hybridSort 函数对其进行排序,最后输出排序前后的数组。您可以运行这段代码来验证排序算法的有效性。如果需要测试更大的数组或不同类型的数据,只需相应地调整 testArray。
以上的代码提供了一种基础的混合排序算法实现。它在小数组上使用插入排序,使用快速排序作为主要的排序算法,但当递归深度变得过深时(为了避免快速排序的最坏情况性能),它会切换到堆排序。
请注意,具体的阈值(如这里的数组大小16用于切换到插入排序)和深度限制可以根据具体的应用场景进行调整。此外,快速排序的分区策略也可以根据需要进行修改。
我的这段代码实现了堆排序算法。堆排序是一种利用堆数据结构的排序算法,分为两个主要阶段:构建最大堆(make_heap)和执行排序(pop_heap)。下面是对您的代码的详细解释和注释。
template <typename T>
void heapSort(vector<T>& arr, int start, int end) {
// 构建最大堆(make_heap)
// 在这一阶段,从数组的中间位置开始向前遍历,处理每个非叶子节点。对于每个节点,如果它大于其父节点,则将其与父节点交换,直至满足最大堆的性质
int mid = start + ((end - start + 1) >> 1);
for (int i = end; i >= mid; i--) {
int ind = i, parent = start + ((ind - start - 1) >> 1);
while (ind > start && parent >= start) {
if (arr[parent] < arr[ind]) swap(arr[parent], arr[ind]);
ind = parent, parent = start + ((ind - start - 1) >> 1);
}
}
// 执行排序(pop_heap)
// 在排序阶段,不断将堆顶元素(即当前最大元素)与堆的最后一个元素交换,并减少堆的大小。每次交换后,它重新调整剩余的堆,以确保堆顶元素是最大的。
while (end > start) {
swap(arr[start], arr[end--]); // pop
int ind = start, child = start + ((ind - start) << 1) + 1;
while (child <= end) {
if (child < end && arr[child] < arr[child + 1]) child++;
if (arr[ind] < arr[child]) swap(arr[ind], arr[child]);
else break;
ind = child, child = start + ((ind - start) << 1) + 1;
}
}
}
优化和注意事项
整体来说,这是一个有效且清晰的堆排序实现,正确地应用了堆的性质来进行排序。
以下是另一种堆排序算法在 C++ 中的实现:
// 堆排序的辅助函数
template <typename T>
void heapify(std::vector<T>& arr, int n, int i) {
int largest = i; // 初始化最大元素为当前节点
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);
}
}
// 堆排序
template <typename T>
void heapSort(std::vector<T>& arr, int start, int end) {
int n = end - start + 1; // 计算堆的大小
// 构建最大堆
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]); // 将当前最大元素移到数组末尾
heapify(arr, i, 0); // 调整剩余数组,使其成为最大堆
}
}
这个代码和我之前实现的不太一样,感觉别有一番新意,供大家参考。
这块我就不详细写代码了,参考我的github,有上面所有排序算法的实现细节,多数是经过我仔细优化过的,比如插入排序等,比网上一般的实现要简单。
通过本博客的介绍,我们不仅理解了各种排序算法的基本原理和它们的适用场景,还学习了如何用C++实现这些算法。每种排序方法都有其独特的优势和局限性,理解这些差异对于选择最合适的排序策略至关重要。无论是在学术研究还是实际应用中,这些排序算法都是解决问题和优化性能的重要工具。