排序算法综述
排序算法是计算机科学中非常重要的一类算法,用于将一组数据按照某种顺序进行排列。常见的排序算法有多种,每种算法在不同情况下有不同的性能表现。本文将介绍几种常见的排序算法,包括选择排序、快速排序、冒泡排序、插入排序、归并排序和堆排序,并对选择排序和快速排序进行更为详尽的介绍。
选择排序(Selection Sort)
思想
选择排序是一种简单直观的排序算法。其基本思想是:
- 从未排序部分中找到最小(或最大)的元素,将其放置在已排序部分的末尾。
- 重复上述过程,直到所有元素都被排序。
时间复杂度
选择排序的时间复杂度为 O(n^2),因为每次找到最小(或最大)元素需要 O(n) 的时间,一共需要进行 n 次选择。
示例代码
以下是选择排序的 C++ 实现,并结合具体例子帮助理解:
#include <iostream>
#include <vector>
#include <algorithm>
// 选择排序的实现函数
void selectionSort(std::vector<int>& arr) {
int n = arr.size();
for (int i = 0; i < n - 1; ++i) {
int minIndex = i;
for (int j = i + 1; j < n; ++j) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
std::swap(arr[minIndex], arr[i]);
}
}
int main() {
std::vector<int> arr = {64, 25, 12, 22, 11};
selectionSort(arr);
std::cout << "Sorted array: ";
for (const auto& num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
例子解析
假设我们有一个数组 arr = {64, 25, 12, 22, 11}
,选择排序的过程如下:
- 初始数组:
{64, 25, 12, 22, 11}
- 第一次迭代:找到最小的元素 11,并将其与第一个元素 64 交换,结果为
{11, 25, 12, 22, 64}
- 第二次迭代:在剩余的元素中找到最小的 12,并将其与第二个元素 25 交换,结果为
{11, 12, 25, 22, 64}
- 第三次迭代:在剩余的元素中找到最小的 22,并将其与第三个元素 25 交换,结果为
{11, 12, 22, 25, 64}
- 第四次迭代:在剩余的元素中找到最小的 25,并将其与第四个元素 25 交换(无需交换),结果为
{11, 12, 22, 25, 64}
- 排序完成。
快速排序(Quick Sort)
思想
快速排序是一种基于分治法的排序算法。其基本思想是:
- 选择一个基准元素(pivot),通常选择数组的最后一个元素。
- 重新排列数组,使得所有比基准元素小的元素放在基准元素的左边,所有比基准元素大的元素放在基准元素的右边(称为分区操作)。
- 递归地对基准元素左边和右边的子数组进行快速排序。
时间复杂度
快速排序的平均时间复杂度为 O(n log n),但在最坏情况下(例如,每次选择的基准元素都是数组的最大或最小值),时间复杂度会退化为 O(n^2)。
示例代码
以下是快速排序的 C++ 实现,并结合具体例子帮助理解:
#include <iostream>
#include <vector>
#include <algorithm>
// 快速排序的分区函数
int partition(std::vector<int>& arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; ++j) {
if (arr[j] < pivot) {
++i;
std::swap(arr[i], arr[j]);
}
}
std::swap(arr[i + 1], arr[high]);
return i + 1;
}
// 快速排序的递归函数
void quickSort(std::vector<int>& arr, int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
// 快速排序的主函数
void quickSort(std::vector<int>& arr) {
quickSort(arr, 0, arr.size() - 1);
}
int main() {
std::vector<int> arr = {10, 7, 8, 9, 1, 5};
quickSort(arr);
std::cout << "Sorted array: ";
for (const auto& num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
例子解析
假设我们有一个数组 arr = {10, 7, 8, 9, 1, 5}
,快速排序的过程如下:
- 初始数组:
{10, 7, 8, 9, 1, 5}
- 选择最右边的元素 5 作为基准元素。重新排列数组,使得所有比 5 小的元素在左边,比 5 大的元素在右边,结果为
{1, 5, 8, 9, 10, 7}
- 对基准元素 5 左边的子数组
{1}
进行快速排序(无需操作)。 - 对基准元素 5 右边的子数组
{8, 9, 10, 7}
进行快速排序。- 选择最右边的元素 7 作为基准元素。重新排列数组,使得所有比 7 小的元素在左边,比 7 大的元素在右边,结果为
{1, 5, 7, 9, 10, 8}
- 对基准元素 7 左边的子数组
{7}
进行快速排序(无需操作)。 - 对基准元素 7 右边的子数组
{9, 10, 8}
进行快速排序。- 选择最右边的元素 8 作为基准元素。重新排列数组,使得所有比 8 小的元素在左边,比 8 大的元素在右边,结果为
{1, 5, 7, 8, 10, 9}
- 对基准元素 8 左边的子数组
{8}
进行快速排序(无需操作)。 - 对基准元素 8 右边的子数组
{10, 9}
进行快速排序。- 选择最右边的元素 9 作为基准元素。重新排列数组,使得所有比 9 小的元素在左边,比 9 大的元素在右边,结果为
{1, 5, 7, 8, 9, 10}
- 对基准元素 9 左边的子数组
{9}
进行快速排序(无需操作)。 - 对基准元素 9 右边的子数组
{10}
进行快速排序(无需操作)。
- 选择最右边的元素 9 作为基准元素。重新排列数组,使得所有比 9 小的元素在左边,比 9 大的元素在右边,结果为
- 选择最右边的元素 8 作为基准元素。重新排列数组,使得所有比 8 小的元素在左边,比 8 大的元素在右边,结果为
- 选择最右边的元素 7 作为基准元素。重新排列数组,使得所有比 7 小的元素在左边,比 7 大的元素在右边,结果为
- 排序完成。
具体来说,partition段代码完成了以下步骤:
-
选择基准元素:在这段代码中,基准元素被选择为数组的最后一个元素,即
arr[high]
。 -
初始化指针:定义两个指针:
i
:初始化为low - 1
,用来跟踪小于基准元素的区域的最后一个位置。j
:用来遍历数组的当前元素。
-
遍历和交换:
- 使用
for
循环遍历数组的元素(从low
到high - 1
)。 - 在每次迭代中,将当前元素
arr[j]
与基准元素pivot
进行比较:- 如果
arr[j]
小于pivot
,则将i
指针向右移动一位(++i
),并交换arr[i]
和arr[j]
的位置。这一步的目的是将小于基准元素的元素移到数组的左边。
- 如果
- 使用
-
最终交换基准元素:
- 循环结束后,将
i + 1
位置的元素与基准元素arr[high]
交换,以确保基准元素位于正确的排序位置。
- 循环结束后,将
-
返回基准元素的位置:
- 返回
i + 1
,即基准元素的位置。
- 返回
具体例子
假设我们有一个数组 arr = {10, 7, 8, 9, 1, 5}
,以及 low = 0
和 high = 5
,执行分区函数的过程如下:
-
选择基准元素:
pivot = arr[5] = 5
i = low - 1 = -1
-
遍历和交换:
j = 0
,arr[0] = 10
,10 >= 5
,不做交换。j = 1
,arr[1] = 7
,7 >= 5
,不做交换。j = 2
,arr[2] = 8
,8 >= 5
,不做交换。j = 3
,arr[3] = 9
,9 >= 5
,不做交换。j = 4
,arr[4] = 1
,1 < 5
,++i
(i = 0
),交换arr[0]
和arr[4]
,结果为:{1, 7, 8, 9, 10, 5}
。
-
最终交换基准元素:
- 交换
arr[i + 1]
和arr[5]
,即交换arr[1]
和arr[5]
,结果为:{1, 5, 8, 9, 10, 7}
。
- 交换
-
返回基准元素的位置:
- 返回
i + 1 = 1
。
- 返回
经过分区操作后,基准元素 5
位于正确的排序位置,其左边是所有小于 5
的元素,右边是所有大于 5
的元素。
分区函数代码
#include <iostream>
#include <vector>
#include <algorithm>
int partition(std::vector<int>& arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; ++j) {
if (arr[j] < pivot) {
++i;
std::swap(arr[i], arr[j]);
}
}
std::swap(arr[i + 1], arr[high]);
return i + 1;
}
int main() {
std::vector<int> arr = {10, 7, 8, 9, 1, 5};
int pi = partition(arr, 0, arr.size() - 1);
std::cout << "Partition index: " << pi << std::endl;
std::cout << "Array after partition: ";
for (const auto& num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
运行上述代码,你将看到分区后的数组以及基准元素的索引位置。
其他排序方法
冒泡排序(Bubble Sort)
思想
冒泡排序是一种简单的排序算法。其基本思想是:
- 重复遍历数组,每次比较相邻的两个元素,如果它们的顺序错误则交换它们。
- 经过一轮遍历后,最大的元素会被移到数组的末尾。
- 重复上述过程,对剩余的元素进行排序。
时间复杂度
冒泡排序的时间复杂度为 O(n^2)。
插入排序(Insertion Sort)
思想
插入排序是一种简单的排序算法。其基本思想是:
- 从第二个元素开始,依次将每个元素插入到已排序部分的适当位置。
- 重复上述过程,直到所有元素都被排序。
时间复杂度
插入排序的时间复杂度为 O(n^2)。
归并排序(Merge Sort)
思想
归并排序是一种基于分治法的排序算法。其基本思想是:
- 将数组分成两个子数组,对每个子数组进行归并排序。
- 合并两个已排序的子数组,得到排序后的数组。
堆排序(Heap Sort)
思想
堆排序是一种基于堆数据结构的排序算法。其基本思想是:
- 将数组构建成一个最大堆。
- 重复从堆中取出最大元素,并将其与堆的最后一个元素交换,缩小堆的大小,然后重新调整堆。
时间复杂度
堆排序的时间复杂度为 O(n log n)。
# 堆排序算法
## 概述
堆是一种特殊的树形数据结构,它满足以下性质:
1. 任何非叶子节点的值都大于或等于其左右子节点的值(大根堆),或者任何非叶子节点的值都小于或等于其左右子节点的值(小根堆)。
2. 堆通常被实现为一个数组,而堆的根节点通常被放在数组的第一个位置。
堆的基本操作包括:
- 插入操作:将一个新元素插入到堆中,并保持堆的性质。
- 删除操作:删除堆顶元素,并保持堆的性质。
- 建堆操作:将一个无序的数组转化为一个堆。
## 堆排序
堆排序是一种基于堆数据结构的排序算法。堆排序的基本思想是:
1. 将待排序的数组构建成一个大根堆。
2. 将堆顶元素(最大值)与堆的最后一个元素交换,然后将剩余的元素重新调整为大根堆。
3. 重复上述过程,直到堆中只剩下一个元素。
堆排序的时间复杂度为 O(n log n),空间复杂度为 O(1),是一种不稳定的排序算法。
## 堆排序与其他排序算法的优缺点
与快速排序相比:
- 优点:堆排序的最坏时间复杂度为 O(n log n),而快速排序的最坏时间复杂度为 O(n^2)。
- 缺点:堆排序的平均性能通常比快速排序差。
与归并排序相比:
- 优点:堆排序的空间复杂度为 O(1),而归并排序的空间复杂度为 O(n)。
- 缺点:堆排序是一种不稳定的排序算法,而归并排序是一种稳定的排序算法。
## C++ 实现
以下是一个使用堆排序来查找数组中第 k 个最大元素的 C++ 实现:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 调整大根堆
void heapify(vector<int>& nums, int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && nums[left] > nums[largest])
largest = left;
if (right < n && nums[right] > nums[largest])
largest = right;
if (largest != i) {
swap(nums[i], nums[largest]);
heapify(nums, n, largest);
}
}
// 构建大根堆
void buildHeap(vector<int>& nums, int n) {
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(nums, n, i);
}
}
// 查找第 k 个最大元素
int findKthLargest(vector<int>& nums, int k) {
int n = nums.size();
buildHeap(nums, n);
for (int i = n - 1; i >= n - k + 1; i--) {
swap(nums[0], nums[i]);
heapify(nums, i, 0);
}
return nums[0];
}
int main() {
vector<int> nums = {3, 2, 1, 5, 6, 4};
int k = 2;
cout << "The " << k << "th largest element is " << findKthLargest(nums, k) << endl;
return 0;
}
## 代码解释
1. `heapify` 函数:调整以索引 `i` 为根的子树,使其满足大根堆的性质。
2. `buildHeap` 函数:构建大根堆。
3. `findKthLargest` 函数:使用堆排序查找第 k 个最大元素。
4. `main` 函数:测试代码。
在 `findKthLargest` 函数中,我们首先构建大根堆,然后通过交换堆顶元素和堆的最后一个元素,并调整堆,最终获取第 k 个最大元素。
这个实现的时间复杂度为 O(n log n),满足题目要求。