下面是一个简要的 C++ 基础搜索与排序的讲义,包括常见的搜索和排序算法实现及其讲解。
C++ 基础搜索与排序
1. 搜索算法
搜索算法用于在数据结构中查找特定元素。最常见的搜索算法有 线性搜索 和 二分搜索。
1.1 线性搜索 (Linear Search)
线性搜索是一种简单的搜索算法,它从数据结构的第一个元素开始,逐个检查元素,直到找到目标元素为止。如果元素存在,返回其位置,否则返回 -1。
如 STL模板中的 find 函数
std::find
的定义
std::find
是 C++ 标准库中 <algorithm>
头文件中的一个函数,用于在容器中查找指定值。其定义如下:
template< class InputIterator, class T >
InputIterator find( InputIterator first, InputIterator last, const T& value );
- first 和 last 是容器的迭代器范围。
- value 是要查找的目标值。
- 函数返回指向找到的第一个目标值的迭代器。如果没有找到,则返回 last。
算法实现步骤:
- 从数组的第一个元素开始,逐个比较。
- 如果当前元素与目标元素相等,则返回当前索引。
- 如果遍历完数组仍未找到目标元素,则返回 -1。
代码实现:
#include <iostream>
using namespace std;
int linearSearch(int arr[], int size, int target) {
for (int i = 0; i < size; ++i) {
if (arr[i] == target) {
return i; // 找到目标,返回索引
}
}
return -1; // 如果未找到目标元素,返回 -1
}
int main() {
int arr[] = {5, 3, 8, 6, 7, 2};
int size = sizeof(arr) / sizeof(arr[0]);
int target = 6;
int result = linearSearch(arr, size, target);
if (result != -1) {
cout << "元素 " << target << " 在索引 " << result << " 处找到" << endl;
} else {
cout << "元素 " << target << " 未找到" << endl;
}
return 0;
}
时间复杂度:
- 最坏情况:O(n),遍历整个数组。
- 平均情况:O(n),需要在大部分情况下遍历一半数组。
1.2 二分搜索 (Binary Search)
二分搜索是一种高效的搜索算法,只能在已排序的数组中使用。它通过不断将搜索范围对半分,迅速缩小搜索区间,从而提高查找效率。
算法步骤:
- 先将数组排序(如果未排序)。
- 确定中间元素,若该元素等于目标元素,则返回其索引。
- 如果目标小于中间元素,则在左半部分继续搜索;否则在右半部分继续搜索。
- 直到找到目标元素,或者搜索区间为空。
代码实现:
#include <iostream>
using namespace std;
int binarySearch(int arr[], int size, int target) {
int left = 0, right = size - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防止溢出
if (arr[mid] == target) {
return mid; // 找到目标,返回索引
}
if (arr[mid] < target) {
left = mid + 1; // 在右半部分继续搜索
} else {
right = mid - 1; // 在左半部分继续搜索
}
}
return -1; // 如果未找到目标元素,返回 -1
}
int main() {
int arr[] = {2, 3, 5, 6, 7, 8};
int size = sizeof(arr) / sizeof(arr[0]);
int target = 6;
int result = binarySearch(arr, size, target);
if (result != -1) {
cout << "元素 " << target << " 在索引 " << result << " 处找到" << endl;
} else {
cout << "元素 " << target << " 未找到" << endl;
}
return 0;
}
时间复杂度:
- 最坏情况:O(log n)
- 平均情况:O(log n)
注意:二分搜索需要数组是已排序的。
2. 排序算法
排序算法用于将数据按一定的顺序排列。常见的排序算法有 冒泡排序、选择排序、插入排序 和 快速排序。
2.1 冒泡排序 (Bubble Sort)
冒泡排序是一种简单的排序算法。它通过重复地遍历待排序的数列,比较相邻的元素,如果它们的顺序错误就交换它们,直到整个数列有序。
算法步骤:
- 从头到尾遍历数组,比较相邻的元素。
- 如果元素顺序错误,就交换它们的位置。
- 每一轮遍历后,最大元素将“冒泡”到数组的末尾。
- 重复上述过程,直到没有需要交换的元素。
代码实现:
#include <iostream>
using namespace std;
void bubbleSort(int arr[], int size) {
for (int i = 0; i < size - 1; ++i) {
for (int j = 0; j < size - 1 - i; ++j) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]); // 交换相邻元素
}
}
}
}
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int size = sizeof(arr) / sizeof(arr[0]);
bubbleSort(arr, size);
cout << "排序后的数组:";
for (int i = 0; i < size; ++i) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
时间复杂度:
- 最坏情况:O(n²)
- 平均情况:O(n²)
- 最好情况:O(n),当数组已经排好序时。
2.2 选择排序 (Selection Sort)
选择排序是一种简单的排序算法。它每次从未排序部分选择最小(或最大)元素,放到已排序部分的末尾。
算法步骤:
- 在未排序的部分中找到最小元素。
- 将其与未排序部分的第一个元素交换位置。
- 重复该过程,直到整个数组有序。
代码实现:
#include <iostream>
using namespace std;
void selectionSort(int arr[], int size) {
for (int i = 0; i < size - 1; ++i) {
int minIndex = i;
for (int j = i + 1; j < size; ++j) {
if (arr[j] < arr[minIndex]) {
minIndex = j; // 找到最小元素的索引
}
}
swap(arr[i], arr[minIndex]); // 交换最小元素与当前元素
}
}
int main() {
int arr[] = {64, 25, 12, 22, 11};
int size = sizeof(arr) / sizeof(arr[0]);
selectionSort(arr, size);
cout << "排序后的数组:";
for (int i = 0; i < size; ++i) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
时间复杂度:
- 最坏情况:O(n²)
- 平均情况:O(n²)
- 最好情况:O(n²)
2.3 插入排序 (Insertion Sort)
插入排序通过逐步将未排序部分的元素插入已排序部分,使得每一步得到的都是一个已排序的子数组。
算法步骤:
- 从第二个元素开始,逐个插入到前面已经排好序的部分。
- 每次插入元素时,将其与已排序部分的元素进行比较,找到正确位置。
代码实现:
#include <iostream>
using namespace std;
void insertionSort(int arr[], int size) {
for (int i = 1; i < size; ++i) {
int key = arr[i];
int j = i - 1;
// 将大于 key 的元素往右移一位
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key; // 插入 key 到正确位置
}
}
int main() {
int arr[] = {12, 11, 13, 5, 6};
int size = sizeof(arr) / sizeof(arr[0]);
insertionSort(arr, size);
cout << "排序后的数组:";
for (int i = 0; i < size; ++i) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
时间复杂度:
- 最坏情况:O(n²),当数组是逆序排列时,每次插入都会导致最大移动。
- 平均情况:O(n²),大部分情况下也需要进行大量的比较和移动。
- 最好情况:O(n),当数组已经是有序的情况下,插入排序的效率非常高,因为无需进行元素交换。
2.4 快速排序 (Quick Sort)
快速排序是一种分治排序算法,它通过一个基准元素(pivot)将数组分成两部分,将小于基准的元素放在基准的左边,大于基准的元素放在右边,然后分别递归地对这两部分进行排序。
算法步骤:
- 选择一个基准元素(通常是数组的第一个、最后一个或随机一个元素)。
- 将数组分成两个部分,左边部分的元素小于基准,右边部分的元素大于基准。
- 对左、右部分分别递归进行快速排序。
- 递归直到数组大小为1或空。
代码实现:
#include <iostream>
using namespace std;
int partition(int arr[], int low, int high) {
int pivot = arr[high]; // 选择数组的最后一个元素作为基准
int i = low - 1; // i 是小于 pivot 的元素的最后一个位置
for (int j = low; j < high; ++j) {
if (arr[j] <= pivot) {
i++;
swap(arr[i], arr[j]); // 将小于 pivot 的元素交换到左边
}
}
swap(arr[i + 1], arr[high]); // 将 pivot 放到正确的位置
return i + 1; // 返回基准元素的位置
}
void quickSort(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); // 排序右半部分
}
}
int main() {
int arr[] = {10, 7, 8, 9, 1, 5};
int size = sizeof(arr) / sizeof(arr[0]);
quickSort(arr, 0, size - 1);
cout << "排序后的数组:";
for (int i = 0; i < size; ++i) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
时间复杂度:
- 最坏情况:O(n²),当每次选取的基准元素是最大或最小元素时(例如在已经有序的数组中,基准总是极端值)。
- 平均情况:O(n log n),快速排序大多数情况下能将数组平均分为两部分,形成对数级的递归深度。
- 最好情况:O(n log n),在最佳情况下,基准能有效地将数组分成两部分。
快速排序的优点:
- 平均情况下,快速排序的时间复杂度为O(n log n),比冒泡排序、选择排序等O(n²)的算法要高效。
- 排序过程中使用了分治思想,数据交换频繁,性能优于其他简单的排序算法。
注意:
- 快速排序在某些极端情况下,性能可能退化为O(n²),尤其当数组本身是已经有序的,或者每次选取的基准元素不合适时。
- 对于较小的数组,快速排序的递归和交换可能导致性能下降,此时使用插入排序可能更好。
总结与建议
常见排序算法对比:
排序算法 | 最坏时间复杂度 | 平均时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n²) | O(n²) | O(n) | O(1) | 稳定 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
插入排序 | O(n²) | O(n²) | O(n) | O(1) | 稳定 |
快速排序 | O(n²) | O(n log n) | O(n log n) | O(log n) | 不稳定 |
推荐:
- 对于小型数组,插入排序较为高效。
- 对于大部分无序数组,快速排序通常是首选算法,它的时间复杂度较低,尤其适用于较大的数据集。
- 如果数据较为接近有序,或者是部分有序,可以考虑使用插入排序来优化性能。
- 冒泡排序和选择排序的效率较低,通常仅在学习算法时使用,实际应用中不推荐使用这两种排序算法。